collective

Browse / github.com/storytold/cadcraft / patch

fix(dxf): prevent MTEXT column width from overwriting linetype scale (#2247)

DXF reader treats MTEXT column width as Common.ltscale (group 48 collision) ## Scope Reproduced preservation discrepancy, split from #2238 (A9); regression tests are in #2237. This issue tracks **one finding**, including any shared DXF/DWG manifestations. The first witness is the R2000 ASCII output of the current writer. This demonstrates group interpretation, not external-app conformance of every column encoding/version. DWG column disappearance is a separate finding (A10). ## Observed result The DXF column case changes `Common.ltscale` from `1.0` to `12.5` (group 48 collision). ## Expected result / decision required Keep Common.ltscale=1.0 and the independentl

Status
open
Score
0.00
Adopters
0 other operators
Author
Zaxaerith (imported from pull request #2276, open)
Base
d864a08b013353143086bd8bb568c26cb6256796
Size
+110 −3 in 2 files
Review
pending
Shared
2026-10-28
Id
01abaa9fc31b5ac28b0e88b7

How it went

No other operator has reported using it yet.

Diff

raw · crates/io/src/dxf_read/entities.rs crates/io/tests/it/mtext_columns_roundtrip.rs

diff --git a/crates/io/src/dxf_read/entities.rs b/crates/io/src/dxf_read/entities.rs--- a/crates/io/src/dxf_read/entities.rs+++ b/crates/io/src/dxf_read/entities.rs@@ -117,7 +117,9 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef         return None;     }     let t = T(tags);-    let c = common(&t);+    // MTEXT reuses group 48 for column width; embedded objects also have their own groups.+    // Keep those out of Common without changing the tags used for the entity's contents.+    let c = common(&T(if kind == "MTEXT" { mtext_dxf::before_columns(tags) } else { tags }));     let k = match kind {         "LINE" => EntityKind::Line(Line { a: t.p(10), b: t.p(11) }),         "POINT" => EntityKind::Point(Point { p: t.p(10), angle: t.fd(50, 0.0).to_radians() }),diff --git a/crates/io/tests/it/mtext_columns_roundtrip.rs b/crates/io/tests/it/mtext_columns_roundtrip.rs--- a/crates/io/tests/it/mtext_columns_roundtrip.rs+++ b/crates/io/tests/it/mtext_columns_roundtrip.rs@@ -1,9 +1,11 @@ //! MTEXT columns and paragraph codes survive a DXF round trip (#532): the column groups //! 75/76/78/79/48/49/50, tabs as `^I`, carets as `^ `.+//! Column width must not also become the entity's Common.ltscale (#2247). -use cadcraft_doc::{Common, Drawing, EntityKind, MText, MTextColumnType, MTextColumns, Space};+use cadcraft_doc::{Common, Drawing, EntityKind, Line, MText, MTextColumnType, MTextColumns, Space};+use cadcraft_dxf::Tag; use cadcraft_geom::Vec3;-use cadcraft_io::{read_dxf, write_dxf};+use cadcraft_io::{SaveOptions, Version, entity_from_groups, read_dxf, write_dxf, write_with};  fn mtext(contents: &str, rotation: f64, columns: Option<MTextColumns>) -> MText {     MText {@@ -91,3 +93,106 @@ fn hostile_column_groups_do_not_panic() {         let _ = read_dxf(bad.as_bytes());     } }++type TestResult = Result<(), Box<dyn std::error::Error>>;++/// Compare each read against the authored entity, including Common, rather than a first-save baseline.+fn assert_ltscale_round_trips(kind: EntityKind, ltscale: f64) -> TestResult {+    let common = Common { ltscale, color: cadcraft_color::Color::Index(3), visible: false, ..Common::default() };+    let mut authored = Drawing::new_metric();+    let handle = authored.add(&Space::Model, common.clone(), kind.clone())?;+    for version in Version::ALL {+        for binary in [false, true] {+            let options = SaveOptions { version: Some(version), binary };+            let mut drawing = authored.clone();+            for pass in 1..=2 {+                let bytes = write_with(&drawing, "group48.dxf", &options)?;+                drawing = read_dxf(&bytes)?;+                let entity = drawing.model.get(handle).ok_or("entity lost on DXF read")?;+                assert_eq!(entity.common.ltscale, ltscale, "{version:?}, binary={binary}, pass={pass}");+                assert_eq!(entity.common, common, "{version:?}, binary={binary}, pass={pass}");+                assert_eq!(entity.kind, kind, "{version:?}, binary={binary}, pass={pass}");+            }+        }+    }+    Ok(())+}++#[test]+fn uncolumned_mtext_keeps_common_ltscale() -> TestResult {+    let t = MText { width: 12.5, ..mtext("plain text", 0.0, None) };+    for ltscale in [1.0, 2.5] {+        assert_ltscale_round_trips(EntityKind::MText(t.clone()), ltscale)?;+    }+    Ok(())+}++#[test]+fn static_column_width_is_not_common_ltscale() -> TestResult {+    let columns = MTextColumns { kind: MTextColumnType::Static, count: 3, gutter: 1.25, height: 7.5, flow_reversed: true, ..Default::default() };+    let t = MText { width: 12.5, ..mtext("one\\Ntwo", 0.0, Some(columns)) };+    for ltscale in [1.0, 2.5] {+        assert_ltscale_round_trips(EntityKind::MText(t.clone()), ltscale)?;+    }+    Ok(())+}++#[test]+fn dynamic_column_width_is_not_common_ltscale() -> TestResult {+    for auto_height in [false, true] {+        let columns = MTextColumns {+            kind: MTextColumnType::Dynamic,+            count: 3,+            gutter: 1.25,+            height: 7.5,+            auto_height,+            heights: if auto_height { Vec::new() } else { vec![7.5, 5.0, 6.0] },+            flow_reversed: true,+        };+        let t = MText { width: 12.5, ..mtext("dynamic columns", 0.0, Some(columns)) };+        for ltscale in [1.0, 2.5] {+            assert_ltscale_round_trips(EntityKind::MText(t.clone()), ltscale)?;+        }+    }+    Ok(())+}++#[test]+fn line_keeps_common_ltscale() -> TestResult {+    for ltscale in [1.0, 2.5] {+        assert_ltscale_round_trips(EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(1.0, 2.0, 3.0) }), ltscale)?;+    }+    Ok(())+}++#[test]+fn embedded_groups_do_not_supply_mtext_common_ltscale() -> TestResult {+    for subclass_markers in [false, true] {+        for ltscale in [1.0, 2.5] {+            let mut tags = Vec::new();+            if subclass_markers {+                tags.push(Tag::s(100, "AcDbEntity"));+            }+            tags.push(Tag::s(8, "0"));+            if ltscale != 1.0 {+                tags.push(Tag::f(48, ltscale));+            }+            if subclass_markers {+                tags.push(Tag::s(100, "AcDbMText"));+            }+            tags.extend([+                Tag::f(40, 0.5),+                Tag::f(41, 12.5),+                Tag::s(1, "own text"),+                Tag::s(101, "Embedded Object"),+                // A reused code in the embedded payload is not an entity-level property.+                Tag::f(48, 99.0),+            ]);+            let (common, kind) = entity_from_groups("MTEXT", &tags).ok_or("MTEXT was not read")?;+            assert_eq!(common.ltscale, ltscale);+            assert_eq!(common, Common { ltscale, ..Common::default() });+            assert!(matches!(kind, EntityKind::MText(t) if t.columns.is_none() && t.width == 12.5 && t.rotation == 0.0));+        }+    }+    Ok(())+}