@pixerate/editor 0.99.0 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,12 +14,19 @@ import {
14
14
  import {
15
15
  KAPPA,
16
16
  VECTOR_ARROWHEADS,
17
+ VECTOR_BLEND_MODES,
18
+ VECTOR_BLEND_MODE_LABELS,
17
19
  VECTOR_BLEND_SPACINGS,
18
20
  VECTOR_CORNER_KINDS,
19
21
  VECTOR_EFFECT_DEFAULTS,
20
22
  VECTOR_EFFECT_LABELS,
21
23
  VECTOR_EFFECT_PARAM_SPECS,
22
24
  VECTOR_EFFECT_TYPES,
25
+ VECTOR_FILTER_DEFAULTS,
26
+ VECTOR_FILTER_LABELS,
27
+ VECTOR_FILTER_MENU,
28
+ VECTOR_FILTER_PARAM_SPECS,
29
+ VECTOR_FILTER_TYPES,
23
30
  VECTOR_GRADIENT_INTERPOLATIONS,
24
31
  VECTOR_GRADIENT_MAX_PERIODS,
25
32
  VECTOR_GRADIENT_MIN_STOPS,
@@ -29,6 +36,7 @@ import {
29
36
  VECTOR_PATHFINDER_OPERATIONS,
30
37
  VECTOR_PRESSURE_NOMINAL,
31
38
  VECTOR_STROKE_GRADIENT_MODES,
39
+ VECTOR_STROKE_SETTING_KEYS,
32
40
  VECTOR_TOOLS,
33
41
  VECTOR_TRANSFORM_HANDLES,
34
42
  VECTOR_WIDTH_PRESETS,
@@ -37,6 +45,7 @@ import {
37
45
  VECTOR_WIDTH_PROFILE_MAX_SCALE,
38
46
  VECTOR_WIDTH_SAME_T,
39
47
  activeVectorEffects,
48
+ activeVectorFilters,
40
49
  applyVectorCornersAttribute,
41
50
  applyVectorEffects,
42
51
  applyVectorGradientPatch,
@@ -60,6 +69,7 @@ import {
60
69
  cornerSetback,
61
70
  correspondMorphContours,
62
71
  createVectorEffect,
72
+ createVectorFilter,
63
73
  createVectorMorph,
64
74
  dashIntervals,
65
75
  defaultVectorGradient,
@@ -86,8 +96,11 @@ import {
86
96
  hasStrokeExtras,
87
97
  hasVectorAppearance,
88
98
  hasVectorCorners,
99
+ hasVectorPaintStacks,
89
100
  hitTestVectorNode,
90
101
  hitTestVectorNodes,
102
+ hitTestVectorPaintLayer,
103
+ hitTestVectorPaintStack,
91
104
  hitTestVectorStroke,
92
105
  inferAnchorType,
93
106
  insertAnchor,
@@ -102,7 +115,11 @@ import {
102
115
  interpolateWidthProfiles,
103
116
  isCentredVectorGradient,
104
117
  isClosedBezierShape,
118
+ isDrawnVectorPaintLayer,
119
+ isPlainVectorPaintLayer,
105
120
  isUniformWidthProfile,
121
+ isVectorBlendMode,
122
+ isVectorFilterType,
106
123
  isVectorGradient,
107
124
  isVectorLiveBoolean,
108
125
  lineSubpath,
@@ -121,9 +138,13 @@ import {
121
138
  moveVectorGradientStop,
122
139
  nearestSegment,
123
140
  nearestStrokePosition,
141
+ needsVectorPaintLayers,
142
+ newVectorStrokeLayer,
124
143
  nextVectorCornerKind,
125
144
  nodeMatrix,
126
145
  normalizeVectorEffectParams,
146
+ normalizeVectorFilterParams,
147
+ normalizeVectorPaintStacks,
127
148
  offsetPaths,
128
149
  parseSvgTransform,
129
150
  parseVectorColor,
@@ -153,7 +174,11 @@ import {
153
174
  sampleVectorGradientAt,
154
175
  sampleVectorPaintDefAt,
155
176
  sanitizeVectorEffects,
177
+ sanitizeVectorFills,
178
+ sanitizeVectorFilters,
156
179
  sanitizeVectorGradient,
180
+ sanitizeVectorStrokeSettings,
181
+ sanitizeVectorStrokes,
157
182
  sanitizeWidthProfile,
158
183
  setAnchorCorner,
159
184
  setSubpathCorners,
@@ -185,6 +210,8 @@ import {
185
210
  vectorAppearanceNodes,
186
211
  vectorAppearanceOutline,
187
212
  vectorAppearanceShapes,
213
+ vectorBlendModeCss,
214
+ vectorBlendModeFromCss,
188
215
  vectorBoxMatrix,
189
216
  vectorCornerAt,
190
217
  vectorCorners,
@@ -196,6 +223,11 @@ import {
196
223
  vectorEnvelopeFrame,
197
224
  vectorEnvelopeMap,
198
225
  vectorEnvelopeMesh,
226
+ vectorFills,
227
+ vectorFilterMarkup,
228
+ vectorFilterParamKeys,
229
+ vectorFilterReach,
230
+ vectorFilterSvg,
199
231
  vectorGradientArea,
200
232
  vectorGradientAspect,
201
233
  vectorGradientFocalParam,
@@ -215,22 +247,30 @@ import {
215
247
  vectorMeshOutline,
216
248
  vectorNodeBounds,
217
249
  vectorNodeEnvelope,
250
+ vectorNodeFilterSvg,
251
+ vectorNodeLocalPaintBounds,
218
252
  vectorNodeRotation,
219
253
  vectorNodeToBezier,
220
254
  vectorOperandOperation,
255
+ vectorPaintLayerNodes,
221
256
  vectorPaintToSvg,
222
257
  vectorParentMatrix,
223
258
  vectorPathData,
224
259
  vectorPathLocalBounds,
225
260
  vectorPathWorldBezier,
226
261
  vectorRepeaterCopies,
262
+ vectorStacksHaveGradient,
227
263
  vectorStrokeGradientDefs,
228
264
  vectorStrokeLineArea,
229
265
  vectorStrokeOutline,
230
266
  vectorStrokeOutlinePath,
231
267
  vectorStrokeParts,
232
268
  vectorStrokeReach,
269
+ vectorStrokeSettings,
270
+ vectorStrokeStackReach,
233
271
  vectorStrokeSvgParts,
272
+ vectorStrokes,
273
+ vectorTargetPaint,
234
274
  vectorWorldMatrix,
235
275
  walkVectorNodes,
236
276
  widthProfileAt,
@@ -240,8 +280,11 @@ import {
240
280
  widthProfilePresetId,
241
281
  widthProfileToAttribute,
242
282
  wigglePaths,
283
+ withVectorFills,
284
+ withVectorStrokes,
285
+ withVectorTargetPaint,
243
286
  zigzagPaths
244
- } from "../chunk-YGZNGDIT.js";
287
+ } from "../chunk-63EFQCT5.js";
245
288
  import {
246
289
  bezierPathBounds,
247
290
  bezierPathToSvg,
@@ -322,19 +365,19 @@ var HANDLE_HIT = 6;
322
365
  var BAR_HIT = 5;
323
366
  var fmt = (n) => Math.round(n * 100) / 100;
324
367
  var apply = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
325
- function vectorGradientTargetPath(doc, selection, target) {
368
+ function vectorGradientTargetPath(doc, selection, target, layer = null) {
326
369
  for (const id of selection) {
327
370
  const node = findVectorNode(doc.nodes, id);
328
371
  if (!node) continue;
329
- const path = collectPathNodes(node).find((p) => isVectorGradient(p[target]));
372
+ const path = collectPathNodes(node).find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));
330
373
  if (path) return path;
331
374
  }
332
375
  return null;
333
376
  }
334
- function getVectorGradientAnnotator(doc, selection, viewport, target) {
335
- const path = vectorGradientTargetPath(doc, selection, target);
377
+ function getVectorGradientAnnotator(doc, selection, viewport, target, layer = null) {
378
+ const path = vectorGradientTargetPath(doc, selection, target, layer);
336
379
  if (!path) return null;
337
- const gradient = path[target];
380
+ const gradient = vectorTargetPaint(path, target, layer);
338
381
  const world = vectorWorldMatrix(doc.nodes, path.id) ?? IDENTITY_MATRIX;
339
382
  const screen = multiplyMatrices(world, [viewport.zoom, 0, 0, viewport.zoom, viewport.x, viewport.y]);
340
383
  const bounds = vectorPathLocalBounds(path);
@@ -2655,7 +2698,9 @@ var VECTOR_STYLE_KEYS = [
2655
2698
  "arrowAlign",
2656
2699
  "startArrowScale",
2657
2700
  "endArrowScale",
2658
- "strokeWidthProfile"
2701
+ "strokeWidthProfile",
2702
+ "fills",
2703
+ "strokes"
2659
2704
  ];
2660
2705
  var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
2661
2706
  var DEFAULT_DEFAULTS = {
@@ -2975,6 +3020,24 @@ var VECTOR_PATHFINDER_LABELS = {
2975
3020
  outline: "Outline",
2976
3021
  minusBack: "Minus Back"
2977
3022
  };
3023
+ function normalizeVectorTree(nodes, prefer) {
3024
+ let changed = false;
3025
+ const out = nodes.map((n) => {
3026
+ let next = n.type === "path" ? normalizeVectorPaintStacks(n, prefer) : n;
3027
+ if (next.type === "group") {
3028
+ const children = normalizeVectorTree(next.children, prefer);
3029
+ if (children !== next.children) next = { ...next, children };
3030
+ }
3031
+ if (next !== n) changed = true;
3032
+ return next;
3033
+ });
3034
+ return changed ? out : nodes;
3035
+ }
3036
+ function loadedDocument(doc) {
3037
+ const copy = cloneVectorDocument(doc);
3038
+ const nodes = normalizeVectorTree(copy.nodes, "stack");
3039
+ return nodes === copy.nodes ? copy : { ...copy, nodes };
3040
+ }
2978
3041
  function mapTree(nodes, fn) {
2979
3042
  let changed = false;
2980
3043
  const out = [];
@@ -3054,7 +3117,7 @@ var VectorEditorController = class {
3054
3117
  blendStart = null;
3055
3118
  /** Options new blends get (the toolbars edit them along with the selected blend's). */
3056
3119
  blendDefaults = {};
3057
- gradientEdit = { target: "fill", stop: null };
3120
+ gradientEdit = { target: "fill", stop: null, layer: null };
3058
3121
  freeDistort = false;
3059
3122
  /** What the highlight shows, to skip notifying when the pointer stays over the same face. */
3060
3123
  builderHoverKey = null;
@@ -3069,7 +3132,7 @@ var VectorEditorController = class {
3069
3132
  state;
3070
3133
  constructor(options = {}) {
3071
3134
  this.history = options.history ?? new HistoryManager();
3072
- this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
3135
+ this.doc = options.document ? loadedDocument(options.document) : { width: 800, height: 600, nodes: [] };
3073
3136
  this.doc = this.refreshed(this.doc);
3074
3137
  this.tool = options.tool ?? "select";
3075
3138
  this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
@@ -3161,7 +3224,7 @@ var VectorEditorController = class {
3161
3224
  }
3162
3225
  /** `doc` with its blends' steps brought up to date (the same object when they already are). */
3163
3226
  refreshed(doc) {
3164
- const nodes = refreshVectorBlends(doc.nodes);
3227
+ const nodes = refreshVectorBlends(normalizeVectorTree(doc.nodes, "shorthand"));
3165
3228
  return nodes === doc.nodes ? doc : { ...doc, nodes };
3166
3229
  }
3167
3230
  notify(docChanged = false) {
@@ -3480,7 +3543,7 @@ var VectorEditorController = class {
3480
3543
  /** Replaces the document. Undoable unless `{ history: false }` (then history is cleared). */
3481
3544
  setDocument(doc, options = {}) {
3482
3545
  this.cancelDrag();
3483
- const next = cloneVectorDocument(doc);
3546
+ const next = loadedDocument(doc);
3484
3547
  if (options.history === false) {
3485
3548
  this.doc = next;
3486
3549
  this.selection = [];
@@ -3594,7 +3657,7 @@ var VectorEditorController = class {
3594
3657
  for (const key of VECTOR_STYLE_KEYS) {
3595
3658
  if (input[key] !== void 0) own[key] = input[key];
3596
3659
  }
3597
- return { id: this.newId(), type: "path", name: input.name ?? names[input.kind], ...style, ...own, subpaths: [subpath] };
3660
+ return withStyle({ id: this.newId(), type: "path", name: input.name ?? names[input.kind], ...style, subpaths: [subpath] }, own);
3598
3661
  }
3599
3662
  /** Adds a shape drawn with the current defaults. Returns its id. */
3600
3663
  addShape(input, options = {}) {
@@ -3604,7 +3667,8 @@ var VectorEditorController = class {
3604
3667
  /** Adds a path from SVG path data. Returns its id. */
3605
3668
  addPath(d, style = {}, options = {}) {
3606
3669
  const subpaths = bezierToSubpaths(d);
3607
- const node = { id: this.newId(), type: "path", name: style.name ?? "Path", ...this.styleFor(false), ...style, subpaths };
3670
+ const { name, ...own } = style;
3671
+ const node = withStyle({ id: this.newId(), type: "path", name: name ?? "Path", ...this.styleFor(false), subpaths }, own);
3608
3672
  return this.addNode(node, { label: "Add Path", ...options });
3609
3673
  }
3610
3674
  styleFor(open) {
@@ -3617,7 +3681,10 @@ var VectorEditorController = class {
3617
3681
  if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
3618
3682
  for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
3619
3683
  if (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));
3620
- return out;
3684
+ let styled = out;
3685
+ if (Array.isArray(d.fills)) styled = withVectorFills(styled, open ? [] : d.fills);
3686
+ if (Array.isArray(d.strokes)) styled = withVectorStrokes(styled, d.strokes);
3687
+ return styled;
3621
3688
  }
3622
3689
  /** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
3623
3690
  updateNode(id, patch, options = {}) {
@@ -3666,9 +3733,9 @@ var VectorEditorController = class {
3666
3733
  // Gradients
3667
3734
  // -------------------------------------------------------------------------
3668
3735
  /** Which paint (fill or stroke) the gradient tool and editor work on. */
3669
- setGradientTarget(target) {
3670
- if (target === this.gradientEdit.target) return;
3671
- this.gradientEdit = { target, stop: null };
3736
+ setGradientTarget(target, layer = null) {
3737
+ if (target === this.gradientEdit.target && layer === this.gradientEdit.layer) return;
3738
+ this.gradientEdit = { target, stop: null, layer };
3672
3739
  this.notify();
3673
3740
  }
3674
3741
  /** Selects a stop of the gradient being edited (index into its sorted stops), or none. */
@@ -3679,7 +3746,7 @@ var VectorEditorController = class {
3679
3746
  }
3680
3747
  /** The gradient tool's annotator for the selection (screen pixels), or null when its paint is not a gradient. */
3681
3748
  getGradientAnnotator() {
3682
- return getVectorGradientAnnotator(this.doc, this.selection, this.viewport, this.gradientEdit.target);
3749
+ return getVectorGradientAnnotator(this.doc, this.selection, this.viewport, this.gradientEdit.target, this.gradientEdit.layer);
3683
3750
  }
3684
3751
  /** Unlocked paths of `ids` (default: the selection). */
3685
3752
  gradientPaths(ids = this.selection) {
@@ -3699,32 +3766,35 @@ var VectorEditorController = class {
3699
3766
  */
3700
3767
  setGradient(patch, options = {}) {
3701
3768
  const target = options.target ?? this.gradientEdit.target;
3769
+ const layer = options.layer !== void 0 ? options.layer : options.target === void 0 ? this.gradientEdit.layer : null;
3702
3770
  const paths = this.gradientPaths(options.ids ?? this.selection);
3703
3771
  if (!paths.length) return { ok: false, message: "Nothing is selected." };
3704
3772
  const next = /* @__PURE__ */ new Map();
3773
+ let first = null;
3705
3774
  for (const path of paths) {
3706
- const paint = path[target];
3775
+ const paint = vectorTargetPaint(path, target, layer);
3707
3776
  const base = isVectorGradient(paint) ? paint : defaultVectorGradient(patch.type ?? "linear", paint);
3708
3777
  const g = applyVectorGradientPatch(base, patch);
3709
3778
  if (!g) return { ok: false, message: "That is not a valid gradient: it needs at least one stop with an offset and a colour." };
3710
- next.set(path.id, { ...path, [target]: g });
3779
+ first ??= g;
3780
+ next.set(path.id, withVectorTargetPaint(path, target, layer, g));
3711
3781
  }
3712
3782
  this.change(options.label ?? "Gradient", () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), options.merge);
3713
- const stops = next.values().next().value?.[target]?.stops.length ?? 0;
3783
+ const stops = first?.stops.length ?? 0;
3714
3784
  if (this.gradientEdit.stop !== null && this.gradientEdit.stop >= stops) this.gradientEdit = { ...this.gradientEdit, stop: null };
3715
3785
  return { ok: true };
3716
3786
  }
3717
3787
  /** Applies `fn` to every target gradient of the selection (paths whose paint is not a gradient are skipped). */
3718
3788
  editGradients(label, fn, merge) {
3719
- const target = this.gradientEdit.target;
3720
- const paths = this.gradientPaths().filter((p) => isVectorGradient(p[target]));
3789
+ const { target, layer } = this.gradientEdit;
3790
+ const paths = this.gradientPaths().filter((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));
3721
3791
  if (!paths.length) return { ok: false, message: "The selection has no gradient." };
3722
3792
  const next = /* @__PURE__ */ new Map();
3723
3793
  for (const path of paths) {
3724
- const g = path[target];
3794
+ const g = vectorTargetPaint(path, target, layer);
3725
3795
  const stops = fn(sortedVectorGradientStops(g));
3726
3796
  if (!stops) return { ok: false, message: "A gradient keeps at least two stops." };
3727
- next.set(path.id, { ...path, [target]: { ...g, stops } });
3797
+ next.set(path.id, withVectorTargetPaint(path, target, layer, { ...g, stops }));
3728
3798
  }
3729
3799
  this.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);
3730
3800
  return { ok: true };
@@ -3763,10 +3833,10 @@ var VectorEditorController = class {
3763
3833
  /** Adds a stop at `offset` (painting what the gradient paints there) to the selection's gradients and selects it. */
3764
3834
  addGradientStop(offset) {
3765
3835
  let index = 0;
3766
- const target = this.gradientEdit.target;
3836
+ const { target, layer } = this.gradientEdit;
3767
3837
  const result = this.editGradients("Add Gradient Stop", (stops) => {
3768
- const path = this.gradientPaths().find((p) => isVectorGradient(p[target]));
3769
- const r = insertVectorGradientStop({ ...path[target], stops }, offset);
3838
+ const path = this.gradientPaths().find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));
3839
+ const r = insertVectorGradientStop({ ...vectorTargetPaint(path, target, layer), stops }, offset);
3770
3840
  index = r.index;
3771
3841
  return r.stops;
3772
3842
  });
@@ -3785,7 +3855,8 @@ var VectorEditorController = class {
3785
3855
  }
3786
3856
  /** `doc` with path `id`'s `target` paint replaced. */
3787
3857
  withGradient(doc, id, target, g) {
3788
- return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === id ? { ...n, [target]: g } : n) };
3858
+ const layer = this.gradientEdit.layer;
3859
+ return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === id && n.type === "path" ? withVectorTargetPaint(n, target, layer, g) : n) };
3789
3860
  }
3790
3861
  downGradient(screen, p, input) {
3791
3862
  const before = this.snapshot();
@@ -3830,7 +3901,7 @@ var VectorEditorController = class {
3830
3901
  for (const id2 of drag.targets) {
3831
3902
  const node2 = findVectorNode(base.nodes, id2);
3832
3903
  if (!node2) continue;
3833
- const paint = node2[target];
3904
+ const paint = vectorTargetPaint(node2, target, this.gradientEdit.layer);
3834
3905
  const g2 = isVectorGradient(paint) ? paint : defaultVectorGradient("linear", paint);
3835
3906
  doc = this.withGradient(doc, id2, target, setVectorGradientVector(g2, vectorPathLocalBounds(node2), this.toLocal(id2, drag.startDoc, base), this.toLocal(id2, end, base)));
3836
3907
  }
@@ -3840,7 +3911,7 @@ var VectorEditorController = class {
3840
3911
  }
3841
3912
  const id = drag.nodeId;
3842
3913
  const node = findVectorNode(base.nodes, id);
3843
- const g = node[target];
3914
+ const g = vectorTargetPaint(node, target, this.gradientEdit.layer);
3844
3915
  const bounds = vectorPathLocalBounds(node);
3845
3916
  const local = (q) => this.toLocal(id, q, base);
3846
3917
  const part = drag.part;
@@ -4067,39 +4138,35 @@ var VectorEditorController = class {
4067
4138
  */
4068
4139
  outlineStroke(options = {}) {
4069
4140
  const roots = options.ids ?? this.selectedRoots();
4141
+ const strokesOf = (p) => vectorPaintLayerNodes(p).filter((l) => l.kind === "stroke" && l.drawn && (l.node.strokeWidth ?? 1) > 0);
4070
4142
  const paths = [];
4071
4143
  for (const id of roots) {
4072
4144
  const node = this.getNode(id);
4073
4145
  if (node) {
4074
- for (const p of collectPathNodes(node)) if (!p.locked && hasPaint(p.stroke) && (p.strokeWidth ?? 1) > 0) paths.push(p);
4146
+ for (const p of collectPathNodes(node)) if (!p.locked && strokesOf(p).length) paths.push(p);
4075
4147
  }
4076
4148
  }
4077
4149
  if (!paths.length) return [];
4078
4150
  const created = [];
4079
4151
  const replace = /* @__PURE__ */ new Map();
4080
4152
  for (const p of paths) {
4081
- const outline = bezierToSubpaths(vectorStrokeOutlinePath(vectorAppearanceOutline(p), p));
4082
- if (!outline.length) continue;
4083
- const {
4084
- stroke,
4085
- strokeWidth: _w,
4086
- strokeLinecap: _c,
4087
- strokeLinejoin: _j,
4088
- strokeDasharray: _d,
4089
- strokeAlign: _a,
4090
- startArrow: _s,
4091
- endArrow: _e,
4092
- arrowAlign: _al,
4093
- startArrowScale: _ss,
4094
- endArrowScale: _es,
4095
- strokeWidthProfile: _wp,
4096
- effects,
4097
- ...rest
4098
- } = cloneVectorNode(p);
4099
- const id = this.newId();
4100
- const strokeNode = { ...rest, id, name: `${p.name ?? "Path"} Stroke`, subpaths: outline, fill: stroke ?? null, stroke: null, fillRule: "nonzero" };
4101
- const list = hasPaint(p.fill) ? [{ ...rest, ...effects ? { effects } : {}, stroke: null }, strokeNode] : [strokeNode];
4102
- created.push(id);
4153
+ const { effects, ...rest } = cloneVectorNode(p);
4154
+ const unstroked = withVectorStrokes(rest, []);
4155
+ for (const key of VECTOR_STROKE_SETTING_KEYS) delete unstroked[key];
4156
+ const list = vectorFills(p).length ? [{ ...unstroked, ...effects ? { effects } : {} }] : [];
4157
+ const layers = strokesOf(p);
4158
+ for (const l of layers) {
4159
+ const outline = bezierToSubpaths(vectorStrokeOutlinePath(vectorAppearanceOutline(p), l.node));
4160
+ if (!outline.length) continue;
4161
+ const id = this.newId();
4162
+ const { strokes: _s, fills: _f, ...plain2 } = unstroked;
4163
+ const name = layers.length > 1 ? `${p.name ?? "Path"} Stroke ${l.index + 1}` : `${p.name ?? "Path"} Stroke`;
4164
+ const fill = { paint: l.layer.paint };
4165
+ if (l.layer.opacity !== void 0) fill.opacity = l.layer.opacity;
4166
+ if (l.layer.blendMode) fill.blendMode = l.layer.blendMode;
4167
+ list.push(withVectorFills({ ...plain2, id, name, subpaths: outline, stroke: null, fillRule: "nonzero" }, [fill]));
4168
+ created.push(id);
4169
+ }
4103
4170
  replace.set(p.id, list);
4104
4171
  }
4105
4172
  if (!created.length) return [];
@@ -4114,6 +4181,229 @@ var VectorEditorController = class {
4114
4181
  return created;
4115
4182
  }
4116
4183
  // -------------------------------------------------------------------------
4184
+ // Fill and stroke stacks, blend modes, filters
4185
+ // -------------------------------------------------------------------------
4186
+ /** Paths of the selected (or given) nodes whose stacks can be edited (groups give every path inside; blend steps are left out). */
4187
+ stackTargets(ids) {
4188
+ const list = ids ?? this.selection;
4189
+ if (!list.length) return "Nothing is selected.";
4190
+ const out = /* @__PURE__ */ new Map();
4191
+ for (const id of list) {
4192
+ const node = this.getNode(id);
4193
+ if (!node) return `There is no node "${id}".`;
4194
+ for (const p of collectPathNodes(node)) if (!p.blendStep) out.set(p.id, p);
4195
+ }
4196
+ if (!out.size) return "The selection has no paths (fills and strokes belong to paths).";
4197
+ return [...out.values()];
4198
+ }
4199
+ /** Applies `fn` to the `kind` stack of every target path as one undo step (an error string from any path cancels it). */
4200
+ editStacks(kind, ids, label, fn, merge) {
4201
+ const targets = this.stackTargets(ids);
4202
+ if (typeof targets === "string") return { ok: false, message: targets };
4203
+ const next = /* @__PURE__ */ new Map();
4204
+ for (const path of targets) {
4205
+ const stack = kind === "fill" ? vectorFills(path) : vectorStrokes(path);
4206
+ const result = fn(stack.map((l) => ({ ...l })), path);
4207
+ if (typeof result === "string") return { ok: false, message: result };
4208
+ next.set(path.id, kind === "fill" ? withVectorFills(path, result) : withVectorStrokes(path, result));
4209
+ }
4210
+ this.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);
4211
+ return { ok: true };
4212
+ }
4213
+ /**
4214
+ * Reads a layer patch from untrusted input: paint, opacity, blend mode, visibility and (strokes)
4215
+ * the stroke settings, where `null` removes a setting. Returns an error string for invalid values.
4216
+ */
4217
+ layerPatch(kind, patch) {
4218
+ const set = {};
4219
+ const unset = [];
4220
+ if ("paint" in patch) {
4221
+ const paint = patch.paint;
4222
+ if (paint === null || typeof paint === "string" && paint.trim()) set.paint = paint === null ? null : paint.trim();
4223
+ else {
4224
+ const g = sanitizeVectorGradient(paint);
4225
+ if (!g) return "paint must be a CSS colour, a gradient { type, stops: [{ offset, color }] } or null.";
4226
+ set.paint = g;
4227
+ }
4228
+ }
4229
+ if (patch.opacity !== void 0) {
4230
+ if (typeof patch.opacity !== "number" || !Number.isFinite(patch.opacity)) return "opacity must be a number from 0 to 1.";
4231
+ const o = Math.min(1, Math.max(0, patch.opacity));
4232
+ if (o < 1) set.opacity = o;
4233
+ else unset.push("opacity");
4234
+ }
4235
+ if (patch.blendMode !== void 0) {
4236
+ if (!isVectorBlendMode(patch.blendMode)) return `Unknown blend mode "${String(patch.blendMode)}". Use one of: ${VECTOR_BLEND_MODES.join(", ")}.`;
4237
+ if (patch.blendMode !== "normal") set.blendMode = patch.blendMode;
4238
+ else unset.push("blendMode");
4239
+ }
4240
+ if (patch.visible !== void 0) {
4241
+ if (typeof patch.visible !== "boolean") return "visible must be true or false.";
4242
+ if (patch.visible) unset.push("visible");
4243
+ else set.visible = false;
4244
+ }
4245
+ if (kind === "stroke") {
4246
+ const settings = sanitizeVectorStrokeSettings(patch);
4247
+ for (const key of VECTOR_STROKE_SETTING_KEYS) {
4248
+ if (!(key in patch) || patch[key] === void 0) continue;
4249
+ if (patch[key] === null || key === "strokeDasharray" && Array.isArray(patch[key]) && !patch[key].length) unset.push(key);
4250
+ else if (settings[key] === void 0) return `Invalid ${key}: ${JSON.stringify(patch[key])}.`;
4251
+ else set[key] = settings[key];
4252
+ }
4253
+ }
4254
+ return { set, unset };
4255
+ }
4256
+ /**
4257
+ * Adds a fill or stroke layer to the selected (or given) paths (groups: every path inside), at
4258
+ * the top of the stack or at `index` (0 is the bottom). A new stroke takes the settings of the
4259
+ * stack's top stroke unless `layer` gives them. One undo step.
4260
+ */
4261
+ addPaintLayer(kind, layer = {}, options = {}) {
4262
+ const patch = this.layerPatch(kind, layer);
4263
+ if (typeof patch === "string") return { ok: false, message: patch };
4264
+ return this.editStacks(kind, options.ids, kind === "fill" ? "Add Fill" : "Add Stroke", (stack, path) => {
4265
+ const base = kind === "fill" ? { paint: "#808080" } : newVectorStrokeLayer(path, {});
4266
+ const added = { ...base, ...patch.set };
4267
+ for (const key of patch.unset) delete added[key];
4268
+ const at = options.index === void 0 ? stack.length : Math.min(stack.length, Math.max(0, Math.round(options.index)));
4269
+ return [...stack.slice(0, at), added, ...stack.slice(at)];
4270
+ });
4271
+ }
4272
+ /**
4273
+ * Changes layer `index` (0 is the bottom) of the fill or stroke stack of the selected (or given)
4274
+ * paths: paint, opacity, blend mode, visibility and, for strokes, any stroke setting (`null`
4275
+ * removes it). Fails, changing nothing, when a path has no layer `index`. With `merge` (true, or
4276
+ * a key), consecutive edits of the layer fold into one undo step (a slider drag).
4277
+ */
4278
+ updatePaintLayer(kind, index, patch, options = {}) {
4279
+ const parsed = this.layerPatch(kind, patch);
4280
+ if (typeof parsed === "string") return { ok: false, message: parsed };
4281
+ const merge = options.merge === true ? `paint-layer:${kind}:${index}` : options.merge || void 0;
4282
+ return this.editStacks(
4283
+ kind,
4284
+ options.ids,
4285
+ kind === "fill" ? "Edit Fill" : "Edit Stroke",
4286
+ (stack, path) => {
4287
+ if (!stack[index]) return `Path "${path.id}" has no ${kind} ${index} (it has ${stack.length}; 0 is the bottom).`;
4288
+ const layer = { ...stack[index], ...parsed.set };
4289
+ for (const key of parsed.unset) delete layer[key];
4290
+ return stack.map((l, i) => i === index ? layer : l);
4291
+ },
4292
+ merge
4293
+ );
4294
+ }
4295
+ /** Removes layer `index` of the fill or stroke stack of the selected (or given) paths. */
4296
+ removePaintLayer(kind, index, options = {}) {
4297
+ return this.editStacks(
4298
+ kind,
4299
+ options.ids,
4300
+ kind === "fill" ? "Remove Fill" : "Remove Stroke",
4301
+ (stack, path) => stack[index] ? stack.filter((_, i) => i !== index) : `Path "${path.id}" has no ${kind} ${index} (it has ${stack.length}; 0 is the bottom).`
4302
+ );
4303
+ }
4304
+ /** Moves layer `from` of the fill or stroke stack to position `to` (0 is the bottom) on the selected (or given) paths. */
4305
+ movePaintLayer(kind, from, to, options = {}) {
4306
+ return this.editStacks(kind, options.ids, kind === "fill" ? "Reorder Fills" : "Reorder Strokes", (stack, path) => {
4307
+ if (!stack[from]) return `Path "${path.id}" has no ${kind} ${from} (it has ${stack.length}; 0 is the bottom).`;
4308
+ const out = stack.slice();
4309
+ const [layer] = out.splice(from, 1);
4310
+ out.splice(Math.min(out.length, Math.max(0, Math.round(to))), 0, layer);
4311
+ return out;
4312
+ });
4313
+ }
4314
+ /** Replaces the whole fill or stroke stack of the selected (or given) paths (malformed layers are dropped). */
4315
+ setPaintLayers(kind, layers, options = {}) {
4316
+ const clean = kind === "fill" ? sanitizeVectorFills(layers) : sanitizeVectorStrokes(layers);
4317
+ return this.editStacks(kind, options.ids, kind === "fill" ? "Set Fills" : "Set Strokes", () => clean);
4318
+ }
4319
+ /** Sets the blend mode of the selected (or given) nodes (paths or groups); `normal` removes it. */
4320
+ setBlendMode(mode, options = {}) {
4321
+ if (!isVectorBlendMode(mode)) return { ok: false, message: `Unknown blend mode "${String(mode)}". Use one of: ${VECTOR_BLEND_MODES.join(", ")}.` };
4322
+ const ids = options.ids ?? this.selectedRoots();
4323
+ if (!ids.length) return { ok: false, message: "Nothing is selected." };
4324
+ for (const id of ids) if (!this.getNode(id)) return { ok: false, message: `There is no node "${id}".` };
4325
+ const set = new Set(ids);
4326
+ this.change(
4327
+ "Blend Mode",
4328
+ () => this.setDoc(
4329
+ mapTree(this.doc.nodes, (n) => {
4330
+ if (!set.has(n.id) || (n.blendMode ?? "normal") === mode) return n;
4331
+ const { blendMode: _b, ...rest } = n;
4332
+ return mode === "normal" ? rest : { ...rest, blendMode: mode };
4333
+ })
4334
+ )
4335
+ );
4336
+ return { ok: true };
4337
+ }
4338
+ /**
4339
+ * Adds a filter of `type` to the selected (or given) nodes, at the end of each stack (or at
4340
+ * `index`), with default parameters merged with `params`. One undo step; returns the new filter
4341
+ * ids (one per node, in order), empty for an unknown type or nothing selected.
4342
+ */
4343
+ addFilter(type, options = {}) {
4344
+ if (!isVectorFilterType(type)) return [];
4345
+ const targets = this.effectTargets(options.ids);
4346
+ if (!targets.length) return [];
4347
+ const created = [];
4348
+ const next = /* @__PURE__ */ new Map();
4349
+ for (const node of targets) {
4350
+ const list = node.filters ?? [];
4351
+ let k = list.length + 1;
4352
+ while (list.some((f) => f.id === `filter${k}`)) k++;
4353
+ const filter = createVectorFilter(type, `filter${k}`, options.params);
4354
+ const at = options.index === void 0 ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));
4355
+ next.set(node.id, { ...node, filters: [...list.slice(0, at), filter, ...list.slice(at)] });
4356
+ created.push(filter.id);
4357
+ }
4358
+ this.change(`Add ${VECTOR_FILTER_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));
4359
+ return created;
4360
+ }
4361
+ editFilters(nodeId, label, fn, merge) {
4362
+ const node = this.getNode(nodeId);
4363
+ if (!node) return { ok: false, message: `There is no node "${nodeId}".` };
4364
+ const result = fn(node.filters ?? []);
4365
+ if (typeof result === "string") return { ok: false, message: result };
4366
+ this.change(label, () => this.replaceNode(nodeId, result.length ? { ...node, filters: result } : (({ filters: _f, ...rest }) => rest)(node)), merge);
4367
+ return { ok: true };
4368
+ }
4369
+ /**
4370
+ * Turns a filter on or off and changes its parameters (invalid values are ignored). With
4371
+ * `merge`, consecutive updates of the same filter fold into one undo step.
4372
+ */
4373
+ updateFilter(nodeId, filterId, patch, options = {}) {
4374
+ const merge = options.merge === true ? `filter:${nodeId}:${filterId}` : options.merge || void 0;
4375
+ const current = this.getNode(nodeId)?.filters?.find((f) => f.id === filterId);
4376
+ return this.editFilters(
4377
+ nodeId,
4378
+ `Edit ${current ? VECTOR_FILTER_LABELS[current.type] : "Filter"}`,
4379
+ (list) => {
4380
+ const i = list.findIndex((f2) => f2.id === filterId);
4381
+ if (i < 0) return `Node "${nodeId}" has no filter "${filterId}".`;
4382
+ const f = list[i];
4383
+ const params = patch.params ? normalizeVectorFilterParams(f.type, patch.params, f.params) : f.params;
4384
+ const next = { ...f, params, ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {} };
4385
+ if (next.enabled !== false) delete next.enabled;
4386
+ return list.map((x, j) => j === i ? next : x);
4387
+ },
4388
+ merge
4389
+ );
4390
+ }
4391
+ /** Removes a filter from a node's stack. */
4392
+ removeFilter(nodeId, filterId) {
4393
+ return this.editFilters(nodeId, "Remove Filter", (list) => list.some((f) => f.id === filterId) ? list.filter((f) => f.id !== filterId) : `Node "${nodeId}" has no filter "${filterId}".`);
4394
+ }
4395
+ /** Moves a filter to position `index` of its node's stack. */
4396
+ moveFilter(nodeId, filterId, index) {
4397
+ return this.editFilters(nodeId, "Reorder Filters", (list) => {
4398
+ const i = list.findIndex((f2) => f2.id === filterId);
4399
+ if (i < 0) return `Node "${nodeId}" has no filter "${filterId}".`;
4400
+ const out = list.slice();
4401
+ const [f] = out.splice(i, 1);
4402
+ out.splice(Math.min(out.length, Math.max(0, Math.round(index))), 0, f);
4403
+ return out;
4404
+ });
4405
+ }
4406
+ // -------------------------------------------------------------------------
4117
4407
  // Path effects
4118
4408
  // -------------------------------------------------------------------------
4119
4409
  /** Selected (or given) nodes that can carry effects: the roots of the selection. */
@@ -7300,12 +7590,16 @@ function scaleFor(handle, b, p, mods = {}) {
7300
7590
  return { sx, sy, origin };
7301
7591
  }
7302
7592
  function withStyle(node, style) {
7303
- const next = { ...node, ...style };
7593
+ const { fills, strokes, ...shorthand } = style;
7594
+ let next = { ...node, ...shorthand };
7304
7595
  if ("strokeWidthProfile" in style) {
7305
7596
  const p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];
7306
7597
  if (p.length) next.strokeWidthProfile = p;
7307
7598
  else delete next.strokeWidthProfile;
7308
7599
  }
7600
+ next = normalizeVectorPaintStacks(next, "shorthand");
7601
+ if (fills !== void 0) next = withVectorFills(next, sanitizeVectorFills(fills ?? []));
7602
+ if (strokes !== void 0) next = withVectorStrokes(next, sanitizeVectorStrokes(strokes ?? []));
7309
7603
  return next;
7310
7604
  }
7311
7605
  function hiddenOrLocked(nodes, id) {
@@ -7329,37 +7623,99 @@ function pencilPressureProfile(points, pressures) {
7329
7623
  var fmt2 = (n) => Math.round(n * 100) / 100;
7330
7624
  var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
7331
7625
  var VECTOR_BUILDER_HATCH_GAP = 6;
7332
- function renderNode(stored, defs, idPrefix) {
7626
+ function nodeFinish(node, filters, idPrefix) {
7627
+ const out = {};
7628
+ const blend = vectorBlendModeCss(node.blendMode);
7629
+ if (blend) out.blendMode = blend;
7630
+ const filter = vectorNodeFilterSvg(node, `${idPrefix}filter-${node.id}`);
7631
+ if (filter) {
7632
+ filters.push(filter);
7633
+ out.filter = `url(#${filter.id})`;
7634
+ }
7635
+ return out;
7636
+ }
7637
+ function lineStyle(node) {
7638
+ return {
7639
+ strokeWidth: node.strokeWidth ?? 1,
7640
+ ...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
7641
+ ...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
7642
+ ...node.strokeDasharray?.length ? { strokeDasharray: node.strokeDasharray.join(" ") } : {}
7643
+ };
7644
+ }
7645
+ function renderLayers(node, defs, idPrefix) {
7646
+ const out = [];
7647
+ for (const l of vectorPaintLayerNodes(node)) {
7648
+ if (!l.drawn) continue;
7649
+ const role = l.kind;
7650
+ const paint = vectorPaintToSvg(l.layer.paint, `${idPrefix}${role}-${node.id}-${l.index}`, { node: l.node, ...role === "stroke" ? { role } : {} });
7651
+ defs.push(...paint.defs);
7652
+ const blend = vectorBlendModeCss(l.layer.blendMode);
7653
+ const base = { kind: role, index: l.index, opacity: l.layer.opacity ?? 1, ...blend ? { blendMode: blend } : {} };
7654
+ if (role === "fill") out.push({ ...base, fill: paint.value, stroke: "none", strokeWidth: 0 });
7655
+ else {
7656
+ const strokeParts = vectorStrokeSvgParts(l.node, `${idPrefix}clip-${node.id}-${l.index}`);
7657
+ out.push({ ...base, fill: "none", stroke: paint.value, ...lineStyle(l.node), ...strokeParts ? { strokeParts } : {} });
7658
+ }
7659
+ }
7660
+ return out;
7661
+ }
7662
+ function renderNode(stored, defs, filters, idPrefix) {
7333
7663
  if (stored.visible === false) return null;
7334
7664
  const node = expandVectorAppearance(stored);
7335
7665
  if (node.type === "group") {
7336
- const children = node.children.map((c) => renderNode(c, defs, idPrefix)).filter((c) => !!c);
7337
- return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: node.opacity ?? 1, children };
7666
+ const children = node.children.map((c) => renderNode(c, defs, filters, idPrefix)).filter((c) => !!c);
7667
+ return {
7668
+ kind: "group",
7669
+ id: node.id,
7670
+ ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
7671
+ opacity: node.opacity ?? 1,
7672
+ ...nodeFinish(node, filters, idPrefix),
7673
+ children
7674
+ };
7675
+ }
7676
+ const finish = nodeFinish(node, filters, idPrefix);
7677
+ const common = {
7678
+ kind: "path",
7679
+ id: node.id,
7680
+ ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
7681
+ d: vectorPathData(node),
7682
+ fillRule: node.fillRule ?? "nonzero",
7683
+ opacity: node.opacity ?? 1,
7684
+ ...finish
7685
+ };
7686
+ if (needsVectorPaintLayers(node)) {
7687
+ const layers = renderLayers(node, defs, idPrefix);
7688
+ const fill2 = layers.filter((l) => l.kind === "fill").at(-1);
7689
+ const stroke2 = layers.filter((l) => l.kind === "stroke").at(-1);
7690
+ return {
7691
+ ...common,
7692
+ fill: fill2?.fill ?? "none",
7693
+ stroke: stroke2?.stroke ?? "none",
7694
+ strokeWidth: stroke2?.strokeWidth ?? node.strokeWidth ?? 1,
7695
+ ...stroke2?.strokeLinecap ? { strokeLinecap: stroke2.strokeLinecap } : {},
7696
+ ...stroke2?.strokeLinejoin ? { strokeLinejoin: stroke2.strokeLinejoin } : {},
7697
+ ...stroke2?.strokeDasharray ? { strokeDasharray: stroke2.strokeDasharray } : {},
7698
+ ...stroke2?.strokeParts ? { strokeParts: stroke2.strokeParts } : {},
7699
+ layers
7700
+ };
7338
7701
  }
7339
7702
  const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`, { node });
7340
7703
  const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`, { node, role: "stroke" });
7341
7704
  defs.push(...fill.defs, ...stroke.defs);
7342
7705
  const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
7343
7706
  return {
7344
- kind: "path",
7345
- id: node.id,
7346
- ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
7707
+ ...common,
7347
7708
  ...strokeParts ? { strokeParts } : {},
7348
- d: vectorPathData(node),
7349
7709
  fill: fill.value,
7350
7710
  stroke: stroke.value,
7351
- strokeWidth: node.strokeWidth ?? 1,
7352
- ...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
7353
- ...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
7354
- ...node.strokeDasharray?.length ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
7355
- fillRule: node.fillRule ?? "nonzero",
7356
- opacity: node.opacity ?? 1
7711
+ ...lineStyle(node)
7357
7712
  };
7358
7713
  }
7359
7714
  function getVectorSceneRenderData(state, options = {}) {
7360
7715
  const defs = [];
7716
+ const filters = [];
7361
7717
  const prefix = options.idPrefix ?? "ve-";
7362
- const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
7718
+ const nodes = state.document.nodes.map((n) => renderNode(n, defs, filters, prefix)).filter((n) => !!n);
7363
7719
  const v = state.viewport;
7364
7720
  return {
7365
7721
  transform: `translate(${fmt2(v.x)} ${fmt2(v.y)}) scale(${v.zoom})`,
@@ -7367,6 +7723,7 @@ function getVectorSceneRenderData(state, options = {}) {
7367
7723
  height: state.document.height,
7368
7724
  ...state.document.background ? { background: state.document.background } : {},
7369
7725
  defs,
7726
+ filters,
7370
7727
  nodes
7371
7728
  };
7372
7729
  }
@@ -7593,7 +7950,7 @@ function getVectorEnvelopePanel(state) {
7593
7950
  };
7594
7951
  }
7595
7952
  function gradientOverlay(state) {
7596
- const a = getVectorGradientAnnotator(state.document, state.selection, state.viewport, state.gradientEdit.target);
7953
+ const a = getVectorGradientAnnotator(state.document, state.selection, state.viewport, state.gradientEdit.target, state.gradientEdit.layer ?? null);
7597
7954
  if (!a) return null;
7598
7955
  const hover = state.interaction ? null : state.hover?.gradient;
7599
7956
  const handleHovered = (h) => hover?.kind === "handle" && hover.handle === h;
@@ -7831,8 +8188,8 @@ function getVectorGradientEditor(state) {
7831
8188
  if (node) paths.push(...collect(node));
7832
8189
  }
7833
8190
  if (!paths.length) return null;
7834
- const target = state.gradientEdit.target;
7835
- const paint = paths[0][target];
8191
+ const { target, layer } = state.gradientEdit;
8192
+ const paint = vectorTargetPaint(paths[0], target, layer ?? null);
7836
8193
  const gradient = isVectorGradient(paint) ? paint : null;
7837
8194
  const sorted = gradient ? sortedVectorGradientStops(gradient) : [];
7838
8195
  const stopIndex = state.gradientEdit.stop;
@@ -7846,6 +8203,7 @@ function getVectorGradientEditor(state) {
7846
8203
  }).join(", ")})` : null;
7847
8204
  return {
7848
8205
  target,
8206
+ layer: layer ?? null,
7849
8207
  gradient,
7850
8208
  color: vectorPaintToHex(paint),
7851
8209
  stops,
@@ -7856,7 +8214,7 @@ function getVectorGradientEditor(state) {
7856
8214
  interpolation: gradient?.interpolation ?? "srgb",
7857
8215
  smoothness: gradient?.smoothness ?? 0,
7858
8216
  strokeMode: gradient?.strokeMode ?? "within",
7859
- mixed: paths.some((p) => JSON.stringify(p[target] ?? null) !== JSON.stringify(paint ?? null))
8217
+ mixed: paths.some((p) => JSON.stringify(vectorTargetPaint(p, target, layer ?? null) ?? null) !== JSON.stringify(paint ?? null))
7860
8218
  };
7861
8219
  }
7862
8220
  var VECTOR_PATH_ACTIONS = [
@@ -7900,6 +8258,66 @@ function getVectorEffectsPanel(state) {
7900
8258
  expandable: nodes.some(hasVectorAppearance)
7901
8259
  };
7902
8260
  }
8261
+ function vectorPaintSwatch(paint) {
8262
+ if (typeof paint === "string") return paint;
8263
+ if (isVectorGradient(paint)) {
8264
+ return `linear-gradient(to right, ${expandVectorGradientStops(paint).map((s) => {
8265
+ const c = parseVectorColor(s.color);
8266
+ const a = (c?.[3] ?? 1) * (s.opacity ?? 1);
8267
+ const color = c ? `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(a * 1e3) / 1e3})` : s.color;
8268
+ return `${color} ${Math.round(s.offset * 1e4) / 100}%`;
8269
+ }).join(", ")})`;
8270
+ }
8271
+ return "transparent";
8272
+ }
8273
+ function getVectorAppearancePanel(state) {
8274
+ const nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n) => !!n && !n.blendStep);
8275
+ if (!nodes.length) return null;
8276
+ const paths = [];
8277
+ for (const n of nodes) for (const p of collect(n)) if (!p.blendStep && !paths.some((q) => q.id === p.id)) paths.push(p);
8278
+ const first = paths[0];
8279
+ const edit = state.gradientEdit;
8280
+ const layer = (kind, l, index, count) => ({
8281
+ kind,
8282
+ index,
8283
+ paint: l.paint,
8284
+ color: vectorPaintToHex(l.paint),
8285
+ swatch: vectorPaintSwatch(l.paint),
8286
+ gradient: isVectorGradient(l.paint),
8287
+ opacity: l.opacity ?? 1,
8288
+ blendMode: l.blendMode ?? "normal",
8289
+ visible: l.visible !== false,
8290
+ ...kind === "stroke" ? { strokeWidth: l.strokeWidth ?? 1, strokeAlign: l.strokeAlign ?? "center" } : {},
8291
+ ...(l.kind ?? "paint") !== "paint" ? { unknownKind: String(l.kind) } : {},
8292
+ editing: edit.target === kind && (edit.layer === null ? index === count - 1 : edit.layer === index)
8293
+ });
8294
+ const fills = first ? vectorFills(first) : [];
8295
+ const strokes = first ? vectorStrokes(first) : [];
8296
+ const same = (get) => paths.every((p) => JSON.stringify(get(p)) === JSON.stringify(get(first)));
8297
+ const one = nodes.length === 1 ? nodes[0] : null;
8298
+ return {
8299
+ nodeIds: nodes.map((n) => n.id),
8300
+ pathIds: paths.map((p) => p.id),
8301
+ fills: fills.map((l, i) => layer("fill", l, i, fills.length)),
8302
+ strokes: strokes.map((l, i) => layer("stroke", l, i, strokes.length)),
8303
+ blendMode: nodes[0].blendMode ?? "normal",
8304
+ mixed: {
8305
+ fills: !!first && !same(vectorFills),
8306
+ strokes: !!first && !same(vectorStrokes),
8307
+ blendMode: nodes.some((n) => (n.blendMode ?? "normal") !== (nodes[0].blendMode ?? "normal"))
8308
+ },
8309
+ filterNodeId: one?.id ?? null,
8310
+ filters: (one?.filters ?? []).map((f) => ({
8311
+ id: f.id,
8312
+ type: f.type,
8313
+ label: VECTOR_FILTER_LABELS[f.type] ?? f.type,
8314
+ enabled: f.enabled !== false,
8315
+ params: f.params,
8316
+ specs: VECTOR_FILTER_PARAM_SPECS[f.type] ?? []
8317
+ }))
8318
+ };
8319
+ }
8320
+ var VECTOR_BLEND_MODE_MENU = VECTOR_BLEND_MODES.map((mode) => ({ mode, label: VECTOR_BLEND_MODE_LABELS[mode] }));
7903
8321
 
7904
8322
  // src/vector/commands.ts
7905
8323
  var EFFECT_PARAMS_DESCRIPTION = "Effect parameters (missing ones keep their value or default). trim: start, end (percent), offset (degrees), mode simultaneous|individual. roundCorners: radius. puckerBloat: amount (\u2212100\u2026100). twist: angle, center {x, y} (offset from the middle). zigzag: size, ridges, smooth. wiggle: size, detail, smooth, speed, correlation, phase, seed, time (seconds). offset: amount, join miter|round|bevel, miterLimit, copies, copyOffset. merge: mode merge|add|subtract|intersect|exclude. repeater: copies (fractions fade the last), offset, position {x, y}, anchor {x, y} (offset from the middle), rotation, scale {x, y} (percent), startOpacity, endOpacity (percent), composite below|above. envelope: style (a warp preset: arc|arcLower|arcUpper|arch|bulge|shellLower|shellUpper|flag|wave|fish|rise|fisheye|inflate|squeeze|twist, or mesh), bend, horizontal, vertical (percent, \u2212100\u2026100), orientation horizontal|vertical, rows, columns, mesh { us, vs, points } (node space; prefer vector.envelope).";
@@ -7983,15 +8401,23 @@ var PAINT = {
7983
8401
  description: 'A CSS colour, `null` for none, or a gradient object: `{ type: "linear" | "radial" | "conic" | "reflected" | "diamond", stops: [{ offset, color, opacity?, midpoint? }] }` plus the shorthand `angle` / `cx, cy, r` or explicit `start`, `end`, `width`, `focal`, `focalRadius`, `units`, and `spread`, `interpolation`, `smoothness`, `strokeMode` (see vector.setGradient).',
7984
8402
  anyOf: [{ type: "string" }, { type: "null" }, { type: "object", properties: GRADIENT_PROPS, required: ["type", "stops"] }]
7985
8403
  };
7986
- var STYLE_PROPS = {
7987
- fill: PAINT,
7988
- stroke: PAINT,
7989
- strokeWidth: { type: "number", minimum: 0 },
7990
- strokeLinecap: { type: "string", enum: ["butt", "round", "square"] },
7991
- strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"] },
7992
- strokeDasharray: { type: "array", items: { type: "number" } },
7993
- fillRule: { type: "string", enum: ["nonzero", "evenodd"] },
7994
- strokeAlign: { type: "string", enum: ["center", "inside", "outside"], description: "Where the stroke sits on the outline (closed shapes only; open paths stroke centred)." },
8404
+ var BLEND_MODE = {
8405
+ type: "string",
8406
+ enum: VECTOR_BLEND_MODES,
8407
+ description: "How it mixes with what is under it (CSS mix-blend-mode names; linear-dodge draws as plus-lighter). Default normal."
8408
+ };
8409
+ var FILL_LAYER_PROPS = {
8410
+ paint: PAINT,
8411
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0 to 1 (default 1), multiplying the paint's own alpha." },
8412
+ blendMode: BLEND_MODE,
8413
+ visible: { type: "boolean", description: "false hides the layer (kept, not drawn or hit). Default true." }
8414
+ };
8415
+ var STROKE_SETTING_PROPS = {
8416
+ strokeWidth: { type: "number", minimum: 0, description: "Stroke width in document units (default 1)." },
8417
+ strokeLinecap: { type: "string", enum: ["butt", "round", "square"], description: "Line cap (default butt)." },
8418
+ strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"], description: "Line join (default miter)." },
8419
+ strokeDasharray: { type: ["array", "null"], items: { type: "number", minimum: 0 }, description: "Dash and gap lengths in document units; null or [] for a solid line." },
8420
+ strokeAlign: { type: "string", enum: ["center", "inside", "outside"], description: "Where the stroke sits on the outline (closed shapes only; open paths stroke centred). Default center." },
7995
8421
  startArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the start of an open path, or null for none." },
7996
8422
  endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
7997
8423
  arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
@@ -7999,12 +8425,33 @@ var STYLE_PROPS = {
7999
8425
  endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." },
8000
8426
  strokeWidthProfile: WIDTH_POINTS
8001
8427
  };
8428
+ var FILL_LAYER = { type: "object", properties: FILL_LAYER_PROPS, required: ["paint"], additionalProperties: false };
8429
+ var STROKE_LAYER = { type: "object", properties: { ...FILL_LAYER_PROPS, ...STROKE_SETTING_PROPS }, required: ["paint"], additionalProperties: false };
8430
+ var STYLE_PROPS = {
8431
+ fills: { type: "array", items: FILL_LAYER, description: "Replace the fill stack, bottom to top (fills draw under strokes). `fill` alone edits the top fill." },
8432
+ strokes: { type: "array", items: STROKE_LAYER, description: "Replace the stroke stack, bottom to top, each stroke with its own settings. `stroke` and the stroke settings alone edit the top stroke." },
8433
+ fill: PAINT,
8434
+ stroke: PAINT,
8435
+ ...STROKE_SETTING_PROPS,
8436
+ fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Which areas count as inside (default nonzero)." }
8437
+ };
8438
+ var checkPaint = (paint, where) => {
8439
+ if (paint && typeof paint === "object") return sanitizeVectorGradient(paint) ?? fail(`${where} is not a valid gradient: give a type and at least one stop { offset, color }.`);
8440
+ return paint;
8441
+ };
8002
8442
  var pickStyle = (params) => {
8003
8443
  const out = {};
8004
8444
  for (const key of Object.keys(STYLE_PROPS)) if (params[key] !== void 0) out[key] = params[key];
8005
- for (const key of ["fill", "stroke"]) {
8006
- const paint = out[key];
8007
- if (paint && typeof paint === "object") out[key] = sanitizeVectorGradient(paint) ?? fail(`${key} is not a valid gradient: give a type and at least one stop { offset, color }.`);
8445
+ for (const key of ["fill", "stroke"]) if (out[key] !== void 0) out[key] = checkPaint(out[key], key);
8446
+ for (const key of ["fills", "strokes"]) {
8447
+ const stack = out[key];
8448
+ if (!Array.isArray(stack)) continue;
8449
+ stack.forEach((l, i) => {
8450
+ if (!l || typeof l !== "object") fail(`${key}[${i}] must be a layer object { paint, \u2026 }.`);
8451
+ const layer = l;
8452
+ checkPaint(layer.paint, `${key}[${i}].paint`);
8453
+ if (layer.blendMode !== void 0 && !isVectorBlendMode(layer.blendMode)) fail(`${key}[${i}].blendMode "${String(layer.blendMode)}" is not one of: ${VECTOR_BLEND_MODES.join(", ")}.`);
8454
+ });
8008
8455
  }
8009
8456
  return out;
8010
8457
  };
@@ -8053,7 +8500,11 @@ function createVectorCommands(controller, options = {}) {
8053
8500
  ...n.blendStep ? { blendStep: true } : {},
8054
8501
  ...n.effects?.length ? { effects: n.effects } : {},
8055
8502
  ...n.type === "group" && n.liveBoolean ? { liveBoolean: n.liveBoolean } : {},
8056
- ...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}
8503
+ ...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {},
8504
+ ...n.type === "path" && Array.isArray(n.fills) ? { fills: n.fills } : {},
8505
+ ...n.type === "path" && Array.isArray(n.strokes) ? { strokes: n.strokes } : {},
8506
+ ...n.blendMode && n.blendMode !== "normal" ? { blendMode: n.blendMode } : {},
8507
+ ...n.filters?.length ? { filters: n.filters } : {}
8057
8508
  });
8058
8509
  if (n.type === "group") visit(n.children, depth + 1);
8059
8510
  }
@@ -8061,6 +8512,276 @@ function createVectorCommands(controller, options = {}) {
8061
8512
  visit(state.document.nodes, 0);
8062
8513
  return { width: state.document.width, height: state.document.height, nodes: out, selection: state.selection, tool: state.tool };
8063
8514
  };
8515
+ const appearanceOf = (id) => {
8516
+ const n = controller.getNode(id) ?? fail(`There is no node "${id}".`);
8517
+ return {
8518
+ id: n.id,
8519
+ type: n.type,
8520
+ ...n.type === "path" ? { fills: vectorFills(n), strokes: vectorStrokes(n), fillRule: n.fillRule ?? "nonzero" } : {},
8521
+ blendMode: n.blendMode ?? "normal",
8522
+ opacity: n.opacity ?? 1,
8523
+ filters: n.filters ?? []
8524
+ };
8525
+ };
8526
+ const pathOf = (id) => {
8527
+ const n = controller.getNode(id) ?? fail(`There is no node "${id}". Call vector.list for ids.`);
8528
+ if (n.type !== "path") fail(`"${id}" is a group; fills and strokes belong to paths (pass a path id, or use ids on vector.addFill / vector.addStroke for every path inside).`);
8529
+ return n;
8530
+ };
8531
+ const layerIndex = (id, kind, index, key = "index") => {
8532
+ const stack = kind === "fill" ? vectorFills(pathOf(id)) : vectorStrokes(pathOf(id));
8533
+ if (!Number.isInteger(index) || !stack[index]) fail(`${key} ${index} is out of range: "${id}" has ${stack.length} ${kind}${stack.length === 1 ? "" : "s"} (0 is the bottom). Read them with vector.getAppearance.`);
8534
+ };
8535
+ const stackResult = (id, kind) => kind === "fill" ? { id, fills: vectorFills(pathOf(id)) } : { id, strokes: vectorStrokes(pathOf(id)) };
8536
+ const layerPatchOf = (params, keys) => {
8537
+ const out = {};
8538
+ for (const key of keys) if (params[key] !== void 0) out[key] = key === "paint" ? checkPaint(params[key], "paint") : params[key];
8539
+ return out;
8540
+ };
8541
+ const pathsUnder = (list2) => {
8542
+ const out = [];
8543
+ const walk = (n) => n.type === "path" ? out.push(n.id) : n.children.forEach(walk);
8544
+ for (const id of list2) walk(controller.getNode(id));
8545
+ return out;
8546
+ };
8547
+ const FILL_KEYS = Object.keys(FILL_LAYER_PROPS);
8548
+ const STROKE_KEYS2 = [...FILL_KEYS, ...Object.keys(STROKE_SETTING_PROPS)];
8549
+ const layerCommands = (kind) => {
8550
+ const Kind = kind === "fill" ? "Fill" : "Stroke";
8551
+ const props = kind === "fill" ? FILL_LAYER_PROPS : { ...FILL_LAYER_PROPS, ...STROKE_SETTING_PROPS };
8552
+ const keys = kind === "fill" ? FILL_KEYS : STROKE_KEYS2;
8553
+ const NODE = { type: "string", description: "Id of the path (from vector.list)." };
8554
+ const INDEX = { type: "integer", minimum: 0, description: `Position of the ${kind} in the path's ${kind} stack, 0 at the bottom (from vector.getAppearance).` };
8555
+ const listKey = `${kind}s`;
8556
+ return [
8557
+ {
8558
+ ...base,
8559
+ id: `${prefix}add${Kind}`,
8560
+ label: `Add ${Kind}`,
8561
+ keywords: ["appearance", "layer", kind, "stack", "multiple", "paint"],
8562
+ description: kind === "fill" ? "Adds a fill layer to paths (groups: every path inside), on top of the fill stack or at `index`. Fills draw bottom to top, all under the strokes. Returns each path's new fill stack." : "Adds a stroke layer to paths (groups: every path inside), on top of the stroke stack or at `index`, with its own width, alignment, dashes, caps, joins, arrowheads and profile (unset settings come from the stack's current top stroke). Strokes draw bottom to top, over every fill. Returns each path's new stroke stack.",
8563
+ params: {
8564
+ type: "object",
8565
+ properties: {
8566
+ ids: { ...IDS, description: "Paths or groups to add it to (default: the selection; groups give every path inside)." },
8567
+ ...props,
8568
+ paint: { ...PAINT, description: `${PAINT.description} Default ${kind === "fill" ? "#808080" : "#000000"}.` },
8569
+ index: { type: "integer", minimum: 0, description: `Where in the stack to put it, 0 at the bottom (default: on top).` }
8570
+ },
8571
+ additionalProperties: false
8572
+ },
8573
+ undoable: true,
8574
+ enabled: enabledFor,
8575
+ run: (_ctx, params) => {
8576
+ const targets = ids(params);
8577
+ check(controller.addPaintLayer(kind, layerPatchOf(params ?? {}, keys), { ids: targets, index: params?.index }));
8578
+ return { nodes: pathsUnder(targets).map((id) => stackResult(id, kind)) };
8579
+ }
8580
+ },
8581
+ {
8582
+ ...base,
8583
+ id: `${prefix}update${Kind}`,
8584
+ label: `Edit ${Kind}`,
8585
+ keywords: ["appearance", "layer", kind, "opacity", "blend", "hide", "show"],
8586
+ description: `Changes ${kind} \`index\` of path \`nodeId\`: ${kind === "fill" ? "paint, opacity, blend mode, visibility" : "paint, opacity, blend mode, visibility and any stroke setting (null removes an arrowhead, dashes or a profile)"}. Missing fields keep their value. Returns the path's ${kind} stack.`,
8587
+ params: { type: "object", properties: { nodeId: NODE, index: INDEX, ...props }, required: ["nodeId", "index"], additionalProperties: false },
8588
+ undoable: true,
8589
+ run: (_ctx, params) => {
8590
+ layerIndex(params.nodeId, kind, params.index);
8591
+ const patch = layerPatchOf(params, keys);
8592
+ if (!Object.keys(patch).length) fail(`Pass at least one of: ${keys.join(", ")}.`);
8593
+ check(controller.updatePaintLayer(kind, params.index, patch, { ids: [params.nodeId] }));
8594
+ return stackResult(params.nodeId, kind);
8595
+ }
8596
+ },
8597
+ {
8598
+ ...base,
8599
+ id: `${prefix}remove${Kind}`,
8600
+ label: `Remove ${Kind}`,
8601
+ keywords: ["appearance", "layer", kind, "delete"],
8602
+ description: `Removes ${kind} \`index\` from path \`nodeId\`. Returns the path's remaining ${kind} stack.`,
8603
+ params: { type: "object", properties: { nodeId: NODE, index: INDEX }, required: ["nodeId", "index"], additionalProperties: false },
8604
+ undoable: true,
8605
+ run: (_ctx, params) => {
8606
+ layerIndex(params.nodeId, kind, params.index);
8607
+ check(controller.removePaintLayer(kind, params.index, { ids: [params.nodeId] }));
8608
+ return stackResult(params.nodeId, kind);
8609
+ }
8610
+ },
8611
+ {
8612
+ ...base,
8613
+ id: `${prefix}move${Kind}`,
8614
+ label: `Reorder ${Kind}s`,
8615
+ keywords: ["appearance", "layer", kind, "order", "reorder", "stack"],
8616
+ description: `Moves ${kind} \`index\` of path \`nodeId\` to position \`to\` in its ${kind} stack (0 is the bottom). Returns the reordered ${listKey}.`,
8617
+ params: {
8618
+ type: "object",
8619
+ properties: { nodeId: NODE, index: INDEX, to: { type: "integer", minimum: 0, description: `New position, 0 at the bottom (clamped to the stack).` } },
8620
+ required: ["nodeId", "index", "to"],
8621
+ additionalProperties: false
8622
+ },
8623
+ undoable: true,
8624
+ run: (_ctx, params) => {
8625
+ layerIndex(params.nodeId, kind, params.index);
8626
+ check(controller.movePaintLayer(kind, params.index, params.to, { ids: [params.nodeId] }));
8627
+ return stackResult(params.nodeId, kind);
8628
+ }
8629
+ }
8630
+ ];
8631
+ };
8632
+ const FILTER_PARAMS = {
8633
+ type: "object",
8634
+ description: "Filter parameters (missing ones keep their value or default; lengths in document units). dropShadow / innerShadow: color (CSS), opacity (0 to 1), offsetX, offsetY, blur (radius; reaches about 1.5 times it), spread (inner: choke). outerGlow / innerGlow: color, opacity, blur, spread. layerBlur: radius. feather: radius (edge fade width).",
8635
+ properties: {
8636
+ color: { type: "string", description: "Shadow or glow colour (CSS)." },
8637
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Shadow or glow opacity, 0 to 1." },
8638
+ offsetX: { type: "number", description: "Shadow offset to the right, document units." },
8639
+ offsetY: { type: "number", description: "Shadow offset downwards, document units." },
8640
+ blur: { type: "number", minimum: 0, description: "Shadow or glow blur radius, document units." },
8641
+ spread: { type: "number", minimum: 0, description: "Grows the silhouette before blurring (inner shadow and glow: chokes inwards)." },
8642
+ radius: { type: "number", minimum: 0, description: "Layer blur radius, or feather width, document units." }
8643
+ },
8644
+ additionalProperties: false
8645
+ };
8646
+ const checkFilterParams = (type, params) => {
8647
+ const allowed = vectorFilterParamKeys(type);
8648
+ for (const key of Object.keys(params ?? {})) if (!allowed.includes(key)) fail(`${type} has no parameter "${key}"; it takes ${allowed.join(", ")}.`);
8649
+ };
8650
+ const filtersResult = (id) => ({ id, filters: controller.getNode(id)?.filters ?? [] });
8651
+ const filterOf = (nodeId, filterId) => {
8652
+ const node = controller.getNode(nodeId) ?? fail(`There is no node "${nodeId}".`);
8653
+ return node.filters?.find((f) => f.id === filterId) ?? fail(`Node "${nodeId}" has no filter "${filterId}"; it has ${(node.filters ?? []).map((f) => `"${f.id}" (${f.type})`).join(", ") || "none"}.`);
8654
+ };
8655
+ const appearanceCommands = () => [
8656
+ {
8657
+ ...base,
8658
+ id: `${prefix}getAppearance`,
8659
+ label: "Get Appearance",
8660
+ readOnly: true,
8661
+ keywords: ["appearance", "fills", "strokes", "blend mode", "filters", "shadow"],
8662
+ description: "Reads a node's appearance: for paths the fill and stroke stacks (bottom to top, each with paint, opacity, blendMode, visible and, for strokes, their settings) and fill rule; for any node its blend mode, opacity and filters (with ids). Use the indexes and ids with the fill, stroke and filter commands.",
8663
+ params: { type: "object", properties: { id: { type: "string", description: "Node id (from vector.list)." } }, required: ["id"], additionalProperties: false },
8664
+ run: (_ctx, params) => appearanceOf(params.id)
8665
+ },
8666
+ ...layerCommands("fill"),
8667
+ ...layerCommands("stroke"),
8668
+ {
8669
+ ...base,
8670
+ id: `${prefix}setBlendMode`,
8671
+ label: "Blend Mode\u2026",
8672
+ keywords: ["blend", "multiply", "screen", "overlay", "mix-blend-mode", "composite"],
8673
+ description: "Sets how whole nodes (paths or groups, with everything they draw) mix with what is under them. `normal` removes it. Returns each node's blend mode.",
8674
+ params: {
8675
+ type: "object",
8676
+ properties: { ids: IDS, blendMode: { ...BLEND_MODE, description: "The blend mode (CSS mix-blend-mode names; linear-dodge draws as plus-lighter)." } },
8677
+ required: ["blendMode"],
8678
+ additionalProperties: false
8679
+ },
8680
+ undoable: true,
8681
+ enabled: enabledFor,
8682
+ run: (_ctx, params) => {
8683
+ const targets = ids(params);
8684
+ check(controller.setBlendMode(params.blendMode, { ids: targets }));
8685
+ return { nodes: targets.map((id) => ({ id, blendMode: controller.getNode(id)?.blendMode ?? "normal" })) };
8686
+ }
8687
+ },
8688
+ {
8689
+ ...base,
8690
+ id: `${prefix}addFilter`,
8691
+ label: "Add Filter\u2026",
8692
+ keywords: ["filter", "drop shadow", "inner shadow", "glow", "blur", "feather", "effect", "appearance"],
8693
+ description: "Adds a visual filter `type` (dropShadow, innerShadow, outerGlow, innerGlow, layerBlur, feather) to nodes (paths or groups), at the end of each filter stack or at `index`. Filters never change geometry (bounds and hit-testing ignore them) and export as SVG filters. Returns each node's new filter id and its filter stack.",
8694
+ params: {
8695
+ type: "object",
8696
+ properties: {
8697
+ type: { type: "string", enum: VECTOR_FILTER_TYPES, description: "The filter type." },
8698
+ params: FILTER_PARAMS,
8699
+ index: { type: "integer", minimum: 0, description: "Position in the stack (default: last)." },
8700
+ ids: IDS
8701
+ },
8702
+ required: ["type"],
8703
+ additionalProperties: false
8704
+ },
8705
+ undoable: true,
8706
+ enabled: enabledFor,
8707
+ run: (_ctx, params) => {
8708
+ if (!VECTOR_FILTER_TYPES.includes(params.type)) fail(`Unknown filter type "${params.type}"; use one of ${VECTOR_FILTER_TYPES.join(", ")}.`);
8709
+ checkFilterParams(params.type, params.params);
8710
+ const targets = ids(params);
8711
+ const created = controller.addFilter(params.type, { ids: targets, params: params.params, index: params.index });
8712
+ if (!created.length) fail("Nothing to add the filter to (blend steps take no filters).");
8713
+ return { nodes: targets.map((id, i) => ({ filterId: created[i], ...filtersResult(id) })) };
8714
+ }
8715
+ },
8716
+ {
8717
+ ...base,
8718
+ id: `${prefix}updateFilter`,
8719
+ label: "Edit Filter\u2026",
8720
+ keywords: ["filter", "shadow", "glow", "blur", "feather", "enable", "disable"],
8721
+ description: "Changes filter `filterId` of node `nodeId`: its `params` (merged; only the keys its type takes) and whether it is `enabled`. Returns the node's filter stack.",
8722
+ params: {
8723
+ type: "object",
8724
+ properties: {
8725
+ nodeId: { type: "string", description: "Node id." },
8726
+ filterId: { type: "string", description: "Filter id (from vector.addFilter or vector.getAppearance)." },
8727
+ params: FILTER_PARAMS,
8728
+ enabled: { type: "boolean", description: "false turns the filter off (kept in the stack)." }
8729
+ },
8730
+ required: ["nodeId", "filterId"],
8731
+ additionalProperties: false
8732
+ },
8733
+ undoable: true,
8734
+ run: (_ctx, params) => {
8735
+ const filter = filterOf(params.nodeId, params.filterId);
8736
+ checkFilterParams(filter.type, params.params);
8737
+ if (params.params === void 0 && params.enabled === void 0) fail("Pass params or enabled.");
8738
+ check(controller.updateFilter(params.nodeId, params.filterId, { params: params.params, enabled: params.enabled }));
8739
+ return filtersResult(params.nodeId);
8740
+ }
8741
+ },
8742
+ {
8743
+ ...base,
8744
+ id: `${prefix}removeFilter`,
8745
+ label: "Remove Filter",
8746
+ keywords: ["filter", "delete", "shadow", "blur"],
8747
+ description: "Removes filter `filterId` from node `nodeId`. Returns the node's remaining filters.",
8748
+ params: {
8749
+ type: "object",
8750
+ properties: { nodeId: { type: "string", description: "Node id." }, filterId: { type: "string", description: "Filter id." } },
8751
+ required: ["nodeId", "filterId"],
8752
+ additionalProperties: false
8753
+ },
8754
+ undoable: true,
8755
+ run: (_ctx, params) => {
8756
+ filterOf(params.nodeId, params.filterId);
8757
+ check(controller.removeFilter(params.nodeId, params.filterId));
8758
+ return filtersResult(params.nodeId);
8759
+ }
8760
+ },
8761
+ {
8762
+ ...base,
8763
+ id: `${prefix}moveFilter`,
8764
+ label: "Reorder Filters",
8765
+ keywords: ["filter", "order", "reorder"],
8766
+ description: "Moves filter `filterId` of node `nodeId` to position `index` of its stack. Order matters within a kind: later drop shadows draw over earlier ones, later inner effects over earlier ones. Returns the reordered filters.",
8767
+ params: {
8768
+ type: "object",
8769
+ properties: {
8770
+ nodeId: { type: "string", description: "Node id." },
8771
+ filterId: { type: "string", description: "Filter id." },
8772
+ index: { type: "integer", minimum: 0, description: "New position, 0 first (clamped to the stack)." }
8773
+ },
8774
+ required: ["nodeId", "filterId", "index"],
8775
+ additionalProperties: false
8776
+ },
8777
+ undoable: true,
8778
+ run: (_ctx, params) => {
8779
+ filterOf(params.nodeId, params.filterId);
8780
+ check(controller.moveFilter(params.nodeId, params.filterId, params.index));
8781
+ return filtersResult(params.nodeId);
8782
+ }
8783
+ }
8784
+ ];
8064
8785
  return [
8065
8786
  {
8066
8787
  ...base,
@@ -8166,6 +8887,7 @@ function createVectorCommands(controller, options = {}) {
8166
8887
  return { ok: true, ...path ? { gradient: path[target ?? "fill"] } : {} };
8167
8888
  }
8168
8889
  },
8890
+ ...appearanceCommands(),
8169
8891
  {
8170
8892
  ...base,
8171
8893
  id: `${prefix}setCorners`,
@@ -8950,6 +9672,9 @@ export {
8950
9672
  VECTOR_ARROWHEAD_LABELS,
8951
9673
  VECTOR_BLEND_MAX_SMOOTH_STEPS,
8952
9674
  VECTOR_BLEND_MAX_STEPS,
9675
+ VECTOR_BLEND_MODES,
9676
+ VECTOR_BLEND_MODE_LABELS,
9677
+ VECTOR_BLEND_MODE_MENU,
8953
9678
  VECTOR_BLEND_SPACINGS,
8954
9679
  VECTOR_BLEND_SPACING_LABELS,
8955
9680
  VECTOR_BUILDER_HATCH_GAP,
@@ -8967,6 +9692,11 @@ export {
8967
9692
  VECTOR_EFFECT_TYPES,
8968
9693
  VECTOR_ENVELOPE_STYLE_OPTIONS,
8969
9694
  VECTOR_FILL_GAP,
9695
+ VECTOR_FILTER_DEFAULTS,
9696
+ VECTOR_FILTER_LABELS,
9697
+ VECTOR_FILTER_MENU,
9698
+ VECTOR_FILTER_PARAM_SPECS,
9699
+ VECTOR_FILTER_TYPES,
8970
9700
  VECTOR_GRADIENT_INTERPOLATIONS,
8971
9701
  VECTOR_GRADIENT_INTERPOLATION_LABELS,
8972
9702
  VECTOR_GRADIENT_MAX_PERIODS,
@@ -8991,6 +9721,7 @@ export {
8991
9721
  VECTOR_STROKE_ALIGN_LABELS,
8992
9722
  VECTOR_STROKE_GRADIENT_MODES,
8993
9723
  VECTOR_STROKE_GRADIENT_MODE_LABELS,
9724
+ VECTOR_STROKE_SETTING_KEYS,
8994
9725
  VECTOR_STYLE_KEYS,
8995
9726
  VECTOR_TOOLS,
8996
9727
  VECTOR_TOOL_GLYPHS,
@@ -9006,6 +9737,7 @@ export {
9006
9737
  VectorEditorController,
9007
9738
  VectorSnapTargets,
9008
9739
  activeVectorEffects,
9740
+ activeVectorFilters,
9009
9741
  addBuilderHit,
9010
9742
  applyMatrix,
9011
9743
  applyMatrixLinear,
@@ -9037,6 +9769,7 @@ export {
9037
9769
  correspondMorphContours,
9038
9770
  createVectorCommands,
9039
9771
  createVectorEffect,
9772
+ createVectorFilter,
9040
9773
  createVectorMorph,
9041
9774
  curvatureMoveAnchor,
9042
9775
  cutSubpathAt,
@@ -9072,6 +9805,7 @@ export {
9072
9805
  getAnchorWorld,
9073
9806
  getEditablePathIds,
9074
9807
  getTransformHandlePositions,
9808
+ getVectorAppearancePanel,
9075
9809
  getVectorBlendControls,
9076
9810
  getVectorCornerWidgets,
9077
9811
  getVectorEffectsPanel,
@@ -9095,8 +9829,11 @@ export {
9095
9829
  hasStrokeExtras,
9096
9830
  hasVectorAppearance,
9097
9831
  hasVectorCorners,
9832
+ hasVectorPaintStacks,
9098
9833
  hitTestVectorNode,
9099
9834
  hitTestVectorNodes,
9835
+ hitTestVectorPaintLayer,
9836
+ hitTestVectorPaintStack,
9100
9837
  hitTestVectorStroke,
9101
9838
  hitVectorGradientAnnotator,
9102
9839
  inferAnchorType,
@@ -9113,10 +9850,14 @@ export {
9113
9850
  invertMatrix,
9114
9851
  isCentredVectorGradient,
9115
9852
  isClosedBezierShape,
9853
+ isDrawnVectorPaintLayer,
9116
9854
  isIdentityMatrix,
9855
+ isPlainVectorPaintLayer,
9117
9856
  isUniformWidthProfile,
9118
9857
  isValidWarpMesh as isValidVectorMesh,
9119
9858
  isVectorBlendGroup,
9859
+ isVectorBlendMode,
9860
+ isVectorFilterType,
9120
9861
  isVectorGradient,
9121
9862
  isVectorLiveBoolean,
9122
9863
  isVectorTextTarget,
@@ -9141,9 +9882,13 @@ export {
9141
9882
  multiplyMatrices,
9142
9883
  nearestSegment,
9143
9884
  nearestStrokePosition,
9885
+ needsVectorPaintLayers,
9886
+ newVectorStrokeLayer,
9144
9887
  nextVectorCornerKind,
9145
9888
  nodeMatrix,
9146
9889
  normalizeVectorEffectParams,
9890
+ normalizeVectorFilterParams,
9891
+ normalizeVectorPaintStacks,
9147
9892
  offsetPaths,
9148
9893
  orderQuad as orderVectorQuad,
9149
9894
  parseSvgTransform,
@@ -9189,7 +9934,11 @@ export {
9189
9934
  sampleVectorGradientAt,
9190
9935
  sampleVectorPaintDefAt,
9191
9936
  sanitizeVectorEffects,
9937
+ sanitizeVectorFills,
9938
+ sanitizeVectorFilters,
9192
9939
  sanitizeVectorGradient,
9940
+ sanitizeVectorStrokeSettings,
9941
+ sanitizeVectorStrokes,
9193
9942
  sanitizeWidthProfile,
9194
9943
  scaleFor,
9195
9944
  scaleMatrix,
@@ -9239,6 +9988,8 @@ export {
9239
9988
  vectorBlendColorDistance,
9240
9989
  vectorBlendKeyTargets,
9241
9990
  vectorBlendKeys,
9991
+ vectorBlendModeCss,
9992
+ vectorBlendModeFromCss,
9242
9993
  vectorBlendRail,
9243
9994
  vectorBlendRailAt,
9244
9995
  vectorBlendSpine,
@@ -9259,6 +10010,11 @@ export {
9259
10010
  vectorEnvelopeMap,
9260
10011
  vectorEnvelopeMesh,
9261
10012
  vectorFillMap,
10013
+ vectorFills,
10014
+ vectorFilterMarkup,
10015
+ vectorFilterParamKeys,
10016
+ vectorFilterReach,
10017
+ vectorFilterSvg,
9262
10018
  vectorGradientArea,
9263
10019
  vectorGradientAspect,
9264
10020
  vectorGradientBarPosition,
@@ -9290,9 +10046,13 @@ export {
9290
10046
  warpMeshRows as vectorMeshRows,
9291
10047
  vectorNodeBounds,
9292
10048
  vectorNodeEnvelope,
10049
+ vectorNodeFilterSvg,
10050
+ vectorNodeLocalPaintBounds,
9293
10051
  vectorNodeRotation,
9294
10052
  vectorNodeToBezier,
9295
10053
  vectorOperandOperation,
10054
+ vectorPaintLayerNodes,
10055
+ vectorPaintSwatch,
9296
10056
  vectorPaintToHex,
9297
10057
  vectorPaintToSvg,
9298
10058
  vectorParentMatrix,
@@ -9308,14 +10068,19 @@ export {
9308
10068
  vectorShapeBuilderMap,
9309
10069
  vectorShearForSide,
9310
10070
  vectorSkewMatrix,
10071
+ vectorStacksHaveGradient,
9311
10072
  vectorStrokeGradientDefs,
9312
10073
  vectorStrokeLineArea,
9313
10074
  vectorStrokeOutline,
9314
10075
  vectorStrokeOutlinePath,
9315
10076
  vectorStrokeParts,
9316
10077
  vectorStrokeReach,
10078
+ vectorStrokeSettings,
10079
+ vectorStrokeStackReach,
9317
10080
  vectorStrokeSvgParts,
9318
10081
  vectorStrokeWidthGeometry,
10082
+ vectorStrokes,
10083
+ vectorTargetPaint,
9319
10084
  vectorToolLabel,
9320
10085
  warpPresetGrid as vectorWarpPresetGrid,
9321
10086
  warpPresetMesh as vectorWarpPresetMesh,
@@ -9328,6 +10093,9 @@ export {
9328
10093
  widthProfilePresetId,
9329
10094
  widthProfileToAttribute,
9330
10095
  wigglePaths,
10096
+ withVectorFills,
10097
+ withVectorStrokes,
10098
+ withVectorTargetPaint,
9331
10099
  zigzagPaths
9332
10100
  };
9333
10101
  //# sourceMappingURL=index.js.map