mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
459 lines
18 KiB
Rust
459 lines
18 KiB
Rust
use super::envelope::{EnvelopeOptions, PreserveShape, expand_envelope};
|
||
use super::mesh::GradientMesh;
|
||
use super::warp::map_nonlinear;
|
||
use super::{
|
||
Affine, Appearance, AppearanceItem, Arc, Color, FillLayer, ImageObject, Node, NodeId, NodeKind, Paint, PathData, Point, Rect, SubPath, Vec2,
|
||
blend_expand,
|
||
};
|
||
|
||
/// Optional hook that converts nodes the pure evaluation can't map (text) to outlines.
|
||
pub type Outliner<'a> = Option<&'a dyn Fn(&Node) -> Option<Node>>;
|
||
|
||
/// Distort Pattern Fills: the tiles pattern swatch `name` painted with placement `xf` lays over
|
||
/// `region` (see [`crate::PatternDef::instances_in`]); `None` without such a swatch.
|
||
pub type PatternTiles<'a> = &'a dyn Fn(&str, Affine, Rect) -> Option<Vec<Node>>;
|
||
|
||
/// What live evaluation asks of the crates above this one (`vectorcraft-effects` supplies them).
|
||
#[derive(Clone, Copy, Default)]
|
||
pub struct Hooks<'a> {
|
||
/// Type as its glyph outlines, a symbol instance as its art (see [`Outliner`]).
|
||
pub outline: Outliner<'a>,
|
||
/// Distort Appearance: a path's strokes and geometry effects baked into filled art (`None`
|
||
/// when it has none to bake).
|
||
pub appearance: Outliner<'a>,
|
||
/// Distort Pattern Fills: the tiles a pattern fill lays (see [`PatternTiles`]).
|
||
pub pattern: Option<PatternTiles<'a>>,
|
||
/// The document whose symbols' art counts toward the instance budget of a Repeat of their
|
||
/// instances ([`crate::RepeatSpec::expand`]).
|
||
pub symbols: Option<&'a crate::Document>,
|
||
}
|
||
|
||
impl<'a> From<Outliner<'a>> for Hooks<'a> {
|
||
fn from(outline: Outliner<'a>) -> Self {
|
||
Self { outline, ..Default::default() }
|
||
}
|
||
}
|
||
|
||
/// The affine map that best matches `f` near `p` (finite differences with step `eps`).
|
||
pub fn affine_near(f: &dyn Fn(Point) -> Point, p: Point, eps: f64) -> Affine {
|
||
let o = f(p);
|
||
let ex = (f(p + Vec2::new(eps, 0.0)) - f(p - Vec2::new(eps, 0.0))) / (2.0 * eps);
|
||
let ey = (f(p + Vec2::new(0.0, eps)) - f(p - Vec2::new(0.0, eps))) / (2.0 * eps);
|
||
let lin = Affine::new([ex.x, ex.y, ey.x, ey.y, 0.0, 0.0]);
|
||
Affine::translate(o.to_vec2()) * lin * Affine::translate(-p.to_vec2())
|
||
}
|
||
|
||
/// How deep hooks may nest art inside art (symbols in symbols, envelopes in envelopes) before
|
||
/// the rest is left as it is: a file can make a symbol contain itself.
|
||
const MAX_HOOK_DEPTH: u32 = 16;
|
||
|
||
/// How an envelope pushes art through its point map.
|
||
#[derive(Clone, Copy)]
|
||
pub(super) struct Warper<'a> {
|
||
pub(super) f: &'a dyn Fn(Point) -> Point,
|
||
/// Longest piece a segment is cut into before mapping.
|
||
pub(super) piece: f64,
|
||
/// Finite-difference step for the local affine maps gradients follow.
|
||
pub(super) eps: f64,
|
||
/// Rows and columns of the pieces an image is cut into.
|
||
pub(super) cells: usize,
|
||
pub(super) hooks: Hooks<'a>,
|
||
pub(super) options: EnvelopeOptions,
|
||
/// How many hooks the art being mapped came through.
|
||
pub(super) depth: u32,
|
||
}
|
||
|
||
impl Warper<'_> {
|
||
/// This warper one hook deeper (`None` past [`MAX_HOOK_DEPTH`]).
|
||
fn deeper(&self) -> Option<Self> {
|
||
(self.depth < MAX_HOOK_DEPTH).then_some(Self { depth: self.depth + 1, ..*self })
|
||
}
|
||
}
|
||
|
||
/// `to` (art standing in for `from`: its outlines, its art) with `from`'s name, transparency,
|
||
/// opacity mask and effects.
|
||
fn carry_look(from: &Node, mut to: Node) -> Node {
|
||
to.name = from.name.clone();
|
||
to.opacity = from.opacity;
|
||
to.blend = from.blend;
|
||
to.isolate = from.isolate;
|
||
to.knockout = from.knockout;
|
||
to.knockout_shape = from.knockout_shape;
|
||
to.extras_mut().mask = from.extras().mask.clone();
|
||
to.appearance.effects = from.appearance.effects.clone();
|
||
to
|
||
}
|
||
|
||
pub(super) fn map_node(n: &Node, w: &Warper) -> Node {
|
||
let deeper = w.deeper();
|
||
// Distort Appearance: strokes and geometry effects bend with the art.
|
||
if w.options.distort_appearance
|
||
&& let Some(d) = &deeper
|
||
&& let Some(o) = w.hooks.appearance.and_then(|h| h(n))
|
||
{
|
||
return map_node(&o, d);
|
||
}
|
||
// Distort Pattern Fills: the tiles bend with the art (not the patterns inside them).
|
||
if w.options.patterns()
|
||
&& let Some(d) = &deeper
|
||
&& let Some(o) = w.hooks.pattern.and_then(|p| pattern_art(n, p))
|
||
{
|
||
let inner = Warper { options: EnvelopeOptions { distort_pattern_fills: false, ..d.options }, ..*d };
|
||
return map_node(&o, &inner);
|
||
}
|
||
let mut out = n.clone();
|
||
// Distort Linear Gradients: the gradient vectors follow the warp where they sit.
|
||
if w.options.gradients()
|
||
&& let Some(b) = n.geometric_bounds()
|
||
{
|
||
out.appearance.warp_linear_gradients(b, &|p| affine_near(w.f, p, w.eps));
|
||
}
|
||
if let Some(m) = out.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||
m.art = Arc::new(map_node(&m.art, w));
|
||
}
|
||
match &mut out.kind {
|
||
NodeKind::Path { path, live, .. } => {
|
||
*path = map_nonlinear(path, w.piece, w.f);
|
||
*live = None;
|
||
}
|
||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => {
|
||
for c in children.iter_mut() {
|
||
*c = Arc::new(map_node(c, w));
|
||
}
|
||
}
|
||
NodeKind::Mesh(m) => m.map(w.f),
|
||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Intertwine { .. } => {
|
||
if let Some(d) = &deeper {
|
||
return map_node(&expanded_group_hooks(n, w.hooks), d);
|
||
}
|
||
}
|
||
NodeKind::Text(_) | NodeKind::SymbolInstance { .. } => {
|
||
if let Some(d) = &deeper
|
||
&& let Some(o) = w.hooks.outline.and_then(|h| h(n))
|
||
{
|
||
return map_node(&carry_look(n, o), d);
|
||
}
|
||
}
|
||
NodeKind::Image(im) => {
|
||
let im = im.clone();
|
||
return warp_image(&out, &im, w);
|
||
}
|
||
// Its art is plain, so it warps at this depth.
|
||
NodeKind::PlacedDocument(_) => return map_node(&expanded_group_hooks(n, w.hooks), w),
|
||
}
|
||
out
|
||
}
|
||
|
||
/// The affine map taking triangle `s` to triangle `d` (`None` when `s` is degenerate).
|
||
fn affine_between(s: [Point; 3], d: [Point; 3]) -> Option<Affine> {
|
||
let basis = |p: [Point; 3]| Affine::new([p[1].x - p[0].x, p[1].y - p[0].y, p[2].x - p[0].x, p[2].y - p[0].y, p[0].x, p[0].y]);
|
||
let bs = basis(s);
|
||
if bs.determinant().abs() < 1e-12 {
|
||
return None;
|
||
}
|
||
let a = basis(d) * bs.inverse();
|
||
a.is_finite().then_some(a)
|
||
}
|
||
|
||
/// Image `n` (`im`) pushed through the warp as a raster mesh warp: its frame cut into
|
||
/// `w.cells`² cells of two triangles, each a clip group showing the image under the affine map
|
||
/// that takes the triangle where the warp takes its corners (with Preserve Shape Using
|
||
/// Transparency, the image masked by the triangle; with Anti-Alias off, drawn without smoothing).
|
||
/// Each piece reaches a little past its edges (at most half a point) so neighbours overlap and no
|
||
/// seams show.
|
||
fn warp_image(n: &Node, im: &ImageObject, w: &Warper) -> Node {
|
||
let k = w.cells.max(1);
|
||
// Two triangles a cell, three objects each; an image an inner envelope already cut into
|
||
// pieces is cut again here, so the pieces count against [`MAX_GENERATED`]. Past it, the image
|
||
// is left out.
|
||
if !Generating::charge(k.saturating_mul(k).saturating_mul(6)) {
|
||
let mut g = n.clone();
|
||
g.kind = NodeKind::Group { children: vec![], clip: false };
|
||
return g;
|
||
}
|
||
let (pw, ph) = (im.width as f64, im.height as f64);
|
||
let at = |i: usize, j: usize| Point::new(pw * i as f64 / k as f64, ph * j as f64 / k as f64);
|
||
let mut pieces = Vec::with_capacity(2 * k * k);
|
||
for j in 0..k {
|
||
for i in 0..k {
|
||
let s = [at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1)];
|
||
let d = s.map(|p| (w.f)(im.xf * p));
|
||
for t in [[0, 1, 2], [0, 2, 3]] {
|
||
let (st, dt) = (t.map(|v| s[v]), t.map(|v| d[v]));
|
||
let Some(xf) = affine_between(st, dt) else { continue };
|
||
let c = Point::new((dt[0].x + dt[1].x + dt[2].x) / 3.0, (dt[0].y + dt[1].y + dt[2].y) / 3.0);
|
||
let grow = (dt[0].distance(dt[1]).max(dt[1].distance(dt[2])) * 0.02).min(0.5);
|
||
let tri: Vec<Point> = dt
|
||
.iter()
|
||
.map(|p| {
|
||
let v = *p - c;
|
||
let len = v.hypot();
|
||
if len > 1e-12 { *p + v * (grow / len) } else { *p }
|
||
})
|
||
.collect();
|
||
let shape = PathData::single(SubPath::polyline(&tri, true));
|
||
let mut image = Node::new(n.id, NodeKind::Image(Box::new(ImageObject { xf, ..im.clone() })));
|
||
image.extras_mut().pixelated = !w.options.anti_alias;
|
||
if w.options.preserve_shape == PreserveShape::Transparency {
|
||
// Preserve Shape Using Transparency: the triangle is the piece's alpha (a
|
||
// white opacity mask) instead of its clipping path.
|
||
let white = Node::path(n.id, shape, Appearance::basic(Paint::solid(Color::WHITE), Paint::None, 0.0));
|
||
pieces.push(Arc::new(image.with_mask(Some(Box::new(crate::OpacityMask::new(white, true))))));
|
||
continue;
|
||
}
|
||
let mut clip = Node::path(n.id, shape, Appearance::default());
|
||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||
*clipping = true;
|
||
}
|
||
pieces.push(Arc::new(Node::new(n.id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(image)], clip: true })));
|
||
}
|
||
}
|
||
}
|
||
let mut g = n.clone();
|
||
g.kind = NodeKind::Group { children: pieces, clip: false };
|
||
g
|
||
}
|
||
|
||
/// Distort Pattern Fills: path or compound path `n` with each visible pattern fill turned into the
|
||
/// tiles it paints in a clip group shaped like `n` (with the fill's opacity and blend mode), its
|
||
/// other fills and strokes on copies of it, in paint order, under `n`'s transparency. `None`
|
||
/// without a pattern fill whose definition `pattern` finds.
|
||
fn pattern_art(n: &Node, tiles: PatternTiles) -> Option<Node> {
|
||
let (path, rule, _) = crate::blend::node_path(n)?;
|
||
let pattern_fill = |i: &AppearanceItem| matches!(i, AppearanceItem::Fill(f) if f.visible && matches!(f.paint, Paint::Pattern { .. }));
|
||
if !n.appearance.items.iter().any(pattern_fill) {
|
||
return None;
|
||
}
|
||
let bounds = path.bounds()?;
|
||
let def = |i: &AppearanceItem| match i {
|
||
AppearanceItem::Fill(f) if f.visible => match &f.paint {
|
||
Paint::Pattern { pattern: name, xf } => tiles(name, *xf, bounds).map(|t| (t, f.opacity, f.blend)),
|
||
_ => None,
|
||
},
|
||
_ => None,
|
||
};
|
||
let plain = |items: Vec<AppearanceItem>| {
|
||
let mut p = Node {
|
||
appearance: Appearance { items, ..Default::default() }.into(),
|
||
opacity: 1.0,
|
||
blend: Default::default(),
|
||
isolate: false,
|
||
knockout: Default::default(),
|
||
knockout_shape: false,
|
||
name: None,
|
||
..n.clone()
|
||
};
|
||
p.extras_mut().mask = None;
|
||
Arc::new(p)
|
||
};
|
||
let mut pieces = vec![];
|
||
let mut rest = vec![];
|
||
for i in &n.appearance.items {
|
||
let Some((tiles, opacity, blend)) = def(i) else {
|
||
rest.push(i.clone());
|
||
continue;
|
||
};
|
||
if !rest.is_empty() {
|
||
pieces.push(plain(std::mem::take(&mut rest)));
|
||
}
|
||
let mut clip = Node::path(n.id, path.clone(), Appearance::default());
|
||
if let NodeKind::Path { clipping, rule: r, .. } = &mut clip.kind {
|
||
(*clipping, *r) = (true, rule);
|
||
}
|
||
let children = std::iter::once(clip).chain(tiles).map(Arc::new).collect();
|
||
let mut g = Node::new(n.id, NodeKind::Group { children, clip: true });
|
||
g.opacity = opacity;
|
||
g.blend = blend;
|
||
pieces.push(Arc::new(g));
|
||
}
|
||
if !rest.is_empty() {
|
||
pieces.push(plain(rest));
|
||
}
|
||
Some(Node {
|
||
kind: NodeKind::Group { children: pieces, clip: false },
|
||
appearance: Appearance { effects: n.appearance.effects.clone(), ..Default::default() }.into(),
|
||
..n.clone()
|
||
})
|
||
}
|
||
|
||
// =====================================================================================
|
||
// Expansion
|
||
// =====================================================================================
|
||
|
||
/// Is this one of the live kinds?
|
||
pub fn is_live(n: &Node) -> bool {
|
||
matches!(
|
||
n.kind,
|
||
NodeKind::Blend { .. }
|
||
| NodeKind::Envelope { .. }
|
||
| NodeKind::Mesh(_)
|
||
| NodeKind::Repeat(_)
|
||
| NodeKind::PlacedDocument(_)
|
||
| NodeKind::Intertwine { .. }
|
||
)
|
||
}
|
||
|
||
/// Mesh tessellation as filled quad paths (no stroke), `n`×`n` per patch.
|
||
pub fn mesh_quad_nodes(m: &GradientMesh, n: usize) -> Vec<Node> {
|
||
m.quads(n)
|
||
.into_iter()
|
||
.map(|q| {
|
||
let mut ap = Appearance::default();
|
||
let mut fl = FillLayer::new(Paint::solid(q.color));
|
||
fl.opacity = q.opacity;
|
||
ap.items.push(AppearanceItem::Fill(fl));
|
||
Node::path(NodeId(0), PathData::single(SubPath::polyline(&q.pts, true)), ap)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// One level of evaluation of a live object (non-live nodes return themselves). Blend steps and
|
||
/// envelope results may still contain live objects (e.g. a blend of two meshes).
|
||
pub fn expand_live(n: &Node) -> Vec<Node> {
|
||
expand_live_with(n, None)
|
||
}
|
||
|
||
pub fn expand_live_with(n: &Node, outline: Outliner) -> Vec<Node> {
|
||
expand_live_hooks(n, outline.into())
|
||
}
|
||
|
||
/// [`expand_live_with`] with everything an envelope can ask of the crates above (see [`Hooks`]).
|
||
pub fn expand_live_hooks(n: &Node, hooks: Hooks) -> Vec<Node> {
|
||
let _generating = Generating::enter();
|
||
match &n.kind {
|
||
NodeKind::Blend { children, spec } => blend_expand(children, spec),
|
||
NodeKind::Envelope { content, kind, fidelity, options, frame, .. } => expand_envelope(content, kind, *fidelity, *options, **frame, hooks),
|
||
NodeKind::Mesh(m) => mesh_quad_nodes(m, 8),
|
||
NodeKind::Repeat(r) => r.expand(hooks.symbols),
|
||
NodeKind::PlacedDocument(p) => p.art(n),
|
||
NodeKind::Intertwine { children } => crate::intertwine::expand(children),
|
||
_ => vec![n.clone()],
|
||
}
|
||
}
|
||
|
||
/// A live object evaluated into a plain group (keeping the object's id, name and transparency).
|
||
pub fn expanded_group(n: &Node, outline: Outliner) -> Node {
|
||
expanded_group_hooks(n, outline.into())
|
||
}
|
||
|
||
/// [`expanded_group`] with [`Hooks`].
|
||
pub fn expanded_group_hooks(n: &Node, hooks: Hooks) -> Node {
|
||
let _generating = Generating::enter();
|
||
let children = expand_live_hooks(n, hooks).into_iter().map(Arc::new).collect();
|
||
let mut g = Node::new(n.id, NodeKind::Group { children, clip: false });
|
||
g.name = n.name.clone();
|
||
g.visible = n.visible;
|
||
g.locked = n.locked;
|
||
g.opacity = n.opacity;
|
||
g.blend = n.blend;
|
||
g.isolate = n.isolate;
|
||
g.knockout = n.knockout;
|
||
// Knocking out opaque blend steps changes nothing: don't make the outputs work for it.
|
||
if matches!(n.kind, NodeKind::Blend { .. }) && g.knockout == crate::Knockout::On && !crate::blend::knockout_shows(&g) {
|
||
g.knockout = crate::Knockout::Off;
|
||
}
|
||
g.knockout_shape = n.knockout_shape;
|
||
g.extras_mut().mask = n.extras().mask.clone();
|
||
g
|
||
}
|
||
|
||
/// Recursively replace every live object in `n` by plain geometry (for exporters).
|
||
pub fn expand_deep(n: &Node, outline: Outliner) -> Node {
|
||
expand_deep_hooks(n, outline.into())
|
||
}
|
||
|
||
/// [`expand_deep`] with [`Hooks`]. The objects live art makes here count against one
|
||
/// [`MAX_GENERATED`] ([`Generating`]): blends of blends kept their inner blends live, and each
|
||
/// copy expanded on its own, so step counts capped one by one multiplied. Past it, live art
|
||
/// expands to nothing.
|
||
pub fn expand_deep_hooks(n: &Node, hooks: Hooks) -> Node {
|
||
let _generating = Generating::enter();
|
||
let mut out = if is_live(n) {
|
||
let g = expanded_group_hooks(n, hooks);
|
||
if !Generating::charge(g.count()) {
|
||
return Node::new(n.id, NodeKind::Group { children: vec![], clip: false });
|
||
}
|
||
g
|
||
} else {
|
||
n.clone()
|
||
};
|
||
if let Some(ch) = out.children_mut() {
|
||
for c in ch.iter_mut() {
|
||
if subtree_has_live(c) {
|
||
*c = Arc::new(expand_deep_hooks(c, hooks));
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Most objects one evaluation of live art (an expansion, an envelope's mapping and everything
|
||
/// inside them) may make.
|
||
pub const MAX_GENERATED: usize = 4_000_000;
|
||
|
||
std::thread_local! {
|
||
/// What is left of [`MAX_GENERATED`] for the evaluation running on this thread; `None` outside
|
||
/// one.
|
||
static GENERATED_LEFT: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
|
||
}
|
||
|
||
/// The outermost live evaluation on this thread: it starts [`GENERATED_LEFT`] and ends it.
|
||
struct Generating(bool);
|
||
|
||
impl Generating {
|
||
fn enter() -> Self {
|
||
let outermost = GENERATED_LEFT.get().is_none();
|
||
if outermost {
|
||
GENERATED_LEFT.set(Some(MAX_GENERATED));
|
||
}
|
||
Self(outermost)
|
||
}
|
||
|
||
/// Spend `n` objects; `false` (and nothing left) when there aren't that many.
|
||
fn charge(n: usize) -> bool {
|
||
match GENERATED_LEFT.get() {
|
||
None => true,
|
||
Some(left) => {
|
||
GENERATED_LEFT.set(Some(left.saturating_sub(n)));
|
||
n <= left
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Drop for Generating {
|
||
fn drop(&mut self) {
|
||
if self.0 {
|
||
GENERATED_LEFT.set(None);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Does `n` or any descendant need live evaluation?
|
||
pub fn subtree_has_live(n: &Node) -> bool {
|
||
let mut any = false;
|
||
n.walk(&mut |c| any |= is_live(c));
|
||
any
|
||
}
|
||
|
||
/// Largest stroke outset in a subtree (for visual bounds of envelopes). With `symbols`, the
|
||
/// outsets in the art of the symbols its instances show count too
|
||
/// ([`crate::symbols::ArtBox::reach`]), grown by [`crate::SymbolBoxes::brush_pad`]. In boxes made
|
||
/// by [`crate::SymbolBoxes::without_art_strokes`], the outsets in the art do not count, and the pad
|
||
/// does.
|
||
pub(crate) fn max_outset(nodes: &[Arc<Node>], symbols: Option<&crate::SymbolBoxes>) -> f64 {
|
||
let mut o = 0.0f64;
|
||
for n in nodes {
|
||
n.walk(&mut |c| {
|
||
o = o.max(c.appearance.outset());
|
||
if let (Some(s), NodeKind::SymbolInstance { symbol, .. }) = (symbols, &c.kind)
|
||
&& let Some(art) = s.art(symbol)
|
||
{
|
||
let reach = if s.art_strokes() { art.reach } else { 0.0 };
|
||
o = o.max(reach + s.brush_pad(symbol));
|
||
}
|
||
});
|
||
}
|
||
o
|
||
}
|