From f5f430a56e6ec96a61c03a97311aa681a4a8849b Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 19:48:51 +0000 Subject: [PATCH] refactor(doc): move live.rs items to live/{blend_spec,envelope,expand,mesh,warp}.rs --- crates/doc/src/live.rs | 1946 +---------------------------- crates/doc/src/live/blend_spec.rs | 157 +++ crates/doc/src/live/envelope.rs | 332 +++++ crates/doc/src/live/expand.rs | 444 +++++++ crates/doc/src/live/mesh.rs | 761 +++++++++++ crates/doc/src/live/warp.rs | 253 ++++ 6 files changed, 1969 insertions(+), 1924 deletions(-) create mode 100644 crates/doc/src/live/blend_spec.rs create mode 100644 crates/doc/src/live/envelope.rs create mode 100644 crates/doc/src/live/expand.rs create mode 100644 crates/doc/src/live/mesh.rs create mode 100644 crates/doc/src/live/warp.rs diff --git a/crates/doc/src/live.rs b/crates/doc/src/live.rs index f84d0f865..650361374 100644 --- a/crates/doc/src/live.rs +++ b/crates/doc/src/live.rs @@ -34,299 +34,6 @@ fn fifty() -> f64 { 50.0 } -// ===================================================================================== -// Data -// ===================================================================================== - -/// Blend Options → Spacing. -#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum BlendSpacing { - /// Step count chosen from the colour difference (or the distance when colours match). - #[default] - SmoothColor, - /// Specified number of steps between each pair of key objects (1–1000). - Steps(u32), - /// Specified distance (pt) between the steps. - Distance(f64), -} - -/// Blend Options → Orientation. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum BlendOrientation { - #[default] - AlignToPage, - AlignToPath, -} - -/// Distribution of blend steps, as offered by the Blend panel's Type menu. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum BlendEaseType { - #[default] - In, - Out, - InOut, - OutIn, -} - -impl BlendEaseType { - pub fn id(self) -> &'static str { - match self { - Self::In => "easeIn", - Self::Out => "easeOut", - Self::InOut => "easeInOut", - Self::OutIn => "easeOutIn", - } - } -} - -/// Blend panel Type and Ramp. Ramp is a fraction from 0 (even) to 1. -#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct BlendEase { - #[serde(default)] - pub kind: BlendEaseType, - #[serde(default, deserialize_with = "blend_ramp")] - pub ramp: f64, -} - -impl BlendEase { - pub fn is_linear(&self) -> bool { - self.ramp <= 0.0 - } - - /// Only the default Type and Ramp can be omitted without losing a chosen Type at Ramp 0. - pub fn is_default(&self) -> bool { - self.kind == BlendEaseType::In && self.ramp == 0.0 - } - - /// A monotone polynomial distribution, blended with even spacing by Ramp. Public help - /// describes the four distributions, not their numeric curves; these curves are original. - pub fn apply(&self, u: f64) -> f64 { - let u = if u.is_finite() { u.clamp(0.0, 1.0) } else { 0.0 }; - let r = if self.ramp.is_finite() { self.ramp.clamp(0.0, 1.0) } else { 0.0 }; - let eased = match self.kind { - BlendEaseType::In => u * u, - BlendEaseType::Out => u * (2.0 - u), - BlendEaseType::InOut => u * u * (3.0 - 2.0 * u), - BlendEaseType::OutIn if u < 0.5 => 2.0 * u * (1.0 - u), - BlendEaseType::OutIn => 0.5 + 2.0 * (u - 0.5) * (u - 0.5), - }; - u + r * (eased - u) - } -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct BlendSpec { - #[serde(default)] - pub spacing: BlendSpacing, - #[serde(default)] - pub orientation: BlendOrientation, - /// Acceleration of the steps' shapes and positions along the spine between two keys. - #[serde(default, skip_serializing_if = "BlendEase::is_default")] - pub ease: BlendEase, - /// Independent color acceleration. -100..100, with 0 preserving the even color ramp. - #[serde(default, rename = "colorShift", skip_serializing_if = "is_zero", deserialize_with = "blend_color_shift")] - pub color_shift: f64, - /// Replaced spine. `None` = the straight lines between the key objects' centres. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub spine: Option, - /// The anchor of the spine's first subpath each key object sits on (a spine edited on the - /// canvas: moving a key moves its anchor and the other way round). Empty: the keys spread - /// evenly along the spine by arc length (Replace Spine). - #[serde(default, rename = "keyAnchors", skip_serializing_if = "Vec::is_empty")] - pub key_anchors: Vec, - /// Per key object, the anchor of its first subpath the blend starts from (Blend tool clicks - /// on anchor points); `None`: chosen so closed shapes don't twist. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub starts: Vec>, -} - -impl BlendSpec { - /// The start anchor of key `i` (see [`Self::starts`]). - pub fn start(&self, i: usize) -> Option { - self.starts.get(i).copied().flatten().map(|a| a as usize) - } -} - -/// Blend Options set with no blend selected: what new blends start with. -#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct BlendDefaults { - #[serde(default)] - pub spacing: BlendSpacing, - #[serde(default)] - pub orientation: BlendOrientation, -} - -/// How an envelope distorts its content. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum EnvelopeKind { - /// One of the 15 warp styles (`arc`, `arcLower`, … — see [`WarpStyle::from_id`]); bend and - /// distortions in percent (-100..100). - Warp { - style: String, - #[serde(default)] - bend: f64, - #[serde(default)] - h: f64, - #[serde(default)] - v: f64, - #[serde(default = "yes")] - horizontal: bool, - }, - /// A `(rows+1)×(cols+1)` grid of points (row-major) the content's bounding box maps onto. - Mesh { - rows: u32, - cols: u32, - points: Vec, - /// Each point's bezier handles (offsets, see [`H_RIGHT`] …), row-major like `points`. - /// Empty: the smooth (Catmull-Rom) mesh through the points, as before handles existed; - /// editing the mesh's points or handles on the canvas fills them in. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - handles: Vec<[Vec2; 4]>, - }, - /// The content's bounding box maps onto the outline of this path. - TopObject { path: PathData }, -} - -/// Envelope Options → Preserve Shape Using: how a distorted raster keeps the envelope's shape. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum PreserveShape { - #[default] - ClippingMask, - Transparency, -} - -impl PreserveShape { - pub fn id(self) -> &'static str { - match self { - Self::ClippingMask => "clippingMask", - Self::Transparency => "transparency", - } - } - pub fn parse(s: &str) -> Option { - match s { - "clippingMask" => Some(Self::ClippingMask), - "transparency" => Some(Self::Transparency), - _ => None, - } - } -} - -/// Envelope Options besides Fidelity. `Default` is what envelopes saved before these options -/// existed do (their appearance applies after the distortion); new envelopes start from -/// [`EnvelopeOptions::NEW`], as in the reference app. -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", default)] -pub struct EnvelopeOptions { - /// Rasters: Anti-Alias. - pub anti_alias: bool, - /// Rasters: Preserve Shape Using. - pub preserve_shape: PreserveShape, - /// Distort Appearance: strokes and live effects are applied before the distortion, so they - /// bend with the art (off: they apply to the distorted paths). - pub distort_appearance: bool, - /// Distort Linear Gradients (with Distort Appearance): placed gradients bend with the art. - pub distort_linear_gradients: bool, - /// Distort Pattern Fills (with Distort Appearance): pattern tiles bend with the art. - pub distort_pattern_fills: bool, -} - -impl Default for EnvelopeOptions { - fn default() -> Self { - Self { - anti_alias: true, - preserve_shape: PreserveShape::ClippingMask, - distort_appearance: false, - distort_linear_gradients: false, - distort_pattern_fills: false, - } - } -} - -impl EnvelopeOptions { - /// The options a new envelope gets (the reference app's defaults). - pub const NEW: Self = Self { - anti_alias: true, - preserve_shape: PreserveShape::ClippingMask, - distort_appearance: true, - distort_linear_gradients: false, - distort_pattern_fills: false, - }; - /// Do gradients bend with the art? - pub fn gradients(&self) -> bool { - self.distort_appearance && self.distort_linear_gradients - } - /// Do pattern tiles bend with the art? - pub fn patterns(&self) -> bool { - self.distort_appearance && self.distort_pattern_fills - } -} - -/// Handle slots of a [`MeshPoint`]: towards the next column, previous column, next row, previous row. -pub const H_RIGHT: usize = 0; -pub const H_LEFT: usize = 1; -pub const H_DOWN: usize = 2; -pub const H_UP: usize = 3; - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct MeshPoint { - pub p: Point, - pub color: Color, - #[serde(default = "one")] - pub opacity: f32, - /// Tangent handles as offsets from `p` (see [`H_RIGHT`] …). - #[serde(default)] - pub handles: [Vec2; 4], -} - -/// A gradient mesh: `(rows+1)×(cols+1)` points, row-major. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct GradientMesh { - pub rows: u32, - pub cols: u32, - pub points: Vec, -} - -/// One tessellated piece of a mesh: a small quad, its colour at the centre, and its colour and -/// opacity at each corner (in `pts` order), which a renderer shades between so neighbouring quads -/// meet without a step. -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct MeshQuad { - pub pts: [Point; 4], - pub color: Color, - pub opacity: f32, - pub corners: [(Color, f32); 4], -} - -/// Create Gradient Mesh → Appearance. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum MeshAppearance { - #[default] - Flat, - ToCenter, - ToEdge, -} - -impl MeshAppearance { - pub fn parse(s: &str) -> Option { - match s.to_ascii_lowercase().as_str() { - "flat" => Some(Self::Flat), - "center" | "tocenter" | "to center" => Some(Self::ToCenter), - "edge" | "toedge" | "to edge" => Some(Self::ToEdge), - _ => None, - } - } -} - -/// Default envelope fidelity. -pub fn default_fidelity() -> f64 { - fifty() -} - // ===================================================================================== // Blends (evaluation in `crate::blend`) // ===================================================================================== @@ -336,1637 +43,28 @@ pub use crate::blend::{ steps_knockout_shows, }; -// ===================================================================================== -// Warp maps (shared with vectorcraft-effects' Warp effects) -// ===================================================================================== - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum WarpStyle { - Arc, - ArcLower, - ArcUpper, - Arch, - Bulge, - ShellLower, - ShellUpper, - Flag, - Wave, - Fish, - Rise, - Fisheye, - Inflate, - Squeeze, - Twist, -} - -impl WarpStyle { - /// From the id suffix (`arc`, `arcLower`, …). - pub fn from_id(s: &str) -> Option { - use WarpStyle::*; - Some(match s { - "arc" => Arc, - "arcLower" => ArcLower, - "arcUpper" => ArcUpper, - "arch" => Arch, - "bulge" => Bulge, - "shellLower" => ShellLower, - "shellUpper" => ShellUpper, - "flag" => Flag, - "wave" => Wave, - "fish" => Fish, - "rise" => Rise, - "fisheye" => Fisheye, - "inflate" => Inflate, - "squeeze" => Squeeze, - "twist" => Twist, - _ => return None, - }) - } -} - -/// The warp map on normalised coordinates: `b` = bend (-1..1), `dh`/`dv` = distortion (-1..1); -/// `aspect` is the box's half-height over its half-width along the warp (Arc bends in real -/// proportions). -pub fn warp_point(style: WarpStyle, b: f64, dh: f64, dv: f64, aspect: f64, x: f64, y: f64) -> (f64, f64) { - use WarpStyle::*; - let t = (y + 1.0) / 2.0; // 0 at top, 1 at bottom - let par = 1.0 - x * x; // parabola: 1 at centre, 0 at sides - let (mut x2, mut y2) = match style { - Arc => { - if b.abs() < 1e-6 { - (x, y) - } else { - // Bend the centre line into a circular arc of equal length (sweep = |b|·180°), in - // the box's real proportions (lengths in half-widths, so y counts `aspect` each): - // the edges are concentric arcs, and one that would pass the centre stops there - // instead of folding over (#1578). - let k = if aspect.is_finite() && aspect > 0.0 { aspect } else { 1.0 }; - let sweep = b.abs() * PI; - let r0 = 2.0 / sweep; - let yy = if b > 0.0 { y } else { -y } * k; - let a = x / r0; - let r = (r0 - yy).max(0.0); - let (s, c) = a.sin_cos(); - let (nx, ny) = (r * s, (r0 - r * c) / k); - (nx, if b > 0.0 { ny } else { -ny }) - } - } - ArcLower => (x, y + b * par * t), - ArcUpper => (x, y - b * par * (1.0 - t)), - Arch => (x, y - b * par), - Bulge => (x, y + b * par * y), - ShellLower => (x * (1.0 - 0.5 * b * (1.0 - t)), y + b * par * t), - ShellUpper => (x * (1.0 - 0.5 * b * t), y - b * par * (1.0 - t)), - Flag => (x, y + 0.5 * b * (PI * x).sin()), - Wave => (x + 0.1 * b * (PI * y).sin(), y + 0.3 * b * (2.0 * PI * x).sin() * (0.5 + 0.5 * t)), - Fish => (x, y * (1.0 + 0.5 * b * (0.75 * PI * (x + 1.0)).sin())), - Rise => (x, y - b * ((x + 1.0) / 2.0).powi(2) * 2.0 + b), - Fisheye => { - let r2 = x * x + y * y; - let s = if r2 < 1.0 { 1.0 + 0.5 * b * (1.0 - r2) } else { 1.0 }; - (x * s, y * s) - } - Inflate => (x * (1.0 + 0.5 * b * (1.0 - y * y)), y * (1.0 + 0.5 * b * par)), - Squeeze => (x * (1.0 - 0.5 * b * (1.0 - y * y)), y * (1.0 + 0.3 * b * par)), - Twist => { - let r = (x * x + y * y).sqrt(); - let a = -b * PI * 0.5 * (1.0 - r / std::f64::consts::SQRT_2).max(0.0); - let (s, c) = a.sin_cos(); - (x * c - y * s, x * s + y * c) - } - }; - // Perspective-like distortion: horizontal narrows one side, vertical one end. - if dh != 0.0 { - y2 *= (1.0 + dh * x2 * 0.5).max(0.0); - } - if dv != 0.0 { - x2 *= (1.0 + dv * y2 * 0.5).max(0.0); - } - (x2, y2) -} - -/// Segment as a cubic; straight lines get handles at 1/3 and 2/3 so a non-linear map bends them. -fn seg_cubic(sp: &SubPath, i: usize) -> CubicBez { - let c = sp.segment(i); - if sp.segment_is_line(i) { CubicBez::new(c.p0, c.p0.lerp(c.p3, 1.0 / 3.0), c.p0.lerp(c.p3, 2.0 / 3.0), c.p3) } else { c } -} - -fn poly_len(c: &CubicBez) -> f64 { - c.p0.distance(c.p1) + c.p1.distance(c.p2) + c.p2.distance(c.p3) -} - -/// Running length along a polyline: `0, |p0p1|, |p0p1| + |p1p2|, …` (one entry per point). -fn cumulative_lengths(pts: &[Point]) -> Vec { - let mut total = 0.0; - let mut cum = Vec::with_capacity(pts.len()); - cum.push(0.0); - for w in pts.windows(2) { - total += w[0].distance(w[1]); - cum.push(total); - } - cum -} - -/// Most pieces [`map_nonlinear`] splits one segment into. -const MAX_PIECES: usize = 64; - -/// How many pieces of about `piece` a segment of length `len` is split into: one to `most`. -pub fn pieces(len: f64, piece: f64, most: usize) -> usize { - ((len / piece).ceil() as usize).clamp(1, most.max(1)) -} - -/// The piece length to split segments of lengths `lens` with ([`pieces`], at most `most` each) so -/// that the pieces past each segment's first number at most `room`: `piece` when they already do, -/// longer pieces otherwise. A segment of `n` pieces adds `n - 1 ≤ len / piece` points to a path, -/// so pieces of the total length over `room` fit; with no room every segment stays one piece. -pub fn fit_piece(lens: &[f64], piece: f64, most: usize, room: usize) -> f64 { - let added = lens.iter().map(|&len| pieces(len, piece, most) - 1).fold(0, usize::saturating_add); - if added <= room { - return piece; - } - if room == 0 { - return f64::INFINITY; - } - // A NaN length is always one piece. - piece.max(lens.iter().filter(|len| !len.is_nan()).sum::() / room as f64) -} - -/// Map every point of `path` through the non-linear function `f`. Segments are split into pieces -/// no longer than about `max_piece` (up to 64 per segment) and their control points mapped, which -/// approximates the image curve to O(h²). Closedness and subpath structure are preserved. -pub fn map_nonlinear(path: &PathData, max_piece: f64, f: impl Fn(Point) -> Point) -> PathData { - map_nonlinear_within(path, max_piece, usize::MAX, f) -} - -/// [`map_nonlinear`] whose result has at most `max` anchors, or `path`'s count when that is more: -/// where pieces of `max_piece` would make more, the pieces grow until they don't ([`fit_piece`]), -/// a coarser approximation of the image curve. Geometry effects map within their anchor budget. -pub fn map_nonlinear_within(path: &PathData, max_piece: f64, max: usize, f: impl Fn(Point) -> Point) -> PathData { - let mut max_piece = max_piece.max(1e-3); - // Each segment adds one anchor per piece past its first. - let room = max.saturating_sub(path.anchor_count()); - let segs = path.subpaths.iter().map(SubPath::segment_count).fold(0, usize::saturating_add); - if segs.saturating_mul(MAX_PIECES - 1) > room { - let lens: Vec = path.subpaths.iter().flat_map(|sp| (0..sp.segment_count()).map(|i| poly_len(&seg_cubic(sp, i)))).collect(); - max_piece = fit_piece(&lens, max_piece, MAX_PIECES, room); - } - let mut subs = Vec::with_capacity(path.subpaths.len()); - for sp in &path.subpaths { - let n = sp.anchors.len(); - if n == 0 { - continue; - } - let segs = sp.segment_count(); - let mut res: Vec = Vec::new(); - let mut pending_in: Option = None; - for i in 0..n { - let a = &sp.anchors[i]; - let h_in = pending_in.take().unwrap_or_else(|| f(a.h_in)); - res.push(Anchor { p: f(a.p), h_in, h_out: f(a.h_out), kind: a.kind }); - if i < segs { - let c = seg_cubic(sp, i); - let k = pieces(poly_len(&c), max_piece, MAX_PIECES); - for j in 0..k { - let sub = c.subsegment((j as f64 / k as f64)..((j + 1) as f64 / k as f64)); - if let Some(last) = res.last_mut() { - last.h_out = f(sub.p1); - } - if j + 1 < k { - res.push(Anchor { p: f(sub.p3), h_in: f(sub.p2), h_out: f(sub.p3), kind: Default::default() }); - } else { - pending_in = Some(f(sub.p2)); - } - } - } - } - if let Some(h) = pending_in - && sp.closed - && let Some(first) = res.first_mut() - { - first.h_in = h; - } - let anchors = res.into_iter().map(|a| Anchor::with_handles(a.p, a.h_in, a.h_out)).collect(); - subs.push(SubPath::new(anchors, sp.closed)); - } - PathData::new(subs) -} - -// ===================================================================================== -// Coons patches from outlines -// ===================================================================================== - -fn flatten_subpath(sp: &SubPath, tol: f64) -> Vec { - let mut bp = BezPath::new(); - sp.to_bezpath_into(&mut bp); - let mut pts: Vec = Vec::new(); - vectorcraft_geom::kurbo::flatten(bp.iter(), tol, |el| match el { - vectorcraft_geom::PathEl::MoveTo(p) | vectorcraft_geom::PathEl::LineTo(p) if pts.last().is_none_or(|q| q.distance(p) > 1e-9) => pts.push(p), - _ => {} - }); - pts -} - -fn resample(poly: &[Point], m: usize) -> Vec { - if poly.is_empty() { - return vec![Point::ZERO; m]; - } - if poly.len() == 1 { - return vec![poly[0]; m]; - } - let cum = cumulative_lengths(poly); - let total = cum.last().copied().unwrap_or(0.0); - let mut out = Vec::with_capacity(m); - let mut i = 0; - for k in 0..m { - let s = if m > 1 { total * k as f64 / (m - 1) as f64 } else { 0.0 }; - while i + 2 < cum.len() && cum[i + 1] < s { - i += 1; - } - let seg = (cum[i + 1] - cum[i]).max(1e-12); - out.push(poly[i].lerp(poly[i + 1], ((s - cum[i]) / seg).clamp(0.0, 1.0))); - } - out -} - -const SIDE: usize = 65; - -/// Four boundary curves (top TL→TR, right TR→BR, bottom BL→BR, left TL→BL), each resampled to -/// [`SIDE`] points, from the outline of a closed path split at the points nearest its bounding -/// box corners. -#[derive(Clone, Debug)] -pub struct Coons { - top: Vec, - right: Vec, - bottom: Vec, - left: Vec, -} - -impl Coons { - /// A rectangle. - pub fn rect(r: Rect) -> Self { - let line = |a: Point, b: Point| (0..SIDE).map(|i| a.lerp(b, i as f64 / (SIDE - 1) as f64)).collect::>(); - let (tl, tr, br, bl) = (Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)); - Self { top: line(tl, tr), right: line(tr, br), bottom: line(bl, br), left: line(tl, bl) } - } - - pub fn from_path(path: &PathData) -> Option { - // The largest subpath is the outline. - let sp = path.subpaths.iter().filter(|s| s.anchors.len() >= 2).max_by(|a, b| a.area().abs().total_cmp(&b.area().abs()))?; - let r = path.bounds()?; - let tol = (r.width() + r.height()).max(1e-6) / 2000.0; - let mut poly = flatten_subpath(sp, tol); - if poly.len() > 1 - && let (Some(a), Some(b)) = (poly.first(), poly.last()) - && a.distance(*b) < 1e-9 - { - poly.pop(); - } - if poly.len() < 3 { - return Some(Self::rect(r)); - } - // Orient clockwise in y-down (TL → TR → BR → BL). - let area: f64 = (0..poly.len()) - .map(|i| { - let (p, q) = (poly[i], poly[(i + 1) % poly.len()]); - p.x * q.y - q.x * p.y - }) - .sum(); - if area < 0.0 { - poly.reverse(); - } - let nearest = |c: Point| (0..poly.len()).min_by(|i, j| poly[*i].distance_squared(c).total_cmp(&poly[*j].distance_squared(c))).unwrap_or(0); - let tl = nearest(Point::new(r.x0, r.y0)); - let tr = nearest(Point::new(r.x1, r.y0)); - let br = nearest(Point::new(r.x1, r.y1)); - let bl = nearest(Point::new(r.x0, r.y1)); - let n = poly.len(); - let arc = |i: usize, j: usize| -> Vec { - let mut v = vec![poly[i]]; - let mut k = i; - while k != j { - k = (k + 1) % n; - v.push(poly[k]); - } - v - }; - let top = resample(&arc(tl, tr), SIDE); - let right = resample(&arc(tr, br), SIDE); - let mut bottom = resample(&arc(br, bl), SIDE); - bottom.reverse(); - let mut left = resample(&arc(bl, tl), SIDE); - left.reverse(); - 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 * (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) - } - - /// The surface point at (u, v) ∈ [0,1]². - pub fn eval(&self, u: f64, v: f64) -> Point { - let (t, b, l, r) = (Self::sample(&self.top, u), Self::sample(&self.bottom, u), Self::sample(&self.left, v), Self::sample(&self.right, v)); - let (p00, p10, p01, p11) = (self.top[0], self.top[SIDE - 1], self.bottom[0], self.bottom[SIDE - 1]); - let c = (1.0 - v) * t.to_vec2() + v * b.to_vec2() + (1.0 - u) * l.to_vec2() + u * r.to_vec2() - - ((1.0 - u) * (1.0 - v) * p00.to_vec2() + u * (1.0 - v) * p10.to_vec2() + (1.0 - u) * v * p01.to_vec2() + u * v * p11.to_vec2()); - c.to_point() - } - - /// The four corners TL, TR, BR, BL. - pub fn corners(&self) -> [Point; 4] { - [self.top[0], self.top[SIDE - 1], self.bottom[SIDE - 1], self.bottom[0]] - } -} - -// ===================================================================================== -// Gradient meshes -// ===================================================================================== - -/// `0, 1/n, …, 1` for `n` (clamped to 1..=200) even steps. -fn even_steps(n: u32) -> Vec { - let n = n.clamp(1, 200); - (0..=n).map(|i| i as f64 / n as f64).collect() -} - -/// The number of points of a `(rows+1)×(cols+1)` grid, `None` past `usize`: counts come from -/// files, and u32::MAX rows and columns (65,535 on 32-bit targets) would wrap to an empty grid. -fn grid_len(rows: u32, cols: u32) -> Option { - (rows as usize).checked_add(1)?.checked_mul((cols as usize).checked_add(1)?) -} - -impl GradientMesh { - pub fn idx(&self, r: usize, c: usize) -> usize { - r * (self.cols as usize + 1) + c - } - pub fn is_valid(&self) -> bool { - self.rows >= 1 && self.cols >= 1 && grid_len(self.rows, self.cols) == Some(self.points.len()) - } - - /// A 1×1 mesh from its four corners (bottom-left, top-left, top-right, bottom-right in u/v - /// order: row 0 is v = 0, columns follow u), each with its handles ([`H_RIGHT`] …). - fn patch(corners: [(Point, [Vec2; 4], Color); 4]) -> Self { - let point = |(p, handles, color): (Point, [Vec2; 4], Color)| MeshPoint { p, color, opacity: 1.0, handles }; - let [a, b, c, d] = corners; - Self { rows: 1, cols: 1, points: vec![point(a), point(d), point(b), point(c)] } - } - - /// A 1×1 mesh from a Coons patch's twelve boundary points (in the order PDF and PostScript - /// shadings list them: v runs cp0 → cp3 along cp1 and cp2, u runs cp0 → cp9 along cp11 and - /// cp10) and its corner colours (at cp0, cp3, cp6, cp9); `None` if a point isn't finite. - pub fn coons(cp: &[Point; 12], colors: [Color; 4]) -> Option { - if !cp.iter().all(|p| p.is_finite()) { - return None; - } - let h = |a: usize, b: usize| cp[b] - cp[a]; - let handles = |right: Vec2, left: Vec2, down: Vec2, up: Vec2| { - let mut h = [Vec2::ZERO; 4]; - (h[H_RIGHT], h[H_LEFT], h[H_DOWN], h[H_UP]) = (right, left, down, up); - h - }; - let [c0, c1, c2, c3] = colors; - Some(Self::patch([ - (cp[0], handles(h(0, 11), Vec2::ZERO, h(0, 1), Vec2::ZERO), c0), - (cp[3], handles(h(3, 4), Vec2::ZERO, Vec2::ZERO, h(3, 2)), c1), - (cp[6], handles(Vec2::ZERO, h(6, 5), Vec2::ZERO, h(6, 7)), c2), - (cp[9], handles(Vec2::ZERO, h(9, 10), h(9, 8), Vec2::ZERO), c3), - ])) - } - - /// A 1×1 mesh of a Gouraud-shaded triangle (a patch with two corners together); `None` if a - /// point isn't finite. - pub fn triangle(corners: [(Point, Color); 3]) -> Option { - if !corners.iter().all(|(p, _)| p.is_finite()) { - return None; - } - let [a, b, c] = corners.map(|(p, color)| (p, [Vec2::ZERO; 4], color)); - Some(Self::patch([a, b, c, c])) - } - - /// A mesh over surface `s` (u, v ∈ [0,1]) with handles from its partial derivatives. - pub fn from_surface(rows: u32, cols: u32, s: &dyn Fn(f64, f64) -> Point, color: &dyn Fn(f64, f64) -> Color) -> Self { - let (us, vs) = (even_steps(cols), even_steps(rows)); - Self::from_grid(&us, &vs, s, &|r, c, _| (color(us[c], vs[r]), 1.0)) - } - - /// A mesh over surface `s` (u, v ∈ [0,1]) whose column lines sit at `us` and row lines at `vs` - /// (ascending, at least two each; equal neighbours make a patch of no width, a sharp colour - /// change), with handles from its partial derivatives. `color(row, col, point)` gives each - /// point its colour and opacity. - pub fn from_grid(us: &[f64], vs: &[f64], s: &dyn Fn(f64, f64) -> Point, color: &dyn Fn(usize, usize, Point) -> (Color, f32)) -> Self { - let (rows, cols) = (vs.len().saturating_sub(1), us.len().saturating_sub(1)); - let eps = 1e-3; - let mut points = Vec::with_capacity(us.len() * vs.len()); - for (r, &v) in vs.iter().enumerate() { - for (c, &u) in us.iter().enumerate() { - let p = s(u, v); - let d_u = (s((u + eps).min(1.0), v) - s((u - eps).max(0.0), v)) / ((u + eps).min(1.0) - (u - eps).max(0.0)); - let d_v = (s(u, (v + eps).min(1.0)) - s(u, (v - eps).max(0.0))) / ((v + eps).min(1.0) - (v - eps).max(0.0)); - // A third of the way to the neighbouring line along the tangent; outward handles on - // the border are unused, so they stay at the point. - let h = |d: Vec2, from: f64, to: Option<&f64>| to.map_or(Vec2::ZERO, |to| d * ((to - from) / 3.0)); - let handles = [ - h(d_u, u, us.get(c + 1)), - h(d_u, u, c.checked_sub(1).map(|i| &us[i])), - h(d_v, v, vs.get(r + 1)), - h(d_v, v, r.checked_sub(1).map(|i| &vs[i])), - ]; - let (color, opacity) = color(r, c, p); - points.push(MeshPoint { p, color, opacity, handles }); - } - } - Self { rows: rows as u32, cols: cols as u32, points } - } - - /// Create Gradient Mesh for a path: the outline split at its corners, coloured per `appearance`. - pub fn for_path(path: &PathData, rows: u32, cols: u32, base: Color, appearance: MeshAppearance, highlight: f64) -> Option { - let mut m = Self::for_path_with(path, rows, cols, &|_| (base, 1.0))?; - m.highlight(appearance, highlight); - Some(m) - } - - /// Create Gradient Mesh for a path: the outline split at its corners, each point taking the - /// colour and opacity `color_at` gives at its position (a gradient fill's, say). - pub fn for_path_with(path: &PathData, rows: u32, cols: u32, color_at: &dyn Fn(Point) -> (Color, f32)) -> Option { - let coons = Coons::from_path(path)?; - Some(Self::from_grid(&even_steps(cols), &even_steps(rows), &|u, v| coons.eval(u, v), &|_, _, p| color_at(p))) - } - - /// Create Gradient Mesh → Appearance: lighten the points towards white, to the centre or to - /// the edges, by at most `highlight` %. - pub fn highlight(&mut self, appearance: MeshAppearance, highlight: f64) { - let h = (highlight / 100.0).clamp(0.0, 1.0) as f32; - let (rows, cols) = (self.rows as usize, self.cols as usize); - for r in 0..=rows { - for c in 0..=cols { - let (u, v) = (c as f64 / cols as f64, r as f64 / rows as f64); - let d = ((u - 0.5).abs().max((v - 0.5).abs()) * 2.0) as f32; // 0 centre … 1 edge - let w = match appearance { - MeshAppearance::Flat => 0.0, - MeshAppearance::ToCenter => (1.0 - d) * h, - MeshAppearance::ToEdge => d * h, - }; - if w > 0.0 { - let i = self.idx(r, c); - self.points[i].color = lerp_color(&self.points[i].color, &Color::WHITE, w); - } - } - } - } - - /// Horizontal edge from (r, c) to (r, c+1). - pub fn edge_u(&self, r: usize, c: usize) -> CubicBez { - let (a, b) = (&self.points[self.idx(r, c)], &self.points[self.idx(r, c + 1)]); - CubicBez::new(a.p, a.p + a.handles[H_RIGHT], b.p + b.handles[H_LEFT], b.p) - } - /// Vertical edge from (r, c) to (r+1, c). - pub fn edge_v(&self, r: usize, c: usize) -> CubicBez { - let (a, b) = (&self.points[self.idx(r, c)], &self.points[self.idx(r + 1, c)]); - CubicBez::new(a.p, a.p + a.handles[H_DOWN], b.p + b.handles[H_UP], b.p) - } - - /// Coons patch point of patch (r, c) at local (u, v). - pub fn eval(&self, r: usize, c: usize, u: f64, v: f64) -> Point { - let (t, b) = (self.edge_u(r, c).eval(u), self.edge_u(r + 1, c).eval(u)); - let (l, rr) = (self.edge_v(r, c).eval(v), self.edge_v(r, c + 1).eval(v)); - let p00 = self.points[self.idx(r, c)].p.to_vec2(); - let p10 = self.points[self.idx(r, c + 1)].p.to_vec2(); - let p01 = self.points[self.idx(r + 1, c)].p.to_vec2(); - let p11 = self.points[self.idx(r + 1, c + 1)].p.to_vec2(); - ((1.0 - v) * t.to_vec2() + v * b.to_vec2() + (1.0 - u) * l.to_vec2() + u * rr.to_vec2() - - ((1.0 - u) * (1.0 - v) * p00 + u * (1.0 - v) * p10 + (1.0 - u) * v * p01 + u * v * p11)) - .to_point() - } - - /// Bilinear colour and opacity of patch (r, c) at local (u, v). Corners all in CMYK (or all - /// grey) mix in that space, as a PDF shading mixes in its own colour space: the inks come out - /// exact, and a CMYK document separates no colour to draw them. Other corners mix in RGB. - pub fn color_at(&self, r: usize, c: usize, u: f64, v: f64) -> (Color, f32) { - let q = - [&self.points[self.idx(r, c)], &self.points[self.idx(r, c + 1)], &self.points[self.idx(r + 1, c)], &self.points[self.idx(r + 1, c + 1)]]; - let w = [(1.0 - u) * (1.0 - v), u * (1.0 - v), (1.0 - u) * v, u * v]; - let op = q.iter().zip(w).map(|(p, w)| p.opacity * w as f32).sum(); - let cmyk: Option> = - q.iter().map(|p| if let Color::Cmyk { c, m, y, k } = p.color { Some([c, m, y, k]) } else { None }).collect(); - if let Some(inks) = cmyk { - let mut mix = [0.0f32; 4]; - for (ink, w) in inks.iter().zip(w) { - for (m, x) in mix.iter_mut().zip(ink) { - *m += x * w as f32; - } - } - let [c, m, y, k] = mix; - return (Color::Cmyk { c, m, y, k }, op); - } - let grey: Option> = q.iter().map(|p| if let Color::Gray { k } = p.color { Some(k) } else { None }).collect(); - if let Some(ks) = grey { - return (Color::Gray { k: ks.iter().zip(w).map(|(k, w)| k * w as f32).sum() }, op); - } - let mut rgb = [0.0f32; 3]; - for (p, w) in q.iter().zip(w) { - for (ch, x) in rgb.iter_mut().zip(p.color.to_rgb()) { - *ch += x * w as f32; - } - } - (Color::rgb(rgb[0], rgb[1], rgb[2]), op) - } - - /// Tessellate into `n`×`n` quads per patch, each coloured at its centre and its corners. - pub fn quads(&self, n: usize) -> Vec { - if !self.is_valid() { - return vec![]; - } - let n = n.clamp(1, 64); - let mut out = Vec::with_capacity(self.rows as usize * self.cols as usize * n * n); - let mut grid = vec![Point::ZERO; (n + 1) * (n + 1)]; - let mut colors = vec![(Color::default(), 0.0f32); (n + 1) * (n + 1)]; - for r in 0..self.rows as usize { - for c in 0..self.cols as usize { - for j in 0..=n { - for i in 0..=n { - let (u, v) = (i as f64 / n as f64, j as f64 / n as f64); - grid[j * (n + 1) + i] = self.eval(r, c, u, v); - colors[j * (n + 1) + i] = self.color_at(r, c, u, v); - } - } - for j in 0..n { - for i in 0..n { - let (color, opacity) = self.color_at(r, c, (i as f64 + 0.5) / n as f64, (j as f64 + 0.5) / n as f64); - let g = |a: usize, b: usize| grid[b * (n + 1) + a]; - let k = |a: usize, b: usize| colors[b * (n + 1) + a]; - out.push(MeshQuad { - pts: [g(i, j), g(i + 1, j), g(i + 1, j + 1), g(i, j + 1)], - color, - opacity, - corners: [k(i, j), k(i + 1, j), k(i + 1, j + 1), k(i, j + 1)], - }); - } - } - } - } - out - } - - /// The mesh's outer boundary as a closed path. - pub fn outline(&self) -> PathData { - if !self.is_valid() { - return PathData::default(); - } - let (rows, cols) = (self.rows as usize, self.cols as usize); - let mut segs: Vec = vec![]; - for c in 0..cols { - segs.push(self.edge_u(0, c)); - } - for r in 0..rows { - segs.push(self.edge_v(r, cols)); - } - for c in (0..cols).rev() { - let e = self.edge_u(rows, c); - segs.push(CubicBez::new(e.p3, e.p2, e.p1, e.p0)); - } - for r in (0..rows).rev() { - let e = self.edge_v(r, 0); - segs.push(CubicBez::new(e.p3, e.p2, e.p1, e.p0)); - } - let mut anchors: Vec = Vec::with_capacity(segs.len()); - for (i, s) in segs.iter().enumerate() { - let prev = &segs[(i + segs.len() - 1) % segs.len()]; - anchors.push(Anchor::with_handles(s.p0, prev.p2, s.p1)); - } - PathData::single(SubPath::new(anchors, true)) - } - - /// Mesh lines (all row and column edges) as open paths, for outline view and editing overlays. - pub fn lines(&self) -> PathData { - if !self.is_valid() { - return PathData::default(); - } - let (rows, cols) = (self.rows as usize, self.cols as usize); - let mut subs = vec![]; - let cub = |e: CubicBez| SubPath::new(vec![Anchor::with_handles(e.p0, e.p0, e.p1), Anchor::with_handles(e.p3, e.p2, e.p3)], false); - for r in 0..=rows { - for c in 0..cols { - subs.push(cub(self.edge_u(r, c))); - } - } - for c in 0..=cols { - for r in 0..rows { - subs.push(cub(self.edge_v(r, c))); - } - } - PathData::new(subs) - } - - /// Control bounds (points and handles). - pub fn bounds(&self) -> Option { - let mut it = self.points.iter().flat_map(|p| std::iter::once(p.p).chain(p.handles.iter().map(move |h| p.p + *h))); - let first = it.next()?; - Some(it.fold(Rect::from_points(first, first), |r, q| r.union_pt(q))) - } - - pub fn transform(&mut self, a: Affine) { - let lin = Affine::new({ - let c = a.as_coeffs(); - [c[0], c[1], c[2], c[3], 0.0, 0.0] - }); - for p in &mut self.points { - p.p = a * p.p; - for h in &mut p.handles { - *h = (lin * h.to_point()).to_vec2(); - } - } - } - - /// Map points (and handle ends) through a non-linear function. - pub fn map(&mut self, f: &dyn Fn(Point) -> Point) { - for p in &mut self.points { - let q = f(p.p); - for h in &mut p.handles { - *h = f(p.p + *h) - q; - } - p.p = q; - } - } - - /// Which patch and local (u, v) contains `p` (sampled search; `None` outside the mesh). - pub fn locate(&self, p: Point) -> Option<(usize, usize, f64, f64)> { - if !self.is_valid() { - return None; - } - let bp = self.outline().to_bezpath(); - if bp.winding(p) == 0 { - return None; - } - const N: usize = 16; - let mut best = (f64::INFINITY, 0, 0, 0.0, 0.0); - for r in 0..self.rows as usize { - for c in 0..self.cols as usize { - for j in 0..=N { - for i in 0..=N { - let (u, v) = (i as f64 / N as f64, j as f64 / N as f64); - let d = self.eval(r, c, u, v).distance_squared(p); - if d < best.0 { - best = (d, r, c, u, v); - } - } - } - } - } - // Refine with a few Newton-free local searches. - let (_, r, c, mut u, mut v) = best; - let mut step = 0.5 / N as f64; - for _ in 0..12 { - let mut improved = false; - for (du, dv) in [(step, 0.0), (-step, 0.0), (0.0, step), (0.0, -step)] { - let (nu, nv) = ((u + du).clamp(0.0, 1.0), (v + dv).clamp(0.0, 1.0)); - if self.eval(r, c, nu, nv).distance_squared(p) < self.eval(r, c, u, v).distance_squared(p) { - u = nu; - v = nv; - improved = true; - } - } - if !improved { - step /= 2.0; - } - } - Some((r, c, u, v)) - } - - /// Index of the mesh point within `tol` of `p`. - pub fn point_near(&self, p: Point, tol: f64) -> Option { - self.points - .iter() - .enumerate() - .filter(|(_, q)| q.p.distance(p) <= tol) - .min_by(|a, b| a.1.p.distance(p).total_cmp(&b.1.p.distance(p))) - .map(|(i, _)| i) - } - - fn surface_du(&self, r: usize, c: usize, u: f64, v: f64) -> Vec2 { - let e = 1e-3; - let (a, b) = ((u - e).max(0.0), (u + e).min(1.0)); - (self.eval(r, c, b, v) - self.eval(r, c, a, v)) / (b - a) - } - fn surface_dv(&self, r: usize, c: usize, u: f64, v: f64) -> Vec2 { - let e = 1e-3; - let (a, b) = ((v - e).max(0.0), (v + e).min(1.0)); - (self.eval(r, c, u, b) - self.eval(r, c, u, a)) / (b - a) - } - - /// Insert a mesh row through patch row `r` at local `v`. - pub fn insert_row(&mut self, r: usize, v: f64) { - let cols = self.cols as usize; - let v = v.clamp(0.01, 0.99); - let mut row = Vec::with_capacity(cols + 1); - // Split the vertical edges. - let mut ups = vec![]; - for c in 0..=cols { - let e = self.edge_v(r, c); - let (l, rr) = (e.subsegment(0.0..v), e.subsegment(v..1.0)); - ups.push((l.p1 - l.p0, rr.p2 - rr.p3)); - let pc = c.min(cols - 1); - let u = if c == cols { 1.0 } else { 0.0 }; - let (color, opacity) = self.color_at(r, pc, u, v); - // Horizontal handles: isocurve derivatives on either side. - let right = if c < cols { self.surface_du(r, c, 0.0, v) / (3.0) } else { Vec2::ZERO }; - let left = if c > 0 { -self.surface_du(r, c - 1, 1.0, v) / 3.0 } else { Vec2::ZERO }; - row.push(MeshPoint { p: l.p3, color, opacity, handles: [right, left, rr.p1 - rr.p0, l.p2 - l.p3] }); - } - for (c, (down, up)) in ups.into_iter().enumerate() { - let i0 = self.idx(r, c); - let i1 = self.idx(r + 1, c); - self.points[i0].handles[H_DOWN] = down; - self.points[i1].handles[H_UP] = up; - } - let at = self.idx(r + 1, 0); - self.points.splice(at..at, row); - self.rows += 1; - } - - /// Insert a mesh column through patch column `c` at local `u`. - pub fn insert_col(&mut self, c: usize, u: f64) { - let rows = self.rows as usize; - let u = u.clamp(0.01, 0.99); - let mut col = Vec::with_capacity(rows + 1); - for r in 0..=rows { - let e = self.edge_u(r, c); - let (l, rr) = (e.subsegment(0.0..u), e.subsegment(u..1.0)); - let pr = r.min(rows - 1); - let v = if r == rows { 1.0 } else { 0.0 }; - let (color, opacity) = self.color_at(pr, c, u, v); - let down = if r < rows { self.surface_dv(r, c, u, 0.0) / 3.0 } else { Vec2::ZERO }; - let up = if r > 0 { -self.surface_dv(r - 1, c, u, 1.0) / 3.0 } else { Vec2::ZERO }; - col.push((MeshPoint { p: l.p3, color, opacity, handles: [rr.p1 - rr.p0, l.p2 - l.p3, down, up] }, l.p1 - l.p0, rr.p2 - rr.p3)); - } - let cols = self.cols as usize; - let mut pts = Vec::with_capacity((rows + 1) * (cols + 2)); - for (r, (mp, right, left)) in col.into_iter().enumerate() { - for cc in 0..=cols { - let mut q = self.points[r * (cols + 1) + cc].clone(); - if cc == c { - q.handles[H_RIGHT] = right; - } - if cc == c + 1 { - q.handles[H_LEFT] = left; - } - pts.push(q); - if cc == c { - pts.push(mp.clone()); - } - } - } - self.points = pts; - self.cols += 1; - } - - /// Mesh tool click: add a row and a column through `p` (inside the mesh). Returns the new point index. - pub fn add_lines_at(&mut self, p: Point) -> Option { - let (r, c, u, v) = self.locate(p)?; - let mut rr = r; - let mut cc = c; - let row_new = v > 0.02 && v < 0.98; - let col_new = u > 0.02 && u < 0.98; - if row_new { - self.insert_row(r, v); - rr = r + 1; - } else if v >= 0.98 { - rr = r + 1; - } - if col_new { - self.insert_col(c, u); - cc = c + 1; - } else if u >= 0.98 { - cc = c + 1; - } - Some(self.idx(rr, cc)) - } - - /// Mesh tool Alt-click: delete the mesh lines through point `index` (interior lines only). - pub fn remove_point_lines(&mut self, index: usize) -> bool { - // A mesh from a file may not have the points its rows and columns need: it is indexed below. - if !self.is_valid() || index >= self.points.len() { - return false; - } - let w = self.cols as usize + 1; - let (r, c) = (index / w, index % w); - let mut changed = false; - if r > 0 && r < self.rows as usize { - // Merge the edges above and below: keep the outer handles. - for cc in 0..w { - let above = self.idx(r - 1, cc); - let below = self.idx(r + 1, cc); - let mid = self.idx(r, cc); - let (pa, pm, pb) = (self.points[above].p, self.points[mid].p, self.points[below].p); - let _ = pm; - self.points[above].handles[H_DOWN] = (pb - pa) / 3.0; - self.points[below].handles[H_UP] = (pa - pb) / 3.0; - } - self.points.drain(r * w..(r + 1) * w); - self.rows -= 1; - changed = true; - } - let w = self.cols as usize + 1; - if c > 0 && c < self.cols as usize { - let rows = self.rows as usize; - for rr in 0..=rows { - let (l, rt) = (rr * w + c - 1, rr * w + c + 1); - let (pl, pr) = (self.points[l].p, self.points[rt].p); - self.points[l].handles[H_RIGHT] = (pr - pl) / 3.0; - self.points[rt].handles[H_LEFT] = (pl - pr) / 3.0; - } - let mut k = 0; - self.points.retain(|_| { - let keep = k % w != c; - k += 1; - keep - }); - self.cols -= 1; - changed = true; - } - changed - } -} - -// ===================================================================================== -// Envelopes -// ===================================================================================== - -/// Union of the geometric bounds of `nodes`. -pub fn nodes_bounds(nodes: &[Arc]) -> Option { - nodes.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.geometric_bounds())) -} - -/// A `(rows+1)×(cols+1)` grid over `r` (row-major). -pub fn grid_points(r: Rect, rows: u32, cols: u32) -> Vec { - let mut v = Vec::with_capacity(((rows + 1) * (cols + 1)) as usize); - for i in 0..=rows { - for j in 0..=cols { - v.push(Point::new(r.x0 + r.width() * j as f64 / cols.max(1) as f64, r.y0 + r.height() * i as f64 / rows.max(1) as f64)); - } - } - v -} - -fn catmull(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f64) -> Vec2 { - let t2 = t * t; - let t3 = t2 * t; - 0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t3) -} - -/// 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, 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); - let cc = c.clamp(0, cols as i64); - let base = pts[rr as usize * (cols + 1) + cc as usize].to_vec2(); - // Linear extrapolation outside the grid. - let er = r - rr; - let ec = c - cc; - let mut p = base; - if er != 0 && rows >= 1 { - let inner = (rr - er.signum()).clamp(0, rows as i64); - p += (base - pts[inner as usize * (cols + 1) + cc as usize].to_vec2()) * er.abs() as f64; - } - if ec != 0 && cols >= 1 { - let inner = (cc - ec.signum()).clamp(0, cols as i64); - p += (base - pts[rr as usize * (cols + 1) + inner as usize].to_vec2()) * ec.abs() as f64; - } - p - }; - // 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() { - let r = ri - 1 + k as i64; - *rv = catmull(at(r, ci - 1), at(r, ci), at(r, ci + 1), at(r, ci + 2), fu); - } - catmull(rowsv[0], rowsv[1], rowsv[2], rowsv[3], fv).to_point() -} - -/// 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 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) = (src.width() / 2.0, src.height() / 2.0); - let horizontal = *horizontal; - 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, hh / hw.max(1e-9), x, y) - } else { - let (a, b2) = warp_point(st, b, dh, dv, hw / hh.max(1e-9), y, x); - (b2, a) - }; - frame * Point::new(c.x + x2 * hw, c.y + y2 * hh) - }) - } - EnvelopeKind::Mesh { rows, cols, points, handles } => { - if !handles.is_empty() - && let Some(m) = envelope_grid(*rows, *cols, points, handles) - { - return Box::new(move |u: f64, v: f64| mesh_at(&m, u, v)); - } - if *rows == 0 || *cols == 0 || grid_len(*rows, *cols) != Some(points.len()) { - return Box::new(flat); - } - let (rows, cols) = (*rows as usize, *cols as usize); - 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 |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 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)) -} - -/// Catmull-Rom tangents of a `(rows+1)×(cols+1)` point grid (row-major) as mesh handles, a third -/// of the tangent towards each neighbour (past the borders the grid goes on straight): the curves -/// an envelope mesh without handles of its own runs along between its points. -pub fn smooth_handles(rows: u32, cols: u32, points: &[Point]) -> Vec<[Vec2; 4]> { - let (rows, cols) = (rows as usize, cols as usize); - let at = |r: usize, c: usize| points.get(r * (cols + 1) + c).copied().unwrap_or_default(); - let tangent = |prev: Option, p: Point, next: Option| match (prev, next) { - (Some(a), Some(b)) => (b - a) / 2.0, - (None, Some(b)) => b - p, - (Some(a), None) => p - a, - (None, None) => Vec2::ZERO, - }; - let mut out = Vec::with_capacity(points.len()); - for r in 0..=rows { - for c in 0..=cols { - let p = at(r, c); - let du = tangent(c.checked_sub(1).map(|c| at(r, c)), p, (c < cols).then(|| at(r, c + 1))) / 3.0; - let dv = tangent(r.checked_sub(1).map(|r| at(r, c)), p, (r < rows).then(|| at(r + 1, c))) / 3.0; - // Handles pointing out of the grid are unused: they stay at the point. - let keep = |inside: bool, d: Vec2| if inside { d } else { Vec2::ZERO }; - let mut h = [Vec2::ZERO; 4]; - (h[H_RIGHT], h[H_LEFT], h[H_DOWN], h[H_UP]) = (keep(c < cols, du), keep(c > 0, -du), keep(r < rows, dv), keep(r > 0, -dv)); - out.push(h); - } - } - out -} - -/// A mesh envelope's grid (`rows`×`cols` patches, `points` and their `handles`; without handles -/// the smooth ones, see [`smooth_handles`]) as a gradient mesh with black points: what the -/// canvas draws and the Mesh tool and Direct Selection edit. `None` for a malformed grid. -pub fn envelope_grid(rows: u32, cols: u32, points: &[Point], handles: &[[Vec2; 4]]) -> Option { - if rows == 0 || cols == 0 || grid_len(rows, cols) != Some(points.len()) { - return None; - } - let smooth; - let handles = if handles.len() == points.len() { - handles - } else { - smooth = smooth_handles(rows, cols, points); - &smooth - }; - let points = points.iter().zip(handles).map(|(p, h)| MeshPoint { p: *p, color: Color::BLACK, opacity: 1.0, handles: *h }).collect(); - Some(GradientMesh { rows, cols, points }) -} - -/// The point of mesh `m` at (u, v) over the whole mesh (each patch an equal share; past the edges -/// the outer patches go on). -fn mesh_at(m: &GradientMesh, u: f64, v: f64) -> Point { - let (rows, cols) = (m.rows.max(1) as i64, m.cols.max(1) as i64); - let (su, sv) = (u * cols as f64, v * rows as f64); - let (c, r) = ((su.floor() as i64).clamp(0, cols - 1), (sv.floor() as i64).clamp(0, rows - 1)); - m.eval(r as usize, c as usize, su - c as f64, sv - r as f64) -} - -/// The envelope `id` is, or the innermost one it sits in. -pub fn envelope_of(doc: &crate::Document, id: NodeId) -> Option<&Node> { - doc.ancestry(id)?.into_iter().rev().find_map(|a| doc.node(a).filter(|n| matches!(n.kind, NodeKind::Envelope { .. }))) -} - -/// What a selected envelope shows on the canvas: its mesh lines (a warp's surface on a 4×4 grid, -/// a mesh envelope's own grid) or its top object's outline, and for a mesh envelope the grid to -/// edit (its points and handles). -pub fn envelope_overlay(n: &Node) -> Option<(PathData, Option)> { - let NodeKind::Envelope { kind, .. } = &n.kind else { return None }; - match &**kind { - EnvelopeKind::Mesh { rows, cols, points, handles } => { - let grid = envelope_grid(*rows, *cols, points, handles)?; - Some((grid.lines(), Some(grid))) - } - EnvelopeKind::Warp { .. } => Some((EnvelopeMap::of(n)?.surface_mesh(4, 4, Color::BLACK).lines(), None)), - EnvelopeKind::TopObject { path } => Some((path.clone(), None)), - } -} - -/// 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 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], kind: &'a EnvelopeKind, frame: Affine) -> Option { - 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 { - 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 { - 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 { - const N: usize = 16; - let mut r: Option = 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>; - -/// 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>; - -/// 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>, - /// 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> 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.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 -} - -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 { - 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); - // 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 = 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(Box::new(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 { - 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| { - 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() - }) -} - -/// 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], kind: &EnvelopeKind, fidelity: f64, outline: Outliner) -> Vec { - expand_envelope(content, kind, fidelity, EnvelopeOptions::default(), Affine::IDENTITY, outline.into()) -} - -/// 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], kind: &EnvelopeKind, fidelity: f64, options: EnvelopeOptions, frame: Affine, hooks: Hooks) -> Vec { - 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: a mesh's points, a top object's path, the warp's surface (sampled). -pub fn envelope_bounds(content: &[Arc], kind: &EnvelopeKind, frame: Affine) -> Option { - match kind { - EnvelopeKind::Mesh { rows, cols, points, handles } if !handles.is_empty() => envelope_grid(*rows, *cols, points, handles)?.outline().bounds(), - 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 { .. } => EnvelopeMap::new(content, kind, frame)?.bounds(), - } -} - -// ===================================================================================== -// 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 { - 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 { - expand_live_with(n, None) -} - -pub fn expand_live_with(n: &Node, outline: Outliner) -> Vec { - 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 { - 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> = 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], 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 -} - -/// Reading a native file never fails over one bad blend number: out-of-range values clamp, -/// and anything that is not a finite number (NaN, null, text) becomes the default. The -/// commands still refuse such input (`ease_param`). -fn blend_number<'de, D: serde::Deserializer<'de>>(d: D, min: f64, max: f64) -> Result { - let v = serde_json::Value::deserialize(d)?; - Ok(v.as_f64().filter(|n| n.is_finite()).map_or(0.0, |n| n.clamp(min, max))) -} - -fn blend_ramp<'de, D: serde::Deserializer<'de>>(d: D) -> Result { - blend_number(d, 0.0, 1.0) -} - -fn blend_color_shift<'de, D: serde::Deserializer<'de>>(d: D) -> Result { - blend_number(d, -100.0, 100.0) -} - -/// Whether an optional stored color shift has its unchanged default. -fn is_zero(v: &f64) -> bool { - *v == 0.0 -} - -impl BlendSpec { - /// Shift the color transition independently of shape placement. The exponential curve is - /// original; the documented behavior specifies the direction, not numeric sample values. - pub fn color_position(&self, u: f64) -> f64 { - let u = if u.is_finite() { u.clamp(0.0, 1.0) } else { 0.0 }; - let shift = if self.color_shift.is_finite() { self.color_shift.clamp(-100.0, 100.0) } else { 0.0 }; - if shift == 0.0 { u } else { u.powf(2.0f64.powf(shift / 50.0)) } - } -} +mod blend_spec; +mod envelope; +mod expand; +mod mesh; +mod warp; + +pub use blend_spec::{BlendDefaults, BlendEase, BlendEaseType, BlendOrientation, BlendSpacing, BlendSpec}; +#[cfg(test)] +use blend_spec::{blend_color_shift, blend_ramp}; +pub use envelope::{ + EnvelopeKind, EnvelopeMap, EnvelopeOptions, H_DOWN, H_LEFT, H_RIGHT, H_UP, PreserveShape, envelope_bounds, envelope_expand, envelope_frame, + envelope_grid, envelope_mapper, envelope_of, envelope_overlay, smooth_handles, +}; +pub(crate) use expand::max_outset; +pub use expand::{ + Hooks, MAX_GENERATED, Outliner, PatternTiles, affine_near, expand_deep, expand_deep_hooks, expand_live, expand_live_hooks, expand_live_with, + expanded_group, expanded_group_hooks, is_live, mesh_quad_nodes, subtree_has_live, +}; +#[cfg(test)] +use mesh::grid_eval; +pub use mesh::{Coons, GradientMesh, MeshAppearance, MeshPoint, MeshQuad, default_fidelity, grid_points, nodes_bounds}; +pub use warp::{WarpStyle, fit_piece, map_nonlinear, map_nonlinear_within, pieces, warp_point}; #[cfg(test)] mod tests { diff --git a/crates/doc/src/live/blend_spec.rs b/crates/doc/src/live/blend_spec.rs new file mode 100644 index 000000000..79c129030 --- /dev/null +++ b/crates/doc/src/live/blend_spec.rs @@ -0,0 +1,157 @@ +use super::*; + +// ===================================================================================== +// Data +// ===================================================================================== + +/// Blend Options → Spacing. +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum BlendSpacing { + /// Step count chosen from the colour difference (or the distance when colours match). + #[default] + SmoothColor, + /// Specified number of steps between each pair of key objects (1–1000). + Steps(u32), + /// Specified distance (pt) between the steps. + Distance(f64), +} + +/// Blend Options → Orientation. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum BlendOrientation { + #[default] + AlignToPage, + AlignToPath, +} + +/// Distribution of blend steps, as offered by the Blend panel's Type menu. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum BlendEaseType { + #[default] + In, + Out, + InOut, + OutIn, +} + +impl BlendEaseType { + pub fn id(self) -> &'static str { + match self { + Self::In => "easeIn", + Self::Out => "easeOut", + Self::InOut => "easeInOut", + Self::OutIn => "easeOutIn", + } + } +} + +/// Blend panel Type and Ramp. Ramp is a fraction from 0 (even) to 1. +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct BlendEase { + #[serde(default)] + pub kind: BlendEaseType, + #[serde(default, deserialize_with = "blend_ramp")] + pub ramp: f64, +} + +impl BlendEase { + pub fn is_linear(&self) -> bool { + self.ramp <= 0.0 + } + + /// Only the default Type and Ramp can be omitted without losing a chosen Type at Ramp 0. + pub fn is_default(&self) -> bool { + self.kind == BlendEaseType::In && self.ramp == 0.0 + } + + /// A monotone polynomial distribution, blended with even spacing by Ramp. Public help + /// describes the four distributions, not their numeric curves; these curves are original. + pub fn apply(&self, u: f64) -> f64 { + let u = if u.is_finite() { u.clamp(0.0, 1.0) } else { 0.0 }; + let r = if self.ramp.is_finite() { self.ramp.clamp(0.0, 1.0) } else { 0.0 }; + let eased = match self.kind { + BlendEaseType::In => u * u, + BlendEaseType::Out => u * (2.0 - u), + BlendEaseType::InOut => u * u * (3.0 - 2.0 * u), + BlendEaseType::OutIn if u < 0.5 => 2.0 * u * (1.0 - u), + BlendEaseType::OutIn => 0.5 + 2.0 * (u - 0.5) * (u - 0.5), + }; + u + r * (eased - u) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct BlendSpec { + #[serde(default)] + pub spacing: BlendSpacing, + #[serde(default)] + pub orientation: BlendOrientation, + /// Acceleration of the steps' shapes and positions along the spine between two keys. + #[serde(default, skip_serializing_if = "BlendEase::is_default")] + pub ease: BlendEase, + /// Independent color acceleration. -100..100, with 0 preserving the even color ramp. + #[serde(default, rename = "colorShift", skip_serializing_if = "is_zero", deserialize_with = "blend_color_shift")] + pub color_shift: f64, + /// Replaced spine. `None` = the straight lines between the key objects' centres. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub spine: Option, + /// The anchor of the spine's first subpath each key object sits on (a spine edited on the + /// canvas: moving a key moves its anchor and the other way round). Empty: the keys spread + /// evenly along the spine by arc length (Replace Spine). + #[serde(default, rename = "keyAnchors", skip_serializing_if = "Vec::is_empty")] + pub key_anchors: Vec, + /// Per key object, the anchor of its first subpath the blend starts from (Blend tool clicks + /// on anchor points); `None`: chosen so closed shapes don't twist. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub starts: Vec>, +} + +impl BlendSpec { + /// The start anchor of key `i` (see [`Self::starts`]). + pub fn start(&self, i: usize) -> Option { + self.starts.get(i).copied().flatten().map(|a| a as usize) + } +} + +/// Blend Options set with no blend selected: what new blends start with. +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct BlendDefaults { + #[serde(default)] + pub spacing: BlendSpacing, + #[serde(default)] + pub orientation: BlendOrientation, +} + +/// Reading a native file never fails over one bad blend number: out-of-range values clamp, +/// and anything that is not a finite number (NaN, null, text) becomes the default. The +/// commands still refuse such input (`ease_param`). +fn blend_number<'de, D: serde::Deserializer<'de>>(d: D, min: f64, max: f64) -> Result { + let v = serde_json::Value::deserialize(d)?; + Ok(v.as_f64().filter(|n| n.is_finite()).map_or(0.0, |n| n.clamp(min, max))) +} + +pub(super) fn blend_ramp<'de, D: serde::Deserializer<'de>>(d: D) -> Result { + blend_number(d, 0.0, 1.0) +} + +pub(super) fn blend_color_shift<'de, D: serde::Deserializer<'de>>(d: D) -> Result { + blend_number(d, -100.0, 100.0) +} + +/// Whether an optional stored color shift has its unchanged default. +fn is_zero(v: &f64) -> bool { + *v == 0.0 +} + +impl BlendSpec { + /// Shift the color transition independently of shape placement. The exponential curve is + /// original; the documented behavior specifies the direction, not numeric sample values. + pub fn color_position(&self, u: f64) -> f64 { + let u = if u.is_finite() { u.clamp(0.0, 1.0) } else { 0.0 }; + let shift = if self.color_shift.is_finite() { self.color_shift.clamp(-100.0, 100.0) } else { 0.0 }; + if shift == 0.0 { u } else { u.powf(2.0f64.powf(shift / 50.0)) } + } +} diff --git a/crates/doc/src/live/envelope.rs b/crates/doc/src/live/envelope.rs new file mode 100644 index 000000000..13d30cef0 --- /dev/null +++ b/crates/doc/src/live/envelope.rs @@ -0,0 +1,332 @@ +use super::expand::{Hooks, Outliner, Warper, map_node}; +use super::mesh::{GradientMesh, MeshPoint, grid_len, grid_points, nodes_bounds, surface_of}; +use super::*; + +/// How an envelope distorts its content. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum EnvelopeKind { + /// One of the 15 warp styles (`arc`, `arcLower`, … — see [`WarpStyle::from_id`]); bend and + /// distortions in percent (-100..100). + Warp { + style: String, + #[serde(default)] + bend: f64, + #[serde(default)] + h: f64, + #[serde(default)] + v: f64, + #[serde(default = "yes")] + horizontal: bool, + }, + /// A `(rows+1)×(cols+1)` grid of points (row-major) the content's bounding box maps onto. + Mesh { + rows: u32, + cols: u32, + points: Vec, + /// Each point's bezier handles (offsets, see [`H_RIGHT`] …), row-major like `points`. + /// Empty: the smooth (Catmull-Rom) mesh through the points, as before handles existed; + /// editing the mesh's points or handles on the canvas fills them in. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + handles: Vec<[Vec2; 4]>, + }, + /// The content's bounding box maps onto the outline of this path. + TopObject { path: PathData }, +} + +/// Envelope Options → Preserve Shape Using: how a distorted raster keeps the envelope's shape. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PreserveShape { + #[default] + ClippingMask, + Transparency, +} + +impl PreserveShape { + pub fn id(self) -> &'static str { + match self { + Self::ClippingMask => "clippingMask", + Self::Transparency => "transparency", + } + } + pub fn parse(s: &str) -> Option { + match s { + "clippingMask" => Some(Self::ClippingMask), + "transparency" => Some(Self::Transparency), + _ => None, + } + } +} + +/// Envelope Options besides Fidelity. `Default` is what envelopes saved before these options +/// existed do (their appearance applies after the distortion); new envelopes start from +/// [`EnvelopeOptions::NEW`], as in the reference app. +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct EnvelopeOptions { + /// Rasters: Anti-Alias. + pub anti_alias: bool, + /// Rasters: Preserve Shape Using. + pub preserve_shape: PreserveShape, + /// Distort Appearance: strokes and live effects are applied before the distortion, so they + /// bend with the art (off: they apply to the distorted paths). + pub distort_appearance: bool, + /// Distort Linear Gradients (with Distort Appearance): placed gradients bend with the art. + pub distort_linear_gradients: bool, + /// Distort Pattern Fills (with Distort Appearance): pattern tiles bend with the art. + pub distort_pattern_fills: bool, +} + +impl Default for EnvelopeOptions { + fn default() -> Self { + Self { + anti_alias: true, + preserve_shape: PreserveShape::ClippingMask, + distort_appearance: false, + distort_linear_gradients: false, + distort_pattern_fills: false, + } + } +} + +impl EnvelopeOptions { + /// The options a new envelope gets (the reference app's defaults). + pub const NEW: Self = Self { + anti_alias: true, + preserve_shape: PreserveShape::ClippingMask, + distort_appearance: true, + distort_linear_gradients: false, + distort_pattern_fills: false, + }; + /// Do gradients bend with the art? + pub fn gradients(&self) -> bool { + self.distort_appearance && self.distort_linear_gradients + } + /// Do pattern tiles bend with the art? + pub fn patterns(&self) -> bool { + self.distort_appearance && self.distort_pattern_fills + } +} + +/// Handle slots of a [`MeshPoint`]: towards the next column, previous column, next row, previous row. +pub const H_RIGHT: usize = 0; + +pub const H_LEFT: usize = 1; + +pub const H_DOWN: usize = 2; + +pub const H_UP: usize = 3; + +/// 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 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)) +} + +/// Catmull-Rom tangents of a `(rows+1)×(cols+1)` point grid (row-major) as mesh handles, a third +/// of the tangent towards each neighbour (past the borders the grid goes on straight): the curves +/// an envelope mesh without handles of its own runs along between its points. +pub fn smooth_handles(rows: u32, cols: u32, points: &[Point]) -> Vec<[Vec2; 4]> { + let (rows, cols) = (rows as usize, cols as usize); + let at = |r: usize, c: usize| points.get(r * (cols + 1) + c).copied().unwrap_or_default(); + let tangent = |prev: Option, p: Point, next: Option| match (prev, next) { + (Some(a), Some(b)) => (b - a) / 2.0, + (None, Some(b)) => b - p, + (Some(a), None) => p - a, + (None, None) => Vec2::ZERO, + }; + let mut out = Vec::with_capacity(points.len()); + for r in 0..=rows { + for c in 0..=cols { + let p = at(r, c); + let du = tangent(c.checked_sub(1).map(|c| at(r, c)), p, (c < cols).then(|| at(r, c + 1))) / 3.0; + let dv = tangent(r.checked_sub(1).map(|r| at(r, c)), p, (r < rows).then(|| at(r + 1, c))) / 3.0; + // Handles pointing out of the grid are unused: they stay at the point. + let keep = |inside: bool, d: Vec2| if inside { d } else { Vec2::ZERO }; + let mut h = [Vec2::ZERO; 4]; + (h[H_RIGHT], h[H_LEFT], h[H_DOWN], h[H_UP]) = (keep(c < cols, du), keep(c > 0, -du), keep(r < rows, dv), keep(r > 0, -dv)); + out.push(h); + } + } + out +} + +/// A mesh envelope's grid (`rows`×`cols` patches, `points` and their `handles`; without handles +/// the smooth ones, see [`smooth_handles`]) as a gradient mesh with black points: what the +/// canvas draws and the Mesh tool and Direct Selection edit. `None` for a malformed grid. +pub fn envelope_grid(rows: u32, cols: u32, points: &[Point], handles: &[[Vec2; 4]]) -> Option { + if rows == 0 || cols == 0 || grid_len(rows, cols) != Some(points.len()) { + return None; + } + let smooth; + let handles = if handles.len() == points.len() { + handles + } else { + smooth = smooth_handles(rows, cols, points); + &smooth + }; + let points = points.iter().zip(handles).map(|(p, h)| MeshPoint { p: *p, color: Color::BLACK, opacity: 1.0, handles: *h }).collect(); + Some(GradientMesh { rows, cols, points }) +} + +/// The point of mesh `m` at (u, v) over the whole mesh (each patch an equal share; past the edges +/// the outer patches go on). +pub(super) fn mesh_at(m: &GradientMesh, u: f64, v: f64) -> Point { + let (rows, cols) = (m.rows.max(1) as i64, m.cols.max(1) as i64); + let (su, sv) = (u * cols as f64, v * rows as f64); + let (c, r) = ((su.floor() as i64).clamp(0, cols - 1), (sv.floor() as i64).clamp(0, rows - 1)); + m.eval(r as usize, c as usize, su - c as f64, sv - r as f64) +} + +/// The envelope `id` is, or the innermost one it sits in. +pub fn envelope_of(doc: &crate::Document, id: NodeId) -> Option<&Node> { + doc.ancestry(id)?.into_iter().rev().find_map(|a| doc.node(a).filter(|n| matches!(n.kind, NodeKind::Envelope { .. }))) +} + +/// What a selected envelope shows on the canvas: its mesh lines (a warp's surface on a 4×4 grid, +/// a mesh envelope's own grid) or its top object's outline, and for a mesh envelope the grid to +/// edit (its points and handles). +pub fn envelope_overlay(n: &Node) -> Option<(PathData, Option)> { + let NodeKind::Envelope { kind, .. } = &n.kind else { return None }; + match &**kind { + EnvelopeKind::Mesh { rows, cols, points, handles } => { + let grid = envelope_grid(*rows, *cols, points, handles)?; + Some((grid.lines(), Some(grid))) + } + EnvelopeKind::Warp { .. } => Some((EnvelopeMap::of(n)?.surface_mesh(4, 4, Color::BLACK).lines(), None)), + EnvelopeKind::TopObject { path } => Some((path.clone(), None)), + } +} + +/// 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 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], kind: &'a EnvelopeKind, frame: Affine) -> Option { + 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 { + 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 { + 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 { + const N: usize = 16; + let mut r: Option = 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 + } +} + +/// 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], kind: &EnvelopeKind, fidelity: f64, outline: Outliner) -> Vec { + expand_envelope(content, kind, fidelity, EnvelopeOptions::default(), Affine::IDENTITY, outline.into()) +} + +/// 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. +pub(super) fn expand_envelope( + content: &[Arc], + kind: &EnvelopeKind, + fidelity: f64, + options: EnvelopeOptions, + frame: Affine, + hooks: Hooks, +) -> Vec { + 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: a mesh's points, a top object's path, the warp's surface (sampled). +pub fn envelope_bounds(content: &[Arc], kind: &EnvelopeKind, frame: Affine) -> Option { + match kind { + EnvelopeKind::Mesh { rows, cols, points, handles } if !handles.is_empty() => envelope_grid(*rows, *cols, points, handles)?.outline().bounds(), + 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 { .. } => EnvelopeMap::new(content, kind, frame)?.bounds(), + } +} diff --git a/crates/doc/src/live/expand.rs b/crates/doc/src/live/expand.rs new file mode 100644 index 000000000..09eada113 --- /dev/null +++ b/crates/doc/src/live/expand.rs @@ -0,0 +1,444 @@ +use super::envelope::{EnvelopeOptions, expand_envelope}; +use super::mesh::GradientMesh; +use super::warp::map_nonlinear; +use super::*; + +/// Optional hook that converts nodes the pure evaluation can't map (text) to outlines. +pub type Outliner<'a> = Option<&'a dyn Fn(&Node) -> Option>; + +/// 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>; + +/// 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>, + /// 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> 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.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 { + 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); + // 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 = 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(Box::new(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 { + 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| { + 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 { + 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 { + expand_live_with(n, None) +} + +pub fn expand_live_with(n: &Node, outline: Outliner) -> Vec { + 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 { + 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> = 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], 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 +} diff --git a/crates/doc/src/live/mesh.rs b/crates/doc/src/live/mesh.rs new file mode 100644 index 000000000..2cbe7602c --- /dev/null +++ b/crates/doc/src/live/mesh.rs @@ -0,0 +1,761 @@ +use super::envelope::{EnvelopeKind, H_DOWN, H_LEFT, H_RIGHT, H_UP, envelope_grid, mesh_at}; +use super::warp::{WarpStyle, flatten_subpath, resample, warp_point}; +use super::*; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MeshPoint { + pub p: Point, + pub color: Color, + #[serde(default = "one")] + pub opacity: f32, + /// Tangent handles as offsets from `p` (see [`H_RIGHT`] …). + #[serde(default)] + pub handles: [Vec2; 4], +} + +/// A gradient mesh: `(rows+1)×(cols+1)` points, row-major. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GradientMesh { + pub rows: u32, + pub cols: u32, + pub points: Vec, +} + +/// One tessellated piece of a mesh: a small quad, its colour at the centre, and its colour and +/// opacity at each corner (in `pts` order), which a renderer shades between so neighbouring quads +/// meet without a step. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct MeshQuad { + pub pts: [Point; 4], + pub color: Color, + pub opacity: f32, + pub corners: [(Color, f32); 4], +} + +/// Create Gradient Mesh → Appearance. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum MeshAppearance { + #[default] + Flat, + ToCenter, + ToEdge, +} + +impl MeshAppearance { + pub fn parse(s: &str) -> Option { + match s.to_ascii_lowercase().as_str() { + "flat" => Some(Self::Flat), + "center" | "tocenter" | "to center" => Some(Self::ToCenter), + "edge" | "toedge" | "to edge" => Some(Self::ToEdge), + _ => None, + } + } +} + +/// Default envelope fidelity. +pub fn default_fidelity() -> f64 { + fifty() +} + +const SIDE: usize = 65; + +/// Four boundary curves (top TL→TR, right TR→BR, bottom BL→BR, left TL→BL), each resampled to +/// [`SIDE`] points, from the outline of a closed path split at the points nearest its bounding +/// box corners. +#[derive(Clone, Debug)] +pub struct Coons { + top: Vec, + right: Vec, + bottom: Vec, + left: Vec, +} + +impl Coons { + /// A rectangle. + pub fn rect(r: Rect) -> Self { + let line = |a: Point, b: Point| (0..SIDE).map(|i| a.lerp(b, i as f64 / (SIDE - 1) as f64)).collect::>(); + let (tl, tr, br, bl) = (Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)); + Self { top: line(tl, tr), right: line(tr, br), bottom: line(bl, br), left: line(tl, bl) } + } + + pub fn from_path(path: &PathData) -> Option { + // The largest subpath is the outline. + let sp = path.subpaths.iter().filter(|s| s.anchors.len() >= 2).max_by(|a, b| a.area().abs().total_cmp(&b.area().abs()))?; + let r = path.bounds()?; + let tol = (r.width() + r.height()).max(1e-6) / 2000.0; + let mut poly = flatten_subpath(sp, tol); + if poly.len() > 1 + && let (Some(a), Some(b)) = (poly.first(), poly.last()) + && a.distance(*b) < 1e-9 + { + poly.pop(); + } + if poly.len() < 3 { + return Some(Self::rect(r)); + } + // Orient clockwise in y-down (TL → TR → BR → BL). + let area: f64 = (0..poly.len()) + .map(|i| { + let (p, q) = (poly[i], poly[(i + 1) % poly.len()]); + p.x * q.y - q.x * p.y + }) + .sum(); + if area < 0.0 { + poly.reverse(); + } + let nearest = |c: Point| (0..poly.len()).min_by(|i, j| poly[*i].distance_squared(c).total_cmp(&poly[*j].distance_squared(c))).unwrap_or(0); + let tl = nearest(Point::new(r.x0, r.y0)); + let tr = nearest(Point::new(r.x1, r.y0)); + let br = nearest(Point::new(r.x1, r.y1)); + let bl = nearest(Point::new(r.x0, r.y1)); + let n = poly.len(); + let arc = |i: usize, j: usize| -> Vec { + let mut v = vec![poly[i]]; + let mut k = i; + while k != j { + k = (k + 1) % n; + v.push(poly[k]); + } + v + }; + let top = resample(&arc(tl, tr), SIDE); + let right = resample(&arc(tr, br), SIDE); + let mut bottom = resample(&arc(br, bl), SIDE); + bottom.reverse(); + let mut left = resample(&arc(bl, tl), SIDE); + left.reverse(); + 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 * (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) + } + + /// The surface point at (u, v) ∈ [0,1]². + pub fn eval(&self, u: f64, v: f64) -> Point { + let (t, b, l, r) = (Self::sample(&self.top, u), Self::sample(&self.bottom, u), Self::sample(&self.left, v), Self::sample(&self.right, v)); + let (p00, p10, p01, p11) = (self.top[0], self.top[SIDE - 1], self.bottom[0], self.bottom[SIDE - 1]); + let c = (1.0 - v) * t.to_vec2() + v * b.to_vec2() + (1.0 - u) * l.to_vec2() + u * r.to_vec2() + - ((1.0 - u) * (1.0 - v) * p00.to_vec2() + u * (1.0 - v) * p10.to_vec2() + (1.0 - u) * v * p01.to_vec2() + u * v * p11.to_vec2()); + c.to_point() + } + + /// The four corners TL, TR, BR, BL. + pub fn corners(&self) -> [Point; 4] { + [self.top[0], self.top[SIDE - 1], self.bottom[SIDE - 1], self.bottom[0]] + } +} + +// ===================================================================================== +// Gradient meshes +// ===================================================================================== + +/// `0, 1/n, …, 1` for `n` (clamped to 1..=200) even steps. +fn even_steps(n: u32) -> Vec { + let n = n.clamp(1, 200); + (0..=n).map(|i| i as f64 / n as f64).collect() +} + +/// The number of points of a `(rows+1)×(cols+1)` grid, `None` past `usize`: counts come from +/// files, and u32::MAX rows and columns (65,535 on 32-bit targets) would wrap to an empty grid. +pub(super) fn grid_len(rows: u32, cols: u32) -> Option { + (rows as usize).checked_add(1)?.checked_mul((cols as usize).checked_add(1)?) +} + +impl GradientMesh { + pub fn idx(&self, r: usize, c: usize) -> usize { + r * (self.cols as usize + 1) + c + } + pub fn is_valid(&self) -> bool { + self.rows >= 1 && self.cols >= 1 && grid_len(self.rows, self.cols) == Some(self.points.len()) + } + + /// A 1×1 mesh from its four corners (bottom-left, top-left, top-right, bottom-right in u/v + /// order: row 0 is v = 0, columns follow u), each with its handles ([`H_RIGHT`] …). + fn patch(corners: [(Point, [Vec2; 4], Color); 4]) -> Self { + let point = |(p, handles, color): (Point, [Vec2; 4], Color)| MeshPoint { p, color, opacity: 1.0, handles }; + let [a, b, c, d] = corners; + Self { rows: 1, cols: 1, points: vec![point(a), point(d), point(b), point(c)] } + } + + /// A 1×1 mesh from a Coons patch's twelve boundary points (in the order PDF and PostScript + /// shadings list them: v runs cp0 → cp3 along cp1 and cp2, u runs cp0 → cp9 along cp11 and + /// cp10) and its corner colours (at cp0, cp3, cp6, cp9); `None` if a point isn't finite. + pub fn coons(cp: &[Point; 12], colors: [Color; 4]) -> Option { + if !cp.iter().all(|p| p.is_finite()) { + return None; + } + let h = |a: usize, b: usize| cp[b] - cp[a]; + let handles = |right: Vec2, left: Vec2, down: Vec2, up: Vec2| { + let mut h = [Vec2::ZERO; 4]; + (h[H_RIGHT], h[H_LEFT], h[H_DOWN], h[H_UP]) = (right, left, down, up); + h + }; + let [c0, c1, c2, c3] = colors; + Some(Self::patch([ + (cp[0], handles(h(0, 11), Vec2::ZERO, h(0, 1), Vec2::ZERO), c0), + (cp[3], handles(h(3, 4), Vec2::ZERO, Vec2::ZERO, h(3, 2)), c1), + (cp[6], handles(Vec2::ZERO, h(6, 5), Vec2::ZERO, h(6, 7)), c2), + (cp[9], handles(Vec2::ZERO, h(9, 10), h(9, 8), Vec2::ZERO), c3), + ])) + } + + /// A 1×1 mesh of a Gouraud-shaded triangle (a patch with two corners together); `None` if a + /// point isn't finite. + pub fn triangle(corners: [(Point, Color); 3]) -> Option { + if !corners.iter().all(|(p, _)| p.is_finite()) { + return None; + } + let [a, b, c] = corners.map(|(p, color)| (p, [Vec2::ZERO; 4], color)); + Some(Self::patch([a, b, c, c])) + } + + /// A mesh over surface `s` (u, v ∈ [0,1]) with handles from its partial derivatives. + pub fn from_surface(rows: u32, cols: u32, s: &dyn Fn(f64, f64) -> Point, color: &dyn Fn(f64, f64) -> Color) -> Self { + let (us, vs) = (even_steps(cols), even_steps(rows)); + Self::from_grid(&us, &vs, s, &|r, c, _| (color(us[c], vs[r]), 1.0)) + } + + /// A mesh over surface `s` (u, v ∈ [0,1]) whose column lines sit at `us` and row lines at `vs` + /// (ascending, at least two each; equal neighbours make a patch of no width, a sharp colour + /// change), with handles from its partial derivatives. `color(row, col, point)` gives each + /// point its colour and opacity. + pub fn from_grid(us: &[f64], vs: &[f64], s: &dyn Fn(f64, f64) -> Point, color: &dyn Fn(usize, usize, Point) -> (Color, f32)) -> Self { + let (rows, cols) = (vs.len().saturating_sub(1), us.len().saturating_sub(1)); + let eps = 1e-3; + let mut points = Vec::with_capacity(us.len() * vs.len()); + for (r, &v) in vs.iter().enumerate() { + for (c, &u) in us.iter().enumerate() { + let p = s(u, v); + let d_u = (s((u + eps).min(1.0), v) - s((u - eps).max(0.0), v)) / ((u + eps).min(1.0) - (u - eps).max(0.0)); + let d_v = (s(u, (v + eps).min(1.0)) - s(u, (v - eps).max(0.0))) / ((v + eps).min(1.0) - (v - eps).max(0.0)); + // A third of the way to the neighbouring line along the tangent; outward handles on + // the border are unused, so they stay at the point. + let h = |d: Vec2, from: f64, to: Option<&f64>| to.map_or(Vec2::ZERO, |to| d * ((to - from) / 3.0)); + let handles = [ + h(d_u, u, us.get(c + 1)), + h(d_u, u, c.checked_sub(1).map(|i| &us[i])), + h(d_v, v, vs.get(r + 1)), + h(d_v, v, r.checked_sub(1).map(|i| &vs[i])), + ]; + let (color, opacity) = color(r, c, p); + points.push(MeshPoint { p, color, opacity, handles }); + } + } + Self { rows: rows as u32, cols: cols as u32, points } + } + + /// Create Gradient Mesh for a path: the outline split at its corners, coloured per `appearance`. + pub fn for_path(path: &PathData, rows: u32, cols: u32, base: Color, appearance: MeshAppearance, highlight: f64) -> Option { + let mut m = Self::for_path_with(path, rows, cols, &|_| (base, 1.0))?; + m.highlight(appearance, highlight); + Some(m) + } + + /// Create Gradient Mesh for a path: the outline split at its corners, each point taking the + /// colour and opacity `color_at` gives at its position (a gradient fill's, say). + pub fn for_path_with(path: &PathData, rows: u32, cols: u32, color_at: &dyn Fn(Point) -> (Color, f32)) -> Option { + let coons = Coons::from_path(path)?; + Some(Self::from_grid(&even_steps(cols), &even_steps(rows), &|u, v| coons.eval(u, v), &|_, _, p| color_at(p))) + } + + /// Create Gradient Mesh → Appearance: lighten the points towards white, to the centre or to + /// the edges, by at most `highlight` %. + pub fn highlight(&mut self, appearance: MeshAppearance, highlight: f64) { + let h = (highlight / 100.0).clamp(0.0, 1.0) as f32; + let (rows, cols) = (self.rows as usize, self.cols as usize); + for r in 0..=rows { + for c in 0..=cols { + let (u, v) = (c as f64 / cols as f64, r as f64 / rows as f64); + let d = ((u - 0.5).abs().max((v - 0.5).abs()) * 2.0) as f32; // 0 centre … 1 edge + let w = match appearance { + MeshAppearance::Flat => 0.0, + MeshAppearance::ToCenter => (1.0 - d) * h, + MeshAppearance::ToEdge => d * h, + }; + if w > 0.0 { + let i = self.idx(r, c); + self.points[i].color = lerp_color(&self.points[i].color, &Color::WHITE, w); + } + } + } + } + + /// Horizontal edge from (r, c) to (r, c+1). + pub fn edge_u(&self, r: usize, c: usize) -> CubicBez { + let (a, b) = (&self.points[self.idx(r, c)], &self.points[self.idx(r, c + 1)]); + CubicBez::new(a.p, a.p + a.handles[H_RIGHT], b.p + b.handles[H_LEFT], b.p) + } + /// Vertical edge from (r, c) to (r+1, c). + pub fn edge_v(&self, r: usize, c: usize) -> CubicBez { + let (a, b) = (&self.points[self.idx(r, c)], &self.points[self.idx(r + 1, c)]); + CubicBez::new(a.p, a.p + a.handles[H_DOWN], b.p + b.handles[H_UP], b.p) + } + + /// Coons patch point of patch (r, c) at local (u, v). + pub fn eval(&self, r: usize, c: usize, u: f64, v: f64) -> Point { + let (t, b) = (self.edge_u(r, c).eval(u), self.edge_u(r + 1, c).eval(u)); + let (l, rr) = (self.edge_v(r, c).eval(v), self.edge_v(r, c + 1).eval(v)); + let p00 = self.points[self.idx(r, c)].p.to_vec2(); + let p10 = self.points[self.idx(r, c + 1)].p.to_vec2(); + let p01 = self.points[self.idx(r + 1, c)].p.to_vec2(); + let p11 = self.points[self.idx(r + 1, c + 1)].p.to_vec2(); + ((1.0 - v) * t.to_vec2() + v * b.to_vec2() + (1.0 - u) * l.to_vec2() + u * rr.to_vec2() + - ((1.0 - u) * (1.0 - v) * p00 + u * (1.0 - v) * p10 + (1.0 - u) * v * p01 + u * v * p11)) + .to_point() + } + + /// Bilinear colour and opacity of patch (r, c) at local (u, v). Corners all in CMYK (or all + /// grey) mix in that space, as a PDF shading mixes in its own colour space: the inks come out + /// exact, and a CMYK document separates no colour to draw them. Other corners mix in RGB. + pub fn color_at(&self, r: usize, c: usize, u: f64, v: f64) -> (Color, f32) { + let q = + [&self.points[self.idx(r, c)], &self.points[self.idx(r, c + 1)], &self.points[self.idx(r + 1, c)], &self.points[self.idx(r + 1, c + 1)]]; + let w = [(1.0 - u) * (1.0 - v), u * (1.0 - v), (1.0 - u) * v, u * v]; + let op = q.iter().zip(w).map(|(p, w)| p.opacity * w as f32).sum(); + let cmyk: Option> = + q.iter().map(|p| if let Color::Cmyk { c, m, y, k } = p.color { Some([c, m, y, k]) } else { None }).collect(); + if let Some(inks) = cmyk { + let mut mix = [0.0f32; 4]; + for (ink, w) in inks.iter().zip(w) { + for (m, x) in mix.iter_mut().zip(ink) { + *m += x * w as f32; + } + } + let [c, m, y, k] = mix; + return (Color::Cmyk { c, m, y, k }, op); + } + let grey: Option> = q.iter().map(|p| if let Color::Gray { k } = p.color { Some(k) } else { None }).collect(); + if let Some(ks) = grey { + return (Color::Gray { k: ks.iter().zip(w).map(|(k, w)| k * w as f32).sum() }, op); + } + let mut rgb = [0.0f32; 3]; + for (p, w) in q.iter().zip(w) { + for (ch, x) in rgb.iter_mut().zip(p.color.to_rgb()) { + *ch += x * w as f32; + } + } + (Color::rgb(rgb[0], rgb[1], rgb[2]), op) + } + + /// Tessellate into `n`×`n` quads per patch, each coloured at its centre and its corners. + pub fn quads(&self, n: usize) -> Vec { + if !self.is_valid() { + return vec![]; + } + let n = n.clamp(1, 64); + let mut out = Vec::with_capacity(self.rows as usize * self.cols as usize * n * n); + let mut grid = vec![Point::ZERO; (n + 1) * (n + 1)]; + let mut colors = vec![(Color::default(), 0.0f32); (n + 1) * (n + 1)]; + for r in 0..self.rows as usize { + for c in 0..self.cols as usize { + for j in 0..=n { + for i in 0..=n { + let (u, v) = (i as f64 / n as f64, j as f64 / n as f64); + grid[j * (n + 1) + i] = self.eval(r, c, u, v); + colors[j * (n + 1) + i] = self.color_at(r, c, u, v); + } + } + for j in 0..n { + for i in 0..n { + let (color, opacity) = self.color_at(r, c, (i as f64 + 0.5) / n as f64, (j as f64 + 0.5) / n as f64); + let g = |a: usize, b: usize| grid[b * (n + 1) + a]; + let k = |a: usize, b: usize| colors[b * (n + 1) + a]; + out.push(MeshQuad { + pts: [g(i, j), g(i + 1, j), g(i + 1, j + 1), g(i, j + 1)], + color, + opacity, + corners: [k(i, j), k(i + 1, j), k(i + 1, j + 1), k(i, j + 1)], + }); + } + } + } + } + out + } + + /// The mesh's outer boundary as a closed path. + pub fn outline(&self) -> PathData { + if !self.is_valid() { + return PathData::default(); + } + let (rows, cols) = (self.rows as usize, self.cols as usize); + let mut segs: Vec = vec![]; + for c in 0..cols { + segs.push(self.edge_u(0, c)); + } + for r in 0..rows { + segs.push(self.edge_v(r, cols)); + } + for c in (0..cols).rev() { + let e = self.edge_u(rows, c); + segs.push(CubicBez::new(e.p3, e.p2, e.p1, e.p0)); + } + for r in (0..rows).rev() { + let e = self.edge_v(r, 0); + segs.push(CubicBez::new(e.p3, e.p2, e.p1, e.p0)); + } + let mut anchors: Vec = Vec::with_capacity(segs.len()); + for (i, s) in segs.iter().enumerate() { + let prev = &segs[(i + segs.len() - 1) % segs.len()]; + anchors.push(Anchor::with_handles(s.p0, prev.p2, s.p1)); + } + PathData::single(SubPath::new(anchors, true)) + } + + /// Mesh lines (all row and column edges) as open paths, for outline view and editing overlays. + pub fn lines(&self) -> PathData { + if !self.is_valid() { + return PathData::default(); + } + let (rows, cols) = (self.rows as usize, self.cols as usize); + let mut subs = vec![]; + let cub = |e: CubicBez| SubPath::new(vec![Anchor::with_handles(e.p0, e.p0, e.p1), Anchor::with_handles(e.p3, e.p2, e.p3)], false); + for r in 0..=rows { + for c in 0..cols { + subs.push(cub(self.edge_u(r, c))); + } + } + for c in 0..=cols { + for r in 0..rows { + subs.push(cub(self.edge_v(r, c))); + } + } + PathData::new(subs) + } + + /// Control bounds (points and handles). + pub fn bounds(&self) -> Option { + let mut it = self.points.iter().flat_map(|p| std::iter::once(p.p).chain(p.handles.iter().map(move |h| p.p + *h))); + let first = it.next()?; + Some(it.fold(Rect::from_points(first, first), |r, q| r.union_pt(q))) + } + + pub fn transform(&mut self, a: Affine) { + let lin = Affine::new({ + let c = a.as_coeffs(); + [c[0], c[1], c[2], c[3], 0.0, 0.0] + }); + for p in &mut self.points { + p.p = a * p.p; + for h in &mut p.handles { + *h = (lin * h.to_point()).to_vec2(); + } + } + } + + /// Map points (and handle ends) through a non-linear function. + pub fn map(&mut self, f: &dyn Fn(Point) -> Point) { + for p in &mut self.points { + let q = f(p.p); + for h in &mut p.handles { + *h = f(p.p + *h) - q; + } + p.p = q; + } + } + + /// Which patch and local (u, v) contains `p` (sampled search; `None` outside the mesh). + pub fn locate(&self, p: Point) -> Option<(usize, usize, f64, f64)> { + if !self.is_valid() { + return None; + } + let bp = self.outline().to_bezpath(); + if bp.winding(p) == 0 { + return None; + } + const N: usize = 16; + let mut best = (f64::INFINITY, 0, 0, 0.0, 0.0); + for r in 0..self.rows as usize { + for c in 0..self.cols as usize { + for j in 0..=N { + for i in 0..=N { + let (u, v) = (i as f64 / N as f64, j as f64 / N as f64); + let d = self.eval(r, c, u, v).distance_squared(p); + if d < best.0 { + best = (d, r, c, u, v); + } + } + } + } + } + // Refine with a few Newton-free local searches. + let (_, r, c, mut u, mut v) = best; + let mut step = 0.5 / N as f64; + for _ in 0..12 { + let mut improved = false; + for (du, dv) in [(step, 0.0), (-step, 0.0), (0.0, step), (0.0, -step)] { + let (nu, nv) = ((u + du).clamp(0.0, 1.0), (v + dv).clamp(0.0, 1.0)); + if self.eval(r, c, nu, nv).distance_squared(p) < self.eval(r, c, u, v).distance_squared(p) { + u = nu; + v = nv; + improved = true; + } + } + if !improved { + step /= 2.0; + } + } + Some((r, c, u, v)) + } + + /// Index of the mesh point within `tol` of `p`. + pub fn point_near(&self, p: Point, tol: f64) -> Option { + self.points + .iter() + .enumerate() + .filter(|(_, q)| q.p.distance(p) <= tol) + .min_by(|a, b| a.1.p.distance(p).total_cmp(&b.1.p.distance(p))) + .map(|(i, _)| i) + } + + fn surface_du(&self, r: usize, c: usize, u: f64, v: f64) -> Vec2 { + let e = 1e-3; + let (a, b) = ((u - e).max(0.0), (u + e).min(1.0)); + (self.eval(r, c, b, v) - self.eval(r, c, a, v)) / (b - a) + } + fn surface_dv(&self, r: usize, c: usize, u: f64, v: f64) -> Vec2 { + let e = 1e-3; + let (a, b) = ((v - e).max(0.0), (v + e).min(1.0)); + (self.eval(r, c, u, b) - self.eval(r, c, u, a)) / (b - a) + } + + /// Insert a mesh row through patch row `r` at local `v`. + pub fn insert_row(&mut self, r: usize, v: f64) { + let cols = self.cols as usize; + let v = v.clamp(0.01, 0.99); + let mut row = Vec::with_capacity(cols + 1); + // Split the vertical edges. + let mut ups = vec![]; + for c in 0..=cols { + let e = self.edge_v(r, c); + let (l, rr) = (e.subsegment(0.0..v), e.subsegment(v..1.0)); + ups.push((l.p1 - l.p0, rr.p2 - rr.p3)); + let pc = c.min(cols - 1); + let u = if c == cols { 1.0 } else { 0.0 }; + let (color, opacity) = self.color_at(r, pc, u, v); + // Horizontal handles: isocurve derivatives on either side. + let right = if c < cols { self.surface_du(r, c, 0.0, v) / (3.0) } else { Vec2::ZERO }; + let left = if c > 0 { -self.surface_du(r, c - 1, 1.0, v) / 3.0 } else { Vec2::ZERO }; + row.push(MeshPoint { p: l.p3, color, opacity, handles: [right, left, rr.p1 - rr.p0, l.p2 - l.p3] }); + } + for (c, (down, up)) in ups.into_iter().enumerate() { + let i0 = self.idx(r, c); + let i1 = self.idx(r + 1, c); + self.points[i0].handles[H_DOWN] = down; + self.points[i1].handles[H_UP] = up; + } + let at = self.idx(r + 1, 0); + self.points.splice(at..at, row); + self.rows += 1; + } + + /// Insert a mesh column through patch column `c` at local `u`. + pub fn insert_col(&mut self, c: usize, u: f64) { + let rows = self.rows as usize; + let u = u.clamp(0.01, 0.99); + let mut col = Vec::with_capacity(rows + 1); + for r in 0..=rows { + let e = self.edge_u(r, c); + let (l, rr) = (e.subsegment(0.0..u), e.subsegment(u..1.0)); + let pr = r.min(rows - 1); + let v = if r == rows { 1.0 } else { 0.0 }; + let (color, opacity) = self.color_at(pr, c, u, v); + let down = if r < rows { self.surface_dv(r, c, u, 0.0) / 3.0 } else { Vec2::ZERO }; + let up = if r > 0 { -self.surface_dv(r - 1, c, u, 1.0) / 3.0 } else { Vec2::ZERO }; + col.push((MeshPoint { p: l.p3, color, opacity, handles: [rr.p1 - rr.p0, l.p2 - l.p3, down, up] }, l.p1 - l.p0, rr.p2 - rr.p3)); + } + let cols = self.cols as usize; + let mut pts = Vec::with_capacity((rows + 1) * (cols + 2)); + for (r, (mp, right, left)) in col.into_iter().enumerate() { + for cc in 0..=cols { + let mut q = self.points[r * (cols + 1) + cc].clone(); + if cc == c { + q.handles[H_RIGHT] = right; + } + if cc == c + 1 { + q.handles[H_LEFT] = left; + } + pts.push(q); + if cc == c { + pts.push(mp.clone()); + } + } + } + self.points = pts; + self.cols += 1; + } + + /// Mesh tool click: add a row and a column through `p` (inside the mesh). Returns the new point index. + pub fn add_lines_at(&mut self, p: Point) -> Option { + let (r, c, u, v) = self.locate(p)?; + let mut rr = r; + let mut cc = c; + let row_new = v > 0.02 && v < 0.98; + let col_new = u > 0.02 && u < 0.98; + if row_new { + self.insert_row(r, v); + rr = r + 1; + } else if v >= 0.98 { + rr = r + 1; + } + if col_new { + self.insert_col(c, u); + cc = c + 1; + } else if u >= 0.98 { + cc = c + 1; + } + Some(self.idx(rr, cc)) + } + + /// Mesh tool Alt-click: delete the mesh lines through point `index` (interior lines only). + pub fn remove_point_lines(&mut self, index: usize) -> bool { + // A mesh from a file may not have the points its rows and columns need: it is indexed below. + if !self.is_valid() || index >= self.points.len() { + return false; + } + let w = self.cols as usize + 1; + let (r, c) = (index / w, index % w); + let mut changed = false; + if r > 0 && r < self.rows as usize { + // Merge the edges above and below: keep the outer handles. + for cc in 0..w { + let above = self.idx(r - 1, cc); + let below = self.idx(r + 1, cc); + let mid = self.idx(r, cc); + let (pa, pm, pb) = (self.points[above].p, self.points[mid].p, self.points[below].p); + let _ = pm; + self.points[above].handles[H_DOWN] = (pb - pa) / 3.0; + self.points[below].handles[H_UP] = (pa - pb) / 3.0; + } + self.points.drain(r * w..(r + 1) * w); + self.rows -= 1; + changed = true; + } + let w = self.cols as usize + 1; + if c > 0 && c < self.cols as usize { + let rows = self.rows as usize; + for rr in 0..=rows { + let (l, rt) = (rr * w + c - 1, rr * w + c + 1); + let (pl, pr) = (self.points[l].p, self.points[rt].p); + self.points[l].handles[H_RIGHT] = (pr - pl) / 3.0; + self.points[rt].handles[H_LEFT] = (pl - pr) / 3.0; + } + let mut k = 0; + self.points.retain(|_| { + let keep = k % w != c; + k += 1; + keep + }); + self.cols -= 1; + changed = true; + } + changed + } +} + +// ===================================================================================== +// Envelopes +// ===================================================================================== + +/// Union of the geometric bounds of `nodes`. +pub fn nodes_bounds(nodes: &[Arc]) -> Option { + nodes.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.geometric_bounds())) +} + +/// A `(rows+1)×(cols+1)` grid over `r` (row-major). +pub fn grid_points(r: Rect, rows: u32, cols: u32) -> Vec { + let mut v = Vec::with_capacity(((rows + 1) * (cols + 1)) as usize); + for i in 0..=rows { + for j in 0..=cols { + v.push(Point::new(r.x0 + r.width() * j as f64 / cols.max(1) as f64, r.y0 + r.height() * i as f64 / rows.max(1) as f64)); + } + } + v +} + +fn catmull(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f64) -> Vec2 { + let t2 = t * t; + let t3 = t2 * t; + 0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t3) +} + +/// 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, also past the square). +pub(super) 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); + let cc = c.clamp(0, cols as i64); + let base = pts[rr as usize * (cols + 1) + cc as usize].to_vec2(); + // Linear extrapolation outside the grid. + let er = r - rr; + let ec = c - cc; + let mut p = base; + if er != 0 && rows >= 1 { + let inner = (rr - er.signum()).clamp(0, rows as i64); + p += (base - pts[inner as usize * (cols + 1) + cc as usize].to_vec2()) * er.abs() as f64; + } + if ec != 0 && cols >= 1 { + let inner = (cc - ec.signum()).clamp(0, cols as i64); + p += (base - pts[rr as usize * (cols + 1) + inner as usize].to_vec2()) * ec.abs() as f64; + } + p + }; + // 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() { + let r = ri - 1 + k as i64; + *rv = catmull(at(r, ci - 1), at(r, ci), at(r, ci + 1), at(r, ci + 2), fu); + } + catmull(rowsv[0], rowsv[1], rowsv[2], rowsv[3], fv).to_point() +} + +/// 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. +pub(super) fn surface_of<'a>(kind: &'a EnvelopeKind, src: Rect, frame: Affine) -> Box 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) = (src.width() / 2.0, src.height() / 2.0); + let horizontal = *horizontal; + 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, hh / hw.max(1e-9), x, y) + } else { + let (a, b2) = warp_point(st, b, dh, dv, hw / hh.max(1e-9), y, x); + (b2, a) + }; + frame * Point::new(c.x + x2 * hw, c.y + y2 * hh) + }) + } + EnvelopeKind::Mesh { rows, cols, points, handles } => { + if !handles.is_empty() + && let Some(m) = envelope_grid(*rows, *cols, points, handles) + { + return Box::new(move |u: f64, v: f64| mesh_at(&m, u, v)); + } + if *rows == 0 || *cols == 0 || grid_len(*rows, *cols) != Some(points.len()) { + return Box::new(flat); + } + let (rows, cols) = (*rows as usize, *cols as usize); + 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 |u: f64, v: f64| co.eval(u, v)), + None => Box::new(flat), + }, + } +} diff --git a/crates/doc/src/live/warp.rs b/crates/doc/src/live/warp.rs new file mode 100644 index 000000000..cacf26ec5 --- /dev/null +++ b/crates/doc/src/live/warp.rs @@ -0,0 +1,253 @@ +use super::*; + +// ===================================================================================== +// Warp maps (shared with vectorcraft-effects' Warp effects) +// ===================================================================================== + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WarpStyle { + Arc, + ArcLower, + ArcUpper, + Arch, + Bulge, + ShellLower, + ShellUpper, + Flag, + Wave, + Fish, + Rise, + Fisheye, + Inflate, + Squeeze, + Twist, +} + +impl WarpStyle { + /// From the id suffix (`arc`, `arcLower`, …). + pub fn from_id(s: &str) -> Option { + use WarpStyle::*; + Some(match s { + "arc" => Arc, + "arcLower" => ArcLower, + "arcUpper" => ArcUpper, + "arch" => Arch, + "bulge" => Bulge, + "shellLower" => ShellLower, + "shellUpper" => ShellUpper, + "flag" => Flag, + "wave" => Wave, + "fish" => Fish, + "rise" => Rise, + "fisheye" => Fisheye, + "inflate" => Inflate, + "squeeze" => Squeeze, + "twist" => Twist, + _ => return None, + }) + } +} + +/// The warp map on normalised coordinates: `b` = bend (-1..1), `dh`/`dv` = distortion (-1..1); +/// `aspect` is the box's half-height over its half-width along the warp (Arc bends in real +/// proportions). +pub fn warp_point(style: WarpStyle, b: f64, dh: f64, dv: f64, aspect: f64, x: f64, y: f64) -> (f64, f64) { + use WarpStyle::*; + let t = (y + 1.0) / 2.0; // 0 at top, 1 at bottom + let par = 1.0 - x * x; // parabola: 1 at centre, 0 at sides + let (mut x2, mut y2) = match style { + Arc => { + if b.abs() < 1e-6 { + (x, y) + } else { + // Bend the centre line into a circular arc of equal length (sweep = |b|·180°), in + // the box's real proportions (lengths in half-widths, so y counts `aspect` each): + // the edges are concentric arcs, and one that would pass the centre stops there + // instead of folding over (#1578). + let k = if aspect.is_finite() && aspect > 0.0 { aspect } else { 1.0 }; + let sweep = b.abs() * PI; + let r0 = 2.0 / sweep; + let yy = if b > 0.0 { y } else { -y } * k; + let a = x / r0; + let r = (r0 - yy).max(0.0); + let (s, c) = a.sin_cos(); + let (nx, ny) = (r * s, (r0 - r * c) / k); + (nx, if b > 0.0 { ny } else { -ny }) + } + } + ArcLower => (x, y + b * par * t), + ArcUpper => (x, y - b * par * (1.0 - t)), + Arch => (x, y - b * par), + Bulge => (x, y + b * par * y), + ShellLower => (x * (1.0 - 0.5 * b * (1.0 - t)), y + b * par * t), + ShellUpper => (x * (1.0 - 0.5 * b * t), y - b * par * (1.0 - t)), + Flag => (x, y + 0.5 * b * (PI * x).sin()), + Wave => (x + 0.1 * b * (PI * y).sin(), y + 0.3 * b * (2.0 * PI * x).sin() * (0.5 + 0.5 * t)), + Fish => (x, y * (1.0 + 0.5 * b * (0.75 * PI * (x + 1.0)).sin())), + Rise => (x, y - b * ((x + 1.0) / 2.0).powi(2) * 2.0 + b), + Fisheye => { + let r2 = x * x + y * y; + let s = if r2 < 1.0 { 1.0 + 0.5 * b * (1.0 - r2) } else { 1.0 }; + (x * s, y * s) + } + Inflate => (x * (1.0 + 0.5 * b * (1.0 - y * y)), y * (1.0 + 0.5 * b * par)), + Squeeze => (x * (1.0 - 0.5 * b * (1.0 - y * y)), y * (1.0 + 0.3 * b * par)), + Twist => { + let r = (x * x + y * y).sqrt(); + let a = -b * PI * 0.5 * (1.0 - r / std::f64::consts::SQRT_2).max(0.0); + let (s, c) = a.sin_cos(); + (x * c - y * s, x * s + y * c) + } + }; + // Perspective-like distortion: horizontal narrows one side, vertical one end. + if dh != 0.0 { + y2 *= (1.0 + dh * x2 * 0.5).max(0.0); + } + if dv != 0.0 { + x2 *= (1.0 + dv * y2 * 0.5).max(0.0); + } + (x2, y2) +} + +/// Segment as a cubic; straight lines get handles at 1/3 and 2/3 so a non-linear map bends them. +fn seg_cubic(sp: &SubPath, i: usize) -> CubicBez { + let c = sp.segment(i); + if sp.segment_is_line(i) { CubicBez::new(c.p0, c.p0.lerp(c.p3, 1.0 / 3.0), c.p0.lerp(c.p3, 2.0 / 3.0), c.p3) } else { c } +} + +fn poly_len(c: &CubicBez) -> f64 { + c.p0.distance(c.p1) + c.p1.distance(c.p2) + c.p2.distance(c.p3) +} + +/// Running length along a polyline: `0, |p0p1|, |p0p1| + |p1p2|, …` (one entry per point). +fn cumulative_lengths(pts: &[Point]) -> Vec { + let mut total = 0.0; + let mut cum = Vec::with_capacity(pts.len()); + cum.push(0.0); + for w in pts.windows(2) { + total += w[0].distance(w[1]); + cum.push(total); + } + cum +} + +/// Most pieces [`map_nonlinear`] splits one segment into. +const MAX_PIECES: usize = 64; + +/// How many pieces of about `piece` a segment of length `len` is split into: one to `most`. +pub fn pieces(len: f64, piece: f64, most: usize) -> usize { + ((len / piece).ceil() as usize).clamp(1, most.max(1)) +} + +/// The piece length to split segments of lengths `lens` with ([`pieces`], at most `most` each) so +/// that the pieces past each segment's first number at most `room`: `piece` when they already do, +/// longer pieces otherwise. A segment of `n` pieces adds `n - 1 ≤ len / piece` points to a path, +/// so pieces of the total length over `room` fit; with no room every segment stays one piece. +pub fn fit_piece(lens: &[f64], piece: f64, most: usize, room: usize) -> f64 { + let added = lens.iter().map(|&len| pieces(len, piece, most) - 1).fold(0, usize::saturating_add); + if added <= room { + return piece; + } + if room == 0 { + return f64::INFINITY; + } + // A NaN length is always one piece. + piece.max(lens.iter().filter(|len| !len.is_nan()).sum::() / room as f64) +} + +/// Map every point of `path` through the non-linear function `f`. Segments are split into pieces +/// no longer than about `max_piece` (up to 64 per segment) and their control points mapped, which +/// approximates the image curve to O(h²). Closedness and subpath structure are preserved. +pub fn map_nonlinear(path: &PathData, max_piece: f64, f: impl Fn(Point) -> Point) -> PathData { + map_nonlinear_within(path, max_piece, usize::MAX, f) +} + +/// [`map_nonlinear`] whose result has at most `max` anchors, or `path`'s count when that is more: +/// where pieces of `max_piece` would make more, the pieces grow until they don't ([`fit_piece`]), +/// a coarser approximation of the image curve. Geometry effects map within their anchor budget. +pub fn map_nonlinear_within(path: &PathData, max_piece: f64, max: usize, f: impl Fn(Point) -> Point) -> PathData { + let mut max_piece = max_piece.max(1e-3); + // Each segment adds one anchor per piece past its first. + let room = max.saturating_sub(path.anchor_count()); + let segs = path.subpaths.iter().map(SubPath::segment_count).fold(0, usize::saturating_add); + if segs.saturating_mul(MAX_PIECES - 1) > room { + let lens: Vec = path.subpaths.iter().flat_map(|sp| (0..sp.segment_count()).map(|i| poly_len(&seg_cubic(sp, i)))).collect(); + max_piece = fit_piece(&lens, max_piece, MAX_PIECES, room); + } + let mut subs = Vec::with_capacity(path.subpaths.len()); + for sp in &path.subpaths { + let n = sp.anchors.len(); + if n == 0 { + continue; + } + let segs = sp.segment_count(); + let mut res: Vec = Vec::new(); + let mut pending_in: Option = None; + for i in 0..n { + let a = &sp.anchors[i]; + let h_in = pending_in.take().unwrap_or_else(|| f(a.h_in)); + res.push(Anchor { p: f(a.p), h_in, h_out: f(a.h_out), kind: a.kind }); + if i < segs { + let c = seg_cubic(sp, i); + let k = pieces(poly_len(&c), max_piece, MAX_PIECES); + for j in 0..k { + let sub = c.subsegment((j as f64 / k as f64)..((j + 1) as f64 / k as f64)); + if let Some(last) = res.last_mut() { + last.h_out = f(sub.p1); + } + if j + 1 < k { + res.push(Anchor { p: f(sub.p3), h_in: f(sub.p2), h_out: f(sub.p3), kind: Default::default() }); + } else { + pending_in = Some(f(sub.p2)); + } + } + } + } + if let Some(h) = pending_in + && sp.closed + && let Some(first) = res.first_mut() + { + first.h_in = h; + } + let anchors = res.into_iter().map(|a| Anchor::with_handles(a.p, a.h_in, a.h_out)).collect(); + subs.push(SubPath::new(anchors, sp.closed)); + } + PathData::new(subs) +} + +// ===================================================================================== +// Coons patches from outlines +// ===================================================================================== + +pub(super) fn flatten_subpath(sp: &SubPath, tol: f64) -> Vec { + let mut bp = BezPath::new(); + sp.to_bezpath_into(&mut bp); + let mut pts: Vec = Vec::new(); + vectorcraft_geom::kurbo::flatten(bp.iter(), tol, |el| match el { + vectorcraft_geom::PathEl::MoveTo(p) | vectorcraft_geom::PathEl::LineTo(p) if pts.last().is_none_or(|q| q.distance(p) > 1e-9) => pts.push(p), + _ => {} + }); + pts +} + +pub(super) fn resample(poly: &[Point], m: usize) -> Vec { + if poly.is_empty() { + return vec![Point::ZERO; m]; + } + if poly.len() == 1 { + return vec![poly[0]; m]; + } + let cum = cumulative_lengths(poly); + let total = cum.last().copied().unwrap_or(0.0); + let mut out = Vec::with_capacity(m); + let mut i = 0; + for k in 0..m { + let s = if m > 1 { total * k as f64 / (m - 1) as f64 } else { 0.0 }; + while i + 2 < cum.len() && cum[i + 1] < s { + i += 1; + } + let seg = (cum[i + 1] - cum[i]).max(1e-12); + out.push(poly[i].lerp(poly[i + 1], ((s - cum[i]) / seg).clamp(0.0, 1.0))); + } + out +}