DXF export: charge symbol art as it is placed unclipped, test the segment budget

Count nine-slice symbol art with clips off, as place_symbol_art_unclipped
places it, so art crossing the guides is not over-charged; add a test that
the flattening segment budget stops a path independently of the point
budget; format.
This commit is contained in:
Dylan M. Taylor
2026-11-02 22:33:39 -05:00
parent f1d4969864
commit ed4a663986
3 changed files with 32 additions and 6 deletions
+21 -1
View File
@@ -567,7 +567,8 @@ impl<'a> Scene<'a> {
// empty or not: every instance's art counts against one budget for the export.
let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
let count = art.count_with_masks() as u64;
let most = doc.symbol_slice_map(symbol, *xf).map_or(count, |m| m.most_placed(&art, doc));
// Counted as `place_symbol_art_unclipped` places it: no clip groups.
let most = doc.symbol_slice_map(symbol, *xf).map_or(count, |m| m.most_placed_unclipped(&art, doc));
let placed = vectorcraft_doc::charge_symbol_art(&mut self.symbol_nodes, most, || {
let placed = doc.place_symbol_art_unclipped(symbol, &art, *xf);
let n = placed.as_ref().map_or(0, |a| a.count_with_masks() as u64);
@@ -1322,4 +1323,23 @@ mod budget_tests {
assert!(points <= MAX_POINTS, "{points} points");
assert!(!polys.iter().any(|p| p.contains(&Point::new(20.0, 20.0))), "the square past the budget is left out");
}
#[test]
fn flattening_a_path_of_repeated_points_stops_at_its_segment_budget() {
// Repeats of a point are dropped, so these keep one point but still count as segments:
// the segment budget, not the point budget, leaves out the square after them.
let mut bp = BezPath::new();
bp.move_to((0.0, 0.0));
for _ in 0..MAX_FLATTEN_SEGMENTS {
bp.line_to((0.0, 0.0));
}
bp.move_to((10.0, 10.0));
bp.line_to((20.0, 10.0));
bp.line_to((20.0, 20.0));
bp.close_path();
let polys = polygons(&bp);
let points: usize = polys.iter().map(Vec::len).sum();
assert!(points < MAX_POINTS, "{points} points");
assert!(!polys.iter().any(|p| p.contains(&Point::new(20.0, 20.0))), "the square past the budget is left out");
}
}
+2 -1
View File
@@ -1017,7 +1017,8 @@ fn symbols_fanning_out_into_empty_symbols_stop_at_the_symbol_budget() {
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "L0".into(), art: Arc::new(empty) });
for level in 1..=5 {
let below = format!("L{}", level - 1);
let children = (0..100).map(|_| Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: below.clone(), xf: Affine::IDENTITY }))).collect();
let children =
(0..100).map(|_| Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: below.clone(), xf: Affine::IDENTITY }))).collect();
let art = Node::group(d.alloc_id(), children);
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: format!("L{level}"), art: Arc::new(art) });
}
+9 -4
View File
@@ -364,6 +364,11 @@ impl NineSliceMap {
self.most_at(art, &boxes, 0)
}
/// [`Self::most_placed`] for [`Self::place_unclipped`]: the most objects it makes of `art`.
pub fn most_placed_unclipped(&self, art: &Node, doc: &Document) -> u64 {
Self { clips: false, ..*self }.most_placed(art, doc)
}
/// [`Self::most_placed`] for `n` at nesting depth `depth`, following [`Self::place_at`].
fn most_at(&self, n: &Node, boxes: &SymbolBoxes, depth: u32) -> u64 {
let whole = n.count_with_masks() as u64;
@@ -642,10 +647,7 @@ impl Document {
/// ([`NineSliceMap::place_unclipped`]).
pub fn place_symbol_art_unclipped(&self, name: &str, art: &Node, xf: Affine) -> Option<Node> {
match self.symbol_slice_map(name, xf) {
Some(m) => {
let m = NineSliceMap { clips: false, ..m };
(m.most_placed(art, self) <= MAX_SYMBOL_NODES).then(|| m.place(art, self))
}
Some(m) => (m.most_placed_unclipped(art, self) <= MAX_SYMBOL_NODES).then(|| m.place_unclipped(art, self)),
None => Some(transformed(art, xf)),
}
}
@@ -1072,6 +1074,9 @@ mod tests {
let bar = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 40.0, 100.0, 60.0)), Appearance::basic(gradient, Paint::None, 0.0));
assert_eq!(m.most_placed(&bar, &d), 10);
assert_eq!(m.place(&bar, &d).count(), 10);
// Unclipped, as DXF writes it, the bar is cut at the guides and stays one path.
assert_eq!(m.most_placed_unclipped(&bar, &d), 1);
assert_eq!(m.place_unclipped(&bar, &d).count(), 1);
// Art nested past the depth limit, clipped in all nine cells.
let mut deep = square(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), 0.0);
for _ in 0..=MAX_SLICE_DEPTH {