forked from 1b-agent/opencadstudio-upstream
feat(block): BEDIT block editor as a dedicated space tab (#261)
Selecting a block and running BEDIT (command, ribbon button, or a double-click on the block) opens a block-editor "space" tab, named after the block, alongside Model/Layout in the status bar. The tab draws ONLY the block record's own entities in block-local coordinates; edits are live on the definition, so every INSERT updates. Save Block / Discard buttons (side toolbar) finish the session; Discard restores the on-entry snapshot. Space switching is locked while a block is open, and BEDIT and REFEDIT are mutually exclusive. Implementation: a Scene.block_edit_block override at the top of current_layout_block_handle scopes render/pick/new-entity ownership to the edited block while current_layout stays "Model" (so the pervasive Model-vs-paper code keeps model behaviour). The model render/pick paths that hardcoded model_space_block_handle (wire tile set, hatch render + pick, block-instance meshes) are made binding-aware so the block isolation actually takes effect and model content does not leak in. Entering fits the camera to the block (fit_all already scopes via the binding). Renaming the block tab renames the block itself — new Scene::rename_block re-keys the record, updates the Block marker and repoints every INSERT reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4fc34c6a2
commit
3548de332c
8 files changed
+435
-18
No files matched your search
@@ -289,6 +289,196 @@ 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() {
|
||||||
|
self.command_line
|
||||||
|
.push_error("BEDIT: finish the active REFEDIT (REFCLOSE) first.");
|
||||||
|
} else {
|
||||||
|
// Jump straight to begin when a single INSERT is preselected.
|
||||||
|
let selected: Vec<_> =
|
||||||
|
self.tabs[i].scene.selected_entities().into_iter().collect();
|
||||||
|
if selected.len() == 1 {
|
||||||
|
if let Some(acadrust::EntityType::Insert(_)) =
|
||||||
|
selected.first().map(|(_, e)| e)
|
||||||
|
{
|
||||||
|
let handle = selected[0].0;
|
||||||
|
let _ =
|
||||||
|
self.dispatch_command(&format!("BEDIT_BEGIN:{}", handle.value()));
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cmd_obj = BlockEditPickCommand::new();
|
||||||
|
self.command_line.push_info(&cmd_obj.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd if cmd.starts_with("BEDIT_BEGIN:") => {
|
||||||
|
use crate::modules::draw::modify::block_edit::BlockEditSession;
|
||||||
|
use acadrust::Handle;
|
||||||
|
|
||||||
|
let handle_u64: u64 = cmd["BEDIT_BEGIN:".len()..].parse().unwrap_or(0);
|
||||||
|
let insert_handle = Handle::new(handle_u64);
|
||||||
|
|
||||||
|
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
||||||
|
Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
|
||||||
|
_ => {
|
||||||
|
self.command_line
|
||||||
|
.push_error("BEDIT: selected object is not a block reference.");
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve the block record; reject external references (xrefs).
|
||||||
|
let (br_handle, is_xref) = match self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get(&insert.block_name)
|
||||||
|
{
|
||||||
|
Some(br) => (br.handle, br.flags.is_xref),
|
||||||
|
None => {
|
||||||
|
self.command_line.push_error(&format!(
|
||||||
|
"BEDIT: block \"{}\" not found.",
|
||||||
|
insert.block_name
|
||||||
|
));
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if is_xref {
|
||||||
|
self.command_line
|
||||||
|
.push_error("BEDIT: cannot edit an external reference (xref).");
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot the block's block-local entities so Discard can restore
|
||||||
|
// them (skip structural Block/BlockEnd/AttDef, mirroring REFEDIT).
|
||||||
|
let snapshot: Vec<_> = {
|
||||||
|
let br = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get(&insert.block_name)
|
||||||
|
.unwrap();
|
||||||
|
br.entity_handles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||||
|
.filter(|e| {
|
||||||
|
!matches!(
|
||||||
|
e,
|
||||||
|
acadrust::EntityType::Block(_)
|
||||||
|
| acadrust::EntityType::BlockEnd(_)
|
||||||
|
| acadrust::EntityType::AttributeDefinition(_)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.push_undo_snapshot(i, "BEDIT");
|
||||||
|
|
||||||
|
let return_layout = self.tabs[i].scene.current_layout.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" {
|
||||||
|
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 {
|
||||||
|
block_name: insert.block_name.clone(),
|
||||||
|
br_handle,
|
||||||
|
return_layout,
|
||||||
|
snapshot,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
// Frame the camera on the block's own geometry (block-local, near
|
||||||
|
// origin) — fit_all() goes through current_layout_block_handle so
|
||||||
|
// it already scopes to the edited block. Without this the view
|
||||||
|
// stays wherever model/paper space was. (#261)
|
||||||
|
self.tabs[i].scene.fit_all();
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_info(&format!(
|
||||||
|
"BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.",
|
||||||
|
insert.block_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
"BEDIT_SAVE" => {
|
||||||
|
let session = match self.tabs[i].block_edit.take() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
self.command_line
|
||||||
|
.push_error("BEDIT_SAVE: no block editor is open.");
|
||||||
|
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].scene.set_current_layout(session.return_layout.clone());
|
||||||
|
self.tabs[i].scene.rebuild_derived_caches();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"BEDIT: Block \"{}\" saved. All references updated.",
|
||||||
|
session.block_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
"BEDIT_DISCARD" => {
|
||||||
|
let session = match self.tabs[i].block_edit.take() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
self.command_line
|
||||||
|
.push_error("BEDIT_DISCARD: no block editor is open.");
|
||||||
|
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
|
||||||
|
// the REFCLOSE_SAVE write-back).
|
||||||
|
let old_handles: Vec<_> = match self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get(&session.block_name)
|
||||||
|
{
|
||||||
|
Some(br) => br.entity_handles.clone(),
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
for h in &old_handles {
|
||||||
|
self.tabs[i].scene.document.remove_entity(*h);
|
||||||
|
}
|
||||||
|
if let Some(br) = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get_mut(&session.block_name)
|
||||||
|
{
|
||||||
|
br.entity_handles.clear();
|
||||||
|
}
|
||||||
|
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].scene.set_current_layout(session.return_layout.clone());
|
||||||
|
self.tabs[i].scene.rebuild_derived_caches();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"BEDIT: Block \"{}\" edit discarded.",
|
||||||
|
session.block_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
cmd if cmd.starts_with("REFEDIT_BEGIN:") => {
|
cmd if cmd.starts_with("REFEDIT_BEGIN:") => {
|
||||||
use crate::modules::draw::modify::refedit::{
|
use crate::modules::draw::modify::refedit::{
|
||||||
apply_insert_transform, RefEditSession,
|
apply_insert_transform, RefEditSession,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::command::CadCommand;
|
use crate::command::CadCommand;
|
||||||
use crate::io::linetypes;
|
use crate::io::linetypes;
|
||||||
|
use crate::modules::draw::modify::block_edit::BlockEditSession;
|
||||||
use crate::modules::draw::modify::refedit::RefEditSession;
|
use crate::modules::draw::modify::refedit::RefEditSession;
|
||||||
use crate::scene::pick::grip::GripEdit;
|
use crate::scene::pick::grip::GripEdit;
|
||||||
use crate::scene::GripDef;
|
use crate::scene::GripDef;
|
||||||
@@ -205,6 +206,8 @@ pub(super) struct DocumentTab {
|
|||||||
pub(super) paper_bg_color: Option<[f32; 4]>,
|
pub(super) paper_bg_color: Option<[f32; 4]>,
|
||||||
/// Active REFEDIT session, if any.
|
/// Active REFEDIT session, if any.
|
||||||
pub(super) refedit_session: Option<RefEditSession>,
|
pub(super) refedit_session: Option<RefEditSession>,
|
||||||
|
/// Active BEDIT block-editor space session, if any (issue #261).
|
||||||
|
pub(super) block_edit: Option<BlockEditSession>,
|
||||||
/// Currently active MLeader style name.
|
/// Currently active MLeader style name.
|
||||||
pub(super) active_mleader_style: String,
|
pub(super) active_mleader_style: String,
|
||||||
/// Last camera_generation value written back to the document.
|
/// Last camera_generation value written back to the document.
|
||||||
@@ -437,6 +440,7 @@ impl DocumentTab {
|
|||||||
bg_color: None,
|
bg_color: None,
|
||||||
paper_bg_color: None,
|
paper_bg_color: None,
|
||||||
refedit_session: None,
|
refedit_session: None,
|
||||||
|
block_edit: None,
|
||||||
active_mleader_style: "Standard".to_string(),
|
active_mleader_style: "Standard".to_string(),
|
||||||
last_synced_camera_gen: 0,
|
last_synced_camera_gen: 0,
|
||||||
is_start: false,
|
is_start: false,
|
||||||
|
|||||||
@@ -2592,9 +2592,9 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
// Double-clicking a block with attributes opens the
|
// Double-clicking a block with attributes opens the
|
||||||
// attribute editor (edit its values); a block with
|
// attribute editor (edit its values); a block with
|
||||||
// no attributes enters in-place block edit (REFEDIT),
|
// no attributes opens the block editor (BEDIT) — a
|
||||||
// so its geometry can be edited and the change
|
// space tab scoped to the block's own geometry, so
|
||||||
// reflects in every instance. (#136, #192)
|
// edits reflect in every instance. (#136, #192, #261)
|
||||||
let insert_has_attrs = matches!(
|
let insert_has_attrs = matches!(
|
||||||
self.tabs[i].scene.document.get_entity(handle),
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
Some(AcadEntityType::Insert(ins)) if !ins.attributes.is_empty()
|
Some(AcadEntityType::Insert(ins)) if !ins.attributes.is_empty()
|
||||||
@@ -2606,9 +2606,12 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
self.tabs[i].scene.document.get_entity(handle),
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
Some(AcadEntityType::Insert(_))
|
Some(AcadEntityType::Insert(_))
|
||||||
);
|
);
|
||||||
if is_insert && self.tabs[i].refedit_session.is_none() {
|
if is_insert
|
||||||
|
&& self.tabs[i].refedit_session.is_none()
|
||||||
|
&& self.tabs[i].block_edit.is_none()
|
||||||
|
{
|
||||||
return Task::done(Message::Command(format!(
|
return Task::done(Message::Command(format!(
|
||||||
"REFEDIT_BEGIN:{}",
|
"BEDIT_BEGIN:{}",
|
||||||
handle.value()
|
handle.value()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -2989,6 +2992,14 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
|
|
||||||
pub(super) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
pub(super) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
// 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 going_to_paper = name != "Model";
|
let going_to_paper = name != "Model";
|
||||||
// Persist the camera of the layout we're leaving BEFORE switching
|
// Persist the camera of the layout we're leaving BEFORE switching
|
||||||
// so returning to it restores where the user left off (the
|
// so returning to it restores where the user left off (the
|
||||||
@@ -3073,6 +3084,33 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
let new_name = new_name.trim().to_string();
|
let new_name = new_name.trim().to_string();
|
||||||
if !new_name.is_empty() && new_name != orig {
|
if !new_name.is_empty() && new_name != orig {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
// 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);
|
||||||
|
if is_block_tab {
|
||||||
|
if self.tabs[i].scene.document.block_records.get(&new_name).is_some() {
|
||||||
|
self.command_line
|
||||||
|
.push_error(&format!("\"{}\" name already in use", new_name));
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line
|
||||||
|
.push_output(&format!("Block \"{orig}\" → \"{new_name}\""));
|
||||||
|
} else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(&format!("Could not rename block \"{orig}\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let exists = self.tabs[i]
|
let exists = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.layout_names()
|
.layout_names()
|
||||||
|
|||||||
+22
-2
@@ -1097,6 +1097,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BEDIT block editor: right-edge Save Block / Discard toolbar (#261).
|
||||||
|
if tab.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(),
|
||||||
|
) {
|
||||||
|
viewport_stack = viewport_stack.push(tb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Quick Properties: compact floating property panel on selection,
|
// Quick Properties: compact floating property panel on selection,
|
||||||
// anchored at the canvas top-left so it doesn't track the cursor.
|
// anchored at the canvas top-left so it doesn't track the cursor.
|
||||||
if self.quick_properties && !tab.is_start {
|
if self.quick_properties && !tab.is_start {
|
||||||
@@ -1328,8 +1337,19 @@ impl OpenCADStudio {
|
|||||||
self.polar_popup_open,
|
self.polar_popup_open,
|
||||||
self.dyn_input,
|
self.dyn_input,
|
||||||
self.snapper.otrack_enabled,
|
self.snapper.otrack_enabled,
|
||||||
tab.scene.layout_names(),
|
{
|
||||||
tab.scene.current_layout.clone(),
|
// In a BEDIT block editor, show the block as an extra
|
||||||
|
// active space tab alongside Model/layouts. (#261)
|
||||||
|
let mut names = tab.scene.layout_names();
|
||||||
|
if let Some(be) = &tab.block_edit {
|
||||||
|
names.push(be.block_name.clone());
|
||||||
|
}
|
||||||
|
names
|
||||||
|
},
|
||||||
|
tab.block_edit
|
||||||
|
.as_ref()
|
||||||
|
.map(|be| be.block_name.clone())
|
||||||
|
.unwrap_or_else(|| tab.scene.current_layout.clone()),
|
||||||
self.layout_rename_state.as_ref(),
|
self.layout_rename_state.as_ref(),
|
||||||
tab.scene.first_viewport_scale(),
|
tab.scene.first_viewport_scale(),
|
||||||
tab.scene.viewport_count(),
|
tab.scene.viewport_count(),
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// BEDIT — block editor as a dedicated "space" tab (issue #261).
|
||||||
|
//
|
||||||
|
// Workflow:
|
||||||
|
// 1. BEDIT: user picks an INSERT (or has one pre-selected).
|
||||||
|
// 2. A block-editor space opens: rendering, picking and new geometry are
|
||||||
|
// scoped to the block's record, so ONLY the block's own (block-local)
|
||||||
|
// entities are drawn — no INSERT transform, they show exactly as stored.
|
||||||
|
// 3. The user edits them with normal commands. Because they ARE the block
|
||||||
|
// definition's entities, edits are live.
|
||||||
|
// 4. Save Block (BEDIT_SAVE): keep the edits and leave the block space.
|
||||||
|
// Discard (BEDIT_DISCARD): restore the block to its on-entry snapshot.
|
||||||
|
//
|
||||||
|
// Unlike REFEDIT (which copies entities into model space with the INSERT
|
||||||
|
// transform and inverse-transforms them back on save), a BEDIT space edits the
|
||||||
|
// block record in place, so there is no copy-back and no transform — the space
|
||||||
|
// render filter (`current_layout_block_handle`) already draws only the block.
|
||||||
|
|
||||||
|
use acadrust::{EntityType, Handle};
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
|
|
||||||
|
/// Right-edge side-toolbar buttons shown while a BEDIT block-editor space is
|
||||||
|
/// active: save the block edits and leave, or discard them.
|
||||||
|
pub fn block_edit_tools() -> Vec<ToolDef> {
|
||||||
|
vec![
|
||||||
|
ToolDef {
|
||||||
|
id: "BEDIT_SAVE",
|
||||||
|
label: "Save Block",
|
||||||
|
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mt_ok.svg")),
|
||||||
|
event: ModuleEvent::Command("BEDIT_SAVE".to_string()),
|
||||||
|
},
|
||||||
|
ToolDef {
|
||||||
|
id: "BEDIT_DISCARD",
|
||||||
|
label: "Discard Block Edit",
|
||||||
|
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mt_cancel.svg")),
|
||||||
|
event: ModuleEvent::Command("BEDIT_DISCARD".to_string()),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Session state (held in DocumentTab) ───────────────────────────────────
|
||||||
|
|
||||||
|
/// Active BEDIT block-editor session. Lives in `DocumentTab::block_edit`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BlockEditSession {
|
||||||
|
/// Name of the block being edited (also the space-tab label).
|
||||||
|
pub block_name: String,
|
||||||
|
/// Handle of the block record whose entities are being edited.
|
||||||
|
pub br_handle: Handle,
|
||||||
|
/// Space to return to when the block editor closes (the layout that was
|
||||||
|
/// active when BEDIT began).
|
||||||
|
pub return_layout: String,
|
||||||
|
/// The block's block-local entities captured on entry, so Discard can
|
||||||
|
/// restore the definition (structural Block/BlockEnd/AttDef excluded).
|
||||||
|
pub snapshot: Vec<EntityType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BEDIT pick command ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Step 1: wait for the user to pick a single INSERT entity to edit.
|
||||||
|
pub struct BlockEditPickCommand;
|
||||||
|
|
||||||
|
impl BlockEditPickCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for BlockEditPickCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BEDIT"
|
||||||
|
}
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
"BEDIT Select block reference to edit:".into()
|
||||||
|
}
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||||
|
if handle.is_null() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
// Signal the host to open the block-editor space for this INSERT.
|
||||||
|
CmdResult::Relaunch(format!("BEDIT_BEGIN:{}", handle.value()), vec![handle])
|
||||||
|
}
|
||||||
|
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["BEDIT"] }); // BlockEditPickCommand
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["BEDIT_SAVE"] });
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["BEDIT_DISCARD"] });
|
||||||
@@ -11,6 +11,7 @@ pub mod lengthen;
|
|||||||
pub mod mirror;
|
pub mod mirror;
|
||||||
pub mod offset;
|
pub mod offset;
|
||||||
pub mod pedit;
|
pub mod pedit;
|
||||||
|
pub mod block_edit;
|
||||||
pub mod refedit;
|
pub mod refedit;
|
||||||
pub mod reverse;
|
pub mod reverse;
|
||||||
pub mod rotate;
|
pub mod rotate;
|
||||||
|
|||||||
+47
-6
@@ -109,9 +109,14 @@ impl Scene {
|
|||||||
self.ensure_layer(&layer);
|
self.ensure_layer(&layer);
|
||||||
|
|
||||||
// Route to the correct block based on current editing mode:
|
// Route to the correct block based on current editing mode:
|
||||||
|
// - BEDIT block editor: geometry belongs to the edited block record,
|
||||||
|
// so it becomes part of the block definition (issue #261).
|
||||||
// - PSPACE (paper layout, no active viewport): paper-space layout block.
|
// - PSPACE (paper layout, no active viewport): paper-space layout block.
|
||||||
// - MSPACE or model layout: model space (document default).
|
// - MSPACE or model layout: model space (document default).
|
||||||
let handle = if self.current_layout != "Model" && self.active_viewport.is_none() {
|
let handle = if let Some(br) = self.block_edit_block {
|
||||||
|
entity.common_mut().owner_handle = br;
|
||||||
|
self.document.add_entity(entity).unwrap_or(Handle::NULL)
|
||||||
|
} else if self.current_layout != "Model" && self.active_viewport.is_none() {
|
||||||
let layout_name = self.current_layout.clone();
|
let layout_name = self.current_layout.clone();
|
||||||
self.document
|
self.document
|
||||||
.add_entity_to_layout(entity, &layout_name)
|
.add_entity_to_layout(entity, &layout_name)
|
||||||
@@ -139,6 +144,41 @@ impl Scene {
|
|||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rename a block definition: re-key its record, update the Block marker's
|
||||||
|
/// name, and repoint every INSERT that referenced the old name so all
|
||||||
|
/// instances keep resolving. Returns false if `old` is missing, `new` is
|
||||||
|
/// already taken, or the names are equal (case-insensitive). (#261)
|
||||||
|
pub fn rename_block(&mut self, old: &str, new: &str) -> bool {
|
||||||
|
if old.eq_ignore_ascii_case(new) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.document.block_records.get(new).is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(mut br) = self.document.block_records.remove(old) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let block_marker = br.block_entity_handle;
|
||||||
|
br.name = new.to_string();
|
||||||
|
if self.document.block_records.add(br).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Keep the Block marker entity's name in sync (used on DXF/DWG save).
|
||||||
|
if let Some(EntityType::Block(b)) = self.document.get_entity_mut(block_marker) {
|
||||||
|
b.name = new.to_string();
|
||||||
|
}
|
||||||
|
// Repoint every INSERT reference so all instances keep resolving.
|
||||||
|
for e in self.document.entities_mut() {
|
||||||
|
if let EntityType::Insert(ins) = e {
|
||||||
|
if ins.block_name.eq_ignore_ascii_case(old) {
|
||||||
|
ins.block_name = new.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.bump_geometry();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Replace the entity stored under `entity`'s handle with `entity`, keeping
|
/// Replace the entity stored under `entity`'s handle with `entity`, keeping
|
||||||
/// its identity (handle + owning block), and refresh the derived
|
/// its identity (handle + owning block), and refresh the derived
|
||||||
/// hatch/image/mesh caches so the edit is visible. Returns `false` when no
|
/// hatch/image/mesh caches so the edit is visible. Returns `false` when no
|
||||||
@@ -478,11 +518,12 @@ impl Scene {
|
|||||||
// block) — they're tessellated separately via Insert
|
// block) — they're tessellated separately via Insert
|
||||||
// explosion and only the laid-out copies should appear.
|
// explosion and only the laid-out copies should appear.
|
||||||
self.belongs_to_visible_block(handle, c.owner_handle, layout_block)
|
self.belongs_to_visible_block(handle, c.owner_handle, layout_block)
|
||||||
|| self.belongs_to_visible_block(
|
|| (self.block_edit_block.is_none()
|
||||||
handle,
|
&& self.belongs_to_visible_block(
|
||||||
c.owner_handle,
|
handle,
|
||||||
self.model_space_block_handle(),
|
c.owner_handle,
|
||||||
)
|
self.model_space_block_handle(),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.flat_map(|(&handle, model)| {
|
.flat_map(|(&handle, model)| {
|
||||||
let entity = self.document.get_entity(handle);
|
let entity = self.document.get_entity(handle);
|
||||||
|
|||||||
+29
-5
@@ -835,6 +835,12 @@ pub struct Scene {
|
|||||||
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
|
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
|
||||||
/// Active layout name — "Model" or a paper space layout name.
|
/// Active layout name — "Model" or a paper space layout name.
|
||||||
pub current_layout: String,
|
pub current_layout: String,
|
||||||
|
/// When `Some`, the active space is a BEDIT block editor (issue #261):
|
||||||
|
/// rendering, picking and new-entity ownership are scoped to this block
|
||||||
|
/// record's handle instead of the layout's, so only the block's own
|
||||||
|
/// (block-local) entities are drawn and edited. `current_layout` stays
|
||||||
|
/// "Model" so the rest of the Model-vs-paper code keeps model behaviour.
|
||||||
|
pub block_edit_block: Option<Handle>,
|
||||||
/// UCS→world rotation for the ViewCube, kept in sync with the tab's active
|
/// UCS→world rotation for the ViewCube, kept in sync with the tab's active
|
||||||
/// UCS by `DocumentTab::sync_ucs_to_scene`. Identity = WCS. Applied only in
|
/// UCS by `DocumentTab::sync_ucs_to_scene`. Identity = WCS. Applied only in
|
||||||
/// model space so the cube's faces follow the user's coordinate system.
|
/// model space so the cube's faces follow the user's coordinate system.
|
||||||
@@ -1019,6 +1025,7 @@ impl Scene {
|
|||||||
paper_sheet_cache: RefCell::new(None),
|
paper_sheet_cache: RefCell::new(None),
|
||||||
paper_projected_cache: RefCell::new(HashMap::default()),
|
paper_projected_cache: RefCell::new(HashMap::default()),
|
||||||
current_layout: "Model".to_string(),
|
current_layout: "Model".to_string(),
|
||||||
|
block_edit_block: None,
|
||||||
viewcube_ucs: glam::Mat4::IDENTITY,
|
viewcube_ucs: glam::Mat4::IDENTITY,
|
||||||
hatches: HashMap::default(),
|
hatches: HashMap::default(),
|
||||||
meshes: HashMap::default(),
|
meshes: HashMap::default(),
|
||||||
@@ -1523,6 +1530,13 @@ impl Scene {
|
|||||||
/// second paper tab → "*Paper_Space0"
|
/// second paper tab → "*Paper_Space0"
|
||||||
/// Nth paper tab → "*Paper_Space{N-2}"
|
/// Nth paper tab → "*Paper_Space{N-2}"
|
||||||
fn current_layout_block_handle(&self) -> Handle {
|
fn current_layout_block_handle(&self) -> Handle {
|
||||||
|
// BEDIT block editor: the active space IS a block record — draw / pick /
|
||||||
|
// own only its own (block-local) entities (issue #261).
|
||||||
|
if let Some(h) = self.block_edit_block {
|
||||||
|
if !h.is_null() {
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Locate the Layout object for the active layout name.
|
// Locate the Layout object for the active layout name.
|
||||||
let layout = self.document.objects.values().find_map(|obj| {
|
let layout = self.document.objects.values().find_map(|obj| {
|
||||||
if let ObjectType::Layout(l) = obj {
|
if let ObjectType::Layout(l) = obj {
|
||||||
@@ -2401,7 +2415,11 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
// Build once: full tessellation, no cull (region = None), no zoom LOD
|
// Build once: full tessellation, no cull (region = None), no zoom LOD
|
||||||
// (wpp = None). Held for the life of this geometry epoch.
|
// (wpp = None). Held for the life of this geometry epoch.
|
||||||
let block = self.model_space_block_handle();
|
// In a BEDIT block editor the resident set is the edited block's own
|
||||||
|
// (block-local) entities; otherwise model space. (#261)
|
||||||
|
let block = self
|
||||||
|
.block_edit_block
|
||||||
|
.unwrap_or_else(|| self.model_space_block_handle());
|
||||||
let t_tess = iced::time::Instant::now();
|
let t_tess = iced::time::Instant::now();
|
||||||
let mut wires = self.wires_for_block_culled(block, None, None, None, None);
|
let mut wires = self.wires_for_block_culled(block, None, None, None, None);
|
||||||
self.apply_refedit_fade(&mut wires, self.bg_color);
|
self.apply_refedit_fade(&mut wires, self.bg_color);
|
||||||
@@ -2678,9 +2696,14 @@ impl Scene {
|
|||||||
.filter(|(&h, _)| self.mesh_entity_visible(h))
|
.filter(|(&h, _)| self.mesh_entity_visible(h))
|
||||||
.map(|(_, set)| set.clone())
|
.map(|(_, set)| set.clone())
|
||||||
.collect();
|
.collect();
|
||||||
// Block-definition solids are instanced per model-space INSERT so a
|
// Block-definition solids are instanced per INSERT of the ACTIVE space's
|
||||||
// block placed at an INSERT scale renders at the right size. (#123)
|
// block so a block placed at an INSERT scale renders at the right size
|
||||||
all.extend(self.instanced_block_meshes(self.model_space_block_handle()));
|
// (#123) — model space normally, the edited block in a BEDIT editor so
|
||||||
|
// model-space solids don't leak into it (#261).
|
||||||
|
all.extend(self.instanced_block_meshes(
|
||||||
|
self.block_edit_block
|
||||||
|
.unwrap_or_else(|| self.model_space_block_handle()),
|
||||||
|
));
|
||||||
let arc = Arc::new(all);
|
let arc = Arc::new(all);
|
||||||
*self.mesh_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
*self.mesh_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
@@ -2940,7 +2963,8 @@ impl Scene {
|
|||||||
// matters when the layout block handle differs from the
|
// matters when the layout block handle differs from the
|
||||||
// entity's owner (issue: hatch fill not selectable).
|
// entity's owner (issue: hatch fill not selectable).
|
||||||
if self.belongs_to_visible_block(h, c.owner_handle, layout_block)
|
if self.belongs_to_visible_block(h, c.owner_handle, layout_block)
|
||||||
|| self.belongs_to_visible_block(h, c.owner_handle, model_block)
|
|| (self.block_edit_block.is_none()
|
||||||
|
&& self.belongs_to_visible_block(h, c.owner_handle, model_block))
|
||||||
{
|
{
|
||||||
Some((h, m.clone()))
|
Some((h, m.clone()))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in new issue
Block a user