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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-14 13:03:04 +03:00
1 parent a4fc34c6a2
commit 3548de332c
8 files changed
+435 -18

No files matched your search

+47 -6
View File
@@ -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);
+29 -5
View File
@@ -835,6 +835,12 @@ pub struct Scene {
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
/// 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<Handle>,
/// 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 {