fix(io): a moved DIMENSION draws its block at its insertion point (group 12) (#2634) (#2636)

A DIMENSION moved by an older AutoCAD keeps its *D block contents where
they were and the move in DXF group 12, and is drawn translated by the
insertion point. Group 12 was never read into Dimension, so the block
rendered at its pre-move position and the extents reached there too.

Read group 12 into a new Dimension::insertion_point, draw the block
translated by it, move its bounds with it in the extents, test the
block's text where it draws, and write group 12 back whenever the block
written is the one read from the file. Regenerated geometry (edits drop
the block link) sits at its definition points, so no offset is kept or
written for it.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
This commit is contained in:
Rahul
2026-11-02 15:01:26 -07:00
committed by GitHub
co-authored by rahul05ranjan
parent 0bc025965a
commit 03101e8d77
29 changed files with 142 additions and 6 deletions
+5
View File
@@ -52,6 +52,11 @@ pub struct Dimension {
/// Anonymous block with the rendered geometry (from files); regenerated when edited.
#[serde(default)]
pub block: Option<String>,
/// Insertion point (DXF group 12): a dimension moved by an older AutoCAD keeps its offset
/// here with the `*D` block's contents left where they were, and is drawn translated by it.
/// Zero once the geometry is regenerated from the definition points.
#[serde(default)]
pub insertion_point: Vec3,
/// Per-dimension style overrides (DIMOVERRIDE): DimStyle field names (camelCase) → values,
/// merged over the named style.
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
+4
View File
@@ -212,6 +212,10 @@ fn bounds_in<'a>(d: &'a Drawing, e: &Entity, depth: usize, blocks: &mut HashMap<
inner
}
};
// The block draws translated by the insertion point (group 12), so its bounds
// move with it (#2634); the cache above keeps the block's own bounds.
let off = dm.insertion_point.xy();
let inner = if off == Vec2::ZERO { inner } else { Bounds2 { min: inner.min + off, max: inner.max + off } };
b = b.union(&inner);
}
b
+39
View File
@@ -55,6 +55,44 @@ fn frozen_layers_are_left_out_of_extents_and_layers_turned_off_are_not() {
assert_eq!(d.extents(&Space::Model).max, Vec2::new(1.0, 1.0));
}
/// DXF group 12: a dimension moved by an older AutoCAD keeps its `*D` block where it was and
/// the move in the insertion point, and the block draws translated by it. Extents follow the
/// drawing, not the stale block contents (#2634).
#[test]
fn dimension_insertion_point_moves_its_block_in_extents() {
let moved = |ins: Vec3| {
let mut d = Drawing::new_metric();
let mut blk = Block::new("*D1");
blk.entities.push(on_layer("0", line((850.0, 920.0), (860.0, 920.0)), 2));
d.blocks.insert("*D1".into(), std::sync::Arc::new(blk));
let dm = Dimension {
kind: DimKind::Aligned,
defpt: Vec3::new(5.0, 1.0, 0.0),
text_mid: Vec3::ZERO,
p13: Vec3::ZERO,
p14: Vec3::new(10.0, 0.0, 0.0),
p15: Vec3::ZERO,
p16: Vec3::ZERO,
text: String::new(),
style: "Standard".into(),
measurement: 0.0,
text_rotation: 0.0,
user_text_pos: false,
block: Some("*D1".into()),
insertion_point: ins,
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
};
d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).unwrap();
d
};
// Without the offset the block's far-away contents reach the extents.
assert!(moved(Vec3::ZERO).extents(&Space::Model).max.x > 800.0);
// With it, the block draws at the definition points and the extents stay at the drawing.
assert!(moved(Vec3::new(-850.0, -920.0, 0.0)).extents(&Space::Model).max.x < 20.0);
}
/// ZOOM Extents in AutoCAD 2024 (accoreconsole, EXTMIN/EXTMAX read back; AutoCAD pads them by a
/// few hundredths): a layer turned off counts and a frozen one does not, for an entity, for an
/// entity inside a block, and for a block reference itself.
@@ -637,6 +675,7 @@ fn invisible_dimblock_entity_is_ignored_in_dimension_extents() {
overrides: Default::default(),
assoc: vec![],
marks: Default::default(),
insertion_point: Vec3::ZERO,
}),
)
.unwrap();
+1
View File
@@ -22,6 +22,7 @@ pub(super) fn dim(s: &Session, kind: DimKind, defpt: Vec2, p13: Vec2, p14: Vec2,
overrides: s.doc().map(|d| d.dim_overrides()).unwrap_or_default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
})
}
+3
View File
@@ -32,6 +32,7 @@ fn dim(kind: DimKind, p13: Vec2, p14: Vec2, defpt: Vec2) -> EntityKind {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
})
}
@@ -182,6 +183,7 @@ pub fn bracket() -> Drawing {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}),
);
add(
@@ -203,6 +205,7 @@ pub fn bracket() -> Drawing {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}),
);
+9 -5
View File
@@ -117,11 +117,15 @@ fn in_text(d: &Drawing, e: &Entity, p: Vec2) -> bool {
// A dimension with a block (as AutoCAD saves them) is drawn from that block: its text is
// the text there, not the text the dimension's own geometry would place.
EntityKind::Dimension(dm) => match dm.block.as_ref().and_then(|n| d.block(n)) {
Some(blk) => blk.entities.iter().any(|be| match &be.kind {
EntityKind::MText(t) => in_box(cadcraft_render::layout_mtext_entity(d, t).strokes.into_iter().flatten(), t.rotation, p),
EntityKind::Text(_) => entity_bounds(d, be, 0).contains(p),
_ => false,
}),
// The block draws translated by the insertion point (group 12): test there (#2634).
Some(blk) => {
let p = p - dm.insertion_point.xy();
blk.entities.iter().any(|be| match &be.kind {
EntityKind::MText(t) => in_box(cadcraft_render::layout_mtext_entity(d, t).strokes.into_iter().flatten(), t.rotation, p),
EntityKind::Text(_) => entity_bounds(d, be, 0).contains(p),
_ => false,
})
}
None => {
let g = cadcraft_render::dimension_in(d, dm);
in_box(g.text.iter().flatten().chain(g.text_glyphs.iter().flatten().flatten()).copied(), g.text_angle, p)
+1
View File
@@ -208,6 +208,7 @@ fn derived_dimension_fields_are_not_differences() {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
let mut s = Session::new();
s.doc_mut().unwrap().add(&Space::Model, Common::default(), EntityKind::Dimension(dim(30.0, 7.0))).unwrap();
+4
View File
@@ -314,6 +314,7 @@ fn a_named_block_whose_dimension_draws_another_block_shows_the_other_definition(
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
let blocks = |dim_end: f64, first: u64| {
let mut part = Block::new("PART");
@@ -400,6 +401,7 @@ fn a_named_block_whose_dimension_block_has_another_generated_name_keeps_this_dra
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
let mut part = Block::new("PART");
part.entities.push(Entity::new(Handle(first), EntityKind::Dimension(dim)));
@@ -514,6 +516,7 @@ fn a_dimension_block_copy_keeps_its_geometry_when_the_snapshot_is_saved() {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
let mut s = Session::new();
let mut drawn = Block::new("*D1");
@@ -833,6 +836,7 @@ fn a_dimension_whose_block_is_past_the_nesting_limit_is_drawn_again() {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
for k in 1..=MAX_BLOCK_DEPTH {
let mut b = Block::new(&format!("B{k}"));
+1
View File
@@ -197,6 +197,7 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef
text_rotation: t.fd(53, 0.0).to_radians(),
user_text_pos: t.i(70).unwrap_or(0) & 128 != 0,
block: t.s(2).filter(|s| !s.is_empty()),
insertion_point: t.p(12),
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
+6
View File
@@ -399,6 +399,12 @@ fn write_dimension_entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper
let style = d.dim_style(&dm.style).cloned().unwrap_or_default();
let g = cadcraft_render::dimension_geometry(dm, &style, d.header.f64("DIMSCALE", 1.0));
w.p(11, if dm.user_text_pos { dm.text_mid } else { g.text_pos.to3(0.0) });
// Group 12: a block read from a file keeps the dimension's moved-by offset (AutoCAD
// draws the block translated by it); regenerated geometry already sits where the
// definition points place it, so it writes no offset.
if dm.block.is_some() && dm.insertion_point != Vec3::ZERO {
w.p(12, dm.insertion_point);
}
let ty: i64 = match dm.kind {
DimKind::Linear { .. } => 0,
DimKind::Aligned => 1,
+2
View File
@@ -98,6 +98,7 @@ fn sample() -> Drawing {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}),
)
.unwrap();
@@ -558,6 +559,7 @@ fn dim(kind: DimKind, p13: Vec3, p14: Vec3) -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
@@ -27,6 +27,7 @@ fn dim(kind: DimKind, defpt: Vec3, p13: Vec3, p14: Vec3, p15: Vec3, p16: Vec3, a
overrides: Default::default(),
assoc,
marks: Default::default(),
insertion_point: Vec3::ZERO,
})
}
+1
View File
@@ -98,6 +98,7 @@ fn s25(kind: DimKind, p13: Vec3, p14: Vec3, defpt: Vec3) -> Drawing {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).unwrap();
d
@@ -27,6 +27,7 @@ fn linear(a: [f64; 2], b: [f64; 2], at: [f64; 2]) -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
@@ -158,6 +159,47 @@ fn reopened_dimension_reuses_its_block_name_and_handles() {
assert_eq!(again.handseed, back.handseed);
}
#[test]
fn a_moved_dimension_keeps_its_group_12_offset() {
let mut d = Drawing::new_metric();
// What an older AutoCAD left: the block contents where the dimension was measured, the
// move kept in group 12 (insertion point), as #2634's drawing has it.
let mut b = Block::new("*D3");
let h = d.new_handle();
b.entities.push(Entity::new(h, EntityKind::Line(Line { a: Vec3::new(150.0, 80.0, 0.0), b: Vec3::new(160.0, 80.0, 0.0) })));
d.blocks.insert("*D3".into(), Arc::new(b));
let mut dm = linear([0.0, 0.0], [10.0, 0.0], [5.0, 3.0]);
dm.block = Some("*D3".into());
dm.insertion_point = Vec3::new(-150.0, -80.0, 0.0);
let h = d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).unwrap();
// The block is written as read and the offset round-trips as group 12.
let back = cadcraft_io::read_dxf(cadcraft_io::write_dxf(&d).as_bytes()).unwrap();
let Some(EntityKind::Dimension(dm)) = back.entity(h).map(|e| &e.kind) else { panic!("dimension lost") };
assert_eq!(dm.insertion_point, Vec3::new(-150.0, -80.0, 0.0));
assert_eq!(dm.block.as_deref(), Some("*D3"));
let p = pairs(&cadcraft_io::write_dxf(&d));
let at = p.iter().position(|(c, v)| *c == 0 && v == "DIMENSION").unwrap();
let own = &p[at..p[at + 1..].iter().position(|(c, _)| *c == 0).map(|k| at + 1 + k).unwrap()];
assert!(own.iter().any(|(c, v)| *c == 12 && v.parse::<f64>() == Ok(-150.0)), "no group 12 x");
assert!(own.iter().any(|(c, v)| *c == 22 && v.parse::<f64>() == Ok(-80.0)), "no group 12 y");
// Edited (edits drop the block link): geometry regenerated from the definition points,
// so the save writes no offset.
let mut edited = back.clone();
edited
.modify_entity(h, |e| {
if let EntityKind::Dimension(dm) = &mut e.kind {
dm.block = None;
}
})
.unwrap();
let q = pairs(&cadcraft_io::write_dxf(&edited));
let at = q.iter().position(|(c, v)| *c == 0 && v == "DIMENSION").unwrap();
let own = &q[at..q[at + 1..].iter().position(|(c, _)| *c == 0).map(|k| at + 1 + k).unwrap()];
assert!(!own.iter().any(|(c, _)| *c == 12), "regenerated geometry keeps a stale offset");
}
#[test]
fn hostile_handle_seed_does_not_panic() {
let mut d = Drawing::new_metric();
+2
View File
@@ -34,6 +34,7 @@ fn height_written(style_scale: f64, overrides: serde_json::Value) -> f64 {
overrides: overrides.as_object().cloned().unwrap_or_default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).unwrap();
block_mtext_height(&cadcraft_io::write_dxf(&d))
@@ -73,6 +74,7 @@ fn angular_drawing(separator_override: Option<&str>) -> Drawing {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
if let Some(separator) = separator_override {
dm.overrides.insert("DIMDSEP".into(), serde_json::json!(separator));
@@ -24,6 +24,7 @@ fn dim(kind: DimKind, marks: DimMarks) -> EntityKind {
overrides: Default::default(),
assoc: Vec::new(),
marks,
insertion_point: Vec3::ZERO,
})
}
@@ -60,6 +60,7 @@ fn dimension(overrides: serde_json::Value) -> EntityKind {
overrides: overrides.as_object().cloned().unwrap_or_default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
})
}
+1
View File
@@ -174,6 +174,7 @@ fn linear() -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
@@ -118,6 +118,7 @@ pub(crate) fn dimensions() -> Vec<Case> {
overrides: Default::default(),
assoc: Vec::new(),
marks: DimMarks::default(),
insertion_point: Vec3::ZERO,
}),
);
let mut block = Block::new("*D42");
+1
View File
@@ -1034,6 +1034,7 @@ fn add_associated_dimension(d: &mut Drawing, a: cadcraft_doc::Handle, b: cadcraf
overrides: Default::default(),
assoc: vec![at("p13", a), at("p14", b)],
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).expect("add dimension");
}
@@ -38,6 +38,7 @@ fn dimension(label: &str) -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
+1
View File
@@ -19,6 +19,7 @@ fn lin(p1: (f64, f64), p2: (f64, f64), on: (f64, f64), rot: f64) -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
+8 -1
View File
@@ -313,7 +313,10 @@ fn draw_dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dim
if let Some(blk) = dm.block.as_ref().and_then(|n| ctx.d.block(n))
&& ctx.depth < cadcraft_doc::MAX_BLOCK_DEPTH
{
let sub = Ctx { block_pstyle: b.named, block_transparency: b.transparency, ..sub_ctx(ctx, e, Mat3::IDENTITY) };
// DXF group 12: a moved dimension keeps its `*D` block where it was and the offset in
// the insertion point, so the block draws translated by it (#2634).
let off = dm.insertion_point.xy();
let sub = Ctx { block_pstyle: b.named, block_transparency: b.transparency, ..sub_ctx(ctx, e, Mat3::translate(off)) };
// DIMTFILL: the block's MTEXT carries no background mask here, so a background
// around each of the block's texts is drawn before the block's first text or first
// line of the text's marks, except for a text that a SOLID of the block covers.
@@ -333,6 +336,10 @@ fn draw_dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dim
.take(MAX_FILL_TEXTS)
.filter(|be| !covered(be))
.filter_map(|be| block_text_box(ctx.d, be, gap))
.map(|mut corners| {
corners.iter_mut().for_each(|c| *c += off);
corners
})
.collect()
};
let mut fill_pending = !boxes.is_empty();
+1
View File
@@ -370,6 +370,7 @@ fn lin_dim() -> cadcraft_doc::Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
@@ -23,6 +23,7 @@ fn dim(kind: DimKind) -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
@@ -22,6 +22,7 @@ fn text(vars: serde_json::Value) -> (String, String) {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
let mut st = DimStyle::default();
let rejected = st.apply_fields(vars.as_object().unwrap());
+1
View File
@@ -26,6 +26,7 @@ fn dimension() -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
@@ -23,6 +23,7 @@ fn linear(len: f64) -> Dimension {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
}
}
+1
View File
@@ -47,6 +47,7 @@ fn dimensions_take_the_viewport_layer_colour() {
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
insertion_point: Vec3::ZERO,
};
d.add(&Space::Model, on("A"), EntityKind::Dimension(dim)).unwrap();
d.add(&Space::Model, on("A"), EntityKind::Line(Line { a: Vec3::new(0.0, -1.0, 0.0), b: Vec3::new(10.0, -1.0, 0.0) })).unwrap();