mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 12:24:19 +00:00
Merge pull request #7646 from dotCooCoo/fix/nine-slice-symbol-lookups
Symbols: measuring and picking in 9-slice instances reuse one set of symbol boxes
This commit is contained in:
+27
-23
@@ -974,43 +974,45 @@ fn hit_any(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Option
|
||||
/// The art of a symbol instance with its own appearance changes, in the symbol's coordinates, and
|
||||
/// how the canvas places each object of it: through the symbol's 9-slice guides when the instance
|
||||
/// is scaled by them ([`Document::place_symbol_art`]), otherwise by the instance's transform.
|
||||
struct InstanceArt {
|
||||
struct InstanceArt<'a> {
|
||||
art: Node,
|
||||
map: Option<crate::NineSliceMap>,
|
||||
xf: vectorcraft_geom::Affine,
|
||||
/// Quick boxes, which measure the instances in the art as it is placed through the guides.
|
||||
boxes: SymbolBoxes<'a>,
|
||||
}
|
||||
|
||||
impl InstanceArt {
|
||||
impl<'a> InstanceArt<'a> {
|
||||
/// The art of symbol instance `inst`. `None` for anything else, a missing symbol, or art that
|
||||
/// placing through the guides could make more than [`crate::MAX_SYMBOL_NODES`] objects of,
|
||||
/// which [`Document::place_symbol_art`] does not place and the canvas does not draw.
|
||||
fn of(doc: &Document, inst: &Node) -> Option<Self> {
|
||||
fn of(doc: &'a Document, inst: &Node) -> Option<Self> {
|
||||
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return None };
|
||||
let sym = doc.symbols.iter().find(|s| s.name == *symbol)?;
|
||||
let art = crate::symbol_overrides::dynamic_art(&sym.art, inst);
|
||||
let map = doc.symbol_slice_map(symbol, *xf);
|
||||
if map.as_ref().is_some_and(|m| m.most_placed(&art, doc) > crate::MAX_SYMBOL_NODES) {
|
||||
let boxes = doc.symbol_boxes().quick();
|
||||
let art = crate::symbol_overrides::dynamic_art(boxes.symbol_art(symbol)?, inst);
|
||||
let map = boxes.slice_map(symbol, *xf);
|
||||
if map.as_ref().is_some_and(|m| m.most_placed_with(&art, &boxes) > crate::MAX_SYMBOL_NODES) {
|
||||
return None;
|
||||
}
|
||||
Some(Self { art, map, xf: *xf })
|
||||
Some(Self { art, map, xf: *xf, boxes })
|
||||
}
|
||||
|
||||
/// The map that the objects inside group or layer `n` are moved by as one piece: `whole`, the
|
||||
/// map of an object around `n` that is moved as one piece, or else the map of the 9-slice cell
|
||||
/// `n` lies in ([`crate::NineSliceMap::one_cell`]). `None` when `n` is placed through the guides
|
||||
/// object by object.
|
||||
fn whole(&self, n: &Node, doc: &Document, whole: Option<vectorcraft_geom::Affine>) -> Option<vectorcraft_geom::Affine> {
|
||||
whole.or_else(|| self.map.as_ref()?.one_cell(n, doc))
|
||||
fn whole(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> Option<vectorcraft_geom::Affine> {
|
||||
whole.or_else(|| self.map.as_ref()?.one_cell_with(n, &self.boxes))
|
||||
}
|
||||
|
||||
/// Object `n` of the art placed on the canvas as it is drawn, clip groups included: moved by
|
||||
/// `whole` when an object around it is moved as one piece ([`Self::whole`]). `None` when
|
||||
/// placing it through the guides could make more than [`crate::MAX_SYMBOL_NODES`] objects, as
|
||||
/// [`Document::place_symbol_art`] refuses it.
|
||||
fn place(&self, n: &Node, doc: &Document, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
|
||||
fn place(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
|
||||
match (&self.map, whole) {
|
||||
(_, Some(a)) => Some(moved(n, a)),
|
||||
(Some(m), None) => (m.most_placed(n, doc) <= crate::MAX_SYMBOL_NODES).then(|| m.place(n, doc)),
|
||||
(Some(m), None) => (m.most_placed_with(n, &self.boxes) <= crate::MAX_SYMBOL_NODES).then(|| m.place_with(n, &self.boxes)),
|
||||
(None, None) => Some(moved(n, self.xf)),
|
||||
}
|
||||
}
|
||||
@@ -1018,10 +1020,13 @@ impl InstanceArt {
|
||||
/// Object `n` of the art placed on the canvas without clip groups
|
||||
/// ([`crate::NineSliceMap::place_unclipped`]), as [`Self::place`] places it and within the
|
||||
/// same limit ([`Document::place_symbol_art_unclipped`]).
|
||||
fn place_unclipped(&self, n: &Node, doc: &Document, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
|
||||
fn place_unclipped(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
|
||||
match (&self.map, whole) {
|
||||
(_, Some(a)) => Some(moved(n, a)),
|
||||
(Some(m), None) => (m.most_placed_unclipped(n, doc) <= crate::MAX_SYMBOL_NODES).then(|| m.place_unclipped(n, doc)),
|
||||
(Some(m), None) => {
|
||||
let m = m.unclipped();
|
||||
(m.most_placed_with(n, &self.boxes) <= crate::MAX_SYMBOL_NODES).then(|| m.place_with(n, &self.boxes))
|
||||
}
|
||||
(None, None) => Some(moved(n, self.xf)),
|
||||
}
|
||||
}
|
||||
@@ -1046,15 +1051,14 @@ fn moved(n: &Node, xf: vectorcraft_geom::Affine) -> Node {
|
||||
/// draw. `None` when `inst` is not a symbol instance or no editable object of its art is under `p`.
|
||||
pub fn symbol_part_at(doc: &Document, inst: &Node, p: Point, opt: HitOptions) -> Option<Vec<u32>> {
|
||||
struct Find<'a> {
|
||||
doc: &'a Document,
|
||||
art: &'a InstanceArt,
|
||||
art: &'a InstanceArt<'a>,
|
||||
p: Point,
|
||||
opt: HitOptions,
|
||||
symbols: &'a SymbolBoxes<'a>,
|
||||
}
|
||||
impl Find<'_> {
|
||||
fn hit(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> bool {
|
||||
self.art.place(n, self.doc, whole).is_some_and(|n| hit_any(&n, self.p, self.opt, self.symbols).is_some())
|
||||
self.art.place(n, whole).is_some_and(|n| hit_any(&n, self.p, self.opt, self.symbols).is_some())
|
||||
}
|
||||
fn skips_guide(&self, n: &Node) -> bool {
|
||||
self.opt.skip_guides && matches!(n.kind, NodeKind::Path { guide: true, .. })
|
||||
@@ -1067,11 +1071,11 @@ pub fn symbol_part_at(doc: &Document, inst: &Node, p: Point, opt: HitOptions) ->
|
||||
if path.len() >= crate::symbol_overrides::MAX_PATH_DEPTH {
|
||||
return self.hit(n, whole);
|
||||
}
|
||||
let whole = self.art.whole(n, self.doc, whole);
|
||||
let whole = self.art.whole(n, whole);
|
||||
if n.clips()
|
||||
&& let Some(first) = children.first()
|
||||
{
|
||||
let Some(clip) = self.art.place_unclipped(first, self.doc, whole) else { return false };
|
||||
let Some(clip) = self.art.place_unclipped(first, whole) else { return false };
|
||||
if self.opt.clip_outlines
|
||||
&& first.visible
|
||||
&& !first.locked
|
||||
@@ -1103,7 +1107,7 @@ pub fn symbol_part_at(doc: &Document, inst: &Node, p: Point, opt: HitOptions) ->
|
||||
let art = InstanceArt::of(doc, inst)?;
|
||||
let symbols = doc.symbol_boxes();
|
||||
let mut path = vec![];
|
||||
Find { doc, art: &art, p, opt, symbols: &symbols }.find(&art.art, &mut path, None).then_some(path)
|
||||
Find { art: &art, p, opt, symbols: &symbols }.find(&art.art, &mut path, None).then_some(path)
|
||||
}
|
||||
|
||||
/// The outlines of the object at `path` in symbol instance `inst`'s art (see [`symbol_part_at`]),
|
||||
@@ -1123,11 +1127,11 @@ pub fn symbol_part_outline(doc: &Document, inst: &Node, path: &[u32]) -> Option<
|
||||
let mut n = &art.art;
|
||||
for &i in path {
|
||||
let children = n.children()?;
|
||||
whole = art.whole(n, doc, whole);
|
||||
whole = art.whole(n, whole);
|
||||
if i != 0
|
||||
&& n.clips()
|
||||
&& let Some(first) = children.first()
|
||||
&& let Some(clip) = art.place_unclipped(first, doc, whole)?.geometric_bounds()
|
||||
&& let Some(clip) = art.place_unclipped(first, whole)?.geometric_bounds()
|
||||
{
|
||||
match meet(Some(clip), limit) {
|
||||
Some(r) => limit = Some(r),
|
||||
@@ -1136,7 +1140,7 @@ pub fn symbol_part_outline(doc: &Document, inst: &Node, path: &[u32]) -> Option<
|
||||
}
|
||||
n = children.get(usize::try_from(i).ok()?)?;
|
||||
}
|
||||
outline_within(&art.place(part, doc, whole)?, limit, &mut out);
|
||||
outline_within(&art.place(part, whole)?, limit, &mut out);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
|
||||
@@ -1586,6 +1586,53 @@ fn symbol_art_past_the_object_limit_picks_nothing() {
|
||||
assert!(symbol_part_outline(&d, n, &[0]).is_some());
|
||||
}
|
||||
|
||||
/// Picking and outlining an object of a 9-slice instance's art, and placing that art, read the
|
||||
/// symbol list once for each call, however many instances the art holds.
|
||||
#[test]
|
||||
fn picking_in_9_slice_art_reads_the_symbol_list_once() {
|
||||
let mut d = Document::new(400.0, 400.0);
|
||||
for i in 0..2_000 {
|
||||
let dot = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), Appearance::default_art());
|
||||
d.symbols.push(crate::Symbol { dynamic: false, name: format!("Dot {i}"), art: Arc::new(dot) });
|
||||
}
|
||||
// A frame, a group of 20 squares in its top left cell, and 20 instances of the dots across
|
||||
// the guides.
|
||||
let square = |d: &mut Document, x: f64, y: f64| {
|
||||
Arc::new(Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, y, x + 4.0, y + 4.0)), Appearance::default_art()))
|
||||
};
|
||||
let corner: Vec<_> = (0..20).map(|i| square(&mut d, f64::from(i % 5) * 4.0, f64::from(i / 5) * 4.0)).collect();
|
||||
let dot = |d: &mut Document, i: u32| {
|
||||
let xf = vectorcraft_geom::Affine::translate((f64::from(i) * 4.5 + 5.0, 48.0)) * vectorcraft_geom::Affine::scale(4.0);
|
||||
Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: format!("Dot {i}"), xf }))
|
||||
};
|
||||
let across: Vec<_> = (0..20).map(|i| dot(&mut d, i)).collect();
|
||||
let frame = Arc::new(Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default_art()));
|
||||
let mut kids = vec![frame, Arc::new(Node::group(d.alloc_id(), corner))];
|
||||
kids.extend(across);
|
||||
let root = Node::group(d.alloc_id(), kids);
|
||||
d.symbols.push(crate::Symbol { dynamic: true, name: "Panel".into(), art: Arc::new(root) });
|
||||
d.set_symbol_nine_slice("Panel", Some(crate::SymbolNineSlice::default()));
|
||||
let xf = vectorcraft_geom::Affine::scale_non_uniform(2.0, 1.5);
|
||||
let inst = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Panel".into(), xf });
|
||||
let art = d.symbols[2_000].art.clone();
|
||||
let symbols = d.symbols.len();
|
||||
let read = || crate::symbols::SYMBOLS_READ.with(std::cell::Cell::get);
|
||||
let reads = |f: &dyn Fn()| {
|
||||
let before = read();
|
||||
f();
|
||||
read() - before
|
||||
};
|
||||
// Picking also tests the placed art with boxes of its own, as a click outside symbols does.
|
||||
let n = reads(&|| assert_eq!(symbol_part_at(&d, &inst, Point::new(2.0, 2.0), HitOptions::default()).map(|p| p[0]), Some(1)));
|
||||
assert!(n <= 2 * symbols, "picking read {n} symbols");
|
||||
let n = reads(&|| assert!(symbol_part_outline(&d, &inst, &[2]).is_some()));
|
||||
assert!(n <= symbols, "outlining read {n} symbols");
|
||||
let n = reads(&|| assert!(d.place_symbol_art("Panel", &art, xf).is_some()));
|
||||
assert!(n <= symbols, "placing read {n} symbols");
|
||||
let n = reads(&|| assert!(d.place_symbol_art_unclipped("Panel", &art, xf).is_some()));
|
||||
assert!(n <= symbols, "placing without clips read {n} symbols");
|
||||
}
|
||||
|
||||
/// A clip group that lies in one 9-slice cell is moved by that cell as one piece, everything in it
|
||||
/// included, so an object in it is picked and outlined there even when placing that object through
|
||||
/// the guides on its own would pass [`crate::MAX_SYMBOL_NODES`] objects.
|
||||
|
||||
@@ -463,8 +463,13 @@ impl NineSliceMap {
|
||||
///
|
||||
/// Strokes keep their weight, as they do for any instance.
|
||||
pub fn place(&self, art: &Node, doc: &Document) -> Node {
|
||||
let boxes = doc.symbol_boxes().quick();
|
||||
self.place_at(art, &boxes, 0)
|
||||
self.place_with(art, &doc.symbol_boxes().quick())
|
||||
}
|
||||
|
||||
/// [`Self::place`] with the instances in `art` measured by `boxes`, quick boxes or boxes
|
||||
/// measuring symbol art.
|
||||
pub(crate) fn place_with(&self, art: &Node, boxes: &SymbolBoxes) -> Node {
|
||||
self.place_at(art, boxes, 0)
|
||||
}
|
||||
|
||||
/// [`Self::place`] without clip groups, for writers that do not apply clipping (DXF, WMF):
|
||||
@@ -480,16 +485,25 @@ impl NineSliceMap {
|
||||
/// object drawn in clip groups counts one group plus a clip path and a copy for each cell its
|
||||
/// box reaches. That is never more than [`MAX_SLICE_GROWTH`] for each object of `art`.
|
||||
pub fn most_placed(&self, art: &Node, doc: &Document) -> u64 {
|
||||
let boxes = doc.symbol_boxes().quick();
|
||||
self.most_at(art, &boxes, 0)
|
||||
self.most_placed_with(art, &doc.symbol_boxes().quick())
|
||||
}
|
||||
|
||||
/// [`Self::most_placed`] with the instances in `art` measured by `boxes`, quick boxes or boxes
|
||||
/// measuring symbol art.
|
||||
pub(crate) fn most_placed_with(&self, art: &Node, boxes: &SymbolBoxes) -> u64 {
|
||||
self.most_at(art, boxes, 0)
|
||||
}
|
||||
|
||||
/// The affine map that [`Self::place`] and [`Self::place_unclipped`] move object `n` by as one
|
||||
/// piece, with everything in it: the map of the cell its box lies in, or of the cell at the
|
||||
/// origin when it has no box. `None` when its box crosses a guide.
|
||||
pub fn one_cell(&self, n: &Node, doc: &Document) -> Option<Affine> {
|
||||
let boxes = doc.symbol_boxes().quick();
|
||||
let Some(b) = n.geometric(Some(&boxes)).map(|b| self.natural.transform_rect_bbox(b)).filter(|b| finite(*b)) else {
|
||||
self.one_cell_with(n, &doc.symbol_boxes().quick())
|
||||
}
|
||||
|
||||
/// [`Self::one_cell`] with the instances in `n` measured by `boxes`.
|
||||
pub(crate) fn one_cell_with(&self, n: &Node, boxes: &SymbolBoxes) -> Option<Affine> {
|
||||
let Some(b) = n.geometric(Some(boxes)).map(|b| self.natural.transform_rect_bbox(b)).filter(|b| finite(*b)) else {
|
||||
return Some(self.cell_at(Point::ZERO));
|
||||
};
|
||||
let (cols, rows) = (self.x.cells(b.x0, b.x1), self.y.cells(b.y0, b.y1));
|
||||
@@ -501,6 +515,11 @@ impl NineSliceMap {
|
||||
Self { clips: false, ..*self }.most_placed(art, doc)
|
||||
}
|
||||
|
||||
/// This map placing without clip groups, as [`Self::place_unclipped`] does.
|
||||
pub(crate) fn unclipped(&self) -> Self {
|
||||
Self { clips: false, ..*self }
|
||||
}
|
||||
|
||||
/// [`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;
|
||||
@@ -671,7 +690,7 @@ impl NineSliceMap {
|
||||
}
|
||||
|
||||
/// `n` moved by `a`, its strokes at their own weight.
|
||||
fn transformed(n: &Node, a: Affine) -> Node {
|
||||
pub(crate) fn transformed(n: &Node, a: Affine) -> Node {
|
||||
let mut out = n.clone();
|
||||
out.transform(a, false);
|
||||
out
|
||||
@@ -765,10 +784,7 @@ impl Document {
|
||||
/// `None` when the symbol has no guides or the instance is at its natural size. The guides cut
|
||||
/// the art's box with the instances in it measured by their own art.
|
||||
pub fn symbol_slice_map(&self, name: &str, xf: Affine) -> Option<NineSliceMap> {
|
||||
let guides = self.symbol_nine_slice(name)?;
|
||||
let sym = self.symbols.iter().find(|s| s.name == name)?;
|
||||
let art = self.symbol_boxes().quick().art_bounds(name).or_else(|| sym.art.geometric_bounds())?;
|
||||
NineSliceMap::new(guides, art, self.symbol_natural_xf(name), xf)
|
||||
self.symbol_boxes().quick().slice_map(name, xf)
|
||||
}
|
||||
|
||||
/// [`Self::symbol_slice_map`] with the guides cutting `art`, the symbol's art box already
|
||||
@@ -782,9 +798,10 @@ impl Document {
|
||||
/// it has them ([`NineSliceMap::place`]), otherwise by `xf`. `None` when placing it through
|
||||
/// the guides could make more than [`MAX_SYMBOL_NODES`] objects ([`NineSliceMap::most_placed`]).
|
||||
pub fn place_symbol_art(&self, name: &str, art: &Node, xf: Affine) -> Option<Node> {
|
||||
match self.symbol_slice_map(name, xf) {
|
||||
Some(m) if m.most_placed(art, self) > MAX_SYMBOL_NODES => None,
|
||||
Some(m) => Some(m.place(art, self)),
|
||||
let boxes = self.symbol_boxes().quick();
|
||||
match boxes.slice_map(name, xf) {
|
||||
Some(m) if m.most_placed_with(art, &boxes) > MAX_SYMBOL_NODES => None,
|
||||
Some(m) => Some(m.place_with(art, &boxes)),
|
||||
None => Some(transformed(art, xf)),
|
||||
}
|
||||
}
|
||||
@@ -795,8 +812,8 @@ impl Document {
|
||||
pub fn edited_nine_slice(&self) -> Option<EditedNineSlice> {
|
||||
let se = self.symbol_edit.as_ref()?;
|
||||
let guides = self.symbol_nine_slice(&se.symbol)?;
|
||||
let sym = self.symbols.iter().find(|s| s.name == se.symbol)?;
|
||||
let art = self.symbol_boxes().quick().art_bounds(&se.symbol).or_else(|| sym.art.geometric_bounds())?;
|
||||
let boxes = self.symbol_boxes().quick();
|
||||
let art = boxes.art_bounds(&se.symbol).or_else(|| boxes.symbol_art(&se.symbol)?.geometric_bounds())?;
|
||||
let natural = self.symbol_natural_xf(&se.symbol);
|
||||
let b = natural.transform_rect_bbox(art);
|
||||
if !finite(b) || b.width() <= 1e-9 || b.height() <= 1e-9 || natural.determinant().abs() < 1e-18 {
|
||||
@@ -811,8 +828,9 @@ impl Document {
|
||||
/// [`Self::place_symbol_art`] for writers that do not apply clipping
|
||||
/// ([`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) => (m.most_placed_unclipped(art, self) <= MAX_SYMBOL_NODES).then(|| m.place_unclipped(art, self)),
|
||||
let boxes = self.symbol_boxes().quick();
|
||||
match boxes.slice_map(name, xf).map(|m| m.unclipped()) {
|
||||
Some(m) => (m.most_placed_with(art, &boxes) <= MAX_SYMBOL_NODES).then(|| m.place_with(art, &boxes)),
|
||||
None => Some(transformed(art, xf)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use std::sync::Arc;
|
||||
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
use crate::{AppearanceItem, Document, MAX_SYMBOL_NODES, Node, NodeKind, SYMBOL_HALF};
|
||||
use crate::symbol_nine_slice::transformed;
|
||||
use crate::{AppearanceItem, Document, MAX_SYMBOL_NODES, NineSliceMap, Node, NodeKind, SYMBOL_HALF};
|
||||
|
||||
/// The box a symbol instance is measured by when its symbol's art has no box
|
||||
/// ([`SymbolBoxes::art`]), and in [`Node::geometric_bounds`]: the 20 × 20 square around the origin
|
||||
@@ -128,6 +129,8 @@ impl Document {
|
||||
/// Each symbol name of this document → the index of the first symbol of that name (the one a
|
||||
/// name finds).
|
||||
pub(crate) fn symbol_names(&self) -> SymbolNames {
|
||||
#[cfg(test)]
|
||||
SYMBOLS_READ.with(|c| c.set(c.get() + self.symbols.len()));
|
||||
let mut names = HashMap::with_capacity(self.symbols.len());
|
||||
for (i, s) in self.symbols.iter().enumerate() {
|
||||
names.entry(s.name.clone()).or_insert(i);
|
||||
@@ -136,6 +139,12 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// How many symbols [`Document::symbol_names`] has read on this thread.
|
||||
pub(crate) static SYMBOLS_READ: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
impl<'a> SymbolBoxes<'a> {
|
||||
/// The document whose symbols these are.
|
||||
pub fn document(&self) -> &'a Document {
|
||||
@@ -437,8 +446,8 @@ impl<'a> SymbolBoxes<'a> {
|
||||
// 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.symbol_art(name)?;
|
||||
let most = match self.doc.symbol_slice_map(name, linear) {
|
||||
Some(m) => m.most_placed(art, self.doc),
|
||||
let most = match self.slice_map(name, linear) {
|
||||
Some(m) => self.in_art(|| m.most_placed_with(art, self)),
|
||||
None => art.count_with_masks() as u64,
|
||||
};
|
||||
let used = self.sliced_placed.get();
|
||||
@@ -455,6 +464,14 @@ impl<'a> SymbolBoxes<'a> {
|
||||
v.map(|v| moved.transform_rect_bbox(v))
|
||||
}
|
||||
|
||||
/// [`Document::symbol_slice_map`] with the guides cutting the art box these boxes measure for
|
||||
/// the symbol (its geometric part, which does not depend on the mode of the boxes).
|
||||
pub(crate) fn slice_map(&self, name: &str, xf: Affine) -> Option<NineSliceMap> {
|
||||
let guides = self.doc.symbol_nine_slice(name)?;
|
||||
let art = self.art_bounds(name).or_else(|| self.symbol_art(name)?.geometric_bounds())?;
|
||||
NineSliceMap::new(guides, art, self.doc.symbol_natural_xf(name), xf)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -464,10 +481,10 @@ impl<'a> SymbolBoxes<'a> {
|
||||
/// 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)?,
|
||||
let art = match self.slice_map(name, xf) {
|
||||
Some(m) if self.in_art(|| m.most_placed_with(art, self)) > most => return None,
|
||||
Some(m) => self.in_art(|| m.place_with(art, self)),
|
||||
None => transformed(art, xf),
|
||||
};
|
||||
Some((self.in_art(|| art.visual(Some(self))), art.count_with_masks() as u64))
|
||||
})
|
||||
@@ -689,6 +706,39 @@ mod tests {
|
||||
assert_eq!(s.sliced_placed.get(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measuring_9_slice_instances_reads_the_symbol_list_once() {
|
||||
// 2,000 symbols, then a frame with guides and a symbol with guides holding the frame
|
||||
// scaled 2 times across, each shown by 200 instances scaled by different amounts.
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
for i in 0..2000 {
|
||||
let dot = rect(&mut d, Rect::new(0.0, 0.0, 1.0, 1.0), 0.0);
|
||||
symbol(&mut d, &format!("Dot {i}"), vec![dot]);
|
||||
}
|
||||
let frame = rect(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), 1.0);
|
||||
symbol(&mut d, "Panel", vec![frame]);
|
||||
d.set_symbol_nine_slice("Panel", Some(crate::SymbolNineSlice::default()));
|
||||
let wide = instance(&mut d, "Panel", Affine::scale_non_uniform(2.0, 1.0));
|
||||
symbol(&mut d, "Holder", vec![wide]);
|
||||
d.set_symbol_nine_slice("Holder", Some(crate::SymbolNineSlice::default()));
|
||||
let scaled = |d: &mut Document, name: &str| -> Vec<Node> {
|
||||
(1..=200).map(|i| instance(d, name, Affine::scale_non_uniform(1.0 + f64::from(i) / 100.0, 1.5))).collect()
|
||||
};
|
||||
let (panels, holders) = (scaled(&mut d, "Panel"), scaled(&mut d, "Holder"));
|
||||
// What each instance measures in boxes of its own.
|
||||
let exact: Vec<_> = panels.iter().map(|n| n.visual_bounds_with(&d.symbol_boxes())).collect();
|
||||
let quick: Vec<_> = holders.iter().map(|n| n.visual_bounds_with(&d.symbol_boxes().quick())).collect();
|
||||
let read = || SYMBOLS_READ.with(Cell::get);
|
||||
let before = read();
|
||||
let s = d.symbol_boxes();
|
||||
assert_eq!(panels.iter().map(|n| n.visual_bounds_with(&s)).collect::<Vec<_>>(), exact);
|
||||
let s = d.symbol_boxes().quick();
|
||||
assert_eq!(holders.iter().map(|n| n.visual_bounds_with(&s)).collect::<Vec<_>>(), quick);
|
||||
// Each set of boxes reads the list once to find symbols by name.
|
||||
let symbols = d.symbols.len();
|
||||
assert!(read() - before <= 2 * symbols, "{} symbols read", read() - before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_symbols_and_symbols_inside_themselves_measure_as_the_square() {
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
|
||||
Reference in New Issue
Block a user