mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 21:44:57 +00:00
A Transform effect whose result would pass the geometry budget (2 million anchors or 8 million lines drawn) is left out and the art stays where it was, but the raster effects listed before it still scaled and moved with it: before a Transform effect that scales the art 1000 times, a Drop Shadow's offset grew 1000 times and its blur went to the 1000 pt limit around art that had not moved, and Expand kept those values. The geometry pass now notes each Transform effect that moves the art once and that it leaves out, in the Carry it already returns, and Carry::hide_skipped hides those effects in the list the raster effects are read from. The canvas (a path's own effects and its fills' and strokes'), its bounds, bake (SVG and PDF export), shape art, Expand, Expand Appearance and Outline Stroke use it, and a fill's Transform effect inside a piece no longer scales the effects before it when the budget leaves it out. The budget itself is unchanged. A path drawn as its own art inside a group whose geometry effects run after its members' own read its object effects whole. The reshaping pass now notes a Transform effect that no path in the art took past the budget, and the member's art hides it before its raster effects are read. The bounds skip the extra geometry pass when no raster effect could follow a Transform effect, and painted_reach runs it once for both measurements.
478 lines
23 KiB
Rust
478 lines
23 KiB
Rust
use super::params::{is_geometry, merged_params};
|
|
use super::{
|
|
Appearance, AppearanceItem, BezPath, Effect, FillRule, Node, NodeKind, PathData, Rect, StrokeLayer, Value, WarpStyle, distort, stroke, stylize,
|
|
util, warp,
|
|
};
|
|
use vectorcraft_doc::Document;
|
|
|
|
/// Context the geometry effects may need from the object's appearance.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
pub struct GeomContext<'a> {
|
|
/// The stroke `path.outlineStroke` outlines (its weight, caps, joins, dashes, alignment,
|
|
/// profile and arrowheads): the stroke the effect sits on, else the object's top painted
|
|
/// stroke. `None` outlines a plain 1 pt stroke.
|
|
pub stroke: Option<&'a StrokeLayer>,
|
|
/// The object's fill rule (inside and outside alignment).
|
|
pub rule: FillRule,
|
|
/// The seed of the Transform effect's Random: the object's id, so each object varies its own
|
|
/// way and keeps its result on every redraw.
|
|
pub seed: u64,
|
|
/// The object's appearance, which live-effect plug-ins are given read only (their fills and
|
|
/// strokes).
|
|
pub appearance: Option<&'a Appearance>,
|
|
/// The index in `appearance.items` of the fill or stroke whose own effects these are; `None`:
|
|
/// the object's effects.
|
|
pub item: Option<usize>,
|
|
/// The document that the swatches and patterns named by a live-effect plug-in that paints its
|
|
/// pieces are looked up in. Without one, a result that names any is refused.
|
|
pub doc: Option<&'a Document>,
|
|
/// The object, fill or stroke and effect index a deferred live-effect plug-in is listed by
|
|
/// ([`vectorcraft_plugins::deferred`]). When `None`, it is the object `seed` names, `item` and
|
|
/// the effect's index in the list being applied.
|
|
pub stage: Option<vectorcraft_plugins::deferred::Stage>,
|
|
}
|
|
|
|
impl<'a> GeomContext<'a> {
|
|
/// The context of `n`'s object-level effects.
|
|
pub fn of(n: &'a Node) -> Self {
|
|
let rule = match &n.kind {
|
|
NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. } => *rule,
|
|
_ => FillRule::NonZero,
|
|
};
|
|
let stroke = n.appearance.stroke().filter(|s| !s.paint.is_none() && s.width > 0.0);
|
|
Self { stroke, rule, seed: n.id.0, appearance: Some(&n.appearance), item: None, doc: None, stage: None }
|
|
}
|
|
|
|
/// The context of effect `e`, one of `list`: unless [`Self::stage`] is set, a live-effect
|
|
/// plug-in there is listed by `e`'s index in `list`.
|
|
pub(crate) fn at(self, list: &[Effect], e: &Effect) -> Self {
|
|
let stage = self.stage.or_else(|| {
|
|
let effect = list.iter().position(|x| std::ptr::eq(x, e))?;
|
|
Some(vectorcraft_plugins::deferred::Stage { object: self.seed, effect, item: self.item })
|
|
});
|
|
Self { stage, ..self }
|
|
}
|
|
|
|
/// The context of the effects on `item`, one of the same object's appearance items: a
|
|
/// stroke's own effects outline that stroke, and a plug-in is told which item it is on.
|
|
pub fn item(self, item: &'a AppearanceItem) -> Self {
|
|
let at = self.appearance.and_then(|a| a.items.iter().position(|i| std::ptr::eq(i, item)));
|
|
match item {
|
|
AppearanceItem::Stroke(s) => Self { stroke: Some(s), item: at, ..self },
|
|
AppearanceItem::Fill(_) => Self { item: at, ..self },
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Most anchors a chain of geometry effects makes of one path, or of all the paths of a type,
|
|
/// symbol or image art together; a path that already has more keeps its own count as the limit.
|
|
/// Roughen resamples each segment into up to 2000 points, Zig Zag into up to 101 and the warps
|
|
/// into up to 64, so stacked effects multiply: three Roughens of a rectangle needed tens of
|
|
/// gigabytes (#3578). Each effect builds within it, and one whose result would still pass it is
|
|
/// left out.
|
|
pub(crate) const MAX_EFFECT_ANCHORS: usize = 2_000_000;
|
|
|
|
/// Most lines the renderer is estimated to draw a chain of geometry effects' result with at
|
|
/// 1 px/pt ([`util::line_estimate`]), or the path's own estimate when that is more. A large
|
|
/// Roughen or Zig Zag makes segments thousands of points long, each drawn as up to hundreds of
|
|
/// lines: two million anchors of them came to 141 million lines, 7 GB in the renderer and past
|
|
/// the 2²⁷ lines vello takes of one path (#3578). Art of small curves draws its anchors as a few
|
|
/// lines each, so this rarely binds before [`MAX_EFFECT_ANCHORS`] does.
|
|
pub(crate) const MAX_EFFECT_LINES: usize = 8_000_000;
|
|
|
|
/// What geometry effects may make of a path, or what a path costs: its anchors and the lines the
|
|
/// renderer is estimated to draw it with.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) struct Budget {
|
|
pub anchors: usize,
|
|
pub lines: usize,
|
|
}
|
|
|
|
impl Budget {
|
|
/// The budget of a chain of geometry effects.
|
|
pub(crate) const EFFECTS: Self = Self { anchors: MAX_EFFECT_ANCHORS, lines: MAX_EFFECT_LINES };
|
|
|
|
/// What `path` costs.
|
|
pub(crate) fn of(path: &PathData) -> Self {
|
|
Self { anchors: path.anchor_count(), lines: util::line_estimate(path) }
|
|
}
|
|
|
|
/// This budget, raised to `cost` where that is more.
|
|
pub(crate) fn at_least(self, cost: Self) -> Self {
|
|
Self { anchors: self.anchors.max(cost.anchors), lines: self.lines.max(cost.lines) }
|
|
}
|
|
|
|
/// Does `cost` fit this budget?
|
|
pub(crate) fn fits(self, cost: Self) -> bool {
|
|
cost.anchors <= self.anchors && cost.lines <= self.lines
|
|
}
|
|
|
|
/// What is left of this budget once `cost` is spent.
|
|
pub(crate) fn minus(self, cost: Self) -> Self {
|
|
Self { anchors: self.anchors.saturating_sub(cost.anchors), lines: self.lines.saturating_sub(cost.lines) }
|
|
}
|
|
|
|
/// This cost and `cost` together.
|
|
pub(crate) fn plus(self, cost: Self) -> Self {
|
|
Self { anchors: self.anchors.saturating_add(cost.anchors), lines: self.lines.saturating_add(cost.lines) }
|
|
}
|
|
}
|
|
|
|
/// Evaluate the visible geometry effects of `effects` on `path`, in order. `bounds` is the
|
|
/// reference box of the first effect (usually the object's geometric bounds); each later effect
|
|
/// uses the bounds of its input. Raster and unknown effects are skipped, and so is an effect
|
|
/// whose result would pass 2 million anchors or 8 million lines drawn (or the path's own counts,
|
|
/// when they are more).
|
|
pub fn apply_geometry(effects: &[Effect], path: &PathData, bounds: Rect) -> PathData {
|
|
apply_geometry_with(effects, path, bounds, &GeomContext::default())
|
|
}
|
|
|
|
/// [`apply_geometry`] with an explicit [`GeomContext`].
|
|
pub fn apply_geometry_with(effects: &[Effect], path: &PathData, bounds: Rect, ctx: &GeomContext) -> PathData {
|
|
apply_geometry_within(effects, path, bounds, ctx, Budget::EFFECTS)
|
|
}
|
|
|
|
/// Does effect `e` safely commute with pure translation when it is terminal?
|
|
/// Only positive offsets with round joins commute exactly with pure translations for non-zero fills (#4145).
|
|
fn is_terminal_offset_eligible(e: &Effect) -> bool {
|
|
if e.id != "path.offsetPath" {
|
|
return false;
|
|
}
|
|
let p = merged_params(&e.id, &e.params);
|
|
util::join(&p, "joins") == vectorcraft_pathops::Join::Round && util::num(&p, "offset", 10.0) >= 0.0
|
|
}
|
|
|
|
/// Optimize a list of visible geometry effects: move pure-translation Transform effects with
|
|
/// copies after a terminal Offset Path effect when the effective style is safe (NonZero fill and
|
|
/// no stroke), so that the offset path is evaluated once on the base path rather than on dozens
|
|
/// of overlapping copies, avoiding combinatorial explosions in the planar sweep (#4145).
|
|
pub(crate) fn optimize_geometry_effects<'a>(effects: &[&'a Effect], style_safe: bool) -> Vec<&'a Effect> {
|
|
let mut out: Vec<&'a Effect> = effects.to_vec();
|
|
if !style_safe || out.len() < 2 {
|
|
return out;
|
|
}
|
|
let mut changed = true;
|
|
while changed {
|
|
changed = false;
|
|
for i in 0..out.len().saturating_sub(1) {
|
|
let e1 = out[i];
|
|
if e1.id == "distort.transform" {
|
|
let p1 = merged_params(&e1.id, &e1.params);
|
|
if let Some((_, copies)) = crate::distort::transform_move(&p1)
|
|
&& copies > 0
|
|
&& is_terminal_offset_eligible(out[i + 1])
|
|
&& out.get(i + 2..).is_some_and(|tail| tail.iter().all(|e| e.id == "distort.transform"))
|
|
{
|
|
out.swap(i, i + 1);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// [`apply_geometry_with`] whose effects keep to `budget`, raised to what `path` costs where that
|
|
/// is more.
|
|
pub(crate) fn apply_geometry_within(effects: &[Effect], path: &PathData, bounds: Rect, ctx: &GeomContext, budget: Budget) -> PathData {
|
|
apply_geometry_carrying_within(effects, path, bounds, ctx, budget, None)
|
|
}
|
|
|
|
/// [`apply_geometry_with`], and the [`Carry`] that moves the object's gradients along with the
|
|
/// effects that move the object as a whole.
|
|
pub fn apply_geometry_carrying(effects: &[Effect], path: &PathData, bounds: Rect, ctx: &GeomContext) -> (PathData, Carry) {
|
|
let mut carry = Carry::default();
|
|
let out = apply_geometry_carrying_within(effects, path, bounds, ctx, Budget::EFFECTS, Some(&mut carry));
|
|
(out, carry)
|
|
}
|
|
|
|
/// [`apply_geometry_carrying`] on kurbo paths.
|
|
pub fn apply_geometry_bez_carrying(effects: &[Effect], path: &BezPath, bounds: Rect, ctx: &GeomContext) -> (BezPath, Carry) {
|
|
let (out, carry) = apply_geometry_carrying(effects, &PathData::from_bezpath(path), bounds, ctx);
|
|
(out.to_bezpath(), carry)
|
|
}
|
|
|
|
/// How gradients follow geometry effects. The Transform effect without copies maps placed
|
|
/// gradients as it maps the object (with copies, `crate::reshape` gives each copy its own). Free
|
|
/// Distort and the Warp effects move and scale them from the object's bounds before the effect to
|
|
/// its bounds after it. For the other geometry effects it is empty. A gradient fitted to the
|
|
/// object fits the geometry each effect makes, until a Transform effect turns, reflects or scales
|
|
/// it unevenly (a map after which a refit would not match the mapped gradient): it is fixed to its
|
|
/// fit on the geometry that effect takes, and then follows that effect and the ones after it as a
|
|
/// placed gradient does.
|
|
///
|
|
/// It also lists the visible Transform effects that move the art once and that the pass left out,
|
|
/// their results past the budget, by their index in the effect list it was given: the raster
|
|
/// effects before such an effect stay as they are, as the art does ([`Self::hide_skipped`]).
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct Carry(Vec<CarryStep>, Vec<usize>);
|
|
|
|
/// Does `m` fix a gradient fitted to the object to its fit before mapping it? It does when a refit
|
|
/// would not reproduce the map (a turn, a reflection or an uneven scale) and `m` keeps area: a map
|
|
/// that flattens the geometry to a line (a scale of 0) leaves the gradient fitted, since mapping it
|
|
/// would flatten it to one color ([`vectorcraft_doc::node::flattens`]).
|
|
pub(crate) fn fixes_fit(m: vectorcraft_geom::Affine) -> bool {
|
|
!vectorcraft_doc::node::flattens(m) && !vectorcraft_doc::node::keeps_gradient_fit(m)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
enum CarryStep {
|
|
/// Map `m`; `from` is the geometric bounds of the geometry it takes, which a fitted gradient
|
|
/// fits before `m` moves it (none: fitted gradients stay fitted).
|
|
Affine { m: vectorcraft_geom::Affine, from: Option<Rect> },
|
|
/// From box `from` to box `to` (strokes: their stroke-inflated boxes).
|
|
Boxes { from: Rect, to: Rect },
|
|
}
|
|
|
|
impl Carry {
|
|
/// Does effect `id` with merged params `p` on geometry with reference box `b` (`seed` for
|
|
/// Random) move placed gradients? Free Distort does unless it leaves every corner in place, a
|
|
/// Warp effect unless it has no bend and no distortion, and the Transform effect does without
|
|
/// copies (with copies the original stays where it is, and `crate::reshape` gives each copy
|
|
/// its own gradients).
|
|
pub(crate) fn follows(id: &str, p: &Value, b: Rect, seed: u64) -> bool {
|
|
match id {
|
|
"distort.transform" => {
|
|
let (m, copies) = distort::transform_map(b, p, seed);
|
|
copies == 0 && m != vectorcraft_geom::Affine::IDENTITY && m.is_finite()
|
|
}
|
|
"distort.freeDistort" => {
|
|
let corners = distort::free_distort_corners(b, p);
|
|
let tol = 1e-9 * b.width().hypot(b.height()).max(1.0);
|
|
let still =
|
|
[(b.x0, b.y0), (b.x1, b.y0), (b.x1, b.y1), (b.x0, b.y1)].iter().zip(&corners).all(|(a, c)| c.distance((*a).into()) <= tol);
|
|
!still && corners.iter().all(|c| c.x.is_finite() && c.y.is_finite())
|
|
}
|
|
_ if id.starts_with("warp.") => crate::warp::bends(id, p),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Does effect `id` move placed gradients from each path's bounds before it to its bounds
|
|
/// after it, rather than by one map for the whole object?
|
|
pub(crate) fn by_bounds(id: &str) -> bool {
|
|
id == "distort.freeDistort" || id.starts_with("warp.")
|
|
}
|
|
|
|
/// How placed gradients follow effect `id` (as [`Self::follows`]) on an object whose geometric
|
|
/// bounds are `from` before the effect and `to` after it; empty when they stay in place.
|
|
pub(crate) fn of(id: &str, p: &Value, b: Rect, from: Option<Rect>, to: Option<Rect>, seed: u64) -> Carry {
|
|
if !Self::follows(id, p, b, seed) {
|
|
return Carry::default();
|
|
}
|
|
let step = match id {
|
|
"distort.transform" => Some(CarryStep::Affine { m: distort::transform_map(b, p, seed).0, from }),
|
|
_ => match (from, to) {
|
|
(Some(from), Some(to)) if from != to && [to.x0, to.y0, to.x1, to.y1].iter().all(|v| v.is_finite()) => {
|
|
Some(CarryStep::Boxes { from, to })
|
|
}
|
|
_ => None,
|
|
},
|
|
};
|
|
Carry(step.into_iter().collect(), vec![])
|
|
}
|
|
|
|
/// Does it leave gradients where they are?
|
|
pub fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
|
|
/// Does it move any of the gradients of `ap` (those drawn at a place in document space, a
|
|
/// freeform gradient's own points included)? Callers check first, so an appearance shared with
|
|
/// other objects is not copied for nothing.
|
|
pub fn moves(&self, ap: &Appearance) -> bool {
|
|
!self.0.is_empty() && (ap.has_positioned_gradient() || (self.pin().is_some() && ap.has_unplaced_gradient()))
|
|
}
|
|
|
|
/// The first step that fixes gradients fitted to the object: a map a refit would not
|
|
/// reproduce, on geometry whose bounds are known.
|
|
fn pin(&self) -> Option<usize> {
|
|
self.0.iter().position(|s| matches!(s, CarryStep::Affine { m, from: Some(_) } if fixes_fit(*m)))
|
|
}
|
|
|
|
/// Move the gradients of `ap` along. Placed gradients take every step in turn; fitted ones
|
|
/// are fixed to their fit at the first step [`Self::pin`] finds and take it and the steps
|
|
/// after it.
|
|
pub fn apply(&self, ap: &mut Appearance) {
|
|
if !self.moves(ap) {
|
|
return;
|
|
}
|
|
// The steps move placed gradients only; fitted ones become placed at the pin.
|
|
let pin = self.pin();
|
|
for (k, s) in self.0.iter().enumerate() {
|
|
if Some(k) == pin
|
|
&& let CarryStep::Affine { from: Some(b), .. } = *s
|
|
{
|
|
ap.pin_gradients(b);
|
|
}
|
|
match *s {
|
|
CarryStep::Affine { m, .. } => ap.transform_gradients(m),
|
|
CarryStep::Boxes { from, to } => crate::reshape::follow_boxes(ap, from, to),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn push(&mut self, more: Carry) {
|
|
self.0.extend(more.0);
|
|
self.1.extend(more.1);
|
|
}
|
|
|
|
/// Did the pass leave out a Transform effect that moves the art once ([`Self::hide_skipped`])?
|
|
pub fn skipped(&self) -> bool {
|
|
!self.1.is_empty()
|
|
}
|
|
|
|
/// Hide each Transform effect of `effects`, the list the pass was given, that it left out (its
|
|
/// result past the budget): the art stays where it was, so the raster effects listed before it
|
|
/// do not follow it ([`crate::raster::followed`]). Other effects stay as they are.
|
|
pub fn hide_skipped(&self, effects: &mut [Effect]) {
|
|
for &i in &self.1 {
|
|
if let Some(e) = effects.get_mut(i).filter(|e| e.id == "distort.transform") {
|
|
e.visible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Note that the Transform effect at index `i` of the pass's list was left out.
|
|
pub(crate) fn skip(&mut self, i: usize) {
|
|
if !self.1.contains(&i) {
|
|
self.1.push(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// [`apply_geometry_within`], adding to `carry` how gradients follow each effect.
|
|
pub(crate) fn apply_geometry_carrying_within(
|
|
effects: &[Effect],
|
|
path: &PathData,
|
|
bounds: Rect,
|
|
ctx: &GeomContext,
|
|
budget: Budget,
|
|
mut carry: Option<&mut Carry>,
|
|
) -> PathData {
|
|
// `budget` raised to what `path` costs, measured once, and only for a path that has geometry
|
|
// effects.
|
|
let has_stroke = match (ctx.item, ctx.appearance) {
|
|
(Some(idx), Some(app)) => match app.items.get(idx) {
|
|
Some(AppearanceItem::Stroke(s)) => !s.paint.is_none() && s.width > 0.0,
|
|
Some(AppearanceItem::Fill(_)) => false,
|
|
None => ctx.stroke.is_some_and(|s| !s.paint.is_none() && s.width > 0.0),
|
|
},
|
|
_ => ctx.stroke.is_some_and(|s| !s.paint.is_none() && s.width > 0.0),
|
|
};
|
|
let style_safe = ctx.rule == FillRule::NonZero && !has_stroke;
|
|
let visible_fx: Vec<&Effect> = effects.iter().filter(|e| e.visible && is_geometry(&e.id)).collect();
|
|
let fx = optimize_geometry_effects(&visible_fx, style_safe);
|
|
let mut limit = None;
|
|
let mut cur = path.clone();
|
|
let mut b = bounds;
|
|
let mut first = true;
|
|
for e in fx {
|
|
if !first {
|
|
b = cur.bounds().unwrap_or(b);
|
|
}
|
|
first = false;
|
|
if cur.is_empty() {
|
|
break;
|
|
}
|
|
let p = merged_params(&e.id, &e.params);
|
|
let limit = *limit.get_or_insert_with(|| budget.at_least(Budget::of(path)));
|
|
if let Some(next) = apply_one(&e.id, &p, &cur, b, &ctx.at(effects, e), limit) {
|
|
if let Some(c) = carry.as_deref_mut() {
|
|
c.push(Carry::of(&e.id, &p, b, cur.bounds(), next.bounds(), ctx.seed));
|
|
}
|
|
cur = next;
|
|
} else if let Some(c) = carry.as_deref_mut()
|
|
&& e.id == "distort.transform"
|
|
&& !crate::raster::holds_back(e)
|
|
&& let Some(i) = effects.iter().position(|x| std::ptr::eq(x, e))
|
|
{
|
|
// The raster effects before it stay as the art does.
|
|
c.skip(i);
|
|
}
|
|
}
|
|
cur
|
|
}
|
|
|
|
/// Convenience wrapper on kurbo paths.
|
|
pub fn apply_geometry_bez(effects: &[Effect], path: &BezPath, bounds: Rect, ctx: &GeomContext) -> BezPath {
|
|
apply_geometry_with(effects, &PathData::from_bezpath(path), bounds, ctx).to_bezpath()
|
|
}
|
|
|
|
/// Geometry effect `id` with params `p` on `path`, whose reference box is `b`; `None` when its
|
|
/// result would not fit `budget` (the effects that multiply anchors build within its anchors).
|
|
pub(crate) fn apply_one(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &GeomContext, budget: Budget) -> Option<PathData> {
|
|
let out = apply_unchecked(id, p, path, b, ctx, budget.anchors);
|
|
let cost = Budget::of(&out);
|
|
if budget.fits(cost) {
|
|
return Some(out);
|
|
}
|
|
if cost.lines <= budget.lines || !builds_within(id) {
|
|
return None;
|
|
}
|
|
// Anchors drawn as many lines each (a large Roughen or Zig Zag): once more with as many fewer
|
|
// anchors as there were lines too many, and a tenth to spare, as segments vary in their lines.
|
|
let fewer = (cost.anchors as f64 * 0.9 * (budget.lines as f64 / cost.lines as f64)) as usize;
|
|
let out = apply_unchecked(id, p, path, b, ctx, fewer.min(budget.anchors));
|
|
budget.fits(Budget::of(&out)).then_some(out)
|
|
}
|
|
|
|
/// Does geometry effect `id` build within the anchors it is given?
|
|
fn builds_within(id: &str) -> bool {
|
|
matches!(id, "distort.freeDistort" | "distort.roughen" | "distort.transform" | "distort.twist" | "distort.zigZag") || id.starts_with("warp.")
|
|
}
|
|
|
|
/// [`apply_one`] whose result may pass `max` when the effect doesn't build within it.
|
|
fn apply_unchecked(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &GeomContext, max: usize) -> PathData {
|
|
use util::{join, num};
|
|
if let Some(plugin) = vectorcraft_plugins::effect::plugin_id(id) {
|
|
let paints = vectorcraft_plugins::effect::Paints { appearance: ctx.appearance, item: ctx.item, at: ctx.stage };
|
|
return vectorcraft_plugins::effect::apply(plugin, p, path, b, ctx.rule, paints, ctx.doc).unwrap_or_else(|| path.clone());
|
|
}
|
|
match id {
|
|
"distort.freeDistort" => distort::free_distort(path, b, p, max),
|
|
"distort.puckerBloat" => distort::pucker_bloat(path, b, num(p, "amount", 0.0)),
|
|
"distort.roughen" => distort::roughen(path, b, p, max),
|
|
"distort.transform" => distort::transform_within(path, b, p, ctx.seed, max),
|
|
"distort.tweak" => distort::tweak(path, b, p),
|
|
"distort.twist" => distort::twist(path, b, num(p, "angle", 10.0), max),
|
|
"distort.zigZag" => distort::zig_zag(path, b, p, max),
|
|
"path.offsetPath" => {
|
|
let off = num(p, "offset", 10.0).clamp(-1e5, 1e5);
|
|
if off.abs() < 1e-9 {
|
|
return path.clone();
|
|
}
|
|
vectorcraft_pathops::offset_path(path, off, join(p, "joins"), num(p, "miterLimit", 4.0).clamp(1.0, 500.0))
|
|
}
|
|
// Outline Object returns the geometry unchanged. Type reaches every geometry effect as its
|
|
// glyph outlines (`reshape::reshape`), so type that carries it is drawn as those outlines.
|
|
"path.outlineObject" => path.clone(),
|
|
"path.outlineStroke" => {
|
|
let fallback;
|
|
let base = match ctx.stroke {
|
|
Some(s) => s,
|
|
None => {
|
|
fallback = StrokeLayer::new(vectorcraft_doc::color::Paint::None, 1.0);
|
|
&fallback
|
|
}
|
|
};
|
|
let st = match p.get("width").and_then(Value::as_f64) {
|
|
Some(w) => std::borrow::Cow::Owned(StrokeLayer { width: w, ..base.clone() }),
|
|
None => std::borrow::Cow::Borrowed(base),
|
|
};
|
|
if st.width.is_nan() || st.width <= 0.0 {
|
|
return path.clone();
|
|
}
|
|
stroke::outline_region(path, ctx.rule, &st)
|
|
}
|
|
"convertToShape.rectangle" | "convertToShape.roundedRectangle" | "convertToShape.ellipse" => stylize::convert_to_shape(id, b, p),
|
|
"stylize.roundCorners" => stylize::round_corners(path, num(p, "radius", 10.0)),
|
|
"stylize.scribble" => stylize::scribble(path, b, p),
|
|
_ if id.starts_with(crate::addons::path::PREFIX) => crate::addons::path::apply(id, p, path, max),
|
|
_ => match id.strip_prefix("warp.").and_then(WarpStyle::from_id) {
|
|
Some(style) => warp::warp(path, b, style, p, max),
|
|
None => path.clone(),
|
|
},
|
|
}
|
|
}
|