Files
vectorcraft/crates/doc/src/inline.rs
T
dotCooCoo bc24ad5352 Symbols: a symbol nested in symbol art is measured by its own art
Document::symbol_art_bounds measured a symbol's art without the
document, so an instance of another symbol in that art counted as the
20 x 20 square. It now measures the art through SymbolBoxes, so a nested
instance takes its own symbol's art, and an inline graphic of the outer
symbol is sized by it and follows it when that symbol changes.

- The inline resolve order also follows the instances that size a
  symbol's art, so a symbol is measured after the type in the art of
  the symbols it holds is laid out again.
- A symbol whose art reaches itself through an instance and an inline
  graphic is an empty slot. Only instances that the measurement uses
  count, so one a clipping path hides adds no dependency.
- A missing symbol, a symbol without art, a symbol that holds an
  instance of itself, and nesting 32 deep still measure as the square.
- SymbolBoxes finds symbols through a name index built once, and the
  inline resolve pass and inline_art_bounds share one index, instead
  of scanning the symbol list for each lookup.
2026-11-14 05:10:41 -08:00

639 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Inline graphics in text ([`crate::TextRun::inline`]): resolving each inline symbol's art bounds
//! from the document's symbols, which the text layout needs to size and place it.
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock, Weak};
use vectorcraft_geom::{Affine, Rect};
use crate::symbols::SymbolNames;
use crate::{Document, Node, NodeId, NodeKind, Symbol, SymbolBoxes, TextObject};
/// The document key under which the app records each symbol's natural size (`{name: [w, h]}`).
pub const SYMBOL_SIZES: &str = "symbolSizes";
/// Half the side of the square symbol art is normalised to.
pub const SYMBOL_HALF: f64 = 10.0;
/// The box symbol art `art` is measured by in [`Document::symbol_art_bounds`]: its visual bounds,
/// or its geometric bounds when it has no visual bounds.
fn art_extent(art: &Node, boxes: &SymbolBoxes) -> Option<Rect> {
art.visual_bounds_with(boxes).or_else(|| art.geometric_bounds_with(boxes))
}
/// Usable art bounds: finite, non-degenerate.
fn usable(r: Option<Rect>) -> Option<Rect> {
r.filter(|r| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()) && r.width() >= 0.0 && r.height() > 1e-9)
}
impl Document {
/// Symbol space → the symbol's art at its natural size. Symbols made in the app keep their
/// art in a 20 × 20 square around the origin and their size as drawn in the document's
/// `symbolSizes` (an instance is placed with that scale); other symbols are as they are.
pub fn symbol_natural_xf(&self, name: &str) -> Affine {
let size = self.unknown.get(SYMBOL_SIZES).and_then(|m| m.get(name)).and_then(|v| Some((v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)));
match size {
Some((w, h)) if w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0 => {
Affine::scale_non_uniform(w / (2.0 * SYMBOL_HALF), h / (2.0 * SYMBOL_HALF))
}
_ => Affine::IDENTITY,
}
}
/// Visual bounds of the art of the symbol named `name` at its natural size (see
/// [`Self::symbol_natural_xf`]), if it exists and has a usable extent. The symbol instances in
/// the art are measured by their own symbols' art ([`Node::visual_bounds_with`]), or by the
/// 20 × 20 square where [`crate::SymbolBoxes`] measures them by it.
pub fn symbol_art_bounds(&self, name: &str) -> Option<Rect> {
self.symbol_art_bounds_in(name, &self.symbol_boxes())
}
/// [`Self::symbol_art_bounds`], with the symbols found and the instances measured by `boxes`
/// (new boxes of this document).
fn symbol_art_bounds_in(&self, name: &str, boxes: &SymbolBoxes) -> Option<Rect> {
let b = usable(art_extent(boxes.symbol_art(name)?, boxes))?;
usable(Some(self.symbol_natural_xf(name).transform_rect_bbox(b)))
}
/// The art bounds an inline graphic of the symbol named `name` takes: [`Self::symbol_art_bounds`],
/// or `None` (an empty slot) when the symbol's art shows the symbol itself inline, directly or
/// through the inline graphics and symbol instances in other symbols' art, so that its size
/// would depend on itself.
pub fn inline_art_bounds(&self, name: &str) -> Option<Rect> {
let (cyclic, names) = self.inline_cycles();
if cyclic.contains(name) { None } else { self.symbol_art_bounds_in(name, &self.symbol_boxes().with_names(&names)) }
}
/// The names of the symbols that reach themselves through the inline graphics in symbol art
/// ([`InlineGraph`]), with each symbol name → the index of the first symbol of that name, found
/// once while the symbols stay as they are.
fn inline_cycles(&self) -> (Arc<HashSet<String>>, Arc<SymbolNames>) {
if let Some(found) = self.inline_cycles.get(&self.symbols) {
return found;
}
let graph = InlineGraph::of(self);
let found = (Arc::new(graph.cyclic), graph.names);
self.inline_cycles.set(&self.symbols, &found);
found
}
/// Does `t` have inline graphics whose resolved bounds differ from the document's symbols?
fn inline_stale(&self, t: &TextObject) -> bool {
stale_by(t, &|n| self.inline_art_bounds(n))
}
/// `t` with its inline graphics' art bounds resolved from this document's symbols (borrowed
/// when nothing changes). Renderers and exporters lay text out through this, so they never
/// depend on the cached bounds being current.
pub fn inline_resolved<'a>(&self, t: &'a TextObject) -> Cow<'a, TextObject> {
if !self.inline_stale(t) {
return Cow::Borrowed(t);
}
let mut t = t.clone();
self.resolve_text_inline(&mut t);
Cow::Owned(t)
}
/// Fill in the art bounds of `t`'s inline graphics ([`Self::inline_art_bounds`]); returns
/// whether any changed (the text's cached layout is then cleared).
pub fn resolve_text_inline(&self, t: &mut TextObject) -> bool {
resolve_runs(t, &|n| self.inline_art_bounds(n))
}
/// Resolve the inline graphics of every text object in the document (after opening a document
/// or changing its symbols), without laying the type out again: see
/// [`Self::resolve_inline_art_with`].
pub fn resolve_inline_art(&mut self) -> Vec<NodeId> {
self.resolve_inline_art_with(&mut |_| {})
}
/// Resolve the inline graphics of every text object in the document: in the layers, the art
/// of symbols, graph designs and pattern tiles, and the opacity masks and live objects in
/// them. Each text whose inline graphics changed has its cached layout cleared and is passed
/// to `layout` to be laid out again. The symbols are resolved first, each after the symbols its
/// inline graphics and symbol instances show, so a symbol's art is measured with its type, and
/// the type in the art of the symbols its instances show, already laid out; a
/// symbol that shows itself inline ([`Self::inline_art_bounds`]) is shown as an empty slot.
/// Returns the ids of the texts in the layers that changed.
pub fn resolve_inline_art_with(&mut self, layout: &mut dyn FnMut(&mut TextObject)) -> Vec<NodeId> {
let mut out = vec![];
let mut unused = vec![];
let graph = InlineGraph::of(self);
let measured = Measured::default();
for &i in &graph.order {
let mut art = self.symbols[i].art.clone();
if resolve_tree(&mut art, &|n| measured.get(n, || graph.bounds(self, n)), layout, &mut unused) {
self.symbols[i].art = art;
}
}
for i in 0..self.layers.len() {
let mut layer = self.layers[i].clone();
if resolve_tree(&mut layer, &|n| measured.get(n, || graph.bounds(self, n)), layout, &mut out) {
self.layers[i] = layer;
}
}
for i in 0..self.graph_designs.len() {
let mut art = self.graph_designs[i].art.clone();
if resolve_tree(&mut art, &|n| measured.get(n, || graph.bounds(self, n)), layout, &mut unused) {
self.graph_designs[i].art = art;
}
}
for i in 0..self.patterns.len() {
for j in 0..self.patterns[i].art.len() {
let mut art = self.patterns[i].art[j].clone();
if resolve_tree(&mut art, &|n| measured.get(n, || graph.bounds(self, n)), layout, &mut unused) {
self.patterns[i].art[j] = art;
}
}
}
out
}
}
/// Whether `t` has inline graphics whose bounds differ from what `bounds` gives their symbols.
fn stale_by(t: &TextObject, bounds: &dyn Fn(&str) -> Option<Rect>) -> bool {
t.runs.iter().filter_map(|r| r.inline.as_ref()).any(|a| a.bounds != bounds(&a.symbol))
}
/// Gives `t`'s inline graphics the bounds `bounds` gives their symbols; returns whether any changed
/// (the text's cached layout is then cleared).
fn resolve_runs(t: &mut TextObject, bounds: &dyn Fn(&str) -> Option<Rect>) -> bool {
let mut changed = false;
for a in t.runs.iter_mut().filter_map(|r| r.inline.as_mut()) {
let b = bounds(&a.symbol);
if a.bounds != b {
a.bounds = b;
changed = true;
}
}
if changed {
t.cached_bounds = None;
t.cached_baselines.clear();
t.cached_ink = None;
}
changed
}
/// Resolves the inline graphics of the type in tree `n` (opacity mask art included) with
/// `bounds`, passing each text that changed to `layout` and adding its id to `changed`; returns
/// whether the tree changed. Each node is visited once, and only the nodes on the way to changed
/// type are copied.
fn resolve_tree(
n: &mut Arc<Node>,
bounds: &dyn Fn(&str) -> Option<Rect>,
layout: &mut dyn FnMut(&mut TextObject),
changed: &mut Vec<NodeId>,
) -> bool {
let mut copied = false;
if matches!(&n.kind, NodeKind::Text(t) if stale_by(t, bounds)) {
let node = Arc::make_mut(n);
if let NodeKind::Text(t) = &mut node.kind
&& resolve_runs(t, bounds)
{
layout(t);
changed.push(node.id);
}
copied = true;
}
let mask = n.extras().mask.as_deref().and_then(|m| {
let mut art = m.art.clone();
resolve_tree(&mut art, bounds, layout, changed).then_some(art)
});
let mut kids = vec![];
for (i, c) in n.children().into_iter().flatten().enumerate() {
let mut c = c.clone();
if resolve_tree(&mut c, bounds, layout, changed) {
kids.push((i, c));
}
}
if mask.is_none() && kids.is_empty() {
return copied;
}
let node = Arc::make_mut(n);
if let Some(art) = mask
&& let Some(m) = node.extras_if_any().and_then(|e| e.mask.as_deref_mut())
{
m.art = art;
}
if let Some(ch) = node.children_mut() {
for (i, c) in kids {
ch[i] = c;
}
}
true
}
/// The art bounds of each symbol, measured once in a pass of [`Document::resolve_inline_art_with`].
/// A symbol is first asked for after its own type is resolved (the pass goes in
/// [`InlineGraph::order`]), so its measurement stays current for the rest of the pass.
#[derive(Default)]
struct Measured(RefCell<HashMap<String, Option<Rect>>>);
impl Measured {
/// The bounds kept for `name`, or `measure`'s, kept for later.
fn get(&self, name: &str, measure: impl FnOnce() -> Option<Rect>) -> Option<Rect> {
if let Some(&b) = self.0.borrow().get(name) {
return b;
}
let b = measure();
self.0.borrow_mut().insert(name.to_owned(), b);
b
}
}
/// The names of the symbols the inline graphics in tree `n` show (opacity mask art included).
fn inline_symbols(n: &Node) -> Vec<String> {
fn collect(n: &Node, out: &mut Vec<String>) {
if let NodeKind::Text(t) = &n.kind {
out.extend(t.runs.iter().filter_map(|r| r.inline.as_ref().map(|a| a.symbol.clone())));
}
if let Some(m) = n.extras().mask.as_deref() {
collect(&m.art, out);
}
for c in n.children().into_iter().flatten() {
collect(c, out);
}
}
let mut out = vec![];
collect(n, &mut out);
out
}
/// The symbols of a document as their sizes depend on each other: a symbol's art depends on the
/// symbols its type shows inline, and on the symbols its instances show when those instances are
/// measured by their symbols' art ([`crate::SymbolBoxes`]).
struct InlineGraph {
/// Every symbol index, each after the symbols its art depends on (except along a cycle).
order: Vec<usize>,
/// The names of the symbols that reach themselves through inline graphics. Instances alone make
/// no cycle: an instance of a symbol that holds an instance of itself is measured by the
/// 20 × 20 square, so every cycle passes through an inline graphic.
cyclic: HashSet<String>,
/// Each symbol name → the index of the first symbol of that name, which stay the same while
/// [`Document::resolve_inline_art_with`] changes the symbols' art.
names: Arc<SymbolNames>,
}
impl InlineGraph {
/// The graph of `d`'s symbols: each symbol's art is walked once, and, when the art of any
/// symbol holds an inline graphic, measured once with the art boxes of the symbols its
/// instances show measured once in all ([`SymbolBoxes::consulted`]: an instance that does not
/// size the art, such as one a clipping path hides, adds nothing). The order and cycles are
/// found in time linear in the symbols and their inline references and instances (Tarjan's
/// strongly connected components, without recursion).
fn of(d: &Document) -> Self {
let names = Arc::new(d.symbol_names());
let boxes = d.symbol_boxes().quick().with_names(&names);
let inline: Vec<Vec<String>> = d.symbols.iter().map(|s| inline_symbols(&s.art)).collect();
// Without inline graphics in symbol art, no symbol's type depends on another symbol.
let any_inline = inline.iter().any(|v| !v.is_empty());
let edges: Vec<Vec<usize>> = d
.symbols
.iter()
.zip(inline)
.map(|(s, inline)| {
let measured = if any_inline {
boxes.consulted(|| {
art_extent(&s.art, &boxes);
})
} else {
HashSet::new()
};
let shows = inline.into_iter().chain(measured);
let mut e: Vec<usize> = shows.filter_map(|n| names.get(n.as_str()).copied()).collect();
e.sort_unstable();
e.dedup();
e
})
.collect();
let (order, cyclic) = components(&edges);
let cyclic = order.iter().filter(|&&i| cyclic[i]).map(|&i| d.symbols[i].name.clone()).collect();
Self { order, cyclic, names }
}
/// [`Document::inline_art_bounds`] with the cycles found in this graph, for `d` with the
/// symbols' names and order the graph was made with.
fn bounds(&self, d: &Document, name: &str) -> Option<Rect> {
if self.cyclic.contains(name) { None } else { d.symbol_art_bounds_in(name, &d.symbol_boxes().with_names(&self.names)) }
}
}
/// The nodes of the graph `edges` (node → the nodes it depends on), each after the nodes it
/// depends on except within a cycle, and whether each node is on a cycle (a component of more than
/// one node, or an edge to itself).
fn components(edges: &[Vec<usize>]) -> (Vec<usize>, Vec<bool>) {
const UNSEEN: usize = usize::MAX;
let n = edges.len();
let (mut index, mut low, mut on_stack) = (vec![UNSEEN; n], vec![0; n], vec![false; n]);
let (mut stack, mut order, mut cyclic) = (vec![], Vec::with_capacity(n), vec![false; n]);
let mut next = 0;
// The nodes being visited, each with the position of its next edge.
let mut calls: Vec<(usize, usize)> = vec![];
for root in 0..n {
if index[root] != UNSEEN {
continue;
}
index[root] = next;
low[root] = next;
next += 1;
stack.push(root);
on_stack[root] = true;
calls.push((root, 0));
while let Some(top) = calls.last_mut() {
let v = top.0;
if let Some(&w) = edges[v].get(top.1) {
top.1 += 1;
if index[w] == UNSEEN {
index[w] = next;
low[w] = next;
next += 1;
stack.push(w);
on_stack[w] = true;
calls.push((w, 0));
} else if on_stack[w] {
low[v] = low[v].min(index[w]);
}
continue;
}
calls.pop();
if let Some(&(u, _)) = calls.last() {
low[u] = low[u].min(low[v]);
}
if low[v] == index[v] {
let start = order.len();
while let Some(w) = stack.pop() {
on_stack[w] = false;
order.push(w);
if w == v {
break;
}
}
if order.len() - start > 1 || edges[v].contains(&v) {
for &w in &order[start..] {
cyclic[w] = true;
}
}
}
}
}
(order, cyclic)
}
/// The symbols [`Document::inline_cycles`] last found, with the name of each symbol and a weak
/// pointer to its art. While a weak pointer to an art's allocation is held, nothing else can be at
/// that address, and the art can't be changed in place (`Arc::get_mut` refuses it and
/// `Arc::make_mut` moves it), so any edit inside a symbol's art, and any symbol added, removed,
/// renamed or replaced, gives symbols the cache does not match. Clones of a document share it.
#[derive(Default)]
pub(crate) struct InlineCycles(RwLock<Option<Arc<Found>>>);
/// The names of the symbols on a cycle, and each symbol name → the index of the first symbol of that
/// name.
type Cycles = (Arc<HashSet<String>>, Arc<SymbolNames>);
/// The name and art of each symbol the cycles were found in, and what was found.
type Found = (Vec<(String, Weak<Node>)>, Cycles);
impl InlineCycles {
/// The cached names, when they were found in `symbols`.
fn get(&self, symbols: &[Symbol]) -> Option<Cycles> {
let g = self.0.read().ok()?;
let (from, found) = &**g.as_ref()?;
let same = from.len() == symbols.len()
&& from.iter().zip(symbols).all(|((name, art), s)| *name == s.name && std::ptr::eq(art.as_ptr(), Arc::as_ptr(&s.art)));
same.then(|| found.clone())
}
/// Keep `found`, found in `symbols`.
fn set(&self, symbols: &[Symbol], found: &Cycles) {
if let Ok(mut g) = self.0.write() {
*g = Some(Arc::new((symbols.iter().map(|s| (s.name.clone(), Arc::downgrade(&s.art))).collect(), found.clone())));
}
}
}
impl Clone for InlineCycles {
fn clone(&self) -> Self {
InlineCycles(RwLock::new(self.0.read().ok().and_then(|g| g.clone())))
}
}
/// A cache, not document content: two documents are equal whatever their caches hold.
impl PartialEq for InlineCycles {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl std::fmt::Debug for InlineCycles {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("InlineCycles")
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The position of each node in `order`.
fn positions(order: &[usize]) -> Vec<usize> {
let mut at = vec![0; order.len()];
for (p, &i) in order.iter().enumerate() {
at[i] = p;
}
at
}
#[test]
fn components_put_dependencies_first_and_find_every_cycle_member() {
// 0 → 1 → 2 → 0 is a cycle, and 3, reached from 1, joins it through 2.
// 4 depends on 0 and is on no cycle; 5 shows itself.
let edges = vec![vec![1], vec![2, 3], vec![0], vec![2], vec![0], vec![5]];
let (order, cyclic) = components(&edges);
let mut sorted = order.clone();
sorted.sort_unstable();
assert_eq!(sorted, [0, 1, 2, 3, 4, 5]);
assert_eq!(cyclic, [true, true, true, true, false, true]);
let at = positions(&order);
assert!(at[4] > at[0], "{order:?}");
// A chain: each node after the one it depends on.
let (order, cyclic) = components(&[vec![], vec![0], vec![1]]);
assert_eq!(order, [0, 1, 2]);
assert!(cyclic.iter().all(|c| !c));
}
/// Type at the bottom of 2,000 nested groups is checked once, not once for each group above it,
/// and only the groups on the way to it are copied.
#[test]
fn resolving_deeply_nested_type_visits_each_node_once() {
use std::cell::Cell;
use vectorcraft_geom::Point;
let mut t = TextObject::point(Point::ZERO, "Type", crate::CharStyle::default());
t.runs.push(crate::TextRun::new("", crate::CharStyle::default()));
t.runs[1].inline = Some(crate::InlineArt { symbol: "S".into(), scale: 1.0, baseline_shift: 0.0, bounds: None });
let depth = 2_000;
let mut tree = Arc::new(Node::new(NodeId(0), NodeKind::Text(Box::new(t))));
for i in 1..=depth {
let side = Arc::new(Node::group(NodeId(depth + i), vec![]));
tree = Arc::new(Node::group(NodeId(i), vec![side, tree]));
}
let before = tree.clone();
let asked = Cell::new(0);
let r = Rect::new(0.0, 0.0, 10.0, 20.0);
let bounds = |_: &str| {
asked.set(asked.get() + 1);
Some(r)
};
let (mut laid_out, mut changed) = (0, vec![]);
assert!(resolve_tree(&mut tree, &bounds, &mut |_| laid_out += 1, &mut changed));
assert!(asked.get() <= 2, "bounds asked {} times for one inline graphic", asked.get());
assert_eq!(laid_out, 1);
assert_eq!(changed, [NodeId(0)]);
// The bottom of the new tree holds the resolved type; the side groups are shared.
let (mut new, mut old) = (&tree, &before);
while let (Some(n), Some(o)) = (new.children(), old.children()) {
assert!(Arc::ptr_eq(&n[0], &o[0]));
(new, old) = (&n[1], &o[1]);
}
let NodeKind::Text(t) = &new.kind else { panic!("the type is at the bottom") };
assert_eq!(t.runs[1].inline.as_ref().and_then(|a| a.bounds), Some(r));
// Nothing left to change: the tree is kept and the type is checked once.
asked.set(0);
let kept = tree.clone();
assert!(!resolve_tree(&mut tree, &bounds, &mut |_| laid_out += 1, &mut changed));
assert!(Arc::ptr_eq(&tree, &kept));
assert_eq!(asked.get(), 1);
}
#[test]
fn a_symbol_is_measured_once_in_a_pass() {
let measured = Measured::default();
let count = std::cell::Cell::new(0);
let measure = |b: Option<Rect>| {
count.set(count.get() + 1);
b
};
let r = Some(Rect::new(0.0, 0.0, 1.0, 1.0));
for _ in 0..3 {
assert_eq!(measured.get("A", || measure(r)), r);
assert_eq!(measured.get("Missing", || measure(None)), None);
}
assert_eq!(count.get(), 2);
}
/// A rectangle `r` filled black, with a 2 pt black stroke.
fn stroked(d: &mut Document, r: Rect) -> Arc<Node> {
use vectorcraft_color::{Color, Paint};
let look = crate::Appearance::basic(Paint::solid(Color::BLACK), Paint::solid(Color::BLACK), 2.0);
Arc::new(Node::path(d.alloc_id(), vectorcraft_geom::shapes::rectangle(r), look))
}
fn instance(d: &mut Document, symbol: &str, xf: Affine) -> Arc<Node> {
Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: symbol.into(), xf }))
}
/// Symbol `name` holding `art` in a group.
fn symbol(d: &mut Document, name: &str, art: Vec<Arc<Node>>) {
let group = Node::group(d.alloc_id(), art);
d.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(group) });
}
#[test]
fn a_symbol_nested_in_symbol_art_is_measured_by_its_own_art() {
// `Far` is a rectangle from (200, 100) to (240, 130) with a 2 pt stroke, far from the
// 20 × 20 square around the origin.
let mut d = Document::new(500.0, 500.0);
let far = stroked(&mut d, Rect::new(200.0, 100.0, 240.0, 130.0));
symbol(&mut d, "Far", vec![far]);
let moved = instance(&mut d, "Far", Affine::translate((-100.0, 0.0)));
symbol(&mut d, "Moved", vec![moved.clone()]);
let scaled = instance(&mut d, "Far", Affine::scale(2.0));
symbol(&mut d, "Scaled", vec![scaled]);
symbol(&mut d, "Sized", vec![moved]);
d.unknown.insert(SYMBOL_SIZES.into(), serde_json::json!({ "Sized": [40.0, 20.0] }));
assert_eq!(d.symbol_art_bounds("Far"), Some(Rect::new(199.0, 99.0, 241.0, 131.0)));
// The stroke paints 1 pt around the moved rectangle.
assert_eq!(d.symbol_art_bounds("Moved"), Some(Rect::new(99.0, 99.0, 141.0, 131.0)));
// Scaled 2 times, the stroke keeps its weight.
assert_eq!(d.symbol_art_bounds("Scaled"), Some(Rect::new(399.0, 199.0, 481.0, 261.0)));
// At its natural size, 40 × 20 for the 20 × 20 square: 2 times across.
assert_eq!(d.symbol_art_bounds("Sized"), Some(Rect::new(198.0, 99.0, 282.0, 131.0)));
assert_eq!(d.inline_art_bounds("Moved"), d.symbol_art_bounds("Moved"));
}
#[test]
fn nested_instances_without_art_of_their_own_are_measured_by_the_square() {
let mut d = Document::new(500.0, 500.0);
let r = Rect::new(0.0, 0.0, 10.0, 10.0);
// An instance of a missing symbol and one of a symbol with no art, beside a rectangle.
symbol(&mut d, "Empty", vec![]);
let pieces = vec![stroked(&mut d, r), instance(&mut d, "Nowhere", Affine::translate((100.0, 0.0)))];
symbol(&mut d, "Missing", pieces);
let pieces = vec![stroked(&mut d, r), instance(&mut d, "Empty", Affine::translate((100.0, 0.0)))];
symbol(&mut d, "Holds empty", pieces);
let with_square = Some(Rect::new(-1.0, -10.0, 110.0, 11.0));
assert_eq!(d.symbol_art_bounds("Empty"), None);
assert_eq!(d.symbol_art_bounds("Missing"), with_square);
assert_eq!(d.symbol_art_bounds("Holds empty"), with_square);
// A symbol that holds an instance of itself: the instance is measured by the square, and
// an inline graphic of it is not an empty slot.
let pieces = vec![stroked(&mut d, r), instance(&mut d, "Loop", Affine::translate((100.0, 0.0)))];
symbol(&mut d, "Loop", pieces);
assert_eq!(d.symbol_art_bounds("Loop"), with_square);
assert_eq!(d.inline_art_bounds("Loop"), with_square);
// A chain of 40 symbols, each holding a rectangle and an instance of the next one 1 pt to
// the right: symbols nest 39 deep in Link 0's art, past the limit of 32, so its instance of
// Link 1 is measured by the square.
for i in 0..40 {
let unit = stroked(&mut d, Rect::new(0.0, 0.0, 1.0, 1.0));
let next = instance(&mut d, &format!("Link {}", i + 1), Affine::translate((1.0, 0.0)));
symbol(&mut d, &format!("Link {i}"), if i < 39 { vec![unit, next] } else { vec![unit] });
}
assert_eq!(d.symbol_art_bounds("Link 0"), Some(Rect::new(-9.0, -10.0, 11.0, 10.0)));
// Link 8's art nests 31 deep: measured by the art of the links after it.
assert_eq!(d.symbol_art_bounds("Link 8"), Some(Rect::new(-1.0, -1.0, 33.0, 2.0)));
}
/// Type showing the symbol named `symbol` inline.
fn showing(d: &mut Document, symbol: &str) -> Arc<Node> {
let style = crate::CharStyle::default();
let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "Type", style.clone());
t.runs.push(crate::TextRun::new("", style));
t.runs[1].inline = Some(crate::InlineArt { symbol: symbol.into(), scale: 1.0, baseline_shift: 0.0, bounds: None });
Arc::new(Node::new(d.alloc_id(), NodeKind::Text(Box::new(t))))
}
#[test]
fn only_instances_that_size_a_symbols_art_make_it_depend_on_their_symbol() {
// B's type shows A inline. A's art is a clip group: a clipping rectangle, then an instance
// of B that the rectangle clips, so A's size does not depend on B's.
let mut d = Document::new(500.0, 500.0);
let text = showing(&mut d, "A");
symbol(&mut d, "B", vec![text]);
let clip = stroked(&mut d, Rect::new(0.0, 0.0, 10.0, 10.0));
let inner = instance(&mut d, "B", Affine::IDENTITY);
let group = Arc::new(Node::new(d.alloc_id(), NodeKind::Group { children: vec![clip, inner.clone()], clip: true }));
symbol(&mut d, "A", vec![group]);
assert_eq!(d.inline_art_bounds("A"), Some(Rect::new(-1.0, -1.0, 11.0, 11.0)));
// Without the clip, A's size depends on B's and B's on A's: both are empty slots.
let art = Arc::new(Node::group(d.alloc_id(), vec![inner]));
if let Some(a) = d.symbols.iter_mut().find(|s| s.name == "A") {
a.art = art;
}
assert_eq!(d.inline_art_bounds("A"), None);
assert_eq!(d.inline_art_bounds("B"), None);
}
#[test]
fn components_handle_a_long_chain_without_recursion() {
let n = 200_000;
let edges: Vec<Vec<usize>> = (0..n).map(|i| if i + 1 < n { vec![i + 1] } else { vec![] }).collect();
let (order, cyclic) = components(&edges);
assert_eq!(order.first(), Some(&(n - 1)));
assert_eq!(order.last(), Some(&0));
assert!(cyclic.iter().all(|c| !c));
}
}