From 86dbc42d42f8f26fe09eee240168c2e87c514a38 Mon Sep 17 00:00:00 2001 From: theironchef <6278036+theironchef@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:14:39 -0700 Subject: [PATCH] DXF import: a hatch spline edge without fit data keeps its loop A spline edge read an optional fit count (97) after its control points. Files before DXF 2010 have no fit data there, so the 97 that follows is the loop's count of source objects: the edge took it, then failed reading fit points where the 330 handles are, and the whole hatch was dropped without a warning. That is every associative hatch bounded by a spline from AutoCAD R2000 to R2007. A fit count is 0 or followed by fit points (11), and a source count by handles (330), so the edge now only takes the 97 when it is 0 or points follow. Fixes #175. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D --- crates/cad/src/import/entity.rs | 9 ++++++++- crates/cad/src/import/tests.rs | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/cad/src/import/entity.rs b/crates/cad/src/import/entity.rs index 124d196a..fe5b95f0 100644 --- a/crates/cad/src/import/entity.rs +++ b/crates/cad/src/import/entity.rs @@ -547,7 +547,14 @@ fn edge(c: &mut Cursor<'_, '_>) -> Option { ctrl.push(c.point(10)?); weights.push(c.num(42).unwrap_or(1.0)); } - let fit: Vec = match c.count(97) { + // Fit data (DXF 2010 and later): a count, then that many points. Older files have none, + // and the 97 that may follow is the boundary's count of source objects (then their + // handles), so it is only the fit count when it is 0 or points follow. + let fit_count = match (c.g.get(c.i), c.g.get(c.i + 1)) { + (Some(count), next) if count.code == 97 => number(count.value) == Some(0.0) || next.is_some_and(|p| p.code == 11), + _ => false, + }; + let fit: Vec = match fit_count.then(|| c.count(97)).flatten() { Some(n) => (0..n).map(|_| c.point(11)).collect::>()?, None => vec![], }; diff --git a/crates/cad/src/import/tests.rs b/crates/cad/src/import/tests.rs index 5a4b1ec6..a2ed7633 100644 --- a/crates/cad/src/import/tests.rs +++ b/crates/cad/src/import/tests.rs @@ -319,6 +319,41 @@ fn solid_hatches_fill_and_pattern_hatches_outline() { assert!(square.appearance.fill().is_some_and(|f| !f.paint.is_none())); } +/// A solid hatch: a loop of a line and a spline edge (`fit` its fit data, if any), followed by +/// `source` (the loop's source-object count and handles), and a square hole. +fn spline_edged_hatch(fit: &[(i32, &'static str)], source: &[(i32, &'static str)]) -> Vec<(i32, &'static str)> { + let mut e = vec![(0, "HATCH"), (62, "1"), (10, "0"), (20, "0"), (30, "0"), (2, "SOLID"), (70, "1"), (71, "1"), (91, "2")]; + e.extend([(92, "1"), (93, "2"), (72, "1"), (10, "0"), (20, "0"), (11, "100"), (21, "0")]); + e.extend([(72, "4"), (94, "3"), (73, "0"), (74, "0"), (95, "8"), (96, "4")]); + e.extend(["0", "0", "0", "0", "1", "1", "1", "1"].map(|k| (40, k))); + e.extend([(10, "100"), (20, "0"), (10, "100"), (20, "100"), (10, "0"), (20, "100"), (10, "0"), (20, "0")]); + e.extend_from_slice(fit); + e.extend_from_slice(source); + e.extend([(92, "2"), (72, "0"), (73, "1"), (93, "4"), (10, "40"), (20, "20"), (10, "60"), (20, "20")]); + e.extend([(10, "60"), (20, "40"), (10, "40"), (20, "40"), (97, "0"), (75, "0"), (76, "1"), (98, "0")]); + e +} + +#[test] +fn hatch_spline_edges_with_or_without_fit_data() { + // Before DXF 2010 a spline edge has no fit data, so a 97 right after it is the loop's own + // count of source objects; from 2010 on it has a fit count, here 0, before that. + let source = [(97, "1"), (330, "1F")]; + for fit in [&[][..], &[(97, "0")][..]] { + let r = open(&entities(&spline_edged_hatch(fit, &source)), &points()); + let [hatch] = &r.document.layers[0].children().unwrap()[..] else { panic!("fit data {fit:?}: the hatch is lost") }; + let NodeKind::Compound { children, .. } = &hatch.kind else { panic!("{:?}", hatch.kind) }; + assert_eq!(children.len(), 2, "the outline and the hole, fit data {fit:?}"); + // The spline edge peaks three quarters of the way up its control polygon. + let b = bounds(hatch); + assert!((b.width() - 100.0).abs() < 1e-6 && (b.height() - 75.0).abs() < 1e-6, "{b:?}"); + } + // Fit points after the count are still read (and ignored when there are control points). + let fit = [(97, "2"), (11, "100"), (21, "0"), (11, "0"), (21, "0")]; + let r = open(&entities(&spline_edged_hatch(&fit, &source)), &points()); + assert_eq!(r.document.layers[0].children().unwrap().len(), 1); +} + #[test] fn text_and_multiline_text() { let tables = [(0, "TABLE"), (2, "STYLE"), (0, "STYLE"), (2, "Notes"), (3, "DejaVuSans.ttf"), (0, "ENDTAB")];