mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
306 lines
15 KiB
Rust
306 lines
15 KiB
Rust
//! Use Preview Bounds: objects measured as they are drawn, with their geometry effects.
|
|
//!
|
|
//! [`drawn_art`] gives an object whose geometry effects change what it draws as plain art. A path
|
|
//! or compound path is given as its effected geometry, and a fill or stroke with geometry effects
|
|
//! of its own paints its own geometry. Type, images, live objects and symbol instances are given as
|
|
//! their reshaped art ([`reshape`]), and a symbol instance is also given as its reshaped art when
|
|
//! its symbol's art has geometry effects. Groups and layers with geometry or Pathfinder effects of
|
|
//! their own, or with a fill or stroke of their own that has geometry effects or paints members
|
|
//! with geometry effects, are given as their evaluated art ([`evaluate_container`]). Blends,
|
|
//! envelopes and repeats whose objects have geometry effects are given as their evaluated art
|
|
//! ([`expanded_live_group`]), with each of those objects given as its own art. A clip group or
|
|
//! layer whose clipping path has strokes with geometry effects is given as that path's geometry
|
|
//! before its effects, which the renderer clips by, and those strokes as drawn. Fills and
|
|
//! strokes that paint nothing (hidden, or painted None) are left out. [`bounds_of`],
|
|
//! [`oriented_bounds`] and [`anchor_bounds`] measure the visual bounds of that art, strokes
|
|
//! included, where [`Document::bounds_of`], [`Document::oriented_bounds`] and
|
|
//! [`Selection::anchor_bounds`] measure the objects themselves. Raster effects (shadows, glows,
|
|
//! blurs) are not measured. In one object, the art of at most 4,096 symbol instances is measured
|
|
//! with its geometry effects, and the other instances in it are measured without them.
|
|
|
|
use std::cell::{Cell, RefCell};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use vectorcraft_doc::color::Paint;
|
|
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeId, NodeKind, OrientedBox, Selection, SymbolBoxes};
|
|
use vectorcraft_geom::{Affine, PathData, Rect, Shape, union_opt};
|
|
|
|
use crate::group::has_own_paint;
|
|
use crate::reshape::{geometry_effects, leaf_geometry};
|
|
use crate::{
|
|
GeomContext, apply_geometry_with, evaluate_container, expanded_live_group, has_geometry, has_item_geometry, has_pathfinder, needs_outline,
|
|
outline_art, paints, reshape,
|
|
};
|
|
|
|
/// Most symbol instances in one object whose art [`Drawn`] measures with its geometry effects:
|
|
/// past it, the other instances in the object are measured without geometry effects.
|
|
const MAX_INSTANCES: usize = 4096;
|
|
|
|
/// The bounds of `ids`: with `preview`, their visual bounds as drawn ([`visual_bounds`]), else
|
|
/// their geometric bounds ([`Document::bounds_of`]).
|
|
pub fn bounds_of(doc: &Document, ids: &[NodeId], preview: bool) -> Option<Rect> {
|
|
if !preview {
|
|
return doc.bounds_of(ids, false);
|
|
}
|
|
let symbols = doc.symbol_boxes();
|
|
let drawn = Drawn::new(&symbols);
|
|
ids.iter().filter_map(|id| doc.node(*id)).fold(None, |acc, n| union_opt(acc, drawn.next().visual_bounds(n)))
|
|
}
|
|
|
|
/// The bounding box of `ids` square to their shared angle ([`Document::bbox_angle`]), measured as
|
|
/// [`bounds_of`] measures them: [`Document::oriented_bounds`] with `preview` false.
|
|
pub fn oriented_bounds(doc: &Document, ids: &[NodeId], preview: bool) -> Option<OrientedBox> {
|
|
if !preview {
|
|
return doc.oriented_bounds(ids, false);
|
|
}
|
|
let angle = doc.bbox_angle(ids);
|
|
if angle == 0.0 {
|
|
return bounds_of(doc, ids, true).map(OrientedBox::aligned);
|
|
}
|
|
let to_local = OrientedBox { angle, rect: Rect::ZERO }.to_doc().inverse();
|
|
let symbols = doc.symbol_boxes();
|
|
let drawn = Drawn::new(&symbols);
|
|
let rect = ids.iter().filter_map(|id| doc.node(*id)).fold(None, |acc, n| union_opt(acc, drawn.next().bounds_in(n, to_local)))?;
|
|
Some(OrientedBox { angle, rect })
|
|
}
|
|
|
|
/// With anchors selected with Direct Selection, the box around those anchors and the objects
|
|
/// selected whole, measured by [`bounds_of`]. `None` without any ([`Selection::anchor_bounds`]).
|
|
pub fn anchor_bounds(selection: &Selection, doc: &Document, preview: bool) -> Option<Rect> {
|
|
if selection.anchors.is_empty() {
|
|
return None;
|
|
}
|
|
union_opt(bounds_of(doc, &selection.whole(), preview), selection.anchors_box(doc))
|
|
}
|
|
|
|
/// The visual bounds of `n` as drawn: those of its [`drawn_art`], else
|
|
/// [`Node::visual_bounds_with`].
|
|
pub fn visual_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
|
Drawn::new(symbols).visual_bounds(n)
|
|
}
|
|
|
|
/// [`visual_bounds`] of `n` as `a` would place it, without changing it ([`Node::bounds_in`]):
|
|
/// the art is drawn where `n` is and then placed by `a`.
|
|
pub fn bounds_in(symbols: &SymbolBoxes, n: &Node, a: Affine) -> Option<Rect> {
|
|
Drawn::new(symbols).bounds_in(n, a)
|
|
}
|
|
|
|
/// `n` as plain art with the geometry effects in it applied, at every depth, symbol instances
|
|
/// measured by `symbols`. Raster effects stay on the art. A group, layer or intertwine without a
|
|
/// clip is rebuilt with the drawn art of each member that has one. A clip group or layer becomes a
|
|
/// group of its clipping path without strokes and the drawn art of the path's strokes, when they
|
|
/// have one. `None` when no geometry effect in `n` changes what it draws.
|
|
pub fn drawn_art(symbols: &SymbolBoxes, n: &Node) -> Option<Node> {
|
|
Drawn::new(symbols).drawn_art(n)
|
|
}
|
|
|
|
/// Whether [`drawn_art`] gives `n` art of its own, or would give one to an object in it. A symbol
|
|
/// instance in `n` counts when its symbol's art has a geometry effect.
|
|
pub fn draws_geometry(symbols: &SymbolBoxes, n: &Node) -> bool {
|
|
Drawn::new(symbols).draws_geometry(n)
|
|
}
|
|
|
|
/// One measurement: whether the art of each symbol checked so far draws geometry, by symbol name,
|
|
/// and how many more instances' art it measures with its geometry effects in the current object ([`MAX_INSTANCES`]).
|
|
struct Drawn<'s, 'd> {
|
|
symbols: &'s SymbolBoxes<'d>,
|
|
draws: RefCell<HashMap<String, bool>>,
|
|
left: Cell<usize>,
|
|
}
|
|
|
|
impl<'s, 'd> Drawn<'s, 'd> {
|
|
fn new(symbols: &'s SymbolBoxes<'d>) -> Self {
|
|
Drawn { symbols, draws: RefCell::default(), left: Cell::new(MAX_INSTANCES) }
|
|
}
|
|
|
|
/// Sets the count of instances left back to [`MAX_INSTANCES`], for the next object.
|
|
fn next(&self) -> &Self {
|
|
self.left.set(MAX_INSTANCES);
|
|
self
|
|
}
|
|
|
|
/// See [`visual_bounds`].
|
|
fn visual_bounds(&self, n: &Node) -> Option<Rect> {
|
|
match self.drawn_art(n) {
|
|
Some(art) => art.visual_bounds_with(self.symbols),
|
|
None => n.visual_bounds_with(self.symbols),
|
|
}
|
|
}
|
|
|
|
/// See [`bounds_in`].
|
|
fn bounds_in(&self, n: &Node, a: Affine) -> Option<Rect> {
|
|
if a == Affine::IDENTITY {
|
|
return self.visual_bounds(n);
|
|
}
|
|
match self.drawn_art(n) {
|
|
Some(art) => art.bounds_in(self.symbols, a, true),
|
|
None => n.bounds_in(self.symbols, a, true),
|
|
}
|
|
}
|
|
|
|
/// See [`drawn_art`].
|
|
fn drawn_art(&self, n: &Node) -> Option<Node> {
|
|
let Some(mut art) = self.own_art(n) else { return self.clip_art(n).or_else(|| self.with_drawn_members(n)) };
|
|
// The art's members can carry geometry effects of their own: type's fills and strokes, the
|
|
// members of an evaluated group, the clipping path of an evaluated clip group and the
|
|
// objects in a symbol's art.
|
|
if let Some(m) = self.clip_art(&art).or_else(|| self.with_drawn_members(&art)) {
|
|
art = m;
|
|
}
|
|
Some(art)
|
|
}
|
|
|
|
/// See [`draws_geometry`].
|
|
fn draws_geometry(&self, n: &Node) -> bool {
|
|
let mut found = false;
|
|
n.walk(&mut |c| {
|
|
found = found
|
|
|| match &c.kind {
|
|
NodeKind::SymbolInstance { symbol, .. } => !geometry_effects(c).is_empty() || self.symbol_draws(symbol),
|
|
_ => reshapes(c),
|
|
};
|
|
});
|
|
found
|
|
}
|
|
|
|
/// Whether the art of the symbol named `name` has effects and [`Self::draws_geometry`], worked
|
|
/// out once for each name.
|
|
fn symbol_draws(&self, name: &str) -> bool {
|
|
if let Some(d) = self.draws.borrow().get(name) {
|
|
return *d;
|
|
}
|
|
let d = self.symbols.has_effects(name) && self.symbols.measure_art(name, |art| self.draws_geometry(art)).unwrap_or(false);
|
|
self.draws.borrow_mut().insert(name.to_string(), d);
|
|
d
|
|
}
|
|
|
|
/// The art the geometry effects of `n` itself give it, or that a symbol instance gets from the
|
|
/// geometry effects in its symbol's art. A group or layer with fills or strokes of its own that
|
|
/// paint, around members with geometry effects, is evaluated too: its fills and strokes paint
|
|
/// the members' geometry before their effects. A blend, envelope or repeat whose objects have
|
|
/// geometry effects is evaluated as the renderer evaluates it ([`expanded_live_group`]), and
|
|
/// the objects in its evaluated art keep their geometry effects. `None` when it has none, and
|
|
/// for a symbol instance past [`MAX_INSTANCES`].
|
|
fn own_art(&self, n: &Node) -> Option<Node> {
|
|
let inner = || n.children().is_some_and(|ch| ch.iter().any(|c| self.draws_geometry(c)));
|
|
let painted = || has_own_paint(n) && inner();
|
|
match &n.kind {
|
|
NodeKind::Group { .. } | NodeKind::Layer { .. } if reshapes(n) || painted() => evaluate_container(Some(self.symbols.document()), n),
|
|
NodeKind::Path { .. } | NodeKind::Compound { .. } if reshapes(n) => path_art(n),
|
|
NodeKind::SymbolInstance { symbol, .. } => {
|
|
if geometry_effects(n).is_empty() && !self.symbol_draws(symbol) {
|
|
return None;
|
|
}
|
|
let left = self.left.get().checked_sub(1)?;
|
|
self.left.set(left);
|
|
let art = self.symbols.measure_art(symbol, Arc::clone)?;
|
|
// An instance in perspective is projected by `reshape`.
|
|
let doc = Some(self.symbols.document());
|
|
reshape(doc, n, Some(&*art)).or_else(|| outline_art(doc, n, Some(&*art)))
|
|
}
|
|
_ if reshapes(n) => reshape(Some(self.symbols.document()), n, None),
|
|
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) if inner() => {
|
|
Some(expanded_live_group(Some(self.symbols.document()), n))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// `n`, a group, layer or intertwine without a clip, with each member that has a
|
|
/// [`Self::drawn_art`] replaced by it. `None` for any other object and when no member has one.
|
|
fn with_drawn_members(&self, n: &Node) -> Option<Node> {
|
|
if n.clips() || !matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. } | NodeKind::Intertwine { .. }) {
|
|
return None;
|
|
}
|
|
let drawn: Vec<(usize, Node)> = n.children()?.iter().enumerate().filter_map(|(i, c)| Some((i, self.drawn_art(c)?))).collect();
|
|
if drawn.is_empty() {
|
|
return None;
|
|
}
|
|
let mut m = n.clone();
|
|
let children = m.children_mut()?;
|
|
for (i, c) in drawn {
|
|
if let Some(slot) = children.get_mut(i) {
|
|
*slot = Arc::new(c);
|
|
}
|
|
}
|
|
Some(m)
|
|
}
|
|
|
|
/// Clip group or layer `n` as its clipping path's strokes draw it: a group of the clipping path
|
|
/// without its strokes, measured as the region it clips to, and its strokes ([`Node::clip_paint`])
|
|
/// as their [`Self::drawn_art`]. The renderer clips by the clipping path's geometry before its
|
|
/// geometry effects ([`crate::clip_outline`]) and paints the path's fills inside that region and
|
|
/// its strokes over the clipped art, unclipped. `None` for any other object, when the clipping
|
|
/// path is hidden and when its strokes have no drawn art.
|
|
fn clip_art(&self, n: &Node) -> Option<Node> {
|
|
if !n.clips() {
|
|
return None;
|
|
}
|
|
let clip = n.children()?.first().filter(|c| c.visible)?;
|
|
let strokes = self.drawn_art(&clip.clip_paint().stroke?)?;
|
|
Some(Node::group(n.id, vec![Arc::new(unstroked(clip)), Arc::new(strokes)]))
|
|
}
|
|
}
|
|
|
|
/// Clipping path `clip` without its strokes or its characters' strokes.
|
|
fn unstroked(clip: &Node) -> Node {
|
|
let mut n = clip.clone();
|
|
n.appearance.items.retain(AppearanceItem::is_fill);
|
|
if let NodeKind::Text(t) = &mut n.kind {
|
|
for r in &mut t.runs {
|
|
r.style.stroke = Paint::None;
|
|
}
|
|
}
|
|
n
|
|
}
|
|
|
|
/// Whether `n` itself has a geometry effect that [`Drawn::own_art`] applies, its own or one of its
|
|
/// fill's or stroke's (a group or layer: also a Pathfinder effect; a symbol instance: only its own).
|
|
fn reshapes(n: &Node) -> bool {
|
|
match &n.kind {
|
|
NodeKind::Group { .. } | NodeKind::Layer { .. } => {
|
|
has_pathfinder(n) || !geometry_effects(n).is_empty() || n.appearance.items.iter().any(|i| paints(i) && has_geometry(i.effects()))
|
|
}
|
|
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => {
|
|
has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(|i| paints(i) && has_geometry(i.effects()))
|
|
}
|
|
_ => needs_outline(n) && (!geometry_effects(n).is_empty() || has_item_geometry(n)),
|
|
}
|
|
}
|
|
|
|
/// Path or compound path `n` as a group of paths: its geometry after its own geometry effects
|
|
/// painted by its fills and strokes without geometry effects, and that geometry after the
|
|
/// geometry effects of each other fill or stroke that paints, painted by that fill or stroke, as
|
|
/// the renderer draws them. The first path is left out when only fills or strokes with geometry
|
|
/// effects paint. The paths keep `n`'s id and fill rule and carry no effects.
|
|
fn path_art(n: &Node) -> Option<Node> {
|
|
let (path, rule) = leaf_geometry(n)?;
|
|
let ctx = GeomContext::of(n);
|
|
let g = apply_geometry_with(&n.appearance.effects, &path, path.to_bezpath().bounding_box(), &ctx);
|
|
let piece = |geometry: PathData, items: Vec<AppearanceItem>| {
|
|
let mut ap: Appearance = (*n.appearance).clone();
|
|
ap.items = items;
|
|
ap.effects.clear();
|
|
for i in &mut ap.items {
|
|
i.effects_mut().clear();
|
|
}
|
|
let mut p = Node::path(n.id, geometry, ap);
|
|
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
|
|
*r = rule;
|
|
}
|
|
Arc::new(p)
|
|
};
|
|
let (own, plain): (Vec<&AppearanceItem>, Vec<&AppearanceItem>) = n.appearance.items.iter().partition(|i| paints(i) && has_geometry(i.effects()));
|
|
let mut pieces = vec![];
|
|
if own.is_empty() || plain.iter().any(|i| paints(i)) {
|
|
pieces.push(piece(g.clone(), plain.into_iter().cloned().collect()));
|
|
}
|
|
let bounds = g.to_bezpath().bounding_box();
|
|
for item in own {
|
|
let geometry = apply_geometry_with(item.effects(), &g, bounds, &ctx.item(item));
|
|
pieces.push(piece(geometry, vec![item.clone()]));
|
|
}
|
|
Some(Node::group(n.id, pieces))
|
|
}
|