fix(dimension): re-bake the *D block after in-place edits (#181)

OCS renders a dimension live from its definition points but exports a
baked *D block that other applications draw. An in-place edit (DIMTEDIT,
text override, DIMSPACE, any ReplaceEntity/ReplaceMany edit) changed the
dimension while leaving the old block, so the export kept the pre-edit
graphics — BricsCAD/ODA showed the stale dimension while OCS showed the
edit.

Add explode::invalidate_dim_block, which drops a dimension's baked *D
block (record + owned sub-entities, so no orphan blocks accumulate) and
clears block_name; bake_dimension_blocks then rebuilds a fresh block on
the next save. Call it from the in-place edit paths. The transform path
(MOVE/COPY/PASTE) keeps its own block in sync and is left untouched, so
dimensions loaded from other applications and not edited are preserved.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-08 10:23:48 +03:00
1 parent aca2b952a5
commit 4164ef7111
2 files changed
+142 -1

No files matched your search

+39 -1
View File
@@ -429,7 +429,17 @@ impl OpenCADStudio {
for (handle, entities) in replacements {
self.tabs[i].scene.erase_entities(&[handle]);
for entity in entities {
self.tabs[i].scene.add_entity(entity);
let nh = self.tabs[i].scene.add_entity(entity);
// Drop a stale *D block on any replaced dimension (#181).
if matches!(
self.tabs[i].scene.document.get_entity(nh),
Some(acadrust::EntityType::Dimension(_))
) {
crate::modules::draw::modify::explode::invalidate_dim_block(
&mut self.tabs[i].scene.document,
nh,
);
}
}
}
for entity in additions {
@@ -539,6 +549,21 @@ impl OpenCADStudio {
.into_iter()
.map(|e| self.tabs[i].scene.add_entity(e))
.collect();
// A replaced dimension carries edited geometry/text but still
// names its old *D block; drop that stale block so the next save
// re-bakes it — otherwise BricsCAD/ODA draw the pre-edit
// graphics while OCS shows the edit. (#181)
for &nh in &new_handles {
if matches!(
self.tabs[i].scene.document.get_entity(nh),
Some(acadrust::EntityType::Dimension(_))
) {
crate::modules::draw::modify::explode::invalidate_dim_block(
&mut self.tabs[i].scene.document,
nh,
);
}
}
if let Some(cmd) = &mut self.tabs[i].active_cmd {
cmd.on_entity_replaced(handle, &new_handles);
}
@@ -1872,6 +1897,7 @@ impl OpenCADStudio {
}
CmdResult::DdeditEntity { handle, new_text } => {
let mut updated = false;
let mut is_dim = false;
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
match entity {
acadrust::EntityType::Text(t) => {
@@ -1895,10 +1921,19 @@ impl OpenCADStudio {
let base = d.base_mut();
base.text = new_text;
updated = true;
is_dim = true;
}
_ => {}
}
}
if is_dim {
// The edited override changed the dimension text; drop its
// stale *D block so save re-bakes it. (#181)
crate::modules::draw::modify::explode::invalidate_dim_block(
&mut self.tabs[i].scene.document,
handle,
);
}
if updated {
self.push_undo_snapshot(i, "DDEDIT");
self.tabs[i].dirty = true;
@@ -2269,6 +2304,9 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
_ => {}
}
}
// The dimension line moved, so its baked *D block is stale — drop it so
// the next save re-bakes it (no-op for non-dimensions). (#181)
crate::modules::draw::modify::explode::invalidate_dim_block(&mut scene.document, h);
}
scene.bump_geometry();
}
+103
View File
@@ -1073,6 +1073,50 @@ pub fn bake_dimension_blocks(doc: &mut CadDocument) {
}
}
/// Drop a dimension's baked `*D` block so the next save regenerates it from the
/// dimension's current definition points / text / style.
///
/// OCS renders a dimension live from its definition points, but exports a baked
/// `*D` block that other applications (BricsCAD / ODA) draw instead. An in-place
/// edit — grip drag, DIMTEDIT, restyle, text edit, DIMSPACE — changes the
/// definition points while leaving the old block, so without this the export
/// keeps the pre-edit graphics and the dimension appears wrong everywhere but in
/// OCS. Call this after any such edit; [`bake_dimension_blocks`] then rebuilds a
/// fresh block on save.
///
/// The transform path (MOVE / COPY / PASTE) keeps its own block in sync via
/// `define_transformed_block` and must NOT call this. The removed block's record
/// and owned entities are deleted too, so re-baking on every edit can't
/// accumulate orphan `*D` blocks. No-op when the dimension has no baked block
/// yet (the pending path bakes it fresh on save). (#181)
pub fn invalidate_dim_block(doc: &mut CadDocument, handle: Handle) {
let bn = match doc.get_entity(handle) {
Some(EntityType::Dimension(d)) => d.base().block_name.clone(),
_ => return,
};
if bn.trim().is_empty() {
return;
}
// Dimension graphics live in an anonymous "*D…" block, unique to the one
// dimension, so it is safe to delete along with its sub-entities. Guard on
// the anonymous prefix so a hand-referenced named block is never removed.
if bn.starts_with("*D") || bn.starts_with("*d") {
if let Some(rec) = doc.block_records.remove(&bn) {
let mut owned = rec.entity_handles.clone();
owned.push(rec.block_entity_handle);
owned.push(rec.block_end_handle);
for h in owned {
if !h.is_null() {
doc.remove_entity(h);
}
}
}
}
if let Some(EntityType::Dimension(d)) = doc.get_entity_mut(handle) {
d.base_mut().block_name.clear();
}
}
// ── Command stub (kept for future interactive selection mode) ───────────────
pub struct ExplodeCommand;
@@ -1175,6 +1219,65 @@ mod tests {
);
}
/// Invalidating a baked dimension removes its `*D` block record AND the
/// sub-entities it owned (no orphans), and clears `block_name` so the next
/// save re-bakes a fresh block. Guards the stale-export fix for #181.
#[test]
fn invalidate_removes_block_and_leaves_no_orphan() {
let mut doc = CadDocument::new();
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.unwrap();
bake_dimension_blocks(&mut doc);
let block_name = match doc.get_entity(handle) {
Some(EntityType::Dimension(d)) => d.base().block_name.clone(),
_ => panic!("dimension missing"),
};
assert!(!block_name.trim().is_empty());
let owned: Vec<Handle> = doc
.block_records
.get(&block_name)
.unwrap()
.entity_handles
.clone();
assert!(!owned.is_empty(), "baked block should own sub-entities");
invalidate_dim_block(&mut doc, handle);
// block_name cleared → the dimension is pending again.
match doc.get_entity(handle) {
Some(EntityType::Dimension(d)) => {
assert!(d.base().block_name.trim().is_empty(), "block_name must be cleared");
}
_ => panic!("dimension missing"),
}
// Block record gone.
assert!(
doc.block_records.get(&block_name).is_none(),
"stale block record must be removed"
);
// No orphaned sub-entities left behind.
for h in owned {
assert!(
doc.get_entity(h).is_none(),
"sub-entity {h:?} of the dropped block leaked"
);
}
// A fresh bake re-creates a block, so the round-trip stays valid.
bake_dimension_blocks(&mut doc);
let rebaked = match doc.get_entity(handle) {
Some(EntityType::Dimension(d)) => d.base().block_name.clone(),
_ => panic!("dimension missing"),
};
assert!(!rebaked.trim().is_empty(), "re-bake must assign a new block");
}
// Collect the line segments baked into the dimension's `*D` block.
fn baked_segments(doc: &CadDocument, block_name: &str) -> Vec<(Vector3, Vector3)> {
let rec = doc.block_records.get(block_name).expect("block record");