mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 20:36:02 +00:00
Merge pull request #7598 from dotCooCoo/fix/symbol-art-bounds-nested
Symbols: a symbol nested in symbol art is measured by its own art
This commit is contained in:
+177
-36
@@ -8,13 +8,20 @@ use std::sync::{Arc, RwLock, Weak};
|
||||
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
use crate::{Document, Node, NodeId, NodeKind, Symbol, TextObject};
|
||||
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)
|
||||
@@ -35,27 +42,38 @@ impl Document {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// [`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> {
|
||||
let s = self.symbols.iter().find(|s| s.name == name)?;
|
||||
let b = usable(s.art.visual_bounds().or_else(|| s.art.geometric_bounds()))?;
|
||||
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 in other symbols' art, so that its size would depend on itself.
|
||||
/// 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> {
|
||||
if self.inline_cycles().contains(name) { None } else { self.symbol_art_bounds(name) }
|
||||
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,
|
||||
/// found once while the symbols stay as they are.
|
||||
fn inline_cycles(&self) -> Arc<HashSet<String>> {
|
||||
/// 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 found = Arc::new(InlineGraph::of(self).cyclic);
|
||||
let graph = InlineGraph::of(self);
|
||||
let found = (Arc::new(graph.cyclic), graph.names);
|
||||
self.inline_cycles.set(&self.symbols, &found);
|
||||
found
|
||||
}
|
||||
@@ -94,7 +112,8 @@ impl Document {
|
||||
/// 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 show, so a symbol's art is measured with its type already laid out; a
|
||||
/// 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> {
|
||||
@@ -241,34 +260,48 @@ fn inline_symbols(n: &Node) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Each symbol name of `d` → the index of the first symbol of that name (the one a name finds).
|
||||
fn symbol_index(d: &Document) -> HashMap<&str, usize> {
|
||||
let mut index = HashMap::with_capacity(d.symbols.len());
|
||||
for (i, s) in d.symbols.iter().enumerate() {
|
||||
index.entry(s.name.as_str()).or_insert(i);
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
/// The symbols of a document as their inline graphics depend on each other.
|
||||
/// 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 shows inline (except along a cycle).
|
||||
/// 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.
|
||||
/// 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 the order and cycles are
|
||||
/// found in time linear in the symbols and their inline references (Tarjan's strongly
|
||||
/// connected components, without recursion).
|
||||
/// 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 index = symbol_index(d);
|
||||
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()
|
||||
.map(|s| {
|
||||
let mut e: Vec<usize> = inline_symbols(&s.art).iter().filter_map(|n| index.get(n.as_str()).copied()).collect();
|
||||
.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
|
||||
@@ -276,12 +309,13 @@ impl InlineGraph {
|
||||
.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 }
|
||||
Self { order, cyclic, names }
|
||||
}
|
||||
|
||||
/// [`Document::inline_art_bounds`] with the cycles found in this graph.
|
||||
/// [`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(name) }
|
||||
if self.cyclic.contains(name) { None } else { d.symbol_art_bounds_in(name, &d.symbol_boxes().with_names(&self.names)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,12 +388,16 @@ fn components(edges: &[Vec<usize>]) -> (Vec<usize>, Vec<bool>) {
|
||||
#[derive(Default)]
|
||||
pub(crate) struct InlineCycles(RwLock<Option<Arc<Found>>>);
|
||||
|
||||
/// The name and art of each symbol the cycles were found in, and the names of the symbols on one.
|
||||
type Found = (Vec<(String, Weak<Node>)>, Arc<HashSet<String>>);
|
||||
/// 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<Arc<HashSet<String>>> {
|
||||
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()
|
||||
@@ -368,7 +406,7 @@ impl InlineCycles {
|
||||
}
|
||||
|
||||
/// Keep `found`, found in `symbols`.
|
||||
fn set(&self, symbols: &[Symbol], found: &Arc<HashSet<String>>) {
|
||||
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())));
|
||||
}
|
||||
@@ -485,6 +523,109 @@ mod tests {
|
||||
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;
|
||||
|
||||
+86
-27
@@ -2,7 +2,7 @@
|
||||
//! the document and measures it once, and an instance's bounds are its art's box placed by the
|
||||
//! instance's transform ([`Node::geometric_bounds_with`]).
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::{Cell, OnceCell, RefCell};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -26,6 +26,9 @@ 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]);
|
||||
|
||||
/// Each symbol name of a document → the index of the first symbol of that name.
|
||||
pub(crate) type SymbolNames = HashMap<String, usize>;
|
||||
|
||||
/// A symbol's art measured in symbol space.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct ArtBox {
|
||||
@@ -51,6 +54,10 @@ pub(crate) struct ArtBox {
|
||||
/// symbols change.
|
||||
pub struct SymbolBoxes<'a> {
|
||||
doc: &'a Document,
|
||||
/// Each symbol name of the document → the index of the first symbol of that name (the one a
|
||||
/// name finds), made the first time a name is looked up ([`Self::symbol_art`]) or given by
|
||||
/// [`Self::with_names`].
|
||||
names: OnceCell<Arc<SymbolNames>>,
|
||||
/// The art boxes measured so far, by symbol name (`None` for a symbol whose instances are
|
||||
/// measured by [`SYMBOL_SQUARE`]).
|
||||
measured: RefCell<HashMap<String, Option<ArtBox>>>,
|
||||
@@ -89,6 +96,9 @@ pub struct SymbolBoxes<'a> {
|
||||
sliced_most: u64,
|
||||
/// The widths [`Self::measured_width`] has measured, by kind and symbol name.
|
||||
widths: RefCell<HashMap<(&'static str, String), Option<f64>>>,
|
||||
/// While [`Self::consulted`] runs: the symbols whose art boxes [`Self::art`] has given outside
|
||||
/// the measuring of symbol art.
|
||||
consulted: RefCell<Option<HashSet<String>>>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
@@ -96,6 +106,7 @@ impl Document {
|
||||
pub fn symbol_boxes(&self) -> SymbolBoxes<'_> {
|
||||
SymbolBoxes {
|
||||
doc: self,
|
||||
names: OnceCell::new(),
|
||||
measured: RefCell::default(),
|
||||
depths: RefCell::default(),
|
||||
quick: Cell::new(false),
|
||||
@@ -110,8 +121,19 @@ impl Document {
|
||||
sliced_placed: Cell::new(0),
|
||||
sliced_most: MAX_SYMBOL_NODES,
|
||||
widths: RefCell::default(),
|
||||
consulted: RefCell::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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);
|
||||
}
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SymbolBoxes<'a> {
|
||||
@@ -120,6 +142,26 @@ impl<'a> SymbolBoxes<'a> {
|
||||
self.doc
|
||||
}
|
||||
|
||||
/// Makes these boxes find symbols by `names` ([`Document::symbol_names`]), made from the
|
||||
/// document's symbols while they had the names and order they have now.
|
||||
pub(crate) fn with_names(self, names: &Arc<SymbolNames>) -> Self {
|
||||
let _ = self.names.set(names.clone());
|
||||
self
|
||||
}
|
||||
|
||||
/// The art of the first symbol of the document named `name`, found in time independent of the
|
||||
/// number of symbols after the first lookup.
|
||||
pub(crate) fn symbol_art(&self, name: &str) -> Option<&'a Arc<Node>> {
|
||||
let doc: &'a Document = self.doc;
|
||||
let names = self.names.get_or_init(|| Arc::new(doc.symbol_names()));
|
||||
let i = *names.get(name)?;
|
||||
match doc.symbols.get(i) {
|
||||
Some(s) if s.name == name => Some(&s.art),
|
||||
// Names that no longer match the symbols: the first symbol of that name.
|
||||
_ => doc.symbols.iter().find(|s| s.name == name).map(|s| &s.art),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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. The
|
||||
@@ -160,21 +202,37 @@ impl<'a> SymbolBoxes<'a> {
|
||||
/// art boxes. Returns `None` when [`Self::depth`] is `None` and when the art has no finite
|
||||
/// bounds: instances of the symbol are then measured by [`SYMBOL_SQUARE`].
|
||||
pub(crate) fn art(&self, name: &str) -> Option<ArtBox> {
|
||||
if let Some(b) = self.measured.borrow().get(name) {
|
||||
return *b;
|
||||
let cached = self.measured.borrow().get(name).copied();
|
||||
let b = cached.unwrap_or_else(|| {
|
||||
let b = self.measure_art(name, |art| self.in_art(|| art_box(art, self))).flatten();
|
||||
self.measured.borrow_mut().insert(name.to_string(), b);
|
||||
b
|
||||
});
|
||||
if b.is_some()
|
||||
&& let Some(seen) = self.consulted.borrow_mut().as_mut()
|
||||
&& !seen.contains(name)
|
||||
{
|
||||
seen.insert(name.to_string());
|
||||
}
|
||||
let b = self.measure_art(name, |art| self.in_art(|| art_box(art, self))).flatten();
|
||||
self.measured.borrow_mut().insert(name.to_string(), b);
|
||||
b
|
||||
}
|
||||
|
||||
/// Runs `measure`, and returns the names of the symbols whose art boxes it took from these
|
||||
/// boxes, the art of symbols it measured left out: the symbols whose art the instances it
|
||||
/// measured are measured by.
|
||||
pub(crate) fn consulted(&self, measure: impl FnOnce()) -> HashSet<String> {
|
||||
let was = self.consulted.replace(Some(HashSet::new()));
|
||||
measure();
|
||||
self.consulted.replace(was).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Runs `measure` on the art of the symbol named `name`. Returns `None` when [`Self::depth`]
|
||||
/// is `None`, without running it.
|
||||
pub fn measure_art<R>(&self, name: &str, measure: impl FnOnce(&Arc<Node>) -> R) -> Option<R> {
|
||||
self.depth(name)?;
|
||||
let sym = self.doc.symbols.iter().find(|s| s.name == name)?;
|
||||
let art = self.symbol_art(name)?;
|
||||
self.runs.set(self.runs.get() + 1);
|
||||
Some(measure(&sym.art))
|
||||
Some(measure(art))
|
||||
}
|
||||
|
||||
/// How many times [`Self::measure_art`] has run a measure on symbol art for these boxes, the
|
||||
@@ -297,10 +355,9 @@ impl<'a> SymbolBoxes<'a> {
|
||||
/// other symbols it reaches: the depth of each one it walks to the end, and `None` for those on
|
||||
/// the way down to a symbol that holds an instance of itself or nests too deep.
|
||||
fn find_depth(&self, name: &str, learned: &mut HashMap<String, Option<usize>>) -> Option<usize> {
|
||||
let art = |n: &str| self.doc.symbols.iter().find(|s| s.name == n).map(|s| &s.art);
|
||||
// The symbols on the way down from `name`: each with the symbols its art shows that are
|
||||
// still to be walked, and the deepest nesting found under it so far.
|
||||
let mut path: Vec<(String, Vec<String>, usize)> = vec![(name.to_string(), shown(self.doc, art(name)?), 0)];
|
||||
let mut path: Vec<(String, Vec<String>, usize)> = vec![(name.to_string(), self.shown(self.symbol_art(name)?), 0)];
|
||||
loop {
|
||||
let (_, todo, _) = path.last_mut()?;
|
||||
let Some(next) = todo.pop() else {
|
||||
@@ -330,8 +387,8 @@ impl<'a> SymbolBoxes<'a> {
|
||||
// Symbols nested MAX_DEPTH deep under `name`.
|
||||
None if path.len() >= MAX_DEPTH => return None,
|
||||
None => {
|
||||
if let Some(a) = art(&next) {
|
||||
let todo = shown(self.doc, a);
|
||||
if let Some(a) = self.symbol_art(&next) {
|
||||
let todo = self.shown(a);
|
||||
path.push((next, todo, 0));
|
||||
}
|
||||
}
|
||||
@@ -379,7 +436,7 @@ impl<'a> SymbolBoxes<'a> {
|
||||
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 art = self.symbol_art(name)?;
|
||||
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,
|
||||
@@ -431,11 +488,28 @@ impl<'a> SymbolBoxes<'a> {
|
||||
fn in_art<R>(&self, f: impl FnOnce() -> R) -> R {
|
||||
let pad = self.brush_art.replace(false);
|
||||
let strokes = self.art_strokes.replace(true);
|
||||
let consulted = self.consulted.take();
|
||||
let r = self.quickly(f);
|
||||
self.brush_art.set(pad);
|
||||
self.art_strokes.set(strokes);
|
||||
*self.consulted.borrow_mut() = consulted;
|
||||
r
|
||||
}
|
||||
|
||||
/// The names of the symbols of the document that the instances in `art` show, each once.
|
||||
fn shown(&self, art: &Node) -> Vec<String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut names = vec![];
|
||||
art.walk(&mut |n| {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
|
||||
&& seen.insert(symbol.as_str())
|
||||
&& self.symbol_art(symbol).is_some()
|
||||
{
|
||||
names.push(symbol.clone());
|
||||
}
|
||||
});
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
/// Records every symbol on `path` in `learned` as one whose instances are measured by
|
||||
@@ -447,21 +521,6 @@ fn give_up(path: &[(String, Vec<String>, usize)], learned: &mut HashMap<String,
|
||||
None
|
||||
}
|
||||
|
||||
/// The names of the symbols of `doc` that the instances in `art` show, each once.
|
||||
fn shown(doc: &Document, art: &Node) -> Vec<String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut names = vec![];
|
||||
art.walk(&mut |n| {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
|
||||
&& seen.insert(symbol.as_str())
|
||||
&& doc.symbols.iter().any(|s| s.name == *symbol)
|
||||
{
|
||||
names.push(symbol.clone());
|
||||
}
|
||||
});
|
||||
names
|
||||
}
|
||||
|
||||
/// Measures symbol art `art`, with the instances in it measured by `symbols`.
|
||||
fn art_box(art: &Arc<Node>, symbols: &SymbolBoxes) -> Option<ArtBox> {
|
||||
let finite = |r: &Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite());
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Inline graphics in text: `text.insertInline`, editing over them, undo, following their symbol.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::text::INLINE_CHAR;
|
||||
use vectorcraft_doc::{Node, NodeId, NodeKind, TextObject};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
use crate::*;
|
||||
@@ -325,6 +329,143 @@ fn a_symbol_that_shows_itself_inline_is_an_empty_slot() {
|
||||
assert_eq!(d.inline_art_bounds("Loop"), None);
|
||||
}
|
||||
|
||||
/// A rectangle `r` filled black, without a stroke.
|
||||
fn filled_rect(d: &mut vectorcraft_doc::Document, r: Rect) -> Arc<Node> {
|
||||
let look = vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
|
||||
Arc::new(Node::path(d.alloc_id(), vectorcraft_geom::shapes::rectangle(r), look))
|
||||
}
|
||||
|
||||
/// An instance of symbol `name` placed by `xf`.
|
||||
fn symbol_instance(d: &mut vectorcraft_doc::Document, name: &str, xf: Affine) -> Arc<Node> {
|
||||
Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: name.into(), xf }))
|
||||
}
|
||||
|
||||
/// Gives symbol `name` the art `art` in a group, adding the symbol when there is none of that name.
|
||||
fn set_symbol_art(d: &mut vectorcraft_doc::Document, name: &str, art: Vec<Arc<Node>>) {
|
||||
let art = Arc::new(Node::group(d.alloc_id(), art));
|
||||
match d.symbols.iter_mut().find(|x| x.name == name) {
|
||||
Some(x) => x.art = art,
|
||||
None => d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: name.into(), art }),
|
||||
}
|
||||
}
|
||||
|
||||
/// An inline graphic of a symbol whose art holds an instance of another symbol is sized by that
|
||||
/// symbol's art, and follows it when it changes, through undo, redo and a saved copy.
|
||||
#[test]
|
||||
fn an_inline_graphic_follows_the_art_of_a_symbol_nested_in_its_symbol() {
|
||||
let mut s = session();
|
||||
s.edit("Symbols", |d, _| {
|
||||
let far = filled_rect(d, Rect::new(100.0, 0.0, 160.0, 20.0));
|
||||
set_symbol_art(d, "Far", vec![far]);
|
||||
let inner = symbol_instance(d, "Far", Affine::IDENTITY);
|
||||
set_symbol_art(d, "Holder", vec![inner]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let id = text(&mut s, "ab");
|
||||
let w0 = obj(&s, id).bounds().unwrap().width();
|
||||
s.execute("text.insertInline", &json!({"id": id.0, "at": 1, "symbol": "Holder", "scale": 1})).unwrap();
|
||||
let wide = Some(Rect::new(100.0, 0.0, 160.0, 20.0));
|
||||
let t = obj(&s, id);
|
||||
assert_eq!(inline_bounds(&t), wide);
|
||||
// 20 pt tall at 20 pt type and 3 times as wide: the text grows by 60 pt.
|
||||
let w1 = t.bounds().unwrap().width();
|
||||
assert!((w1 - w0 - 60.0).abs() < 1.5, "{w0} → {w1}");
|
||||
// Far redefined as a 10 × 40 rectangle: the graphic is 20 pt tall and 5 pt wide.
|
||||
s.edit("Redefine Far", |d, _| {
|
||||
let tall = filled_rect(d, Rect::new(0.0, 0.0, 10.0, 40.0));
|
||||
set_symbol_art(d, "Far", vec![tall]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let tall = Some(Rect::new(0.0, 0.0, 10.0, 40.0));
|
||||
let t = obj(&s, id);
|
||||
assert_eq!(inline_bounds(&t), tall);
|
||||
assert_eq!(s.doc().unwrap().doc.symbol_art_bounds("Holder"), tall);
|
||||
let w2 = t.bounds().unwrap().width();
|
||||
assert!((w2 - w0 - 5.0).abs() < 1.5, "{w0} → {w2}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(inline_bounds(&obj(&s, id)), wide);
|
||||
assert!((obj(&s, id).bounds().unwrap().width() - w1).abs() < 1e-6);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(inline_bounds(&obj(&s, id)), tall);
|
||||
// Opening a saved copy measures it the same way.
|
||||
let doc = (*s.doc().unwrap().doc).clone();
|
||||
let mut back = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap();
|
||||
back.title = "copy".into();
|
||||
let mut s2 = Session::new();
|
||||
s2.add_document(back, None);
|
||||
assert_eq!(inline_bounds(&obj(&s2, id)), tall);
|
||||
assert!((obj(&s2, id).bounds().unwrap().width() - w2).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Symbol C shows Holder inline, Holder holds an instance of B, and B shows A inline: with the
|
||||
/// symbols listed C, Holder, B, A, a change to A reaches C through B's and Holder's new sizes.
|
||||
#[test]
|
||||
fn symbols_are_resolved_after_the_symbols_their_instances_show() {
|
||||
let mut s = session();
|
||||
circle_symbol(&mut s, "A", 10.0);
|
||||
let t = text(&mut s, "z");
|
||||
s.execute("text.insertInline", &json!({"id": t.0, "at": 0, "symbol": "A"})).unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "B", "ids": [t.0]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
s.edit("Holder", |d, _| {
|
||||
let inner = symbol_instance(d, "B", Affine::IDENTITY);
|
||||
set_symbol_art(d, "Holder", vec![inner]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let t = text(&mut s, "z");
|
||||
s.execute("text.insertInline", &json!({"id": t.0, "at": 0, "symbol": "Holder"})).unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "C", "ids": [t.0]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
s.edit("Reverse the symbols", |d, _| {
|
||||
d.symbols.reverse();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let h0 = s.doc().unwrap().doc.symbol_art_bounds("Holder");
|
||||
redefine_as_wide_ellipse(&mut s, "A");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_ne!(d.symbol_art_bounds("Holder"), h0);
|
||||
assert_eq!(inline_bounds(&art_text(&s, "C")), d.symbol_art_bounds("Holder"));
|
||||
}
|
||||
|
||||
/// Symbol A holds an instance of B, and B's type shows A inline: the size of each would depend on
|
||||
/// itself, so both are empty slots, and stay so through later edits.
|
||||
#[test]
|
||||
fn a_symbol_that_shows_itself_through_an_instance_is_an_empty_slot() {
|
||||
let mut s = session();
|
||||
circle_symbol(&mut s, "A", 10.0);
|
||||
let t = text(&mut s, "ab");
|
||||
s.execute("text.insertInline", &json!({"id": t.0, "at": 1, "symbol": "A"})).unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "B", "ids": [t.0]})).unwrap();
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
s.edit("A holds B", |d, _| {
|
||||
let inner = symbol_instance(d, "B", Affine::IDENTITY);
|
||||
set_symbol_art(d, "A", vec![inner]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let line = text(&mut s, "x");
|
||||
s.execute("text.insertInline", &json!({"id": line.0, "at": 0, "symbol": "A"})).unwrap();
|
||||
for _ in 0..2 {
|
||||
for name in ["A", "B"] {
|
||||
assert_eq!(s.doc().unwrap().doc.inline_art_bounds(name), None, "{name}");
|
||||
}
|
||||
assert_eq!(inline_bounds(&art_text(&s, "B")), None);
|
||||
assert_eq!(inline_bounds(&obj(&s, line)), None);
|
||||
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
}
|
||||
let _ = s.execute("document.serialize", &json!({"format": "png"})).unwrap();
|
||||
// Redefined without the instance, A is on no cycle.
|
||||
redefine_as_wide_ellipse(&mut s, "A");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(d.inline_art_bounds("A").is_some());
|
||||
assert_eq!(inline_bounds(&art_text(&s, "B")), d.symbol_art_bounds("A"));
|
||||
assert_eq!(inline_bounds(&obj(&s, line)), d.symbol_art_bounds("A"));
|
||||
}
|
||||
|
||||
/// A list paragraph that starts with an inline graphic draws its bullet before the graphic, and
|
||||
/// live SVG text writes the bullet.
|
||||
#[test]
|
||||
|
||||
@@ -1325,6 +1325,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Manual kerning in right-to-left text:** a manual kerning value on a Hebrew or Arabic character changes the space between it and the next character in reading order, the one drawn to its left, under Auto and Optical kerning. The character before it keeps its place, and the caret between the pair stays beside the next character. The side the space goes on is VectorCraft's own rule. Behavior reference: [Line and character spacing](https://web.archive.org/web/20260916050333/https://helpx.adobe.com/illustrator/using/line-character-spacing.html). Test: `rtl_manual_kerning_spaces_the_character_from_the_next_in_reading_order`.
|
||||
- **Manual kerning on inline graphics:** a manual kerning value on an inline graphic's character spaces it from the next character in reading order by the same distance as on a letter of its run's size, in left-to-right, right-to-left and vertical type; Auto and Optical kerning still apply no font pairs to it. This is VectorCraft's own rule. Test: `manual_kerning_on_an_inline_graphic_spaces_it_like_a_letter`.
|
||||
- **Inline graphics in symbol art follow their symbols:** when a symbol changes, type with an inline graphic of it is laid out again wherever the type is: in the layers, in the art of other symbols, graph designs and pattern tiles, and in opacity masks. A symbol's type is laid out before the type that shows that symbol inline, so a symbol whose art holds an inline graphic, its instances and the inline graphics that show it take its new size. Opening and reverting a document and Refresh Font List resolve them in the same order. An inline graphic of a symbol whose art shows that symbol inline, directly or through other symbols, is an empty slot. This is VectorCraft's own rule. Tests: `inline_graphics_in_symbol_art_follow_their_symbol`, `inline_graphics_in_masks_and_pattern_tiles_follow_their_symbol`, `symbols_are_resolved_after_the_symbols_they_show_inline`, `a_symbol_that_shows_itself_inline_is_an_empty_slot` in `crates/engine/src/tests/inline.rs`.
|
||||
- **Inline graphics of symbols that hold other symbols:** an inline graphic of a symbol whose art holds an instance of another symbol is sized by that symbol's own art, placed by the instance, and follows it when that symbol changes. A symbol's type is laid out after the type in the art of the symbols its instances show. An inline graphic of a symbol whose art reaches that symbol through an instance and an inline graphic is an empty slot; an instance that does not size the art, such as one a clipping path hides, does not count. An instance of a missing symbol, of a symbol without art, of a symbol that holds an instance of itself, or of a symbol whose art nests symbols 32 deep is measured by the 20 × 20 square. This is VectorCraft's own rule. Tests: `an_inline_graphic_follows_the_art_of_a_symbol_nested_in_its_symbol`, `symbols_are_resolved_after_the_symbols_their_instances_show`, `a_symbol_that_shows_itself_through_an_instance_is_an_empty_slot` in `crates/engine/src/tests/inline.rs`; `a_symbol_nested_in_symbol_art_is_measured_by_its_own_art`, `nested_instances_without_art_of_their_own_are_measured_by_the_square`, `only_instances_that_size_a_symbols_art_make_it_depend_on_their_symbol` in `crates/doc/src/inline.rs`.
|
||||
- **Recent font pickers (#3515):** The shared Character, Properties, Glyphs and Find Font family picker lists recently applied families first, once each, in most-recent order and limited by Preferences › Type › Number of Recent Fonts. Search and filters apply to both sections; unavailable families are omitted. Section headings cannot be selected or previewed. Confirmed character-range font edits update the same saved history; previews and failed edits do not. Requirement: [issue #3515](https://github.com/storytold/vectorcraft/issues/3515).
|
||||
- **Show samples on white (a VectorCraft addition):** Preferences › Type › Show samples on white (`fontSamplesOnWhite`, off by default) draws each family's sample in every font menu, and in Type → Font in the in-window menu bar, in dark type on a white area in every interface brightness. A highlighted row's color shows above and below the white area, and the names and the rest of the menu keep the interface's colors. The checkbox is dimmed while Enable in-menu font previews is off. Tests: `crates/ui-egui/src/tests/font_menu.rs`, `crates/ui-egui/src/font_menu/tests.rs`, `crates/ui-egui/src/prefs_dialog/tests.rs`.
|
||||
- **Find Font: Replace With Font From:** a dropdown under Replace With Font chooses the list the replacement font comes from: Document lists the families the document uses, once each; Recent lists the recently applied families that are installed (fonts applied with Find Font's Change and Change All included), newest first, as many as Preferences › Type › Number of Recent Fonts allows; System, the default, lists the installed families available in the font menu. The dialog opens on System with Source Sans 3 chosen, or the font menu's first family when Source Sans 3 is not installed. Choosing a list keeps the chosen font when the list has it and it is installed; otherwise Document and Recent choose the list's first installed font, and System chooses Source Sans 3 or the font menu's first family. Change and Change All are unavailable when the list has no installed font (Recent before any font was applied) and when the chosen family is not installed or is not in the shown list. Behavior reference: [Find and replace fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-and-replace-fonts.html) (public documentation). Tests: `crates/ui-egui/src/find_font.rs`.
|
||||
|
||||
Reference in New Issue
Block a user