Merge pull request #199 from storytold/m8-envelope-distort-all
M8.16: Envelopes bend images, symbols, gradients, patterns and appearance; warps keep their frame
This commit is contained in:
commit
5a13626faa
|
|
@ -144,7 +144,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
|||
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
|
||||
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder edge erase, large-offset bug open) | 3–6 |
|
||||
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, Area Type Options, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (searchable) and found by name on open/import, CJK names in the UI, Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects, tab stops + Tabs panel done; tab leaders, spell check, vertical type pending | 45–60 |
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass under way (M8.2 tool options persist and Liquify tools share one brush, M8.1 Delete removes Width points and Puppet Warp pins, not the art, M8.8 Define Grid dialog with viewing angle, units, colours, M8.15 Envelope dialogs, Reset with Warp/Mesh, full Envelope Options, M8.16 enveloped type exports distorted, M8.5 Puppet Warp pins rotate, multi-select, Control bar, M8.3 Liquify Tool Options, pen pressure, Brush Affects, M8.9 perspective grid presets and manager, M8.16 test fix (EPS dates), M8.12 Perspective Selection scales, copies, moves perpendicular, Transform Again, M8.18 Blend Options dialog, anchor-targeted Blend tool, add keys, M8.19 blend spine and keys editable on canvas, Release keeps spine, M8.6 Puppet Warp pins appear at once, warp from rest shape, follow Undo, M8.13 movable perspective planes, plane dialog, Move Plane to Match Object, M8.10 perspective Lock Grid, Lock Station Point, Snap to Grid, Show Rulers, M8.4 Liquify hold-to-apply, safe scope, incremental strokes, M8.7 Width tool on compound paths, every subpath, cursors, M8.20 blend interpolation fidelity, knockout, Expand/Release keep attributes) | 8–12 |
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass under way (M8.2 tool options persist and Liquify tools share one brush, M8.1 Delete removes Width points and Puppet Warp pins, not the art, M8.8 Define Grid dialog with viewing angle, units, colours, M8.15 Envelope dialogs, Reset with Warp/Mesh, full Envelope Options, M8.16 enveloped type exports distorted, M8.5 Puppet Warp pins rotate, multi-select, Control bar, M8.3 Liquify Tool Options, pen pressure, Brush Affects, M8.9 perspective grid presets and manager, M8.16 test fix (EPS dates), M8.12 Perspective Selection scales, copies, moves perpendicular, Transform Again, M8.18 Blend Options dialog, anchor-targeted Blend tool, add keys, M8.19 blend spine and keys editable on canvas, Release keeps spine, M8.6 Puppet Warp pins appear at once, warp from rest shape, follow Undo, M8.13 movable perspective planes, plane dialog, Move Plane to Match Object, M8.10 perspective Lock Grid, Lock Station Point, Snap to Grid, Show Rulers, M8.4 Liquify hold-to-apply, safe scope, incremental strokes, M8.7 Width tool on compound paths, every subpath, cursors, M8.20 blend interpolation fidelity, knockout, Expand/Release keep attributes, M8.16 envelopes bend images, symbols, gradients, patterns, appearance; warps keep their frame) | 8–12 |
|
||||
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder and Color Adjustments; SVG Filters, Document Raster Effects Settings, 3D pending | 86–135 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
|
||||
| M11 | Artboards & views (artboard panel/tool, Trim View, middle-button pan and print tiling done; multiple windows, presentation polish) | 🟡 | 15–25 |
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ impl<'a> Scene<'a> {
|
|||
}
|
||||
// Live blends, envelopes, meshes and repeats are written as their evaluated art.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => {
|
||||
let g = vectorcraft_effects::expand_live_deep(n);
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(c, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -892,6 +892,30 @@ impl Appearance {
|
|||
g.transform(a);
|
||||
}
|
||||
}
|
||||
/// Envelope Options → Distort Linear Gradients: bend the linear gradients of an object whose
|
||||
/// geometric bounds are `bounds` with a warp, given its local affine approximation at a point
|
||||
/// (taken at the middle of each gradient's vector). Unplaced ones are fixed to their fit first;
|
||||
/// other gradients stay as they are.
|
||||
pub fn warp_linear_gradients(&mut self, bounds: vectorcraft_geom::Rect, near: &dyn Fn(vectorcraft_geom::Point) -> vectorcraft_geom::Affine) {
|
||||
for i in &mut self.items {
|
||||
let (g, b) = match i {
|
||||
AppearanceItem::Fill(FillLayer { paint: Paint::Gradient(g), .. }) => (g, bounds),
|
||||
AppearanceItem::Stroke(s) => {
|
||||
let b = s.paint_bounds(bounds);
|
||||
let Paint::Gradient(g) = &mut s.paint else { continue };
|
||||
(g, b)
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
if g.gradient.kind != vectorcraft_color::GradientKind::Linear {
|
||||
continue;
|
||||
}
|
||||
g.pin(b);
|
||||
if let Some(geom) = g.geom {
|
||||
g.transform(near(geom.start.midpoint(geom.end)));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Map placed gradients through a warp, given its local affine approximation at a point (taken
|
||||
/// at each gradient's centre: the start of a radial, the middle of a linear vector). Freeform
|
||||
/// points each follow the warp at their own position.
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ pub fn lerp_path(a: &PathData, b: &PathData, t: f64) -> PathData {
|
|||
|
||||
/// Path data of a path or compound path node (compound children concatenated), its fill rule and
|
||||
/// the number of subpaths each compound member holds.
|
||||
fn node_path(n: &Node) -> Option<(PathData, FillRule, Option<Vec<usize>>)> {
|
||||
pub(crate) fn node_path(n: &Node) -> Option<(PathData, FillRule, Option<Vec<usize>>)> {
|
||||
match &n.kind {
|
||||
NodeKind::Path { path, rule, .. } => Some((path.clone(), *rule, None)),
|
||||
NodeKind::Compound { children, rule } => {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ use vectorcraft_color::{Color, Paint};
|
|||
use vectorcraft_geom::{Affine, Anchor, BezPath, CubicBez, ParamCurve, PathData, Point, Rect, Shape, SubPath, Vec2};
|
||||
|
||||
use crate::appearance::{Appearance, AppearanceItem, FillLayer};
|
||||
use crate::node::{Node, NodeId, NodeKind};
|
||||
use crate::node::{ImageObject, Node, NodeId, NodeKind};
|
||||
|
||||
fn yes() -> bool {
|
||||
true
|
||||
|
|
@ -539,9 +539,11 @@ impl Coons {
|
|||
Some(Self { top, right, bottom, left })
|
||||
}
|
||||
|
||||
/// The point at `u` along a side; past its ends the end segments go on straight (art an
|
||||
/// envelope bends can reach past the content's box: outlined strokes).
|
||||
fn sample(side: &[Point], u: f64) -> Point {
|
||||
let s = u.clamp(0.0, 1.0) * (side.len() - 1) as f64;
|
||||
let i = (s.floor() as usize).min(side.len() - 2);
|
||||
let s = u * (side.len() - 1) as f64;
|
||||
let i = (s.floor().max(0.0) as usize).min(side.len() - 2);
|
||||
side[i].lerp(side[i + 1], s - i as f64)
|
||||
}
|
||||
|
||||
|
|
@ -1023,7 +1025,7 @@ fn catmull(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f64) -> Vec2 {
|
|||
}
|
||||
|
||||
/// Point map from the unit square onto a point grid (tensor-product Catmull-Rom; linear
|
||||
/// extrapolation at the borders so an undistorted grid is the identity).
|
||||
/// extrapolation at the borders so an undistorted grid is the identity, also past the square).
|
||||
fn grid_eval(rows: usize, cols: usize, pts: &[Point], u: f64, v: f64) -> Point {
|
||||
let at = |r: i64, c: i64| -> Vec2 {
|
||||
let rr = r.clamp(0, rows as i64);
|
||||
|
|
@ -1043,9 +1045,9 @@ fn grid_eval(rows: usize, cols: usize, pts: &[Point], u: f64, v: f64) -> Point {
|
|||
}
|
||||
p
|
||||
};
|
||||
let su = (u.clamp(0.0, 1.0) * cols as f64).min(cols as f64 - 1e-9);
|
||||
let sv = (v.clamp(0.0, 1.0) * rows as f64).min(rows as f64 - 1e-9);
|
||||
let (ci, ri) = (su.floor() as i64, sv.floor() as i64);
|
||||
// Past the grid's edges the outer patches go on (art can reach past the content's box).
|
||||
let (su, sv) = (u * cols as f64, v * rows as f64);
|
||||
let (ci, ri) = ((su.floor() as i64).clamp(0, cols as i64 - 1), (sv.floor() as i64).clamp(0, rows as i64 - 1));
|
||||
let (fu, fv) = (su - ci as f64, sv - ri as f64);
|
||||
let mut rowsv = [Vec2::ZERO; 4];
|
||||
for (k, rv) in rowsv.iter_mut().enumerate() {
|
||||
|
|
@ -1055,113 +1057,416 @@ fn grid_eval(rows: usize, cols: usize, pts: &[Point], u: f64, v: f64) -> Point {
|
|||
catmull(rowsv[0], rowsv[1], rowsv[2], rowsv[3], fv).to_point()
|
||||
}
|
||||
|
||||
/// The envelope's point map for content whose bounding box is `src`.
|
||||
pub fn envelope_mapper<'a>(kind: &'a EnvelopeKind, src: Rect) -> Box<dyn Fn(Point) -> Point + 'a> {
|
||||
let w = src.width().max(1e-9);
|
||||
let h = src.height().max(1e-9);
|
||||
/// The surface (u, v ∈ [0,1]) an envelope of `kind` maps content onto, for content whose bounds in
|
||||
/// the envelope's own `frame` (its axes → the document) are `src`: a warp bends along the frame's
|
||||
/// axes; a mesh's points and a top object's path are in the document already.
|
||||
fn surface_of<'a>(kind: &'a EnvelopeKind, src: Rect, frame: Affine) -> Box<dyn Fn(f64, f64) -> Point + 'a> {
|
||||
let flat = move |u: f64, v: f64| frame * Point::new(src.x0 + u * src.width(), src.y0 + v * src.height());
|
||||
match kind {
|
||||
EnvelopeKind::Warp { style, bend, h: dh, v: dv, horizontal } => {
|
||||
let st = WarpStyle::from_id(style).unwrap_or(WarpStyle::Arc);
|
||||
let b = bend.clamp(-100.0, 100.0) / 100.0;
|
||||
let (dh, dv) = (dh.clamp(-100.0, 100.0) / 100.0, dv.clamp(-100.0, 100.0) / 100.0);
|
||||
let c = src.center();
|
||||
let (hw, hh) = (w / 2.0, h / 2.0);
|
||||
let (hw, hh) = (src.width() / 2.0, src.height() / 2.0);
|
||||
let horizontal = *horizontal;
|
||||
Box::new(move |q: Point| {
|
||||
let (x, y) = ((q.x - c.x) / hw, (q.y - c.y) / hh);
|
||||
Box::new(move |u: f64, v: f64| {
|
||||
let (x, y) = (2.0 * u - 1.0, 2.0 * v - 1.0);
|
||||
let (x2, y2) = if horizontal {
|
||||
warp_point(st, b, dh, dv, x, y)
|
||||
} else {
|
||||
let (a, b2) = warp_point(st, b, dh, dv, y, x);
|
||||
(b2, a)
|
||||
};
|
||||
Point::new(c.x + x2 * hw, c.y + y2 * hh)
|
||||
frame * Point::new(c.x + x2 * hw, c.y + y2 * hh)
|
||||
})
|
||||
}
|
||||
EnvelopeKind::Mesh { rows, cols, points } => {
|
||||
let (rows, cols) = (*rows as usize, *cols as usize);
|
||||
if rows == 0 || cols == 0 || points.len() != (rows + 1) * (cols + 1) {
|
||||
return Box::new(|q| q);
|
||||
return Box::new(flat);
|
||||
}
|
||||
Box::new(move |q: Point| grid_eval(rows, cols, points, (q.x - src.x0) / w, (q.y - src.y0) / h))
|
||||
Box::new(move |u: f64, v: f64| grid_eval(rows, cols, points, u, v))
|
||||
}
|
||||
EnvelopeKind::TopObject { path } => match Coons::from_path(path) {
|
||||
Some(co) => Box::new(move |q: Point| co.eval((q.x - src.x0) / w, (q.y - src.y0) / h)),
|
||||
None => Box::new(|q| q),
|
||||
Some(co) => Box::new(move |u: f64, v: f64| co.eval(u, v)),
|
||||
None => Box::new(flat),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The envelope's point map for content whose bounding box is `src` (page axes).
|
||||
pub fn envelope_mapper<'a>(kind: &'a EnvelopeKind, src: Rect) -> Box<dyn Fn(Point) -> Point + 'a> {
|
||||
let s = surface_of(kind, src, Affine::IDENTITY);
|
||||
let (w, h) = (src.width().max(1e-9), src.height().max(1e-9));
|
||||
Box::new(move |q: Point| s((q.x - src.x0) / w, (q.y - src.y0) / h))
|
||||
}
|
||||
|
||||
/// The frame an envelope keeps after transform `m` (its frame so far composed with the
|
||||
/// transform): identity when its axes stay square to the page, unflipped, since evaluating on
|
||||
/// the page axes gives the same result then.
|
||||
pub fn envelope_frame(m: Affine) -> Affine {
|
||||
let [a, b, c, d, _, _] = m.as_coeffs();
|
||||
if b.abs() < 1e-12 && c.abs() < 1e-12 && a > 0.0 && d > 0.0 { Affine::IDENTITY } else { m }
|
||||
}
|
||||
|
||||
/// An envelope's point map: content placed in the envelope's own frame maps, by its bounds there,
|
||||
/// onto the unit square and from there onto the envelope's surface ([`surface_of`]).
|
||||
pub struct EnvelopeMap<'a> {
|
||||
src: Rect,
|
||||
frame: Affine,
|
||||
to_frame: Affine,
|
||||
surface: Box<dyn Fn(f64, f64) -> Point + 'a>,
|
||||
}
|
||||
|
||||
impl<'a> EnvelopeMap<'a> {
|
||||
/// The map of an envelope of `kind` around `content` with `frame`. `None` when the content
|
||||
/// has no bounds.
|
||||
pub fn new(content: &[Arc<Node>], kind: &'a EnvelopeKind, frame: Affine) -> Option<Self> {
|
||||
let frame = if frame.is_finite() && frame.determinant().abs() > 1e-12 { frame } else { Affine::IDENTITY };
|
||||
let to_frame = frame.inverse();
|
||||
let src = if frame == Affine::IDENTITY {
|
||||
nodes_bounds(content)?
|
||||
} else {
|
||||
content.iter().try_fold(None, |acc, c| {
|
||||
let mut m = (**c).clone();
|
||||
m.transform(to_frame, false);
|
||||
Some(vectorcraft_geom::union_opt(acc, m.geometric_bounds()))
|
||||
})??
|
||||
};
|
||||
Some(Self { src, frame, to_frame, surface: surface_of(kind, src, frame) })
|
||||
}
|
||||
|
||||
/// The map of envelope node `n` (`None` for other nodes).
|
||||
pub fn of(n: &'a Node) -> Option<Self> {
|
||||
match &n.kind {
|
||||
NodeKind::Envelope { content, kind, frame, .. } => Self::new(content, kind, *frame),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The content's bounds in the envelope's frame.
|
||||
pub fn src(&self) -> Rect {
|
||||
self.src
|
||||
}
|
||||
|
||||
/// The surface point at (u, v) ∈ [0,1]².
|
||||
pub fn at(&self, u: f64, v: f64) -> Point {
|
||||
(self.surface)(u, v)
|
||||
}
|
||||
|
||||
/// Where the envelope puts document point `p`.
|
||||
pub fn map(&self, p: Point) -> Point {
|
||||
let q = self.to_frame * p;
|
||||
let (w, h) = (self.src.width().max(1e-9), self.src.height().max(1e-9));
|
||||
self.at((q.x - self.src.x0) / w, (q.y - self.src.y0) / h)
|
||||
}
|
||||
|
||||
/// A flat `(rows+1)×(cols+1)` grid over the content, in the envelope's frame (row-major).
|
||||
pub fn grid(&self, rows: u32, cols: u32) -> Vec<Point> {
|
||||
grid_points(self.src, rows, cols).into_iter().map(|p| self.frame * p).collect()
|
||||
}
|
||||
|
||||
/// The surface sampled on a `rows`×`cols` grid as a mesh whose handles follow it and whose
|
||||
/// points are `color`: Release gives it back as the envelope's shape, Reset with Mesh starts
|
||||
/// from it.
|
||||
pub fn surface_mesh(&self, rows: u32, cols: u32, color: Color) -> GradientMesh {
|
||||
GradientMesh::from_surface(rows, cols, &|u, v| self.at(u, v), &|_, _| color)
|
||||
}
|
||||
|
||||
/// The surface's bounds (sampled; a mesh's points and a top object's path bound it exactly).
|
||||
fn bounds(&self) -> Option<Rect> {
|
||||
const N: usize = 16;
|
||||
let mut r: Option<Rect> = None;
|
||||
for j in 0..=N {
|
||||
for i in 0..=N {
|
||||
let q = self.at(i as f64 / N as f64, j as f64 / N as f64);
|
||||
r = Some(r.map_or(Rect::from_points(q, q), |r| r.union_pt(q)));
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>>;
|
||||
|
||||
fn map_node(n: &Node, f: &dyn Fn(Point) -> Point, piece: f64, outline: Outliner) -> 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>>,
|
||||
}
|
||||
|
||||
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)]
|
||||
struct Warper<'a> {
|
||||
f: &'a dyn Fn(Point) -> Point,
|
||||
/// Longest piece a segment is cut into before mapping.
|
||||
piece: f64,
|
||||
/// Finite-difference step for the local affine maps gradients follow.
|
||||
eps: f64,
|
||||
/// Rows and columns of the pieces an image is cut into.
|
||||
cells: usize,
|
||||
hooks: Hooks<'a>,
|
||||
options: EnvelopeOptions,
|
||||
/// How many hooks the art being mapped came through.
|
||||
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.mask = from.mask.clone();
|
||||
to.appearance.effects = from.appearance.effects.clone();
|
||||
to
|
||||
}
|
||||
|
||||
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) = &mut out.mask {
|
||||
m.art = Arc::new(map_node(&m.art, w));
|
||||
}
|
||||
match &mut out.kind {
|
||||
NodeKind::Path { path, live, .. } => {
|
||||
*path = map_nonlinear(path, piece, f);
|
||||
*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, f, piece, outline));
|
||||
*c = Arc::new(map_node(c, w));
|
||||
}
|
||||
}
|
||||
NodeKind::Mesh(m) => m.map(f),
|
||||
NodeKind::Mesh(m) => m.map(w.f),
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) => {
|
||||
let g = expanded_group(n, outline);
|
||||
return map_node(&g, f, piece, outline);
|
||||
}
|
||||
NodeKind::Text(_) => {
|
||||
if let Some(o) = outline.and_then(|h| h(n)) {
|
||||
return map_node(&o, f, piece, outline);
|
||||
if let Some(d) = &deeper {
|
||||
return map_node(&expanded_group_hooks(n, w.hooks), d);
|
||||
}
|
||||
}
|
||||
NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => {}
|
||||
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);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Evaluate an envelope: its content mapped through the envelope.
|
||||
/// 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. 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);
|
||||
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 mut clip = Node::path(n.id, PathData::single(SubPath::polyline(&tri, true)), Appearance::default());
|
||||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||||
*clipping = true;
|
||||
}
|
||||
let image = Node::new(n.id, NodeKind::Image(ImageObject { xf, ..im.clone() }));
|
||||
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>| {
|
||||
Arc::new(Node {
|
||||
appearance: Appearance { items, ..Default::default() },
|
||||
opacity: 1.0,
|
||||
blend: Default::default(),
|
||||
isolate: false,
|
||||
knockout: Default::default(),
|
||||
knockout_shape: false,
|
||||
mask: None,
|
||||
name: None,
|
||||
..n.clone()
|
||||
})
|
||||
};
|
||||
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() },
|
||||
..n.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Evaluate an envelope of `kind` around `content` on the page axes with the options envelopes
|
||||
/// had before Envelope Options (see [`expand_live_hooks`] for an envelope node).
|
||||
pub fn envelope_expand(content: &[Arc<Node>], kind: &EnvelopeKind, fidelity: f64, outline: Outliner) -> Vec<Node> {
|
||||
let Some(src) = nodes_bounds(content) else { return content.iter().map(|c| (**c).clone()).collect() };
|
||||
let f = envelope_mapper(kind, src);
|
||||
let diag = src.width().hypot(src.height()).max(1e-6);
|
||||
let piece = diag / (8.0 + fidelity.clamp(0.0, 100.0) / 100.0 * 56.0);
|
||||
content.iter().map(|c| map_node(c, &*f, piece, outline)).collect()
|
||||
expand_envelope(content, kind, fidelity, EnvelopeOptions::default(), Affine::IDENTITY, outline.into())
|
||||
}
|
||||
|
||||
/// The surface an envelope maps `content` onto, sampled on a `rows`×`cols` grid as a mesh whose
|
||||
/// handles follow the surface and whose points are `color`: Release gives it back as the
|
||||
/// envelope's shape and Reset with Mesh starts from it. `None` when the content has no bounds.
|
||||
pub fn envelope_surface(content: &[Arc<Node>], kind: &EnvelopeKind, rows: u32, cols: u32, color: Color) -> Option<GradientMesh> {
|
||||
let src = nodes_bounds(content)?;
|
||||
let f = envelope_mapper(kind, src);
|
||||
let at = |u: f64, v: f64| f(Point::new(src.x0 + u * src.width(), src.y0 + v * src.height()));
|
||||
Some(GradientMesh::from_surface(rows, cols, &at, &|_, _| color))
|
||||
/// An envelope's content pushed through its map: Fidelity sets how finely curves are cut (and
|
||||
/// into how many pieces images are), the options what bends with the art.
|
||||
fn expand_envelope(content: &[Arc<Node>], kind: &EnvelopeKind, fidelity: f64, options: EnvelopeOptions, frame: Affine, hooks: Hooks) -> Vec<Node> {
|
||||
let Some(map) = EnvelopeMap::new(content, kind, frame) else { return content.iter().map(|c| (**c).clone()).collect() };
|
||||
let src = map.src();
|
||||
let fid = if fidelity.is_finite() { fidelity.clamp(0.0, 100.0) / 100.0 } else { 0.5 };
|
||||
let f = |p: Point| map.map(p);
|
||||
let w = Warper {
|
||||
f: &f,
|
||||
piece: src.width().hypot(src.height()).max(1e-6) / (8.0 + fid * 56.0),
|
||||
eps: (src.width().max(src.height()) * 0.05).max(0.5),
|
||||
cells: (4.0 + fid * 12.0).round() as usize,
|
||||
hooks,
|
||||
options,
|
||||
depth: 0,
|
||||
};
|
||||
content.iter().map(|c| map_node(c, &w)).collect()
|
||||
}
|
||||
|
||||
/// Envelope bounds (the image of the content box, sampled).
|
||||
pub fn envelope_bounds(content: &[Arc<Node>], kind: &EnvelopeKind) -> Option<Rect> {
|
||||
/// Envelope bounds: a mesh's points, a top object's path, the warp's surface (sampled).
|
||||
pub fn envelope_bounds(content: &[Arc<Node>], kind: &EnvelopeKind, frame: Affine) -> Option<Rect> {
|
||||
match kind {
|
||||
EnvelopeKind::Mesh { points, .. } => {
|
||||
let first = *points.first()?;
|
||||
Some(points.iter().fold(Rect::from_points(first, first), |r, p| r.union_pt(*p)))
|
||||
}
|
||||
EnvelopeKind::TopObject { path } => path.bounds(),
|
||||
EnvelopeKind::Warp { .. } => {
|
||||
let src = nodes_bounds(content)?;
|
||||
let f = envelope_mapper(kind, src);
|
||||
const N: usize = 16;
|
||||
let mut r: Option<Rect> = None;
|
||||
for j in 0..=N {
|
||||
for i in 0..=N {
|
||||
let q = f(Point::new(src.x0 + src.width() * i as f64 / N as f64, src.y0 + src.height() * j as f64 / N as f64));
|
||||
r = Some(r.map_or(Rect::from_points(q, q), |r| r.union_pt(q)));
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
EnvelopeKind::Warp { .. } => EnvelopeMap::new(content, kind, frame)?.bounds(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1195,9 +1500,14 @@ pub fn expand_live(n: &Node) -> Vec<Node> {
|
|||
}
|
||||
|
||||
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> {
|
||||
match &n.kind {
|
||||
NodeKind::Blend { children, spec } => blend_expand(children, spec),
|
||||
NodeKind::Envelope { content, kind, fidelity, .. } => envelope_expand(content, kind, *fidelity, outline),
|
||||
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(),
|
||||
_ => vec![n.clone()],
|
||||
|
|
@ -1206,7 +1516,12 @@ pub fn expand_live_with(n: &Node, outline: Outliner) -> Vec<Node> {
|
|||
|
||||
/// 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 {
|
||||
let children = expand_live_with(n, outline).into_iter().map(Arc::new).collect();
|
||||
expanded_group_hooks(n, outline.into())
|
||||
}
|
||||
|
||||
/// [`expanded_group`] with [`Hooks`].
|
||||
pub fn expanded_group_hooks(n: &Node, hooks: Hooks) -> Node {
|
||||
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;
|
||||
|
|
@ -1226,11 +1541,16 @@ pub fn expanded_group(n: &Node, outline: Outliner) -> Node {
|
|||
|
||||
/// Recursively replace every live object in `n` by plain geometry (for exporters).
|
||||
pub fn expand_deep(n: &Node, outline: Outliner) -> Node {
|
||||
let mut out = if is_live(n) { expanded_group(n, outline) } else { n.clone() };
|
||||
expand_deep_hooks(n, outline.into())
|
||||
}
|
||||
|
||||
/// [`expand_deep`] with [`Hooks`].
|
||||
pub fn expand_deep_hooks(n: &Node, hooks: Hooks) -> Node {
|
||||
let mut out = if is_live(n) { expanded_group_hooks(n, hooks) } 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(c, outline));
|
||||
*c = Arc::new(expand_deep_hooks(c, hooks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1288,6 +1608,16 @@ mod tests {
|
|||
assert!((p.x - 25.0).abs() < 1e-6 && (p.y - 25.0).abs() < 1e-6, "{p:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_go_on_past_the_content_box() {
|
||||
// A stroke outline reaches past the box: a flat mesh and a rectangle stay the identity there.
|
||||
let r = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
let p = grid_eval(2, 2, &grid_points(r, 2, 2), -0.05, 1.1);
|
||||
assert!((p.x + 5.0).abs() < 1e-6 && (p.y - 110.0).abs() < 1e-6, "{p:?}");
|
||||
let c = Coons::from_path(&shapes::rectangle(r)).unwrap().eval(1.05, -0.1);
|
||||
assert!((c.x - 105.0).abs() < 1e-6 && (c.y + 10.0).abs() < 1e-6, "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_identity() {
|
||||
let r = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
|
|
|
|||
|
|
@ -275,6 +275,11 @@ pub enum NodeKind {
|
|||
/// Envelope Options besides Fidelity.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
options: crate::live::EnvelopeOptions,
|
||||
/// The envelope's own axes (→ the document): the transforms it took since it was made that
|
||||
/// the page axes can't stand for (rotation, shear, reflection). The content maps from its
|
||||
/// bounds in this frame, and a warp bends along it.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
frame: Affine,
|
||||
},
|
||||
/// Gradient mesh.
|
||||
Mesh(GradientMesh),
|
||||
|
|
@ -536,7 +541,7 @@ impl Node {
|
|||
let b = crate::live::nodes_bounds(children);
|
||||
vectorcraft_geom::union_opt(b, spec.spine.as_ref().and_then(|s| s.bounds()))
|
||||
}
|
||||
NodeKind::Envelope { content, kind, .. } => crate::live::envelope_bounds(content, kind),
|
||||
NodeKind::Envelope { content, kind, frame, .. } => crate::live::envelope_bounds(content, kind, *frame),
|
||||
NodeKind::Mesh(m) => m.bounds(),
|
||||
NodeKind::Repeat(r) => r.bounds(),
|
||||
}
|
||||
|
|
@ -639,10 +644,11 @@ impl Node {
|
|||
s.transform(a);
|
||||
}
|
||||
}
|
||||
NodeKind::Envelope { content, kind, .. } => {
|
||||
NodeKind::Envelope { content, kind, frame, .. } => {
|
||||
for c in content.iter_mut() {
|
||||
Arc::make_mut(c).transform_scaled(a, sc);
|
||||
}
|
||||
*frame = crate::live::envelope_frame(a * *frame);
|
||||
match kind {
|
||||
EnvelopeKind::Mesh { points, .. } => {
|
||||
for p in points.iter_mut() {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeKind, StrokeLayer};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, FillLayer, Node, NodeKind, StrokeLayer};
|
||||
use vectorcraft_geom::{FillRule, PathData};
|
||||
|
||||
use crate::group::{has_own_paint, paints};
|
||||
|
|
@ -281,6 +281,48 @@ pub fn expand_art(d: &mut Document, n: &mut Node, stroke_art: StrokeArt) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Envelope Options → Distort Appearance: path or compound path `n` as plain filled art that an
|
||||
/// envelope bends as a whole: its geometry effects applied, every visible fill a copy of the path
|
||||
/// painted by that fill and every visible stroke without a brush its filled outline in the
|
||||
/// stroke's paint, opacity and blend mode (a brushed stroke stays a stroke), in paint order, as
|
||||
/// the members of a group keeping `n`'s id, transparency, opacity mask and raster effects. `None`
|
||||
/// when it has neither a stroke to outline nor a geometry effect, so bending first changes nothing.
|
||||
pub fn bake_appearance(n: &Node) -> Option<Node> {
|
||||
if !matches!(n.kind, NodeKind::Path { clipping: false, guide: false, .. } | NodeKind::Compound { .. }) {
|
||||
return None;
|
||||
}
|
||||
let outlined = |i: &AppearanceItem| paints(i) && matches!(i, AppearanceItem::Stroke(s) if s.brush.is_none());
|
||||
let geometric = has_geometry(&n.appearance.effects) || n.appearance.items.iter().any(|i| has_geometry(item_effects(i)));
|
||||
if !geometric && !n.appearance.items.iter().any(outlined) {
|
||||
return None;
|
||||
}
|
||||
let (g, rule, ctx, mut m) = leaf_base(n)?;
|
||||
let piece = |path: PathData, rule: FillRule, item: AppearanceItem| {
|
||||
let mut p = Node::path(n.id, path, Appearance { items: vec![item], ..Default::default() });
|
||||
if let NodeKind::Path { rule: r, .. } = &mut p.kind {
|
||||
*r = rule;
|
||||
}
|
||||
Arc::new(p)
|
||||
};
|
||||
let mut pieces = vec![];
|
||||
for item in n.appearance.items.iter().filter(|i| paints(i)) {
|
||||
let ig = apply(item_effects(item), &g, &ctx.item(item));
|
||||
let mut it = item.clone();
|
||||
clear_item_effects(&mut it);
|
||||
pieces.push(match &it {
|
||||
AppearanceItem::Stroke(st) if st.brush.is_none() => {
|
||||
let fill = FillLayer { opacity: st.opacity, blend: st.blend, ..FillLayer::new(st.paint.clone()) };
|
||||
piece(crate::stroke::outline_region(&ig, rule, st), FillRule::NonZero, AppearanceItem::Fill(fill))
|
||||
}
|
||||
_ => piece(ig, rule, it),
|
||||
});
|
||||
}
|
||||
m.appearance.items.clear();
|
||||
m.appearance.contents_index = None;
|
||||
m.kind = NodeKind::Group { children: pieces, clip: false };
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// A copy of `doc` with every live geometry effect baked into plain paths, or `None` when the
|
||||
/// document has none (export it as is).
|
||||
pub fn bake_document(doc: &Document) -> Option<Document> {
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ use vectorcraft_doc::{AppearanceItem, Effect, Node, NodeKind, StrokeLayer};
|
|||
use vectorcraft_geom::{BezPath, FillRule, PathData, Rect};
|
||||
|
||||
pub use adjust::{ADJUSTMENTS, ColorMap, ImageHook, adjust, adjust_in_document, color_map, curve_at, curve_points, has_adjustment, is_adjustment};
|
||||
pub use bake::{StrokeArt, bake_document, expand_art, expand_leaf, fresh_ids, needs_bake};
|
||||
pub use bake::{StrokeArt, bake_appearance, bake_document, expand_art, expand_leaf, fresh_ids, needs_bake};
|
||||
pub use clip::clip_outline;
|
||||
pub use group::{
|
||||
OutlineHook, PATHFINDER_EFFECTS, evaluate_container, has_container_appearance, has_pathfinder, is_pathfinder, member_shapes, paints,
|
||||
pathfinder_children,
|
||||
};
|
||||
pub use live::{expand_live, expand_live_deep, text_outliner};
|
||||
pub use live::{expand_live, expand_live_deep, expanded_live_group, text_outliner};
|
||||
pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks};
|
||||
pub use raster::{RasterFx, outset, raster_effects};
|
||||
pub use reshape::{expand_outlined, needs_outline, outline_art, outline_text, reshape};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
//! Live objects (blends, envelopes, gradient meshes, repeats) evaluated alike for the renderer and
|
||||
//! every exporter, with what the pure evaluation in [`vectorcraft_doc::live`] can't do itself: type
|
||||
//! inside an envelope distorts as its glyph outlines, run by run ([`outline_text`]).
|
||||
//! every exporter, with what the pure evaluation in [`vectorcraft_doc::live`] can't do itself
|
||||
//! ([`Hooks`]): type inside an envelope distorts as its glyph outlines, run by run
|
||||
//! ([`outline_text`]); symbol instances as their symbol's art; with Distort Appearance strokes and
|
||||
//! geometry effects bend with the art ([`bake_appearance`]); with Distort Pattern Fills the pattern
|
||||
//! tiles do.
|
||||
|
||||
use vectorcraft_doc::Node;
|
||||
use vectorcraft_doc::live::{self, Outliner};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::outline_text;
|
||||
use vectorcraft_doc::live::{self, Hooks, Outliner};
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
|
||||
use crate::{bake_appearance, outline_text};
|
||||
|
||||
/// The text outliner live evaluation uses: type as its glyph outlines, one path per run in the
|
||||
/// run's paint ([`outline_text`]).
|
||||
|
|
@ -13,13 +19,39 @@ pub fn text_outliner() -> Outliner<'static> {
|
|||
Some(&outline_text)
|
||||
}
|
||||
|
||||
/// One level of evaluation of live object `n` ([`live::expand_live`]) with type outlined.
|
||||
pub fn expand_live(n: &Node) -> Vec<Node> {
|
||||
live::expand_live_with(n, text_outliner())
|
||||
/// Type outlined, or a symbol instance as its symbol's art (from `doc`) placed by the instance.
|
||||
fn outline(doc: Option<&Document>, n: &Node) -> Option<Node> {
|
||||
match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
let sym = doc?.symbols.iter().find(|s| s.name == *symbol)?;
|
||||
let mut art = (*sym.art).clone();
|
||||
art.transform(*xf, false);
|
||||
Some(Node::group(n.id, vec![Arc::new(art)]))
|
||||
}
|
||||
_ => outline_text(n),
|
||||
}
|
||||
}
|
||||
|
||||
/// `n` with every live object in it replaced by its evaluated art ([`live::expand_deep`]) and type
|
||||
/// inside envelopes outlined: what the exporters write for live objects.
|
||||
pub fn expand_live_deep(n: &Node) -> Node {
|
||||
live::expand_deep(n, text_outliner())
|
||||
/// Run `f` with the hooks live evaluation needs; symbols and patterns come from `doc` (without a
|
||||
/// document, symbol instances and pattern tiles stay as they are).
|
||||
fn with_hooks<R>(doc: Option<&Document>, f: impl FnOnce(Hooks) -> R) -> R {
|
||||
let outline = |n: &Node| outline(doc, n);
|
||||
let tiles = |name: &str, xf: Affine, region: Rect| Some(doc?.pattern(name)?.instances_in(xf, region));
|
||||
f(Hooks { outline: Some(&outline), appearance: Some(&bake_appearance), pattern: Some(&tiles) })
|
||||
}
|
||||
|
||||
/// One level of evaluation of live object `n` ([`live::expand_live_hooks`]).
|
||||
pub fn expand_live(doc: Option<&Document>, n: &Node) -> Vec<Node> {
|
||||
with_hooks(doc, |h| live::expand_live_hooks(n, h))
|
||||
}
|
||||
|
||||
/// Live object `n` evaluated into a plain group ([`live::expanded_group_hooks`]): Expand.
|
||||
pub fn expanded_live_group(doc: Option<&Document>, n: &Node) -> Node {
|
||||
with_hooks(doc, |h| live::expanded_group_hooks(n, h))
|
||||
}
|
||||
|
||||
/// `n` with every live object in it replaced by its evaluated art ([`live::expand_deep_hooks`]):
|
||||
/// what the exporters write for live objects.
|
||||
pub fn expand_live_deep(doc: Option<&Document>, n: &Node) -> Node {
|
||||
with_hooks(doc, |h| live::expand_deep_hooks(n, h))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ use std::sync::Arc;
|
|||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::live::{
|
||||
self, BlendDefaults, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, EnvelopeOptions, GradientMesh, MeshAppearance, PreserveShape, Spine,
|
||||
self, BlendDefaults, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, EnvelopeMap, EnvelopeOptions, GradientMesh, MeshAppearance,
|
||||
PreserveShape, Spine,
|
||||
};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_geom::{Affine, PathData, Point};
|
||||
|
|
@ -930,7 +931,7 @@ fn envelope_defaults(s: &Session) -> (EnvelopeOptions, f64) {
|
|||
}
|
||||
|
||||
fn envelope(id: NodeId, content: Vec<Arc<Node>>, kind: EnvelopeKind, (options, fidelity): (EnvelopeOptions, f64)) -> Node {
|
||||
Node::new(id, NodeKind::Envelope { content, kind, fidelity, editing: false, options })
|
||||
Node::new(id, NodeKind::Envelope { content, kind, fidelity, editing: false, options, frame: Affine::IDENTITY })
|
||||
}
|
||||
|
||||
/// `rows` and `cols` (or `columns`), 1..50 each, defaulting to `default`.
|
||||
|
|
@ -954,14 +955,11 @@ fn env_make_warp(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
/// A mesh envelope of `rows`×`cols` patches over `content`: following the surface of `shape` (an
|
||||
/// envelope's current kind), or a flat grid over the content's bounds.
|
||||
fn mesh_kind(content: &[Arc<Node>], shape: Option<&EnvelopeKind>, rows: u32, cols: u32) -> Option<EnvelopeKind> {
|
||||
let points = match shape {
|
||||
Some(k) => live::envelope_surface(content, k, rows, cols, Color::BLACK)?.points.into_iter().map(|m| m.p).collect(),
|
||||
None => live::grid_points(live::nodes_bounds(content)?, rows, cols),
|
||||
};
|
||||
Some(EnvelopeKind::Mesh { rows, cols, points })
|
||||
/// A mesh envelope of `rows`×`cols` patches for an envelope with map `map`: following its
|
||||
/// current surface (`maintain`), or a flat grid over its content.
|
||||
fn mesh_kind(map: &EnvelopeMap, maintain: bool, rows: u32, cols: u32) -> EnvelopeKind {
|
||||
let points = if maintain { map.surface_mesh(rows, cols, Color::BLACK).points.into_iter().map(|m| m.p).collect() } else { map.grid(rows, cols) };
|
||||
EnvelopeKind::Mesh { rows, cols, points }
|
||||
}
|
||||
|
||||
fn env_make_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
|
@ -970,7 +968,8 @@ fn env_make_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let roots = roots_param(s, p)?;
|
||||
let st = s.doc()?;
|
||||
let nodes: Vec<Arc<Node>> = roots.iter().filter_map(|id| st.doc.node(*id).cloned()).map(Arc::new).collect();
|
||||
let kind = mesh_kind(&nodes, None, rows, cols).ok_or_else(|| bad(C, "selection has no bounds"))?;
|
||||
let src = live::nodes_bounds(&nodes).ok_or_else(|| bad(C, "selection has no bounds"))?;
|
||||
let kind = EnvelopeKind::Mesh { rows, cols, points: live::grid_points(src, rows, cols) };
|
||||
let defaults = envelope_defaults(s);
|
||||
let id = s.edit("Make Envelope", |d, sel| wrap(d, sel, &roots, |id, content| envelope(id, content, kind, defaults)))?;
|
||||
Ok(json!({ "id": id.0 }))
|
||||
|
|
@ -1005,13 +1004,15 @@ fn env_make_top(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
/// The kind `f` makes of each selected envelope (from its content and its current kind).
|
||||
fn reshaped(s: &Session, f: impl Fn(&[Arc<Node>], &EnvelopeKind) -> Result<EnvelopeKind>) -> Result<Vec<(NodeId, EnvelopeKind)>> {
|
||||
/// The kind `f` makes of each selected envelope (from the envelope and its current kind).
|
||||
fn reshaped(s: &Session, f: impl Fn(&Node, &EnvelopeKind) -> Result<EnvelopeKind>) -> Result<Vec<(NodeId, EnvelopeKind)>> {
|
||||
let doc = &s.doc()?.doc;
|
||||
let mut out = vec![];
|
||||
for e in selected_of(s, is_envelope) {
|
||||
if let Some(NodeKind::Envelope { content, kind, .. }) = doc.node(e).map(|n| &n.kind) {
|
||||
out.push((e, f(content, kind)?));
|
||||
if let Some(n) = doc.node(e)
|
||||
&& let NodeKind::Envelope { kind, .. } = &n.kind
|
||||
{
|
||||
out.push((e, f(n, kind)?));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
|
@ -1039,13 +1040,14 @@ fn env_reset_warp(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
fn env_reset_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.envelope.resetWithMesh";
|
||||
let maintain = bool_or(p, "maintainShape", true);
|
||||
let kinds = reshaped(s, |content, kind| {
|
||||
let kinds = reshaped(s, |env, kind| {
|
||||
let own = match kind {
|
||||
EnvelopeKind::Mesh { rows, cols, .. } => (*rows, *cols),
|
||||
_ => (4, 4),
|
||||
};
|
||||
let (rows, cols) = rows_cols(p, C, own)?;
|
||||
mesh_kind(content, maintain.then_some(kind), rows, cols).ok_or_else(|| bad(C, "the envelope's content has no bounds"))
|
||||
let map = EnvelopeMap::of(env).ok_or_else(|| bad(C, "the envelope's content has no bounds"))?;
|
||||
Ok(mesh_kind(&map, maintain, rows, cols))
|
||||
})?;
|
||||
set_kinds(s, "Reset with Mesh", kinds)
|
||||
}
|
||||
|
|
@ -1096,9 +1098,9 @@ fn env_info(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
Ok(v)
|
||||
}
|
||||
|
||||
/// What Release gives back as an envelope's shape: a top object's path, or the surface of a warp
|
||||
/// or mesh envelope as a gradient mesh, painted grey (id 0).
|
||||
fn envelope_shape(content: &[Arc<Node>], kind: &EnvelopeKind) -> Option<Node> {
|
||||
/// What Release gives back as the shape of envelope `env` (of `kind`): a top object's path, or the
|
||||
/// surface of a warp or mesh envelope as a gradient mesh, painted grey (id 0).
|
||||
fn envelope_shape(env: &Node, kind: &EnvelopeKind) -> Option<Node> {
|
||||
let grey = Color::gray(0.25);
|
||||
let (rows, cols) = match kind {
|
||||
EnvelopeKind::TopObject { path } => {
|
||||
|
|
@ -1107,7 +1109,7 @@ fn envelope_shape(content: &[Arc<Node>], kind: &EnvelopeKind) -> Option<Node> {
|
|||
EnvelopeKind::Mesh { rows, cols, .. } => (*rows, *cols),
|
||||
EnvelopeKind::Warp { .. } => (4, 4),
|
||||
};
|
||||
live::envelope_surface(content, kind, rows, cols, grey).map(|m| Node::new(NodeId(0), NodeKind::Mesh(m)))
|
||||
EnvelopeMap::of(env).map(|map| Node::new(NodeId(0), NodeKind::Mesh(map.surface_mesh(rows, cols, grey))))
|
||||
}
|
||||
|
||||
/// Released content keeps envelope `env`'s opacity, blend mode, isolation, knockout and opacity
|
||||
|
|
@ -1143,7 +1145,7 @@ fn env_release(s: &mut Session, _: &Value) -> Result<Value> {
|
|||
let Some(n) = d.node(*e).cloned() else { continue };
|
||||
let NodeKind::Envelope { content, kind, .. } = &n.kind else { continue };
|
||||
let mut nodes = carry_transparency(d, &n, content.iter().map(|c| (**c).clone()).collect());
|
||||
if let Some(mut shape) = envelope_shape(content, kind) {
|
||||
if let Some(mut shape) = envelope_shape(&n, kind) {
|
||||
shape.id = d.alloc_id();
|
||||
nodes.push(shape);
|
||||
}
|
||||
|
|
@ -1195,7 +1197,7 @@ fn env_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
/// transparency and opacity mask; the generated pieces get ids of their own.
|
||||
fn expand_envelope(d: &mut Document, id: NodeId) -> Result<()> {
|
||||
let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
let mut g = live::expanded_group(&n, vectorcraft_render::effects::text_outliner());
|
||||
let mut g = vectorcraft_render::effects::expanded_live_group(Some(d), &n);
|
||||
fix_ids(d, &mut g);
|
||||
// Outlined type repeats its object's id on its pieces.
|
||||
vectorcraft_render::effects::fresh_ids(d, &mut g, &mut Default::default());
|
||||
|
|
|
|||
|
|
@ -1210,6 +1210,8 @@ mod tests_emptytype;
|
|||
#[cfg(test)]
|
||||
mod tests_envelope;
|
||||
#[cfg(test)]
|
||||
mod tests_envelope_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_expand;
|
||||
#[cfg(test)]
|
||||
mod tests_eyedropper;
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ fn reset_with_warp_and_mesh_switch_the_kind_and_keep_the_content() {
|
|||
assert!(s.execute("object.envelope.resetWithWarp", &json!({"style": "nope"})).is_err());
|
||||
// Back to a mesh that keeps the arch's shape: the top middle point is where the warp put it.
|
||||
s.execute("object.envelope.resetWithWarp", &json!({"style": "arch", "bend": 50, "horizontal": true})).unwrap();
|
||||
let top = live::envelope_surface(&node(&s, e).children().unwrap().clone(), &kind(&s, e), 2, 2, vectorcraft_color::Color::BLACK).unwrap();
|
||||
let top = live::EnvelopeMap::of(&node(&s, e)).unwrap().surface_mesh(2, 2, vectorcraft_color::Color::BLACK);
|
||||
s.execute("object.envelope.resetWithMesh", &json!({"rows": 2, "cols": 2})).unwrap();
|
||||
let EnvelopeKind::Mesh { rows: 2, cols: 2, points } = kind(&s, e) else { panic!("{:?}", kind(&s, e)) };
|
||||
assert!(points[1].distance(top.points[1].p) < 1e-9 && points[1].y < 99.0, "{:?}", points[1]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,298 @@
|
|||
//! What envelopes distort: the envelope's own frame under transforms, images (a raster mesh warp),
|
||||
//! symbols, linear gradients, pattern fills, strokes and effects (Distort Appearance).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
|
||||
use vectorcraft_doc::live::EnvelopeOptions;
|
||||
use vectorcraft_doc::{AppearanceItem, Node, NodeKind, PatternDef, Symbol};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, shapes};
|
||||
use vectorcraft_render::effects;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn id_of(v: &Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn node(s: &Session, id: NodeId) -> Node {
|
||||
s.doc().unwrap().doc.node(id).unwrap().clone()
|
||||
}
|
||||
|
||||
fn sel(s: &mut Session, ids: &[NodeId]) {
|
||||
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
|
||||
}
|
||||
|
||||
/// A 200×100 rectangle at (100, 100), filled red, no stroke.
|
||||
fn rect(s: &mut Session) -> NodeId {
|
||||
let a = id_of(&s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 100})).unwrap());
|
||||
s.edit("red", |d, _| {
|
||||
d.node_mut(a).unwrap().appearance = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
a
|
||||
}
|
||||
|
||||
/// Envelope the selection with an arch warp (`bend` %) and `options` → the envelope.
|
||||
fn arch(s: &mut Session, bend: f64, options: Value) -> NodeId {
|
||||
let e = id_of(&s.execute("object.envelope.makeWithWarp", &json!({"style": "arch", "bend": bend})).unwrap());
|
||||
s.execute("object.envelope.options", &options).unwrap();
|
||||
e
|
||||
}
|
||||
|
||||
/// The envelope evaluated as the canvas and the exporters do.
|
||||
fn evaluated(s: &Session, e: NodeId) -> Vec<Node> {
|
||||
effects::expand_live(Some(&s.doc().unwrap().doc), &node(s, e))
|
||||
}
|
||||
|
||||
fn anchors(nodes: &[Node]) -> Vec<Point> {
|
||||
let mut v = vec![];
|
||||
for n in nodes {
|
||||
n.walk(&mut |c| v.extend(c.path_data().into_iter().flat_map(|p| p.anchors().map(|(_, _, a)| a.p))));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn frame(s: &Session, e: NodeId) -> Affine {
|
||||
match node(s, e).kind {
|
||||
NodeKind::Envelope { frame, .. } => frame,
|
||||
k => panic!("{k:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression: rotating a warp envelope rotated its content and re-warped it along the page axes.
|
||||
#[test]
|
||||
fn a_rotated_warp_envelope_turns_with_its_warp() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let e = arch(&mut s, 50.0, json!({}));
|
||||
let before = anchors(&evaluated(&s, e));
|
||||
let c = Point::new(200.0, 150.0);
|
||||
s.execute("object.rotate", &json!({"angle": 90, "origin": [c.x, c.y]})).unwrap();
|
||||
let after = anchors(&evaluated(&s, e));
|
||||
let turn = Affine::rotate_about(-std::f64::consts::FRAC_PI_2, c);
|
||||
assert_eq!(before.len(), after.len());
|
||||
for (p, q) in before.iter().zip(&after) {
|
||||
assert!((turn * *p).distance(*q) < 1e-6, "{:?} vs {q:?}", turn * *p);
|
||||
}
|
||||
assert_ne!(frame(&s, e), Affine::IDENTITY);
|
||||
// Turning back squares the frame with the page again; a plain move or scale never tilts it.
|
||||
s.execute("object.rotate", &json!({"angle": -90, "origin": [c.x, c.y]})).unwrap();
|
||||
assert_eq!(frame(&s, e), Affine::IDENTITY);
|
||||
s.execute("object.scale", &json!({"sx": 150, "sy": 50})).unwrap();
|
||||
assert_eq!(frame(&s, e), Affine::IDENTITY);
|
||||
// A reflection keeps a frame: the arch flips with the art.
|
||||
s.execute("object.reflect", &json!({"axis": "horizontal"})).unwrap();
|
||||
let flipped = anchors(&evaluated(&s, e));
|
||||
let b = node(&s, e).geometric_bounds().unwrap();
|
||||
assert!(flipped.iter().any(|p| p.y > b.y1 - 1.0) && frame(&s, e) != Affine::IDENTITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_frame_round_trips_natively() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let e = arch(&mut s, 50.0, json!({}));
|
||||
s.execute("object.rotate", &json!({"angle": 30})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(d, false)).unwrap();
|
||||
assert_eq!(back.node(e), d.node(e));
|
||||
let mut old = serde_json::to_value(node(&s, e)).unwrap();
|
||||
old["kind"].as_object_mut().unwrap().remove("frame");
|
||||
let n: Node = serde_json::from_value(old).unwrap();
|
||||
assert!(matches!(n.kind, NodeKind::Envelope { frame, .. } if frame == Affine::IDENTITY), "older envelopes stand square to the page");
|
||||
}
|
||||
|
||||
/// A selected embedded 40×40 pt red image at (100, 100).
|
||||
fn red_image(s: &mut Session) -> NodeId {
|
||||
let img = image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 0, 0, 255]));
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
s.edit("Place", |d, sel| {
|
||||
d.images.insert("pic".into(), vectorcraft_doc::ImageBlob::png(png));
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::translate((100.0, 100.0)) * Affine::scale(10.0);
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 4, height: 4, xf, link: None, placement: Default::default() };
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(im)))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn images_bend_as_a_raster_mesh_warp() {
|
||||
let mut s = session();
|
||||
let im = red_image(&mut s);
|
||||
sel(&mut s, &[im]);
|
||||
let e = arch(&mut s, 80.0, json!({}));
|
||||
let out = evaluated(&s, e);
|
||||
let mut pieces = 0;
|
||||
out[0].walk(&mut |c| pieces += matches!(c.kind, NodeKind::Image(_)) as usize);
|
||||
assert!(pieces >= 32, "{pieces} pieces");
|
||||
// The arch lifts the image's middle above its top edge, not its corners.
|
||||
let img = vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true);
|
||||
let b = node(&s, e).geometric_bounds().unwrap();
|
||||
assert!(b.y0 < 95.0, "{b:?}");
|
||||
let mid = img.pixel(120, 97);
|
||||
assert!(mid[0] > 200 && mid[1] < 60, "the middle rose: {mid:?}");
|
||||
assert!(img.pixel(101, 95)[1] > 200, "the corner stayed: {:?}", img.pixel(101, 95));
|
||||
// The pieces export as clipped images, bent in the SVG too.
|
||||
let svg = s.execute("document.export", &json!({"format": "svg"})).unwrap()["dataBase64"].as_str().unwrap().to_string();
|
||||
let svg = String::from_utf8(vectorcraft_format::base64_decode(&svg).unwrap()).unwrap();
|
||||
assert!(svg.matches("<use").count() >= 32 && svg.contains("clip"), "{}", &svg[..svg.len().min(400)]);
|
||||
assert_eq!(svg.matches("<image").count(), 1, "the pieces share the image's pixels");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbols_bend_with_the_envelope() {
|
||||
let mut s = session();
|
||||
let inst = s
|
||||
.edit("symbol", |d, sel| {
|
||||
// Symbol art is normalised to ±10 (its instance's transform sizes it).
|
||||
let art = Node::path(
|
||||
NodeId(0),
|
||||
shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)),
|
||||
vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
|
||||
);
|
||||
d.symbols.push(Symbol { name: "Box".into(), art: Arc::new(art) });
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::translate((200.0, 150.0)) * Affine::scale_non_uniform(10.0, 5.0);
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Box".into(), xf }))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})
|
||||
.unwrap();
|
||||
sel(&mut s, &[inst]);
|
||||
let e = arch(&mut s, 60.0, json!({}));
|
||||
let pts = anchors(&evaluated(&s, e));
|
||||
assert!(pts.iter().any(|p| p.y < 90.0), "the symbol's art rose in the middle");
|
||||
// Editing the symbol redraws the envelope (the renderer's cache follows the symbol's art).
|
||||
let mut r = vectorcraft_render::Renderer::new();
|
||||
let region = Rect::new(0.0, 0.0, 400.0, 300.0);
|
||||
assert!(r.render_region(&s.doc().unwrap().doc, region, 1.0, true).pixel(200, 150)[1] < 60);
|
||||
s.edit("edit symbol", |d, _| {
|
||||
let art = Node::path(NodeId(0), shapes::rectangle(Rect::new(-10.0, -10.0, -8.0, -8.0)), Default::default());
|
||||
d.symbols[0].art = Arc::new(art);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert!(r.render_region(&s.doc().unwrap().doc, region, 1.0, true).pixel(200, 150)[1] > 200);
|
||||
}
|
||||
|
||||
fn strokes(nodes: &[Node]) -> usize {
|
||||
let mut n = 0;
|
||||
for c in nodes {
|
||||
c.walk(&mut |c| n += c.appearance.items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count());
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distort_appearance_bends_strokes_and_effects_with_the_art() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
s.execute("paint.setStroke", &json!({"ids": [a.0], "color": "#000000"})).unwrap();
|
||||
s.execute("stroke.set", &json!({"ids": [a.0], "weight": 8})).unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
let e = arch(&mut s, 60.0, json!({"distortAppearance": true}));
|
||||
let out = evaluated(&s, e);
|
||||
assert_eq!(strokes(&out), 0, "the stroke became its bent outline");
|
||||
// Off: the stroke paints along the bent path at its own weight.
|
||||
s.execute("object.envelope.options", &json!({"distortAppearance": false})).unwrap();
|
||||
assert_eq!(strokes(&evaluated(&s, e)), 1);
|
||||
// Geometry effects bake into the art first too.
|
||||
s.execute("object.envelope.editContents", &json!({"editing": true})).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect": "distort.zigZag", "params": {"size": 5, "ridges": 10}})).unwrap();
|
||||
s.execute("object.envelope.editContents", &json!({"editing": false})).unwrap();
|
||||
s.execute("object.envelope.options", &json!({"distortAppearance": true})).unwrap();
|
||||
let out = evaluated(&s, e);
|
||||
let mut effects_left = 0;
|
||||
out[0].walk(&mut |c| effects_left += c.appearance.effects.len());
|
||||
assert_eq!(effects_left, 0);
|
||||
assert!(anchors(&out).len() > anchors(&[node(&s, a)]).len());
|
||||
}
|
||||
|
||||
fn gradient_geom(nodes: &[Node]) -> Option<vectorcraft_color::GradientGeom> {
|
||||
let mut g = None;
|
||||
for n in nodes {
|
||||
n.walk(&mut |c| {
|
||||
if let Paint::Gradient(gp) = c.appearance.fill_paint() {
|
||||
g = g.or(gp.geom);
|
||||
}
|
||||
});
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_gradients_bend_when_asked() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
s.edit("gradient", |d, _| {
|
||||
d.node_mut(a).unwrap().appearance.set_fill(Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
let e = arch(&mut s, 60.0, json!({"distortAppearance": true}));
|
||||
assert!(gradient_geom(&evaluated(&s, e)).is_none(), "off: the gradient fits the bent shape");
|
||||
s.execute("object.envelope.options", &json!({"distortLinearGradients": true})).unwrap();
|
||||
let g = gradient_geom(&evaluated(&s, e)).expect("on: the gradient is placed and follows the warp");
|
||||
// Arch lifts the middle: the vector (left to right through the middle) moved up.
|
||||
assert!(g.start.midpoint(g.end).y < 150.0 - 1.0, "{g:?}");
|
||||
// It needs Distort Appearance.
|
||||
s.execute("object.envelope.options", &json!({"distortAppearance": false})).unwrap();
|
||||
assert!(gradient_geom(&evaluated(&s, e)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_fills_bend_when_asked() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
s.edit("pattern", |d, _| {
|
||||
let dot = Node::path(
|
||||
NodeId(0),
|
||||
shapes::rectangle(Rect::new(0.0, 0.0, 5.0, 5.0)),
|
||||
vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
|
||||
);
|
||||
let mut def = PatternDef::new("Dots", vec![Arc::new(dot)]);
|
||||
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
|
||||
d.patterns.push(def);
|
||||
d.node_mut(a).unwrap().appearance.set_fill(vectorcraft_doc::pattern::pattern_paint("Dots"));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
sel(&mut s, &[a]);
|
||||
let e = arch(&mut s, 60.0, json!({"distortAppearance": true}));
|
||||
let clip_groups = |out: &[Node]| {
|
||||
let mut n = 0;
|
||||
out[0].walk(&mut |c| n += matches!(c.kind, NodeKind::Group { clip: true, .. }) as usize);
|
||||
n
|
||||
};
|
||||
assert_eq!(clip_groups(&evaluated(&s, e)), 0, "off: the pattern fills the bent shape unbent");
|
||||
s.execute("object.envelope.options", &json!({"distortPatternFills": true})).unwrap();
|
||||
let out = evaluated(&s, e);
|
||||
assert_eq!(clip_groups(&out), 1);
|
||||
assert!(anchors(&out).len() > 50 * 4, "the tiles are art");
|
||||
assert!(!matches!(out[0].appearance.fill_paint(), Paint::Pattern { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_envelopes_distort_appearance_and_old_ones_keep_their_look() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s);
|
||||
sel(&mut s, &[a]);
|
||||
let e = arch(&mut s, 50.0, json!({}));
|
||||
assert!(matches!(node(&s, e).kind, NodeKind::Envelope { options, .. } if options == EnvelopeOptions::NEW));
|
||||
}
|
||||
|
|
@ -399,8 +399,10 @@ fn envelope_release_and_expand() {
|
|||
s.execute("object.envelope.expand", &json!({})).unwrap();
|
||||
let g = node(&s, e);
|
||||
assert!(matches!(g.kind, NodeKind::Group { .. }));
|
||||
let p = g.children().unwrap()[0].path_data().unwrap().anchor_count();
|
||||
assert!(p > 4);
|
||||
// (Distort Appearance, on for new envelopes, expands the stroke too: the fill comes first.)
|
||||
let mut fill = None;
|
||||
g.walk(&mut |c| fill = fill.or_else(|| c.path_data().map(|p| p.anchor_count())));
|
||||
assert!(fill.unwrap() > 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ impl<'a> Scene<'a> {
|
|||
}
|
||||
// Live blends, envelopes, meshes and repeats are written as their evaluated art.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => {
|
||||
let g = vectorcraft_effects::expand_live_deep(n);
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(c, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ impl<'a> Scene<'a> {
|
|||
}
|
||||
// Live blends, envelopes, meshes and repeats are written as their evaluated art.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => {
|
||||
let g = vectorcraft_effects::expand_live_deep(n);
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(c, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -983,7 +983,7 @@ impl Exporter<'_> {
|
|||
}
|
||||
// Live blends/envelopes/meshes export their evaluated (expanded) form.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => {
|
||||
let g = vectorcraft_effects::expand_live_deep(n);
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(s, c, page, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -834,6 +834,16 @@ impl Renderer {
|
|||
&& let Some(region) = self.clip_of(clip)
|
||||
{
|
||||
let (fill, stroke) = self.clip_paint_of(clip);
|
||||
// An image inside its own frame painted straight into the clipping path, without
|
||||
// a clip layer (envelopes cut distorted images into many such pieces).
|
||||
if let ([image], None, None) = (rest, &fill, &stroke)
|
||||
&& let NodeKind::Image(im) = &image.kind
|
||||
&& plain_image(image, im, ®ion.0)
|
||||
{
|
||||
self.draw_image_in(ctx, f, im, Some(®ion));
|
||||
self.stats.drawn += 1;
|
||||
return;
|
||||
}
|
||||
let blends = self.blends_through(n);
|
||||
let bounds = if blends { Some(region.0.bounding_box()) } else { None };
|
||||
let comp = Composite { clip: Some((®ion.0, region.1)), blends, bounds, ..Default::default() };
|
||||
|
|
@ -1264,6 +1274,12 @@ impl Renderer {
|
|||
}
|
||||
|
||||
fn draw_image(&mut self, ctx: &mut RenderContext, f: &Frame, im: &vectorcraft_doc::ImageObject) {
|
||||
self.draw_image_in(ctx, f, im, None);
|
||||
}
|
||||
|
||||
/// Draw an image, or with `area` (a region in the document inside the image's frame, see
|
||||
/// [`plain_image`]) the image painted into that region.
|
||||
fn draw_image_in(&mut self, ctx: &mut RenderContext, f: &Frame, im: &vectorcraft_doc::ImageObject, area: Option<&(BezPath, FillRule)>) {
|
||||
let rect = Rect::new(0.0, 0.0, im.width as f64, im.height as f64);
|
||||
// Outline mode draws the image's frame, and with Document Setup → Show Images in Outline
|
||||
// Mode its pixels in greyscale under the frame.
|
||||
|
|
@ -1278,10 +1294,20 @@ impl Renderer {
|
|||
let pm = if outline { pm } else { self.ink_image(&cache_key, &pm, f.ink) };
|
||||
let sx = im.width as f64 / pm.width().max(1) as f64;
|
||||
let sy = im.height as f64 / pm.height().max(1) as f64;
|
||||
ctx.set_transform(f.view * im.xf);
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler: peniko::ImageSampler::default() });
|
||||
ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy));
|
||||
ctx.fill_rect(&rect);
|
||||
match area {
|
||||
Some((bp, rule)) => {
|
||||
ctx.set_transform(f.view);
|
||||
ctx.set_paint_transform(im.xf * Affine::scale_non_uniform(sx, sy));
|
||||
ctx.set_fill_rule(fill_rule(*rule));
|
||||
ctx.fill_path(bp);
|
||||
}
|
||||
None => {
|
||||
ctx.set_transform(f.view * im.xf);
|
||||
ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy));
|
||||
ctx.fill_rect(&rect);
|
||||
}
|
||||
}
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
if outline {
|
||||
|
|
@ -1352,6 +1378,21 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Can image node `n` (`im`) clipped by `region` be painted straight into the region: visible,
|
||||
/// with no transparency, effects or paint of its own, and the region inside the image's frame
|
||||
/// (within a pixel), so no paint past the image's edge shows?
|
||||
fn plain_image(n: &Node, im: &vectorcraft_doc::ImageObject, region: &BezPath) -> bool {
|
||||
if !(n.visible && n.has_default_transparency() && n.appearance.items.is_empty() && n.appearance.effects.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
let det = im.xf.determinant();
|
||||
if !(det.abs() > 1e-12 && det.is_finite()) {
|
||||
return false;
|
||||
}
|
||||
let b = im.xf.inverse().transform_rect_bbox(region.bounding_box());
|
||||
b.x0 >= -1.0 && b.y0 >= -1.0 && b.x1 <= im.width as f64 + 1.0 && b.y1 <= im.height as f64 + 1.0
|
||||
}
|
||||
|
||||
/// Opacity-mask coverage of one premultiplied pixel: luminance, with the area outside the mask
|
||||
/// art black (clip) or white (no clip), optionally inverted.
|
||||
fn mask_value(r: u8, g: u8, b: u8, a: u8, clip: bool, invert: bool) -> u8 {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
//! Live objects in the renderer.
|
||||
//!
|
||||
//! Blends and envelopes are evaluated by `vectorcraft_doc::live` and the resulting objects cached
|
||||
//! by `Arc` identity of the live node (so the steps keep stable `Arc`s and hit the geometry cache
|
||||
//! frame after frame). Gradient meshes are tessellated into many small solid-colour quads (vello
|
||||
//! Blends and envelopes are evaluated by `vectorcraft_doc::live` (through
|
||||
//! `vectorcraft_effects::expand_live`, as the exporters do) and the resulting objects cached by
|
||||
//! `Arc` identity of the live node and of the symbols and patterns it draws (so the steps keep
|
||||
//! stable `Arc`s and hit the geometry cache frame after frame, and editing a symbol redraws an
|
||||
//! envelope around an instance of it). Gradient meshes are tessellated into many small solid-colour quads (vello
|
||||
//! has no mesh shading), with the density chosen from the on-screen patch size; each quad is
|
||||
//! grown by half a device pixel along its edges so neighbours overlap and no antialiasing seams
|
||||
//! show.
|
||||
|
|
@ -11,7 +13,7 @@ use std::collections::HashMap;
|
|||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::live::{GradientMesh, MeshQuad};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
use vectorcraft_geom::{BezPath, Vec2};
|
||||
use vello_cpu::RenderContext;
|
||||
use vello_cpu::peniko;
|
||||
|
|
@ -24,7 +26,7 @@ type MeshEntry = (Arc<Node>, Arc<Vec<MeshQuad>>, u64);
|
|||
/// Per-renderer cache of evaluated live objects.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct LiveCache {
|
||||
expanded: HashMap<usize, (Arc<Node>, Expanded, u64)>,
|
||||
expanded: HashMap<usize, (Arc<Node>, Expanded, u64, u64)>,
|
||||
meshes: HashMap<(usize, usize), MeshEntry>,
|
||||
stamp: u64,
|
||||
}
|
||||
|
|
@ -42,8 +44,34 @@ impl LiveCache {
|
|||
}
|
||||
|
||||
/// One level of evaluation of a live node (blend steps, envelope result with type outlined run by
|
||||
/// run, mesh → flat pieces); the exporters evaluate them alike. Non-live nodes return themselves.
|
||||
pub use crate::effects::expand_live;
|
||||
/// run, mesh → flat pieces) without a document (symbol instances and pattern tiles in envelopes
|
||||
/// stay as they are). Non-live nodes return themselves.
|
||||
pub fn expand_live(n: &Node) -> Vec<Node> {
|
||||
crate::effects::expand_live(None, n)
|
||||
}
|
||||
|
||||
/// What an evaluated live node depends on besides itself: the art of the symbols and the patterns
|
||||
/// it draws (their `Arc`s outlive document copies, so this changes only when one is edited).
|
||||
fn resources_key(doc: &Document, n: &Node) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
n.walk(&mut |c| {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &c.kind
|
||||
&& let Some(s) = doc.symbols.iter().find(|s| s.name == *symbol)
|
||||
{
|
||||
(Arc::as_ptr(&s.art) as usize).hash(&mut h);
|
||||
}
|
||||
for i in &c.appearance.items {
|
||||
if let vectorcraft_color::Paint::Pattern { pattern, .. } = i.paint()
|
||||
&& let Some(def) = doc.pattern(pattern)
|
||||
{
|
||||
def.art.iter().for_each(|a| (Arc::as_ptr(a) as usize).hash(&mut h));
|
||||
[def.tile.x0, def.tile.y0, def.tile.x1, def.tile.y1].iter().for_each(|v| v.to_bits().hash(&mut h));
|
||||
}
|
||||
}
|
||||
});
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Subdivisions per patch for a patch about `size_px` device pixels across.
|
||||
fn mesh_level(m: &GradientMesh, bounds_px: f64) -> usize {
|
||||
|
|
@ -52,21 +80,23 @@ fn mesh_level(m: &GradientMesh, bounds_px: f64) -> usize {
|
|||
}
|
||||
|
||||
impl Renderer {
|
||||
fn live_expanded(&mut self, a: &Arc<Node>, cache: bool) -> Expanded {
|
||||
fn live_expanded(&mut self, doc: &Document, a: &Arc<Node>, cache: bool) -> Expanded {
|
||||
let key = Arc::as_ptr(a) as usize;
|
||||
let resources = resources_key(doc, a);
|
||||
if cache
|
||||
&& let Some(e) = self.live.expanded.get_mut(&key)
|
||||
&& Arc::ptr_eq(&e.0, a)
|
||||
&& e.3 == resources
|
||||
{
|
||||
e.2 = self.live.stamp;
|
||||
return e.1.clone();
|
||||
}
|
||||
let v: Expanded = match crate::effects::pathfinder_children(a, crate::effects::text_outliner()) {
|
||||
Some(children) => Arc::new(children),
|
||||
None => Arc::new(expand_live(a).into_iter().map(Arc::new).collect()),
|
||||
None => Arc::new(crate::effects::expand_live(Some(doc), a).into_iter().map(Arc::new).collect()),
|
||||
};
|
||||
if cache {
|
||||
self.live.expanded.insert(key, (a.clone(), v.clone(), self.live.stamp));
|
||||
self.live.expanded.insert(key, (a.clone(), v.clone(), self.live.stamp, resources));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
|
@ -96,7 +126,7 @@ impl Renderer {
|
|||
let knockout = !f.opts.outline
|
||||
&& matches!(a.kind, NodeKind::Blend { .. })
|
||||
&& a.knockout.resolve(self.knockout)
|
||||
&& vectorcraft_doc::live::steps_knockout_shows(&self.live_expanded(a, true));
|
||||
&& vectorcraft_doc::live::steps_knockout_shows(&self.live_expanded(f.doc, a, true));
|
||||
let enclosing = std::mem::replace(&mut self.knockout, knockout);
|
||||
if !f.opts.outline && (opacity < 1.0 || a.blend != vectorcraft_color::BlendMode::Normal || a.isolate || knockout) {
|
||||
let blends = self.blends_through(a);
|
||||
|
|
@ -115,7 +145,7 @@ impl Renderer {
|
|||
match &a.kind {
|
||||
NodeKind::Mesh(m) => self.draw_mesh(ctx, f, a, m, cache),
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => {
|
||||
let items = self.live_expanded(a, cache);
|
||||
let items = self.live_expanded(f.doc, a, cache);
|
||||
// The flag holds while this blend is drawn (see `draw_live`).
|
||||
if self.knockout && matches!(a.kind, NodeKind::Blend { .. }) {
|
||||
self.draw_knockout(ctx, f, &items);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ fn write(doc: &Document, opts: &ExportOptions, native: Option<&[u8]>, id_prefix:
|
|||
symbols: HashMap::new(),
|
||||
symbol_nest: 0,
|
||||
fonts: Vec::new(),
|
||||
shared_images: HashMap::new(),
|
||||
};
|
||||
w.assign_name_ids();
|
||||
// Page Isolated Blending / Page Knockout Group: the page content is one isolated group.
|
||||
|
|
@ -247,6 +248,9 @@ struct Writer<'a> {
|
|||
symbol_nest: u32,
|
||||
/// The characters type uses from each face ([`ExportOptions::embed_fonts`]).
|
||||
fonts: Vec<FontUse>,
|
||||
/// Images written once in the defs and drawn with `<use>`, by key and size: the pieces an
|
||||
/// envelope cuts a distorted image into share its pixels.
|
||||
shared_images: HashMap<(String, u32, u32), String>,
|
||||
}
|
||||
|
||||
/// The characters type uses from one face, under one `@font-face` description: the family the
|
||||
|
|
@ -433,6 +437,58 @@ impl Writer<'_> {
|
|||
self.defs.push('\n');
|
||||
}
|
||||
}
|
||||
/// The `href` of image `im`: its file with [`ImageMode::Link`] (or when its pixels are
|
||||
/// missing), else its bytes. `None` when it has neither.
|
||||
fn image_href(&mut self, im: &vectorcraft_doc::ImageObject) -> Option<String> {
|
||||
let (doc, link) = (self.doc, self.opts.images == ImageMode::Link);
|
||||
match (im.link.as_ref().filter(|_| link), doc.images.get(&im.key)) {
|
||||
(Some(l), _) => Some(l.path.clone()),
|
||||
(None, Some(b)) if !b.bytes.is_empty() => Some(self.blob_href(b)),
|
||||
_ => im.link.as_ref().map(|l| l.path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Images drawn more than once in `art` (the pieces of a distorted image) go into the defs
|
||||
/// once; each piece then draws them with `<use>`.
|
||||
fn share_images(&mut self, art: &Node) {
|
||||
let mut seen: HashMap<(String, u32, u32), usize> = HashMap::new();
|
||||
art.walk(&mut |c| {
|
||||
if let NodeKind::Image(im) = &c.kind {
|
||||
*seen.entry((im.key.clone(), im.width, im.height)).or_default() += 1;
|
||||
}
|
||||
});
|
||||
let mut repeated: Vec<_> = seen.into_iter().filter(|(k, n)| *n > 1 && !self.shared_images.contains_key(k)).map(|(k, _)| k).collect();
|
||||
repeated.sort();
|
||||
for (key, width, height) in repeated {
|
||||
let link = self.linked_of(art, &key);
|
||||
let im = vectorcraft_doc::ImageObject { key: key.clone(), width, height, xf: Affine::IDENTITY, link, placement: Default::default() };
|
||||
let Some(href) = self.image_href(&im) else { continue };
|
||||
let id = self.fresh_id("image");
|
||||
self.def(
|
||||
0,
|
||||
&format!(
|
||||
"<image id=\"{id}\" width=\"{width}\" height=\"{height}\" preserveAspectRatio=\"none\" xlink:href=\"{}\"/>",
|
||||
xml_escape(&href)
|
||||
),
|
||||
);
|
||||
self.shared_images.insert((key, width, height), id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The link of the first image with blob `key` in `art`.
|
||||
fn linked_of(&self, art: &Node, key: &str) -> Option<vectorcraft_doc::LinkInfo> {
|
||||
let mut link = None;
|
||||
art.walk(&mut |c| {
|
||||
if let NodeKind::Image(im) = &c.kind
|
||||
&& im.key == key
|
||||
&& link.is_none()
|
||||
{
|
||||
link = im.link.clone();
|
||||
}
|
||||
});
|
||||
link
|
||||
}
|
||||
|
||||
fn fresh_id(&mut self, prefix: &str) -> String {
|
||||
let mut i = 1;
|
||||
loop {
|
||||
|
|
@ -1233,18 +1289,15 @@ impl Writer<'_> {
|
|||
}
|
||||
NodeKind::Text(t) => self.text_node(n, t),
|
||||
NodeKind::Image(im) => {
|
||||
let (doc, link) = (self.doc, self.opts.images == ImageMode::Link);
|
||||
let href = match (im.link.as_ref().filter(|_| link), doc.images.get(&im.key)) {
|
||||
(Some(l), _) => l.path.clone(),
|
||||
(None, Some(b)) if !b.bytes.is_empty() => self.blob_href(b),
|
||||
_ => match &im.link {
|
||||
Some(l) => l.path.clone(),
|
||||
None => return,
|
||||
},
|
||||
};
|
||||
let id = self.id_attr(n);
|
||||
let m = self.matrix(self.xf * im.xf);
|
||||
let a = self.attrs(&css::transparency(n));
|
||||
if let Some(shared) = self.shared_images.get(&(im.key.clone(), im.width, im.height)) {
|
||||
let shared = shared.clone();
|
||||
self.line(&format!("<use{id} transform=\"{m}\"{a} xlink:href=\"#{shared}\"/>"));
|
||||
return;
|
||||
}
|
||||
let Some(href) = self.image_href(im) else { return };
|
||||
self.line(&format!(
|
||||
"<image{id} width=\"{}\" height=\"{}\" transform=\"{m}\" preserveAspectRatio=\"none\"{a} xlink:href=\"{}\"/>",
|
||||
im.width,
|
||||
|
|
@ -1290,7 +1343,8 @@ impl Writer<'_> {
|
|||
}
|
||||
// Live blends/envelopes/meshes export their evaluated (expanded) form.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => {
|
||||
let g = vectorcraft_effects::expand_live_deep(n);
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
self.share_images(&g);
|
||||
self.node_body(&g);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ mod width;
|
|||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
use vectorcraft_geom::Point;
|
||||
|
||||
use crate::Tool;
|
||||
|
||||
|
|
@ -79,14 +79,7 @@ pub fn warp_node_with(n: &mut Node, f: &dyn Fn(Point) -> Point) {
|
|||
n.appearance.warp_gradients(&|p| affine_near(f, p, eps));
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
pub use vectorcraft_doc::live::affine_near;
|
||||
|
||||
/// Every anchor and handle position in `n` (for meshes and bounds).
|
||||
pub fn collect_points(n: &Node, out: &mut Vec<Point>) {
|
||||
|
|
@ -127,7 +120,7 @@ mod tests {
|
|||
use super::*;
|
||||
use vectorcraft_color::{Gradient, GradientPaint, Paint};
|
||||
use vectorcraft_doc::Appearance;
|
||||
use vectorcraft_geom::{PathData, Rect, shapes};
|
||||
use vectorcraft_geom::{Affine, PathData, Rect, shapes};
|
||||
|
||||
#[test]
|
||||
fn create_covers_all_distort_tools() {
|
||||
|
|
|
|||
|
|
@ -1829,6 +1829,12 @@ envelopes, the path for a top-object envelope); the content keeps the envelope's
|
|||
opacity mask.
|
||||
Type inside an envelope distorts as its glyph outlines, run by run in each run's paint, on the canvas and in every
|
||||
export (SVG, PDF, EPS, EMF/WMF, DXF).
|
||||
Everything inside bends with the envelope: images as a raster mesh warp (cut into pieces by Fidelity, each a clipped
|
||||
image under its own affine map; SVG shares the pixels between the pieces), symbol instances as their symbol's art,
|
||||
and with Distort Appearance strokes (as their filled outlines) and geometry effects; with it, Distort Linear Gradients
|
||||
bends linear gradients and Distort Pattern Fills the pattern tiles. A transformed envelope keeps its warp in its own
|
||||
frame (`kind.frame` in `document.inspect`; rotating a warp envelope turns the warp with it), and move and scale keep
|
||||
it square to the page.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"object.envelope.makeWithWarp","params":{"style":"arch","bend":40}}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue