3D: a symbol's 9-slice guides and overrides count before its art is placed

The check that refuses a symbol instance before its art is placed
counted only the symbol's own objects. Placing art through 9-slice
guides can make up to 28 objects of each one, and an instance's
dynamic-symbol overrides can turn a path that would be cut at the
guides into clip groups in every cell it reaches. Such an instance
passed the check, and its art was placed in full before the work
limit refused it.

The check now counts the symbol's own objects first, then the most
objects placing makes (NineSliceMap::most_placed) of the art with the
instance's overrides applied, and refuses the instance with the same
message before anything is placed.
This commit is contained in:
dotCooCoo
2026-11-10 14:24:33 -08:00
parent 349385fd56
commit 7dfe2d9d2a
2 changed files with 63 additions and 8 deletions
+15 -8
View File
@@ -129,16 +129,23 @@ impl Resolver<'_> {
}
self.spend(1)?;
match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => {
// Counted before the art is placed, so a symbol past the limit is never copied.
let held = self.doc.and_then(|d| d.symbols.iter().find(|s| s.name == *symbol)).map_or(0, |s| s.art.count_with_masks());
if self.work.saturating_add(held) > MAX_WORK {
NodeKind::SymbolInstance { symbol, xf } => {
let missing = || format!("{label} can't build a solid from the instance of symbol \"{symbol}\": the symbol's art isn't available");
let doc = self.doc.ok_or_else(missing)?;
let sym = doc.symbols.iter().find(|s| s.name == *symbol).ok_or_else(missing)?;
// Counted before the art is placed, so a symbol past the limit is never copied:
// first its own objects, which the instance's overrides keep, then the most
// objects placing makes, with those overrides and the symbol's 9-slice guides.
if self.work.saturating_add(sym.art.count_with_masks()) > MAX_WORK {
return Err(self.too_much_work());
}
let art = crate::outline_object(self.doc, n).ok_or_else(|| {
format!("{label} can't build a solid from the instance of symbol \"{symbol}\": the symbol's art isn't available")
})?;
Ok(self.list(kids(&art), depth + 1)?.0)
let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
let most = doc.symbol_slice_map(symbol, *xf).map_or(art.count_with_masks() as u64, |m| m.most_placed(&art, doc));
if (self.work as u64).saturating_add(most) > MAX_WORK as u64 {
return Err(self.too_much_work());
}
let placed = doc.place_symbol_art(symbol, &art, *xf).ok_or_else(missing)?;
Ok(self.list(&[Arc::new(placed)], depth + 1)?.0)
}
NodeKind::Blend { children, spec } => {
// Counted before the steps are made, so a blend past the limit is never expanded.
@@ -375,6 +375,54 @@ fn a_symbol_holding_more_objects_than_the_work_limit_is_refused_before_its_art_i
assert!(work <= 2, "refused before the symbol's art is placed and listed: {work}");
}
/// A gradient fill: a path filled with one is drawn in clip groups where it crosses 9-slice guides.
fn gradient() -> Paint {
use vectorcraft_doc::color::{Gradient, GradientPaint};
Paint::Gradient(Box::new(GradientPaint::new(Gradient::default())))
}
/// A document whose symbol "Sliced" has 9-slice guides and holds `count` paths across all of them,
/// each filled with `fill`, and an Extrude on an instance of it at twice its size.
fn sliced(count: usize, fill: impl Fn() -> Paint) -> (Document, Node) {
let mut d = document();
let members = (0..count).map(|i| {
Arc::new(Node::path(NodeId(i as u64 + 1), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::basic(fill(), Paint::None, 0.0)))
});
d.symbols.push(Symbol { dynamic: true, name: "Sliced".into(), art: Arc::new(Node::group(NodeId(0), members.collect())) });
d.set_symbol_nine_slice("Sliced", Some(vectorcraft_doc::SymbolNineSlice::default()));
let n = with(instance(30, "Sliced", Affine::scale(2.0)), EXTRUDE, json!({}));
(d, n)
}
#[test]
fn a_symbol_whose_9_slice_guides_would_place_too_many_objects_is_refused_before_its_art_is_placed() {
// Each gradient path becomes a group of nine clip groups, so the art placed is far past the
// limit while the symbol itself holds less than a tenth of it.
let (d, n) = sliced(source::MAX_WORK / 20, gradient);
assert!(d.symbols[1].art.count_with_masks() < source::MAX_WORK);
let err = validate_three_d_in(Some(&d), &n, EXTRUDE, &json!({})).unwrap_err();
assert!(err.contains("hold too many objects"), "{err}");
let work = source::WORK.with(std::cell::Cell::get);
assert!(work <= 2, "refused before the symbol's art is placed and listed: {work}");
}
#[test]
fn a_dynamic_symbol_whose_overrides_would_place_too_many_objects_is_refused_before_its_art_is_placed() {
// Solid paths are cut at the guides and stay one path each; the instance's overrides fill them
// with a gradient, which places each in nine clip groups.
let (d, mut n) = sliced(source::MAX_WORK / 20, || Paint::solid(Color::BLACK));
n.extras_mut().symbol_overrides = (0..source::MAX_WORK / 20)
.map(|i| vectorcraft_doc::SymbolOverride { path: vec![i as u32], fill: Some(gradient()), ..Default::default() })
.collect();
let err = validate_three_d_in(Some(&d), &n, EXTRUDE, &json!({})).unwrap_err();
assert!(err.contains("hold too many objects"), "{err}");
let work = source::WORK.with(std::cell::Cell::get);
assert!(work <= 2, "refused before the instance's art is placed and listed: {work}");
// A few such paths build.
let (d, n) = sliced(4, || Paint::solid(Color::BLACK));
assert!(solid_art_in(Some(&d), &n).is_some());
}
#[test]
fn empty_sources_and_sources_inside_sources_build_or_refuse_cleanly() {
let mut d = document();