diff --git a/crates/engine/src/cmd/modify.rs b/crates/engine/src/cmd/modify.rs index ae5a271e..7134f73a 100644 --- a/crates/engine/src/cmd/modify.rs +++ b/crates/engine/src/cmd/modify.rs @@ -1403,7 +1403,7 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option Handle { + v["handle"].as_str().and_then(Handle::parse_hex).unwrap() +} + +/// The issue's drawing: an SHX text style, a dimension style using it, a diameter dimension +/// of a circle of radius 30. +fn session() -> (Session, Handle) { + let mut s = Session::new(); + s.execute("units", &json!({"measurement": "metric"})).ok(); + s.execute("style", &json!({"name": "R", "font": "romans.shx", "current": true})).unwrap(); + s.execute("dimstyle", &json!({"name": "D", "current": true, "DIMTXSTY": "R", "DIMTXT": 5, "DIMASZ": 5, "DIMDEC": 0})).unwrap(); + let c = h(&s.execute("circle", &json!({"center": [0, 0], "radius": 30})).unwrap()); + let dim = h(&s.execute("dimdiameter", &json!({"handle": c.hex(), "at": [40, 40]})).unwrap()); + (s, dim) +} + +/// The MTEXT contents (group 1) inside each `*D…` block of a DXF. +fn dblock_texts(dxf: &str) -> Vec { + let lines: Vec<&str> = dxf.lines().map(str::trim).collect(); + let mut out = Vec::new(); + let (mut in_d, mut in_mtext) = (false, false); + for pair in lines.chunks(2) { + let [code, value] = pair else { continue }; + match (*code, *value) { + ("0", "BLOCK") => in_d = false, + ("2", name) if !in_mtext && name.starts_with("*D") => in_d = true, + ("0", "MTEXT") => in_mtext = in_d, + ("0", _) => in_mtext = false, + ("1", text) if in_mtext => out.push(text.to_string()), + _ => {} + } + } + out +} + +#[test] +fn the_dimension_block_writes_the_sign_as_a_code() { + let (s, dim) = session(); + let d = s.doc().unwrap(); + let EntityKind::Dimension(dm) = &d.entity(dim).unwrap().kind else { panic!("not a dimension") }; + assert!(matches!(dm.kind, cadcraft_engine::doc::DimKind::Diameter)); + let texts = dblock_texts(&cadcraft_io::write_dxf(d)); + assert_eq!(texts, vec!["%%c60".to_string()], "the *D block's MTEXT"); + // Read back, CADCraft still shows the sign. + let read = cadcraft_io::read_dxf(cadcraft_io::write_dxf(d).as_bytes()).unwrap(); + let dm = read.model.iter().find_map(|e| if let EntityKind::Dimension(dm) = &e.kind { Some(dm.clone()) } else { None }).unwrap(); + let st = read.dim_styles.iter().find(|x| x.name.eq_ignore_ascii_case(&dm.style)).cloned().unwrap_or_default(); + assert_eq!(cadcraft_render::dimension_geometry(&dm, &st, 1.0).value, "⌀60"); +} + +#[test] +fn exploding_keeps_the_sign_as_a_code() { + let (mut s, dim) = session(); + s.execute("explode", &json!({"handles": [dim.hex()]})).unwrap(); + let d = s.doc().unwrap(); + let texts: Vec = + d.model.iter().filter_map(|e| if let EntityKind::MText(m) = &e.kind { Some(m.contents.clone()) } else { None }).collect(); + assert_eq!(texts, vec!["%%c60".to_string()]); + let saved = cadcraft_io::write_dxf(d); + assert!(!saved.contains('⌀'), "no U+2300 is saved"); +} diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index 03326b1c..bd75f70c 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -1424,7 +1424,7 @@ fn dim_block_entities(d: &Drawing, dm: &Dimension, layer: &str) -> Vec { attach: 5, rotation: g.text_angle, style: style.text_style.clone(), - contents: g.value.clone(), + contents: g.mtext_contents(), line_spacing: 1.0, line_spacing_exact: false, columns: None, diff --git a/crates/render/src/dim.rs b/crates/render/src/dim.rs index 599fce73..d9504c56 100644 --- a/crates/render/src/dim.rs +++ b/crates/render/src/dim.rs @@ -53,6 +53,22 @@ pub struct DimGeometry { } impl DimGeometry { + /// `value` as the contents of a plain MTEXT (the dimension's `*D` block, its EXPLODE + /// pieces): the diameter sign as `%%c`, not the decoded ⌀ (U+2300), which no SHX font has, + /// so other programs plot nothing for it, while every font draws `%%c` (#1645). A `%` + /// that would run into a code is written `%%%`. + pub fn mtext_contents(&self) -> String { + let mut out = String::with_capacity(self.value.len() + 2); + let mut chars = self.value.chars().peekable(); + while let Some(c) = chars.next() { + match c { + '⌀' => out.push_str("%%c"), + '%' if matches!(chars.peek(), Some('%' | '⌀')) => out.push_str("%%%"), + c => out.push(c), + } + } + out + } fn dim(&mut self, l: Vec) { if l.len() >= 2 { self.lines.push(l); @@ -1381,6 +1397,7 @@ mod tests { d.defpt = Vec3::new(-3.0, 0.0, 0.0); let g = dimension_geometry(&d, &st_with(|s| s.decimals = 1), 1.0); assert_eq!(g.value, "⌀6.0"); + assert_eq!(g.mtext_contents(), "%%c6.0", "the *D block's text (#1645)"); let mut a = lin((10.0, 0.0), (0.0, 10.0), (3.0, 3.0), 0.0); a.kind = DimKind::Angular3P; a.p15 = Vec3::ZERO; @@ -1430,6 +1447,28 @@ mod tests { let g = dimension_geometry(&d, &DimStyle::default(), 1.0); assert!(g.lines.is_empty() && g.fills.is_empty(), "{:?}", g.lines); } + + /// #1645: the MTEXT contents give back the dimension's text when decoded, with the diameter + /// sign as `%%c` (no SHX font has U+2300) and a `%` that would start a code escaped. + #[test] + fn mtext_contents_write_the_diameter_sign_as_a_code() { + let with = |v: &str| DimGeometry { value: v.into(), ..DimGeometry::default() }; + for (value, contents) in [ + ("⌀60", "%%c60"), + ("2x ⌀8.5 THRU", "2x %%c8.5 THRU"), + ("R3.0", "R3.0"), + ("36.9° ±0.1", "36.9° ±0.1"), + ("50%", "50%"), + ("5%⌀6", "5%%%%%c6"), + ("%%", "%%%%"), + ("", ""), + ] { + let g = with(value); + assert_eq!(g.mtext_contents(), contents, "{value:?}"); + assert_eq!(decode_value(&g.mtext_contents()), value, "{value:?} reads back"); + assert!(!g.mtext_contents().contains('⌀')); + } + } } #[cfg(test)] diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 1fced63c..a5684c18 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (a file's own standard-named arrowhead blocks are kept) · **Target:** Autodesk AutoCAD 2027 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (a diameter dimension's `*D` text keeps the sign as `%%c`) · **Target:** Autodesk AutoCAD 2027 Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work items are in [gaps.md](gaps.md#file-formats); the weighted score feeds @@ -77,6 +77,7 @@ on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG | POLYLINE 2D/3D | yes | yes (as LWPOLYLINE / 3D POLYLINE) | polyface and polygon meshes flattened: lossy | | TEXT, MTEXT, ATTDEF, INSERT + ATTRIB | yes; multiline attributes (embedded MTEXT) read and drawn | yes; multiline attributes written as embedded MTEXT (DXF 2007 and later; older versions: one line) | ATTDEF prompt kept (#137) | | DIMENSION (all 7 kinds) | yes | yes, with anonymous `*D` blocks | a reopened dimension's `*D` block is regenerated under its name, unreferenced ones dropped (#495); DIMASSOC objects written; angular block text honors DIMDSEP, including per-dimension overrides (#1646) | +| DIMENSION (all 7 kinds) | yes | yes, with anonymous `*D` blocks; their text keeps the diameter sign as `%%c`, so SHX styles plot it (#1645) | a reopened dimension's `*D` block is regenerated under its name, unreferenced ones dropped (#495); DIMASSOC objects written | | LEADER | yes | yes | | | MULTILEADER | yes (#459) | yes, native MULTILEADER with MLEADERSTYLE (#459) | several landings per multileader are folded into one; per-object overrides and annotative scale contexts not kept | | HATCH pattern / solid | yes | yes; associative hatches keep their boundary objects in blocks too, and the boundary carries the hatch reactor (stretched dynamic blocks drag the fill in AutoCAD) | | @@ -140,6 +141,7 @@ our output in AutoCAD black-box. |---|---|---| | 2026-10-19 | minor | DXF/DWG save: angular dimension block text honors DIMDSEP, including per-dimension overrides (#1646) | | 2026-10-19 | minor | A file's own arrowhead blocks with standard names (AutoCAD's `_Dot`, `_DotSmall`), named only by a dimension style, are kept and saved as they were instead of being replaced by CADCraft's solids; PURGE keeps arrowhead blocks dimension styles name | +| 2026-10-20 | minor | DIMENSION: the `*D` block's MTEXT, and the MTEXT EXPLODE leaves, hold the diameter sign as `%%c` instead of the decoded U+2300, which no SHX font has, so other programs plotted diameter dimensions in an SHX style without the sign (#1645) | | 2026-10-17 | minor | DWG read: DGN line style components no line style definition reaches are left out before the DXF bridge, with a read note; an 83 MB site plan with 888,483 of them (1.2 GB of DXF, over the limit) now opens (#1482) | | 2026-10-12 | minor | PDF output: object transparency plotted as PDF alpha (`/ExtGState` `/ca` `/CA`) when the page setup's or the plot's "Plot transparency" is on; PNG plots blend it, SVG uses `opacity`; plots without it are unchanged byte for byte | | 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space |