Merge pull request #6474 from dotCooCoo/fix/3d-symbol-work-limit-bound

3D: a symbol's 9-slice guides and overrides count before its art is placed
This commit is contained in:
BFlat
2026-11-11 06:27:59 +07:00
committed by GitHub
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();