mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
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:
@@ -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,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user