From 3548de332c90b0dfe27c783fb855fda38741a49b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 14 Jul 2026 13:03:04 +0300 Subject: [PATCH] feat(block): BEDIT block editor as a dedicated space tab (#261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/commands/inquiry.rs | 190 ++++++++++++++++++++++++++ src/app/document.rs | 4 + src/app/update/viewport.rs | 48 ++++++- src/app/view/mod.rs | 24 +++- src/modules/draw/modify/block_edit.rs | 99 ++++++++++++++ src/modules/draw/modify/mod.rs | 1 + src/scene/entity.rs | 53 ++++++- src/scene/mod.rs | 34 ++++- 8 files changed, 435 insertions(+), 18 deletions(-) create mode 100644 src/modules/draw/modify/block_edit.rs diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 11ee5ce0b..48a3c39d4 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -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:") => { use crate::modules::draw::modify::refedit::{ apply_insert_transform, RefEditSession, diff --git a/src/app/document.rs b/src/app/document.rs index 9f01a6a60..df181602f 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -1,5 +1,6 @@ use crate::command::CadCommand; use crate::io::linetypes; +use crate::modules::draw::modify::block_edit::BlockEditSession; use crate::modules::draw::modify::refedit::RefEditSession; use crate::scene::pick::grip::GripEdit; use crate::scene::GripDef; @@ -205,6 +206,8 @@ pub(super) struct DocumentTab { pub(super) paper_bg_color: Option<[f32; 4]>, /// Active REFEDIT session, if any. pub(super) refedit_session: Option, + /// Active BEDIT block-editor space session, if any (issue #261). + pub(super) block_edit: Option, /// Currently active MLeader style name. pub(super) active_mleader_style: String, /// Last camera_generation value written back to the document. @@ -437,6 +440,7 @@ impl DocumentTab { bg_color: None, paper_bg_color: None, refedit_session: None, + block_edit: None, active_mleader_style: "Standard".to_string(), last_synced_camera_gen: 0, is_start: false, diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 975ac0a2b..33e8002f1 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2592,9 +2592,9 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { } // Double-clicking a block with attributes opens the // attribute editor (edit its values); a block with - // no attributes enters in-place block edit (REFEDIT), - // so its geometry can be edited and the change - // reflects in every instance. (#136, #192) + // no attributes opens the block editor (BEDIT) — a + // space tab scoped to the block's own geometry, so + // edits reflect in every instance. (#136, #192, #261) let insert_has_attrs = matches!( self.tabs[i].scene.document.get_entity(handle), Some(AcadEntityType::Insert(ins)) if !ins.attributes.is_empty() @@ -2606,9 +2606,12 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { self.tabs[i].scene.document.get_entity(handle), 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!( - "REFEDIT_BEGIN:{}", + "BEDIT_BEGIN:{}", handle.value() ))); } @@ -2989,6 +2992,14 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { pub(super) fn on_layout_switch(&mut self, name: String) -> Task { 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"; // Persist the camera of the layout we're leaving BEFORE switching // 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 { let new_name = new_name.trim().to_string(); if !new_name.is_empty() && new_name != orig { 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] .scene .layout_names() diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 4fd92ec2f..474fce296 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -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, // anchored at the canvas top-left so it doesn't track the cursor. if self.quick_properties && !tab.is_start { @@ -1328,8 +1337,19 @@ impl OpenCADStudio { self.polar_popup_open, self.dyn_input, 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(), tab.scene.first_viewport_scale(), tab.scene.viewport_count(), diff --git a/src/modules/draw/modify/block_edit.rs b/src/modules/draw/modify/block_edit.rs new file mode 100644 index 000000000..8b2d88657 --- /dev/null +++ b/src/modules/draw/modify/block_edit.rs @@ -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 { + 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, +} + +// ── 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"] }); diff --git a/src/modules/draw/modify/mod.rs b/src/modules/draw/modify/mod.rs index 4e5bd302d..1ed7f7d22 100644 --- a/src/modules/draw/modify/mod.rs +++ b/src/modules/draw/modify/mod.rs @@ -11,6 +11,7 @@ pub mod lengthen; pub mod mirror; pub mod offset; pub mod pedit; +pub mod block_edit; pub mod refedit; pub mod reverse; pub mod rotate; diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 22d131b29..8284f8ce5 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -109,9 +109,14 @@ impl Scene { self.ensure_layer(&layer); // 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. // - 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(); self.document .add_entity_to_layout(entity, &layout_name) @@ -139,6 +144,41 @@ impl Scene { 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 /// its identity (handle + owning block), and refresh the derived /// 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 // 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, - self.model_space_block_handle(), - ) + || (self.block_edit_block.is_none() + && self.belongs_to_visible_block( + handle, + c.owner_handle, + self.model_space_block_handle(), + )) }) .flat_map(|(&handle, model)| { let entity = self.document.get_entity(handle); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index c7b0e056b..8de206665 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -835,6 +835,12 @@ pub struct Scene { paper_projected_cache: RefCell)>>, /// Active layout name — "Model" or a paper space layout name. 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, /// 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 /// 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_projected_cache: RefCell::new(HashMap::default()), current_layout: "Model".to_string(), + block_edit_block: None, viewcube_ucs: glam::Mat4::IDENTITY, hatches: HashMap::default(), meshes: HashMap::default(), @@ -1523,6 +1530,13 @@ impl Scene { /// second paper tab → "*Paper_Space0" /// Nth paper tab → "*Paper_Space{N-2}" 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. let layout = self.document.objects.values().find_map(|obj| { if let ObjectType::Layout(l) = obj { @@ -2401,7 +2415,11 @@ impl Scene { } // Build once: full tessellation, no cull (region = None), no zoom LOD // (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 mut wires = self.wires_for_block_culled(block, None, None, None, None); self.apply_refedit_fade(&mut wires, self.bg_color); @@ -2678,9 +2696,14 @@ impl Scene { .filter(|(&h, _)| self.mesh_entity_visible(h)) .map(|(_, set)| set.clone()) .collect(); - // Block-definition solids are instanced per model-space INSERT so a - // block placed at an INSERT scale renders at the right size. (#123) - all.extend(self.instanced_block_meshes(self.model_space_block_handle())); + // Block-definition solids are instanced per INSERT of the ACTIVE space's + // block so a block placed at an INSERT scale renders at the right size + // (#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); *self.mesh_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); arc @@ -2940,7 +2963,8 @@ impl Scene { // matters when the layout block handle differs from the // entity's owner (issue: hatch fill not selectable). 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())) } else {