From 4792ca2941e09813e3973b14e42807d574da071d Mon Sep 17 00:00:00 2001 From: assamaral Date: Fri, 30 Oct 2026 23:16:55 -0300 Subject: [PATCH] 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Claude Sonnet 5.5 --- crates/io/src/dxf_write/common.rs | 15 +++++++- crates/io/tests/it/attdef_insert_dwg.rs | 49 +++++++++++++++++++++++++ crates/io/tests/it/main.rs | 1 + 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 crates/io/tests/it/attdef_insert_dwg.rs diff --git a/crates/io/src/dxf_write/common.rs b/crates/io/src/dxf_write/common.rs index 6ba3ce8ef..b09d90410 100644 --- a/crates/io/src/dxf_write/common.rs +++ b/crates/io/src/dxf_write/common.rs @@ -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); } diff --git a/crates/io/tests/it/attdef_insert_dwg.rs b/crates/io/tests/it/attdef_insert_dwg.rs new file mode 100644 index 000000000..9fd1a1a3a --- /dev/null +++ b/crates/io/tests/it/attdef_insert_dwg.rs @@ -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)); + } + } +} diff --git a/crates/io/tests/it/main.rs b/crates/io/tests/it/main.rs index 07b1e8c2e..5d6c343d0 100644 --- a/crates/io/tests/it/main.rs +++ b/crates/io/tests/it/main.rs @@ -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;