@pixerate/editor 0.190.0 → 0.191.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.
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  loadOpenTypeFont,
3
- parseOpenTypeFont
4
- } from "../chunk-M7XIC2FC.js";
3
+ parseOpenTypeFont,
4
+ traceField
5
+ } from "../chunk-RQ2EM5TY.js";
5
6
  import {
6
7
  ADJUSTMENT_KEYS,
7
8
  ADJUSTMENT_KINDS,
@@ -383,7 +384,7 @@ import {
383
384
  writePsd,
384
385
  xyToTempTint,
385
386
  xyToUv
386
- } from "../chunk-7ZWLDVFH.js";
387
+ } from "../chunk-TXYXF32N.js";
387
388
  import {
388
389
  parseCssColor
389
390
  } from "../chunk-3CI7FRMK.js";
@@ -630,6 +631,7 @@ import {
630
631
  roundSubpaths,
631
632
  spliceRuns,
632
633
  styleAt,
634
+ subpathsToBezier,
633
635
  textIndexAt,
634
636
  translateMatrix,
635
637
  warpMeshColumns,
@@ -641,7 +643,9 @@ import {
641
643
  } from "../chunk-LPIKXPBM.js";
642
644
  import "../chunk-YXKLVD56.js";
643
645
  import "../chunk-M6DNZNVY.js";
644
- import "../chunk-WEIK3UYB.js";
646
+ import {
647
+ bezierPathToSvg
648
+ } from "../chunk-WEIK3UYB.js";
645
649
  import "../chunk-PWBZH3FK.js";
646
650
  import {
647
651
  EffectUnsupportedError,
@@ -2426,6 +2430,67 @@ function hexRgb(hex) {
2426
2430
  return [v >> 16 & 255, v >> 8 & 255, v & 255];
2427
2431
  }
2428
2432
 
2433
+ // src/image-editor/selection/path.ts
2434
+ function selectionToSubpaths(mask, options = {}) {
2435
+ const b = mask.bounds;
2436
+ if (!b) return [];
2437
+ const x0 = Math.max(0, b.x - 1);
2438
+ const y0 = Math.max(0, b.y - 1);
2439
+ const x1 = Math.min(mask.width, b.x + b.width + 1);
2440
+ const y1 = Math.min(mask.height, b.y + b.height + 1);
2441
+ const w = x1 - x0;
2442
+ const h = y1 - y0;
2443
+ const data = mask.data;
2444
+ const field = new Float32Array(w * h);
2445
+ for (let y = 0; y < h; y++) {
2446
+ const row = (y0 + y) * mask.width + x0;
2447
+ for (let x = 0; x < w; x++) field[y * w + x] = data[row + x] / 255;
2448
+ }
2449
+ const threshold = Math.min(254, Math.max(1, options.threshold ?? 128)) / 255;
2450
+ const subpaths = traceField(field, w, h, threshold, {
2451
+ tolerance: Math.max(0.1, options.tolerance ?? 0.5),
2452
+ cornerRoundness: options.cornerRoundness ?? 0.5,
2453
+ minArea: options.minArea ?? 4,
2454
+ squareCorners: true
2455
+ });
2456
+ return subpaths.map((sp) => mapSubpaths([sp], (p) => ({ x: p.x + x0, y: p.y + y0 }))[0]);
2457
+ }
2458
+ function subpathsToSvgPath(subpaths, precision = 2) {
2459
+ return bezierPathToSvg(subpathsToBezier(subpaths), { precision });
2460
+ }
2461
+ function pathToSelection(components, width, height, options = {}) {
2462
+ let x0 = Infinity;
2463
+ let y0 = Infinity;
2464
+ let x1 = -Infinity;
2465
+ let y1 = -Infinity;
2466
+ for (const c of components) {
2467
+ for (const sp of c.subpaths) {
2468
+ for (const a of sp.anchors) {
2469
+ for (const p of [a, a.in, a.out]) {
2470
+ if (!p) continue;
2471
+ x0 = Math.min(x0, p.x);
2472
+ y0 = Math.min(y0, p.y);
2473
+ x1 = Math.max(x1, p.x);
2474
+ y1 = Math.max(y1, p.y);
2475
+ }
2476
+ }
2477
+ }
2478
+ }
2479
+ const rx0 = Math.max(0, Math.floor(x0));
2480
+ const ry0 = Math.max(0, Math.floor(y0));
2481
+ const rx1 = Math.min(width, Math.ceil(x1));
2482
+ const ry1 = Math.min(height, Math.ceil(y1));
2483
+ if (!(rx1 > rx0 && ry1 > ry0)) return createSelectionMask(width, height);
2484
+ const rect = { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 };
2485
+ const cov = componentsCoverage(components, rect, options.fillRule ?? "nonzero");
2486
+ const hard = options.antiAlias === false;
2487
+ const bytes = new Uint8Array(cov.length);
2488
+ for (let i = 0; i < cov.length; i++) bytes[i] = hard ? cov[i] >= 0.5 ? 255 : 0 : coverageToByte(cov[i]);
2489
+ const mask = selectionFromRegion(width, height, rect, bytes);
2490
+ const feather = options.feather ?? 0;
2491
+ return feather > 0 && mask.bounds ? modifySelection(mask, "feather", feather) : mask;
2492
+ }
2493
+
2429
2494
  // src/image-editor/layers/shape-type-ui.ts
2430
2495
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
2431
2496
  kind: "rect",
@@ -6273,21 +6338,61 @@ var ImageEditorController = class {
6273
6338
  * hiding everything) or `path` (reveals inside the path given). Replaces an existing vector mask;
6274
6339
  * the raster mask is kept and both apply. One undo step. Returns the vector mask.
6275
6340
  */
6276
- addVectorMask(id, from = "reveal-all", path = {}) {
6341
+ addVectorMask(id, from = "reveal-all", path = {}, trace2 = {}) {
6277
6342
  this.vectorMaskLayer(id);
6278
6343
  let vm;
6279
- if (from === "path") {
6344
+ if (from === "selection") {
6345
+ const subpaths = this.traceSelection(trace2);
6346
+ vm = { components: [{ op: "combine", subpaths }], fillRule: "evenodd" };
6347
+ } else if (from === "path") {
6280
6348
  vm = { components: this.vectorMaskComponents(path) };
6281
6349
  if (path.fillRule === "evenodd") vm.fillRule = "evenodd";
6282
6350
  } else if (from === "reveal-all" || from === "hide-all") {
6283
6351
  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
6352
  vm = { components: [], ...from === "reveal-all" ? { inverted: true } : {} };
6285
6353
  } else {
6286
- throw new LayerOperationError(`Unknown vector mask source "${String(from)}". Use reveal-all, hide-all or path.`);
6354
+ throw new LayerOperationError(`Unknown vector mask source "${String(from)}". Use reveal-all, hide-all, path or selection.`);
6287
6355
  }
6288
6356
  this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: vm })));
6289
6357
  return vm;
6290
6358
  }
6359
+ /** The selection's outline as subpaths; throws when nothing is selected or nothing is left to trace. */
6360
+ traceSelection(options) {
6361
+ const sel = this.state.selection;
6362
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then make a path from the selection.");
6363
+ if (options.tolerance !== void 0 && !(options.tolerance >= 0.1 && options.tolerance <= 10)) throw new LayerOperationError("tolerance must be from 0.1 to 10 px.");
6364
+ const subpaths = selectionToSubpaths(sel, options);
6365
+ if (!subpaths.length) throw new LayerOperationError("The selection is too small or too faint to trace (below minArea, or under half selected); lower minArea or threshold.");
6366
+ return subpaths;
6367
+ }
6368
+ /**
6369
+ * Make Work Path: the selection's outline as closed Bézier subpaths in document pixels, holes
6370
+ * included (fill them even-odd), within `tolerance` px. Changes nothing. Throws without a selection.
6371
+ */
6372
+ selectionToPath(options = {}) {
6373
+ const subpaths = this.traceSelection(options);
6374
+ const b = vectorMaskBounds({ components: [{ op: "combine", subpaths }] });
6375
+ const r = (n) => Math.round(n * 100) / 100;
6376
+ return {
6377
+ subpaths: roundSubpaths(subpaths),
6378
+ svg: subpathsToSvgPath(subpaths),
6379
+ fillRule: "evenodd",
6380
+ bounds: b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null,
6381
+ anchors: subpaths.reduce((n, sp) => n + sp.anchors.length, 0)
6382
+ };
6383
+ }
6384
+ /**
6385
+ * Make Selection: fills a path (`shape`, `subpaths` or `components`, document pixels) into a
6386
+ * selection combined with the current one by `mode`, with `feather` px and `antiAlias`. One
6387
+ * undo step. Returns whether anything is selected.
6388
+ */
6389
+ pathToSelection(path, options = {}) {
6390
+ if (options.feather !== void 0 && !(options.feather >= 0 && options.feather <= 1e3)) throw new LayerOperationError("feather must be from 0 to 1000 px.");
6391
+ const components = this.vectorMaskComponents(path);
6392
+ const { width, height } = this.naturalSize;
6393
+ const mask = pathToSelection(components, width, height, { ...options, fillRule: path.fillRule ?? options.fillRule });
6394
+ return this.setSelection(mask, options.mode ?? "replace");
6395
+ }
6291
6396
  /** Replaces a vector mask's path (its properties are kept). One undo step. Returns the vector mask. */
6292
6397
  setVectorMaskPath(id, path) {
6293
6398
  const { vm } = this.requireVectorMask(id);
@@ -11150,7 +11255,7 @@ var DEPTH_MASK_CONTROLS = [
11150
11255
  // src/image-editor/playback.ts
11151
11256
  var IMAGE_PLAYBACK_KIND = "layer";
11152
11257
  var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
11153
- var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask)$/;
11258
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection)$/;
11154
11259
  var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
11155
11260
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
11156
11261
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -15917,6 +16022,52 @@ function createImageShapeTools(controller) {
15917
16022
  fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." }
15918
16023
  };
15919
16024
  const LAYER = { type: "string", minLength: 1, description: "The layer (see list_layers). Not a shape layer: its path already is its vector mask." };
16025
+ const TRACE_PROPS = {
16026
+ tolerance: { type: "number", minimum: 0.1, maximum: 10, description: "How far (px) the path may stray from the selection edge (default 0.5; lower is tighter, more anchors)." },
16027
+ cornerRoundness: { type: "number", minimum: 0, maximum: 1, description: "0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5)." },
16028
+ minArea: { type: "number", minimum: 0, description: "Islands and holes smaller than this many square px are left out (default 4)." }
16029
+ };
16030
+ const TRACE_KEYS = ["tolerance", "cornerRoundness", "minArea"];
16031
+ const traceOptions = (args) => {
16032
+ const o = {};
16033
+ for (const k of TRACE_KEYS) if (typeof args[k] === "number") o[k] = args[k];
16034
+ return o;
16035
+ };
16036
+ const selectionToPath = {
16037
+ name: "selection_to_path",
16038
+ title: "Make a path from the selection",
16039
+ description: "Make Work Path: traces the selection's outline (where it is at least half selected) into closed B\xE9zier subpaths in document pixels, holes included, within tolerance px. Changes nothing. Returns svg (compact SVG path data) and fillRule evenodd, ready for add_vector_mask, set_vector_mask_path or add_shape_layer (pass the svg as subpaths and fillRule evenodd), plus bounds and the anchor count. Use add_vector_mask from selection to do it in one step.",
16040
+ inputSchema: { type: "object", properties: { ...TRACE_PROPS, anchors: { type: "boolean", description: "Also return the subpaths as anchors with handles (default false: svg only)." } }, additionalProperties: false },
16041
+ annotations: { title: "Make a path from the selection", ...READ10 },
16042
+ handler: (args) => {
16043
+ const out = controller.selectionToPath(traceOptions(args));
16044
+ const { subpaths, ...rest } = out;
16045
+ return args.anchors === true ? out : rest;
16046
+ }
16047
+ };
16048
+ const pathToSelection2 = {
16049
+ name: "path_to_selection",
16050
+ title: "Make a selection from a path",
16051
+ description: "Make Selection: fills a path (a live shape, free subpaths or SVG path data, or components, in document pixels, as add_shape_layer takes) into a selection, combined with the current one by mode, with feather px and antiAlias. Use it to select along B\xE9zier curves, or to turn a path from selection_to_path or get_vector_mask back into a selection. One undo step. Returns the selection.",
16052
+ inputSchema: {
16053
+ type: "object",
16054
+ $defs: DEFS2,
16055
+ properties: {
16056
+ ...PATH_PROPS,
16057
+ feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather radius in px (default 0)." },
16058
+ antiAlias: { type: "boolean", description: "Soft edges (default true); false makes every pixel fully in or out." },
16059
+ mode: { type: "string", enum: ["replace", "add", "subtract", "intersect"], description: "How it combines with the current selection (default replace)." }
16060
+ },
16061
+ additionalProperties: false
16062
+ },
16063
+ annotations: { title: "Make a selection from a path", ...WRITE8 },
16064
+ handler: (args) => {
16065
+ const mode = args.mode;
16066
+ controller.pathToSelection(pathSpec(args), { feather: args.feather, antiAlias: args.antiAlias, mode });
16067
+ const sel = controller.getSelection();
16068
+ return { empty: !sel, bounds: sel?.bounds ?? null, area: Math.round(sel ? selectionArea(sel) : 0) };
16069
+ }
16070
+ };
15920
16071
  const getVectorMask = {
15921
16072
  name: "get_vector_mask",
15922
16073
  title: "Read a vector mask",
@@ -15928,14 +16079,15 @@ function createImageShapeTools(controller) {
15928
16079
  const addVectorMask = {
15929
16080
  name: "add_vector_mask",
15930
16081
  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.",
16082
+ 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; from selection traces the current selection (within tolerance px); 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
16083
  inputSchema: {
15933
16084
  type: "object",
15934
16085
  $defs: DEFS2,
15935
16086
  properties: {
15936
16087
  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
16088
+ from: { type: "string", enum: ["path", "selection", "reveal-all", "hide-all"], description: "path (default when a shape, subpaths or components are given), selection (trace the current selection), reveal-all or hide-all (an empty path)." },
16089
+ ...PATH_PROPS,
16090
+ ...TRACE_PROPS
15939
16091
  },
15940
16092
  required: ["layerId"],
15941
16093
  additionalProperties: false
@@ -15946,7 +16098,8 @@ function createImageShapeTools(controller) {
15946
16098
  const spec = pathSpec(args);
15947
16099
  const hasPath = spec.components?.length || spec.shape !== void 0 || spec.subpaths !== void 0;
15948
16100
  const from = args.from ?? (hasPath ? "path" : "reveal-all");
15949
- controller.addVectorMask(id, from, spec);
16101
+ if (from !== "selection" && TRACE_KEYS.some((k) => args[k] !== void 0)) throw new Error("tolerance, cornerRoundness and minArea apply only to from selection.");
16102
+ controller.addVectorMask(id, from, spec, traceOptions(args));
15950
16103
  return { vectorMask: controller.getVectorMask(id) };
15951
16104
  }
15952
16105
  };
@@ -16010,7 +16163,7 @@ function createImageShapeTools(controller) {
16010
16163
  return { layerId: id, vectorMask: null };
16011
16164
  }
16012
16165
  };
16013
- return [get, add2, update, setComponent, addComponent, removeComponent, merge, getVectorMask, addVectorMask, setVectorMaskPath, setVectorMaskProps, rasterizeVectorMask, removeVectorMask];
16166
+ return [get, add2, update, setComponent, addComponent, removeComponent, merge, getVectorMask, addVectorMask, setVectorMaskPath, setVectorMaskProps, rasterizeVectorMask, removeVectorMask, selectionToPath, pathToSelection2];
16014
16167
  }
16015
16168
 
16016
16169
  // src/image-editor/type-tools.ts
@@ -16782,6 +16935,7 @@ function layerMenu(controller) {
16782
16935
  { action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
16783
16936
  { action: "add-vector-mask", label: "Add Vector Mask (Reveal All)", enabled: Boolean(active && active.type !== "shape" && !active.mask?.source) },
16784
16937
  { action: "add-vector-mask-hide", label: "Add Vector Mask (Hide All)", enabled: Boolean(active && active.type !== "shape" && !active.mask?.source) },
16938
+ { action: "add-vector-mask-selection", label: "Add Vector Mask from Selection", enabled: Boolean(active && active.type !== "shape" && !active.mask?.source) && hasSel },
16785
16939
  { action: "delete-vector-mask", label: "Delete Vector Mask", enabled: Boolean(active?.vectorMask) },
16786
16940
  { action: "toggle-vector-mask", label: active?.vectorMask?.enabled === false ? "Enable Vector Mask" : "Disable Vector Mask", enabled: Boolean(active?.vectorMask) },
16787
16941
  { action: "rasterize-vector-mask", label: "Rasterize Vector Mask", enabled: Boolean(active?.vectorMask) },
@@ -16860,6 +17014,9 @@ async function runLayerMenuAction(controller, action) {
16860
17014
  case "add-vector-mask-hide":
16861
17015
  if (id) controller.addVectorMask(id, action === "add-vector-mask" ? "reveal-all" : "hide-all");
16862
17016
  break;
17017
+ case "add-vector-mask-selection":
17018
+ if (id) controller.addVectorMask(id, "selection");
17019
+ break;
16863
17020
  case "delete-vector-mask":
16864
17021
  if (id) controller.removeVectorMask(id);
16865
17022
  break;
@@ -18850,6 +19007,7 @@ export {
18850
19007
  patchTextStyle,
18851
19008
  pathStrokeCoverage,
18852
19009
  pathStrokeDabs,
19010
+ pathToSelection,
18853
19011
  perDabStrength,
18854
19012
  photoFilterMatrix,
18855
19013
  pigment8,
@@ -18978,6 +19136,7 @@ export {
18978
19136
  selectionRing,
18979
19137
  selectionToCoverage,
18980
19138
  selectionToPixels,
19139
+ selectionToSubpaths,
18981
19140
  selectionValueAt,
18982
19141
  serializeChannels,
18983
19142
  serializeColorMode,
@@ -19035,6 +19194,7 @@ export {
19035
19194
  stepSlotTs,
19036
19195
  strokeGrainCatch,
19037
19196
  styleAt,
19197
+ subpathsToSvgPath,
19038
19198
  sumHistograms,
19039
19199
  surfaceBytes,
19040
19200
  surfaceDepth,
@@ -67,7 +67,7 @@ export type DropZone = 'above' | 'below' | 'inside';
67
67
  export declare function dropZone(fraction: number, isGroup: boolean): DropZone;
68
68
  /** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */
69
69
  export declare function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null;
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';
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' | 'add-vector-mask-selection' | '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';
71
71
  export interface LayerMenuItem {
72
72
  action: LayerMenuAction;
73
73
  label: string;
@@ -67,7 +67,7 @@ export type DropZone = 'above' | 'below' | 'inside';
67
67
  export declare function dropZone(fraction: number, isGroup: boolean): DropZone;
68
68
  /** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */
69
69
  export declare function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null;
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';
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' | 'add-vector-mask-selection' | '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';
71
71
  export interface LayerMenuItem {
72
72
  action: LayerMenuAction;
73
73
  label: string;
@@ -38,8 +38,8 @@ export declare function transformVectorMask(vm: VectorMask, f: (p: {
38
38
  }, affine?: AffineMatrix): VectorMask;
39
39
  /** A linked vector mask moved with its layer by (dx, dy); unlinked ones stay. */
40
40
  export declare function followLayerMove<T extends ImageLayer>(layer: T, dx: number, dy: number): T;
41
- /** Bounds of a vector mask's paths (document pixels), or `null` when it has none. */
42
- export declare function vectorMaskBounds(vm: VectorMask): CropBox | null;
41
+ /** Bounds of the area a vector mask's paths enclose (document pixels, the curves themselves, not their handles), or `null` when it has none. */
42
+ export declare function vectorMaskBounds(vm: Pick<VectorMask, 'components'>): CropBox | null;
43
43
  /**
44
44
  * A vector mask's thumbnail over the `width × height` canvas, at most `maxSide` px: grey RGBA,
45
45
  * white where the path reveals (cached per mask object).
@@ -38,8 +38,8 @@ export declare function transformVectorMask(vm: VectorMask, f: (p: {
38
38
  }, affine?: AffineMatrix): VectorMask;
39
39
  /** A linked vector mask moved with its layer by (dx, dy); unlinked ones stay. */
40
40
  export declare function followLayerMove<T extends ImageLayer>(layer: T, dx: number, dy: number): T;
41
- /** Bounds of a vector mask's paths (document pixels), or `null` when it has none. */
42
- export declare function vectorMaskBounds(vm: VectorMask): CropBox | null;
41
+ /** Bounds of the area a vector mask's paths enclose (document pixels, the curves themselves, not their handles), or `null` when it has none. */
42
+ export declare function vectorMaskBounds(vm: Pick<VectorMask, 'components'>): CropBox | null;
43
43
  /**
44
44
  * A vector mask's thumbnail over the `width × height` canvas, at most `maxSide` px: grey RGBA,
45
45
  * white where the path reveals (cached per mask object).
@@ -11,3 +11,4 @@ export * from './segmentation.cjs';
11
11
  export * from './stroke.cjs';
12
12
  export * from './channels.cjs';
13
13
  export * from './mask-files.cjs';
14
+ export * from './path.cjs';
@@ -11,3 +11,4 @@ export * from './segmentation.js';
11
11
  export * from './stroke.js';
12
12
  export * from './channels.js';
13
13
  export * from './mask-files.js';
14
+ export * from './path.js';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Selections ⇄ paths (Photoshop's Make Work Path and Make Selection): a selection's outline traced
3
+ * into Bézier subpaths, and a path filled into a selection.
4
+ */
5
+ import type { VectorSubpath } from '../../vector/types.cjs';
6
+ import type { ShapeComponent, ShapeFillRule } from '../layers/model.cjs';
7
+ import { type SelectionMask } from './mask.cjs';
8
+ export interface SelectionToPathOptions {
9
+ /** How far (px) the path may stray from the outline (default 0.5; Photoshop's Make Work Path tolerance). */
10
+ tolerance?: number;
11
+ /** 0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5). */
12
+ cornerRoundness?: number;
13
+ /** Islands and holes smaller than this many square pixels are left out (default 4). */
14
+ minArea?: number;
15
+ /** Coverage (0..255) where the outline runs (default 128, half selected). */
16
+ threshold?: number;
17
+ }
18
+ /**
19
+ * The outline of a selection as closed subpaths in document pixels (holes included, so fill them
20
+ * even-odd). Traced in the selection's bounds only; an empty selection gives none.
21
+ */
22
+ export declare function selectionToSubpaths(mask: SelectionMask, options?: SelectionToPathOptions): VectorSubpath[];
23
+ /** Subpaths as compact SVG path data (absolute, `precision` decimals). */
24
+ export declare function subpathsToSvgPath(subpaths: readonly VectorSubpath[], precision?: number): string;
25
+ export interface PathToSelectionOptions {
26
+ /** Feather radius in px applied after filling (default 0). */
27
+ feather?: number;
28
+ /** Soft edges (default true); false makes every pixel fully in or out. */
29
+ antiAlias?: boolean;
30
+ /** How overlapping subpaths fill (default nonzero). */
31
+ fillRule?: ShapeFillRule;
32
+ }
33
+ /**
34
+ * Make Selection: the area of `components` (document pixels, combined by their ops) over a
35
+ * `width × height` image, computed in the path's bounds only.
36
+ */
37
+ export declare function pathToSelection(components: readonly ShapeComponent[], width: number, height: number, options?: PathToSelectionOptions): SelectionMask;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Selections ⇄ paths (Photoshop's Make Work Path and Make Selection): a selection's outline traced
3
+ * into Bézier subpaths, and a path filled into a selection.
4
+ */
5
+ import type { VectorSubpath } from '../../vector/types.js';
6
+ import type { ShapeComponent, ShapeFillRule } from '../layers/model.js';
7
+ import { type SelectionMask } from './mask.js';
8
+ export interface SelectionToPathOptions {
9
+ /** How far (px) the path may stray from the outline (default 0.5; Photoshop's Make Work Path tolerance). */
10
+ tolerance?: number;
11
+ /** 0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5). */
12
+ cornerRoundness?: number;
13
+ /** Islands and holes smaller than this many square pixels are left out (default 4). */
14
+ minArea?: number;
15
+ /** Coverage (0..255) where the outline runs (default 128, half selected). */
16
+ threshold?: number;
17
+ }
18
+ /**
19
+ * The outline of a selection as closed subpaths in document pixels (holes included, so fill them
20
+ * even-odd). Traced in the selection's bounds only; an empty selection gives none.
21
+ */
22
+ export declare function selectionToSubpaths(mask: SelectionMask, options?: SelectionToPathOptions): VectorSubpath[];
23
+ /** Subpaths as compact SVG path data (absolute, `precision` decimals). */
24
+ export declare function subpathsToSvgPath(subpaths: readonly VectorSubpath[], precision?: number): string;
25
+ export interface PathToSelectionOptions {
26
+ /** Feather radius in px applied after filling (default 0). */
27
+ feather?: number;
28
+ /** Soft edges (default true); false makes every pixel fully in or out. */
29
+ antiAlias?: boolean;
30
+ /** How overlapping subpaths fill (default nonzero). */
31
+ fillRule?: ShapeFillRule;
32
+ }
33
+ /**
34
+ * Make Selection: the area of `components` (document pixels, combined by their ops) over a
35
+ * `width × height` image, computed in the path's bounds only.
36
+ */
37
+ export declare function pathToSelection(components: readonly ShapeComponent[], width: number, height: number, options?: PathToSelectionOptions): SelectionMask;
@@ -7,7 +7,7 @@ var _chunkZ64NRDEHcjs = require('../chunk-Z64NRDEH.cjs');
7
7
 
8
8
 
9
9
 
10
- var _chunkT4U4O6DScjs = require('../chunk-T4U4O6DS.cjs');
10
+ var _chunk2UR4OX3Bcjs = require('../chunk-2UR4OX3B.cjs');
11
11
  require('../chunk-A2IS7EHE.cjs');
12
12
 
13
13
 
@@ -1653,11 +1653,11 @@ function header(state, options) {
1653
1653
  return out;
1654
1654
  }
1655
1655
  function serializePaintDocument(state, options = {}) {
1656
- const { layers, assets } = _chunkT4U4O6DScjs.serializeLayers.call(void 0, state.layers);
1656
+ const { layers, assets } = _chunk2UR4OX3Bcjs.serializeLayers.call(void 0, state.layers);
1657
1657
  return { ...header(state, options), layers, assets };
1658
1658
  }
1659
1659
  async function serializePaintDocumentExternal(state, store, options = {}) {
1660
- const { layers, assets } = await _chunkT4U4O6DScjs.serializeLayersExternal.call(void 0, state.layers, store);
1660
+ const { layers, assets } = await _chunk2UR4OX3Bcjs.serializeLayersExternal.call(void 0, state.layers, store);
1661
1661
  return { ...header(state, options), layers, assets };
1662
1662
  }
1663
1663
  function paintLayersOnly(layers, problems) {
@@ -1694,7 +1694,7 @@ function deserializePaintDocument(raw) {
1694
1694
  const canvas = normalizeCanvas({ width: r.width, height: r.height, dpi: _nullishCoalesce(r.dpi, () => ( void 0)) });
1695
1695
  const paper = _chunkYQ5C5BVKcjs.normalizePaper.call(void 0, _nullishCoalesce(r.paper, () => ( {})));
1696
1696
  const assets = r.assets && typeof r.assets === "object" ? r.assets : {};
1697
- const layers = paintLayersOnly(_chunkT4U4O6DScjs.deserializeLayers.call(void 0, r.layers, { assets, problems }), problems);
1697
+ const layers = paintLayersOnly(_chunk2UR4OX3Bcjs.deserializeLayers.call(void 0, r.layers, { assets, problems }), problems);
1698
1698
  const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
1699
1699
  const activeLayerId = wanted && _chunkHGTDMKNEcjs.findLayer.call(void 0, layers, wanted) ? wanted : _nullishCoalesce(_optionalChain([_chunkHGTDMKNEcjs.flattenLayers.call(void 0, layers), 'access', _70 => _70.filter, 'call', _71 => _71((l) => l.type === "pixel"), 'access', _72 => _72.pop, 'call', _73 => _73(), 'optionalAccess', _74 => _74.id]), () => ( null));
1700
1700
  const wetEvents = Array.isArray(r.wetEvents) ? r.wetEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.clock) && typeof e.kind === "string") : [];
@@ -1710,7 +1710,7 @@ async function deserializePaintDocumentAsync(raw, store) {
1710
1710
  if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
1711
1711
  const r = raw;
1712
1712
  const problems = [];
1713
- const assets = await _chunkT4U4O6DScjs.resolveExternalAssets.call(void 0, _nullishCoalesce(r.assets, () => ( {})), store, problems);
1713
+ const assets = await _chunk2UR4OX3Bcjs.resolveExternalAssets.call(void 0, _nullishCoalesce(r.assets, () => ( {})), store, problems);
1714
1714
  const loaded = deserializePaintDocument({ ...r, assets });
1715
1715
  return { ...loaded, problems: [...problems, ...loaded.problems] };
1716
1716
  }
@@ -5409,7 +5409,7 @@ var PaintEditorController = (_class10 = class _PaintEditorController {
5409
5409
  return next.type === "group" ? { ...next, children: toStandard(next.children) } : next;
5410
5410
  });
5411
5411
  const layers = toStandard(dried);
5412
- const result = _chunkT4U4O6DScjs.exportPsd.call(void 0, { width, height, layers: [paperLayer, ...layers], merged, resolution: dpi });
5412
+ const result = _chunk2UR4OX3Bcjs.exportPsd.call(void 0, { width, height, layers: [paperLayer, ...layers], merged, resolution: dpi });
5413
5413
  for (const [mode, names] of changed) {
5414
5414
  const label = _nullishCoalesce(_optionalChain([_chunkCWM3CUUXcjs.BLEND_MODES, 'access', _271 => _271.find, 'call', _272 => _272((m) => m.id === mode), 'optionalAccess', _273 => _273.label]), () => ( mode));
5415
5415
  const std = _nullishCoalesce(_optionalChain([_chunkCWM3CUUXcjs.BLEND_MODES, 'access', _274 => _274.find, 'call', _275 => _275((m) => m.id === _chunkCWM3CUUXcjs.standardBlendMode.call(void 0, mode)), 'optionalAccess', _276 => _276.label]), () => ( _chunkCWM3CUUXcjs.standardBlendMode.call(void 0, mode)));
@@ -7,7 +7,7 @@ import {
7
7
  resolveExternalAssets,
8
8
  serializeLayers,
9
9
  serializeLayersExternal
10
- } from "../chunk-7ZWLDVFH.js";
10
+ } from "../chunk-TXYXF32N.js";
11
11
  import "../chunk-3CI7FRMK.js";
12
12
  import {
13
13
  EffectRendererPool