diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 6d93ecff8..45fec88b0 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -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(); } diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index bbc7f2cfe..004dc5624 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -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 = 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");