fix(io): a centred ATTDEF keeps its insertion point in a DWG (#2464) (#2466)

* test(io): a centred ATTDEF keeps its insertion point in a DWG (#2464)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(io): an ATTDEF with no default value is placed by its tag's width in DWG (#2464)

The writer keeps a justified text's stored insertion point only when it is near where the
baseline starts for the text's width; an ATTDEF with an empty default value has width 0, so every
AutoCAD insertion point further than half a height from the alignment point was replaced by the
alignment point. The tag is what is drawn: judge against its width.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(io): keep a stored ATTDEF point that is close to the empty text's start

The tag-width baseline check replaced a point read from a drawing whenever the
font's width of the tag put the computed start further than the allowance away,
e.g. a centred LONGTAG with group 10 at x=99.5 and group 11 at x=100 moved to
about 92.17 in DXF and DWG. A stored point close to the empty text's own start
is now kept as before; only otherwise is it judged against the tag.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(io): store an ATTDEF point that differs from the computed baseline

`assert_kept(start)` stored exactly the baseline the tag-width fallback computes, so a write that replaced group 10 with that baseline passed too. The point is now 1 off the start, within the tolerance. Review of #2466.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(io): an empty-value ATTDEF keeps its point with an empty or absent prompt too

The tag-width check applies to every ATTDEF with no default value, since the
CAD displays the tag whatever the prompt: the guard now says `attdef` instead
of reading as a prompt test, and the test covers a prompt, an empty one and none.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
assamaral
2026-10-30 19:16:55 -07:00
committed by GitHub
co-authored by Claude Fable 5.1
parent b4b8ae294a
commit 4792ca2941
3 changed files with 63 additions and 2 deletions
+13 -2
View File
@@ -241,8 +241,19 @@ fn computed_text_insert(d: &Drawing, t: &Text) -> Vec3 {
if q.is_finite() { Vec3::new(q.x, q.y, t.insert.z) } else { t.insert }
}
/// `attdef_prompt` is `Some` for an ATTDEF (its prompt, which may be empty) and `None` for an ATTRIB.
pub(super) fn text_tags(w: &mut W, d: &Drawing, t: &Text, attrib: Option<(&Attrib, Version)>, attdef_prompt: Option<&str>) {
w.p(10, text_insert(d, t));
let attdef = attdef_prompt.is_some();
// An attribute definition with no default value is drawn as its tag, whatever its prompt: the
// baseline start AutoCAD stored is judged against the tag's width too. A stored point that is
// close to the empty text's own start is kept as it is, so the font's width of the tag never
// moves it.
let own = text_insert(d, t);
let insert = match attrib {
Some((a, _)) if attdef && t.value.is_empty() && own != t.insert => text_insert(d, &Text { value: a.tag.clone(), ..t.clone() }),
_ => own,
};
w.p(10, insert);
w.f(40, t.height);
w.s(1, &t.value);
if t.rotation != 0.0 {
@@ -277,7 +288,7 @@ pub(super) fn text_tags(w: &mut W, d: &Drawing, t: &Text, attrib: Option<(&Attri
}
match attrib {
Some((a, target)) => {
w.s(100, if attdef_prompt.is_some() { "AcDbAttributeDefinition" } else { "AcDbAttribute" });
w.s(100, if attdef { "AcDbAttributeDefinition" } else { "AcDbAttribute" });
if target >= Version::R2010 {
w.i(280, 0);
}
+49
View File
@@ -0,0 +1,49 @@
//! A centre-justified ATTDEF keeps its insertion point in a DWG (#2464): AutoCAD puts it where the
//! baseline of the displayed tag starts, which for an attribute with an empty default value is
//! further from the alignment point than half a height.
use cadcraft_doc::EntityKind;
use cadcraft_io::{SaveOptions, Version, read, write_with};
/// The `insert` x of the only ATTDEF in `d`.
fn attdef_x(d: &cadcraft_doc::Drawing) -> f64 {
d.model.iter().find_map(|e| if let EntityKind::AttDef(a) = &e.kind { Some(a.text.insert.x) } else { None }).expect("an ATTDEF")
}
#[test]
fn centred_attdef_with_an_empty_value_keeps_its_insertion_point() {
// The tag "LONGTAG" at height 2.5, centred on x = 100: its baseline starts half a width left.
// The stored point is 1 off that start (within the tolerance), so a write that recomputed
// it from the tag would not pass for keeping it.
let width = cadcraft_fonts::line_width("LONGTAG", 2.5, 1.0);
let start = 100.0 - width / 2.0 - 1.0;
assert!(100.0 - start > 3.0, "the point is well over a height from the alignment point");
assert_kept(start);
}
/// A stored point that a tag-width baseline would move is kept too (review of #2466): the check
/// does not rely on the font's width of the tag, so this one is only 0.5 from the alignment point.
#[test]
fn centred_attdef_point_near_the_alignment_point_is_kept() {
assert_kept(99.5);
}
/// A centred ATTDEF with an empty default value and group 10 at `start`, group 11 at x = 100 is
/// read from DXF and kept by a write as DXF and as DWG (both versions). The displayed text is the
/// tag whatever the prompt: with a prompt, an empty one (group 3 empty) and none (no group 3).
fn assert_kept(start: f64) {
for prompt in ["3\nprompt\n", "3\n\n", ""] {
let dxf = format!(
"0\nSECTION\n2\nENTITIES\n0\nATTDEF\n8\n0\n10\n{start}\n20\n5.0\n40\n2.5\n1\n\n7\nSTANDARD\n72\n1\n11\n100.0\n21\n5.0\n{prompt}2\nLONGTAG\n70\n8\n0\nENDSEC\n0\nEOF\n"
);
let d = read(dxf.as_bytes(), "t.dxf").unwrap();
assert!((attdef_x(&d) - start).abs() < 1e-6, "read from DXF");
let as_dxf = cadcraft_io::write(&d, "t.dxf").unwrap();
assert!((attdef_x(&read(&as_dxf, "t.dxf").unwrap()) - start).abs() < 1e-6, "DXF, prompt {prompt:?}: insertion point changed");
for version in [Version::R2000, Version::R2018] {
let dwg = write_with(&d, "t.dwg", &SaveOptions { version: Some(version), binary: false }).unwrap();
let back = read(&dwg, "t.dwg").unwrap();
assert!((attdef_x(&back) - start).abs() < 1e-6, "{version:?}, prompt {prompt:?}: insertion point {} instead of {start}", attdef_x(&back));
}
}
}
+1
View File
@@ -5,6 +5,7 @@ mod angle_header;
mod annotative_orientation;
mod annotative_roundtrip;
mod assoc_array_roundtrip;
mod attdef_insert_dwg;
mod attrib_modes;
mod attrib_props;
mod block_expansion;