From 23bc1c00279da6d5944b19c020bb406cfd839422 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:06:46 +0000 Subject: [PATCH] Fix quadratic paper layout de-duplication in DXF import Fixes #3595 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/cad/src/import/reader.rs | 3 ++- crates/cad/src/import/tests.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/cad/src/import/reader.rs b/crates/cad/src/import/reader.rs index 1d83475c3..a2d1dc11d 100644 --- a/crates/cad/src/import/reader.rs +++ b/crates/cad/src/import/reader.rs @@ -423,6 +423,7 @@ fn font_family(g: &[Pair<'_>]) -> String { /// objects but with paper-space art has one paper layout. fn layouts(objects: &[Record<'_>], block_records: &HashMap, entities: &[Record<'_>]) -> Vec { let mut paper: Vec<(i64, LayoutDef)> = vec![]; + let mut seen = std::collections::HashSet::new(); for r in objects.iter().filter(|r| r.kind == "LAYOUT") { // The layout's own groups follow its subclass marker. let own = r.g.iter().position(|p| p.code == 100 && p.value.trim() == "AcDbLayout").and_then(|i| r.g.get(i + 1..)).unwrap_or(&[]); @@ -432,7 +433,7 @@ fn layouts(objects: &[Record<'_>], block_records: &HashMap, enti continue; } let Some(block) = block else { continue }; - if !paper.iter().any(|(_, l)| l.name.eq_ignore_ascii_case(name)) { + if seen.insert(name.to_ascii_lowercase()) { paper.push((own.int_or(71, i64::MAX), LayoutDef { name: name.to_string(), block: Some(block) })); } } diff --git a/crates/cad/src/import/tests.rs b/crates/cad/src/import/tests.rs index 4845e8d2a..a7bc39ceb 100644 --- a/crates/cad/src/import/tests.rs +++ b/crates/cad/src/import/tests.rs @@ -1061,3 +1061,32 @@ fn entities_extruded_along_minus_z_are_mirrored() { let b = bounds(&leaves(&doc)[0]); assert!((b.center().x + 50.0).abs() < 1e-6, "the centre's x is mirrored: {b:?}"); } + +/// A drawing with one paper block record (handle `A`) and a LAYOUT object per name. +fn with_layouts(names: &[String]) -> Vec { + let mut s = String::from( + " 0\nSECTION\n 2\nTABLES\n 0\nTABLE\n 2\nBLOCK_RECORD\n 0\nBLOCK_RECORD\n 5\nA\n 2\n*Paper_Space\n 0\nENDTAB\n 0\nENDSEC\n 0\nSECTION\n 2\nOBJECTS\n", + ); + for n in names { + s.push_str(&format!(" 0\nLAYOUT\n100\nAcDbLayout\n 1\n{n}\n330\nA\n")); + } + s.push_str(" 0\nENDSEC\n 0\nEOF\n"); + s.into_bytes() +} + +#[test] +fn many_paper_layouts_are_read_in_linear_time() { + let names: Vec = (0..100_000).map(|i| format!("L{i}")).collect(); + let bytes = with_layouts(&names); + let t = std::time::Instant::now(); + let info = info(&bytes).unwrap(); + assert_eq!(info.layouts.len(), names.len() + 1); + assert!(t.elapsed() < std::time::Duration::from_secs(5), "layout de-duplication is quadratic: {:?}", t.elapsed()); +} + +#[test] +fn paper_layout_names_are_unique_ignoring_case() { + let names = ["Layout1", "LAYOUT1", "Other"].map(String::from); + let info = info(&with_layouts(&names)).unwrap(); + assert_eq!(info.layouts, ["Model", "Layout1", "Other"]); +}