fix(block): open nested editor tabs

Closes #538
This commit is contained in:
Hakan Seven committed 2026-07-28 19:13:27 +03:00
1 parent 00cc17ea55
commit d77eb8f419
8 files changed
+296 -79

No files matched your search

+128 -38
View File
@@ -335,11 +335,7 @@ impl OpenCADStudio {
"BEDIT" => {
use crate::modules::draw::modify::block_edit::BlockEditPickCommand;
if self.tabs[i].block_edit.is_some() {
self.command_line.push_error(
"BEDIT: a block editor is already open. Save or discard it first.",
);
} else if self.tabs[i].refedit_session.is_some() {
if self.tabs[i].refedit_session.is_some() {
self.command_line
.push_error("BEDIT: finish the active REFEDIT (REFCLOSE) first.");
} else {
@@ -399,6 +395,16 @@ impl OpenCADStudio {
.push_error("BEDIT: cannot edit an external reference (xref).");
return Some(Task::none());
}
if self.tabs[i]
.block_edits
.iter()
.any(|session| session.br_handle == br_handle)
{
self.tabs[i].active_cmd = None;
return Some(Task::done(Message::BlockEditSwitch(
insert.block_name.clone(),
)));
}
// Snapshot the block's block-local entities so Discard can restore
// them (skip structural Block/BlockEnd/AttDef, mirroring REFEDIT).
@@ -425,23 +431,42 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "BEDIT");
let return_layout = self.tabs[i].scene.current_layout.clone();
// Capture the camera before the editor reframes it, so leaving
// the block editor returns the view exactly where it was (#425).
let return_camera = self.tabs[i].scene.camera.borrow().clone();
let current_camera = self.tabs[i].scene.camera.borrow().clone();
let (return_layout, return_block, return_camera) =
if let Some(parent_index) = self.tabs[i].active_block_edit {
let parent = &mut self.tabs[i].block_edits[parent_index];
parent.editor_camera = current_camera.clone();
(
parent.return_layout.clone(),
Some(parent.block_name.clone()),
parent.return_camera.clone(),
)
} else {
self.tabs[i].scene.sync_camera_to_document();
(
self.tabs[i].scene.current_layout.clone(),
None,
current_camera.clone(),
)
};
// A block editor renders in model style; switch to Model first so
// the paper-space code paths stay off, then scope to the block.
if return_layout != "Model" {
if self.tabs[i].scene.current_layout != "Model" {
self.tabs[i].scene.set_current_layout("Model".to_string());
}
self.tabs[i].scene.block_edit_block = Some(br_handle);
self.tabs[i].block_edit = Some(BlockEditSession {
self.tabs[i].block_edits.push(BlockEditSession {
block_name: insert.block_name.clone(),
br_handle,
return_layout,
return_block,
snapshot,
return_camera,
editor_camera: current_camera,
});
self.tabs[i].active_block_edit = Some(self.tabs[i].block_edits.len() - 1);
self.tabs[i].scene.deselect_all();
// The first click of a double-click selects the model-space
@@ -461,6 +486,11 @@ impl OpenCADStudio {
// it already scopes to the edited block. Without this the view
// stays wherever model/paper space was. (#261)
self.tabs[i].scene.fit_all();
let editor_camera = self.tabs[i].scene.camera.borrow().clone();
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
session.editor_camera = editor_camera;
}
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
self.refresh_properties();
self.tabs[i].active_cmd = None;
self.tabs[i].dirty = true;
@@ -471,27 +501,28 @@ impl OpenCADStudio {
}
"BEDIT_SAVE" => {
let session = match self.tabs[i].block_edit.take() {
Some(s) => s,
let session = match self.tabs[i].active_block_edit.take() {
Some(index) if index < self.tabs[i].block_edits.len() => {
self.tabs[i].block_edits.remove(index)
}
None => {
self.command_line
.push_error("BEDIT_SAVE: no block editor is open.");
return Some(Task::none());
}
Some(_) => {
self.command_line
.push_error("BEDIT_SAVE: invalid block editor state.");
return Some(Task::none());
}
};
// Edits are live on the block record — just leave the block space.
self.tabs[i].scene.block_edit_block = None;
self.tabs[i].scene.deselect_all();
self.tabs[i].active_grip = None;
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.tabs[i].scene.set_current_layout(session.return_layout.clone());
// Return the view to where it was when BEDIT began (#425).
*self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].scene.rebuild_derived_caches();
self.refresh_properties();
self.restore_after_block_edit_close(
i,
session.return_block.as_deref(),
&session.return_layout,
&session.return_camera,
);
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"BEDIT: Block \"{}\" saved. All references updated.",
@@ -500,13 +531,20 @@ impl OpenCADStudio {
}
"BEDIT_DISCARD" => {
let session = match self.tabs[i].block_edit.take() {
Some(s) => s,
let session = match self.tabs[i].active_block_edit.take() {
Some(index) if index < self.tabs[i].block_edits.len() => {
self.tabs[i].block_edits.remove(index)
}
None => {
self.command_line
.push_error("BEDIT_DISCARD: no block editor is open.");
return Some(Task::none());
}
Some(_) => {
self.command_line
.push_error("BEDIT_DISCARD: invalid block editor state.");
return Some(Task::none());
}
};
// Restore the block definition to its on-entry snapshot: remove the
// block's current entities, then re-add the snapshot ones (mirrors
@@ -531,27 +569,25 @@ impl OpenCADStudio {
{
br.entity_handles.clear();
}
let block_name = session.block_name.clone();
let return_block = session.return_block.clone();
let return_layout = session.return_layout.clone();
let return_camera = session.return_camera.clone();
for mut entity in session.snapshot {
entity.common_mut().handle = acadrust::Handle::NULL;
entity.common_mut().owner_handle = session.br_handle;
let _ = self.tabs[i].scene.document.add_entity(entity);
}
self.tabs[i].scene.block_edit_block = None;
self.tabs[i].scene.deselect_all();
self.tabs[i].active_grip = None;
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.tabs[i].scene.set_current_layout(session.return_layout.clone());
// Return the view to where it was when BEDIT began (#425).
*self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].scene.rebuild_derived_caches();
self.refresh_properties();
self.restore_after_block_edit_close(
i,
return_block.as_deref(),
&return_layout,
&return_camera,
);
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"BEDIT: Block \"{}\" edit discarded.",
session.block_name
block_name
));
}
@@ -1150,6 +1186,60 @@ impl OpenCADStudio {
Some(self.finish_dispatch(cmd))
}
fn restore_after_block_edit_close(
&mut self,
i: usize,
return_block: Option<&str>,
return_layout: &str,
return_camera: &crate::scene::view::camera::Camera,
) {
self.tabs[i].scene.block_edit_block = None;
self.tabs[i].scene.deselect_all();
self.tabs[i].active_grip = None;
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
let parent_index = return_block.and_then(|parent_name| {
self.tabs[i]
.block_edits
.iter()
.position(|candidate| candidate.block_name == parent_name)
});
if let Some(parent_index) = parent_index {
let (br_handle, editor_camera) = {
let parent = &self.tabs[i].block_edits[parent_index];
(parent.br_handle, parent.editor_camera.clone())
};
self.tabs[i].scene.set_current_layout("Model".to_string());
self.tabs[i].scene.block_edit_block = Some(br_handle);
self.tabs[i].active_block_edit = Some(parent_index);
*self.tabs[i].scene.camera.borrow_mut() = editor_camera;
} else {
let return_layout = if self.tabs[i]
.scene
.layout_names()
.iter()
.any(|name| name == return_layout)
{
return_layout.to_string()
} else {
"Model".to_string()
};
self.tabs[i].active_block_edit = None;
self.tabs[i].scene.set_current_layout(return_layout);
*self.tabs[i].scene.camera.borrow_mut() = return_camera.clone();
}
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
self.tabs[i].scene.rebuild_derived_caches();
self.tabs[i].refresh_active_ucs();
self.refresh_properties();
self.adopt_view_display(i);
self.sync_dyn_fields();
}
/// ADDSELECTED — start the draw command that creates the same kind of
/// object as the currently-selected one, adopting its general properties
/// (layer, colour, linetype, lineweight, linetype scale) as the current
+17 -3
View File
@@ -231,8 +231,11 @@ pub(super) struct DocumentTab {
pub(super) paper_bg_color: Option<[f32; 4]>,
/// Active REFEDIT session, if any.
pub(super) refedit_session: Option<RefEditSession>,
/// Active BEDIT block-editor space session, if any (issue #261).
pub(super) block_edit: Option<BlockEditSession>,
/// Open BEDIT block tabs. Definitions are edited live; each tab owns its
/// entry snapshot and camera so nested blocks can remain open independently.
pub(super) block_edits: Vec<BlockEditSession>,
/// Index of the block tab currently shown, or `None` for Model/Paper space.
pub(super) active_block_edit: Option<usize>,
/// Currently active MLeader style name.
pub(super) active_mleader_style: String,
/// Last camera_generation value written back to the document.
@@ -258,6 +261,16 @@ pub(super) struct DocumentTab {
}
impl DocumentTab {
pub(super) fn active_block_edit_session(&self) -> Option<&BlockEditSession> {
self.active_block_edit
.and_then(|index| self.block_edits.get(index))
}
pub(super) fn active_block_edit_session_mut(&mut self) -> Option<&mut BlockEditSession> {
self.active_block_edit
.and_then(|index| self.block_edits.get_mut(index))
}
/// The active WCS↔UCS converter for this tab — identity when no UCS is set.
/// Every consumer that needs UCS-relative coordinates goes through this.
pub(super) fn ucs_xform(&self) -> super::helpers::UcsXform {
@@ -478,7 +491,8 @@ impl DocumentTab {
bg_color: None,
paper_bg_color: None,
refedit_session: None,
block_edit: None,
block_edits: Vec::new(),
active_block_edit: None,
active_mleader_style: "Standard".to_string(),
last_synced_camera_gen: 0,
#[cfg(not(target_arch = "wasm32"))]
+2
View File
@@ -1957,6 +1957,8 @@ pub enum Message {
PspaceCommand,
/// Switch to a named layout ("Model" or paper space layout name).
LayoutSwitch(String),
/// Switch to an already-open BEDIT block tab.
BlockEditSwitch(String),
/// Move a paper layout before/after another paper layout.
LayoutReorder {
from: String,
+5
View File
@@ -3358,6 +3358,11 @@ impl OpenCADStudio {
self.on_layout_switch(name)
}
Message::BlockEditSwitch(name) => {
self.layout_list_open = false;
self.on_block_edit_switch(name)
}
Message::LayoutReorder { from, to, after } => {
let i = self.active_tab;
if self.tabs[i].is_start {
+98 -23
View File
@@ -370,7 +370,12 @@ impl OpenCADStudio {
let gen = self.tabs[i].scene.camera_generation;
if gen != self.tabs[i].last_synced_camera_gen {
self.tabs[i].last_synced_camera_gen = gen;
if self.tabs[i].scene.sync_camera_to_document() {
if self.tabs[i].active_block_edit.is_some() {
let camera = self.tabs[i].scene.camera.borrow().clone();
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
session.editor_camera = camera;
}
} else if self.tabs[i].scene.sync_camera_to_document() {
self.tabs[i].dirty = true;
}
}
@@ -3134,17 +3139,14 @@ impl OpenCADStudio {
self.tabs[i].scene.document.get_entity(handle),
Some(AcadEntityType::Insert(ins)) if !ins.attributes.is_empty()
);
if insert_has_attrs {
if insert_has_attrs && self.tabs[i].active_block_edit.is_none() {
return Task::done(Message::AttrEditorOpen(handle));
}
let is_insert = matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(AcadEntityType::Insert(_))
);
if is_insert
&& self.tabs[i].refedit_session.is_none()
&& self.tabs[i].block_edit.is_none()
{
if is_insert && self.tabs[i].refedit_session.is_none() {
return Task::done(Message::Command(format!(
"BEDIT_BEGIN:{}",
handle.value()
@@ -3679,6 +3681,59 @@ impl OpenCADStudio {
self.on_layout_switch_inner(name, false)
}
pub(crate) fn on_block_edit_switch(&mut self, name: String) -> Task<Message> {
let i = self.active_tab;
if self.tabs[i].is_start {
return Task::none();
}
let Some(target_index) = self.tabs[i]
.block_edits
.iter()
.position(|session| session.block_name == name)
else {
self.command_line
.push_error(&format!("BEDIT: block tab \"{name}\" is not open."));
return Task::none();
};
if self.tabs[i].active_block_edit == Some(target_index) {
return Task::none();
}
let cancel_task = self.cancel_active_command_for_space_change();
let current_camera = self.tabs[i].scene.camera.borrow().clone();
if let Some(active_index) = self.tabs[i].active_block_edit {
if let Some(session) = self.tabs[i].block_edits.get_mut(active_index) {
session.editor_camera = current_camera;
}
} else {
self.tabs[i].scene.sync_camera_to_document();
}
let (br_handle, editor_camera) = {
let session = &self.tabs[i].block_edits[target_index];
(session.br_handle, session.editor_camera.clone())
};
self.layout_rename_state = None;
self.tabs[i].scene.active_viewport = None;
self.tabs[i].scene.set_current_layout("Model".to_string());
self.tabs[i].scene.block_edit_block = Some(br_handle);
self.tabs[i].active_block_edit = Some(target_index);
*self.tabs[i].scene.camera.borrow_mut() = editor_camera;
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
self.tabs[i].scene.deselect_all();
self.tabs[i].active_grip = None;
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.tabs[i].refresh_active_ucs();
self.tabs[i].scene.bump_geometry_no_blocks();
self.refresh_properties();
self.adopt_view_display(i);
self.sync_dyn_fields();
cancel_task
}
/// MVIEW's "Insert view > New" flow deliberately visits Model space to
/// define a view and then returns to its paper layout. This is the only
/// layout transition allowed to preserve an active command.
@@ -3700,19 +3755,16 @@ impl OpenCADStudio {
.push_info("Open or create a drawing to switch layouts.");
return Task::none();
}
// A BEDIT block editor locks the active space; finish it with
// Save Block or Discard before switching spaces. (#261)
if self.tabs[i].block_edit.is_some() {
self.command_line.push_info(
"Finish the block editor (Save Block or Discard) before switching spaces.",
);
return Task::none();
}
let perf = crate::perf::enabled();
let perf_total = Instant::now();
let perf_from = self.tabs[i].scene.current_layout.clone();
let context_changed =
self.tabs[i].scene.current_layout != name || self.tabs[i].scene.active_viewport.is_some();
let leaving_block_edit = self.tabs[i].active_block_edit.is_some();
let perf_from = self.tabs[i]
.active_block_edit_session()
.map(|session| session.block_name.clone())
.unwrap_or_else(|| self.tabs[i].scene.current_layout.clone());
let context_changed = leaving_block_edit
|| self.tabs[i].scene.current_layout != name
|| self.tabs[i].scene.active_viewport.is_some();
let preserve_active_command = preserve_active_command
&& self.tabs[i]
.active_cmd
@@ -3730,6 +3782,24 @@ impl OpenCADStudio {
} else {
Task::none()
};
if let Some(active_index) = self.tabs[i].active_block_edit.take() {
let camera = self.tabs[i].scene.camera.borrow().clone();
let (return_layout, return_camera) = {
let session = &mut self.tabs[i].block_edits[active_index];
session.editor_camera = camera;
(session.return_layout.clone(), session.return_camera.clone())
};
let return_layout = if self.tabs[i].scene.layout_names().contains(&return_layout) {
return_layout
} else {
"Model".to_string()
};
self.tabs[i].scene.block_edit_block = None;
self.tabs[i].scene.set_current_layout(return_layout);
*self.tabs[i].scene.camera.borrow_mut() = return_camera;
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].scene.bump_geometry_no_blocks();
}
let going_to_paper = name != "Model";
// Persist the camera of the layout we're leaving BEFORE switching
// so returning to it restores where the user left off (the
@@ -3849,10 +3919,8 @@ impl OpenCADStudio {
// BEDIT block tab: renaming it renames the BLOCK itself —
// its record, marker and every INSERT reference. (#261)
let is_block_tab = self.tabs[i]
.block_edit
.as_ref()
.map(|be| be.block_name == orig)
.unwrap_or(false);
.active_block_edit_session()
.is_some_and(|session| session.block_name == orig);
if is_block_tab {
if self.tabs[i]
.scene
@@ -3866,8 +3934,15 @@ impl OpenCADStudio {
} else {
self.push_undo_snapshot(i, "BLOCK RENAME");
if self.tabs[i].scene.rename_block(&orig, &new_name) {
if let Some(be) = self.tabs[i].block_edit.as_mut() {
be.block_name = new_name.clone();
if let Some(session) =
self.tabs[i].active_block_edit_session_mut()
{
session.block_name = new_name.clone();
}
for session in &mut self.tabs[i].block_edits {
if session.return_block.as_deref() == Some(orig.as_str()) {
session.return_block = Some(new_name.clone());
}
}
self.tabs[i].dirty = true;
self.command_line
+13 -10
View File
@@ -1084,7 +1084,7 @@ impl OpenCADStudio {
}
// BEDIT block editor: right-edge Save Block / Discard toolbar (#261).
if tab.block_edit.is_some() && !tab.is_start {
if tab.active_block_edit.is_some() && !tab.is_start {
if let Some(tb) = crate::ui::side_toolbar::view(
&crate::modules::draw::modify::block_edit::block_edit_tools(),
) {
@@ -1375,10 +1375,14 @@ impl OpenCADStudio {
let coords_mode = tab.scene.document.header.coords_mode;
let picking = tab.active_cmd.is_some();
let layout_names = tab.scene.layout_names();
let mut displayed_layouts = layout_names.clone();
if let Some(be) = &tab.block_edit {
displayed_layouts.push(be.block_name.clone());
}
let block_tabs = tab
.block_edits
.iter()
.map(|session| session.block_name.clone())
.collect();
let active_block = tab
.active_block_edit_session()
.map(|session| session.block_name.clone());
let status_menu_data = crate::ui::statusbar::StatusMenuData {
layout_names: layout_names.clone(),
polar_custom_input: &self.polar_custom_input,
@@ -1401,12 +1405,11 @@ impl OpenCADStudio {
self.polar_increment_deg,
self.dyn_input,
self.snapper.otrack_enabled,
displayed_layouts,
layout_names.clone(),
block_tabs,
layout_names.into_iter().skip(1).collect(),
tab.block_edit
.as_ref()
.map(|be| be.block_name.clone())
.unwrap_or_else(|| tab.scene.current_layout.clone()),
tab.scene.current_layout.clone(),
active_block,
tab.is_start,
self.layout_rename_state.as_ref(),
tab.scene.first_viewport_scale(),