forked from 1b-agent/opencadstudio-upstream
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:
1 parent
aca2b952a5
commit
4164ef7111
2 files changed
+142
-1
No files matched your search
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user