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
@@ -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() }),@@ -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(())+}