@pixerate/editor 0.210.0 → 0.211.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.
@@ -301,6 +301,7 @@ import {
301
301
  rasterizeCoverage,
302
302
  rasterizeSmartObject,
303
303
  readPsd,
304
+ readShapeComponents,
304
305
  referencePoint,
305
306
  removeAlpha,
306
307
  removeLayer,
@@ -401,7 +402,7 @@ import {
401
402
  writePsd,
402
403
  xyToTempTint,
403
404
  xyToUv
404
- } from "../chunk-KFIOEOCQ.js";
405
+ } from "../chunk-W3TOZTL7.js";
405
406
  import {
406
407
  parseCssColor
407
408
  } from "../chunk-3CI7FRMK.js";
@@ -1477,8 +1478,9 @@ var ImageEditorRenderer = class {
1477
1478
  ctx.restore();
1478
1479
  }
1479
1480
  const { width: iw, height: ih } = state.imageDimensions;
1480
- const quickMaskSel = display === "quickMask" && state.quickMaskEditing && !state.selection?.bounds ? this.emptyMask(Math.round(iw), Math.round(ih)) : null;
1481
- if (state.selection?.bounds && display !== "none" || quickMaskSel || options.draftSelectionPath?.length) {
1481
+ const shownSel = state.selectionPreview !== void 0 ? state.selectionPreview : state.selection;
1482
+ const quickMaskSel = display === "quickMask" && state.quickMaskEditing && !shownSel?.bounds ? this.emptyMask(Math.round(iw), Math.round(ih)) : null;
1483
+ if (shownSel?.bounds && display !== "none" || quickMaskSel || options.draftSelectionPath?.length) {
1482
1484
  ctx.save();
1483
1485
  try {
1484
1486
  applyImageProjectionToContext(ctx, projection);
@@ -1486,8 +1488,8 @@ var ImageEditorRenderer = class {
1486
1488
  ctx.rect(region.x, region.y, region.width, region.height);
1487
1489
  ctx.clip();
1488
1490
  if (quickMaskSel) this.drawQuickMask(ctx, quickMaskSel);
1489
- else if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
1490
- else if (state.selection?.bounds && display === "ants") this.drawAnts(ctx, state.selection, projection.scale / u, options.antsOffset ?? 0);
1491
+ else if (shownSel?.bounds && display === "quickMask") this.drawQuickMask(ctx, shownSel);
1492
+ else if (shownSel?.bounds && display === "ants") this.drawAnts(ctx, shownSel, projection.scale / u, options.antsOffset ?? 0);
1491
1493
  if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
1492
1494
  } catch {
1493
1495
  }
@@ -2861,6 +2863,58 @@ function magneticAnchorSpacing(frequency = 57) {
2861
2863
  return 8 + (100 - f);
2862
2864
  }
2863
2865
 
2866
+ // src/image-editor/paths.ts
2867
+ var WORK_PATH_NAME = "Work Path";
2868
+ var counter = 0;
2869
+ function newPathId() {
2870
+ counter = (counter + 1) % 1e6;
2871
+ return `path-${Date.now().toString(36)}-${counter.toString(36)}`;
2872
+ }
2873
+ function pathSvgData(path) {
2874
+ return subpathsToSvgPath(path.components.flatMap((c) => c.subpaths));
2875
+ }
2876
+ function pathBounds(path) {
2877
+ return vectorMaskBounds({ components: path.components });
2878
+ }
2879
+ function pathAnchorCount(path) {
2880
+ return path.components.reduce((n, c) => n + c.subpaths.reduce((m, sp) => m + sp.anchors.length, 0), 0);
2881
+ }
2882
+ function serializePaths(paths) {
2883
+ return paths.map((p) => ({
2884
+ id: p.id,
2885
+ name: p.name,
2886
+ components: JSON.parse(JSON.stringify(p.components)),
2887
+ ...p.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
2888
+ ...p.work ? { work: true } : {}
2889
+ }));
2890
+ }
2891
+ function readPaths(stored, problems = []) {
2892
+ if (!Array.isArray(stored)) return [];
2893
+ const out = [];
2894
+ const ids = /* @__PURE__ */ new Set();
2895
+ let work = false;
2896
+ for (const [i, raw] of stored.entries()) {
2897
+ const r = raw;
2898
+ const name = typeof r?.name === "string" && r.name.trim() ? r.name.trim() : `Path ${i + 1}`;
2899
+ try {
2900
+ const components = readShapeComponents(r?.components);
2901
+ let id = typeof r?.id === "string" && r.id ? r.id : newPathId();
2902
+ if (ids.has(id)) id = newPathId();
2903
+ ids.add(id);
2904
+ const path = { id, name, components };
2905
+ if (r?.fillRule === "evenodd") path.fillRule = "evenodd";
2906
+ if (r?.work === true && !work) {
2907
+ path.work = true;
2908
+ work = true;
2909
+ }
2910
+ out.push(path);
2911
+ } catch (err) {
2912
+ problems.push(`Path "${name}" could not be read (${err instanceof Error ? err.message : String(err)}) and was left out.`);
2913
+ }
2914
+ }
2915
+ return out;
2916
+ }
2917
+
2864
2918
  // src/image-editor/layers/shape-type-ui.ts
2865
2919
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
2866
2920
  kind: "rect",
@@ -5643,6 +5697,7 @@ var ImageEditorController = class {
5643
5697
  outputOpacity: s.outputOpacity,
5644
5698
  selection: s.selection ?? null,
5645
5699
  ...s.channels?.length ? { channels: s.channels } : {},
5700
+ ...s.paths?.length ? { paths: s.paths } : {},
5646
5701
  source: {
5647
5702
  sourceUrl: s.sourceUrl,
5648
5703
  element: this.imageElement,
@@ -5680,6 +5735,9 @@ var ImageEditorController = class {
5680
5735
  s.selection = entry.selection;
5681
5736
  if (entry.channels?.length) s.channels = entry.channels;
5682
5737
  else delete s.channels;
5738
+ if (entry.paths?.length) s.paths = entry.paths;
5739
+ else delete s.paths;
5740
+ if (s.activePathId && !s.paths?.some((p) => p.id === s.activePathId)) s.activePathId = null;
5683
5741
  if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
5684
5742
  this.imageElement = entry.source.element;
5685
5743
  s.sourceUrl = entry.source.sourceUrl;
@@ -6491,6 +6549,138 @@ var ImageEditorController = class {
6491
6549
  const mask = pathToSelection(components, width, height, { ...options, fillRule: path.fillRule ?? options.fillRule });
6492
6550
  return this.setSelection(mask, options.mode ?? "replace");
6493
6551
  }
6552
+ // --- Document paths (the Paths panel) ---
6553
+ /** The document's paths: saved paths and the Work Path, in panel order. */
6554
+ getPaths() {
6555
+ return this.state.paths ?? [];
6556
+ }
6557
+ getPath(id) {
6558
+ return this.state.paths?.find((p) => p.id === id) ?? null;
6559
+ }
6560
+ requirePath(id) {
6561
+ const path = this.getPath(id);
6562
+ if (!path) {
6563
+ const ids = this.getPaths().map((p) => `${p.id} (${p.name})`);
6564
+ throw new LayerOperationError(`There is no path "${id}"; the paths are ${ids.join(", ") || "none (make a work path from a selection, or add one)"}.`);
6565
+ }
6566
+ return path;
6567
+ }
6568
+ /** Selects a path in the Paths panel (or none). View state: no undo step. */
6569
+ setActivePath(id) {
6570
+ if (id !== null) this.requirePath(id);
6571
+ if ((this.state.activePathId ?? null) === id) return;
6572
+ this.state.activePathId = id;
6573
+ this.notify();
6574
+ }
6575
+ commitPaths(next, active) {
6576
+ this.recordHistory();
6577
+ if (next.length) this.state.paths = next;
6578
+ else delete this.state.paths;
6579
+ if (active !== void 0) this.state.activePathId = active;
6580
+ else if (this.state.activePathId && !next.some((p) => p.id === this.state.activePathId)) this.state.activePathId = null;
6581
+ this.notify();
6582
+ }
6583
+ /**
6584
+ * Paths › Make Work Path: the selection's outline, traced, becomes the Work Path (replacing the
6585
+ * last one, unless it was saved). One undo step. Returns the path.
6586
+ */
6587
+ makeWorkPath(options = {}) {
6588
+ const subpaths = this.traceSelection(options);
6589
+ const paths = this.getPaths();
6590
+ const old = paths.find((p) => p.work);
6591
+ const work = { id: old?.id ?? newPathId(), name: WORK_PATH_NAME, components: [{ op: "combine", subpaths }], fillRule: "evenodd", work: true };
6592
+ this.commitPaths(old ? paths.map((p) => p === old ? work : p) : [...paths, work], work.id);
6593
+ return work;
6594
+ }
6595
+ /** A new saved path from geometry (`shape`, `subpaths` or `components`). One undo step. */
6596
+ addPath(input) {
6597
+ const paths = this.getPaths();
6598
+ const path = { id: newPathId(), name: input.name?.trim() || `Path ${paths.filter((p) => !p.work).length + 1}`, components: this.vectorMaskComponents(input) };
6599
+ if (input.fillRule === "evenodd") path.fillRule = "evenodd";
6600
+ this.commitPaths([...paths.filter((p) => !p.work), path, ...paths.filter((p) => p.work)], path.id);
6601
+ return path;
6602
+ }
6603
+ /** Replaces a path's geometry (its name is kept). One undo step. */
6604
+ setPathGeometry(id, input) {
6605
+ const old = this.requirePath(id);
6606
+ const path = { ...old, components: this.vectorMaskComponents(input) };
6607
+ if (input.fillRule === "evenodd") path.fillRule = "evenodd";
6608
+ else if (input.fillRule === "nonzero") delete path.fillRule;
6609
+ this.commitPaths(this.getPaths().map((p) => p.id === id ? path : p));
6610
+ return path;
6611
+ }
6612
+ /** Renames a path; renaming the Work Path saves it (Photoshop's Save Path). One undo step. */
6613
+ renamePath(id, name) {
6614
+ const old = this.requirePath(id);
6615
+ const trimmed = name.trim();
6616
+ if (!trimmed) throw new LayerOperationError("A path needs a name.");
6617
+ const path = { ...old, name: trimmed };
6618
+ delete path.work;
6619
+ this.commitPaths(this.getPaths().map((p) => p.id === id ? path : p));
6620
+ return path;
6621
+ }
6622
+ /** Duplicates a path as a saved path. One undo step. */
6623
+ duplicatePath(id, name) {
6624
+ const old = this.requirePath(id);
6625
+ const copy = { ...old, id: newPathId(), name: name?.trim() || `${old.name} copy`, components: JSON.parse(JSON.stringify(old.components)) };
6626
+ delete copy.work;
6627
+ const paths = this.getPaths();
6628
+ this.commitPaths([...paths.filter((p) => !p.work), copy, ...paths.filter((p) => p.work)], copy.id);
6629
+ return copy;
6630
+ }
6631
+ /** Deletes a path. One undo step. */
6632
+ deletePath(id) {
6633
+ this.requirePath(id);
6634
+ this.commitPaths(this.getPaths().filter((p) => p.id !== id));
6635
+ }
6636
+ /** The area a path fills over the image, as a selection mask. */
6637
+ pathArea(path, options = {}) {
6638
+ const { width, height } = this.naturalSize;
6639
+ return pathToSelection(path.components, width, height, { ...options, fillRule: path.fillRule ?? options.fillRule });
6640
+ }
6641
+ /** Paths › Make Selection: loads a path as the selection, combined by `mode`. One undo step. */
6642
+ loadPathAsSelection(id, options = {}) {
6643
+ if (options.feather !== void 0 && !(options.feather >= 0 && options.feather <= 1e3)) throw new LayerOperationError("feather must be from 0 to 1000 px.");
6644
+ return this.setSelection(this.pathArea(this.requirePath(id), options), options.mode ?? "replace");
6645
+ }
6646
+ /**
6647
+ * Paths › Fill Path: fills the path's area on a pixel layer (default the active one) with a
6648
+ * colour (default the foreground). The selection is not changed. One undo step.
6649
+ */
6650
+ fillPath(id, options = {}) {
6651
+ const area = this.pathArea(this.requirePath(id));
6652
+ if (!area.bounds) throw new LayerOperationError("The path encloses no area to fill.");
6653
+ return this.withSelection(area, () => this.fillPixels({ ...options, useSelection: true }));
6654
+ }
6655
+ /**
6656
+ * Paths › Stroke Path: strokes along the path's outline on a pixel layer (default the active
6657
+ * one), `width` px wide and centred on it. The selection is not changed. One undo step.
6658
+ */
6659
+ strokePath(id, options) {
6660
+ const area = this.pathArea(this.requirePath(id), { antiAlias: true });
6661
+ if (!area.bounds) throw new LayerOperationError("The path encloses no area; stroke a closed path.");
6662
+ return this.withSelection(area, () => this.strokeSelection({ ...options, location: "center" }));
6663
+ }
6664
+ /** Paths › Add Vector Mask: masks a layer (default the active one) with a document path. One undo step. */
6665
+ addVectorMaskFromPath(pathId, layerId) {
6666
+ const path = this.requirePath(pathId);
6667
+ const id = layerId ?? this.state.activeLayerId;
6668
+ if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
6669
+ return this.addVectorMask(id, "path", { components: path.components.map((c) => ({ op: c.op, subpaths: c.subpaths })), ...path.fillRule ? { fillRule: path.fillRule } : {} });
6670
+ }
6671
+ /** Runs `fn` with `mask` as the selection, then puts the selection back, all as one undo step. */
6672
+ withSelection(mask, fn) {
6673
+ return this.asOneStep(() => {
6674
+ const before = this.state.selection ?? null;
6675
+ this.state.selection = mask;
6676
+ try {
6677
+ return fn();
6678
+ } finally {
6679
+ this.state.selection = before;
6680
+ this.notify();
6681
+ }
6682
+ });
6683
+ }
6494
6684
  /** Replaces a vector mask's path (its properties are kept). One undo step. Returns the vector mask. */
6495
6685
  setVectorMaskPath(id, path) {
6496
6686
  const { vm } = this.requireVectorMask(id);
@@ -8511,6 +8701,17 @@ var ImageEditorController = class {
8511
8701
  this.notify();
8512
8702
  return true;
8513
8703
  }
8704
+ /**
8705
+ * Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
8706
+ * moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo
8707
+ * step, not saved.
8708
+ */
8709
+ setSelectionPreview(mask) {
8710
+ if (this.state.selectionPreview === mask) return;
8711
+ if (mask === void 0) delete this.state.selectionPreview;
8712
+ else this.state.selectionPreview = mask;
8713
+ this.notify();
8714
+ }
8514
8715
  /** Select › All. */
8515
8716
  selectAll() {
8516
8717
  const { width, height } = this.naturalSize;
@@ -10482,6 +10683,10 @@ var ImageEditorController = class {
10482
10683
  }));
10483
10684
  if (psdChannels.length) s.channels = psdChannels;
10484
10685
  else delete s.channels;
10686
+ const psdPaths = (result.paths ?? []).map((p) => ({ id: newPathId(), name: p.name, components: p.components, ...p.work ? { work: true } : {} }));
10687
+ if (psdPaths.length) s.paths = psdPaths;
10688
+ else delete s.paths;
10689
+ s.activePathId = null;
10485
10690
  s.selectedAnnotationId = null;
10486
10691
  s.activeLayerId = defaultActiveLayerId(s.layers);
10487
10692
  s.activeTarget = "pixels";
@@ -10679,7 +10884,7 @@ var ImageEditorController = class {
10679
10884
  opacity: c.opacity,
10680
10885
  indicates: c.indicates
10681
10886
  }));
10682
- const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, ...color });
10887
+ const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, paths: this.getPaths(), ...color });
10683
10888
  warnings.push(...result.warnings);
10684
10889
  return { bytes: result.bytes, warnings, psb: result.psb, width, height };
10685
10890
  }
@@ -10753,6 +10958,7 @@ var ImageEditorController = class {
10753
10958
  out = this.serializeState();
10754
10959
  if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
10755
10960
  if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
10961
+ if (this.state.paths?.length) out.paths = serializePaths(this.state.paths);
10756
10962
  } finally {
10757
10963
  if (root) {
10758
10964
  this.state = current;
@@ -10797,6 +11003,10 @@ var ImageEditorController = class {
10797
11003
  const channels = deserializeChannels(data.channels, this.naturalSize, problems);
10798
11004
  if (channels.length) this.state.channels = channels;
10799
11005
  else delete this.state.channels;
11006
+ const paths = readPaths(data.paths, problems);
11007
+ if (paths.length) this.state.paths = paths;
11008
+ else delete this.state.paths;
11009
+ this.state.activePathId = null;
10800
11010
  if (problems.length) this.lastLoadProblems = [...this.lastLoadProblems, ...problems];
10801
11011
  this.notify();
10802
11012
  }
@@ -11773,7 +11983,7 @@ var DEPTH_MASK_CONTROLS = [
11773
11983
  // src/image-editor/playback.ts
11774
11984
  var IMAGE_PLAYBACK_KIND = "layer";
11775
11985
  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)$/;
11776
- var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_focus_area|select_sky|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|select_text_shape)$/;
11986
+ var SELECT = /^(make_work_path|add_path|set_path_geometry|rename_path|duplicate_path|delete_path|fill_path|stroke_path|select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_focus_area|select_sky|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|select_text_shape)$/;
11777
11987
  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|set_text_orientation|convert_text_to_shape)$/;
11778
11988
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
11779
11989
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -14497,6 +14707,9 @@ var SelectionGesture = class {
14497
14707
  /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
14498
14708
  error = null;
14499
14709
  pending = Promise.resolve();
14710
+ /** Quick Selection while dragging: the selection before the stroke, how the stroke combines, the pixels. */
14711
+ quick = null;
14712
+ livePreviews = Promise.resolve();
14500
14713
  get active() {
14501
14714
  return this.start !== null || this.polygon !== null || this.magnetic !== null;
14502
14715
  }
@@ -14504,6 +14717,46 @@ var SelectionGesture = class {
14504
14717
  get settled() {
14505
14718
  return this.pending;
14506
14719
  }
14720
+ /** Resolves once no live Quick Selection preview is being computed. */
14721
+ get previewed() {
14722
+ return this.livePreviews;
14723
+ }
14724
+ /** Grows the Quick Selection preview to the stroke so far: one at a time, the newest points following. */
14725
+ growPreview() {
14726
+ const q = this.quick;
14727
+ if (!q) return;
14728
+ if (q.running) {
14729
+ q.dirty = true;
14730
+ return;
14731
+ }
14732
+ q.running = true;
14733
+ this.livePreviews = (async () => {
14734
+ do {
14735
+ q.dirty = false;
14736
+ const stroke = this.points.slice();
14737
+ const options = this.getOptions();
14738
+ try {
14739
+ const pixels = await q.pixels;
14740
+ if (!pixels || this.quick !== q) break;
14741
+ const { mask } = await this.controller.runSelectionJob(
14742
+ {
14743
+ op: "quick",
14744
+ stroke,
14745
+ size: options.quickSize ?? 30,
14746
+ grow: q.mode === "add",
14747
+ enhanceEdge: options.quickEnhance ?? false,
14748
+ selection: q.mode === "add" && q.base?.bounds ? serializeMask(q.base) : null
14749
+ },
14750
+ pixels
14751
+ );
14752
+ if (this.quick === q) this.controller.setSelectionPreview(combineSelections(q.base, mask, q.mode));
14753
+ } catch {
14754
+ break;
14755
+ }
14756
+ } while (q.dirty && this.quick === q);
14757
+ q.running = false;
14758
+ })();
14759
+ }
14507
14760
  /** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
14508
14761
  get magneticAnchors() {
14509
14762
  const m = this.magnetic;
@@ -14515,7 +14768,8 @@ var SelectionGesture = class {
14515
14768
  if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
14516
14769
  if (!this.start || !this.current) return null;
14517
14770
  const { tool } = this.getOptions();
14518
- if (tool === "lasso" || tool === "quick") return this.points.length > 1 ? this.points.slice() : null;
14771
+ if (tool === "quick") return null;
14772
+ if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
14519
14773
  const rect = normalizeRect({
14520
14774
  x: this.start.x,
14521
14775
  y: this.start.y,
@@ -14628,6 +14882,18 @@ var SelectionGesture = class {
14628
14882
  this.start = point;
14629
14883
  this.current = point;
14630
14884
  this.points = [point];
14885
+ if (options.tool === "quick") {
14886
+ const picked = selectionModeFromModifiers(modifiers, options.mode);
14887
+ const base = this.controller.getSelection();
14888
+ this.quick = {
14889
+ base,
14890
+ mode: picked === "replace" && base ? "add" : picked,
14891
+ pixels: this.controller.getSamplePixels({ sample: options.quickSampleAll === false ? "active-layer" : "composite" }).catch(() => null),
14892
+ running: false,
14893
+ dirty: false
14894
+ };
14895
+ this.growPreview();
14896
+ }
14631
14897
  }
14632
14898
  /** Pointer move; returns true when the draft changed (re-render). */
14633
14899
  move(point, modifiers = {}) {
@@ -14654,6 +14920,7 @@ var SelectionGesture = class {
14654
14920
  const last = this.points[this.points.length - 1];
14655
14921
  if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
14656
14922
  this.points.push(point);
14923
+ if (drawn === "quick") this.growPreview();
14657
14924
  }
14658
14925
  return true;
14659
14926
  }
@@ -14689,7 +14956,8 @@ var SelectionGesture = class {
14689
14956
  this.start = null;
14690
14957
  this.current = null;
14691
14958
  this.points = [];
14692
- const strokeMode = mode === "replace" && this.controller.getSelection() ? "add" : mode;
14959
+ const strokeMode = this.quick?.mode ?? (mode === "replace" && this.controller.getSelection() ? "add" : mode);
14960
+ this.quick = null;
14693
14961
  this.pending = this.controller.quickSelect([stroke], {
14694
14962
  size: options.quickSize ?? 30,
14695
14963
  mode: strokeMode,
@@ -14700,7 +14968,7 @@ var SelectionGesture = class {
14700
14968
  (err) => {
14701
14969
  this.error = err instanceof Error ? err.message : String(err);
14702
14970
  }
14703
- );
14971
+ ).finally(() => this.controller.setSelectionPreview(void 0));
14704
14972
  return false;
14705
14973
  }
14706
14974
  const path = this.draftPath;
@@ -14829,6 +15097,10 @@ var SelectionGesture = class {
14829
15097
  }
14830
15098
  /** Abandons a drag (or a polygonal lasso) without changing the selection. */
14831
15099
  cancel() {
15100
+ if (this.quick) {
15101
+ this.quick = null;
15102
+ this.controller.setSelectionPreview(void 0);
15103
+ }
14832
15104
  this.start = null;
14833
15105
  this.current = null;
14834
15106
  this.points = [];
@@ -16945,6 +17217,19 @@ function createImageShapeTools(controller) {
16945
17217
  fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." }
16946
17218
  };
16947
17219
  const LAYER = { type: "string", minLength: 1, description: "The layer (see list_layers). Not a shape layer: its path already is its vector mask." };
17220
+ const PATH_ID = { type: "string", minLength: 1, description: "A document path id from list_paths." };
17221
+ const fromPathId = (args) => {
17222
+ if (args.pathId === void 0) return null;
17223
+ const own = Object.keys(PATH_PROPS).filter((k) => args[k] !== void 0);
17224
+ if (own.length) throw new Error(`Give pathId or ${own.join(" / ")}, not both.`);
17225
+ const path = controller.getPath(String(args.pathId));
17226
+ if (!path) throw new Error(`No path "${String(args.pathId)}". Call list_paths for ids.`);
17227
+ return { components: path.components.map((c) => ({ op: c.op, subpaths: c.subpaths })), ...path.fillRule ? { fillRule: path.fillRule } : {} };
17228
+ };
17229
+ const pathSummary = (id) => {
17230
+ const p = controller.getPath(id);
17231
+ return { id: p.id, name: p.name, work: Boolean(p.work), fillRule: p.fillRule ?? "nonzero", anchors: pathAnchorCount(p), bounds: pathBounds(p) };
17232
+ };
16948
17233
  const TRACE_PROPS = {
16949
17234
  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)." },
16950
17235
  cornerRoundness: { type: "number", minimum: 0, maximum: 1, description: "0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5)." },
@@ -16976,6 +17261,7 @@ function createImageShapeTools(controller) {
16976
17261
  type: "object",
16977
17262
  $defs: DEFS2,
16978
17263
  properties: {
17264
+ pathId: { ...PATH_ID, description: "A document path from list_paths, instead of shape / subpaths / components." },
16979
17265
  ...PATH_PROPS,
16980
17266
  feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather radius in px (default 0)." },
16981
17267
  antiAlias: { type: "boolean", description: "Soft edges (default true); false makes every pixel fully in or out." },
@@ -16986,7 +17272,7 @@ function createImageShapeTools(controller) {
16986
17272
  annotations: { title: "Make a selection from a path", ...WRITE8 },
16987
17273
  handler: (args) => {
16988
17274
  const mode = args.mode;
16989
- controller.pathToSelection(pathSpec(args), { feather: args.feather, antiAlias: args.antiAlias, mode });
17275
+ controller.pathToSelection(fromPathId(args) ?? pathSpec(args), { feather: args.feather, antiAlias: args.antiAlias, mode });
16990
17276
  const sel = controller.getSelection();
16991
17277
  return { empty: !sel, bounds: sel?.bounds ?? null, area: Math.round(sel ? selectionArea(sel) : 0) };
16992
17278
  }
@@ -17008,7 +17294,8 @@ function createImageShapeTools(controller) {
17008
17294
  $defs: DEFS2,
17009
17295
  properties: {
17010
17296
  layerId: LAYER,
17011
- 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)." },
17297
+ from: { type: "string", enum: ["path", "selection", "reveal-all", "hide-all"], description: "path (default when a shape, subpaths, components or pathId are given), selection (trace the current selection), reveal-all or hide-all (an empty path)." },
17298
+ pathId: { ...PATH_ID, description: "A document path from list_paths to use as the mask (Paths \u203A Add Vector Mask), instead of shape / subpaths / components." },
17012
17299
  ...PATH_PROPS,
17013
17300
  ...TRACE_PROPS
17014
17301
  },
@@ -17018,7 +17305,7 @@ function createImageShapeTools(controller) {
17018
17305
  annotations: { title: "Add a vector mask", ...WRITE8 },
17019
17306
  handler: (args) => {
17020
17307
  const id = layerArg(args.layerId);
17021
- const spec = pathSpec(args);
17308
+ const spec = fromPathId(args) ?? pathSpec(args);
17022
17309
  const hasPath = spec.components?.length || spec.shape !== void 0 || spec.subpaths !== void 0;
17023
17310
  const from = args.from ?? (hasPath ? "path" : "reveal-all");
17024
17311
  if (from !== "selection" && TRACE_KEYS.some((k) => args[k] !== void 0)) throw new Error("tolerance, cornerRoundness and minArea apply only to from selection.");
@@ -17086,7 +17373,139 @@ function createImageShapeTools(controller) {
17086
17373
  return { layerId: id, vectorMask: null };
17087
17374
  }
17088
17375
  };
17089
- return [get, add2, update, setComponent, addComponent, removeComponent, merge, getVectorMask, addVectorMask, setVectorMaskPath, setVectorMaskProps, rasterizeVectorMask, removeVectorMask, selectionToPath, pathToSelection2];
17376
+ const listPaths = {
17377
+ name: "list_paths",
17378
+ title: "List document paths",
17379
+ description: "Paths panel: the document's saved paths and its Work Path (paths kept with the document, on no layer), each with id, name, work (true for the Work Path, which the next make_work_path replaces), fill rule, anchor count and bounds, and which is active. Use the ids with path_to_selection, add_vector_mask, fill_path, stroke_path and the other path tools.",
17380
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
17381
+ annotations: { title: "List document paths", ...READ10 },
17382
+ handler: () => ({ paths: controller.getPaths().map((p) => pathSummary(p.id)), activePathId: controller.getState().activePathId ?? null })
17383
+ };
17384
+ const getPath2 = {
17385
+ name: "get_path",
17386
+ title: "Read a document path",
17387
+ description: "Reads a document path: its summary, svg (compact SVG path data) and components (op, subpaths of anchors with handles, in document pixels), ready to edit and pass back to set_path_geometry.",
17388
+ inputSchema: { type: "object", properties: { pathId: PATH_ID }, required: ["pathId"], additionalProperties: false },
17389
+ annotations: { title: "Read a document path", ...READ10 },
17390
+ handler: ({ pathId }) => {
17391
+ const p = controller.getPath(String(pathId));
17392
+ if (!p) throw new Error(`No path "${String(pathId)}". Call list_paths for ids.`);
17393
+ return { path: { ...pathSummary(p.id), svg: pathSvgData(p), components: p.components } };
17394
+ }
17395
+ };
17396
+ const makeWorkPath = {
17397
+ name: "make_work_path",
17398
+ title: "Make a work path from the selection",
17399
+ description: "Paths \u203A Make Work Path: traces the selection's outline (within tolerance px) into the Work Path, replacing the last Work Path unless it was saved (rename_path saves it). Use it to keep a selection as an editable, resolution-independent outline. One undo step. Returns the path.",
17400
+ inputSchema: { type: "object", properties: { ...TRACE_PROPS }, additionalProperties: false },
17401
+ annotations: { title: "Make a work path from the selection", ...WRITE8 },
17402
+ handler: (args) => ({ path: pathSummary(controller.makeWorkPath(traceOptions(args)).id) })
17403
+ };
17404
+ const addPath = {
17405
+ name: "add_path",
17406
+ title: "Add a document path",
17407
+ description: "Adds a saved document path from a live shape, free subpaths or SVG path data, or components (document pixels), as add_shape_layer takes them. One undo step. Returns the path.",
17408
+ inputSchema: {
17409
+ type: "object",
17410
+ $defs: DEFS2,
17411
+ properties: { name: { type: "string", description: 'The path name (default "Path n").' }, ...PATH_PROPS },
17412
+ additionalProperties: false
17413
+ },
17414
+ annotations: { title: "Add a document path", ...WRITE8 },
17415
+ handler: (args) => {
17416
+ const spec = pathSpec(args);
17417
+ if (!spec.components?.length && spec.shape === void 0 && spec.subpaths === void 0) throw new Error("Give the path as shape, subpaths or components.");
17418
+ return { path: pathSummary(controller.addPath({ ...spec, name: args.name }).id) };
17419
+ }
17420
+ };
17421
+ const setPathGeometry = {
17422
+ name: "set_path_geometry",
17423
+ title: "Replace a document path",
17424
+ description: "Replaces a document path's geometry (shape, subpaths or components, document pixels); its name is kept. Read it with get_path first to change part of it. One undo step. Returns the path.",
17425
+ inputSchema: { type: "object", $defs: DEFS2, properties: { pathId: PATH_ID, ...PATH_PROPS }, required: ["pathId"], additionalProperties: false },
17426
+ annotations: { title: "Replace a document path", ...WRITE_IDEMPOTENT5 },
17427
+ handler: (args) => ({ path: pathSummary(controller.setPathGeometry(String(args.pathId), pathSpec(args)).id) })
17428
+ };
17429
+ const renamePath = {
17430
+ name: "rename_path",
17431
+ title: "Rename or save a path",
17432
+ description: "Renames a document path. Renaming the Work Path saves it (Paths \u203A Save Path), so make_work_path no longer replaces it. One undo step. Returns the path.",
17433
+ inputSchema: { type: "object", properties: { pathId: PATH_ID, name: { type: "string", minLength: 1, description: "The new name." } }, required: ["pathId", "name"], additionalProperties: false },
17434
+ annotations: { title: "Rename or save a path", ...WRITE_IDEMPOTENT5 },
17435
+ handler: ({ pathId, name }) => ({ path: pathSummary(controller.renamePath(String(pathId), String(name)).id) })
17436
+ };
17437
+ const duplicatePath = {
17438
+ name: "duplicate_path",
17439
+ title: "Duplicate a path",
17440
+ description: 'Duplicates a document path as a saved path (default name "<name> copy"). One undo step. Returns the new path.',
17441
+ inputSchema: { type: "object", properties: { pathId: PATH_ID, name: { type: "string", description: "The copy's name." } }, required: ["pathId"], additionalProperties: false },
17442
+ annotations: { title: "Duplicate a path", ...WRITE8 },
17443
+ handler: ({ pathId, name }) => ({ path: pathSummary(controller.duplicatePath(String(pathId), name).id) })
17444
+ };
17445
+ const deletePath = {
17446
+ name: "delete_path",
17447
+ title: "Delete a path",
17448
+ description: "Deletes a document path (undo_image brings it back). Returns the remaining paths.",
17449
+ inputSchema: { type: "object", properties: { pathId: PATH_ID }, required: ["pathId"], additionalProperties: false },
17450
+ annotations: { title: "Delete a path", ...DESTRUCTIVE6 },
17451
+ handler: ({ pathId }) => {
17452
+ controller.deletePath(String(pathId));
17453
+ return { paths: controller.getPaths().map((p) => pathSummary(p.id)) };
17454
+ }
17455
+ };
17456
+ const PAINT_PROPS = {
17457
+ layerId: { type: "string", description: "The pixel layer to paint on (default the active layer)." },
17458
+ color: { type: "string", description: "A CSS colour such as #ff0000 (default the foreground colour)." },
17459
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." }
17460
+ };
17461
+ const fillPath = {
17462
+ name: "fill_path",
17463
+ title: "Fill a path",
17464
+ description: "Paths \u203A Fill Path: fills the area a document path encloses on a pixel layer with a colour. The selection is not changed. One undo step. Returns what was painted.",
17465
+ inputSchema: { type: "object", properties: { pathId: PATH_ID, ...PAINT_PROPS }, required: ["pathId"], additionalProperties: false },
17466
+ annotations: { title: "Fill a path", ...WRITE8 },
17467
+ handler: (args) => controller.fillPath(String(args.pathId), { layerId: args.layerId, color: args.color, opacity: args.opacity })
17468
+ };
17469
+ const strokePath = {
17470
+ name: "stroke_path",
17471
+ title: "Stroke a path",
17472
+ description: "Paths \u203A Stroke Path: strokes along a document path's outline on a pixel layer, width px wide and centred on it. The selection is not changed. One undo step. Returns what was painted.",
17473
+ inputSchema: {
17474
+ type: "object",
17475
+ properties: { pathId: PATH_ID, width: { type: "number", exclusiveMinimum: 0, maximum: 250, description: "Stroke width in px." }, ...PAINT_PROPS },
17476
+ required: ["pathId", "width"],
17477
+ additionalProperties: false
17478
+ },
17479
+ annotations: { title: "Stroke a path", ...WRITE8 },
17480
+ handler: (args) => controller.strokePath(String(args.pathId), { width: args.width, layerId: args.layerId, color: args.color, opacity: args.opacity })
17481
+ };
17482
+ return [
17483
+ get,
17484
+ add2,
17485
+ update,
17486
+ setComponent,
17487
+ addComponent,
17488
+ removeComponent,
17489
+ merge,
17490
+ getVectorMask,
17491
+ addVectorMask,
17492
+ setVectorMaskPath,
17493
+ setVectorMaskProps,
17494
+ rasterizeVectorMask,
17495
+ removeVectorMask,
17496
+ selectionToPath,
17497
+ pathToSelection2,
17498
+ listPaths,
17499
+ getPath2,
17500
+ makeWorkPath,
17501
+ addPath,
17502
+ setPathGeometry,
17503
+ renamePath,
17504
+ duplicatePath,
17505
+ deletePath,
17506
+ fillPath,
17507
+ strokePath
17508
+ ];
17090
17509
  }
17091
17510
 
17092
17511
  // src/image-editor/type-tools.ts
@@ -17733,6 +18152,9 @@ var SelectAndMaskSession = class {
17733
18152
  dragStart = null;
17734
18153
  pixels = null;
17735
18154
  previewPixels = null;
18155
+ /** The layers below the active one, composited (On Layers shows them where nothing is selected). */
18156
+ below = null;
18157
+ belowPreview = null;
17736
18158
  base;
17737
18159
  previewMask = null;
17738
18160
  baseMask = null;
@@ -17764,10 +18186,24 @@ var SelectAndMaskSession = class {
17764
18186
  this.emit();
17765
18187
  return false;
17766
18188
  }
18189
+ this.below = this.layersBelow();
17767
18190
  this.rescale();
17768
18191
  this.recompute();
17769
18192
  return true;
17770
18193
  }
18194
+ /** The visible layers under the active one, composited at full size, or null when there are none. */
18195
+ layersBelow() {
18196
+ const active = this.controller.getState().activeLayerId;
18197
+ const flat = flattenLayers(this.controller.getLayers()).filter((l) => l.type !== "group");
18198
+ const at = flat.findIndex((l) => l.id === active);
18199
+ const ids = (at > 0 ? flat.slice(0, at) : []).filter((l) => l.visible).map((l) => l.id);
18200
+ if (!ids.length) return null;
18201
+ try {
18202
+ return this.controller.compositePixels(void 0, { layerIds: ids });
18203
+ } catch {
18204
+ return null;
18205
+ }
18206
+ }
17771
18207
  subscribe(listener) {
17772
18208
  this.listeners.add(listener);
17773
18209
  return () => this.listeners.delete(listener);
@@ -17800,6 +18236,7 @@ var SelectAndMaskSession = class {
17800
18236
  const w = Math.max(1, Math.round(px2.width * s));
17801
18237
  const h = Math.max(1, Math.round(px2.height * s));
17802
18238
  this.previewPixels = scalePixels(px2, w, h);
18239
+ this.belowPreview = this.below ? scalePixels(this.below, w, h) : null;
17803
18240
  this.baseMask = this.base?.bounds ? s === 1 ? this.base : resizeSelection(this.base, w, h) : null;
17804
18241
  }
17805
18242
  /**
@@ -17923,6 +18360,20 @@ var SelectAndMaskSession = class {
17923
18360
  }
17924
18361
  break;
17925
18362
  }
18363
+ case "on-layers": {
18364
+ const under = this.belowPreview?.data;
18365
+ let ur = checker;
18366
+ let ug = checker;
18367
+ let ub = checker;
18368
+ if (under) {
18369
+ const ua = under[p + 3] / 255;
18370
+ ur = under[p] * ua + checker * (1 - ua);
18371
+ ug = under[p + 1] * ua + checker * (1 - ua);
18372
+ ub = under[p + 2] * ua + checker * (1 - ua);
18373
+ }
18374
+ o = [r + (ur - r) * hide, g + (ug - g) * hide, b + (ub - b) * hide];
18375
+ break;
18376
+ }
17926
18377
  default:
17927
18378
  o = [r + (checker - r) * hide, g + (checker - g) * hide, b + (checker - b) * hide];
17928
18379
  }
@@ -20250,6 +20701,7 @@ export {
20250
20701
  WGSL_SHADE,
20251
20702
  WGSL_STEP,
20252
20703
  WORKING_SPACE_OPTIONS,
20704
+ WORK_PATH_NAME,
20253
20705
  WORK_SIZE,
20254
20706
  WetGpuEngine,
20255
20707
  WetMedium,
@@ -20715,6 +21167,7 @@ export {
20715
21167
  newEffectId,
20716
21168
  newLayerId,
20717
21169
  newLayerOptions,
21170
+ newPathId,
20718
21171
  normalizeAdjustmentLayer,
20719
21172
  normalizeAdjustmentLayers,
20720
21173
  normalizeAngle,
@@ -20771,8 +21224,11 @@ export {
20771
21224
  parseVersionedDescriptor,
20772
21225
  patchParagraphStyle,
20773
21226
  patchTextStyle,
21227
+ pathAnchorCount,
21228
+ pathBounds,
20774
21229
  pathStrokeCoverage,
20775
21230
  pathStrokeDabs,
21231
+ pathSvgData,
20776
21232
  pathToSelection,
20777
21233
  perDabStrength,
20778
21234
  photoFilterMatrix,
@@ -20825,9 +21281,11 @@ export {
20825
21281
  readAdjustmentControls,
20826
21282
  readDeepRegion,
20827
21283
  readPaintSource,
21284
+ readPaths,
20828
21285
  readPixel,
20829
21286
  readPsd,
20830
21287
  readRegion,
21288
+ readShapeComponents,
20831
21289
  readSourceRegion,
20832
21290
  rectSelection,
20833
21291
  rectToQuad,
@@ -20916,6 +21374,7 @@ export {
20916
21374
  serializeLayers,
20917
21375
  serializeLayersExternal,
20918
21376
  serializeMask,
21377
+ serializePaths,
20919
21378
  serializePatterns,
20920
21379
  serializeSelection,
20921
21380
  setCloneTransformValue,