@pixerate/editor 0.209.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.
Files changed (46) hide show
  1. package/dist/{chunk-QE7D7TZR.cjs → chunk-H65IBXFI.cjs} +74 -25
  2. package/dist/{chunk-X4LUGTQX.cjs → chunk-L52VMQZS.cjs} +21 -10
  3. package/dist/{chunk-DWYQPL2A.js → chunk-LQ5SALHK.js} +13 -2
  4. package/dist/chunk-MQDIP4CF.cjs +462 -0
  5. package/dist/{chunk-KFIOEOCQ.js → chunk-W3TOZTL7.js} +55 -6
  6. package/dist/chunk-XI2NV5U7.js +462 -0
  7. package/dist/{chunk-3EHPLDFQ.cjs → chunk-YF3YZZ6G.cjs} +16 -453
  8. package/dist/{chunk-USQCKRZG.js → chunk-ZGJGSCUW.js} +8 -445
  9. package/dist/image-editor/content-aware-fill-session.d.cts +109 -0
  10. package/dist/image-editor/content-aware-fill-session.d.ts +109 -0
  11. package/dist/image-editor/controller.d.cts +111 -1
  12. package/dist/image-editor/controller.d.ts +111 -1
  13. package/dist/image-editor/index.cjs +1879 -1052
  14. package/dist/image-editor/index.d.cts +2 -0
  15. package/dist/image-editor/index.d.ts +2 -0
  16. package/dist/image-editor/index.js +869 -42
  17. package/dist/image-editor/layers/serialize.d.cts +5 -8
  18. package/dist/image-editor/layers/serialize.d.ts +5 -8
  19. package/dist/image-editor/paths.d.cts +41 -0
  20. package/dist/image-editor/paths.d.ts +41 -0
  21. package/dist/image-editor/psd/export.d.cts +8 -1
  22. package/dist/image-editor/psd/export.d.ts +8 -1
  23. package/dist/image-editor/psd/import.d.cts +7 -0
  24. package/dist/image-editor/psd/import.d.ts +7 -0
  25. package/dist/image-editor/psd/paths.d.cts +11 -0
  26. package/dist/image-editor/psd/paths.d.ts +11 -0
  27. package/dist/image-editor/selection/gesture.d.cts +10 -2
  28. package/dist/image-editor/selection/gesture.d.ts +10 -2
  29. package/dist/image-editor/selection/select-and-mask.d.cts +5 -0
  30. package/dist/image-editor/selection/select-and-mask.d.ts +5 -0
  31. package/dist/image-editor/selection/worker.d.cts +10 -0
  32. package/dist/image-editor/selection/worker.d.ts +10 -0
  33. package/dist/image-editor/selection-worker.cjs +4 -3
  34. package/dist/image-editor/selection-worker.js +2 -1
  35. package/dist/image-editor/types.d.cts +12 -0
  36. package/dist/image-editor/types.d.ts +12 -0
  37. package/dist/paint/heal.d.cts +3 -1
  38. package/dist/paint/heal.d.ts +3 -1
  39. package/dist/paint/index.cjs +4 -2
  40. package/dist/paint/index.js +10 -8
  41. package/dist/paint-editor/index.cjs +6 -6
  42. package/dist/paint-editor/index.js +1 -1
  43. package/package.json +1 -1
  44. package/skills/pixerate-image-editor/SKILL.md +20 -22
  45. package/skills/pixerate-image-editor/references/layer-effects.md +24 -0
  46. package/skills/pixerate-image-selection-masking/SKILL.md +2 -1
@@ -52,7 +52,7 @@ import {
52
52
  workImage,
53
53
  workLuma,
54
54
  workPlanes
55
- } from "../chunk-DWYQPL2A.js";
55
+ } from "../chunk-LQ5SALHK.js";
56
56
  import {
57
57
  ADJUSTMENT_KEYS,
58
58
  ADJUSTMENT_KINDS,
@@ -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";
@@ -523,33 +524,35 @@ import {
523
524
  TransformedPixelSource,
524
525
  applyRetouchToTile,
525
526
  applyToneToDeepTile,
526
- bestOffset,
527
527
  boxBlurPixels,
528
528
  cloneSamplePoint,
529
- contentAwareFill,
530
- dilateMask,
531
529
  dodgeBurn,
532
530
  dodgeBurnCurve,
533
531
  healPixels,
534
532
  isIdentityCloneTransform,
535
533
  luma,
536
- maskBounds,
537
- membraneFill,
538
534
  normalizeRetouch,
539
535
  perDabStrength,
540
536
  readSourceRegion,
541
537
  retouchFilter,
542
538
  rgbaViewOfGrey,
543
539
  saturationOf,
544
- seamlessClone,
545
540
  smudgeEffect,
546
- solveMembrane,
547
541
  sponge,
548
542
  spongeEffect,
549
543
  spotHealPixels,
550
544
  spotHealWindow,
551
545
  toneOf
552
- } from "../chunk-USQCKRZG.js";
546
+ } from "../chunk-ZGJGSCUW.js";
547
+ import {
548
+ bestOffset,
549
+ contentAwareFill,
550
+ dilateMask,
551
+ maskBounds,
552
+ membraneFill,
553
+ seamlessClone,
554
+ solveMembrane
555
+ } from "../chunk-XI2NV5U7.js";
553
556
  import {
554
557
  BODY_SCATTER,
555
558
  DEFAULT_FLOW,
@@ -1475,8 +1478,9 @@ var ImageEditorRenderer = class {
1475
1478
  ctx.restore();
1476
1479
  }
1477
1480
  const { width: iw, height: ih } = state.imageDimensions;
1478
- const quickMaskSel = display === "quickMask" && state.quickMaskEditing && !state.selection?.bounds ? this.emptyMask(Math.round(iw), Math.round(ih)) : null;
1479
- 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) {
1480
1484
  ctx.save();
1481
1485
  try {
1482
1486
  applyImageProjectionToContext(ctx, projection);
@@ -1484,8 +1488,8 @@ var ImageEditorRenderer = class {
1484
1488
  ctx.rect(region.x, region.y, region.width, region.height);
1485
1489
  ctx.clip();
1486
1490
  if (quickMaskSel) this.drawQuickMask(ctx, quickMaskSel);
1487
- else if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
1488
- 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);
1489
1493
  if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
1490
1494
  } catch {
1491
1495
  }
@@ -2859,6 +2863,58 @@ function magneticAnchorSpacing(frequency = 57) {
2859
2863
  return 8 + (100 - f);
2860
2864
  }
2861
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
+
2862
2918
  // src/image-editor/layers/shape-type-ui.ts
2863
2919
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
2864
2920
  kind: "rect",
@@ -5641,6 +5697,7 @@ var ImageEditorController = class {
5641
5697
  outputOpacity: s.outputOpacity,
5642
5698
  selection: s.selection ?? null,
5643
5699
  ...s.channels?.length ? { channels: s.channels } : {},
5700
+ ...s.paths?.length ? { paths: s.paths } : {},
5644
5701
  source: {
5645
5702
  sourceUrl: s.sourceUrl,
5646
5703
  element: this.imageElement,
@@ -5678,6 +5735,9 @@ var ImageEditorController = class {
5678
5735
  s.selection = entry.selection;
5679
5736
  if (entry.channels?.length) s.channels = entry.channels;
5680
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;
5681
5741
  if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
5682
5742
  this.imageElement = entry.source.element;
5683
5743
  s.sourceUrl = entry.source.sourceUrl;
@@ -6489,6 +6549,138 @@ var ImageEditorController = class {
6489
6549
  const mask = pathToSelection(components, width, height, { ...options, fillRule: path.fillRule ?? options.fillRule });
6490
6550
  return this.setSelection(mask, options.mode ?? "replace");
6491
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
+ }
6492
6684
  /** Replaces a vector mask's path (its properties are kept). One undo step. Returns the vector mask. */
6493
6685
  setVectorMaskPath(id, path) {
6494
6686
  const { vm } = this.requireVectorMask(id);
@@ -7903,9 +8095,46 @@ var ImageEditorController = class {
7903
8095
  * (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
7904
8096
  * coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
7905
8097
  * above it. Samples within `sampleRadius` px of the selection (default its larger side, at
7906
- * least 32). Deterministic for a `seed`. One undo step. 8-bit documents only for now.
8098
+ * least 32), or from the given sampling area. Deterministic for a `seed`. One undo step.
8099
+ * 8-bit documents only for now. Runs here; see `contentAwareFillAsync` for the worker.
7907
8100
  */
7908
8101
  contentAwareFill(options = {}) {
8102
+ const plan = this.prepareContentAwareFill(options);
8103
+ const filled = contentAwareFill(plan.window.width, plan.window.height, 4, plan.before, plan.hole, { seed: options.seed ?? 0, exclude: plan.exclude });
8104
+ return this.applyContentAwareFill(plan, Uint8ClampedArray.from(filled), options.output ?? "current");
8105
+ }
8106
+ /**
8107
+ * `contentAwareFill` with the synthesis in the selection worker when there is one, with
8108
+ * `signal` and `onProgress`. The layer is checked again before the result lands.
8109
+ */
8110
+ async contentAwareFillAsync(options = {}) {
8111
+ const plan = this.prepareContentAwareFill(options);
8112
+ const filled = await this.synthesiseFill(plan, options.seed ?? 0, options);
8113
+ return this.applyContentAwareFill(plan, filled, options.output ?? "current");
8114
+ }
8115
+ /**
8116
+ * The fill without changing anything: the window, its pixels before and after (RGBA, the fill
8117
+ * blended by the selection's coverage) and the sampling area used, for a preview.
8118
+ */
8119
+ async previewContentAwareFill(options = {}) {
8120
+ const plan = this.prepareContentAwareFill(options);
8121
+ const filled = await this.synthesiseFill(plan, options.seed ?? 0, options);
8122
+ return { plan, after: this.blendFill(plan, filled, "current") };
8123
+ }
8124
+ async synthesiseFill(plan, seed, options) {
8125
+ const { width, height } = plan.window;
8126
+ const { pixels } = await this.runSelectionJob(
8127
+ { op: "fill", hole: plan.hole, exclude: plan.exclude, seed },
8128
+ { width, height, data: plan.before },
8129
+ options
8130
+ );
8131
+ return pixels;
8132
+ }
8133
+ /**
8134
+ * Checks and gathers what a Content-Aware Fill needs: the selection, the 8-bit pixel layer, the
8135
+ * window around the selection (and the sampling area), and which pixels it may copy from.
8136
+ */
8137
+ prepareContentAwareFill(options = {}) {
7909
8138
  const sel = this.state.selection;
7910
8139
  if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
7911
8140
  if (this.bitDepth !== 8) throw new LayerOperationError("Content-Aware Fill works on 8-bit documents for now; set_bit_depth 8 first, or use spot_heal.");
@@ -7914,33 +8143,70 @@ var ImageEditorController = class {
7914
8143
  const target = this.paintTarget(options.layerId, "pixels", "fill");
7915
8144
  const { width, height } = this.naturalSize;
7916
8145
  const b = sel.bounds;
7917
- const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
7918
- const x0 = Math.max(0, b.x - margin);
7919
- const y0 = Math.max(0, b.y - margin);
7920
- const win = { x: x0, y: y0, width: Math.min(width, b.x + b.width + margin) - x0, height: Math.min(height, b.y + b.height + margin) - y0 };
7921
- if (win.width * win.height > 16e6) throw new LayerOperationError("The area to fill and sample is too large (over 16 MP); fill a smaller selection or lower sampleRadius.");
7922
- const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
7923
- const before = readRegion(target.surface, layerRect);
7924
- const img = new Float32Array(before.length);
7925
- for (let i = 0; i < before.length; i++) img[i] = before[i];
7926
- const coverage = new Float32Array(win.width * win.height);
7927
- const hole = new Uint8Array(win.width * win.height);
8146
+ let area = options.samplingArea ?? null;
8147
+ if (!area && options.samplingChannelId) area = this.requireChannel(options.samplingChannelId).mask;
8148
+ if (!area && options.sampleRects?.length) {
8149
+ for (const r of options.sampleRects) area = combineSelections(area, rectSelection(width, height, r, { antiAlias: false }), "add");
8150
+ }
8151
+ if (area && !area.bounds) throw new LayerOperationError("The sampling area is empty; paint or give an area to sample from.");
8152
+ let win;
8153
+ if (area?.bounds) {
8154
+ const a = area.bounds;
8155
+ const x0 = Math.max(0, Math.min(b.x - 8, a.x));
8156
+ const y0 = Math.max(0, Math.min(b.y - 8, a.y));
8157
+ win = { x: x0, y: y0, width: Math.min(width, Math.max(b.x + b.width + 8, a.x + a.width)) - x0, height: Math.min(height, Math.max(b.y + b.height + 8, a.y + a.height)) - y0 };
8158
+ } else {
8159
+ const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
8160
+ const x0 = Math.max(0, b.x - margin);
8161
+ const y0 = Math.max(0, b.y - margin);
8162
+ win = { x: x0, y: y0, width: Math.min(width, b.x + b.width + margin) - x0, height: Math.min(height, b.y + b.height + margin) - y0 };
8163
+ }
8164
+ if (win.width * win.height > 16e6) throw new LayerOperationError("The area to fill and sample is too large (over 16 MP); fill a smaller selection or sample from a smaller area.");
8165
+ const before = readRegion(target.surface, { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height });
8166
+ const n = win.width * win.height;
8167
+ const coverage = new Uint8Array(n);
8168
+ const hole = new Uint8Array(n);
8169
+ const selData = sel.data;
7928
8170
  for (let y = 0; y < win.height; y++) {
7929
8171
  for (let x = 0; x < win.width; x++) {
7930
- const c = sel.data[(win.y + y) * sel.width + win.x + x] / 255;
8172
+ const c = selData[(win.y + y) * sel.width + win.x + x];
7931
8173
  coverage[y * win.width + x] = c;
7932
8174
  hole[y * win.width + x] = c > 0 ? 1 : 0;
7933
8175
  }
7934
8176
  }
7935
- const filled = contentAwareFill(win.width, win.height, 4, img, hole, { seed: options.seed ?? 0 });
8177
+ let exclude = null;
8178
+ if (area || options.excludeRects?.length) {
8179
+ exclude = new Uint8Array(n);
8180
+ if (area) {
8181
+ const ad = area.data;
8182
+ for (let y = 0; y < win.height; y++) for (let x = 0; x < win.width; x++) if (ad[(win.y + y) * width + win.x + x] < 128) exclude[y * win.width + x] = 1;
8183
+ }
8184
+ for (const r of options.excludeRects ?? []) {
8185
+ for (let y = Math.max(0, Math.floor(r.y) - win.y); y < Math.min(win.height, Math.ceil(r.y + r.height) - win.y); y++) {
8186
+ for (let x = Math.max(0, Math.floor(r.x) - win.x); x < Math.min(win.width, Math.ceil(r.x + r.width) - win.x); x++) exclude[y * win.width + x] = 1;
8187
+ }
8188
+ }
8189
+ }
8190
+ return { layerId: target.id, window: win, bounds: { ...b }, before, hole, coverage, exclude };
8191
+ }
8192
+ /** The window's pixels with `filled` blended in by the coverage (`current`), or the fill alone, its alpha by the coverage (`new-layer`). */
8193
+ blendFill(plan, filled, output) {
8194
+ const { before, coverage } = plan;
7936
8195
  const out = new Uint8ClampedArray(before.length);
7937
8196
  for (let i = 0; i < coverage.length; i++) {
7938
- const k = coverage[i];
8197
+ const k = coverage[i] / 255;
7939
8198
  for (let c = 0; c < 4; c++) {
7940
8199
  const p = i * 4 + c;
7941
8200
  out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
7942
8201
  }
7943
8202
  }
8203
+ return out;
8204
+ }
8205
+ /** Puts a computed fill into the document as one undo step. */
8206
+ applyContentAwareFill(plan, filled, output) {
8207
+ const target = this.paintTarget(plan.layerId, "pixels", "fill");
8208
+ const { window: win, bounds: b } = plan;
8209
+ const out = this.blendFill(plan, filled, output);
7944
8210
  if (output === "new-layer") {
7945
8211
  const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
7946
8212
  const data = new Uint8ClampedArray(b.width * b.height * 4);
@@ -7948,6 +8214,7 @@ var ImageEditorController = class {
7948
8214
  const id = this.addPixelLayer({ name: "Content-Aware Fill", pixels: { width: b.width, height: b.height, data }, x: b.x, y: b.y, aboveId: target.id });
7949
8215
  return { layerId: id, bounds: { ...b }, output };
7950
8216
  }
8217
+ const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
7951
8218
  const surface = writeRegion(target.surface, layerRect, out);
7952
8219
  this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
7953
8220
  return { layerId: target.id, bounds: { ...b }, output };
@@ -8434,6 +8701,17 @@ var ImageEditorController = class {
8434
8701
  this.notify();
8435
8702
  return true;
8436
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
+ }
8437
8715
  /** Select › All. */
8438
8716
  selectAll() {
8439
8717
  const { width, height } = this.naturalSize;
@@ -10405,6 +10683,10 @@ var ImageEditorController = class {
10405
10683
  }));
10406
10684
  if (psdChannels.length) s.channels = psdChannels;
10407
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;
10408
10690
  s.selectedAnnotationId = null;
10409
10691
  s.activeLayerId = defaultActiveLayerId(s.layers);
10410
10692
  s.activeTarget = "pixels";
@@ -10602,7 +10884,7 @@ var ImageEditorController = class {
10602
10884
  opacity: c.opacity,
10603
10885
  indicates: c.indicates
10604
10886
  }));
10605
- 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 });
10606
10888
  warnings.push(...result.warnings);
10607
10889
  return { bytes: result.bytes, warnings, psb: result.psb, width, height };
10608
10890
  }
@@ -10676,6 +10958,7 @@ var ImageEditorController = class {
10676
10958
  out = this.serializeState();
10677
10959
  if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
10678
10960
  if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
10961
+ if (this.state.paths?.length) out.paths = serializePaths(this.state.paths);
10679
10962
  } finally {
10680
10963
  if (root) {
10681
10964
  this.state = current;
@@ -10720,6 +11003,10 @@ var ImageEditorController = class {
10720
11003
  const channels = deserializeChannels(data.channels, this.naturalSize, problems);
10721
11004
  if (channels.length) this.state.channels = channels;
10722
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;
10723
11010
  if (problems.length) this.lastLoadProblems = [...this.lastLoadProblems, ...problems];
10724
11011
  this.notify();
10725
11012
  }
@@ -11696,7 +11983,7 @@ var DEPTH_MASK_CONTROLS = [
11696
11983
  // src/image-editor/playback.ts
11697
11984
  var IMAGE_PLAYBACK_KIND = "layer";
11698
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)$/;
11699
- 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)$/;
11700
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)$/;
11701
11988
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
11702
11989
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -11924,6 +12211,13 @@ var rectProperties = {
11924
12211
  width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
11925
12212
  height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
11926
12213
  };
12214
+ var rectSchemaStrict = {
12215
+ type: "object",
12216
+ properties: rectProperties,
12217
+ required: ["x", "y", "width", "height"],
12218
+ additionalProperties: false,
12219
+ description: "A rectangle in natural image pixels."
12220
+ };
11927
12221
  function shapeOptions(args) {
11928
12222
  return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
11929
12223
  }
@@ -12777,25 +13071,33 @@ function createImageSelectionTools(controller) {
12777
13071
  const fillTool = {
12778
13072
  name: "content_aware_fill",
12779
13073
  title: "Content-Aware Fill",
12780
- description: "Edit \u203A Content-Aware Fill: removes what is selected by filling it with texture and edges synthesised from around it (on a pixel layer, default the active one). Select the object with a small margin (expand 2\u20134 px) first. output current paints into the layer; new-layer puts the fill on its own layer above it, so it can be masked or removed (recommended). sampleRadius limits where it samples from. Deterministic for a seed. 8-bit documents. One undo step. Returns the layer id and filled bounds.",
13074
+ description: "Edit \u203A Content-Aware Fill: removes what is selected by filling it with texture and edges synthesised from around it (on a pixel layer, default the active one). Select the object with a small margin (expand 2\u20134 px) first. output current paints into the layer; new-layer puts the fill on its own layer above it, so it can be masked or removed (recommended). sampleRadius limits where it samples from; sampleRects (only these areas), samplingChannelId (a saved alpha channel) and excludeRects (never these, e.g. another object that would be copied in) set the sampling area as the Content-Aware Fill workspace does. Deterministic for a seed. 8-bit documents. One undo step. Returns the layer id and filled bounds.",
12781
13075
  inputSchema: {
12782
13076
  type: "object",
12783
13077
  properties: {
12784
13078
  output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
12785
13079
  sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
12786
13080
  seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
13081
+ sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
13082
+ excludeRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Never sample from these rectangles (natural image pixels)." },
13083
+ samplingChannelId: { type: "string", description: "Sample only where this alpha channel is selected (see list_channels)." },
12787
13084
  layerId: layerIdSchema
12788
13085
  },
12789
13086
  additionalProperties: false
12790
13087
  },
12791
13088
  annotations: writeAnnotations("Content-Aware Fill"),
12792
- handler: (args) => {
13089
+ handler: async (args, context) => {
12793
13090
  if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
12794
- return controller.contentAwareFill({
13091
+ const rects = (v) => Array.isArray(v) ? v.map((r) => ({ x: num2(r?.x, NaN), y: num2(r?.y, NaN), width: num2(r?.width, NaN), height: num2(r?.height, NaN) })) : void 0;
13092
+ return controller.contentAwareFillAsync({
12795
13093
  output: args.output,
12796
13094
  sampleRadius: args.sampleRadius,
12797
13095
  seed: args.seed,
12798
- layerId: requireLayerId(args.layerId)
13096
+ layerId: requireLayerId(args.layerId),
13097
+ sampleRects: rects(args.sampleRects),
13098
+ excludeRects: rects(args.excludeRects),
13099
+ samplingChannelId: typeof args.samplingChannelId === "string" ? args.samplingChannelId : void 0,
13100
+ signal: context?.signal
12799
13101
  });
12800
13102
  }
12801
13103
  };
@@ -14405,6 +14707,9 @@ var SelectionGesture = class {
14405
14707
  /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
14406
14708
  error = null;
14407
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();
14408
14713
  get active() {
14409
14714
  return this.start !== null || this.polygon !== null || this.magnetic !== null;
14410
14715
  }
@@ -14412,6 +14717,46 @@ var SelectionGesture = class {
14412
14717
  get settled() {
14413
14718
  return this.pending;
14414
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
+ }
14415
14760
  /** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
14416
14761
  get magneticAnchors() {
14417
14762
  const m = this.magnetic;
@@ -14423,7 +14768,8 @@ var SelectionGesture = class {
14423
14768
  if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
14424
14769
  if (!this.start || !this.current) return null;
14425
14770
  const { tool } = this.getOptions();
14426
- 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;
14427
14773
  const rect = normalizeRect({
14428
14774
  x: this.start.x,
14429
14775
  y: this.start.y,
@@ -14536,6 +14882,18 @@ var SelectionGesture = class {
14536
14882
  this.start = point;
14537
14883
  this.current = point;
14538
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
+ }
14539
14897
  }
14540
14898
  /** Pointer move; returns true when the draft changed (re-render). */
14541
14899
  move(point, modifiers = {}) {
@@ -14562,6 +14920,7 @@ var SelectionGesture = class {
14562
14920
  const last = this.points[this.points.length - 1];
14563
14921
  if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
14564
14922
  this.points.push(point);
14923
+ if (drawn === "quick") this.growPreview();
14565
14924
  }
14566
14925
  return true;
14567
14926
  }
@@ -14597,7 +14956,8 @@ var SelectionGesture = class {
14597
14956
  this.start = null;
14598
14957
  this.current = null;
14599
14958
  this.points = [];
14600
- 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;
14601
14961
  this.pending = this.controller.quickSelect([stroke], {
14602
14962
  size: options.quickSize ?? 30,
14603
14963
  mode: strokeMode,
@@ -14608,7 +14968,7 @@ var SelectionGesture = class {
14608
14968
  (err) => {
14609
14969
  this.error = err instanceof Error ? err.message : String(err);
14610
14970
  }
14611
- );
14971
+ ).finally(() => this.controller.setSelectionPreview(void 0));
14612
14972
  return false;
14613
14973
  }
14614
14974
  const path = this.draftPath;
@@ -14737,6 +15097,10 @@ var SelectionGesture = class {
14737
15097
  }
14738
15098
  /** Abandons a drag (or a polygonal lasso) without changing the selection. */
14739
15099
  cancel() {
15100
+ if (this.quick) {
15101
+ this.quick = null;
15102
+ this.controller.setSelectionPreview(void 0);
15103
+ }
14740
15104
  this.start = null;
14741
15105
  this.current = null;
14742
15106
  this.points = [];
@@ -16853,6 +17217,19 @@ function createImageShapeTools(controller) {
16853
17217
  fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." }
16854
17218
  };
16855
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
+ };
16856
17233
  const TRACE_PROPS = {
16857
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)." },
16858
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)." },
@@ -16884,6 +17261,7 @@ function createImageShapeTools(controller) {
16884
17261
  type: "object",
16885
17262
  $defs: DEFS2,
16886
17263
  properties: {
17264
+ pathId: { ...PATH_ID, description: "A document path from list_paths, instead of shape / subpaths / components." },
16887
17265
  ...PATH_PROPS,
16888
17266
  feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather radius in px (default 0)." },
16889
17267
  antiAlias: { type: "boolean", description: "Soft edges (default true); false makes every pixel fully in or out." },
@@ -16894,7 +17272,7 @@ function createImageShapeTools(controller) {
16894
17272
  annotations: { title: "Make a selection from a path", ...WRITE8 },
16895
17273
  handler: (args) => {
16896
17274
  const mode = args.mode;
16897
- 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 });
16898
17276
  const sel = controller.getSelection();
16899
17277
  return { empty: !sel, bounds: sel?.bounds ?? null, area: Math.round(sel ? selectionArea(sel) : 0) };
16900
17278
  }
@@ -16916,7 +17294,8 @@ function createImageShapeTools(controller) {
16916
17294
  $defs: DEFS2,
16917
17295
  properties: {
16918
17296
  layerId: LAYER,
16919
- 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." },
16920
17299
  ...PATH_PROPS,
16921
17300
  ...TRACE_PROPS
16922
17301
  },
@@ -16926,7 +17305,7 @@ function createImageShapeTools(controller) {
16926
17305
  annotations: { title: "Add a vector mask", ...WRITE8 },
16927
17306
  handler: (args) => {
16928
17307
  const id = layerArg(args.layerId);
16929
- const spec = pathSpec(args);
17308
+ const spec = fromPathId(args) ?? pathSpec(args);
16930
17309
  const hasPath = spec.components?.length || spec.shape !== void 0 || spec.subpaths !== void 0;
16931
17310
  const from = args.from ?? (hasPath ? "path" : "reveal-all");
16932
17311
  if (from !== "selection" && TRACE_KEYS.some((k) => args[k] !== void 0)) throw new Error("tolerance, cornerRoundness and minArea apply only to from selection.");
@@ -16994,7 +17373,139 @@ function createImageShapeTools(controller) {
16994
17373
  return { layerId: id, vectorMask: null };
16995
17374
  }
16996
17375
  };
16997
- 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
+ ];
16998
17509
  }
16999
17510
 
17000
17511
  // src/image-editor/type-tools.ts
@@ -17641,6 +18152,9 @@ var SelectAndMaskSession = class {
17641
18152
  dragStart = null;
17642
18153
  pixels = null;
17643
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;
17644
18158
  base;
17645
18159
  previewMask = null;
17646
18160
  baseMask = null;
@@ -17672,10 +18186,24 @@ var SelectAndMaskSession = class {
17672
18186
  this.emit();
17673
18187
  return false;
17674
18188
  }
18189
+ this.below = this.layersBelow();
17675
18190
  this.rescale();
17676
18191
  this.recompute();
17677
18192
  return true;
17678
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
+ }
17679
18207
  subscribe(listener) {
17680
18208
  this.listeners.add(listener);
17681
18209
  return () => this.listeners.delete(listener);
@@ -17708,6 +18236,7 @@ var SelectAndMaskSession = class {
17708
18236
  const w = Math.max(1, Math.round(px2.width * s));
17709
18237
  const h = Math.max(1, Math.round(px2.height * s));
17710
18238
  this.previewPixels = scalePixels(px2, w, h);
18239
+ this.belowPreview = this.below ? scalePixels(this.below, w, h) : null;
17711
18240
  this.baseMask = this.base?.bounds ? s === 1 ? this.base : resizeSelection(this.base, w, h) : null;
17712
18241
  }
17713
18242
  /**
@@ -17831,6 +18360,20 @@ var SelectAndMaskSession = class {
17831
18360
  }
17832
18361
  break;
17833
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
+ }
17834
18377
  default:
17835
18378
  o = [r + (checker - r) * hide, g + (checker - g) * hide, b + (checker - b) * hide];
17836
18379
  }
@@ -19622,6 +20165,278 @@ function createImageEditorDocument(controller, options = {}) {
19622
20165
  }
19623
20166
  });
19624
20167
  }
20168
+
20169
+ // src/image-editor/content-aware-fill-session.ts
20170
+ var CONTENT_AWARE_FILL_TOOLS = [
20171
+ { id: "sampling-brush", label: "Sampling Brush", key: "b" },
20172
+ { id: "hand", label: "Hand", key: "h" },
20173
+ { id: "zoom", label: "Zoom", key: "z" }
20174
+ ];
20175
+ var CONTENT_AWARE_FILL_SAMPLING = [
20176
+ { id: "auto", label: "Auto" },
20177
+ { id: "rectangular", label: "Rectangular" },
20178
+ { id: "custom", label: "Custom" }
20179
+ ];
20180
+ var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
20181
+ sampling: "auto",
20182
+ showSampling: true,
20183
+ opacity: 0.5,
20184
+ tool: "sampling-brush",
20185
+ brushSize: 40,
20186
+ brushMode: "add",
20187
+ seed: 0,
20188
+ output: "new-layer",
20189
+ livePreview: true
20190
+ };
20191
+ var ContentAwareFillSession = class {
20192
+ constructor(controller) {
20193
+ this.controller = controller;
20194
+ }
20195
+ controller;
20196
+ settingsValue = { ...DEFAULT_CONTENT_AWARE_FILL_SETTINGS };
20197
+ plan = null;
20198
+ /** The source view's window (document pixels): the selection with room around it to sample. */
20199
+ win = null;
20200
+ before = null;
20201
+ hole = null;
20202
+ /** 1 where the fill may copy from, per window pixel. */
20203
+ area = null;
20204
+ after = null;
20205
+ lastPoint = null;
20206
+ listeners = /* @__PURE__ */ new Set();
20207
+ dirty = false;
20208
+ /** Why the workspace can't fill (nothing selected, not a pixel layer), for the UI. */
20209
+ error = null;
20210
+ computing = false;
20211
+ revision = 0;
20212
+ get settings() {
20213
+ return this.settingsValue;
20214
+ }
20215
+ /** The source view's window in document pixels (`null` until opened). */
20216
+ get window() {
20217
+ return this.win;
20218
+ }
20219
+ subscribe(listener) {
20220
+ this.listeners.add(listener);
20221
+ return () => this.listeners.delete(listener);
20222
+ }
20223
+ emit() {
20224
+ this.revision++;
20225
+ for (const l of this.listeners) l();
20226
+ }
20227
+ /**
20228
+ * Reads the layer around the selection and makes the first preview. The window holds the
20229
+ * selection and room around it to sample from (its larger side, at least 64 px, each way).
20230
+ */
20231
+ async open() {
20232
+ try {
20233
+ const sel = this.controller.getSelection();
20234
+ const b = sel?.bounds;
20235
+ const margin = b ? Math.max(64, Math.max(b.width, b.height)) : 64;
20236
+ this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
20237
+ } catch (err) {
20238
+ this.error = err instanceof Error ? err.message : String(err);
20239
+ this.emit();
20240
+ return false;
20241
+ }
20242
+ this.win = this.plan.window;
20243
+ this.before = this.plan.before;
20244
+ this.hole = this.plan.hole;
20245
+ this.applySampling("auto");
20246
+ this.emit();
20247
+ await this.refresh();
20248
+ return true;
20249
+ }
20250
+ /** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
20251
+ applySampling(mode) {
20252
+ const win = this.win;
20253
+ const hole = this.hole;
20254
+ const n = win.width * win.height;
20255
+ const area = this.area && mode === "custom" ? this.area : new Uint8Array(n);
20256
+ if (mode === "rectangular") {
20257
+ for (let i = 0; i < n; i++) area[i] = hole[i] ? 0 : 1;
20258
+ } else if (mode === "auto") {
20259
+ const b = this.plan.bounds;
20260
+ const reach = Math.max(24, Math.max(b.width, b.height) / 2);
20261
+ const d = distanceTransform(hole, win.width, win.height);
20262
+ for (let i = 0; i < n; i++) area[i] = !hole[i] && d[i] <= reach ? 1 : 0;
20263
+ }
20264
+ this.area = area;
20265
+ }
20266
+ /** Changes settings; a sampling preset replaces the area, and the preview follows when live. */
20267
+ update(changes) {
20268
+ const prev = this.settingsValue;
20269
+ this.settingsValue = { ...prev, ...changes };
20270
+ if (!this.win) {
20271
+ this.emit();
20272
+ return;
20273
+ }
20274
+ if (changes.sampling && changes.sampling !== prev.sampling && changes.sampling !== "custom") this.applySampling(changes.sampling);
20275
+ this.emit();
20276
+ const fillChanged = changes.sampling !== void 0 || changes.seed !== void 0;
20277
+ if (fillChanged && this.settingsValue.livePreview) void this.refresh();
20278
+ }
20279
+ /** The sampling area as a selection of the whole image (what OK samples from). */
20280
+ samplingMask() {
20281
+ if (!this.win || !this.area) return null;
20282
+ const { width, height } = this.controller.getState().imageDimensions;
20283
+ const bytes = new Uint8Array(this.area.length);
20284
+ for (let i = 0; i < bytes.length; i++) bytes[i] = this.area[i] ? 255 : 0;
20285
+ return selectionFromRegion(width, height, this.win, bytes);
20286
+ }
20287
+ /** Whether (x, y) in document pixels is in the sampling area. */
20288
+ samplesAt(x, y) {
20289
+ const win = this.win;
20290
+ if (!win || !this.area) return false;
20291
+ const xi = Math.floor(x - win.x);
20292
+ const yi = Math.floor(y - win.y);
20293
+ if (xi < 0 || yi < 0 || xi >= win.width || yi >= win.height) return false;
20294
+ return this.area[yi * win.width + xi] === 1;
20295
+ }
20296
+ /** Paints the sampling area with the brush along a stroke segment (document pixels). */
20297
+ paint(from, to, add2) {
20298
+ const win = this.win;
20299
+ const area = this.area;
20300
+ const hole = this.hole;
20301
+ const r = Math.max(0.5, this.settingsValue.brushSize / 2);
20302
+ const x0 = Math.max(0, Math.floor(Math.min(from.x, to.x) - r - win.x));
20303
+ const y0 = Math.max(0, Math.floor(Math.min(from.y, to.y) - r - win.y));
20304
+ const x1 = Math.min(win.width - 1, Math.ceil(Math.max(from.x, to.x) + r - win.x));
20305
+ const y1 = Math.min(win.height - 1, Math.ceil(Math.max(from.y, to.y) + r - win.y));
20306
+ const dx = to.x - from.x;
20307
+ const dy = to.y - from.y;
20308
+ const len2 = dx * dx + dy * dy;
20309
+ for (let y = y0; y <= y1; y++) {
20310
+ for (let x = x0; x <= x1; x++) {
20311
+ const px2 = win.x + x + 0.5 - from.x;
20312
+ const py = win.y + y + 0.5 - from.y;
20313
+ const t = len2 > 0 ? Math.min(1, Math.max(0, (px2 * dx + py * dy) / len2)) : 0;
20314
+ if ((px2 - t * dx) ** 2 + (py - t * dy) ** 2 > r * r) continue;
20315
+ const i = y * win.width + x;
20316
+ area[i] = add2 && !hole[i] ? 1 : 0;
20317
+ }
20318
+ }
20319
+ }
20320
+ /** Pointer down with the Sampling Brush (Alt flips add and subtract). Returns false for other tools. */
20321
+ begin(point, modifiers = {}) {
20322
+ if (this.settingsValue.tool !== "sampling-brush" || !this.win) return false;
20323
+ const add2 = this.settingsValue.brushMode === "add" !== Boolean(modifiers.altKey);
20324
+ this.settingsValue = { ...this.settingsValue, sampling: "custom" };
20325
+ this.lastPoint = point;
20326
+ this.strokeAdds = add2;
20327
+ this.paint(point, point, add2);
20328
+ this.emit();
20329
+ return true;
20330
+ }
20331
+ strokeAdds = true;
20332
+ move(point) {
20333
+ if (!this.lastPoint) return;
20334
+ this.paint(this.lastPoint, point, this.strokeAdds);
20335
+ this.lastPoint = point;
20336
+ this.emit();
20337
+ }
20338
+ /** Pointer up: the preview follows the new area when live. */
20339
+ end() {
20340
+ if (!this.lastPoint) return;
20341
+ this.lastPoint = null;
20342
+ if (this.settingsValue.livePreview) void this.refresh();
20343
+ }
20344
+ /** Recomputes the preview: one at a time, the newest area following when one is running. */
20345
+ async refresh() {
20346
+ if (!this.win) return;
20347
+ if (this.computing) {
20348
+ this.dirty = true;
20349
+ return;
20350
+ }
20351
+ this.computing = true;
20352
+ this.emit();
20353
+ do {
20354
+ this.dirty = false;
20355
+ try {
20356
+ const area = this.samplingMask();
20357
+ if (!area?.bounds) throw new Error("The sampling area is empty; paint where the fill may copy from.");
20358
+ const { plan, after } = await this.controller.previewContentAwareFill({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed });
20359
+ const win = this.win;
20360
+ const out = Uint8ClampedArray.from(this.before);
20361
+ const pw = plan.window;
20362
+ for (let y = 0; y < pw.height; y++) {
20363
+ out.set(after.subarray(y * pw.width * 4, (y + 1) * pw.width * 4), ((pw.y - win.y + y) * win.width + (pw.x - win.x)) * 4);
20364
+ }
20365
+ this.after = out;
20366
+ this.error = null;
20367
+ } catch (err) {
20368
+ this.after = null;
20369
+ this.error = err instanceof Error ? err.message : String(err);
20370
+ }
20371
+ this.emit();
20372
+ } while (this.dirty);
20373
+ this.computing = false;
20374
+ this.emit();
20375
+ }
20376
+ /** Resolves once no preview is being computed. */
20377
+ async idle() {
20378
+ while (this.computing) await new Promise((r) => setTimeout(r, 5));
20379
+ }
20380
+ /** The source view: the layer's pixels, the selection tinted red, the sampling area green. RGBA at the window's size. */
20381
+ renderSource() {
20382
+ if (!this.before || !this.area || !this.hole) return new Uint8ClampedArray(0);
20383
+ const out = Uint8ClampedArray.from(this.before);
20384
+ const k = this.settingsValue.showSampling ? Math.min(1, Math.max(0, this.settingsValue.opacity)) : 0;
20385
+ for (let i = 0; i < this.area.length; i++) {
20386
+ const p = i * 4;
20387
+ if (this.hole[i]) {
20388
+ out[p] = out[p] + (220 - out[p]) * 0.5;
20389
+ out[p + 1] *= 0.5;
20390
+ out[p + 2] *= 0.5;
20391
+ } else if (this.area[i] && k) {
20392
+ out[p] *= 1 - k;
20393
+ out[p + 1] = out[p + 1] + (200 - out[p + 1]) * k;
20394
+ out[p + 2] *= 1 - k;
20395
+ }
20396
+ out[p + 3] = 255;
20397
+ }
20398
+ return out;
20399
+ }
20400
+ /** The preview: the window with the fill in place (the source until the first preview). */
20401
+ renderPreview() {
20402
+ const src = this.after ?? this.before;
20403
+ if (!src) return new Uint8ClampedArray(0);
20404
+ const out = Uint8ClampedArray.from(src);
20405
+ for (let i = 3; i < out.length; i += 4) out[i] = 255;
20406
+ return out;
20407
+ }
20408
+ /** Clears painting and goes back to Auto. */
20409
+ reset() {
20410
+ if (!this.win) return;
20411
+ this.settingsValue = { ...DEFAULT_CONTENT_AWARE_FILL_SETTINGS, tool: this.settingsValue.tool };
20412
+ this.applySampling("auto");
20413
+ this.emit();
20414
+ if (this.settingsValue.livePreview) void this.refresh();
20415
+ }
20416
+ /**
20417
+ * Keys: B, H, Z for the tools, [ and ] for the brush size. Returns true when the key was used.
20418
+ */
20419
+ handleKey(key) {
20420
+ const k = key.toLowerCase();
20421
+ const tool = CONTENT_AWARE_FILL_TOOLS.find((t) => t.key === k);
20422
+ if (tool) {
20423
+ this.update({ tool: tool.id });
20424
+ return true;
20425
+ }
20426
+ if (k === "[" || k === "]") {
20427
+ const size = this.settingsValue.brushSize;
20428
+ this.update({ brushSize: Math.max(1, Math.round(k === "]" ? size * 1.25 : size / 1.25)) });
20429
+ return true;
20430
+ }
20431
+ return false;
20432
+ }
20433
+ /** OK: fills with this sampling area, seed and output as one undo step. */
20434
+ async commit(options = {}) {
20435
+ const area = this.samplingMask();
20436
+ if (!this.plan || !area?.bounds) throw new Error(this.error ?? "The sampling area is empty; paint where the fill may copy from.");
20437
+ return this.controller.contentAwareFillAsync({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed, output: this.settingsValue.output, signal: options.signal });
20438
+ }
20439
+ };
19625
20440
  export {
19626
20441
  ADJUSTMENT_KEYS,
19627
20442
  ADJUSTMENT_KINDS,
@@ -19647,7 +20462,10 @@ export {
19647
20462
  COLOR_MODE_OPTIONS,
19648
20463
  COLOR_RANGE_PRESETS,
19649
20464
  COLOR_RANGE_PRESET_LABELS,
20465
+ CONTENT_AWARE_FILL_SAMPLING,
20466
+ CONTENT_AWARE_FILL_TOOLS,
19650
20467
  ColorGmm,
20468
+ ContentAwareFillSession,
19651
20469
  CoverageMap,
19652
20470
  D50,
19653
20471
  D65,
@@ -19657,6 +20475,7 @@ export {
19657
20475
  DEFAULT_BRUSH,
19658
20476
  DEFAULT_CANVAS_VIEWPORT,
19659
20477
  DEFAULT_CHANNEL_COLOR,
20478
+ DEFAULT_CONTENT_AWARE_FILL_SETTINGS,
19660
20479
  DEFAULT_DEPTH_MASK,
19661
20480
  DEFAULT_FLOW,
19662
20481
  DEFAULT_GLOBAL_LIGHT,
@@ -19882,6 +20701,7 @@ export {
19882
20701
  WGSL_SHADE,
19883
20702
  WGSL_STEP,
19884
20703
  WORKING_SPACE_OPTIONS,
20704
+ WORK_PATH_NAME,
19885
20705
  WORK_SIZE,
19886
20706
  WetGpuEngine,
19887
20707
  WetMedium,
@@ -20347,6 +21167,7 @@ export {
20347
21167
  newEffectId,
20348
21168
  newLayerId,
20349
21169
  newLayerOptions,
21170
+ newPathId,
20350
21171
  normalizeAdjustmentLayer,
20351
21172
  normalizeAdjustmentLayers,
20352
21173
  normalizeAngle,
@@ -20403,8 +21224,11 @@ export {
20403
21224
  parseVersionedDescriptor,
20404
21225
  patchParagraphStyle,
20405
21226
  patchTextStyle,
21227
+ pathAnchorCount,
21228
+ pathBounds,
20406
21229
  pathStrokeCoverage,
20407
21230
  pathStrokeDabs,
21231
+ pathSvgData,
20408
21232
  pathToSelection,
20409
21233
  perDabStrength,
20410
21234
  photoFilterMatrix,
@@ -20457,9 +21281,11 @@ export {
20457
21281
  readAdjustmentControls,
20458
21282
  readDeepRegion,
20459
21283
  readPaintSource,
21284
+ readPaths,
20460
21285
  readPixel,
20461
21286
  readPsd,
20462
21287
  readRegion,
21288
+ readShapeComponents,
20463
21289
  readSourceRegion,
20464
21290
  rectSelection,
20465
21291
  rectToQuad,
@@ -20548,6 +21374,7 @@ export {
20548
21374
  serializeLayers,
20549
21375
  serializeLayersExternal,
20550
21376
  serializeMask,
21377
+ serializePaths,
20551
21378
  serializePatterns,
20552
21379
  serializeSelection,
20553
21380
  setCloneTransformValue,