fix(io): write DXF CLASS instance counts and preserve current leader style (#635)

Squash of PR #635 by @The-Great-Nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
MuhiandClaude Opus 5.5 authored and GitHub committed 2026-10-11 19:53:33 -04:00
1 parent 4e276db858
commit 2fa967db65
5 files changed
+65 -7

No files matched your search

+4 -1
View File
@@ -6,7 +6,8 @@
//!
//! CADCraft's own data lives under the registered application `CADCRAFT` (xdata) and the
//! named-object-dictionary entries `CADCRAFT_CONSTRAINTS`, `CADCRAFT_LAYERSTATES`,
//! `CADCRAFT_PLOTSTYLES` and `CADCRAFT_HANDSEED` (XRECORDs). Other readers keep or ignore them.
//! `CADCRAFT_PLOTSTYLES`, `CADCRAFT_HANDSEED` and `CADCRAFT_MLEADERSTYLE` (XRECORDs). Other
//! readers keep or ignore them.
use std::collections::HashMap;
@@ -32,6 +33,8 @@ pub(crate) const ANNOTATION_KEY: &str = "CADCRAFT_ANNOTATION";
pub(crate) const PLOT_STYLE_NAMES_KEY: &str = "ACAD_PLOTSTYLENAME";
/// Named-object-dictionary key of the XRECORD holding the drawing's next entity handle (hex).
pub(crate) const HANDSEED_KEY: &str = "CADCRAFT_HANDSEED";
/// Named-object-dictionary key of the XRECORD holding the current multileader style.
pub(crate) const MLEADER_STYLE_KEY: &str = "CADCRAFT_MLEADERSTYLE";
/// Most plot style tables and plot style names read from a file.
pub(crate) const MAX_PLOT_STYLE_TABLES: usize = 1000;
pub(crate) const MAX_PLOT_STYLE_NAMES: usize = 100_000;
+3
View File
@@ -1843,6 +1843,9 @@ impl Objects {
}
fn apply(self, d: &mut Drawing, rx: &Rx) {
if let Some(style) = self.xrecord_text(crate::dxf_ext::MLEADER_STYLE_KEY).filter(|s| !s.trim().is_empty()) {
d.header.set_str("CMLEADERSTYLE", &style);
}
// Table styles (named by their ACAD_TABLESTYLE dictionary entries).
for (h, tags) in &self.table_styles {
let Some(name) = self.names.get(h).filter(|n| !n.is_empty()) else { continue };
+38 -5
View File
@@ -1555,6 +1555,9 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
// The drawing's own next handle: table records and objects are numbered above it, so
// reading it back (instead of $HANDSEED) keeps their handles across saves (#495).
xrecords.push((dxf_ext::HANDSEED_KEY, w.h(), vec![format!("{:X}", d.handseed)]));
if let Some(name) = d.header.get("CMLEADERSTYLE").and_then(HVal::as_str) {
xrecords.push((dxf_ext::MLEADER_STYLE_KEY, w.h(), vec![name.to_string()]));
}
if let Some(chunks) = dxf_ext::constraint_chunks(&d.constraints, &d.parametric) {
xrecords.push((dxf_ext::CONSTRAINTS_KEY, w.h(), chunks));
}
@@ -1601,11 +1604,6 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
w.p(10, ext.max.to3(0.0));
}
header_vars(&mut w, d);
// The current multileader style (an R2007 variable; older readers skip it).
if let Some(name) = d.header.get("CMLEADERSTYLE").and_then(HVal::as_str) {
w.s(9, "$CMLEADERSTYLE");
w.s(2, name);
}
let seed_pos = w.t.len();
w.s(9, "$HANDSEED");
w.s(5, "0");
@@ -2255,10 +2253,45 @@ pub fn tags(d: &Drawing, version: Version) -> Vec<Tag> {
*t = Tag::s(5, seed);
}
crate::dxf_raw::finish(&mut w.t, d);
add_class_instance_counts(&mut w.t);
mark_xrefs(&mut w.t, d);
w.t
}
/// Write DXF Reference group 91 on every CLASS record. Count records after `dxf_raw::finish`
/// restores foreign classes and objects so every class gets its actual instance count.
fn add_class_instance_counts(tags: &mut Vec<Tag>) {
let mut counts: HashMap<String, i64> = HashMap::new();
for t in tags.iter().filter(|t| t.code == 0) {
let kind = t.str();
if kind != "CLASS" {
*counts.entry(kind.to_ascii_uppercase()).or_default() += 1;
}
}
let mut start = 0;
while start < tags.len() {
if tags.get(start).is_none_or(|t| t.code != 0 || t.str() != "CLASS") {
start += 1;
continue;
}
let end = tags[start + 1..].iter().position(|t| t.code == 0).map_or(tags.len(), |i| start + 1 + i);
let name = tags[start + 1..end].iter().find(|t| t.code == 1).map(Tag::str).unwrap_or_default().to_ascii_uppercase();
let count = counts.get(&name).copied().unwrap_or(0);
let inserted = if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 91) {
tags[start + 1 + i] = Tag::i(91, count);
false
} else if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 90) {
tags.insert(start + 2 + i, Tag::i(91, count));
true
} else {
false
};
// Inserting the group shifts the next record's start by one.
start = end + usize::from(inserted);
}
}
/// Xref blocks (DXF Reference, BLOCK): bit 4 of group 70 and the external drawing's path in
/// group 1, set on the written BLOCK records of blocks that have an `xref_path`.
fn mark_xrefs(tags: &mut [Tag], d: &Drawing) {
+15
View File
@@ -878,6 +878,21 @@ fn saving_twice_writes_the_same_records() {
assert_eq!(a.counts(), b.counts());
}
#[test]
fn class_records_have_instance_counts_for_generated_and_preserved_records() {
let (_, a, b) = saves(&fixture());
for out in [&a, &b] {
let counts = out.counts();
for class in out.in_section("CLASSES", "CLASS") {
let name = s(class, 1);
let instance_count = class.iter().find(|t| t.code == 91).map(Tag::i64);
assert_eq!(instance_count, counts.get(&name).map(|n| *n as i64), "CLASS {name}");
}
assert!(out.in_section("CLASSES", "CLASS").iter().any(|c| s(c, 1) == "ACME_OBJECT"));
assert_eq!(counts.get("ACME_OBJECT"), Some(&1));
}
}
#[test]
fn erased_entities_take_their_extension_data_with_them() {
let mut d = read_dxf(fixture().as_bytes()).expect("read");
+5 -1
View File
@@ -73,7 +73,11 @@ fn multileader_styles_and_current_style_round_trip() {
};
d.mleader_styles.push(callout);
d.header.set_str("CMLEADERSTYLE", "Callout");
let r = round_trip(&d);
let text = write_dxf(&d);
let tags = cadcraft_dxf::parse(text.as_bytes()).expect("parse written DXF");
let header = cadcraft_dxf::sections(&tags).into_iter().find(|s| s.name == "HEADER").expect("HEADER");
assert!(header.tags.iter().all(|t| t.code != 9 || t.str() != "$CMLEADERSTYLE"));
let r = read_dxf(text.as_bytes()).expect("reopen");
assert_eq!(r.mleader_styles, d.mleader_styles);
assert_eq!(r.header.str("CMLEADERSTYLE", ""), "Callout");
}