Diameter dimensions: the *D block's text keeps the sign as %%c (fixes #1645) (#1696)

The *D block and EXPLODE's MTEXT took the dimension's decoded text, so
%%c arrived as U+2300, which no SHX font has: other programs plotted a
diameter dimension in an SHX style without its sign. They now write
DimGeometry::mtext_contents(): the sign as %%c, which every font draws
(and CADCraft decodes), a % that would run into a code as %%%.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Books
2026-10-24 01:53:32 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent a0dced6d45
commit a94fc0898b
5 changed files with 114 additions and 3 deletions
+1 -1
View File
@@ -1403,7 +1403,7 @@ pub(crate) fn explode_kind(d: &cadcraft_doc::Drawing, e: &Entity) -> Option<Vec<
attach: 5, attach: 5,
rotation: g.text_angle, rotation: g.text_angle,
style: style.text_style.clone(), style: style.text_style.clone(),
contents: g.value.clone(), contents: g.mtext_contents(),
line_spacing: 1.0, line_spacing: 1.0,
line_spacing_exact: false, line_spacing_exact: false,
columns: None, columns: None,
+70
View File
@@ -0,0 +1,70 @@
//! #1645: a diameter dimension's text in its `*D` block, and in the MTEXT EXPLODE leaves, holds
//! the diameter sign as `%%c`, as CADCraft's own TEXT and MTEXT keep it. The decoded ⌀ (U+2300)
//! is in no SHX font, so other programs plotted the dimension in an SHX style ("romans.shx")
//! without its sign. CADCraft still draws and reads the sign.
use cadcraft_engine::Session;
use cadcraft_engine::doc::{EntityKind, Handle};
use serde_json::{Value, json};
fn h(v: &Value) -> 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<String> {
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<String> =
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");
}
+1 -1
View File
@@ -1424,7 +1424,7 @@ fn dim_block_entities(d: &Drawing, dm: &Dimension, layer: &str) -> Vec<Entity> {
attach: 5, attach: 5,
rotation: g.text_angle, rotation: g.text_angle,
style: style.text_style.clone(), style: style.text_style.clone(),
contents: g.value.clone(), contents: g.mtext_contents(),
line_spacing: 1.0, line_spacing: 1.0,
line_spacing_exact: false, line_spacing_exact: false,
columns: None, columns: None,
+39
View File
@@ -53,6 +53,22 @@ pub struct DimGeometry {
} }
impl 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<Vec2>) { fn dim(&mut self, l: Vec<Vec2>) {
if l.len() >= 2 { if l.len() >= 2 {
self.lines.push(l); self.lines.push(l);
@@ -1381,6 +1397,7 @@ mod tests {
d.defpt = Vec3::new(-3.0, 0.0, 0.0); d.defpt = Vec3::new(-3.0, 0.0, 0.0);
let g = dimension_geometry(&d, &st_with(|s| s.decimals = 1), 1.0); let g = dimension_geometry(&d, &st_with(|s| s.decimals = 1), 1.0);
assert_eq!(g.value, "⌀6.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); let mut a = lin((10.0, 0.0), (0.0, 10.0), (3.0, 3.0), 0.0);
a.kind = DimKind::Angular3P; a.kind = DimKind::Angular3P;
a.p15 = Vec3::ZERO; a.p15 = Vec3::ZERO;
@@ -1430,6 +1447,28 @@ mod tests {
let g = dimension_geometry(&d, &DimStyle::default(), 1.0); let g = dimension_geometry(&d, &DimStyle::default(), 1.0);
assert!(g.lines.is_empty() && g.fills.is_empty(), "{:?}", g.lines); 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)] #[cfg(test)]
+3 -1
View File
@@ -1,6 +1,6 @@
# File-format parity # 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 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 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 | | 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) | | 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 | 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 | | | 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 | | 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) | | | 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 | 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-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-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 | 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 | | 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 |