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,
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,
+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,
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,
+39
View File
@@ -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<Vec2>) {
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)]