mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 03:41:28 +00:00
Merge pull request #5685 from dotCooCoo/fix/quick-bounds-nested-nine-slice
Symbols: quick boxes hold art that nested 9-slice guides move
This commit is contained in:
@@ -212,7 +212,10 @@ impl Node {
|
||||
/// [`Node::reach_bounds`] with symbol instances measured by their symbol's art box from
|
||||
/// `symbols`. An instance that only moves its art is measured as the art's visual box moved,
|
||||
/// and any other instance is measured as its geometric box ([`Node::geometric_bounds_with`])
|
||||
/// grown by the farthest a stroke in the art can reach.
|
||||
/// grown by the farthest a stroke in the art can reach. For an instance placed through 9-slice
|
||||
/// guides, that box also holds the art box placed by each cell's map
|
||||
/// ([`crate::NineSliceMap::placement_bounds`]). An instance whose art holds an instance of a
|
||||
/// symbol with 9-slice guides at any depth is measured as its art placed by its transform.
|
||||
pub fn reach_bounds_with(&self, symbols: &SymbolBoxes) -> Option<Rect> {
|
||||
symbols.quickly(|| self.reach(Some(symbols)))
|
||||
}
|
||||
@@ -367,7 +370,7 @@ impl Node {
|
||||
Some((_, art)) if moves_only(*xf) => xf.transform_rect_bbox(art.visual),
|
||||
Some((s, art)) => {
|
||||
let placed = xf.transform_rect_bbox(art.geometric);
|
||||
match s.placed_visual(symbol, *xf) {
|
||||
match s.placed_visual(symbol, *xf).or_else(|| s.sliced_visual(symbol, *xf)) {
|
||||
Some(v) => v.union(placed),
|
||||
// Art placed through 9-slice guides can draw outside `placed`.
|
||||
None => s
|
||||
|
||||
+150
-3
@@ -23,6 +23,9 @@ const MAX_DEPTH: usize = 32;
|
||||
/// holding instances: past it, an estimate stands in ([`SymbolBoxes::effects_reach`]).
|
||||
const MAX_REACHES: usize = 4096;
|
||||
|
||||
/// A symbol's name and the bits of the linear part of a transform that places its art.
|
||||
type Placing = (String, [u64; 4]);
|
||||
|
||||
/// A symbol's art measured in symbol space.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct ArtBox {
|
||||
@@ -37,6 +40,9 @@ pub(crate) struct ArtBox {
|
||||
/// The widest stroke of the objects with a visible brushed stroke in the art, or in the art
|
||||
/// of a symbol an instance in it shows: `None` when no object there has one.
|
||||
pub(crate) brush: Option<f64>,
|
||||
/// Whether the art holds an instance of a symbol with 9-slice guides, or of a symbol whose art
|
||||
/// holds one at any depth.
|
||||
pub(crate) sliced: bool,
|
||||
}
|
||||
|
||||
/// The art boxes of a document's symbols, which [`Node::geometric_bounds_with`],
|
||||
@@ -73,6 +79,14 @@ pub struct SymbolBoxes<'a> {
|
||||
unplaced: RefCell<HashMap<String, f64>>,
|
||||
/// How many times [`Self::measure_art`] has run a measure on symbol art ([`Self::art_measured`]).
|
||||
runs: Cell<usize>,
|
||||
/// The boxes [`Self::sliced_visual`] has measured, by symbol name and the bits of the linear
|
||||
/// part of the transform that placed the art.
|
||||
sliced: RefCell<HashMap<Placing, Option<Rect>>>,
|
||||
/// How many objects of symbol art [`Self::sliced_visual`] has placed, the art of opacity masks
|
||||
/// included.
|
||||
sliced_placed: Cell<u64>,
|
||||
/// The most objects of symbol art [`Self::sliced_visual`] places: [`MAX_SYMBOL_NODES`].
|
||||
sliced_most: u64,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
@@ -90,6 +104,9 @@ impl Document {
|
||||
farthest: RefCell::default(),
|
||||
unplaced: RefCell::default(),
|
||||
runs: Cell::new(0),
|
||||
sliced: RefCell::default(),
|
||||
sliced_placed: Cell::new(0),
|
||||
sliced_most: MAX_SYMBOL_NODES,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +121,9 @@ impl<'a> SymbolBoxes<'a> {
|
||||
/// art box alone. An instance that scales or turns stroked art then measures a little larger
|
||||
/// than with [`Node::visual_bounds_with`], and its art is not transformed to measure it. The
|
||||
/// visual box of an instance placed through 9-slice guides holds the art box placed by each
|
||||
/// cell's map ([`crate::NineSliceMap::placement_bounds`]).
|
||||
/// cell's map ([`crate::NineSliceMap::placement_bounds`]). An instance whose art holds an
|
||||
/// instance of a symbol with 9-slice guides at any depth is measured as its art placed by its
|
||||
/// transform ([`Self::sliced_visual`]).
|
||||
pub fn quick(self) -> Self {
|
||||
self.quick.set(true);
|
||||
self
|
||||
@@ -321,13 +340,64 @@ impl<'a> SymbolBoxes<'a> {
|
||||
if self.quick.get() {
|
||||
return None;
|
||||
}
|
||||
self.place_and_measure(name, xf, most)
|
||||
}
|
||||
|
||||
/// [`Self::placed_visual`] in boxes made by [`Self::quick`] and while symbol art is measured
|
||||
/// too, for a symbol whose art holds an instance of a symbol with 9-slice guides at any depth:
|
||||
/// where that instance draws its art depends on the transform of each instance above it. The
|
||||
/// art is placed once for each symbol and linear part of `xf`, and the box is moved by the rest
|
||||
/// of `xf`. `None` for other symbols, when [`Self::placed_visual`] is `None` outside quick
|
||||
/// boxes, and when placing the art would take the objects of the art these boxes have placed
|
||||
/// for it past [`MAX_SYMBOL_NODES`], the art placed to measure the instances in placed art
|
||||
/// included. The renderer draws no more objects of symbol art than that in one frame.
|
||||
pub(crate) fn sliced_visual(&self, name: &str, xf: Affine) -> Option<Rect> {
|
||||
if !self.art(name)?.sliced {
|
||||
return None;
|
||||
}
|
||||
let [a, b, c, d, e, f] = xf.as_coeffs();
|
||||
let (linear, moved) = (Affine::new([a, b, c, d, 0.0, 0.0]), Affine::translate((e, f)));
|
||||
let key = (name.to_string(), [a, b, c, d].map(f64::to_bits));
|
||||
let cached = self.sliced.borrow().get(&key).copied();
|
||||
let v = match cached {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
// The most objects placing makes, the art of opacity masks included, is set aside
|
||||
// while the art is placed and measured, and what placing did not make is given back.
|
||||
let art = &self.doc.symbols.iter().find(|s| s.name == name)?.art;
|
||||
let most = match self.doc.symbol_slice_map(name, linear) {
|
||||
Some(m) => m.most_placed(art, self.doc),
|
||||
None => art.count_with_masks() as u64,
|
||||
};
|
||||
let used = self.sliced_placed.get();
|
||||
if used.saturating_add(most) > self.sliced_most {
|
||||
return None;
|
||||
}
|
||||
self.sliced_placed.set(used + most);
|
||||
let (v, placed) = self.place_and_count(name, linear, MAX_SYMBOL_NODES).unwrap_or((None, 0));
|
||||
self.sliced_placed.set(self.sliced_placed.get().saturating_sub(most.saturating_sub(placed)));
|
||||
self.sliced.borrow_mut().insert(key, v);
|
||||
v
|
||||
}
|
||||
};
|
||||
v.map(|v| moved.transform_rect_bbox(v))
|
||||
}
|
||||
|
||||
/// The box [`Self::placed_visual_within`] returns outside quick boxes.
|
||||
fn place_and_measure(&self, name: &str, xf: Affine, most: u64) -> Option<Rect> {
|
||||
self.place_and_count(name, xf, most).and_then(|(v, _)| v)
|
||||
}
|
||||
|
||||
/// [`Self::place_and_measure`], with the number of objects of the placed art, the art of
|
||||
/// opacity masks included. `None` when the art is not placed.
|
||||
fn place_and_count(&self, name: &str, xf: Affine, most: u64) -> Option<(Option<Rect>, u64)> {
|
||||
self.measure_art(name, |art| {
|
||||
let art = match self.doc.symbol_slice_map(name, xf) {
|
||||
Some(m) if m.most_placed(art, self.doc) > most => return None,
|
||||
Some(m) => m.place(art, self.doc),
|
||||
None => self.doc.place_symbol_art(name, art, xf)?,
|
||||
};
|
||||
self.in_art(|| art.visual(Some(self)))
|
||||
Some((self.in_art(|| art.visual(Some(self))), art.count_with_masks() as u64))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
@@ -385,6 +455,7 @@ fn art_box(art: &Arc<Node>, symbols: &SymbolBoxes) -> Option<ArtBox> {
|
||||
let reach = crate::live::max_outset(std::slice::from_ref(art), Some(symbols));
|
||||
let mut effects = false;
|
||||
let mut brush: Option<f64> = None;
|
||||
let mut sliced = false;
|
||||
art.walk(&mut |n| {
|
||||
let mut own = n.appearance.effects.iter().chain(n.appearance.items.iter().flat_map(|i| i.effects().iter()));
|
||||
effects |= own.any(|e| e.visible);
|
||||
@@ -397,9 +468,10 @@ fn art_box(art: &Arc<Node>, symbols: &SymbolBoxes) -> Option<ArtBox> {
|
||||
if let Some(w) = symbols.brush_width(symbol) {
|
||||
brush = Some(brush.map_or(w, |b| b.max(w)));
|
||||
}
|
||||
sliced |= symbols.doc.symbol_nine_slice(symbol).is_some() || symbols.art(symbol).is_some_and(|a| a.sliced);
|
||||
}
|
||||
});
|
||||
Some(ArtBox { geometric, visual, reach, effects, brush })
|
||||
Some(ArtBox { geometric, visual, reach, effects, brush, sliced })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -468,6 +540,81 @@ mod tests {
|
||||
assert_eq!(quick.union(exact), quick, "{quick:?} holds {exact:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_boxes_hold_art_that_nested_9_slice_guides_move_past_the_instance() {
|
||||
// The panel of the test above, in a symbol without guides: once at its natural size and
|
||||
// once scaled 2 times across. Where the bar draws depends on the transform the holder's
|
||||
// instance adds to the panel's.
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let bar = rect(&mut d, Rect::new(10.0, 40.0, 70.0, 60.0), 0.0);
|
||||
symbol(&mut d, "Bar", vec![bar]);
|
||||
let frame = rect(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), 0.0);
|
||||
let inner = instance(&mut d, "Bar", Affine::IDENTITY);
|
||||
symbol(&mut d, "Panel", vec![frame, inner]);
|
||||
d.set_symbol_nine_slice("Panel", Some(crate::SymbolNineSlice::default()));
|
||||
let natural = instance(&mut d, "Panel", Affine::IDENTITY);
|
||||
symbol(&mut d, "Holder", vec![natural]);
|
||||
let wide = instance(&mut d, "Panel", Affine::scale_non_uniform(2.0, 1.0));
|
||||
symbol(&mut d, "Wide holder", vec![wide]);
|
||||
// The holder scaled 2 times across scales the panel 2 times across: the bar draws from x
|
||||
// −20 to 160, as in the test above.
|
||||
let holder = instance(&mut d, "Holder", Affine::scale_non_uniform(2.0, 1.0));
|
||||
let quick = holder.visual_bounds_with(&d.symbol_boxes().quick()).unwrap();
|
||||
let drawn = Rect::new(-20.0, 40.0, 160.0, 60.0);
|
||||
assert_eq!(quick.union(drawn), quick, "{quick:?} holds {drawn:?}");
|
||||
let wide_holder = instance(&mut d, "Wide holder", Affine::scale(2.0));
|
||||
for n in [&holder, &wide_holder] {
|
||||
let exact = n.visual_bounds_with(&d.symbol_boxes()).unwrap();
|
||||
assert!(exact.x0 < 0.0, "{exact:?} reaches past the frame's left edge");
|
||||
let quick = n.visual_bounds_with(&d.symbol_boxes().quick()).unwrap();
|
||||
assert_eq!(quick.union(exact), quick, "{quick:?} holds {exact:?}");
|
||||
let reach = n.reach_bounds_with(&d.symbol_boxes()).unwrap();
|
||||
assert_eq!(reach.union(exact), reach, "{reach:?} holds {exact:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_boxes_place_art_that_holds_9_slice_instances_a_bounded_number_of_times() {
|
||||
// A frame with guides, and 16 levels of symbols that each hold an instance of the level
|
||||
// below a little taller and one turned a little: the transforms that place the frame differ
|
||||
// for each of the 2^16 ways down, so measuring every one would place art 2^17 times.
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let frame = rect(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), 0.0);
|
||||
symbol(&mut d, "Level 0", vec![frame]);
|
||||
d.set_symbol_nine_slice("Level 0", Some(crate::SymbolNineSlice::default()));
|
||||
let (taller, turned) = (Affine::scale_non_uniform(1.0, 1.01), Affine::rotate(0.1));
|
||||
for i in 1..=16 {
|
||||
let below = format!("Level {}", i - 1);
|
||||
let pair = vec![instance(&mut d, &below, taller), instance(&mut d, &below, turned)];
|
||||
symbol(&mut d, &format!("Level {i}"), pair);
|
||||
}
|
||||
let top = instance(&mut d, "Level 16", taller);
|
||||
// With room for 5,000 objects: each placement places at least one.
|
||||
let s = SymbolBoxes { sliced_most: 5000, ..d.symbol_boxes().quick() };
|
||||
assert!(top.visual_bounds_with(&s).is_some());
|
||||
assert!(s.sliced_placed.get() <= 5000, "{} objects", s.sliced_placed.get());
|
||||
assert!(s.art_measured() <= 5000 + d.symbols.len(), "{} measures", s.art_measured());
|
||||
// Instances whose transforms differ only by a move place the art once.
|
||||
let level = instance(&mut d, "Level 1", taller);
|
||||
let moved = instance(&mut d, "Level 1", Affine::translate((5.0, 7.0)) * taller);
|
||||
let s = d.symbol_boxes().quick();
|
||||
let b = level.visual_bounds_with(&s).unwrap();
|
||||
let measured = s.art_measured();
|
||||
assert_eq!(moved.visual_bounds_with(&s), Some(Affine::translate((5.0, 7.0)).transform_rect_bbox(b)));
|
||||
assert_eq!(s.art_measured(), measured);
|
||||
// The art of an opacity mask counts toward the limit: a group holding the framed
|
||||
// instance, masked by a group of 10 rectangles, is 13 objects.
|
||||
let mut framed = instance(&mut d, "Level 0", Affine::IDENTITY);
|
||||
let squares = (0..10).map(|_| Arc::new(rect(&mut d, Rect::new(0.0, 0.0, 10.0, 10.0), 0.0))).collect();
|
||||
let mask = Node::group(d.alloc_id(), squares);
|
||||
framed.extras_mut().mask = Some(Box::new(crate::OpacityMask::new(mask, true)));
|
||||
symbol(&mut d, "Masked", vec![framed]);
|
||||
let masked = instance(&mut d, "Masked", taller);
|
||||
let s = d.symbol_boxes().quick();
|
||||
assert!(masked.visual_bounds_with(&s).is_some());
|
||||
assert_eq!(s.sliced_placed.get(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_symbols_and_symbols_inside_themselves_measure_as_the_square() {
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
|
||||
@@ -399,6 +399,28 @@ fn an_instance_is_culled_by_the_art_its_9_slice_guides_move() {
|
||||
assert_eq!(img.pixel(95, 50), [255, 0, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instance_is_culled_by_the_art_nested_9_slice_guides_move() {
|
||||
// The panel of the test above at its natural size, in a symbol without guides. The holder's
|
||||
// instance scales the panel 2 times across and moves it 110 pt right: the bar draws from x 90
|
||||
// to 270 while the frame starts at x 110, out of view.
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let bar = rect_node(&mut d, Rect::new(10.0, 40.0, 70.0, 60.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Bar".into(), art: Arc::new(bar) });
|
||||
let frame = rect_node(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), Paint::None, Paint::None, 0.0);
|
||||
let inner = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Bar".into(), xf: Affine::IDENTITY });
|
||||
let art = Node::group(d.alloc_id(), vec![Arc::new(frame), Arc::new(inner)]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Panel".into(), art: Arc::new(art) });
|
||||
d.set_symbol_nine_slice("Panel", Some(vectorcraft_doc::SymbolNineSlice::default()));
|
||||
let panel = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Panel".into(), xf: Affine::IDENTITY });
|
||||
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Holder".into(), art: Arc::new(panel) });
|
||||
let (id, l) = (d.alloc_id(), d.layers[0].id);
|
||||
let xf = Affine::translate((110.0, 0.0)) * Affine::scale_non_uniform(2.0, 1.0);
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Holder".into(), xf })).unwrap();
|
||||
let img = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default());
|
||||
assert_eq!(img.pixel(95, 50), [255, 0, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instance_with_a_geometry_effect_is_culled_by_its_reshaped_art() {
|
||||
// The art, (0, 0) to (20, 20), drawn from (-100, 10) is out of view, and the Transform effect
|
||||
|
||||
Reference in New Issue
Block a user