@pixerate/editor 0.183.0 → 0.186.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 (50) hide show
  1. package/dist/{chunk-Y46OMERN.js → chunk-FW7N64XK.js} +298 -189
  2. package/dist/{chunk-UHMSMUHK.cjs → chunk-X2KQQZBT.cjs} +358 -249
  3. package/dist/image-editor/controller.d.cts +208 -8
  4. package/dist/image-editor/controller.d.ts +208 -8
  5. package/dist/image-editor/index.cjs +2610 -1025
  6. package/dist/image-editor/index.js +1672 -87
  7. package/dist/image-editor/layers/model.d.cts +5 -1
  8. package/dist/image-editor/layers/model.d.ts +5 -1
  9. package/dist/image-editor/layers/paint-ui.d.cts +2 -1
  10. package/dist/image-editor/layers/paint-ui.d.ts +2 -1
  11. package/dist/image-editor/layers/panel.d.cts +23 -1
  12. package/dist/image-editor/layers/panel.d.ts +23 -1
  13. package/dist/image-editor/layers/serialize.d.cts +3 -0
  14. package/dist/image-editor/layers/serialize.d.ts +3 -0
  15. package/dist/image-editor/psd/channels.d.cts +24 -0
  16. package/dist/image-editor/psd/channels.d.ts +24 -0
  17. package/dist/image-editor/psd/export.d.cts +3 -0
  18. package/dist/image-editor/psd/export.d.ts +3 -0
  19. package/dist/image-editor/psd/import.d.cts +10 -0
  20. package/dist/image-editor/psd/import.d.ts +10 -0
  21. package/dist/image-editor/renderer.d.cts +6 -0
  22. package/dist/image-editor/renderer.d.ts +6 -0
  23. package/dist/image-editor/selection/channels.d.cts +52 -0
  24. package/dist/image-editor/selection/channels.d.ts +52 -0
  25. package/dist/image-editor/selection/gesture.d.cts +47 -2
  26. package/dist/image-editor/selection/gesture.d.ts +47 -2
  27. package/dist/image-editor/selection/index.d.cts +2 -0
  28. package/dist/image-editor/selection/index.d.ts +2 -0
  29. package/dist/image-editor/selection/modify.d.cts +3 -2
  30. package/dist/image-editor/selection/modify.d.ts +3 -2
  31. package/dist/image-editor/selection/overlay.d.cts +2 -0
  32. package/dist/image-editor/selection/overlay.d.ts +2 -0
  33. package/dist/image-editor/selection/stroke.d.cts +8 -0
  34. package/dist/image-editor/selection/stroke.d.ts +8 -0
  35. package/dist/image-editor/selection/tool-options.d.cts +12 -2
  36. package/dist/image-editor/selection/tool-options.d.ts +12 -2
  37. package/dist/image-editor/selection/wand.d.cts +37 -0
  38. package/dist/image-editor/selection/wand.d.ts +37 -0
  39. package/dist/image-editor/selection-tools.d.cts +8 -3
  40. package/dist/image-editor/selection-tools.d.ts +8 -3
  41. package/dist/image-editor/tools.d.cts +10 -2
  42. package/dist/image-editor/tools.d.ts +10 -2
  43. package/dist/image-editor/types.d.cts +12 -0
  44. package/dist/image-editor/types.d.ts +12 -0
  45. package/dist/paint-editor/index.cjs +6 -6
  46. package/dist/paint-editor/index.js +1 -1
  47. package/package.json +1 -1
  48. package/skills/pixerate-image-editor/SKILL.md +19 -21
  49. package/skills/pixerate-image-editor/references/free-transform.md +22 -0
  50. package/skills/pixerate-image-selection-masking/SKILL.md +36 -3
@@ -370,7 +370,7 @@ import {
370
370
  writePsd,
371
371
  xyToTempTint,
372
372
  xyToUv
373
- } from "../chunk-Y46OMERN.js";
373
+ } from "../chunk-FW7N64XK.js";
374
374
  import {
375
375
  parseCssColor
376
376
  } from "../chunk-3CI7FRMK.js";
@@ -939,12 +939,13 @@ function selectionOutline(mask, threshold = 128) {
939
939
  function quickMaskPixels(mask, style = {}) {
940
940
  const [r, g, b] = style.color ?? [255, 0, 0];
941
941
  const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));
942
+ const selected = style.indicates === "selected";
942
943
  const out = new Uint8ClampedArray(mask.data.length * 4);
943
944
  for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {
944
945
  out[p] = r;
945
946
  out[p + 1] = g;
946
947
  out[p + 2] = b;
947
- out[p + 3] = Math.round((255 - mask.data[i]) * k);
948
+ out[p + 3] = Math.round((selected ? mask.data[i] : 255 - mask.data[i]) * k);
948
949
  }
949
950
  return out;
950
951
  }
@@ -1362,15 +1363,31 @@ var ImageEditorRenderer = class {
1362
1363
  }
1363
1364
  ctx.restore();
1364
1365
  }
1365
- const display = options.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
1366
- if (state.selection?.bounds && display !== "none" || options.draftSelectionPath?.length) {
1366
+ const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
1367
+ const shownChannels = (state.channels ?? []).filter((c) => c.visible);
1368
+ if (shownChannels.length) {
1367
1369
  ctx.save();
1368
1370
  try {
1369
1371
  applyImageProjectionToContext(ctx, projection);
1370
1372
  ctx.beginPath();
1371
1373
  ctx.rect(region.x, region.y, region.width, region.height);
1372
1374
  ctx.clip();
1373
- if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
1375
+ for (const c of shownChannels) this.drawMaskOverlay(ctx, c.mask, { color: hexRgb(c.color), opacity: c.opacity, indicates: c.indicates });
1376
+ } catch {
1377
+ }
1378
+ ctx.restore();
1379
+ }
1380
+ const { width: iw, height: ih } = state.imageDimensions;
1381
+ const quickMaskSel = display === "quickMask" && state.quickMaskEditing && !state.selection?.bounds ? this.emptyMask(Math.round(iw), Math.round(ih)) : null;
1382
+ if (state.selection?.bounds && display !== "none" || quickMaskSel || options.draftSelectionPath?.length) {
1383
+ ctx.save();
1384
+ try {
1385
+ applyImageProjectionToContext(ctx, projection);
1386
+ ctx.beginPath();
1387
+ ctx.rect(region.x, region.y, region.width, region.height);
1388
+ ctx.clip();
1389
+ if (quickMaskSel) this.drawQuickMask(ctx, quickMaskSel);
1390
+ else if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
1374
1391
  else if (state.selection?.bounds && display === "ants") this.drawAnts(ctx, state.selection, projection.scale / u, options.antsOffset ?? 0);
1375
1392
  if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
1376
1393
  } catch {
@@ -1776,6 +1793,31 @@ var ImageEditorRenderer = class {
1776
1793
  }
1777
1794
  quickMaskCache = /* @__PURE__ */ new WeakMap();
1778
1795
  outlineCache = /* @__PURE__ */ new WeakMap();
1796
+ emptyMaskCache = null;
1797
+ /** An empty selection of the image's size (Quick Mask mode with nothing selected). */
1798
+ emptyMask(width, height) {
1799
+ const m = this.emptyMaskCache;
1800
+ if (m && m.width === width && m.height === height) return m;
1801
+ this.emptyMaskCache = createSelectionMask(Math.max(1, width), Math.max(1, height));
1802
+ return this.emptyMaskCache;
1803
+ }
1804
+ overlayCache = /* @__PURE__ */ new WeakMap();
1805
+ /** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
1806
+ drawMaskOverlay(ctx, mask, style) {
1807
+ const key = `${style.color?.join(",")}|${style.opacity}|${style.indicates}`;
1808
+ let cached = this.overlayCache.get(mask);
1809
+ if (!cached || cached.key !== key) {
1810
+ const canvas = this.createCanvas(mask.width, mask.height);
1811
+ const mctx = canvas.getContext("2d");
1812
+ if (!mctx || typeof mctx.createImageData !== "function") return;
1813
+ const img = mctx.createImageData(mask.width, mask.height);
1814
+ img.data.set(quickMaskPixels(mask, style));
1815
+ mctx.putImageData(img, 0, 0);
1816
+ cached = { key, canvas };
1817
+ this.overlayCache.set(mask, cached);
1818
+ }
1819
+ ctx.drawImage(cached.canvas, 0, 0, mask.width, mask.height);
1820
+ }
1779
1821
  /** Rubylith over the unselected area, drawn in natural image space. */
1780
1822
  drawQuickMask(ctx, selection) {
1781
1823
  let canvas = this.quickMaskCache.get(selection);
@@ -2320,6 +2362,12 @@ function removeAnnotationLayers(layers) {
2320
2362
  }
2321
2363
  return out;
2322
2364
  }
2365
+ function hexRgb(hex) {
2366
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex);
2367
+ if (!m) return [255, 0, 0];
2368
+ const v = parseInt(m[1], 16);
2369
+ return [v >> 16 & 255, v >> 8 & 255, v & 255];
2370
+ }
2323
2371
 
2324
2372
  // src/image-editor/layers/shape-type-ui.ts
2325
2373
  var DEFAULT_SHAPE_TOOL_OPTIONS = {
@@ -3065,7 +3113,7 @@ var TONE_RANGE_PRESETS = {
3065
3113
  function toneRange(pixels, range, falloff = 20) {
3066
3114
  const n = checkPixels(pixels);
3067
3115
  const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
3068
- const ramp = Math.max(1e-3, falloff);
3116
+ const ramp2 = Math.max(1e-3, falloff);
3069
3117
  const out = new Uint8Array(n);
3070
3118
  const d = pixels.data;
3071
3119
  for (let i = 0; i < n; i++) {
@@ -3073,7 +3121,7 @@ function toneRange(pixels, range, falloff = 20) {
3073
3121
  if (d[p + 3] <= 0) continue;
3074
3122
  const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
3075
3123
  const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
3076
- out[i] = coverageToByte(1 - outside / ramp);
3124
+ out[i] = coverageToByte(1 - outside / ramp2);
3077
3125
  }
3078
3126
  return createSelectionMask(pixels.width, pixels.height, out);
3079
3127
  }
@@ -3102,6 +3150,111 @@ function hueRange(pixels, center) {
3102
3150
  }
3103
3151
  return createSelectionMask(pixels.width, pixels.height, out);
3104
3152
  }
3153
+ var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
3154
+ var COLOR_RANGE_PRESETS = [
3155
+ ...Object.keys(HUE_RANGE_PRESETS),
3156
+ "highlights",
3157
+ "midtones",
3158
+ "shadows",
3159
+ "skinTones"
3160
+ ];
3161
+ function isColorRangePreset(v) {
3162
+ return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
3163
+ }
3164
+ var ramp = (v, lo, hi, soft) => v < lo ? Math.max(0, 1 - (lo - v) / soft) : v > hi ? Math.max(0, 1 - (v - hi) / soft) : 1;
3165
+ function skinToneRange(pixels) {
3166
+ const n = checkPixels(pixels);
3167
+ const out = new Uint8Array(n);
3168
+ const d = pixels.data;
3169
+ for (let i = 0; i < n; i++) {
3170
+ const p = i * 4;
3171
+ if (d[p + 3] <= 0) continue;
3172
+ const r = d[p] / 255;
3173
+ const g = d[p + 1] / 255;
3174
+ const b = d[p + 2] / 255;
3175
+ const mx = Math.max(r, g, b);
3176
+ const mn = Math.min(r, g, b);
3177
+ const chroma = mx - mn;
3178
+ if (chroma <= 0 || mx <= 0) continue;
3179
+ let hue;
3180
+ if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
3181
+ else if (mx === g) hue = (b - r) / chroma + 2;
3182
+ else hue = (r - g) / chroma + 4;
3183
+ hue *= 60;
3184
+ if (hue > 180) hue -= 360;
3185
+ const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
3186
+ out[i] = coverageToByte(k);
3187
+ }
3188
+ return createSelectionMask(pixels.width, pixels.height, out);
3189
+ }
3190
+ function colorRangePreset(pixels, preset, options = {}) {
3191
+ if (preset === "skinTones") return skinToneRange(pixels);
3192
+ if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
3193
+ if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
3194
+ throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
3195
+ }
3196
+ function selectedColorBins(pixels, sel, tolerance) {
3197
+ const S = 64;
3198
+ let bins = new Uint8Array(S * S * S);
3199
+ const d = pixels.data;
3200
+ const n = pixels.width * pixels.height;
3201
+ for (let i = 0; i < n; i++) {
3202
+ if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
3203
+ bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
3204
+ }
3205
+ const r = Math.round(Math.max(0, tolerance) / 4);
3206
+ if (r === 0) return bins;
3207
+ for (const stride of [1, S, S * S]) {
3208
+ const out = new Uint8Array(bins.length);
3209
+ for (let i = 0; i < bins.length; i++) {
3210
+ if (!bins[i]) continue;
3211
+ const c = Math.floor(i / stride) % S;
3212
+ const lo = Math.max(0, c - r);
3213
+ const hi = Math.min(S - 1, c + r);
3214
+ const base = i - c * stride;
3215
+ for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
3216
+ }
3217
+ bins = out;
3218
+ }
3219
+ return bins;
3220
+ }
3221
+ function matcher(pixels, sel, options) {
3222
+ if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
3223
+ const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
3224
+ const d = pixels.data;
3225
+ return (i) => d[i * 4 + 3] > 0 && bins[((d[i * 4] >> 2 << 6) + (d[i * 4 + 1] >> 2)) * 64 + (d[i * 4 + 2] >> 2)] === 1;
3226
+ }
3227
+ function growSelection(pixels, sel, options = {}) {
3228
+ checkPixels(pixels);
3229
+ const match = matcher(pixels, sel, options);
3230
+ const { width: w, height: h } = pixels;
3231
+ const out = new Uint8Array(sel.data);
3232
+ const stack = [];
3233
+ for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
3234
+ while (stack.length) {
3235
+ const i = stack.pop();
3236
+ const x = i % w;
3237
+ const y = (i - x) / w;
3238
+ const visit = (j) => {
3239
+ if (out[j] < 255 && match(j)) {
3240
+ out[j] = 255;
3241
+ stack.push(j);
3242
+ }
3243
+ };
3244
+ if (x > 0) visit(i - 1);
3245
+ if (x < w - 1) visit(i + 1);
3246
+ if (y > 0) visit(i - w);
3247
+ if (y < h - 1) visit(i + w);
3248
+ }
3249
+ return createSelectionMask(w, h, out);
3250
+ }
3251
+ function selectSimilar(pixels, sel, options = {}) {
3252
+ const n = checkPixels(pixels);
3253
+ const match = matcher(pixels, sel, options);
3254
+ const out = new Uint8Array(sel.data);
3255
+ for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
3256
+ return createSelectionMask(pixels.width, pixels.height, out);
3257
+ }
3105
3258
 
3106
3259
  // src/image-editor/selection/segmentation.ts
3107
3260
  var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
@@ -3364,6 +3517,67 @@ function refineSelection(pixels, mask, params = {}) {
3364
3517
  return createSelectionMask(mask.width, mask.height, out);
3365
3518
  }
3366
3519
 
3520
+ // src/image-editor/selection/stroke.ts
3521
+ function selectionRing(sel, width, location) {
3522
+ const half = location === "center" ? width / 2 : width;
3523
+ const outer = location === "inside" ? sel : modifySelection(sel, "expand", half);
3524
+ const inner = location === "outside" ? sel : modifySelection(sel, "contract", half);
3525
+ const out = new Uint8Array(sel.data.length);
3526
+ for (let i = 0; i < out.length; i++) out[i] = Math.max(0, outer.data[i] - inner.data[i]);
3527
+ return createSelectionMask(sel.width, sel.height, out);
3528
+ }
3529
+
3530
+ // src/image-editor/selection/channels.ts
3531
+ var DEFAULT_CHANNEL_COLOR = "#ff0000";
3532
+ function describeChannel(channel) {
3533
+ const { mask, ...rest } = channel;
3534
+ let sum = 0;
3535
+ for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
3536
+ const percent = Math.round(sum / 255 / Math.max(1, mask.data.length) * 1e4) / 100;
3537
+ return { ...rest, bounds: mask.bounds, percent };
3538
+ }
3539
+ function serializeChannels(channels) {
3540
+ return channels.map((c) => ({ ...c, mask: serializeSelection(c.mask) }));
3541
+ }
3542
+ function deserializeChannels(value, size, problems = []) {
3543
+ if (!Array.isArray(value)) return [];
3544
+ const out = [];
3545
+ for (const raw of value) {
3546
+ const v = raw;
3547
+ try {
3548
+ const mask = deserializeSelection(v.mask);
3549
+ if (mask.width !== size.width || mask.height !== size.height) throw new Error("the channel is a different size from the canvas");
3550
+ out.push({
3551
+ id: typeof v.id === "string" && v.id ? v.id : `ch_${out.length + 1}`,
3552
+ name: typeof v.name === "string" && v.name.trim() ? v.name : `Alpha ${out.length + 1}`,
3553
+ mask,
3554
+ color: typeof v.color === "string" && /^#[0-9a-f]{6}$/i.test(v.color) ? v.color : DEFAULT_CHANNEL_COLOR,
3555
+ opacity: typeof v.opacity === "number" && v.opacity >= 0 && v.opacity <= 1 ? v.opacity : 0.5,
3556
+ indicates: v.indicates === "selected" ? "selected" : "masked",
3557
+ visible: v.visible === true
3558
+ });
3559
+ } catch (err) {
3560
+ problems.push(`Alpha channel "${String(v?.name ?? "?")}" could not be read: ${err instanceof Error ? err.message : String(err)}.`);
3561
+ }
3562
+ }
3563
+ return out;
3564
+ }
3565
+ function channelThumbnail(channel, maxSide = 64) {
3566
+ const { width: w, height: h, data } = channel.mask;
3567
+ const k = Math.min(1, maxSide / Math.max(w, h));
3568
+ const tw = Math.max(1, Math.round(w * k));
3569
+ const th = Math.max(1, Math.round(h * k));
3570
+ const out = new Uint8ClampedArray(tw * th * 4);
3571
+ for (let y = 0; y < th; y++) {
3572
+ const sy = Math.min(h - 1, Math.floor((y + 0.5) / k));
3573
+ for (let x = 0; x < tw; x++) {
3574
+ const v = data[sy * w + Math.min(w - 1, Math.floor((x + 0.5) / k))];
3575
+ out.set([v, v, v, 255], (y * tw + x) * 4);
3576
+ }
3577
+ }
3578
+ return { width: tw, height: th, data: out };
3579
+ }
3580
+
3367
3581
  // src/image-editor/effect-layers.ts
3368
3582
  function hasLayersBelow(layers, id) {
3369
3583
  const all = flattenLayers(layers);
@@ -4274,6 +4488,8 @@ var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
4274
4488
  var RETOUCH_TOOLS = ["clone", "heal", "spot-heal", "smudge", "dodge", "burn", "sponge", "blur", "sharpen", "history"];
4275
4489
  var isRetouchTool = (tool) => RETOUCH_TOOLS.includes(tool);
4276
4490
  var STROKE_TOOLS = ["brush", "eraser", "mixer", ...RETOUCH_TOOLS];
4491
+ var QUICK_MASK_TARGET = "quick-mask";
4492
+ var PASTE_MODES = ["new-layer", "in-place", "into", "outside"];
4277
4493
  function paintCoverageInto(base, coverage, apply, rects, depth = null) {
4278
4494
  const tiles = new Map(base.tiles);
4279
4495
  const done = /* @__PURE__ */ new Set();
@@ -4783,6 +4999,10 @@ var ImageEditorController = class {
4783
4999
  } catch {
4784
5000
  }
4785
5001
  }
5002
+ if (options.initialState?.channels) {
5003
+ const channels = deserializeChannels(options.initialState.channels, this.naturalSize, this.lastLoadProblems);
5004
+ if (channels.length) this.state.channels = channels;
5005
+ }
4786
5006
  this.initialSnapshot = this.captureHistoryEntry();
4787
5007
  if (typeof options.image === "string") {
4788
5008
  this.loadImage(options.image);
@@ -4974,8 +5194,11 @@ var ImageEditorController = class {
4974
5194
  }
4975
5195
  const filter = normalizeSmartFilter({ kind: "effect", effect: d.id, provider: this.effects.require().id, name: d.name, values, enabled: options.enabled !== false, blend: options.blend, opacity: options.opacity });
4976
5196
  const res = addSmartFilter(layers, id, filter, this.canvasSize, options.index);
4977
- this.commitLayers(res.layers);
4978
- return { layerId: id, filter: res.filter, converted };
5197
+ const masked = this.asOneStep(() => {
5198
+ this.commitLayers(res.layers);
5199
+ return this.maskFiltersWithSelection(id, options.useSelection);
5200
+ });
5201
+ return { layerId: id, filter: res.filter, converted, masked };
4979
5202
  }
4980
5203
  /**
4981
5204
  * Changes an effect smart filter: `values` merged over its own (`null` resets a control to the
@@ -5160,6 +5383,7 @@ var ImageEditorController = class {
5160
5383
  transform: { ...s.transform },
5161
5384
  outputOpacity: s.outputOpacity,
5162
5385
  selection: s.selection ?? null,
5386
+ ...s.channels?.length ? { channels: s.channels } : {},
5163
5387
  source: {
5164
5388
  sourceUrl: s.sourceUrl,
5165
5389
  element: this.imageElement,
@@ -5195,6 +5419,8 @@ var ImageEditorController = class {
5195
5419
  if (entry.colorMode) s.colorMode = entry.colorMode;
5196
5420
  else delete s.colorMode;
5197
5421
  s.selection = entry.selection;
5422
+ if (entry.channels?.length) s.channels = entry.channels;
5423
+ else delete s.channels;
5198
5424
  if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
5199
5425
  this.imageElement = entry.source.element;
5200
5426
  s.sourceUrl = entry.source.sourceUrl;
@@ -5204,8 +5430,38 @@ var ImageEditorController = class {
5204
5430
  s.selectedAnnotationId = null;
5205
5431
  }
5206
5432
  }
5433
+ /** Set while `asOneStep` runs: the step is already recorded, so nested commands add none. */
5434
+ historyGrouped = false;
5435
+ /**
5436
+ * Runs `fn` as one undo step: history is recorded once, before it, and the commands it calls
5437
+ * record none. If `fn` throws, the step is dropped and the state goes back to before it, so a
5438
+ * failure never leaves half a change.
5439
+ */
5440
+ asOneStep(fn) {
5441
+ if (this.historyGrouped || this.isApplyingHistory) return fn();
5442
+ this.recordHistory();
5443
+ const entry = this.undoStack[this.undoStack.length - 1];
5444
+ this.historyGrouped = true;
5445
+ try {
5446
+ return fn();
5447
+ } catch (err) {
5448
+ if (this.undoStack[this.undoStack.length - 1] === entry) {
5449
+ this.undoStack.pop();
5450
+ this.isApplyingHistory = true;
5451
+ try {
5452
+ this.restoreHistoryEntry(entry);
5453
+ } finally {
5454
+ this.isApplyingHistory = false;
5455
+ }
5456
+ this.notify();
5457
+ }
5458
+ throw err;
5459
+ } finally {
5460
+ this.historyGrouped = false;
5461
+ }
5462
+ }
5207
5463
  recordHistory() {
5208
- if (this.isApplyingHistory) return;
5464
+ if (this.isApplyingHistory || this.historyGrouped) return;
5209
5465
  this.docRevision++;
5210
5466
  this.undoStack.push(this.captureHistoryEntry());
5211
5467
  if (this.undoStack.length > this.maxHistoryDepth) {
@@ -5851,16 +6107,23 @@ var ImageEditorController = class {
5851
6107
  return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };
5852
6108
  }
5853
6109
  /**
5854
- * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black) or
5855
- * `selection` (the selection's coverage). Replaces an existing mask. Makes the mask the paint
6110
+ * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black),
6111
+ * `selection` (the selection's coverage), `hide-selection` (its inverse) or `transparency`
6112
+ * (the layer's own alpha, so the pixels can then be made opaque and edited). Replaces an existing mask. Makes the mask the paint
5856
6113
  * target when the layer is active.
5857
6114
  */
5858
6115
  addLayerMask(id, from = "reveal-all", props = {}) {
5859
6116
  const layer = requireLayer(this.state.layers, id).layer;
5860
6117
  let mask;
5861
- if (from === "selection") {
5862
- const sel = this.state.selection;
5863
- if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, or use reveal-all / hide-all.");
6118
+ if (from === "selection" || from === "hide-selection" || from === "transparency") {
6119
+ let sel;
6120
+ if (from === "transparency") {
6121
+ sel = this.layerTransparency(id);
6122
+ } else {
6123
+ const current = this.state.selection;
6124
+ if (!current?.bounds) throw new LayerOperationError("Nothing is selected; select first, or use reveal-all / hide-all / transparency.");
6125
+ sel = from === "hide-selection" ? invertSelection(current) : current;
6126
+ }
5864
6127
  mask = this.maskFromSelection(sel, props);
5865
6128
  if (layer.type === "pixel" && (layer.offset.x || layer.offset.y)) {
5866
6129
  mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };
@@ -6314,10 +6577,22 @@ var ImageEditorController = class {
6314
6577
  return res.layer.id;
6315
6578
  }
6316
6579
  /** Adds a smart filter (`develop` or `blur`, with `blend` and `opacity`) at `index` (default the top), as one undo step. */
6317
- addSmartFilter(id, filter, index) {
6580
+ addSmartFilter(id, filter, index, options = {}) {
6318
6581
  const res = addSmartFilter(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);
6319
- this.commitLayers(res.layers);
6320
- return res.filter;
6582
+ return this.asOneStep(() => {
6583
+ this.commitLayers(res.layers);
6584
+ this.maskFiltersWithSelection(id, options.useSelection);
6585
+ return res.filter;
6586
+ });
6587
+ }
6588
+ /**
6589
+ * After adding a smart filter: with `useSelection` and a selection, sets the filter mask from
6590
+ * it in the same undo step (Photoshop limits a filter to the selection). Returns whether it did.
6591
+ */
6592
+ maskFiltersWithSelection(id, useSelection) {
6593
+ if (!useSelection || !this.state.selection?.bounds) return false;
6594
+ this.setSmartFilterMask(id, "selection", {}, false);
6595
+ return true;
6321
6596
  }
6322
6597
  /** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */
6323
6598
  updateSmartFilter(id, filterId, changes, commitToHistory = true) {
@@ -6857,6 +7132,12 @@ var ImageEditorController = class {
6857
7132
  }
6858
7133
  /** Resolves and validates what a stroke, fill or clear paints into. */
6859
7134
  paintTarget(layerId, channel, action) {
7135
+ if (channel === "quick-mask" || channel === void 0 && layerId === void 0 && this.state.quickMaskEditing) {
7136
+ if (!this.state.quickMaskEditing) {
7137
+ throw new LayerOperationError(`Quick Mask is off; turn it on (set_quick_mask) to ${action} on the selection, or target a layer.`, `Turn on Quick Mask (Q) to ${action} on the selection.`);
7138
+ }
7139
+ return { id: QUICK_MASK_TARGET, layer: null, channel: "quick-mask", offset: { x: 0, y: 0 }, surface: this.quickMaskSurface(), lockTransparency: false };
7140
+ }
6860
7141
  const id = layerId ?? this.state.activeLayerId;
6861
7142
  if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`, `Pick a layer to ${action} on in the Layers panel.`);
6862
7143
  const layer = requireLayer(this.state.layers, id).layer;
@@ -6925,10 +7206,11 @@ var ImageEditorController = class {
6925
7206
  opacity: brush.opacity,
6926
7207
  blend: target.channel !== "pixels" ? "normal" : brush.blend,
6927
7208
  lockTransparency: target.lockTransparency,
6928
- selection: this.state.selection?.bounds ? this.state.selection : null,
7209
+ // In Quick Mask mode the selection is what is painted, so it does not limit the stroke.
7210
+ selection: target.channel !== "quick-mask" && this.state.selection?.bounds ? this.state.selection : null,
6929
7211
  offset: target.offset
6930
7212
  };
6931
- const depth = target.channel !== "filter-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
7213
+ const depth = target.channel !== "filter-mask" && target.channel !== "quick-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
6932
7214
  if (tool === "mixer") return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply, depth);
6933
7215
  if (isRetouchTool(tool)) return new PaintStrokeSession(this, target, this.retouchStroke(tool, init, brush, target, apply), apply, depth);
6934
7216
  const gradient = tool === "brush" && strokePaint?.type === "gradient" ? resolvePaintGradient(strokePaint, { paint: toHex(color), secondary: toHex(background) }) : null;
@@ -7093,12 +7375,19 @@ var ImageEditorController = class {
7093
7375
  }
7094
7376
  /** @internal Shows a stroke's live surface (no history). */
7095
7377
  _strokeUpdate(layerId, channel, surface, rects) {
7378
+ if (channel === "quick-mask") {
7379
+ this.state.selection = this.selectionFromSurface(surface, this.state.selection ?? null, rects);
7380
+ this.notify();
7381
+ return;
7382
+ }
7096
7383
  this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
7097
7384
  this.notify(channel === "filter-mask" ? void 0 : rects);
7098
7385
  }
7099
7386
  /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */
7100
7387
  _strokeCommit(before, layerId, channel, surface) {
7101
- if (surface) {
7388
+ if (channel === "quick-mask") {
7389
+ this.state.selection = surface ? this.selectionFromSurface(surface) : before.selection;
7390
+ } else if (surface) {
7102
7391
  const prev = before.layers;
7103
7392
  this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
7104
7393
  if (this.bitDepth !== 8) this.state.layers = keepDeepLayers(prev, this.state.layers);
@@ -7145,13 +7434,177 @@ var ImageEditorController = class {
7145
7434
  clearPixels(options = {}) {
7146
7435
  return this.fillOrClear(options, "erase");
7147
7436
  }
7148
- fillOrClear(options, mode) {
7149
- const target = this.paintTarget(options.layerId, options.channel, mode === "paint" ? "fill" : "clear");
7437
+ /**
7438
+ * Edit › Stroke: paints a band `width` px wide along the selection's edge, `inside` it, on the
7439
+ * `center` of the edge or `outside` it (default), with `color` (default the foreground),
7440
+ * `opacity` and `blend`, on a layer's pixels or mask. One undo step. Returns the changed bounds.
7441
+ */
7442
+ strokeSelection(options) {
7443
+ const sel = this.state.selection;
7444
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then stroke the selection.");
7445
+ const width = options.width;
7446
+ if (!(width > 0 && width <= 250)) throw new LayerOperationError("width must be greater than 0 and at most 250 px.");
7447
+ const location = options.location ?? "outside";
7448
+ if (location !== "inside" && location !== "center" && location !== "outside") {
7449
+ throw new LayerOperationError(`Unknown location "${String(location)}"; use inside, center or outside.`);
7450
+ }
7451
+ const ring = selectionRing(sel, width, location);
7452
+ if (!ring.bounds) return { layerId: options.layerId ?? this.state.activeLayerId ?? "", channel: "pixels", bounds: null, dabs: 0 };
7453
+ return this.fillOrClear(options, "paint", ring);
7454
+ }
7455
+ /**
7456
+ * Image › Crop to the selection's bounds (one undo step). Returns the crop box (natural
7457
+ * pixels). Throws without a selection.
7458
+ */
7459
+ cropToSelection() {
7460
+ const sel = this.state.selection;
7461
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select the area to keep, then crop to it.");
7462
+ const crop = { ...sel.bounds };
7463
+ this.setCrop(crop);
7464
+ return crop;
7465
+ }
7466
+ /**
7467
+ * Edit › Content-Aware Fill: fills the selection with texture synthesised from around it
7468
+ * (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
7469
+ * coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
7470
+ * above it. Samples within `sampleRadius` px of the selection (default its larger side, at
7471
+ * least 32). Deterministic for a `seed`. One undo step. 8-bit documents only for now.
7472
+ */
7473
+ contentAwareFill(options = {}) {
7474
+ const sel = this.state.selection;
7475
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
7476
+ 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.");
7477
+ const output = options.output ?? "current";
7478
+ if (output !== "current" && output !== "new-layer") throw new LayerOperationError(`Unknown output "${String(output)}"; use current or new-layer.`);
7479
+ const target = this.paintTarget(options.layerId, "pixels", "fill");
7480
+ const { width, height } = this.naturalSize;
7481
+ const b = sel.bounds;
7482
+ const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
7483
+ const x0 = Math.max(0, b.x - margin);
7484
+ const y0 = Math.max(0, b.y - margin);
7485
+ 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 };
7486
+ 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.");
7487
+ const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
7488
+ const before = readRegion(target.surface, layerRect);
7489
+ const img = new Float32Array(before.length);
7490
+ for (let i = 0; i < before.length; i++) img[i] = before[i];
7491
+ const coverage = new Float32Array(win.width * win.height);
7492
+ const hole = new Uint8Array(win.width * win.height);
7493
+ for (let y = 0; y < win.height; y++) {
7494
+ for (let x = 0; x < win.width; x++) {
7495
+ const c = sel.data[(win.y + y) * sel.width + win.x + x] / 255;
7496
+ coverage[y * win.width + x] = c;
7497
+ hole[y * win.width + x] = c > 0 ? 1 : 0;
7498
+ }
7499
+ }
7500
+ const filled = contentAwareFill(win.width, win.height, 4, img, hole, { seed: options.seed ?? 0 });
7501
+ const out = new Uint8ClampedArray(before.length);
7502
+ for (let i = 0; i < coverage.length; i++) {
7503
+ const k = coverage[i];
7504
+ for (let c = 0; c < 4; c++) {
7505
+ const p = i * 4 + c;
7506
+ out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
7507
+ }
7508
+ }
7509
+ if (output === "new-layer") {
7510
+ const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
7511
+ const data = new Uint8ClampedArray(b.width * b.height * 4);
7512
+ for (let y = 0; y < b.height; y++) data.set(out.subarray(((crop.y + y) * win.width + crop.x) * 4, ((crop.y + y) * win.width + crop.x + b.width) * 4), y * b.width * 4);
7513
+ 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 });
7514
+ return { layerId: id, bounds: { ...b }, output };
7515
+ }
7516
+ const surface = writeRegion(target.surface, layerRect, out);
7517
+ this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
7518
+ return { layerId: target.id, bounds: { ...b }, output };
7519
+ }
7520
+ /** The pixel clipboard (Copy / Cut), or null. */
7521
+ clipboard = null;
7522
+ /** What Copy or Cut last put on the editor's clipboard, or null. */
7523
+ getClipboard() {
7524
+ return this.clipboard;
7525
+ }
7526
+ /** Puts pixels on the editor's clipboard (e.g. an image pasted from the system clipboard). */
7527
+ setClipboard(image) {
7528
+ if (image && (image.data.length !== image.width * image.height * 4 || image.width < 1 || image.height < 1)) {
7529
+ throw new LayerOperationError("Clipboard pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
7530
+ }
7531
+ this.clipboard = image;
7532
+ }
7533
+ /**
7534
+ * Edit › Copy (or Copy Merged with `merged`): the selected pixels of a pixel layer (default the
7535
+ * active one) or of the whole visible image, cropped to the selection's bounds, with the
7536
+ * selection's soft edge in their alpha. Without a selection, the whole canvas. Not an undo step.
7537
+ */
7538
+ copyPixels(options = {}) {
7539
+ const { width, height } = this.naturalSize;
7540
+ const sel = this.state.selection?.bounds ? this.state.selection : null;
7541
+ const rect = sel?.bounds ?? { x: 0, y: 0, width, height };
7542
+ let data;
7543
+ if (options.merged) {
7544
+ data = new Uint8ClampedArray(this.compositePixels(rect).data);
7545
+ } else {
7546
+ const id = options.layerId ?? this.state.activeLayerId;
7547
+ if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
7548
+ const layer = requireLayer(this.state.layers, id).layer;
7549
+ if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is not a pixel layer; copy merged, or rasterize it first.`);
7550
+ data = readRegion(this.surfaceOf(layer), { x: rect.x - layer.offset.x, y: rect.y - layer.offset.y, width: rect.width, height: rect.height });
7551
+ }
7552
+ if (sel) {
7553
+ for (let y = 0; y < rect.height; y++) {
7554
+ for (let x = 0; x < rect.width; x++) {
7555
+ const k = sel.data[(rect.y + y) * sel.width + rect.x + x];
7556
+ const p = (y * rect.width + x) * 4 + 3;
7557
+ data[p] = Math.round(data[p] * k / 255);
7558
+ }
7559
+ }
7560
+ }
7561
+ this.clipboard = { width: rect.width, height: rect.height, x: rect.x, y: rect.y, data };
7562
+ return this.clipboard;
7563
+ }
7564
+ /** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
7565
+ cutPixels(options = {}) {
7566
+ return this.asOneStep(() => {
7567
+ const clip = this.copyPixels({ layerId: options.layerId });
7568
+ this.clearPixels({ layerId: options.layerId, channel: "pixels" });
7569
+ return clip;
7570
+ });
7571
+ }
7572
+ /**
7573
+ * Edit › Paste as a new pixel layer above the active one (one undo step): `new-layer` centred
7574
+ * on the selection (or the canvas), `in-place` where it was copied, `into` centred in the
7575
+ * selection with the selection as its mask (Paste Into), or `outside` with the inverse
7576
+ * (Paste Outside). Returns the new layer id and where it went.
7577
+ */
7578
+ pastePixels(mode = "new-layer", image = this.clipboard) {
7579
+ if (!image) throw new LayerOperationError("The clipboard is empty; copy_pixels first.");
7580
+ if (!PASTE_MODES.includes(mode)) throw new LayerOperationError(`Unknown paste mode "${String(mode)}"; use ${PASTE_MODES.join(", ")}.`);
7581
+ const sel = this.state.selection?.bounds ? this.state.selection : null;
7582
+ if ((mode === "into" || mode === "outside") && !sel) throw new LayerOperationError(`Paste ${mode === "into" ? "Into" : "Outside"} needs a selection; select where it goes first.`);
7583
+ const { width, height } = this.naturalSize;
7584
+ let x;
7585
+ let y;
7586
+ if (mode === "in-place" && image.x !== void 0 && image.y !== void 0) {
7587
+ x = image.x;
7588
+ y = image.y;
7589
+ } else {
7590
+ const frame = sel?.bounds ?? { x: 0, y: 0, width, height };
7591
+ x = Math.round(frame.x + (frame.width - image.width) / 2);
7592
+ y = Math.round(frame.y + (frame.height - image.height) / 2);
7593
+ }
7594
+ return this.asOneStep(() => {
7595
+ const id = this.addPixelLayer({ name: "Pasted", pixels: { width: image.width, height: image.height, data: new Uint8ClampedArray(image.data) }, x, y });
7596
+ if (mode === "into" || mode === "outside") this.addLayerMask(id, mode === "into" ? "selection" : "hide-selection");
7597
+ return { layerId: id, bounds: { x, y, width: image.width, height: image.height }, masked: mode === "into" || mode === "outside" };
7598
+ });
7599
+ }
7600
+ fillOrClear(options, mode, coverageMask) {
7601
+ const target = this.paintTarget(options.layerId, options.channel, mode === "paint" ? coverageMask ? "stroke" : "fill" : "clear");
7150
7602
  const { width, height } = this.naturalSize;
7151
7603
  const opacity = options.opacity ?? 1;
7152
7604
  if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
7153
7605
  const blend = parseBlend(options.blend) ?? "normal";
7154
- const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;
7606
+ const quickMask = target.channel === "quick-mask";
7607
+ const sel = coverageMask ?? (!quickMask && options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null);
7155
7608
  const region = sel?.bounds ?? { x: 0, y: 0, width, height };
7156
7609
  const coverage = new CoverageMap();
7157
7610
  const ones = new Float32Array(region.width * region.height);
@@ -7177,8 +7630,14 @@ var ImageEditorController = class {
7177
7630
  selection: null,
7178
7631
  offset: target.offset
7179
7632
  };
7180
- const depth = target.channel !== "filter-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
7633
+ const depth = target.channel !== "filter-mask" && target.channel !== "quick-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
7181
7634
  const surface = paintCoverageInto(target.surface, coverage, apply, [region], depth);
7635
+ if (quickMask) {
7636
+ this.recordHistory();
7637
+ this.state.selection = this.selectionFromSurface(surface);
7638
+ this.notify();
7639
+ return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };
7640
+ }
7182
7641
  const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));
7183
7642
  this.commitLayers(next);
7184
7643
  return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };
@@ -7325,7 +7784,7 @@ var ImageEditorController = class {
7325
7784
  enabled: spec.enabled,
7326
7785
  opacity: spec.opacity,
7327
7786
  blend: spec.blend,
7328
- mask: spec.mask ?? void 0
7787
+ mask: spec.mask ?? (spec.useSelection && this.state.selection?.bounds ? layerMaskFromSelection(this.state.selection) : void 0)
7329
7788
  });
7330
7789
  while (ids.has(layer.id)) layer.id = newLayerId("adj");
7331
7790
  const { width, height } = this.naturalSize;
@@ -7535,6 +7994,7 @@ var ImageEditorController = class {
7535
7994
  const prev = normalizeSelection(this.state.selection ?? null);
7536
7995
  if (prev === next || prev && next && sameSelection(prev, next)) return false;
7537
7996
  this.recordHistory();
7997
+ if (!next && prev?.bounds) this.lastSelection = prev;
7538
7998
  this.state.selection = next;
7539
7999
  this.notify();
7540
8000
  return true;
@@ -7548,6 +8008,74 @@ var ImageEditorController = class {
7548
8008
  deselect() {
7549
8009
  return this.setSelection(null);
7550
8010
  }
8011
+ /** The selection a Deselect cleared, for `reselect` (not saved, not part of history). */
8012
+ lastSelection = null;
8013
+ /** Whether `reselect` has a selection to bring back. */
8014
+ canReselect() {
8015
+ const { width, height } = this.naturalSize;
8016
+ return Boolean(this.lastSelection && this.lastSelection.width === width && this.lastSelection.height === height);
8017
+ }
8018
+ /** Select › Reselect: brings back the last selection a Deselect cleared, as one undo step. */
8019
+ reselect() {
8020
+ if (!this.canReselect()) return false;
8021
+ return this.setSelection(this.lastSelection);
8022
+ }
8023
+ /**
8024
+ * Single Row / Single Column Marquee: a 1 px line across the whole image at `position`
8025
+ * (natural pixels; y for a row, x for a column).
8026
+ */
8027
+ selectLine(orientation, position, options = {}) {
8028
+ const { width, height } = this.naturalSize;
8029
+ const max = orientation === "row" ? height : width;
8030
+ const at = Math.floor(position);
8031
+ if (!Number.isFinite(position) || at < 0 || at >= max) {
8032
+ throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
8033
+ }
8034
+ const rect = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
8035
+ return this.shapeCommand(rectSelection(width, height, rect, { antiAlias: false }), options);
8036
+ }
8037
+ /** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
8038
+ async growSelection(options = {}) {
8039
+ const sel = this.state.selection;
8040
+ if (!sel?.bounds) return false;
8041
+ const pixels = await this.requirePixels(options);
8042
+ return this.setSelection(growSelection(pixels, sel, options));
8043
+ }
8044
+ /** Select › Similar: adds every pixel within `tolerance` of a selected colour (one undo step). */
8045
+ async selectSimilar(options = {}) {
8046
+ const sel = this.state.selection;
8047
+ if (!sel?.bounds) return false;
8048
+ const pixels = await this.requirePixels(options);
8049
+ return this.setSelection(selectSimilar(pixels, sel, options));
8050
+ }
8051
+ /**
8052
+ * Load Selection from a layer's transparency (Ctrl/⌘-click its thumbnail): its alpha in
8053
+ * document space, combined by `mode`. Works on pixel, shape, type and smart object layers.
8054
+ */
8055
+ selectLayerTransparency(id, options = {}) {
8056
+ const mask = this.layerTransparency(id);
8057
+ return this.shapeCommand(mask, options);
8058
+ }
8059
+ layerTransparency(id) {
8060
+ const layer = requireLayer(this.state.layers, id).layer;
8061
+ if (!hasSurface(layer)) {
8062
+ throw new LayerOperationError(`Layer "${layer.name || id}" has no pixels of its own; only pixel, shape, type and smart object layers do.`);
8063
+ }
8064
+ const { width, height } = this.naturalSize;
8065
+ const out = new Uint8Array(width * height);
8066
+ const pixels = this.getLayerPixels(id);
8067
+ if (!pixels) return createSelectionMask(width, height, out);
8068
+ for (let y = 0; y < pixels.height; y++) {
8069
+ const dy = y + pixels.y;
8070
+ if (dy < 0 || dy >= height) continue;
8071
+ for (let x = 0; x < pixels.width; x++) {
8072
+ const dx = x + pixels.x;
8073
+ if (dx < 0 || dx >= width) continue;
8074
+ out[dy * width + dx] = pixels.data[(y * pixels.width + x) * 4 + 3];
8075
+ }
8076
+ }
8077
+ return createSelectionMask(width, height, out);
8078
+ }
7551
8079
  /** Select › Inverse (with nothing selected, selects everything). */
7552
8080
  invertSelection() {
7553
8081
  const { width, height } = this.naturalSize;
@@ -7555,7 +8083,8 @@ var ImageEditorController = class {
7555
8083
  return this.setSelection(invertSelection(current));
7556
8084
  }
7557
8085
  shapeCommand(mask, options) {
7558
- const shaped = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
8086
+ const feathered = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
8087
+ const shaped = options.invert ? invertSelection(feathered) : feathered;
7559
8088
  return this.setSelection(shaped, options.mode ?? "replace");
7560
8089
  }
7561
8090
  /** Rectangular marquee in natural image pixels. */
@@ -7657,6 +8186,15 @@ var ImageEditorController = class {
7657
8186
  this.shapeCommand(colorRange(pixels, samples, options), options);
7658
8187
  return this.getSelection();
7659
8188
  }
8189
+ /**
8190
+ * Select › Color Range with a preset: `reds` … `magentas`, `highlights` / `midtones` /
8191
+ * `shadows` (with `falloff` levels, default 20) or `skinTones`. One undo step.
8192
+ */
8193
+ async selectColorRangePreset(preset, options = {}) {
8194
+ const pixels = await this.requirePixels(options);
8195
+ this.shapeCommand(colorRangePreset(pixels, preset, options), options);
8196
+ return this.getSelection();
8197
+ }
7660
8198
  /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
7661
8199
  modifySelection(op, radius) {
7662
8200
  const sel = this.state.selection;
@@ -7733,10 +8271,175 @@ var ImageEditorController = class {
7733
8271
  }
7734
8272
  /** Quick Mask view (tints the unselected area). View state: no undo step. */
7735
8273
  setQuickMask(on) {
7736
- if (Boolean(this.state.quickMask) === on) return;
7737
- this.state.quickMask = on;
8274
+ this.setSelectionDisplay(on ? "quickMask" : "ants");
8275
+ }
8276
+ /** How the canvas shows the selection (`state.selectionDisplay`). */
8277
+ getSelectionDisplay() {
8278
+ return this.state.selectionDisplay ?? (this.state.quickMask ? "quickMask" : "ants");
8279
+ }
8280
+ /**
8281
+ * Shows the selection as marching ants, as a Quick Mask tint over the unselected area, or hides
8282
+ * it (the selection still applies). View state: no undo step, not saved.
8283
+ */
8284
+ setSelectionDisplay(display) {
8285
+ if (display !== "ants" && display !== "quickMask" && display !== "none") {
8286
+ throw new Error(`Unknown selection display "${String(display)}". Use ants, quickMask or none.`);
8287
+ }
8288
+ if (this.getSelectionDisplay() === display) return;
8289
+ if (display !== "quickMask") delete this.state.quickMaskEditing;
8290
+ this.state.selectionDisplay = display;
8291
+ this.state.quickMask = display === "quickMask";
8292
+ this.notify();
8293
+ }
8294
+ // --- Alpha channels ---
8295
+ /** The alpha channels (saved selections), in panel order. */
8296
+ getChannels() {
8297
+ return this.state.channels ?? [];
8298
+ }
8299
+ getChannel(id) {
8300
+ return this.state.channels?.find((c) => c.id === id) ?? null;
8301
+ }
8302
+ requireChannel(id) {
8303
+ const channel = this.getChannel(id);
8304
+ if (!channel) {
8305
+ const ids = this.getChannels().map((c) => `${c.id} (${c.name})`);
8306
+ throw new LayerOperationError(`There is no alpha channel "${id}"; the channels are ${ids.join(", ") || "none (save a selection first)"}.`);
8307
+ }
8308
+ return channel;
8309
+ }
8310
+ setChannels(next, commitToHistory = true) {
8311
+ if (commitToHistory) this.recordHistory();
8312
+ if (next.length) this.state.channels = next;
8313
+ else delete this.state.channels;
8314
+ this.notify();
8315
+ }
8316
+ newChannelId() {
8317
+ const ids = new Set(this.getChannels().map((c) => c.id));
8318
+ let n = this.getChannels().length + 1;
8319
+ while (ids.has(`ch_${n}`)) n++;
8320
+ return `ch_${n}`;
8321
+ }
8322
+ /**
8323
+ * Select › Save Selection: a new alpha channel (`name`, default "Alpha n") holding the
8324
+ * selection, or with `channelId` the selection combined into that channel by `mode` (replace,
8325
+ * add, subtract, intersect). One undo step. Throws without a selection.
8326
+ */
8327
+ saveSelection(options = {}) {
8328
+ const sel = this.state.selection;
8329
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then save the selection to a channel.");
8330
+ const { width, height } = this.naturalSize;
8331
+ const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
8332
+ const channels = [...this.getChannels()];
8333
+ if (options.channelId !== void 0) {
8334
+ const existing = this.requireChannel(options.channelId);
8335
+ const mask = combineSelections(existing.mask.bounds ? existing.mask : null, sized, options.mode ?? "replace") ?? createSelectionMask(width, height);
8336
+ const next = { ...existing, mask, ...options.name?.trim() ? { name: options.name.trim() } : {} };
8337
+ this.setChannels(channels.map((c) => c.id === existing.id ? next : c));
8338
+ return next;
8339
+ }
8340
+ const channel = {
8341
+ id: this.newChannelId(),
8342
+ name: options.name?.trim() || `Alpha ${channels.length + 1}`,
8343
+ mask: sized,
8344
+ color: DEFAULT_CHANNEL_COLOR,
8345
+ opacity: 0.5,
8346
+ indicates: "masked",
8347
+ visible: false
8348
+ };
8349
+ this.setChannels([...channels, channel]);
8350
+ return channel;
8351
+ }
8352
+ /**
8353
+ * Select › Load Selection from an alpha channel (inverted with `invert`), combined with the
8354
+ * selection by `mode`. One undo step.
8355
+ */
8356
+ loadSelectionFromChannel(id, options = {}) {
8357
+ const channel = this.requireChannel(id);
8358
+ const mask = options.invert ? invertSelection(channel.mask) : channel.mask;
8359
+ return this.setSelection(mask.bounds ? mask : null, options.mode ?? "replace");
8360
+ }
8361
+ /** Changes a channel's name, overlay colour, opacity, what it indicates or visibility (one undo step). */
8362
+ setChannelProperties(id, props) {
8363
+ const channel = this.requireChannel(id);
8364
+ if (props.name !== void 0 && !props.name.trim()) throw new LayerOperationError("A channel name cannot be empty.");
8365
+ if (props.color !== void 0 && !/^#[0-9a-f]{6}$/i.test(props.color)) throw new LayerOperationError("color must be #rrggbb.");
8366
+ if (props.opacity !== void 0 && !(props.opacity >= 0 && props.opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
8367
+ if (props.indicates !== void 0 && props.indicates !== "masked" && props.indicates !== "selected") throw new LayerOperationError("indicates must be masked or selected.");
8368
+ const next = {
8369
+ ...channel,
8370
+ ...props.name !== void 0 ? { name: props.name.trim() } : {},
8371
+ ...props.color !== void 0 ? { color: props.color.toLowerCase() } : {},
8372
+ ...props.opacity !== void 0 ? { opacity: props.opacity } : {},
8373
+ ...props.indicates !== void 0 ? { indicates: props.indicates } : {},
8374
+ ...props.visible !== void 0 ? { visible: props.visible } : {}
8375
+ };
8376
+ this.setChannels(this.getChannels().map((c) => c.id === id ? next : c));
8377
+ return next;
8378
+ }
8379
+ /** Deletes an alpha channel (one undo step). */
8380
+ deleteChannel(id) {
8381
+ this.requireChannel(id);
8382
+ this.setChannels(this.getChannels().filter((c) => c.id !== id));
8383
+ }
8384
+ /** Duplicates an alpha channel just after it (one undo step). Returns the copy. */
8385
+ duplicateChannel(id, name) {
8386
+ const channel = this.requireChannel(id);
8387
+ const copy = { ...channel, id: this.newChannelId(), name: name?.trim() || `${channel.name} copy` };
8388
+ const channels = [...this.getChannels()];
8389
+ channels.splice(channels.findIndex((c) => c.id === id) + 1, 0, copy);
8390
+ this.setChannels(channels);
8391
+ return copy;
8392
+ }
8393
+ /** Channel summaries (no pixels) for panels and agents. */
8394
+ describeChannels() {
8395
+ return this.getChannels().map(describeChannel);
8396
+ }
8397
+ // --- Quick Mask mode ---
8398
+ /** Whether Quick Mask mode is on (painting edits the selection). */
8399
+ get quickMaskEditing() {
8400
+ return Boolean(this.state.quickMaskEditing);
8401
+ }
8402
+ /**
8403
+ * Quick Mask mode (Q): on, the selection shows as a tint and painting, fills and strokes that
8404
+ * name no layer edit the selection itself (white selects, black deselects, grey partly);
8405
+ * off, it shows as marching ants again. View state: no undo step (the edits are).
8406
+ */
8407
+ setQuickMaskMode(on) {
8408
+ if (this.quickMaskEditing === on) return;
8409
+ if (on) this.state.quickMaskEditing = true;
8410
+ else delete this.state.quickMaskEditing;
8411
+ this.setSelectionDisplay(on ? "quickMask" : "ants");
7738
8412
  this.notify();
7739
8413
  }
8414
+ /** The selection as a 1-channel surface to paint in Quick Mask mode. */
8415
+ quickMaskSurface() {
8416
+ const { width, height } = this.naturalSize;
8417
+ const sel = this.state.selection;
8418
+ if (!sel?.bounds) return emptySurface(1, 0);
8419
+ const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
8420
+ return surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);
8421
+ }
8422
+ /** The selection a painted Quick Mask surface stands for (`rects` changed since `base`). */
8423
+ selectionFromSurface(surface, base = null, rects) {
8424
+ const { width, height } = this.naturalSize;
8425
+ let data;
8426
+ if (base && rects && base.width === width && base.height === height) {
8427
+ data = new Uint8Array(base.data);
8428
+ for (const r of rects) {
8429
+ const x0 = Math.max(0, Math.floor(r.x));
8430
+ const y0 = Math.max(0, Math.floor(r.y));
8431
+ const x1 = Math.min(width, Math.ceil(r.x + r.width));
8432
+ const y1 = Math.min(height, Math.ceil(r.y + r.height));
8433
+ if (x1 <= x0 || y1 <= y0) continue;
8434
+ const part = readRegion(surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
8435
+ for (let y = y0; y < y1; y++) data.set(part.subarray((y - y0) * (x1 - x0), (y - y0 + 1) * (x1 - x0)), y * width + x0);
8436
+ }
8437
+ } else {
8438
+ const all = readRegion(surface, { x: 0, y: 0, width, height });
8439
+ data = new Uint8Array(all.buffer, all.byteOffset, all.length);
8440
+ }
8441
+ return normalizeSelection(createSelectionMask(width, height, data));
8442
+ }
7740
8443
  // --- Layer masks ---
7741
8444
  maskFromSelection(sel, props = {}) {
7742
8445
  const { width, height } = this.naturalSize;
@@ -7778,8 +8481,11 @@ var ImageEditorController = class {
7778
8481
  if (!layer || layer.type !== "adjustment" || !layer.mask) return false;
7779
8482
  return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);
7780
8483
  }
7781
- /** Load Selection from a layer mask (its pixels; density and feather are not applied). */
7782
- selectionFromLayerMask(layerId, mode = "replace") {
8484
+ /**
8485
+ * Load Selection from a layer mask (its pixels; density and feather are not applied), inverted
8486
+ * with `invert`. False without a painted mask (a depth mask is computed, not painted).
8487
+ */
8488
+ selectionFromLayerMask(layerId, mode = "replace", invert2 = false) {
7783
8489
  const layer = this.getLayer(layerId);
7784
8490
  if (!layer?.mask || layer.mask.source) return false;
7785
8491
  const { width, height } = this.naturalSize;
@@ -7787,7 +8493,7 @@ var ImageEditorController = class {
7787
8493
  const oy = layer.type === "pixel" ? layer.offset.y : 0;
7788
8494
  const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });
7789
8495
  const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));
7790
- return this.setSelection(sel, mode);
8496
+ return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
7791
8497
  }
7792
8498
  // --- Histogram ---
7793
8499
  /**
@@ -8847,6 +9553,17 @@ var ImageEditorController = class {
8847
9553
  s.outputOpacity = 1;
8848
9554
  delete s.legacyOutputBlur;
8849
9555
  s.selection = null;
9556
+ const psdChannels = (result.alphaChannels ?? []).map((c, k) => ({
9557
+ id: `ch_${k + 1}`,
9558
+ name: c.name,
9559
+ mask: createSelectionMask(result.width, result.height, c.data),
9560
+ color: c.color,
9561
+ opacity: c.opacity,
9562
+ indicates: c.indicates,
9563
+ visible: false
9564
+ }));
9565
+ if (psdChannels.length) s.channels = psdChannels;
9566
+ else delete s.channels;
8850
9567
  s.selectedAnnotationId = null;
8851
9568
  s.activeLayerId = defaultActiveLayerId(s.layers);
8852
9569
  s.activeTarget = "pixels";
@@ -9037,7 +9754,14 @@ var ImageEditorController = class {
9037
9754
  if (color.colorMode && this.bitDepth === 32) warnings.push(`${color.colorMode.toUpperCase()} files are written at 16 bits per channel at most; the 32-bit samples were converted (colour above 1 clipped).`);
9038
9755
  const depth = color.colorMode && this.bitDepth === 32 ? 16 : this.bitDepth;
9039
9756
  const mergedDeep = depth === 8 ? void 0 : workingToSamples(compositeDeep(prepared, canvas, sources, depth), depth);
9040
- const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, ...color });
9757
+ const alphaChannels = this.getChannels().map((c) => ({
9758
+ name: c.name,
9759
+ data: c.mask.width === width && c.mask.height === height ? c.mask.data : resizeSelection(c.mask, width, height).data,
9760
+ color: c.color,
9761
+ opacity: c.opacity,
9762
+ indicates: c.indicates
9763
+ }));
9764
+ const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, ...color });
9041
9765
  warnings.push(...result.warnings);
9042
9766
  return { bytes: result.bytes, warnings, psb: result.psb, width, height };
9043
9767
  }
@@ -9110,6 +9834,7 @@ var ImageEditorController = class {
9110
9834
  try {
9111
9835
  out = this.serializeState();
9112
9836
  if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
9837
+ if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
9113
9838
  } finally {
9114
9839
  if (root) {
9115
9840
  this.state = current;
@@ -9150,6 +9875,11 @@ var ImageEditorController = class {
9150
9875
  }
9151
9876
  this.state.selection = selection;
9152
9877
  this.dropMismatchedSelection();
9878
+ const problems = [];
9879
+ const channels = deserializeChannels(data.channels, this.naturalSize, problems);
9880
+ if (channels.length) this.state.channels = channels;
9881
+ else delete this.state.channels;
9882
+ if (problems.length) this.lastLoadProblems = [...this.lastLoadProblems, ...problems];
9153
9883
  this.notify();
9154
9884
  }
9155
9885
  /** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */
@@ -10121,9 +10851,9 @@ var DEPTH_MASK_CONTROLS = [
10121
10851
  // src/image-editor/playback.ts
10122
10852
  var IMAGE_PLAYBACK_KIND = "layer";
10123
10853
  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)$/;
10124
- var SELECT = /^(select_region|select_subject|select_by_color|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy)$/;
10854
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|set_quick_mask)$/;
10125
10855
  var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
10126
- var CROP = /^(set_crop|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
10856
+ var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
10127
10857
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
10128
10858
  function imagePlaybackPanel(tool) {
10129
10859
  if (ADJUST.test(tool)) return "adjust";
@@ -10238,8 +10968,24 @@ var SELECTION_TOOLS = [
10238
10968
  { id: "wand", label: "Magic Wand" },
10239
10969
  { id: "rect", label: "Rectangle" },
10240
10970
  { id: "ellipse", label: "Ellipse" },
10241
- { id: "lasso", label: "Lasso" }
10971
+ { id: "row", label: "Single Row" },
10972
+ { id: "column", label: "Single Column" },
10973
+ { id: "lasso", label: "Lasso" },
10974
+ { id: "polygon", label: "Polygonal Lasso" },
10975
+ { id: "range", label: "Color Range" }
10242
10976
  ];
10977
+ var COLOR_RANGE_PRESET_LABELS = {
10978
+ reds: "Reds",
10979
+ yellows: "Yellows",
10980
+ greens: "Greens",
10981
+ cyans: "Cyans",
10982
+ blues: "Blues",
10983
+ magentas: "Magentas",
10984
+ highlights: "Highlights",
10985
+ midtones: "Midtones",
10986
+ shadows: "Shadows",
10987
+ skinTones: "Skin Tones"
10988
+ };
10243
10989
  var SELECTION_MODE_LABELS = {
10244
10990
  replace: "New",
10245
10991
  add: "Add",
@@ -10253,7 +10999,11 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
10253
10999
  contiguous: true,
10254
11000
  antiAlias: true,
10255
11001
  sampleMerged: false,
10256
- distance: "rgb"
11002
+ distance: "rgb",
11003
+ feather: 0,
11004
+ fuzziness: 40,
11005
+ localized: 0,
11006
+ rangeInvert: false
10257
11007
  };
10258
11008
  function selectionModeFromModifiers(modifiers, fallback) {
10259
11009
  if (modifiers.shiftKey && modifiers.altKey) return "intersect";
@@ -10284,6 +11034,41 @@ var modeSchema = {
10284
11034
  description: "How the result combines with the current selection (default replace)."
10285
11035
  };
10286
11036
  var num2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
11037
+ var featherSchema = { type: "number", minimum: 0, maximum: 1e3, description: "Feather the new shape by this many pixels before combining (default 0)." };
11038
+ var antiAliasSchema = { type: "boolean", description: "Soft one-pixel edge (default true)." };
11039
+ var sampleMergedSchema = { type: "boolean", description: "Sample the adjusted image (adjustment layers and filters applied) rather than the pixels as painted (default false)." };
11040
+ var sampleSchema = {
11041
+ type: "string",
11042
+ enum: ["composite", "active-layer"],
11043
+ description: "composite (default): all layers; active-layer: only the active pixel layer (see set_active_layer)."
11044
+ };
11045
+ var pointSchema = {
11046
+ type: "object",
11047
+ properties: { x: { type: "number", description: "x in natural image pixels." }, y: { type: "number", description: "y in natural image pixels." } },
11048
+ required: ["x", "y"],
11049
+ additionalProperties: false
11050
+ };
11051
+ var rectProperties = {
11052
+ x: { type: "number", description: "Left edge in natural image pixels." },
11053
+ y: { type: "number", description: "Top edge in natural image pixels." },
11054
+ width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
11055
+ height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
11056
+ };
11057
+ function shapeOptions(args) {
11058
+ return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
11059
+ }
11060
+ function sampleOptions(args) {
11061
+ if (args.sample !== void 0 && args.sample !== "composite" && args.sample !== "active-layer") {
11062
+ throw new Error(`Unknown sample "${String(args.sample)}". Use composite or active-layer.`);
11063
+ }
11064
+ return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
11065
+ }
11066
+ function rectOf(args) {
11067
+ const rect = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
11068
+ if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) throw new Error("Pass numeric x, y, width and height in natural image pixels.");
11069
+ if (rect.width <= 0 || rect.height <= 0) throw new Error("width and height must be greater than 0.");
11070
+ return rect;
11071
+ }
10287
11072
  function summary(controller) {
10288
11073
  const sel = controller.getSelection();
10289
11074
  const { width, height } = controller.getState().imageDimensions;
@@ -10316,7 +11101,7 @@ function createImageSelectionTools(controller) {
10316
11101
  const selectRegion = {
10317
11102
  name: "select_region",
10318
11103
  title: "Select a region",
10319
- description: 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), a polygon (the lasso: points along an outline, closed automatically), everything ("all"), nothing ("none") or the inverse of the current selection ("invert"). To cut out a subject, trace its contour with polygon (then refine_selection for soft edges such as fur or foliage) rather than selecting its bounding box. One undoable step.',
11104
+ description: 'Deprecated: use select_rect, select_ellipse, select_polygon, select_all, deselect or invert_selection (this alias goes in the next minor release). Selects a rectangle or ellipse (x, y, width, height in natural image pixels), a polygon (the lasso: points along an outline, closed automatically), everything ("all"), nothing ("none") or the inverse of the current selection ("invert"). To cut out a subject, trace its contour with polygon (then refine_selection for soft edges such as fur or foliage) rather than selecting its bounding box. One undoable step.',
10320
11105
  inputSchema: {
10321
11106
  type: "object",
10322
11107
  properties: {
@@ -10382,7 +11167,7 @@ function createImageSelectionTools(controller) {
10382
11167
  const selectByColor = {
10383
11168
  name: "select_by_color",
10384
11169
  title: "Select by colour",
10385
- description: 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with "color" ("#rrggbb") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',
11170
+ description: 'Deprecated: use magic_wand or select_color_range (this alias goes in the next minor release). Magic wand at (x, y) with tolerance (0-255) and contiguous, or with "color" ("#rrggbb") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',
10386
11171
  inputSchema: {
10387
11172
  type: "object",
10388
11173
  properties: {
@@ -10422,7 +11207,7 @@ function createImageSelectionTools(controller) {
10422
11207
  const modify = {
10423
11208
  name: "modify_selection",
10424
11209
  title: "Modify the selection",
10425
- description: "Select \u203A Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.",
11210
+ description: "Select \u203A Modify: expand, contract, feather, border or smooth the selection by radius pixels (grow and shrink are older names for expand and contract). Expand and contract move the edge by distance; to add pixels of similar colour use grow_selection or select_similar. One undoable step.",
10426
11211
  inputSchema: {
10427
11212
  type: "object",
10428
11213
  properties: {
@@ -10512,27 +11297,633 @@ function createImageSelectionTools(controller) {
10512
11297
  return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };
10513
11298
  }
10514
11299
  };
10515
- return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];
10516
- }
10517
- function createImageSubjectSelectionTool(controller) {
10518
- return {
10519
- name: "select_subject",
10520
- title: "Select a subject",
10521
- description: 'Select \u203A Subject: an AI segmentation model finds the objects matching target (a description such as "the dog", "the house", "the sky") in the flattened image and selects their outlines, combined with the selection by mode. Use it to cut a subject out by its contour (then layer_via_copy for a layer of just that subject); describe one object precisely to get only that one. One undoable step. Returns the selection and the objects found (label, bounds); none found leaves the selection unchanged.',
10522
- inputSchema: {
10523
- type: "object",
10524
- properties: {
10525
- target: { type: "string", minLength: 1, description: "What to select, in words." },
10526
- maxSegments: {
10527
- type: "integer",
10528
- minimum: 1,
10529
- maximum: 32,
10530
- description: "Select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
10531
- },
10532
- mode: modeSchema,
10533
- feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
10534
- },
10535
- required: ["target"],
11300
+ const selectAll = {
11301
+ name: "select_all",
11302
+ title: "Select all",
11303
+ description: "Select \u203A All: selects the whole image. One undoable step. Returns the selection.",
11304
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11305
+ annotations: writeAnnotations("Select all", true),
11306
+ handler: () => {
11307
+ controller.selectAll();
11308
+ return summary(controller);
11309
+ }
11310
+ };
11311
+ const deselect = {
11312
+ name: "deselect",
11313
+ title: "Deselect",
11314
+ description: "Select \u203A Deselect: clears the selection, so later edits apply to the whole layer. One undoable step. Returns the (empty) selection.",
11315
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11316
+ annotations: writeAnnotations("Deselect", true),
11317
+ handler: () => {
11318
+ controller.deselect();
11319
+ return summary(controller);
11320
+ }
11321
+ };
11322
+ const invert2 = {
11323
+ name: "invert_selection",
11324
+ title: "Invert the selection",
11325
+ description: "Select \u203A Inverse: selects what was not selected (soft edges stay soft). With nothing selected, selects everything. One undoable step. Returns the selection.",
11326
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11327
+ annotations: writeAnnotations("Invert the selection"),
11328
+ handler: () => {
11329
+ controller.invertSelection();
11330
+ return summary(controller);
11331
+ }
11332
+ };
11333
+ const selectRect = {
11334
+ name: "select_rect",
11335
+ title: "Select a rectangle",
11336
+ description: "Rectangular Marquee: selects x, y, width, height (natural image pixels), combined with the selection by mode. Use it for geometric areas (a frame, a screen, a strip along an edge); for a subject, trace its outline with select_polygon or use select_subject. One undoable step. Returns the selection.",
11337
+ inputSchema: {
11338
+ type: "object",
11339
+ properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
11340
+ required: ["x", "y", "width", "height"],
11341
+ additionalProperties: false
11342
+ },
11343
+ annotations: writeAnnotations("Select a rectangle", true),
11344
+ handler: (args) => {
11345
+ controller.selectRect(rectOf(args), shapeOptions(args));
11346
+ return summary(controller);
11347
+ }
11348
+ };
11349
+ const selectEllipse = {
11350
+ name: "select_ellipse",
11351
+ title: "Select an ellipse",
11352
+ description: "Elliptical Marquee: selects the ellipse inscribed in x, y, width, height (natural image pixels), combined by mode. Use feather for a vignette. One undoable step. Returns the selection.",
11353
+ inputSchema: {
11354
+ type: "object",
11355
+ properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
11356
+ required: ["x", "y", "width", "height"],
11357
+ additionalProperties: false
11358
+ },
11359
+ annotations: writeAnnotations("Select an ellipse", true),
11360
+ handler: (args) => {
11361
+ controller.selectEllipse(rectOf(args), shapeOptions(args));
11362
+ return summary(controller);
11363
+ }
11364
+ };
11365
+ const selectPolygonTool = {
11366
+ name: "select_polygon",
11367
+ title: "Select a polygon (lasso)",
11368
+ description: "Lasso: selects the polygon through points (natural image pixels, in order along the outline, closed automatically), combined by mode. To cut out a subject, follow its contour closely (more points give a tighter edge), then refine_selection for hair or foliage. One undoable step. Returns the selection.",
11369
+ inputSchema: {
11370
+ type: "object",
11371
+ properties: {
11372
+ points: { type: "array", minItems: 3, items: pointSchema, description: "At least 3 vertices along the outline, in order, in natural image pixels." },
11373
+ mode: modeSchema,
11374
+ feather: featherSchema,
11375
+ antiAlias: antiAliasSchema
11376
+ },
11377
+ required: ["points"],
11378
+ additionalProperties: false
11379
+ },
11380
+ annotations: writeAnnotations("Select a polygon (lasso)", true),
11381
+ handler: (args) => {
11382
+ const list = Array.isArray(args.points) ? args.points.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : [];
11383
+ if (list.length < 3 || !list.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) {
11384
+ throw new Error("points needs at least 3 { x, y } vertices in natural image pixels.");
11385
+ }
11386
+ controller.selectPolygon(list, shapeOptions(args));
11387
+ return summary(controller);
11388
+ }
11389
+ };
11390
+ const magicWandTool = {
11391
+ name: "magic_wand",
11392
+ title: "Magic wand",
11393
+ description: "Magic Wand: selects pixels similar in colour to the one at (x, y). tolerance 0..255 (default 32) is the largest channel difference (rgb) or \u0394E scaled so 255 \u2248 \u0394E 100 (perceptual, closer to how colours look); contiguous (default true) keeps only the region connected to the seed. Best on flat backdrops and solid colours; check percent with get_selection before using it. One undoable step. Returns the selection.",
11394
+ inputSchema: {
11395
+ type: "object",
11396
+ properties: {
11397
+ x: { type: "number", description: "Seed x in natural image pixels." },
11398
+ y: { type: "number", description: "Seed y in natural image pixels." },
11399
+ tolerance: { type: "number", minimum: 0, maximum: 255, description: "Colour tolerance 0..255 (default 32)." },
11400
+ contiguous: { type: "boolean", description: "Only the region connected to the seed (default true); false selects every similar pixel." },
11401
+ distance: { type: "string", enum: ["rgb", "perceptual"], description: "How colour difference is measured: rgb (default, as Photoshop) or perceptual (CIE \u0394E)." },
11402
+ antiAlias: antiAliasSchema,
11403
+ sampleMerged: sampleMergedSchema,
11404
+ sample: sampleSchema,
11405
+ mode: modeSchema,
11406
+ feather: featherSchema
11407
+ },
11408
+ required: ["x", "y"],
11409
+ additionalProperties: false
11410
+ },
11411
+ annotations: writeAnnotations("Magic wand", true),
11412
+ handler: async (args) => {
11413
+ const x = num2(args.x, NaN);
11414
+ const y = num2(args.y, NaN);
11415
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("Pass the seed point x and y in natural image pixels.");
11416
+ const { width, height } = controller.getState().imageDimensions;
11417
+ if (x < 0 || y < 0 || x >= width || y >= height) throw new Error(`(${x}, ${y}) is outside the image (0..${width - 1}, 0..${height - 1}).`);
11418
+ if (args.distance !== void 0 && args.distance !== "rgb" && args.distance !== "perceptual") {
11419
+ throw new Error(`Unknown distance "${String(args.distance)}". Use rgb or perceptual.`);
11420
+ }
11421
+ await controller.magicWand(
11422
+ { x, y },
11423
+ {
11424
+ ...shapeOptions(args),
11425
+ ...sampleOptions(args),
11426
+ tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))),
11427
+ contiguous: args.contiguous !== false,
11428
+ distance: args.distance ?? "rgb"
11429
+ }
11430
+ );
11431
+ return summary(controller);
11432
+ }
11433
+ };
11434
+ const colorRangeTool = {
11435
+ name: "select_color_range",
11436
+ title: "Select a colour range",
11437
+ description: "Select \u203A Color Range: selects every pixel matching samples (colours, each optionally at the x, y where it was picked) or a preset (reds \u2026 magentas, highlights, midtones, shadows, skinTones), softly: coverage falls off with colour distance over fuzziness levels. localized limits each positioned sample to a radius. invert selects everything else. Pass samples or preset, not both. One undoable step. Returns the selection.",
11438
+ inputSchema: {
11439
+ type: "object",
11440
+ properties: {
11441
+ samples: {
11442
+ type: "array",
11443
+ minItems: 1,
11444
+ maxItems: 32,
11445
+ items: {
11446
+ type: "object",
11447
+ properties: {
11448
+ color: { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "The sampled colour, #rrggbb." },
11449
+ x: { type: "number", description: "Where it was picked (natural image pixels), for localized." },
11450
+ y: { type: "number", description: "Where it was picked (natural image pixels), for localized." }
11451
+ },
11452
+ required: ["color"],
11453
+ additionalProperties: false
11454
+ },
11455
+ description: "Colours to select (the + eyedropper adds one). Read colours from capture_view or a seed pixel."
11456
+ },
11457
+ preset: { type: "string", enum: [...COLOR_RANGE_PRESETS], description: "A preset range instead of samples." },
11458
+ fuzziness: {
11459
+ type: "number",
11460
+ minimum: 0,
11461
+ maximum: 200,
11462
+ description: "Colour samples: levels over which coverage fades to 0 (default 40). Tone presets: falloff in levels around the range (default 20). Ignored by colour and skin presets."
11463
+ },
11464
+ localized: { type: "number", exclusiveMinimum: 0, description: "Localized Color Clusters: positioned samples fade to 0 at this many pixels from where they were picked (default off)." },
11465
+ invert: { type: "boolean", description: "Select everything except the range (default false)." },
11466
+ sampleMerged: sampleMergedSchema,
11467
+ sample: sampleSchema,
11468
+ mode: modeSchema,
11469
+ feather: featherSchema
11470
+ },
11471
+ additionalProperties: false
11472
+ },
11473
+ annotations: writeAnnotations("Select a colour range", true),
11474
+ handler: async (args) => {
11475
+ const hasSamples = Array.isArray(args.samples) && args.samples.length > 0;
11476
+ if (hasSamples === (args.preset !== void 0)) throw new Error("Pass samples (colours to match) or preset, not both.");
11477
+ const common = { ...sampleOptions(args), mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), invert: args.invert === true };
11478
+ if (!hasSamples) {
11479
+ if (!isColorRangePreset(args.preset)) throw new Error(`Unknown preset "${String(args.preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
11480
+ await controller.selectColorRangePreset(args.preset, { ...common, falloff: num2(args.fuzziness, 20) });
11481
+ } else {
11482
+ const samples = args.samples.map((s, i) => {
11483
+ if (typeof s?.color !== "string" || !/^#?[0-9a-f]{6}$/i.test(s.color.trim())) throw new Error(`samples[${i}].color must be #rrggbb.`);
11484
+ const at = typeof s.x === "number" && typeof s.y === "number" ? { x: s.x, y: s.y } : void 0;
11485
+ return at ? { color: s.color, at } : { color: s.color };
11486
+ });
11487
+ const localized = typeof args.localized === "number" && args.localized > 0 ? args.localized : void 0;
11488
+ await controller.selectColorRange(samples, { ...common, fuzziness: num2(args.fuzziness, 40), localized });
11489
+ }
11490
+ return summary(controller);
11491
+ }
11492
+ };
11493
+ const loadFromImage = {
11494
+ name: "load_selection_from_image",
11495
+ title: "Load a selection from a mask image",
11496
+ description: "Loads a selection from a mask image (an AI segmentation, a saved matte): white = selected by luminance, or one channel. Any size; it is resampled to the image. threshold (0..255) makes it hard-edged; leave it unset to keep soft edges. Follow with refine_selection for a coarse mask. One undoable step. Returns the selection.",
11497
+ inputSchema: {
11498
+ type: "object",
11499
+ properties: {
11500
+ image: { type: "string", minLength: 1, description: "The mask as a data URL (data:image/png;base64,\u2026) or an image URL." },
11501
+ channel: { type: "string", enum: ["luminance", "alpha", "red", "green", "blue"], description: "What to read as coverage (default luminance; alpha for a cutout PNG)." },
11502
+ threshold: { type: "number", minimum: 0, maximum: 255, description: "Values at or above this are selected, the rest not (default: keep soft values)." },
11503
+ invert: { type: "boolean", description: "Black = selected instead (default false)." },
11504
+ mode: modeSchema
11505
+ },
11506
+ required: ["image"],
11507
+ additionalProperties: false
11508
+ },
11509
+ annotations: { ...writeAnnotations("Load a selection from a mask image", true), openWorldHint: true },
11510
+ handler: async (args) => {
11511
+ if (typeof args.image !== "string" || !args.image.trim()) throw new Error("image must be a data URL or image URL.");
11512
+ const channel = args.channel ?? "luminance";
11513
+ if (!["luminance", "alpha", "red", "green", "blue"].includes(channel)) throw new Error(`Unknown channel "${String(channel)}". Use luminance, alpha, red, green or blue.`);
11514
+ await controller.loadSelectionFromImage(args.image, {
11515
+ channel,
11516
+ invert: args.invert === true,
11517
+ threshold: typeof args.threshold === "number" ? Math.min(255, Math.max(0, args.threshold)) : void 0,
11518
+ mode: parseSelectionMode(args.mode)
11519
+ });
11520
+ return summary(controller);
11521
+ }
11522
+ };
11523
+ const loadFromMask = {
11524
+ name: "load_selection_from_mask",
11525
+ title: "Load a selection from a layer mask",
11526
+ description: "Load Selection from a layer's mask (Ctrl/\u2318-click its mask thumbnail): the mask's pixels become the selection, combined by mode (density and feather are not applied). Use it to reuse a mask on another layer or to edit it as a selection. Fails when the layer has no painted mask. One undoable step. Returns the selection.",
11527
+ inputSchema: {
11528
+ type: "object",
11529
+ properties: {
11530
+ layerId: { type: "string", minLength: 1, description: "The layer whose mask to load (see list_layers)." },
11531
+ invert: { type: "boolean", description: "Select what the mask hides instead (default false)." },
11532
+ mode: modeSchema
11533
+ },
11534
+ required: ["layerId"],
11535
+ additionalProperties: false
11536
+ },
11537
+ annotations: writeAnnotations("Load a selection from a layer mask", true),
11538
+ handler: (args) => {
11539
+ const id = String(args.layerId ?? "");
11540
+ const layer = controller.getLayer(id);
11541
+ if (!layer) throw new Error(`No layer "${id}". Call list_layers for ids.`);
11542
+ if (!layer.mask) throw new Error(`Layer "${id}" has no mask. Add one with add_layer_mask, or pick a layer with a mask.`);
11543
+ if (layer.mask.source) throw new Error(`Layer "${id}" has a computed (depth) mask, not a painted one, so it cannot be loaded as a selection.`);
11544
+ controller.selectionFromLayerMask(id, parseSelectionMode(args.mode), args.invert === true);
11545
+ return summary(controller);
11546
+ }
11547
+ };
11548
+ const display = {
11549
+ name: "set_selection_display",
11550
+ title: "Show or hide the selection",
11551
+ description: "How the canvas shows the selection: ants (marching ants), quickMask (a red tint over what is not selected, to judge soft edges and feather) or none (hidden; it still limits edits). View state only: no undo step, not saved. Returns the display and the selection.",
11552
+ inputSchema: {
11553
+ type: "object",
11554
+ properties: { display: { type: "string", enum: ["ants", "quickMask", "none"], description: "ants, quickMask or none." } },
11555
+ required: ["display"],
11556
+ additionalProperties: false
11557
+ },
11558
+ annotations: writeAnnotations("Show or hide the selection", true),
11559
+ handler: (args) => {
11560
+ controller.setSelectionDisplay(args.display);
11561
+ return { display: controller.getSelectionDisplay(), selection: summary(controller) };
11562
+ }
11563
+ };
11564
+ const sampleTolerance = {
11565
+ tolerance: { type: "number", minimum: 0, maximum: 255, description: "Largest per-channel difference from a selected colour, 0..255 (default 32, as magic_wand)." },
11566
+ sampleMerged: sampleMergedSchema,
11567
+ sample: sampleSchema
11568
+ };
11569
+ const growTool = {
11570
+ name: "grow_selection",
11571
+ title: "Grow the selection by colour",
11572
+ description: "Select \u203A Grow: adds the pixels connected to the selection whose colour is within tolerance of a selected colour (to the nearest 4 levels). Use it after a magic_wand click that missed shades of the same area. Unlike modify_selection expand, it follows colour, not distance. Fails when nothing is selected. One undoable step. Returns the selection.",
11573
+ inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
11574
+ annotations: writeAnnotations("Grow the selection by colour"),
11575
+ handler: async (args) => {
11576
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select part of the area first (magic_wand, select_rect).");
11577
+ await controller.growSelection({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
11578
+ return summary(controller);
11579
+ }
11580
+ };
11581
+ const similarTool = {
11582
+ name: "select_similar",
11583
+ title: "Select similar colours",
11584
+ description: "Select \u203A Similar: adds every pixel in the image whose colour is within tolerance of a selected colour, connected or not. Fails when nothing is selected. One undoable step. Returns the selection.",
11585
+ inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
11586
+ annotations: writeAnnotations("Select similar colours"),
11587
+ handler: async (args) => {
11588
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select a sample of the colour first.");
11589
+ await controller.selectSimilar({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
11590
+ return summary(controller);
11591
+ }
11592
+ };
11593
+ const lineTool = {
11594
+ name: "select_line",
11595
+ title: "Select a single row or column",
11596
+ description: "Single Row / Single Column Marquee: selects a 1 px line across the whole image, a row at y = position or a column at x = position (natural image pixels). Use it to fix a dead row or column of pixels, or to stretch an edge line. One undoable step. Returns the selection.",
11597
+ inputSchema: {
11598
+ type: "object",
11599
+ properties: {
11600
+ orientation: { type: "string", enum: ["row", "column"], description: "row (horizontal, at y) or column (vertical, at x)." },
11601
+ position: { type: "number", minimum: 0, description: "y of the row or x of the column, in natural image pixels." },
11602
+ mode: modeSchema
11603
+ },
11604
+ required: ["orientation", "position"],
11605
+ additionalProperties: false
11606
+ },
11607
+ annotations: writeAnnotations("Select a single row or column", true),
11608
+ handler: (args) => {
11609
+ if (args.orientation !== "row" && args.orientation !== "column") throw new Error("orientation must be row or column.");
11610
+ controller.selectLine(args.orientation, num2(args.position, NaN), { mode: parseSelectionMode(args.mode) });
11611
+ return summary(controller);
11612
+ }
11613
+ };
11614
+ const reselectTool = {
11615
+ name: "reselect",
11616
+ title: "Reselect",
11617
+ description: "Select \u203A Reselect: brings back the last selection that deselect (or select_region none) cleared. Fails when there is none (for example after reopening the document). One undoable step. Returns the selection.",
11618
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11619
+ annotations: writeAnnotations("Reselect", true),
11620
+ handler: () => {
11621
+ if (!controller.canReselect()) throw new Error("There is no cleared selection to bring back.");
11622
+ controller.reselect();
11623
+ return summary(controller);
11624
+ }
11625
+ };
11626
+ const transparencyTool = {
11627
+ name: "select_layer_transparency",
11628
+ title: "Select a layer's pixels",
11629
+ description: "Load Selection from a layer's transparency (Ctrl/\u2318-click its thumbnail): selects where the layer has pixels, soft where it is partly transparent, combined by mode. Use it to select a cutout, a shape or text exactly. Pixel, shape, type and smart object layers. One undoable step. Returns the selection.",
11630
+ inputSchema: {
11631
+ type: "object",
11632
+ properties: {
11633
+ layerId: { type: "string", minLength: 1, description: "The layer whose pixels to select (see list_layers)." },
11634
+ mode: modeSchema,
11635
+ feather: featherSchema
11636
+ },
11637
+ required: ["layerId"],
11638
+ additionalProperties: false
11639
+ },
11640
+ annotations: writeAnnotations("Select a layer's pixels", true),
11641
+ handler: (args) => {
11642
+ const id = String(args.layerId ?? "");
11643
+ if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
11644
+ controller.selectLayerTransparency(id, { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0) });
11645
+ return summary(controller);
11646
+ }
11647
+ };
11648
+ const colorSchema = { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "Colour #rrggbb (default the foreground colour)." };
11649
+ const layerIdSchema = { type: "string", minLength: 1, description: "The layer (see list_layers; default the active layer)." };
11650
+ const requireLayerId = (v) => {
11651
+ if (v === void 0) return void 0;
11652
+ const id = String(v);
11653
+ if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
11654
+ return id;
11655
+ };
11656
+ const strokeTool = {
11657
+ name: "stroke_selection",
11658
+ title: "Stroke the selection",
11659
+ description: "Edit \u203A Stroke: paints a band width px wide along the selection edge, outside it (default), inside it or centred on it, with color, opacity and blend, on a layer's pixels or mask (channel). Use it for outlines, frames and borders that follow a shape. Fails when nothing is selected. One undo step. Returns the layer, channel and changed bounds.",
11660
+ inputSchema: {
11661
+ type: "object",
11662
+ properties: {
11663
+ width: { type: "number", exclusiveMinimum: 0, maximum: 250, description: "Band width in pixels." },
11664
+ location: { type: "string", enum: ["outside", "inside", "center"], description: "Where the band lies against the edge (default outside)." },
11665
+ color: colorSchema,
11666
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
11667
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
11668
+ layerId: layerIdSchema,
11669
+ channel: { type: "string", enum: ["pixels", "mask"], description: "Paint the pixels (default) or the layer mask." }
11670
+ },
11671
+ required: ["width"],
11672
+ additionalProperties: false
11673
+ },
11674
+ annotations: writeAnnotations("Stroke the selection"),
11675
+ handler: (args) => {
11676
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select the shape to outline first.");
11677
+ return controller.strokeSelection({
11678
+ width: num2(args.width, NaN),
11679
+ location: args.location,
11680
+ color: args.color,
11681
+ opacity: args.opacity,
11682
+ blend: args.blend,
11683
+ layerId: requireLayerId(args.layerId),
11684
+ channel: args.channel
11685
+ });
11686
+ }
11687
+ };
11688
+ const cropTool = {
11689
+ name: "crop_to_selection",
11690
+ title: "Crop to the selection",
11691
+ description: "Image \u203A Crop to the selection: sets the crop box to the selection's bounds (non-destructive; reset_crop undoes it). Fails when nothing is selected. One undo step. Returns the crop box in natural image pixels.",
11692
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11693
+ annotations: writeAnnotations("Crop to the selection", true),
11694
+ handler: () => {
11695
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select the area to keep first.");
11696
+ return { crop: controller.cropToSelection() };
11697
+ }
11698
+ };
11699
+ const fillTool = {
11700
+ name: "content_aware_fill",
11701
+ title: "Content-Aware Fill",
11702
+ 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.",
11703
+ inputSchema: {
11704
+ type: "object",
11705
+ properties: {
11706
+ output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
11707
+ sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
11708
+ seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
11709
+ layerId: layerIdSchema
11710
+ },
11711
+ additionalProperties: false
11712
+ },
11713
+ annotations: writeAnnotations("Content-Aware Fill"),
11714
+ handler: (args) => {
11715
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
11716
+ return controller.contentAwareFill({
11717
+ output: args.output,
11718
+ sampleRadius: args.sampleRadius,
11719
+ seed: args.seed,
11720
+ layerId: requireLayerId(args.layerId)
11721
+ });
11722
+ }
11723
+ };
11724
+ const copyTool = {
11725
+ name: "copy_pixels",
11726
+ title: "Copy pixels",
11727
+ description: "Edit \u203A Copy: puts the selected pixels of a pixel layer (default the active one), or with merged everything visible (Copy Merged), on the editor clipboard, cropped to the selection with its soft edge as alpha. Without a selection, the whole canvas. The document does not change. Returns the copied size and where it came from; then paste_pixels.",
11728
+ inputSchema: {
11729
+ type: "object",
11730
+ properties: { layerId: layerIdSchema, merged: { type: "boolean", description: "Copy the visible image of all layers (default false)." } },
11731
+ additionalProperties: false
11732
+ },
11733
+ annotations: { title: "Copy pixels", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
11734
+ handler: (args) => {
11735
+ const { width, height, x, y } = controller.copyPixels({ layerId: requireLayerId(args.layerId), merged: args.merged === true });
11736
+ return { width, height, x, y };
11737
+ }
11738
+ };
11739
+ const cutTool = {
11740
+ name: "cut_pixels",
11741
+ title: "Cut pixels",
11742
+ description: "Edit \u203A Cut: copies the selected pixels of a pixel layer (default the active one) to the editor clipboard and clears them from the layer (transparent, or the background colour on a transparency-locked layer). One undo step. Returns the copied size and position; then paste_pixels.",
11743
+ inputSchema: { type: "object", properties: { layerId: layerIdSchema }, additionalProperties: false },
11744
+ annotations: { title: "Cut pixels", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
11745
+ handler: (args) => {
11746
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select what to cut first (cutting everything would empty the layer).");
11747
+ const { width, height, x, y } = controller.cutPixels({ layerId: requireLayerId(args.layerId) });
11748
+ return { width, height, x, y };
11749
+ }
11750
+ };
11751
+ const pasteTool = {
11752
+ name: "paste_pixels",
11753
+ title: "Paste pixels",
11754
+ description: "Edit \u203A Paste the editor clipboard (from copy_pixels or cut_pixels) as a new pixel layer above the active one: new-layer (default, centred on the selection or the canvas), in-place (where it was copied), into (centred in the selection, masked by it: Paste Into) or outside (masked by its inverse). One undo step. Returns the new layerId, its bounds and whether it is masked.",
11755
+ inputSchema: {
11756
+ type: "object",
11757
+ properties: { mode: { type: "string", enum: ["new-layer", "in-place", "into", "outside"], description: "Where and how to paste (default new-layer)." } },
11758
+ additionalProperties: false
11759
+ },
11760
+ annotations: writeAnnotations("Paste pixels"),
11761
+ handler: (args) => controller.pastePixels(args.mode ?? "new-layer")
11762
+ };
11763
+ const channelIdSchema = { type: "string", minLength: 1, description: "The alpha channel (see list_channels)." };
11764
+ const channelResult = (id) => controller.describeChannels().find((c) => c.id === id);
11765
+ const listChannels = {
11766
+ name: "list_channels",
11767
+ title: "List alpha channels",
11768
+ description: "Lists the alpha channels (saved selections kept with the document): id, name, bounds, percent of the image covered, overlay color, opacity, what the tint indicates and whether it is visible. Also whether Quick Mask mode is on.",
11769
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11770
+ annotations: { title: "List alpha channels", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
11771
+ handler: () => ({ channels: controller.describeChannels(), quickMask: controller.quickMaskEditing })
11772
+ };
11773
+ const saveSelectionTool = {
11774
+ name: "save_selection",
11775
+ title: "Save the selection to a channel",
11776
+ description: 'Select \u203A Save Selection: stores the selection in a new alpha channel (name, default "Alpha n"), or with channelId combines it into that channel by mode (replace, add, subtract, intersect). Save any selection that took work, so it can be loaded again with load_selection_from_channel. Fails when nothing is selected. One undo step. Returns the channel.',
11777
+ inputSchema: {
11778
+ type: "object",
11779
+ properties: { channelId: channelIdSchema, name: { type: "string", minLength: 1, description: "Name of a new channel, or a new name for channelId." }, mode: modeSchema },
11780
+ additionalProperties: false
11781
+ },
11782
+ annotations: writeAnnotations("Save the selection to a channel"),
11783
+ handler: (args) => {
11784
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select first, then save it.");
11785
+ const c = controller.saveSelection({ channelId: args.channelId, name: args.name, mode: parseSelectionMode(args.mode) });
11786
+ return { channel: channelResult(c.id) };
11787
+ }
11788
+ };
11789
+ const loadChannelTool = {
11790
+ name: "load_selection_from_channel",
11791
+ title: "Load a selection from a channel",
11792
+ description: "Select \u203A Load Selection from an alpha channel (invert for its inverse), combined with the current selection by mode. One undo step. Returns the selection.",
11793
+ inputSchema: {
11794
+ type: "object",
11795
+ properties: { channelId: channelIdSchema, invert: { type: "boolean", description: "Load the inverse (default false)." }, mode: modeSchema },
11796
+ required: ["channelId"],
11797
+ additionalProperties: false
11798
+ },
11799
+ annotations: writeAnnotations("Load a selection from a channel", true),
11800
+ handler: (args) => {
11801
+ controller.loadSelectionFromChannel(String(args.channelId), { mode: parseSelectionMode(args.mode), invert: args.invert === true });
11802
+ return summary(controller);
11803
+ }
11804
+ };
11805
+ const channelPropsTool = {
11806
+ name: "set_channel_properties",
11807
+ title: "Change an alpha channel",
11808
+ description: "Renames an alpha channel or changes its overlay: color (#rrggbb), opacity (0..1), indicates (masked: tint the unselected area, or selected) and visible (show the tint on the canvas). Pixels do not change. One undo step. Returns the channel.",
11809
+ inputSchema: {
11810
+ type: "object",
11811
+ properties: {
11812
+ channelId: channelIdSchema,
11813
+ name: { type: "string", minLength: 1, description: "New name." },
11814
+ color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Overlay colour #rrggbb." },
11815
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Overlay opacity 0..1." },
11816
+ indicates: { type: "string", enum: ["masked", "selected"], description: "Which area the tint covers." },
11817
+ visible: { type: "boolean", description: "Show the channel as a tint on the canvas." }
11818
+ },
11819
+ required: ["channelId"],
11820
+ additionalProperties: false
11821
+ },
11822
+ annotations: writeAnnotations("Change an alpha channel", true),
11823
+ handler: (args) => {
11824
+ const { channelId, ...props } = args;
11825
+ const c = controller.setChannelProperties(String(channelId), props);
11826
+ return { channel: channelResult(c.id) };
11827
+ }
11828
+ };
11829
+ const duplicateChannelTool = {
11830
+ name: "duplicate_channel",
11831
+ title: "Duplicate an alpha channel",
11832
+ description: 'Copies an alpha channel (name, default "<name> copy") just after it, to keep a version before changing it. One undo step. Returns the copy.',
11833
+ inputSchema: {
11834
+ type: "object",
11835
+ properties: { channelId: channelIdSchema, name: { type: "string", minLength: 1, description: "Name of the copy." } },
11836
+ required: ["channelId"],
11837
+ additionalProperties: false
11838
+ },
11839
+ annotations: writeAnnotations("Duplicate an alpha channel"),
11840
+ handler: (args) => ({ channel: channelResult(controller.duplicateChannel(String(args.channelId), args.name).id) })
11841
+ };
11842
+ const deleteChannelTool = {
11843
+ name: "delete_channel",
11844
+ title: "Delete an alpha channel",
11845
+ description: "Deletes an alpha channel (undo_image brings it back). Returns the remaining channels.",
11846
+ inputSchema: { type: "object", properties: { channelId: channelIdSchema }, required: ["channelId"], additionalProperties: false },
11847
+ annotations: { title: "Delete an alpha channel", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
11848
+ handler: (args) => {
11849
+ controller.deleteChannel(String(args.channelId));
11850
+ return { channels: controller.describeChannels() };
11851
+ }
11852
+ };
11853
+ const quickMaskTool = {
11854
+ name: "set_quick_mask",
11855
+ title: "Quick Mask mode",
11856
+ description: "Turns Quick Mask mode on or off (Q). On, the selection shows as a red tint and paint_stroke, fill_pixels, clear_pixels, stroke_selection and the retouch tools called without layerId edit the selection itself: white selects, black deselects, grey selects partly, and blur_stroke softens its edge. Each edit is one undo step. Turn it off before painting layers again. Returns the mode and the selection.",
11857
+ inputSchema: {
11858
+ type: "object",
11859
+ properties: { enabled: { type: "boolean", description: "true to enter Quick Mask mode, false to leave it." } },
11860
+ required: ["enabled"],
11861
+ additionalProperties: false
11862
+ },
11863
+ annotations: writeAnnotations("Quick Mask mode", true),
11864
+ handler: (args) => {
11865
+ if (typeof args.enabled !== "boolean") throw new Error("enabled must be true or false.");
11866
+ controller.setQuickMaskMode(args.enabled);
11867
+ return { quickMask: controller.quickMaskEditing, selection: summary(controller) };
11868
+ }
11869
+ };
11870
+ return [
11871
+ getSelection,
11872
+ selectAll,
11873
+ deselect,
11874
+ invert2,
11875
+ selectRect,
11876
+ selectEllipse,
11877
+ selectPolygonTool,
11878
+ lineTool,
11879
+ magicWandTool,
11880
+ colorRangeTool,
11881
+ loadFromImage,
11882
+ loadFromMask,
11883
+ transparencyTool,
11884
+ growTool,
11885
+ similarTool,
11886
+ reselectTool,
11887
+ display,
11888
+ selectRegion,
11889
+ selectByColor,
11890
+ modify,
11891
+ refine,
11892
+ applyMask,
11893
+ strokeTool,
11894
+ cropTool,
11895
+ fillTool,
11896
+ copyTool,
11897
+ cutTool,
11898
+ pasteTool,
11899
+ listChannels,
11900
+ saveSelectionTool,
11901
+ loadChannelTool,
11902
+ channelPropsTool,
11903
+ duplicateChannelTool,
11904
+ deleteChannelTool,
11905
+ quickMaskTool
11906
+ ];
11907
+ }
11908
+ function createImageSubjectSelectionTool(controller) {
11909
+ return {
11910
+ name: "select_subject",
11911
+ title: "Select a subject",
11912
+ description: 'Select \u203A Subject: an AI segmentation model finds the objects matching target (a description such as "the dog", "the house", "the sky") in the flattened image and selects their outlines, combined with the selection by mode. Use it to cut a subject out by its contour (then layer_via_copy for a layer of just that subject); describe one object precisely to get only that one. One undoable step. Returns the selection and the objects found (label, bounds); none found leaves the selection unchanged.',
11913
+ inputSchema: {
11914
+ type: "object",
11915
+ properties: {
11916
+ target: { type: "string", minLength: 1, description: "What to select, in words." },
11917
+ maxSegments: {
11918
+ type: "integer",
11919
+ minimum: 1,
11920
+ maximum: 32,
11921
+ description: "Select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
11922
+ },
11923
+ mode: modeSchema,
11924
+ feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
11925
+ },
11926
+ required: ["target"],
10536
11927
  additionalProperties: false
10537
11928
  },
10538
11929
  annotations: { ...writeAnnotations("Select a subject"), openWorldHint: true },
@@ -11272,12 +12663,12 @@ function createImageLayerTools(controller) {
11272
12663
  const addMask = {
11273
12664
  name: "add_layer_mask",
11274
12665
  title: "Add a layer mask",
11275
- description: "Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal) or selection (the selection's coverage). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.",
12666
+ description: "Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal), selection (Reveal Selection: the selection's coverage), hide-selection (its inverse) or transparency (the layer's own alpha, for pixel, shape, type and smart object layers). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.",
11276
12667
  inputSchema: {
11277
12668
  type: "object",
11278
12669
  properties: {
11279
12670
  layerId: LAYER_ID2,
11280
- from: { type: "string", enum: ["reveal-all", "hide-all", "selection"], description: "What the mask starts as (default reveal-all)." },
12671
+ from: { type: "string", enum: ["reveal-all", "hide-all", "selection", "hide-selection", "transparency"], description: "What the mask starts as (default reveal-all)." },
11281
12672
  density: { type: "number", minimum: 0, maximum: 1, description: "Mask density 0..1 (default 1)." },
11282
12673
  feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px (default 0)." }
11283
12674
  },
@@ -11932,6 +13323,9 @@ function handlePaintShortcut(controller, e) {
11932
13323
  const set = (b) => controller.setPaintOptions({ brush: b });
11933
13324
  const key = e.key;
11934
13325
  switch (key.toLowerCase()) {
13326
+ case "q":
13327
+ controller.setQuickMaskMode(!controller.quickMaskEditing);
13328
+ return true;
11935
13329
  case "b":
11936
13330
  controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
11937
13331
  return true;
@@ -12071,8 +13465,8 @@ var TARGET = {
12071
13465
  layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
12072
13466
  channel: {
12073
13467
  type: "string",
12074
- enum: ["pixels", "mask", "filter-mask"],
12075
- description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals."
13468
+ enum: ["pixels", "mask", "filter-mask", "quick-mask"],
13469
+ description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals. In Quick Mask mode (set_quick_mask), quick-mask (the default when no layerId is given) paints the selection: white selects, black deselects."
12076
13470
  }
12077
13471
  };
12078
13472
  var BRUSH = {
@@ -12460,8 +13854,8 @@ var TARGET2 = {
12460
13854
  layerId: { type: "string", description: "Layer to retouch (default the active layer; see list_layers)." },
12461
13855
  channel: {
12462
13856
  type: "string",
12463
- enum: ["pixels", "mask", "filter-mask"],
12464
- description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves."
13857
+ enum: ["pixels", "mask", "filter-mask", "quick-mask"],
13858
+ description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves. In Quick Mask mode (set_quick_mask), quick-mask (the default when no layerId is given) paints the selection: white selects, black deselects."
12465
13859
  }
12466
13860
  };
12467
13861
  var SAMPLE = {
@@ -13331,13 +14725,14 @@ function createImageSmartObjectTools(controller) {
13331
14725
  const addFilter = {
13332
14726
  name: "add_smart_filter",
13333
14727
  title: "Add a smart filter",
13334
- description: "Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id) and the stack.",
14728
+ description: "Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. With a selection (and useSelection, default true), the filter mask is set from it in the same step. One undo step. Returns the filter (with its id), the stack and masked.",
13335
14729
  inputSchema: {
13336
14730
  type: "object",
13337
14731
  properties: {
13338
14732
  layerId: LAYER_ID4,
13339
14733
  kind: { type: "string", enum: ["develop", "blur"], description: "Filter kind." },
13340
14734
  index: { type: "integer", minimum: 0, description: "Position in the stack, 0 = bottom, applied first (default the top)." },
14735
+ useSelection: { type: "boolean", description: "With a selection, set the filter mask from it so the filter only changes the selected area (default true)." },
13341
14736
  ...FILTER_SETTINGS
13342
14737
  },
13343
14738
  required: ["layerId", "kind"],
@@ -13346,8 +14741,9 @@ function createImageSmartObjectTools(controller) {
13346
14741
  annotations: { title: "Add a smart filter", ...WRITE7 },
13347
14742
  handler: (args) => {
13348
14743
  const id = smartId(args.layerId);
13349
- const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index);
13350
- return { layerId: id, filter, smartFilters: info(id).smartFilters };
14744
+ const masked = args.useSelection !== false && controller.hasSelection;
14745
+ const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index, { useSelection: args.useSelection !== false });
14746
+ return { layerId: id, filter, smartFilters: info(id).smartFilters, masked };
13351
14747
  }
13352
14748
  };
13353
14749
  const updateFilter = {
@@ -14160,7 +15556,7 @@ function createImageEditorToolProvider(controller, options = {}) {
14160
15556
  const addLayer = {
14161
15557
  name: "add_adjustment_layer",
14162
15558
  title: "Add an adjustment layer",
14163
- description: "Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: " + ADJUSTMENT_KINDS.map((k) => k.kind).join(", ") + ". Omitted params are neutral.",
15559
+ description: "Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it; with a selection it is masked by it (useSelection, default true; masked in the result says so). Kinds: " + ADJUSTMENT_KINDS.map((k) => k.kind).join(", ") + ". Omitted params are neutral.",
14164
15560
  inputSchema: {
14165
15561
  type: "object",
14166
15562
  properties: {
@@ -14170,7 +15566,8 @@ function createImageEditorToolProvider(controller, options = {}) {
14170
15566
  blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
14171
15567
  name: { type: "string", description: "Layer name shown in the layers panel." },
14172
15568
  aboveId: { type: "string", description: "Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations." },
14173
- parentId: { type: "string", description: "Put the layer at the top of this group (from list_layers)." }
15569
+ parentId: { type: "string", description: "Put the layer at the top of this group (from list_layers)." },
15570
+ useSelection: { type: "boolean", description: "With a selection, mask the new layer with it so the adjustment only affects the selected area (default true; false for a global layer)." }
14174
15571
  },
14175
15572
  required: ["kind"],
14176
15573
  additionalProperties: false
@@ -14182,7 +15579,7 @@ function createImageEditorToolProvider(controller, options = {}) {
14182
15579
  idempotentHint: false,
14183
15580
  openWorldHint: false
14184
15581
  },
14185
- handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {
15582
+ handler: ({ kind, params, opacity, blend, name, aboveId, parentId, useSelection }) => {
14186
15583
  if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind "${kind}".`);
14187
15584
  if (blend !== void 0 && !isBlendMode(blend)) throw new Error(`Unknown blend mode "${blend}".`);
14188
15585
  if (aboveId !== void 0 && parentId !== void 0) throw new Error("Give aboveId or parentId, not both.");
@@ -14190,9 +15587,9 @@ function createImageEditorToolProvider(controller, options = {}) {
14190
15587
  if (ref !== void 0 && !controller.getLayer(ref)) throw new Error(`Unknown layer "${ref}". Call list_layers for ids.`);
14191
15588
  }
14192
15589
  if (parentId !== void 0 && controller.getLayer(parentId)?.type !== "group") throw new Error(`Layer "${parentId}" is not a group.`);
14193
- const id = controller.addAdjustmentLayer({ kind, params, opacity, blend, name, aboveId, parentId });
15590
+ const id = controller.addAdjustmentLayer({ kind, params, opacity, blend, name, aboveId, parentId, useSelection: useSelection !== false });
14194
15591
  const layer = controller.getAdjustmentLayer(id);
14195
- return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };
15592
+ return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id), masked: Boolean(layer.mask) };
14196
15593
  }
14197
15594
  };
14198
15595
  const renderPreview = {
@@ -14326,6 +15723,14 @@ function removeCurvePoint(points, index) {
14326
15723
  var round22 = (v) => Math.round(v * 100) / 100;
14327
15724
 
14328
15725
  // src/image-editor/selection/gesture.ts
15726
+ function snapTo45(from, point) {
15727
+ const dx = point.x - from.x;
15728
+ const dy = point.y - from.y;
15729
+ const len = Math.hypot(dx, dy);
15730
+ if (len === 0) return point;
15731
+ const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
15732
+ return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
15733
+ }
14329
15734
  var SelectionGesture = class {
14330
15735
  constructor(controller, getOptions) {
14331
15736
  this.controller = controller;
@@ -14337,13 +15742,20 @@ var SelectionGesture = class {
14337
15742
  points = [];
14338
15743
  current = null;
14339
15744
  modifiers = {};
15745
+ /** Polygonal lasso corners while one is being drawn. */
15746
+ polygon = null;
15747
+ pressedAt = null;
15748
+ freehand = false;
15749
+ /** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
15750
+ closeRadius = 8;
14340
15751
  /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
14341
15752
  error = null;
14342
15753
  get active() {
14343
- return this.start !== null;
15754
+ return this.start !== null || this.polygon !== null;
14344
15755
  }
14345
15756
  /** The outline of the shape being drawn, or `null`. */
14346
15757
  get draftPath() {
15758
+ if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
14347
15759
  if (!this.start || !this.current) return null;
14348
15760
  const { tool } = this.getOptions();
14349
15761
  if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
@@ -14366,18 +15778,71 @@ var SelectionGesture = class {
14366
15778
  async begin(point, modifiers = {}) {
14367
15779
  const options = this.getOptions();
14368
15780
  this.error = null;
15781
+ if (this.polygon && options.tool !== "polygon") this.cancel();
15782
+ if (options.tool === "polygon") {
15783
+ this.pressedAt = point;
15784
+ this.freehand = false;
15785
+ if (!this.polygon) {
15786
+ this.modifiers = modifiers;
15787
+ this.polygon = [point];
15788
+ this.current = point;
15789
+ return;
15790
+ }
15791
+ const last = this.polygon[this.polygon.length - 1];
15792
+ const p = modifiers.shiftKey ? snapTo45(last, point) : point;
15793
+ const first = this.polygon[0];
15794
+ if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
15795
+ this.close();
15796
+ return;
15797
+ }
15798
+ if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
15799
+ this.current = p;
15800
+ if ((modifiers.detail ?? 1) >= 2) this.close();
15801
+ return;
15802
+ }
14369
15803
  this.modifiers = modifiers;
14370
- if (options.tool === "wand") {
15804
+ if (options.tool === "row" || options.tool === "column") {
14371
15805
  this.start = null;
14372
15806
  try {
15807
+ this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
15808
+ mode: selectionModeFromModifiers(modifiers, options.mode),
15809
+ feather: options.feather ?? 0
15810
+ });
15811
+ } catch (err) {
15812
+ this.error = err instanceof Error ? err.message : String(err);
15813
+ }
15814
+ return;
15815
+ }
15816
+ if (options.tool === "wand" || options.tool === "range") {
15817
+ this.start = null;
15818
+ const common = {
15819
+ sampleMerged: options.sampleMerged,
15820
+ sample: options.sample,
15821
+ feather: options.feather ?? 0,
15822
+ mode: selectionModeFromModifiers(modifiers, options.mode)
15823
+ };
15824
+ try {
15825
+ if (options.tool === "range") {
15826
+ const pixels = await this.controller.getSamplePixels(common);
15827
+ if (!pixels) throw new Error("The image pixels are not available to sample.");
15828
+ const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
15829
+ const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
15830
+ const p = (y * pixels.width + x) * 4;
15831
+ const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
15832
+ await this.controller.selectColorRange([{ color, at: point }], {
15833
+ ...common,
15834
+ fuzziness: options.fuzziness ?? 40,
15835
+ localized: options.localized ? options.localized : void 0,
15836
+ invert: options.rangeInvert ?? false
15837
+ });
15838
+ return;
15839
+ }
14373
15840
  await this.controller.magicWand(point, {
15841
+ ...common,
14374
15842
  tolerance: options.tolerance,
14375
15843
  contiguous: options.contiguous,
14376
15844
  antiAlias: options.antiAlias,
14377
- distance: options.distance,
14378
- sampleMerged: options.sampleMerged,
14379
- sample: options.sample,
14380
- mode: selectionModeFromModifiers(modifiers, options.mode)
15845
+ distance: options.distance
14381
15846
  });
14382
15847
  } catch (err) {
14383
15848
  this.error = err instanceof Error ? err.message : String(err);
@@ -14389,7 +15854,18 @@ var SelectionGesture = class {
14389
15854
  this.points = [point];
14390
15855
  }
14391
15856
  /** Pointer move; returns true when the draft changed (re-render). */
14392
- move(point) {
15857
+ move(point, modifiers = {}) {
15858
+ if (this.polygon) {
15859
+ const last = this.polygon[this.polygon.length - 1];
15860
+ if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
15861
+ this.freehand = true;
15862
+ if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
15863
+ this.current = null;
15864
+ return true;
15865
+ }
15866
+ this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
15867
+ return true;
15868
+ }
14393
15869
  if (!this.start) return false;
14394
15870
  this.current = point;
14395
15871
  if (this.getOptions().tool === "lasso") {
@@ -14401,6 +15877,12 @@ var SelectionGesture = class {
14401
15877
  }
14402
15878
  /** Pointer up: commits the shape. Returns true when the selection changed. */
14403
15879
  end(point, modifiers) {
15880
+ if (this.polygon) {
15881
+ if (point && this.freehand) this.move(point);
15882
+ this.pressedAt = null;
15883
+ this.freehand = false;
15884
+ return false;
15885
+ }
14404
15886
  if (!this.start) return false;
14405
15887
  if (point) this.move(point);
14406
15888
  const options = this.getOptions();
@@ -14416,17 +15898,70 @@ var SelectionGesture = class {
14416
15898
  this.current = null;
14417
15899
  this.points = [];
14418
15900
  if (!path) return mode === "replace" ? this.controller.deselect() : false;
14419
- const opts = { mode, antiAlias: options.antiAlias };
15901
+ const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
14420
15902
  if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
14421
15903
  return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
14422
15904
  }
14423
- /** Abandons a drag without changing the selection. */
15905
+ /** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
15906
+ close() {
15907
+ const polygon = this.polygon;
15908
+ if (!polygon) return false;
15909
+ const options = this.getOptions();
15910
+ this.cancel();
15911
+ if (polygon.length < 3) return false;
15912
+ return this.controller.selectPolygon(polygon, {
15913
+ mode: selectionModeFromModifiers(this.modifiers, options.mode),
15914
+ antiAlias: options.antiAlias,
15915
+ feather: options.feather ?? 0
15916
+ });
15917
+ }
15918
+ /**
15919
+ * Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
15920
+ * removes the last corner. Returns true when the key was used (the host prevents its default).
15921
+ */
15922
+ key(key) {
15923
+ if (!this.polygon) return false;
15924
+ if (key === "Enter") this.close();
15925
+ else if (key === "Escape") this.cancel();
15926
+ else if (key === "Backspace" || key === "Delete") {
15927
+ this.polygon.pop();
15928
+ if (!this.polygon.length) this.cancel();
15929
+ } else return false;
15930
+ return true;
15931
+ }
15932
+ /** Abandons a drag (or a polygonal lasso) without changing the selection. */
14424
15933
  cancel() {
14425
15934
  this.start = null;
14426
15935
  this.current = null;
14427
15936
  this.points = [];
15937
+ this.polygon = null;
15938
+ this.pressedAt = null;
15939
+ this.freehand = false;
14428
15940
  }
14429
15941
  };
15942
+ function handleSelectionShortcut(controller, e, onError) {
15943
+ if (!(e.ctrlKey || e.metaKey)) return false;
15944
+ const key = e.key.toLowerCase();
15945
+ const run = (fn) => {
15946
+ try {
15947
+ fn();
15948
+ } catch (err) {
15949
+ onError?.(err instanceof Error ? err.message : String(err));
15950
+ }
15951
+ return true;
15952
+ };
15953
+ if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
15954
+ if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
15955
+ if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
15956
+ if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
15957
+ if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
15958
+ return false;
15959
+ }
15960
+ function handleQuickMaskKey(controller, e) {
15961
+ if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
15962
+ controller.setQuickMaskMode(!controller.quickMaskEditing);
15963
+ return true;
15964
+ }
14430
15965
 
14431
15966
  // src/image-editor/mosaic-filter.ts
14432
15967
  function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
@@ -14566,6 +16101,21 @@ function applyRegionBlur(imageData, regions, radius = 10) {
14566
16101
  }
14567
16102
 
14568
16103
  // src/image-editor/layers/panel.ts
16104
+ function maskThumbnailClick(controller, layer, e) {
16105
+ if (!layer.mask) return;
16106
+ if (e.ctrlKey || e.metaKey) {
16107
+ controller.selectionFromLayerMask(layer.id, selectionModeFromModifiers(e, "replace"));
16108
+ } else if (e.shiftKey) {
16109
+ controller.setLayerMaskProps(layer.id, { enabled: layer.mask.enabled === false });
16110
+ } else {
16111
+ controller.setActiveLayer(layer.id, "mask");
16112
+ }
16113
+ }
16114
+ function layerThumbnailClick(controller, layer, e) {
16115
+ if (!(e.ctrlKey || e.metaKey)) return false;
16116
+ controller.selectLayerTransparency(layer.id, { mode: selectionModeFromModifiers(e, "replace") });
16117
+ return true;
16118
+ }
14569
16119
  function layerPanelRows(layers, activeId) {
14570
16120
  const rows = [];
14571
16121
  const walk = (list, depth, parentId) => {
@@ -14637,6 +16187,7 @@ function layerMenu(controller) {
14637
16187
  const below = loc ? siblings[loc.index - 1] : void 0;
14638
16188
  const hasSel = controller.hasSelection;
14639
16189
  const isPixel = active?.type === "pixel";
16190
+ const hasOwnPixels = active?.type === "pixel" || active?.type === "shape" || active?.type === "type" || active?.type === "smart";
14640
16191
  return [
14641
16192
  { action: "new-layer", label: "New Layer", enabled: true },
14642
16193
  { action: "new-group", label: "New Group", enabled: true },
@@ -14651,6 +16202,9 @@ function layerMenu(controller) {
14651
16202
  { action: "via-cut", label: "Layer via Cut", enabled: isPixel && hasSel },
14652
16203
  { action: "add-mask", label: "Add Layer Mask", enabled: Boolean(active) },
14653
16204
  { action: "add-mask-selection", label: "Add Mask from Selection", enabled: Boolean(active) && hasSel },
16205
+ { action: "add-mask-hide-selection", label: "Add Mask Hiding Selection", enabled: Boolean(active) && hasSel },
16206
+ { action: "add-mask-transparency", label: "Add Mask from Transparency", enabled: hasOwnPixels },
16207
+ { action: "select-transparency", label: "Select Layer Pixels", enabled: hasOwnPixels },
14654
16208
  { action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
14655
16209
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
14656
16210
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
@@ -14702,6 +16256,15 @@ async function runLayerMenuAction(controller, action) {
14702
16256
  case "add-mask":
14703
16257
  if (id) controller.addLayerMask(id, "reveal-all");
14704
16258
  break;
16259
+ case "add-mask-hide-selection":
16260
+ if (id) controller.addLayerMask(id, "hide-selection");
16261
+ break;
16262
+ case "add-mask-transparency":
16263
+ if (id) controller.addLayerMask(id, "transparency");
16264
+ break;
16265
+ case "select-transparency":
16266
+ if (id) controller.selectLayerTransparency(id);
16267
+ break;
14705
16268
  case "add-mask-selection":
14706
16269
  if (id) controller.addLayerMask(id, "selection");
14707
16270
  break;
@@ -14886,6 +16449,7 @@ async function addLayerOfKind(controller, id, init) {
14886
16449
  if (isAdjustmentKind(kind)) {
14887
16450
  return controller.addAdjustmentLayer({
14888
16451
  kind,
16452
+ useSelection: true,
14889
16453
  ...init
14890
16454
  });
14891
16455
  }
@@ -15834,7 +17398,7 @@ function createImageEffectsToolProvider(controller, options = {}) {
15834
17398
  skill: "pixerate-image-editor",
15835
17399
  target: { noun: "layer", id: "A layer id from list_layers." },
15836
17400
  applyUses: ["filter"],
15837
- applyDetails: "The effect becomes a smart filter: it runs on the layer's pixels after the smart filters below it, with its own blend and opacity, under the smart filter mask, and can be changed or removed later. A pixel, shape or type layer (or a group) is converted to a smart object first, in the same undo step (one layer keeps its id; the result says converted). Generators and overlays go on their own layer through add_effect_layer.",
17401
+ applyDetails: "The effect becomes a smart filter: it runs on the layer's pixels after the smart filters below it, with its own blend and opacity, under the smart filter mask, and can be changed or removed later. A pixel, shape or type layer (or a group) is converted to a smart object first, in the same undo step (one layer keeps its id; the result says converted). With a selection, the filter mask is set from it in the same step, so the filter only changes the selected area (the result says masked); deselect first to filter the whole layer. Generators and overlays go on their own layer through add_effect_layer.",
15838
17402
  provider: () => controller.effectsProvider,
15839
17403
  readOnlyReason,
15840
17404
  list: (target) => {
@@ -15842,8 +17406,8 @@ function createImageEffectsToolProvider(controller, options = {}) {
15842
17406
  return layers.map((l) => ({ target: l.id, ...l.name ? { name: l.name } : {}, effects: imageLayerAppliedEffects(l) })).filter((t) => target || t.effects.length);
15843
17407
  },
15844
17408
  apply: async (target, effect, args) => {
15845
- const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity });
15846
- return result(r.layerId, r.filter.id, { converted: r.converted });
17409
+ const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity, useSelection: true });
17410
+ return result(r.layerId, r.filter.id, { converted: r.converted, masked: r.masked });
15847
17411
  },
15848
17412
  update: async (target, instance, changes) => {
15849
17413
  const layer = layerOf(target);
@@ -15974,6 +17538,8 @@ export {
15974
17538
  CANVAS_SIZE_PRESETS,
15975
17539
  CLONE_TRANSFORM_CONTROLS,
15976
17540
  COLOR_MODE_OPTIONS,
17541
+ COLOR_RANGE_PRESETS,
17542
+ COLOR_RANGE_PRESET_LABELS,
15977
17543
  CoverageMap,
15978
17544
  D50,
15979
17545
  D65,
@@ -15982,6 +17548,7 @@ export {
15982
17548
  DEFAULT_BLENDER,
15983
17549
  DEFAULT_BRUSH,
15984
17550
  DEFAULT_CANVAS_VIEWPORT,
17551
+ DEFAULT_CHANNEL_COLOR,
15985
17552
  DEFAULT_DEPTH_MASK,
15986
17553
  DEFAULT_FLOW,
15987
17554
  DEFAULT_GLOBAL_LIGHT,
@@ -16030,6 +17597,7 @@ export {
16030
17597
  HIDE,
16031
17598
  HISTOGRAM_BINS,
16032
17599
  HUE_RANGE_LABELS,
17600
+ HUE_RANGE_PRESETS,
16033
17601
  IDENTITY_CLONE_TRANSFORM,
16034
17602
  IDENTITY_MATRIX3,
16035
17603
  IMAGE_ADJUSTMENT_CONTROLS,
@@ -16093,6 +17661,7 @@ export {
16093
17661
  PARAGRAPH_STYLE_FIELDS,
16094
17662
  PARAM,
16095
17663
  PARAM_FLOATS,
17664
+ PASTE_MODES,
16096
17665
  PATTERN_BLOCK_KEYS,
16097
17666
  PIGMENT_BANDS,
16098
17667
  PIGMENT_CHANNELS,
@@ -16124,6 +17693,7 @@ export {
16124
17693
  PatternPlacement,
16125
17694
  PatternTile,
16126
17695
  PsdError,
17696
+ QUICK_MASK_TARGET,
16127
17697
  REFINE_EDGE_CONTROLS,
16128
17698
  RETOUCH_CHOICES,
16129
17699
  RETOUCH_OPTION_CONTROLS,
@@ -16281,6 +17851,7 @@ export {
16281
17851
  canvasToImagePoint,
16282
17852
  caretAt,
16283
17853
  changedTiles,
17854
+ channelThumbnail,
16284
17855
  checkEffectAnimation,
16285
17856
  checkEffectTime,
16286
17857
  checkImageEffectPlace,
@@ -16296,6 +17867,7 @@ export {
16296
17867
  colorModePlanes16,
16297
17868
  colorPanelView,
16298
17869
  colorRange,
17870
+ colorRangePreset,
16299
17871
  combineSelections,
16300
17872
  compileAdjustmentLayer,
16301
17873
  componentPath,
@@ -16378,10 +17950,12 @@ export {
16378
17950
  depthMaskAlpha,
16379
17951
  depthOf,
16380
17952
  depthRasterMask,
17953
+ describeChannel,
16381
17954
  describeLayer,
16382
17955
  describeLayerEffects,
16383
17956
  describeSelection,
16384
17957
  describeTransform,
17958
+ deserializeChannels,
16385
17959
  deserializeColorMode,
16386
17960
  deserializeColorProfile,
16387
17961
  deserializeEffects,
@@ -16468,9 +18042,12 @@ export {
16468
18042
  grainHeights,
16469
18043
  greyMorphology,
16470
18044
  groupLayers,
18045
+ growSelection,
16471
18046
  guidePlanes,
16472
18047
  guidedFilterColor,
16473
18048
  handlePaintShortcut,
18049
+ handleQuickMaskKey,
18050
+ handleSelectionShortcut,
16474
18051
  hasActiveEffects,
16475
18052
  hasLayersBelow,
16476
18053
  hasResampledLegacyMask,
@@ -16515,6 +18092,7 @@ export {
16515
18092
  isBitDepth,
16516
18093
  isBlendMode,
16517
18094
  isBrushstrokeBrush,
18095
+ isColorRangePreset,
16518
18096
  isContentsDecoded,
16519
18097
  isDefaultDepthMask,
16520
18098
  isGradientPaint,
@@ -16561,6 +18139,7 @@ export {
16561
18139
  layerName,
16562
18140
  layerPanelRows,
16563
18141
  layerPanels,
18142
+ layerThumbnailClick,
16564
18143
  layerTree,
16565
18144
  layerTypeLabel,
16566
18145
  layerWarning,
@@ -16589,6 +18168,7 @@ export {
16589
18168
  mapSubpaths,
16590
18169
  maskBounds,
16591
18170
  maskSummary,
18171
+ maskThumbnailClick,
16592
18172
  matrixFromCorners,
16593
18173
  matrixFromParams,
16594
18174
  measureCanvasViewport,
@@ -16784,14 +18364,17 @@ export {
16784
18364
  segmentToSelection,
16785
18365
  segmentationOutlines,
16786
18366
  segmentsToSelection,
18367
+ selectSimilar,
16787
18368
  selectionArea,
16788
18369
  selectionFromCoverage,
16789
18370
  selectionFromPixels,
16790
18371
  selectionModeFromModifiers,
16791
18372
  selectionOutline,
18373
+ selectionRing,
16792
18374
  selectionToCoverage,
16793
18375
  selectionToPixels,
16794
18376
  selectionValueAt,
18377
+ serializeChannels,
16795
18378
  serializeColorMode,
16796
18379
  serializeColorProfile,
16797
18380
  serializeContents,
@@ -16821,11 +18404,13 @@ export {
16821
18404
  shapeOutline,
16822
18405
  shapeRenderClip,
16823
18406
  shiftCoverage,
18407
+ skinToneRange,
16824
18408
  smartFilterRows,
16825
18409
  smartObjectLayers,
16826
18410
  smoothCoverage,
16827
18411
  smoothPoints,
16828
18412
  smudgeEffect,
18413
+ snapTo45,
16829
18414
  solidPaint,
16830
18415
  solidSurface,
16831
18416
  solveMembrane,