Merge pull request #5625 from dotCooCoo/fix/quick-bounds-nine-slice

Symbols: the quick box of a 9-slice instance holds art its guides move
This commit is contained in:
BFlat
2026-11-04 22:24:41 +07:00
committed by GitHub
4 changed files with 69 additions and 2 deletions
+6 -1
View File
@@ -369,7 +369,12 @@ impl Node {
let placed = xf.transform_rect_bbox(art.geometric);
match s.placed_visual(symbol, *xf) {
Some(v) => v.union(placed),
None => placed.inflate(art.reach, art.reach),
// Art placed through 9-slice guides can draw outside `placed`.
None => s
.document()
.symbol_slice_map_of(symbol, art.geometric, *xf)
.map_or(placed, |m| m.placement_bounds(art.geometric).union(placed))
.inflate(art.reach, art.reach),
}
}
None => xf.transform_rect_bbox(SYMBOL_SQUARE),
+20
View File
@@ -343,6 +343,20 @@ impl NineSliceMap {
self.cell_xf(self.x.cell(q.x), self.y.cell(q.y))
}
/// A box that holds everything [`Self::place`] draws of art whose geometric box in symbol space
/// is `art`, before strokes and effects: `art` placed by each of the nine cells' maps. An object
/// that crosses a guide and draws symbol art is placed by the map of the cell its center is in,
/// so it can draw outside the box the corner cells place `art` in.
pub fn placement_bounds(&self, art: Rect) -> Rect {
let mut b = self.cell_xf(0, 0).transform_rect_bbox(art);
for j in 0..3 {
for i in 0..3 {
b = b.union(self.cell_xf(i, j).transform_rect_bbox(art));
}
}
b
}
/// The guides in symbol space: the two x values and the two y values.
fn cuts(&self) -> ([f64; 2], [f64; 2]) {
let inv = self.natural.inverse();
@@ -704,6 +718,12 @@ impl Document {
NineSliceMap::new(guides, art, self.symbol_natural_xf(name), xf)
}
/// [`Self::symbol_slice_map`] with the guides cutting `art`, the symbol's art box already
/// measured.
pub(crate) fn symbol_slice_map_of(&self, name: &str, art: Rect, xf: Affine) -> Option<NineSliceMap> {
NineSliceMap::new(self.symbol_nine_slice(name)?, art, self.symbol_natural_xf(name), xf)
}
/// Art `art` of symbol `name` (stained as the caller draws it) placed by an instance's
/// transform `xf`, with strokes at their own weight: through the symbol's 9-slice guides when
/// it has them ([`NineSliceMap::place`]), otherwise by `xf`. `None` when placing it through
+22 -1
View File
@@ -102,7 +102,9 @@ impl<'a> SymbolBoxes<'a> {
/// Makes these boxes measure every symbol instance as [`Node::reach_bounds_with`] does, by its
/// 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.
/// 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`]).
pub fn quick(self) -> Self {
self.quick.set(true);
self
@@ -447,6 +449,25 @@ mod tests {
assert_eq!(scaled.reach_bounds_with(&s), Some(Rect::new(0.0, 0.0, 60.0, 30.0).inflate(reach, reach)));
}
#[test]
fn quick_boxes_hold_art_that_9_slice_guides_move_past_the_instance() {
// A 100 × 100 frame with guides a quarter in, holding an instance of a bar from x 10 to 70.
// Scaled 2 times across, the bar crosses the left guide and is placed by the middle
// column's map, x → 3x − 50: it draws from x −20 to 160, past the frame's left edge at 0.
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 panel = instance(&mut d, "Panel", Affine::scale_non_uniform(2.0, 1.0));
let exact = panel.visual_bounds_with(&d.symbol_boxes()).unwrap();
assert_eq!(exact, Rect::new(-20.0, 0.0, 200.0, 100.0));
let quick = panel.visual_bounds_with(&d.symbol_boxes().quick()).unwrap();
assert_eq!(quick.union(exact), quick, "{quick:?} holds {exact:?}");
}
#[test]
fn missing_symbols_and_symbols_inside_themselves_measure_as_the_square() {
let mut d = Document::new(200.0, 200.0);
+21
View File
@@ -378,6 +378,27 @@ fn a_scaled_instance_is_culled_by_the_effects_in_its_art_as_placed() {
assert_eq!(img.pixel(5, 50), [255, 0, 0, 255]);
}
#[test]
fn an_instance_is_culled_by_the_art_its_9_slice_guides_move() {
// A 100 × 100 frame with guides a quarter in, holding an instance of a red bar from x 10 to
// 70. Scaled 2 times across, the bar crosses the left guide and is placed by the middle
// column's map, x → 3x − 50: moved 110 pt right, it 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 (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: "Panel".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