diff --git a/crates/cad/src/import/build.rs b/crates/cad/src/import/build.rs index ee14e81f5..eb24859f1 100644 --- a/crates/cad/src/import/build.rs +++ b/crates/cad/src/import/build.rs @@ -43,6 +43,7 @@ const NESTED: &str = "blocks nested more than 8 deep, or inside themselves, are const XREFS: &str = "blocks referencing other drawings (external references) are left out"; const TOO_MANY: &str = "the drawing has more than 2,000,000 objects: the rest are left out"; const TOO_MANY_POINTS: &str = "the drawing has more than 4,000,000 path points: the rest are left out"; +const TOO_MUCH_BLOCK_WORK: &str = "the drawing's blocks take too much work to place: the rest are left out"; const TOO_MANY_DASHES: &str = "the drawing has more dashed lines than can be kept: the rest are drawn solid"; const TOO_MUCH_TEXT: &str = "the drawing has more than 64 MB of text and block names: the rest are left out"; @@ -499,6 +500,9 @@ impl<'d, 'a> Build<'d, 'a> { super::curve::bounds(&bp)?; let paint = Paint::solid(rgb_color(self.rgb(p, inh))); let scale = map.determinant().abs().sqrt(); + let path = PathData::from_bezpath(&bp); + // A compound path's loops each keep a copy of its appearance, beside its own. + let copies = if path.subpaths.len() > 1 { path.subpaths.len() + 1 } else { 1 }; let appearance = if fill { Appearance::basic(paint, Paint::None, 0.0) } else { @@ -514,7 +518,7 @@ impl<'d, 'a> Build<'d, 'a> { if let Some(AppearanceItem::Stroke(s)) = ap.items.last_mut() && let Some((dash, dots)) = self.dash(&self.linetype(p, inh), p, scale) { - match self.dash_values.checked_add(dash.pattern.len()).filter(|n| *n <= MAX_DASH_VALUES) { + match dash.pattern.len().checked_mul(copies).and_then(|v| self.dash_values.checked_add(v)).filter(|n| *n <= MAX_DASH_VALUES) { Some(n) => { self.dash_values = n; s.dash = Some(dash); @@ -528,7 +532,6 @@ impl<'d, 'a> Build<'d, 'a> { ap }; let rule = if fill { FillRule::EvenOdd } else { FillRule::NonZero }; - let path = PathData::from_bezpath(&bp); let kind = |path: PathData| NodeKind::Path { path, rule, live: None, clipping: false, guide: false }; let mut n = if path.subpaths.len() > 1 { // Several loops (a hatch with islands) paint as one compound path. Each loop is an @@ -630,7 +633,7 @@ impl<'d, 'a> Build<'d, 'a> { /// The art of block `name` drawn by `map`, as one object. fn art(&mut self, name: &str, map: Affine, inh: &Inherit, depth: u32) -> Option { if self.visits >= MAX_VISITS { - self.warn(TOO_MANY); + self.warn(TOO_MUCH_BLOCK_WORK); return None; } let items = self.block(name); @@ -638,7 +641,7 @@ impl<'d, 'a> Build<'d, 'a> { // makes nothing, so the node budget doesn't bound it). self.visits = self.visits.saturating_add(1 + items.as_ref().map_or(0, |i| i.len())); if self.visits > MAX_VISITS { - self.warn(TOO_MANY); + self.warn(TOO_MUCH_BLOCK_WORK); return None; } let items = items?; @@ -671,12 +674,16 @@ impl<'d, 'a> Build<'d, 'a> { let base = self.d.blocks.get(name).map_or(Point::ZERO, |b| b.base); let map = self.symbol_map * Affine::translate(-base.to_vec2()); let art = self.art(name, map, &key.1, depth); - let made = art.map(|art| { + let made = art.and_then(|art| { let label = self.d.blocks.get(name).map_or(name, |b| b.name.as_str()); + // The symbol keeps its name: text, as its instances' copies of it are. + if !self.charge_text(label.len()) { + return None; + } let symbol = unique(label, &mut self.next_suffix, &self.symbol_names); self.symbol_names.insert(symbol.clone()); self.doc.symbols.push(Symbol { dynamic: false, name: symbol.clone(), art: Arc::new(art) }); - symbol + Some(symbol) }); self.symbols.insert(key, made.clone()); made diff --git a/crates/cad/src/import/tests.rs b/crates/cad/src/import/tests.rs index 978505ddf..d28a48553 100644 --- a/crates/cad/src/import/tests.rs +++ b/crates/cad/src/import/tests.rs @@ -814,7 +814,7 @@ fn nested_array_inserts_of_nothing_stop_at_the_work_budget() { blocks.extend(block("*C", None)); let top = [(0, "INSERT"), (2, "*A"), (10, "0"), (20, "0"), (70, "300"), (44, "1")]; let r = open(&drawing(&[], &[], &blocks, &top), &points()); - assert!(r.warnings.iter().any(|w| w.contains("more than")), "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains("too much work")), "{:?}", r.warnings); } #[test] @@ -1273,5 +1273,77 @@ fn arrays_of_blocks_of_hidden_art_stop_at_the_work_budget() { blocks.push((0, "ENDBLK")); let insert = [(0, "INSERT"), (2, "*U1"), (10, "0"), (20, "0"), (70, "50"), (71, "50"), (44, "1"), (45, "1")]; let r = open(&drawing(&[], &tables, &blocks, &insert), &points()); - assert!(r.warnings.iter().any(|w| w.contains("more than")), "{:?}", r.warnings); + assert!(r.warnings.iter().any(|w| w.contains("too much work")), "{:?}", r.warnings); +} + +#[test] +fn dashed_compound_paths_charge_every_copy_of_their_pattern() { + // 100 pattern hatches of 9 loops in a two-length linetype: each keeps its dashes on the + // compound path and on each loop, 2,000 dash lengths, past the 1,000 tests import with. + let tables = [(0, "TABLE"), (2, "LTYPE"), (0, "LTYPE"), (2, "DASHED"), (73, "2"), (40, "15"), (49, "10"), (49, "-5"), (0, "ENDTAB")]; + let corners = ["0", "10", "20", "30", "40", "50", "60", "70", "80", "90"]; + let mut hatch = vec![(0, "HATCH"), (6, "DASHED"), (10, "0"), (20, "0"), (30, "0"), (2, "ANSI31"), (70, "0"), (71, "0"), (91, "9")]; + for i in 0..9 { + let (a, b) = (corners[i], corners[i + 1]); + hatch.extend([ + (92, "2"), + (72, "0"), + (73, "1"), + (93, "4"), + (10, a), + (20, "0"), + (10, b), + (20, "0"), + (10, b), + (20, "5"), + (10, a), + (20, "5"), + (97, "0"), + ]); + } + hatch.extend([(75, "0"), (76, "1"), (98, "0")]); + let e: Vec<(i32, &str)> = (0..100).flat_map(|_| hatch.iter().copied()).collect(); + let r = open(&drawing(&[], &tables, &[], &e), &points()); + let (mut compounds, mut kept) = (0, 0); + r.document.walk(|n| { + compounds += usize::from(matches!(n.kind, NodeKind::Compound { .. })); + kept += n.appearance.stroke().and_then(|s| s.dash.as_ref()).map_or(0, |d| d.pattern.len()); + }); + assert_eq!(compounds, 100, "{:?}", r.warnings); + assert!(kept > 0 && kept <= 1_000, "{kept} dash lengths kept"); + assert!(r.warnings.iter().any(|w| w.contains("drawn solid")), "{:?}", r.warnings); +} + +#[test] +fn symbols_of_a_long_block_name_stop_at_the_text_budget() { + // A 1,000-byte block name inserted in 100 colours its art takes: a symbol per colour, each + // keeping its own copy of the name, past the 64 KB tests import with. + let name = "N".repeat(1000); + let blocks = [ + (0, "BLOCK"), + (2, name.as_str()), + (10, "0"), + (20, "0"), + (0, "LINE"), + (8, "0"), + (62, "0"), + (10, "0"), + (20, "0"), + (11, "1"), + (21, "0"), + (0, "ENDBLK"), + ]; + let colours: Vec = (1..=100).map(|c| c.to_string()).collect(); + let inserts: Vec<(i32, &str)> = + colours.iter().flat_map(|c| [(0, "INSERT"), (2, name.as_str()), (62, c.as_str()), (10, "0"), (20, "0")]).collect(); + let r = open(&drawing(&[], &[], &blocks, &inserts), &points()); + let mut bytes: usize = r.document.symbols.iter().map(|s| s.name.len()).sum(); + r.document.walk(|n| { + if let NodeKind::SymbolInstance { symbol, .. } = &n.kind { + bytes += symbol.len(); + } + }); + assert!(!r.document.symbols.is_empty()); + assert!(bytes <= 64 << 10, "{bytes} bytes of symbol names"); + assert!(r.warnings.iter().any(|w| w.contains("64 MB of text")), "{:?}", r.warnings); }