@pixerate/editor 0.189.0 → 0.190.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.
Files changed (31) hide show
  1. package/dist/{chunk-ZEHTUR7Z.js → chunk-7ZWLDVFH.js} +971 -753
  2. package/dist/{chunk-ED4L4UD6.cjs → chunk-T4U4O6DS.cjs} +1005 -787
  3. package/dist/image-editor/controller.d.cts +51 -1
  4. package/dist/image-editor/controller.d.ts +51 -1
  5. package/dist/image-editor/free-transform.d.cts +3 -6
  6. package/dist/image-editor/free-transform.d.ts +3 -6
  7. package/dist/image-editor/index.cjs +1219 -901
  8. package/dist/image-editor/index.d.cts +1 -0
  9. package/dist/image-editor/index.d.ts +1 -0
  10. package/dist/image-editor/index.js +321 -3
  11. package/dist/image-editor/layer-tools.d.cts +1 -1
  12. package/dist/image-editor/layer-tools.d.ts +1 -1
  13. package/dist/image-editor/layers/compositor.d.cts +3 -0
  14. package/dist/image-editor/layers/compositor.d.ts +3 -0
  15. package/dist/image-editor/layers/model.d.cts +23 -0
  16. package/dist/image-editor/layers/model.d.ts +23 -0
  17. package/dist/image-editor/layers/panel.d.cts +11 -1
  18. package/dist/image-editor/layers/panel.d.ts +11 -1
  19. package/dist/image-editor/layers/serialize.d.cts +11 -0
  20. package/dist/image-editor/layers/serialize.d.ts +11 -0
  21. package/dist/image-editor/layers/shape.d.cts +5 -0
  22. package/dist/image-editor/layers/shape.d.ts +5 -0
  23. package/dist/image-editor/layers/vector-mask.d.cts +51 -0
  24. package/dist/image-editor/layers/vector-mask.d.ts +51 -0
  25. package/dist/image-editor/psd/import.d.cts +0 -7
  26. package/dist/image-editor/psd/import.d.ts +0 -7
  27. package/dist/paint-editor/index.cjs +6 -6
  28. package/dist/paint-editor/index.js +1 -1
  29. package/package.json +1 -1
  30. package/skills/pixerate-image-editor/SKILL.md +6 -0
  31. package/skills/pixerate-image-selection-masking/SKILL.md +6 -2
@@ -36,6 +36,7 @@ export * from './layers/ops.cjs';
36
36
  export * from './layers/smart.cjs';
37
37
  export * from './smart-tools.cjs';
38
38
  export * from './layers/shape.cjs';
39
+ export * from './layers/vector-mask.cjs';
39
40
  export * from './shape-tools.cjs';
40
41
  export * from './layers/text.cjs';
41
42
  export * from './layers/text-shapers.cjs';
@@ -36,6 +36,7 @@ export * from './layers/ops.js';
36
36
  export * from './layers/smart.js';
37
37
  export * from './smart-tools.js';
38
38
  export * from './layers/shape.js';
39
+ export * from './layers/vector-mask.js';
39
40
  export * from './shape-tools.js';
40
41
  export * from './layers/text.js';
41
42
  export * from './layers/text-shapers.js';
@@ -99,6 +99,7 @@ import {
99
99
  combineSelections,
100
100
  compileAdjustmentLayer,
101
101
  componentPath,
102
+ componentsCoverage,
102
103
  compositeDeep,
103
104
  computeSelectionBounds,
104
105
  contentsBox,
@@ -137,6 +138,7 @@ import {
137
138
  distanceTransform,
138
139
  documentContents,
139
140
  duplicateLayer,
141
+ effectiveLayerMask,
140
142
  encodeDeepSurface,
141
143
  encodeRle,
142
144
  encodeSelectionRle,
@@ -148,6 +150,7 @@ import {
148
150
  filterMaskCoverage,
149
151
  flattenPath,
150
152
  flipCorners,
153
+ followLayerMove,
151
154
  fullSelection,
152
155
  geometryHomography,
153
156
  getAnnotationBounds,
@@ -177,6 +180,7 @@ import {
177
180
  invertMatrix3,
178
181
  invertSelection,
179
182
  isActiveFilter,
183
+ isActiveVectorMask,
180
184
  isAdjustmentKind,
181
185
  isContentsDecoded,
182
186
  isDefaultDepthMask,
@@ -237,6 +241,7 @@ import {
237
241
  offsetLayer,
238
242
  offsetShape,
239
243
  offsetSmartObject,
244
+ offsetVectorMask,
240
245
  parseAdjustmentBlock,
241
246
  parseBlend,
242
247
  parseLfx2,
@@ -290,6 +295,7 @@ import {
290
295
  rerenderText,
291
296
  resizeSelection,
292
297
  resolveExternalAssets,
298
+ resolveVectorMasks,
293
299
  rgbToGray,
294
300
  rgbToHsl,
295
301
  runLayerFilters,
@@ -341,6 +347,7 @@ import {
341
347
  transformMap,
342
348
  transformShapeComponents,
343
349
  transformTypeLayer,
350
+ transformVectorMask,
344
351
  transformedBox,
345
352
  transformedPoint,
346
353
  translateLayer,
@@ -359,6 +366,9 @@ import {
359
366
  updateTypeLayer,
360
367
  userErrorMessage,
361
368
  uvToXy,
369
+ vectorMaskBounds,
370
+ vectorMaskCoverage,
371
+ vectorMaskThumbnail,
362
372
  versionedDescriptorBytes,
363
373
  vibranceLinear,
364
374
  vibrancePixel,
@@ -373,7 +383,7 @@ import {
373
383
  writePsd,
374
384
  xyToTempTint,
375
385
  xyToUv
376
- } from "../chunk-ZEHTUR7Z.js";
386
+ } from "../chunk-7ZWLDVFH.js";
377
387
  import {
378
388
  parseCssColor
379
389
  } from "../chunk-3CI7FRMK.js";
@@ -4164,6 +4174,24 @@ var FreeTransformSession = class {
4164
4174
  }
4165
4175
  /** Resamples every part for the current geometry and shows it (or returns it, for commit). */
4166
4176
  resample(final, preview = {}) {
4177
+ const out = this.resampleContent(final, preview);
4178
+ return { ...out, layers: this.moveVectorMask(out.layers) };
4179
+ }
4180
+ /** A linked vector mask follows the layer's content through the same document-space map. */
4181
+ moveVectorMask(layers) {
4182
+ const g = this.geometryValue;
4183
+ if (!this.layerId || this.copyId || !this.moves.includes("pixels") || isIdentityTransform(g)) return layers;
4184
+ const vm = findLayer(layers, this.layerId)?.layer.vectorMask;
4185
+ if (!vm || vm.linked === false) return layers;
4186
+ const warp = hasWarp(g);
4187
+ const f = warp ? transformMap(g) : null;
4188
+ const h = warp ? null : geometryHomography(g);
4189
+ const affine = !warp && h && Math.abs(h[6]) < 1e-12 && Math.abs(h[7]) < 1e-12 && Math.abs(h[8] - 1) < 1e-12 ? [h[0], h[3], h[1], h[4], h[2], h[5]] : void 0;
4190
+ const map = (p) => (f ? f(p) : h ? applyHomography(h, p) : p) ?? p;
4191
+ const moved = transformVectorMask(vm, map, affine);
4192
+ return mapLayer(layers, this.layerId, (l) => ({ ...l, vectorMask: moved }));
4193
+ }
4194
+ resampleContent(final, preview = {}) {
4167
4195
  if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
4168
4196
  if (this.typeTarget) return { layers: this.resampleType(), selection: this.startSelection };
4169
4197
  const g = this.geometryValue;
@@ -6167,6 +6195,7 @@ var ImageEditorController = class {
6167
6195
  let moved = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };
6168
6196
  const mask = layer.mask;
6169
6197
  if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };
6198
+ moved = followLayerMove(moved, mx, my);
6170
6199
  this.commitLayers(mapLayer(this.state.layers, id, () => moved), void 0, commitToHistory);
6171
6200
  const t = moved.transform ?? [1, 0, 0, 1, 0, 0];
6172
6201
  return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, bounds: this.getLayerBounds(id) };
@@ -6218,6 +6247,139 @@ var ImageEditorController = class {
6218
6247
  if (this.state.activeLayerId === id) this.state.activeTarget = "pixels";
6219
6248
  });
6220
6249
  }
6250
+ // --- Vector masks ---
6251
+ /** The layer for a vector mask command: not a shape layer (its path is its vector mask), not a depth-masked one. */
6252
+ vectorMaskLayer(id) {
6253
+ const layer = requireLayer(this.state.layers, id).layer;
6254
+ const name = layer.name || id;
6255
+ if (layer.type === "shape") throw new LayerOperationError(`Layer "${name}" is a shape layer, whose path already is its vector mask; change it with set_shape_component, or rasterize the layer first.`);
6256
+ if (layer.mask?.source) throw new LayerOperationError(`Layer "${name}" has a depth mask, which cannot be combined with a vector mask; remove the depth mask first.`);
6257
+ return layer;
6258
+ }
6259
+ requireVectorMask(id) {
6260
+ const layer = this.vectorMaskLayer(id);
6261
+ if (!layer.vectorMask) throw new LayerOperationError(`Layer "${layer.name || id}" has no vector mask; add one with add_vector_mask.`);
6262
+ return { layer, vm: layer.vectorMask };
6263
+ }
6264
+ /** Components from a path spec: `components`, or one `shape` / `subpaths`. */
6265
+ vectorMaskComponents(path) {
6266
+ if (path.components?.length && (path.shape !== void 0 || path.subpaths !== void 0)) throw new LayerOperationError("Give components, or shape / subpaths, not both.");
6267
+ if (path.fillRule !== void 0 && path.fillRule !== "nonzero" && path.fillRule !== "evenodd") throw new LayerOperationError("fillRule must be nonzero or evenodd.");
6268
+ const specs = path.components?.length ? path.components : [path];
6269
+ return specs.map((c, i) => this.componentFrom(c, path.components?.length ? `components[${i}].` : ""));
6270
+ }
6271
+ /**
6272
+ * Layer › Vector Mask: `reveal-all` (an empty path showing everything), `hide-all` (an empty path
6273
+ * hiding everything) or `path` (reveals inside the path given). Replaces an existing vector mask;
6274
+ * the raster mask is kept and both apply. One undo step. Returns the vector mask.
6275
+ */
6276
+ addVectorMask(id, from = "reveal-all", path = {}) {
6277
+ this.vectorMaskLayer(id);
6278
+ let vm;
6279
+ if (from === "path") {
6280
+ vm = { components: this.vectorMaskComponents(path) };
6281
+ if (path.fillRule === "evenodd") vm.fillRule = "evenodd";
6282
+ } else if (from === "reveal-all" || from === "hide-all") {
6283
+ if (path.components?.length || path.shape !== void 0 || path.subpaths !== void 0) throw new LayerOperationError(`A ${from} vector mask starts empty; use from path to give a path.`);
6284
+ vm = { components: [], ...from === "reveal-all" ? { inverted: true } : {} };
6285
+ } else {
6286
+ throw new LayerOperationError(`Unknown vector mask source "${String(from)}". Use reveal-all, hide-all or path.`);
6287
+ }
6288
+ this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: vm })));
6289
+ return vm;
6290
+ }
6291
+ /** Replaces a vector mask's path (its properties are kept). One undo step. Returns the vector mask. */
6292
+ setVectorMaskPath(id, path) {
6293
+ const { vm } = this.requireVectorMask(id);
6294
+ const next = { ...vm, components: this.vectorMaskComponents(path) };
6295
+ if (path.fillRule === "evenodd") next.fillRule = "evenodd";
6296
+ else if (path.fillRule === "nonzero") delete next.fillRule;
6297
+ this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: next })));
6298
+ return next;
6299
+ }
6300
+ /** Changes a vector mask's enabled, linked, inverted, density or feather (the path is kept). One undo step. */
6301
+ setVectorMaskProps(id, props) {
6302
+ const { vm } = this.requireVectorMask(id);
6303
+ if (props.density !== void 0 && !(props.density >= 0 && props.density <= 1)) throw new LayerOperationError("density must be from 0 to 1.");
6304
+ if (props.feather !== void 0 && !(props.feather >= 0 && props.feather <= 1e3)) throw new LayerOperationError("feather must be from 0 to 1000 px.");
6305
+ const next = { ...vm };
6306
+ for (const k of ["enabled", "linked", "inverted"]) {
6307
+ if (props[k] === void 0) continue;
6308
+ if (typeof props[k] !== "boolean") throw new LayerOperationError(`${k} must be true or false.`);
6309
+ if (k === "inverted" ? props[k] : !props[k]) next[k] = k === "inverted";
6310
+ else delete next[k];
6311
+ }
6312
+ if (props.density !== void 0) {
6313
+ if (props.density === 1) delete next.density;
6314
+ else next.density = props.density;
6315
+ }
6316
+ if (props.feather !== void 0) {
6317
+ if (props.feather === 0) delete next.feather;
6318
+ else next.feather = props.feather;
6319
+ }
6320
+ this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: next })));
6321
+ return next;
6322
+ }
6323
+ /** Deletes a layer's vector mask (one undo step). */
6324
+ removeVectorMask(id) {
6325
+ this.requireVectorMask(id);
6326
+ this.commitLayers(
6327
+ mapLayer(this.state.layers, id, (l) => {
6328
+ const next = { ...l };
6329
+ delete next.vectorMask;
6330
+ return next;
6331
+ })
6332
+ );
6333
+ }
6334
+ /**
6335
+ * Layer › Rasterize › Vector Mask: multiplies the path (with its density and feather) into the
6336
+ * layer's raster mask, adding one when there is none, and removes the vector mask. The raster
6337
+ * mask keeps its own properties. One undo step. Returns the raster mask.
6338
+ */
6339
+ rasterizeVectorMask(id) {
6340
+ const { layer } = this.requireVectorMask(id);
6341
+ const { width, height } = this.canvasSize;
6342
+ const raster = layer.mask;
6343
+ const plain3 = { ...layer, vectorMask: { ...layer.vectorMask, enabled: true }, mask: raster ? { surface: raster.surface, defaultValue: raster.defaultValue } : void 0 };
6344
+ const baked = effectiveLayerMask(plain3, width, height);
6345
+ const mask = raster ? { ...raster, surface: baked.surface, defaultValue: baked.defaultValue } : baked;
6346
+ this.commitLayers(
6347
+ mapLayer(this.state.layers, id, (l) => {
6348
+ const next = { ...l, mask };
6349
+ delete next.vectorMask;
6350
+ return next;
6351
+ })
6352
+ );
6353
+ return mask;
6354
+ }
6355
+ /** Load Selection from a vector mask's path (density and feather are not applied), inverted with `invert`. */
6356
+ selectionFromVectorMask(id, mode = "replace", invert2 = false) {
6357
+ const { vm } = this.requireVectorMask(id);
6358
+ const { width, height } = this.naturalSize;
6359
+ const cov = componentsCoverage(vm.components, { x: 0, y: 0, width, height }, vm.fillRule ?? "nonzero", vm.inverted === true !== invert2);
6360
+ const bytes = new Uint8Array(cov.length);
6361
+ for (let i = 0; i < cov.length; i++) bytes[i] = Math.round(Math.min(1, Math.max(0, cov[i])) * 255);
6362
+ return this.setSelection(createSelectionMask(width, height, bytes), mode);
6363
+ }
6364
+ /** A layer's vector mask with its paths (rounded), or `null` without one. */
6365
+ getVectorMask(id) {
6366
+ const layer = requireLayer(this.state.layers, id).layer;
6367
+ const vm = layer.vectorMask;
6368
+ if (!vm) return null;
6369
+ const b = vectorMaskBounds(vm);
6370
+ const r = (n) => Math.round(n * 1e3) / 1e3;
6371
+ return {
6372
+ layerId: id,
6373
+ enabled: vm.enabled !== false,
6374
+ linked: vm.linked !== false,
6375
+ inverted: vm.inverted === true,
6376
+ fillRule: vm.fillRule ?? "nonzero",
6377
+ density: vm.density ?? 1,
6378
+ feather: vm.feather ?? 0,
6379
+ bounds: b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null,
6380
+ components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) }))
6381
+ };
6382
+ }
6221
6383
  /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
6222
6384
  setLayerMaskProps(id, props, commitToHistory = true) {
6223
6385
  const layer = requireLayer(this.state.layers, id).layer;
@@ -11658,11 +11820,12 @@ function createImageSelectionTools(controller) {
11658
11820
  const loadFromMask = {
11659
11821
  name: "load_selection_from_mask",
11660
11822
  title: "Load a selection from a layer mask",
11661
- description: "Load Selection from a layer's mask (Ctrl/\u2318-click its mask thumbnail): the mask's pixels become the selection, combined by mode (density and feather are not applied). Use it to reuse a mask on another layer or to edit it as a selection. Fails when the layer has no painted mask. One undoable step. Returns the selection.",
11823
+ description: "Load Selection from a layer's mask (Ctrl/\u2318-click its mask thumbnail): the raster mask's pixels, or with source vector the vector mask's path, become the selection, combined by mode (density and feather are not applied). Use it to reuse a mask on another layer or to edit it as a selection. Fails when the layer has no such mask. One undoable step. Returns the selection.",
11662
11824
  inputSchema: {
11663
11825
  type: "object",
11664
11826
  properties: {
11665
11827
  layerId: { type: "string", minLength: 1, description: "The layer whose mask to load (see list_layers)." },
11828
+ source: { type: "string", enum: ["pixels", "vector"], description: "pixels: the raster (layer) mask (default); vector: the vector mask's path." },
11666
11829
  invert: { type: "boolean", description: "Select what the mask hides instead (default false)." },
11667
11830
  mode: modeSchema
11668
11831
  },
@@ -11674,6 +11837,11 @@ function createImageSelectionTools(controller) {
11674
11837
  const id = String(args.layerId ?? "");
11675
11838
  const layer = controller.getLayer(id);
11676
11839
  if (!layer) throw new Error(`No layer "${id}". Call list_layers for ids.`);
11840
+ if (args.source === "vector") {
11841
+ if (!layer.vectorMask) throw new Error(`Layer "${id}" has no vector mask. Add one with add_vector_mask, or load its pixels mask (source pixels).`);
11842
+ controller.selectionFromVectorMask(id, parseSelectionMode(args.mode), args.invert === true);
11843
+ return summary(controller);
11844
+ }
11677
11845
  if (!layer.mask) throw new Error(`Layer "${id}" has no mask. Add one with add_layer_mask, or pick a layer with a mask.`);
11678
11846
  if (layer.mask.source) throw new Error(`Layer "${id}" has a computed (depth) mask, not a painted one, so it cannot be loaded as a selection.`);
11679
11847
  controller.selectionFromLayerMask(id, parseSelectionMode(args.mode), args.invert === true);
@@ -12540,6 +12708,7 @@ function describeLayer(layer, activeId, boundsOf) {
12540
12708
  clipped: Boolean(layer.clipped),
12541
12709
  locks: layer.locks ? { ...layer.locks } : {},
12542
12710
  mask: maskSummary(layer.mask),
12711
+ vectorMask: layer.vectorMask ? { enabled: layer.vectorMask.enabled !== false, linked: layer.vectorMask.linked !== false, inverted: layer.vectorMask.inverted === true, components: layer.vectorMask.components.length, bounds: vectorMaskBounds(layer.vectorMask) } : null,
12543
12712
  effects: layer.effects ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) } : null,
12544
12713
  active: layer.id === activeId
12545
12714
  };
@@ -15726,7 +15895,122 @@ function createImageShapeTools(controller) {
15726
15895
  annotations: { title: "Merge shape components", ...DESTRUCTIVE6 },
15727
15896
  handler: ({ layerId }) => controller.mergeShapeComponents(shapeId(layerId))
15728
15897
  };
15729
- return [get, add2, update, setComponent, addComponent, removeComponent, merge];
15898
+ const layerArg = (v) => {
15899
+ const id = String(v ?? "");
15900
+ if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
15901
+ return id;
15902
+ };
15903
+ const pathSpec = (args) => {
15904
+ const spec = { ...geometry(args) };
15905
+ if (args.components !== void 0) spec.components = args.components.map(geometry);
15906
+ if (args.fillRule !== void 0) spec.fillRule = args.fillRule;
15907
+ return spec;
15908
+ };
15909
+ const PATH_PROPS = {
15910
+ ...GEOMETRY2,
15911
+ components: {
15912
+ type: "array",
15913
+ minItems: 1,
15914
+ description: "Several components instead of shape / subpaths, bottom first, each with its path operation (combine, subtract, intersect, exclude).",
15915
+ items: { type: "object", description: "One component.", properties: { ...GEOMETRY2, op: OP }, additionalProperties: false }
15916
+ },
15917
+ fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." }
15918
+ };
15919
+ const LAYER = { type: "string", minLength: 1, description: "The layer (see list_layers). Not a shape layer: its path already is its vector mask." };
15920
+ const getVectorMask = {
15921
+ name: "get_vector_mask",
15922
+ title: "Read a vector mask",
15923
+ description: "Reads a layer's vector mask: enabled, linked, inverted, fill rule, density, feather, the bounds of its path and its components (op, live shape, subpaths in document pixels). Use it before set_vector_mask_path to edit the path. Returns null when the layer has none.",
15924
+ inputSchema: { type: "object", properties: { layerId: LAYER }, required: ["layerId"], additionalProperties: false },
15925
+ annotations: { title: "Read a vector mask", ...READ10 },
15926
+ handler: ({ layerId }) => ({ vectorMask: controller.getVectorMask(layerArg(layerId)) })
15927
+ };
15928
+ const addVectorMask = {
15929
+ name: "add_vector_mask",
15930
+ title: "Add a vector mask",
15931
+ description: "Layer \u203A Vector Mask: a path that reveals the layer inside it and stays sharp at any size, on top of any raster mask (both apply). from path takes a live shape, free subpaths or components like add_shape_layer; reveal-all and hide-all start with an empty path. Use it for hard-edged geometric or traced cutouts that must stay editable; use add_layer_mask for soft or painted ones. A linked vector mask moves and transforms with the layer. Replaces an existing vector mask. One undo step. Returns the vector mask.",
15932
+ inputSchema: {
15933
+ type: "object",
15934
+ $defs: DEFS2,
15935
+ properties: {
15936
+ layerId: LAYER,
15937
+ from: { type: "string", enum: ["path", "reveal-all", "hide-all"], description: "path (default when a shape, subpaths or components are given), reveal-all or hide-all (an empty path)." },
15938
+ ...PATH_PROPS
15939
+ },
15940
+ required: ["layerId"],
15941
+ additionalProperties: false
15942
+ },
15943
+ annotations: { title: "Add a vector mask", ...WRITE8 },
15944
+ handler: (args) => {
15945
+ const id = layerArg(args.layerId);
15946
+ const spec = pathSpec(args);
15947
+ const hasPath = spec.components?.length || spec.shape !== void 0 || spec.subpaths !== void 0;
15948
+ const from = args.from ?? (hasPath ? "path" : "reveal-all");
15949
+ controller.addVectorMask(id, from, spec);
15950
+ return { vectorMask: controller.getVectorMask(id) };
15951
+ }
15952
+ };
15953
+ const setVectorMaskPath = {
15954
+ name: "set_vector_mask_path",
15955
+ title: "Replace a vector mask path",
15956
+ description: "Replaces the path of a layer's vector mask (shape, subpaths or components, document pixels); enabled, linked, inverted, density and feather are kept. Read the current path with get_vector_mask first to change part of it. One undo step. Returns the vector mask.",
15957
+ inputSchema: { type: "object", $defs: DEFS2, properties: { layerId: LAYER, ...PATH_PROPS }, required: ["layerId"], additionalProperties: false },
15958
+ annotations: { title: "Replace a vector mask path", ...WRITE_IDEMPOTENT5 },
15959
+ handler: (args) => {
15960
+ const id = layerArg(args.layerId);
15961
+ controller.setVectorMaskPath(id, pathSpec(args));
15962
+ return { vectorMask: controller.getVectorMask(id) };
15963
+ }
15964
+ };
15965
+ const setVectorMaskProps = {
15966
+ name: "set_vector_mask_properties",
15967
+ title: "Set vector mask properties",
15968
+ description: "Changes a layer's vector mask without touching its path: enabled (false ignores it), linked (false: it stays put when the layer moves or transforms), inverted (reveal outside the path), density 0..1 and feather in px (soften the edge without losing the path). One undo step. Returns the vector mask.",
15969
+ inputSchema: {
15970
+ type: "object",
15971
+ properties: {
15972
+ layerId: LAYER,
15973
+ enabled: { type: "boolean", description: "false ignores the vector mask but keeps it." },
15974
+ linked: { type: "boolean", description: "false keeps the path in place when the layer moves or transforms." },
15975
+ inverted: { type: "boolean", description: "true reveals outside the path instead." },
15976
+ density: { type: "number", minimum: 0, maximum: 1, description: "Density 0..1 (default 1): at 0.5 the hidden area still shows half." },
15977
+ feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px (default 0)." }
15978
+ },
15979
+ required: ["layerId"],
15980
+ additionalProperties: false
15981
+ },
15982
+ annotations: { title: "Set vector mask properties", ...WRITE_IDEMPOTENT5 },
15983
+ handler: ({ layerId, ...props }) => {
15984
+ const id = layerArg(layerId);
15985
+ controller.setVectorMaskProps(id, props);
15986
+ return { vectorMask: controller.getVectorMask(id) };
15987
+ }
15988
+ };
15989
+ const rasterizeVectorMask = {
15990
+ name: "rasterize_vector_mask",
15991
+ title: "Rasterize a vector mask",
15992
+ description: "Layer \u203A Rasterize \u203A Vector Mask: multiplies the path (with its density and feather) into the layer's raster mask, adding one if needed, and removes the vector mask, so the mask can be painted. The path can no longer be edited (undo_image brings it back). One undo step. Returns the layer's raster mask summary.",
15993
+ inputSchema: { type: "object", properties: { layerId: LAYER }, required: ["layerId"], additionalProperties: false },
15994
+ annotations: { title: "Rasterize a vector mask", ...DESTRUCTIVE6 },
15995
+ handler: ({ layerId }) => {
15996
+ const id = layerArg(layerId);
15997
+ const mask = controller.rasterizeVectorMask(id);
15998
+ return { layerId: id, mask: { defaultValue: mask.defaultValue, density: mask.density ?? 1, feather: mask.feather ?? 0, enabled: mask.enabled !== false } };
15999
+ }
16000
+ };
16001
+ const removeVectorMask = {
16002
+ name: "remove_vector_mask",
16003
+ title: "Delete a vector mask",
16004
+ description: "Deletes a layer's vector mask without applying it (the raster mask, if any, stays). Disable it instead with set_vector_mask_properties enabled false to keep it. One undo step. Returns the layer id.",
16005
+ inputSchema: { type: "object", properties: { layerId: LAYER }, required: ["layerId"], additionalProperties: false },
16006
+ annotations: { title: "Delete a vector mask", ...DESTRUCTIVE6 },
16007
+ handler: ({ layerId }) => {
16008
+ const id = layerArg(layerId);
16009
+ controller.removeVectorMask(id);
16010
+ return { layerId: id, vectorMask: null };
16011
+ }
16012
+ };
16013
+ return [get, add2, update, setComponent, addComponent, removeComponent, merge, getVectorMask, addVectorMask, setVectorMaskPath, setVectorMaskProps, rasterizeVectorMask, removeVectorMask];
15730
16014
  }
15731
16015
 
15732
16016
  // src/image-editor/type-tools.ts
@@ -16393,6 +16677,11 @@ function maskThumbnailClick(controller, layer, e) {
16393
16677
  controller.setActiveLayer(layer.id, "mask");
16394
16678
  }
16395
16679
  }
16680
+ function vectorMaskThumbnailClick(controller, layer, e) {
16681
+ if (!layer.vectorMask) return;
16682
+ if (e.ctrlKey || e.metaKey) controller.selectionFromVectorMask(layer.id, selectionModeFromModifiers(e, "replace"));
16683
+ else if (e.shiftKey) controller.setVectorMaskProps(layer.id, { enabled: layer.vectorMask.enabled === false });
16684
+ }
16396
16685
  function layerThumbnailClick(controller, layer, e) {
16397
16686
  if (!(e.ctrlKey || e.metaKey)) return false;
16398
16687
  controller.selectLayerTransparency(layer.id, { mode: selectionModeFromModifiers(e, "replace") });
@@ -16491,6 +16780,11 @@ function layerMenu(controller) {
16491
16780
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
16492
16781
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
16493
16782
  { action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
16783
+ { action: "add-vector-mask", label: "Add Vector Mask (Reveal All)", enabled: Boolean(active && active.type !== "shape" && !active.mask?.source) },
16784
+ { action: "add-vector-mask-hide", label: "Add Vector Mask (Hide All)", enabled: Boolean(active && active.type !== "shape" && !active.mask?.source) },
16785
+ { action: "delete-vector-mask", label: "Delete Vector Mask", enabled: Boolean(active?.vectorMask) },
16786
+ { action: "toggle-vector-mask", label: active?.vectorMask?.enabled === false ? "Enable Vector Mask" : "Disable Vector Mask", enabled: Boolean(active?.vectorMask) },
16787
+ { action: "rasterize-vector-mask", label: "Rasterize Vector Mask", enabled: Boolean(active?.vectorMask) },
16494
16788
  { action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || active?.type === "type" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
16495
16789
  { action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
16496
16790
  { action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
@@ -16562,6 +16856,19 @@ async function runLayerMenuAction(controller, action) {
16562
16856
  case "toggle-mask-link":
16563
16857
  if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });
16564
16858
  break;
16859
+ case "add-vector-mask":
16860
+ case "add-vector-mask-hide":
16861
+ if (id) controller.addVectorMask(id, action === "add-vector-mask" ? "reveal-all" : "hide-all");
16862
+ break;
16863
+ case "delete-vector-mask":
16864
+ if (id) controller.removeVectorMask(id);
16865
+ break;
16866
+ case "toggle-vector-mask":
16867
+ if (id && active?.vectorMask) controller.setVectorMaskProps(id, { enabled: active.vectorMask.enabled === false });
16868
+ break;
16869
+ case "rasterize-vector-mask":
16870
+ if (id) controller.rasterizeVectorMask(id);
16871
+ break;
16565
16872
  case "rasterize":
16566
16873
  if (id) await controller.rasterizeLayer(id);
16567
16874
  break;
@@ -18154,6 +18461,7 @@ export {
18154
18461
  combineSelections,
18155
18462
  compileAdjustmentLayer,
18156
18463
  componentPath,
18464
+ componentsCoverage,
18157
18465
  compositeDeep,
18158
18466
  compositeSurfaceOver,
18159
18467
  computeHistogram,
@@ -18273,6 +18581,7 @@ export {
18273
18581
  effectLayerTiming,
18274
18582
  effectPixelsToSurface,
18275
18583
  effectReach,
18584
+ effectiveLayerMask,
18276
18585
  effectiveProof,
18277
18586
  effectsFingerprint,
18278
18587
  effectsMargin,
@@ -18309,6 +18618,7 @@ export {
18309
18618
  flattenPath,
18310
18619
  flipCorners,
18311
18620
  flowField,
18621
+ followLayerMove,
18312
18622
  fontOf,
18313
18623
  formatControlValue,
18314
18624
  fullSelection,
@@ -18373,6 +18683,7 @@ export {
18373
18683
  invertMatrix3,
18374
18684
  invertSelection,
18375
18685
  isActiveFilter,
18686
+ isActiveVectorMask,
18376
18687
  isAdjustmentKind,
18377
18688
  isBitDepth,
18378
18689
  isBlendMode,
@@ -18512,6 +18823,7 @@ export {
18512
18823
  offsetLayer,
18513
18824
  offsetShape,
18514
18825
  offsetSmartObject,
18826
+ offsetVectorMask,
18515
18827
  openMaskFile,
18516
18828
  openPsdFile,
18517
18829
  packRgba,
@@ -18632,6 +18944,7 @@ export {
18632
18944
  resolveMosaicRegionBounds,
18633
18945
  resolvePaintGradient,
18634
18946
  resolveProfile,
18947
+ resolveVectorMasks,
18635
18948
  restyleRange,
18636
18949
  retouchFilter,
18637
18950
  reversePaintGradient,
@@ -18761,6 +19074,7 @@ export {
18761
19074
  transformShapeComponents,
18762
19075
  transformSurface,
18763
19076
  transformTypeLayer,
19077
+ transformVectorMask,
18764
19078
  transformedBounds,
18765
19079
  transformedBox,
18766
19080
  transformedPoint,
@@ -18787,6 +19101,10 @@ export {
18787
19101
  validateEffectGradient,
18788
19102
  validateLayerEffect,
18789
19103
  validateLayerEffects,
19104
+ vectorMaskBounds,
19105
+ vectorMaskCoverage,
19106
+ vectorMaskThumbnail,
19107
+ vectorMaskThumbnailClick,
18790
19108
  versionedDescriptorBytes,
18791
19109
  vibranceLinear,
18792
19110
  vibrancePixel,
@@ -6,7 +6,7 @@ import type { CropBox } from './types.cjs';
6
6
  export declare function maskSummary(mask: RasterMask | undefined): Record<string, unknown> | null;
7
7
  /**
8
8
  * A plain description of a layer (and its children) for agents: ids, types, names, document
9
- * bounds, visibility, opacity, fill, blend, clipping, locks, mask and effects summaries and
9
+ * bounds, visibility, opacity, fill, blend, clipping, locks, mask, vector mask and effects summaries and
10
10
  * whether it is active. `boundsOf` gives a pixel layer's document bounds.
11
11
  */
12
12
  export declare function describeLayer(layer: ImageLayer, activeId: string | null, boundsOf: (layer: ImageLayer) => CropBox | null): Record<string, unknown>;
@@ -6,7 +6,7 @@ import type { CropBox } from './types.js';
6
6
  export declare function maskSummary(mask: RasterMask | undefined): Record<string, unknown> | null;
7
7
  /**
8
8
  * A plain description of a layer (and its children) for agents: ids, types, names, document
9
- * bounds, visibility, opacity, fill, blend, clipping, locks, mask and effects summaries and
9
+ * bounds, visibility, opacity, fill, blend, clipping, locks, mask, vector mask and effects summaries and
10
10
  * whether it is active. `boundsOf` gives a pixel layer's document bounds.
11
11
  */
12
12
  export declare function describeLayer(layer: ImageLayer, activeId: string | null, boundsOf: (layer: ImageLayer) => CropBox | null): Record<string, unknown>;
@@ -42,6 +42,9 @@ export declare function runLayerFiltersFloat(data: Float32Array, width: number,
42
42
  /** The per-tile compositor for image editor layer stacks (see the raster module for the caches). */
43
43
  export declare class LayerCompositor extends RasterCompositor<ImageLayer, CompositorSources> {
44
44
  constructor(options?: CompositorOptions);
45
+ composite(layers: readonly ImageLayer[], rect: RasterRect, sources: CompositorSources, options?: CompositeCallOptions): Uint8ClampedArray;
46
+ compositeTiles(layers: readonly ImageLayer[], rect: RasterRect, sources: CompositorSources, onTile: (tx: number, ty: number, data: Uint8ClampedArray, changed: boolean) => void, options?: CompositeCallOptions): void;
47
+ tile(layers: readonly ImageLayer[], tx: number, ty: number, sources: CompositorSources): Uint8ClampedArray;
45
48
  }
46
49
  /**
47
50
  * The composite of an image editor stack at 16 or 32 bits (see `raster/deep-compositor`): float
@@ -42,6 +42,9 @@ export declare function runLayerFiltersFloat(data: Float32Array, width: number,
42
42
  /** The per-tile compositor for image editor layer stacks (see the raster module for the caches). */
43
43
  export declare class LayerCompositor extends RasterCompositor<ImageLayer, CompositorSources> {
44
44
  constructor(options?: CompositorOptions);
45
+ composite(layers: readonly ImageLayer[], rect: RasterRect, sources: CompositorSources, options?: CompositeCallOptions): Uint8ClampedArray;
46
+ compositeTiles(layers: readonly ImageLayer[], rect: RasterRect, sources: CompositorSources, onTile: (tx: number, ty: number, data: Uint8ClampedArray, changed: boolean) => void, options?: CompositeCallOptions): void;
47
+ tile(layers: readonly ImageLayer[], tx: number, ty: number, sources: CompositorSources): Uint8ClampedArray;
45
48
  }
46
49
  /**
47
50
  * The composite of an image editor stack at 16 or 32 bits (see `raster/deep-compositor`): float
@@ -28,6 +28,29 @@ export type { LayerLocks };
28
28
  export type RasterMask = BaseRasterMask<DepthMaskOptions>;
29
29
  export interface LayerBase extends RasterLayerBase {
30
30
  mask?: RasterMask;
31
+ /** A vector mask (Photoshop's): a path that reveals the layer, on top of `mask`. Not on shape layers. */
32
+ vectorMask?: VectorMask;
33
+ }
34
+ /**
35
+ * A vector mask: shape components in document pixels (the same paths shape layers use) that
36
+ * reveal the layer inside them (outside with `inverted`). It stays sharp at any size; the
37
+ * compositor multiplies it with the layer's raster mask. No components hides everything
38
+ * (reveals everything when inverted).
39
+ */
40
+ export interface VectorMask {
41
+ components: ShapeComponent[];
42
+ /** How overlapping subpaths fill (default nonzero). */
43
+ fillRule?: ShapeFillRule;
44
+ /** Reveal outside the path instead (default false). */
45
+ inverted?: boolean;
46
+ /** Disabled masks are kept but ignored (default true). */
47
+ enabled?: boolean;
48
+ /** `false`: the path stays put when the layer moves or transforms (default true). */
49
+ linked?: boolean;
50
+ /** 0..1 (default 1): at 0.5 the hidden area still shows half. */
51
+ density?: number;
52
+ /** Gaussian feather σ in px (default 0). */
53
+ feather?: number;
31
54
  }
32
55
  /** The base sliders of the old document-level adjustments, as a filter on one layer. */
33
56
  export interface DevelopSettings {
@@ -28,6 +28,29 @@ export type { LayerLocks };
28
28
  export type RasterMask = BaseRasterMask<DepthMaskOptions>;
29
29
  export interface LayerBase extends RasterLayerBase {
30
30
  mask?: RasterMask;
31
+ /** A vector mask (Photoshop's): a path that reveals the layer, on top of `mask`. Not on shape layers. */
32
+ vectorMask?: VectorMask;
33
+ }
34
+ /**
35
+ * A vector mask: shape components in document pixels (the same paths shape layers use) that
36
+ * reveal the layer inside them (outside with `inverted`). It stays sharp at any size; the
37
+ * compositor multiplies it with the layer's raster mask. No components hides everything
38
+ * (reveals everything when inverted).
39
+ */
40
+ export interface VectorMask {
41
+ components: ShapeComponent[];
42
+ /** How overlapping subpaths fill (default nonzero). */
43
+ fillRule?: ShapeFillRule;
44
+ /** Reveal outside the path instead (default false). */
45
+ inverted?: boolean;
46
+ /** Disabled masks are kept but ignored (default true). */
47
+ enabled?: boolean;
48
+ /** `false`: the path stays put when the layer moves or transforms (default true). */
49
+ linked?: boolean;
50
+ /** 0..1 (default 1): at 0.5 the hidden area still shows half. */
51
+ density?: number;
52
+ /** Gaussian feather σ in px (default 0). */
53
+ feather?: number;
31
54
  }
32
55
  /** The base sliders of the old document-level adjustments, as a filter on one layer. */
33
56
  export interface DevelopSettings {
@@ -18,6 +18,16 @@ export declare function maskThumbnailClick(controller: ImageEditorController, la
18
18
  shiftKey?: boolean;
19
19
  altKey?: boolean;
20
20
  }): void;
21
+ /**
22
+ * A click on a layer's vector mask thumbnail: Ctrl/⌘-click loads the path as the selection (Shift
23
+ * adds, Alt subtracts, both intersect) and Shift-click turns the vector mask on or off.
24
+ */
25
+ export declare function vectorMaskThumbnailClick(controller: ImageEditorController, layer: ImageLayer, e: {
26
+ ctrlKey?: boolean;
27
+ metaKey?: boolean;
28
+ shiftKey?: boolean;
29
+ altKey?: boolean;
30
+ }): void;
21
31
  /**
22
32
  * A click on a layer's pixel thumbnail: Ctrl/⌘-click loads the layer's transparency as the
23
33
  * selection (Shift adds, Alt subtracts, both intersect) and returns true; other clicks are left
@@ -57,7 +67,7 @@ export type DropZone = 'above' | 'below' | 'inside';
57
67
  export declare function dropZone(fraction: number, isGroup: boolean): DropZone;
58
68
  /** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */
59
69
  export declare function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null;
60
- export type LayerMenuAction = 'new-layer' | 'new-group' | 'duplicate' | 'delete' | 'group-selected' | 'ungroup' | 'merge-down' | 'merge-visible' | 'flatten' | 'via-copy' | 'via-cut' | 'add-mask' | 'add-mask-selection' | 'add-mask-hide-selection' | 'add-mask-transparency' | 'select-transparency' | 'apply-mask' | 'delete-mask' | 'toggle-mask' | 'toggle-mask-link' | 'toggle-clip' | 'rasterize' | 'convert-smart' | 'edit-contents' | 'smart-via-copy' | 'add-filter-mask';
70
+ export type LayerMenuAction = 'new-layer' | 'new-group' | 'duplicate' | 'delete' | 'group-selected' | 'ungroup' | 'merge-down' | 'merge-visible' | 'flatten' | 'via-copy' | 'via-cut' | 'add-mask' | 'add-vector-mask' | 'add-vector-mask-hide' | 'delete-vector-mask' | 'toggle-vector-mask' | 'rasterize-vector-mask' | 'add-mask-selection' | 'add-mask-hide-selection' | 'add-mask-transparency' | 'select-transparency' | 'apply-mask' | 'delete-mask' | 'toggle-mask' | 'toggle-mask-link' | 'toggle-clip' | 'rasterize' | 'convert-smart' | 'edit-contents' | 'smart-via-copy' | 'add-filter-mask';
61
71
  export interface LayerMenuItem {
62
72
  action: LayerMenuAction;
63
73
  label: string;