@pixerate/editor 0.182.0 → 0.185.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 (37) hide show
  1. package/dist/{chunk-UHMSMUHK.cjs → chunk-C6TRAAG4.cjs} +3 -1
  2. package/dist/{chunk-Y46OMERN.js → chunk-GYYSPMVU.js} +3 -1
  3. package/dist/image-editor/controller.d.cts +152 -8
  4. package/dist/image-editor/controller.d.ts +152 -8
  5. package/dist/image-editor/index.cjs +1986 -832
  6. package/dist/image-editor/index.js +1209 -55
  7. package/dist/image-editor/layers/panel.d.cts +23 -1
  8. package/dist/image-editor/layers/panel.d.ts +23 -1
  9. package/dist/image-editor/selection/gesture.d.cts +40 -2
  10. package/dist/image-editor/selection/gesture.d.ts +40 -2
  11. package/dist/image-editor/selection/index.d.cts +1 -0
  12. package/dist/image-editor/selection/index.d.ts +1 -0
  13. package/dist/image-editor/selection/modify.d.cts +3 -2
  14. package/dist/image-editor/selection/modify.d.ts +3 -2
  15. package/dist/image-editor/selection/stroke.d.cts +8 -0
  16. package/dist/image-editor/selection/stroke.d.ts +8 -0
  17. package/dist/image-editor/selection/tool-options.d.cts +12 -2
  18. package/dist/image-editor/selection/tool-options.d.ts +12 -2
  19. package/dist/image-editor/selection/wand.d.cts +37 -0
  20. package/dist/image-editor/selection/wand.d.ts +37 -0
  21. package/dist/image-editor/selection-tools.d.cts +8 -3
  22. package/dist/image-editor/selection-tools.d.ts +8 -3
  23. package/dist/image-editor/tools.d.cts +8 -2
  24. package/dist/image-editor/tools.d.ts +8 -2
  25. package/dist/image-editor/types.d.cts +2 -0
  26. package/dist/image-editor/types.d.ts +2 -0
  27. package/dist/paint-editor/index.cjs +6 -6
  28. package/dist/paint-editor/index.js +1 -1
  29. package/dist/vector/controller.d.cts +13 -0
  30. package/dist/vector/controller.d.ts +13 -0
  31. package/dist/vector/index.cjs +20 -1
  32. package/dist/vector/index.js +19 -0
  33. package/package.json +1 -1
  34. package/skills/README.md +7 -0
  35. package/skills/pixerate-image-editor/SKILL.md +41 -50
  36. package/skills/pixerate-image-editor/references/free-transform.md +22 -0
  37. package/skills/pixerate-image-selection-masking/SKILL.md +193 -0
@@ -370,7 +370,7 @@ import {
370
370
  writePsd,
371
371
  xyToTempTint,
372
372
  xyToUv
373
- } from "../chunk-Y46OMERN.js";
373
+ } from "../chunk-GYYSPMVU.js";
374
374
  import {
375
375
  parseCssColor
376
376
  } from "../chunk-3CI7FRMK.js";
@@ -1362,7 +1362,7 @@ var ImageEditorRenderer = class {
1362
1362
  }
1363
1363
  ctx.restore();
1364
1364
  }
1365
- const display = options.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
1365
+ const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
1366
1366
  if (state.selection?.bounds && display !== "none" || options.draftSelectionPath?.length) {
1367
1367
  ctx.save();
1368
1368
  try {
@@ -3065,7 +3065,7 @@ var TONE_RANGE_PRESETS = {
3065
3065
  function toneRange(pixels, range, falloff = 20) {
3066
3066
  const n = checkPixels(pixels);
3067
3067
  const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
3068
- const ramp = Math.max(1e-3, falloff);
3068
+ const ramp2 = Math.max(1e-3, falloff);
3069
3069
  const out = new Uint8Array(n);
3070
3070
  const d = pixels.data;
3071
3071
  for (let i = 0; i < n; i++) {
@@ -3073,7 +3073,7 @@ function toneRange(pixels, range, falloff = 20) {
3073
3073
  if (d[p + 3] <= 0) continue;
3074
3074
  const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
3075
3075
  const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
3076
- out[i] = coverageToByte(1 - outside / ramp);
3076
+ out[i] = coverageToByte(1 - outside / ramp2);
3077
3077
  }
3078
3078
  return createSelectionMask(pixels.width, pixels.height, out);
3079
3079
  }
@@ -3102,6 +3102,111 @@ function hueRange(pixels, center) {
3102
3102
  }
3103
3103
  return createSelectionMask(pixels.width, pixels.height, out);
3104
3104
  }
3105
+ var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
3106
+ var COLOR_RANGE_PRESETS = [
3107
+ ...Object.keys(HUE_RANGE_PRESETS),
3108
+ "highlights",
3109
+ "midtones",
3110
+ "shadows",
3111
+ "skinTones"
3112
+ ];
3113
+ function isColorRangePreset(v) {
3114
+ return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
3115
+ }
3116
+ 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;
3117
+ function skinToneRange(pixels) {
3118
+ const n = checkPixels(pixels);
3119
+ const out = new Uint8Array(n);
3120
+ const d = pixels.data;
3121
+ for (let i = 0; i < n; i++) {
3122
+ const p = i * 4;
3123
+ if (d[p + 3] <= 0) continue;
3124
+ const r = d[p] / 255;
3125
+ const g = d[p + 1] / 255;
3126
+ const b = d[p + 2] / 255;
3127
+ const mx = Math.max(r, g, b);
3128
+ const mn = Math.min(r, g, b);
3129
+ const chroma = mx - mn;
3130
+ if (chroma <= 0 || mx <= 0) continue;
3131
+ let hue;
3132
+ if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
3133
+ else if (mx === g) hue = (b - r) / chroma + 2;
3134
+ else hue = (r - g) / chroma + 4;
3135
+ hue *= 60;
3136
+ if (hue > 180) hue -= 360;
3137
+ const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
3138
+ out[i] = coverageToByte(k);
3139
+ }
3140
+ return createSelectionMask(pixels.width, pixels.height, out);
3141
+ }
3142
+ function colorRangePreset(pixels, preset, options = {}) {
3143
+ if (preset === "skinTones") return skinToneRange(pixels);
3144
+ if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
3145
+ if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
3146
+ throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
3147
+ }
3148
+ function selectedColorBins(pixels, sel, tolerance) {
3149
+ const S = 64;
3150
+ let bins = new Uint8Array(S * S * S);
3151
+ const d = pixels.data;
3152
+ const n = pixels.width * pixels.height;
3153
+ for (let i = 0; i < n; i++) {
3154
+ if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
3155
+ bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
3156
+ }
3157
+ const r = Math.round(Math.max(0, tolerance) / 4);
3158
+ if (r === 0) return bins;
3159
+ for (const stride of [1, S, S * S]) {
3160
+ const out = new Uint8Array(bins.length);
3161
+ for (let i = 0; i < bins.length; i++) {
3162
+ if (!bins[i]) continue;
3163
+ const c = Math.floor(i / stride) % S;
3164
+ const lo = Math.max(0, c - r);
3165
+ const hi = Math.min(S - 1, c + r);
3166
+ const base = i - c * stride;
3167
+ for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
3168
+ }
3169
+ bins = out;
3170
+ }
3171
+ return bins;
3172
+ }
3173
+ function matcher(pixels, sel, options) {
3174
+ if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
3175
+ const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
3176
+ const d = pixels.data;
3177
+ 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;
3178
+ }
3179
+ function growSelection(pixels, sel, options = {}) {
3180
+ checkPixels(pixels);
3181
+ const match = matcher(pixels, sel, options);
3182
+ const { width: w, height: h } = pixels;
3183
+ const out = new Uint8Array(sel.data);
3184
+ const stack = [];
3185
+ for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
3186
+ while (stack.length) {
3187
+ const i = stack.pop();
3188
+ const x = i % w;
3189
+ const y = (i - x) / w;
3190
+ const visit = (j) => {
3191
+ if (out[j] < 255 && match(j)) {
3192
+ out[j] = 255;
3193
+ stack.push(j);
3194
+ }
3195
+ };
3196
+ if (x > 0) visit(i - 1);
3197
+ if (x < w - 1) visit(i + 1);
3198
+ if (y > 0) visit(i - w);
3199
+ if (y < h - 1) visit(i + w);
3200
+ }
3201
+ return createSelectionMask(w, h, out);
3202
+ }
3203
+ function selectSimilar(pixels, sel, options = {}) {
3204
+ const n = checkPixels(pixels);
3205
+ const match = matcher(pixels, sel, options);
3206
+ const out = new Uint8Array(sel.data);
3207
+ for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
3208
+ return createSelectionMask(pixels.width, pixels.height, out);
3209
+ }
3105
3210
 
3106
3211
  // src/image-editor/selection/segmentation.ts
3107
3212
  var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
@@ -3364,6 +3469,16 @@ function refineSelection(pixels, mask, params = {}) {
3364
3469
  return createSelectionMask(mask.width, mask.height, out);
3365
3470
  }
3366
3471
 
3472
+ // src/image-editor/selection/stroke.ts
3473
+ function selectionRing(sel, width, location) {
3474
+ const half = location === "center" ? width / 2 : width;
3475
+ const outer = location === "inside" ? sel : modifySelection(sel, "expand", half);
3476
+ const inner = location === "outside" ? sel : modifySelection(sel, "contract", half);
3477
+ const out = new Uint8Array(sel.data.length);
3478
+ for (let i = 0; i < out.length; i++) out[i] = Math.max(0, outer.data[i] - inner.data[i]);
3479
+ return createSelectionMask(sel.width, sel.height, out);
3480
+ }
3481
+
3367
3482
  // src/image-editor/effect-layers.ts
3368
3483
  function hasLayersBelow(layers, id) {
3369
3484
  const all = flattenLayers(layers);
@@ -4274,6 +4389,7 @@ var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
4274
4389
  var RETOUCH_TOOLS = ["clone", "heal", "spot-heal", "smudge", "dodge", "burn", "sponge", "blur", "sharpen", "history"];
4275
4390
  var isRetouchTool = (tool) => RETOUCH_TOOLS.includes(tool);
4276
4391
  var STROKE_TOOLS = ["brush", "eraser", "mixer", ...RETOUCH_TOOLS];
4392
+ var PASTE_MODES = ["new-layer", "in-place", "into", "outside"];
4277
4393
  function paintCoverageInto(base, coverage, apply, rects, depth = null) {
4278
4394
  const tiles = new Map(base.tiles);
4279
4395
  const done = /* @__PURE__ */ new Set();
@@ -4974,8 +5090,11 @@ var ImageEditorController = class {
4974
5090
  }
4975
5091
  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
5092
  const res = addSmartFilter(layers, id, filter, this.canvasSize, options.index);
4977
- this.commitLayers(res.layers);
4978
- return { layerId: id, filter: res.filter, converted };
5093
+ const masked = this.asOneStep(() => {
5094
+ this.commitLayers(res.layers);
5095
+ return this.maskFiltersWithSelection(id, options.useSelection);
5096
+ });
5097
+ return { layerId: id, filter: res.filter, converted, masked };
4979
5098
  }
4980
5099
  /**
4981
5100
  * Changes an effect smart filter: `values` merged over its own (`null` resets a control to the
@@ -5204,8 +5323,38 @@ var ImageEditorController = class {
5204
5323
  s.selectedAnnotationId = null;
5205
5324
  }
5206
5325
  }
5326
+ /** Set while `asOneStep` runs: the step is already recorded, so nested commands add none. */
5327
+ historyGrouped = false;
5328
+ /**
5329
+ * Runs `fn` as one undo step: history is recorded once, before it, and the commands it calls
5330
+ * record none. If `fn` throws, the step is dropped and the state goes back to before it, so a
5331
+ * failure never leaves half a change.
5332
+ */
5333
+ asOneStep(fn) {
5334
+ if (this.historyGrouped || this.isApplyingHistory) return fn();
5335
+ this.recordHistory();
5336
+ const entry = this.undoStack[this.undoStack.length - 1];
5337
+ this.historyGrouped = true;
5338
+ try {
5339
+ return fn();
5340
+ } catch (err) {
5341
+ if (this.undoStack[this.undoStack.length - 1] === entry) {
5342
+ this.undoStack.pop();
5343
+ this.isApplyingHistory = true;
5344
+ try {
5345
+ this.restoreHistoryEntry(entry);
5346
+ } finally {
5347
+ this.isApplyingHistory = false;
5348
+ }
5349
+ this.notify();
5350
+ }
5351
+ throw err;
5352
+ } finally {
5353
+ this.historyGrouped = false;
5354
+ }
5355
+ }
5207
5356
  recordHistory() {
5208
- if (this.isApplyingHistory) return;
5357
+ if (this.isApplyingHistory || this.historyGrouped) return;
5209
5358
  this.docRevision++;
5210
5359
  this.undoStack.push(this.captureHistoryEntry());
5211
5360
  if (this.undoStack.length > this.maxHistoryDepth) {
@@ -5851,16 +6000,23 @@ var ImageEditorController = class {
5851
6000
  return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };
5852
6001
  }
5853
6002
  /**
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
6003
+ * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black),
6004
+ * `selection` (the selection's coverage), `hide-selection` (its inverse) or `transparency`
6005
+ * (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
6006
  * target when the layer is active.
5857
6007
  */
5858
6008
  addLayerMask(id, from = "reveal-all", props = {}) {
5859
6009
  const layer = requireLayer(this.state.layers, id).layer;
5860
6010
  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.");
6011
+ if (from === "selection" || from === "hide-selection" || from === "transparency") {
6012
+ let sel;
6013
+ if (from === "transparency") {
6014
+ sel = this.layerTransparency(id);
6015
+ } else {
6016
+ const current = this.state.selection;
6017
+ if (!current?.bounds) throw new LayerOperationError("Nothing is selected; select first, or use reveal-all / hide-all / transparency.");
6018
+ sel = from === "hide-selection" ? invertSelection(current) : current;
6019
+ }
5864
6020
  mask = this.maskFromSelection(sel, props);
5865
6021
  if (layer.type === "pixel" && (layer.offset.x || layer.offset.y)) {
5866
6022
  mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };
@@ -6314,10 +6470,22 @@ var ImageEditorController = class {
6314
6470
  return res.layer.id;
6315
6471
  }
6316
6472
  /** 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) {
6473
+ addSmartFilter(id, filter, index, options = {}) {
6318
6474
  const res = addSmartFilter(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);
6319
- this.commitLayers(res.layers);
6320
- return res.filter;
6475
+ return this.asOneStep(() => {
6476
+ this.commitLayers(res.layers);
6477
+ this.maskFiltersWithSelection(id, options.useSelection);
6478
+ return res.filter;
6479
+ });
6480
+ }
6481
+ /**
6482
+ * After adding a smart filter: with `useSelection` and a selection, sets the filter mask from
6483
+ * it in the same undo step (Photoshop limits a filter to the selection). Returns whether it did.
6484
+ */
6485
+ maskFiltersWithSelection(id, useSelection) {
6486
+ if (!useSelection || !this.state.selection?.bounds) return false;
6487
+ this.setSmartFilterMask(id, "selection", {}, false);
6488
+ return true;
6321
6489
  }
6322
6490
  /** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */
6323
6491
  updateSmartFilter(id, filterId, changes, commitToHistory = true) {
@@ -7145,13 +7313,176 @@ var ImageEditorController = class {
7145
7313
  clearPixels(options = {}) {
7146
7314
  return this.fillOrClear(options, "erase");
7147
7315
  }
7148
- fillOrClear(options, mode) {
7149
- const target = this.paintTarget(options.layerId, options.channel, mode === "paint" ? "fill" : "clear");
7316
+ /**
7317
+ * Edit › Stroke: paints a band `width` px wide along the selection's edge, `inside` it, on the
7318
+ * `center` of the edge or `outside` it (default), with `color` (default the foreground),
7319
+ * `opacity` and `blend`, on a layer's pixels or mask. One undo step. Returns the changed bounds.
7320
+ */
7321
+ strokeSelection(options) {
7322
+ const sel = this.state.selection;
7323
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then stroke the selection.");
7324
+ const width = options.width;
7325
+ if (!(width > 0 && width <= 250)) throw new LayerOperationError("width must be greater than 0 and at most 250 px.");
7326
+ const location = options.location ?? "outside";
7327
+ if (location !== "inside" && location !== "center" && location !== "outside") {
7328
+ throw new LayerOperationError(`Unknown location "${String(location)}"; use inside, center or outside.`);
7329
+ }
7330
+ const ring = selectionRing(sel, width, location);
7331
+ if (!ring.bounds) return { layerId: options.layerId ?? this.state.activeLayerId ?? "", channel: "pixels", bounds: null, dabs: 0 };
7332
+ return this.fillOrClear(options, "paint", ring);
7333
+ }
7334
+ /**
7335
+ * Image › Crop to the selection's bounds (one undo step). Returns the crop box (natural
7336
+ * pixels). Throws without a selection.
7337
+ */
7338
+ cropToSelection() {
7339
+ const sel = this.state.selection;
7340
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select the area to keep, then crop to it.");
7341
+ const crop = { ...sel.bounds };
7342
+ this.setCrop(crop);
7343
+ return crop;
7344
+ }
7345
+ /**
7346
+ * Edit › Content-Aware Fill: fills the selection with texture synthesised from around it
7347
+ * (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
7348
+ * coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
7349
+ * above it. Samples within `sampleRadius` px of the selection (default its larger side, at
7350
+ * least 32). Deterministic for a `seed`. One undo step. 8-bit documents only for now.
7351
+ */
7352
+ contentAwareFill(options = {}) {
7353
+ const sel = this.state.selection;
7354
+ if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
7355
+ 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.");
7356
+ const output = options.output ?? "current";
7357
+ if (output !== "current" && output !== "new-layer") throw new LayerOperationError(`Unknown output "${String(output)}"; use current or new-layer.`);
7358
+ const target = this.paintTarget(options.layerId, "pixels", "fill");
7359
+ const { width, height } = this.naturalSize;
7360
+ const b = sel.bounds;
7361
+ const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
7362
+ const x0 = Math.max(0, b.x - margin);
7363
+ const y0 = Math.max(0, b.y - margin);
7364
+ 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 };
7365
+ 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.");
7366
+ const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
7367
+ const before = readRegion(target.surface, layerRect);
7368
+ const img = new Float32Array(before.length);
7369
+ for (let i = 0; i < before.length; i++) img[i] = before[i];
7370
+ const coverage = new Float32Array(win.width * win.height);
7371
+ const hole = new Uint8Array(win.width * win.height);
7372
+ for (let y = 0; y < win.height; y++) {
7373
+ for (let x = 0; x < win.width; x++) {
7374
+ const c = sel.data[(win.y + y) * sel.width + win.x + x] / 255;
7375
+ coverage[y * win.width + x] = c;
7376
+ hole[y * win.width + x] = c > 0 ? 1 : 0;
7377
+ }
7378
+ }
7379
+ const filled = contentAwareFill(win.width, win.height, 4, img, hole, { seed: options.seed ?? 0 });
7380
+ const out = new Uint8ClampedArray(before.length);
7381
+ for (let i = 0; i < coverage.length; i++) {
7382
+ const k = coverage[i];
7383
+ for (let c = 0; c < 4; c++) {
7384
+ const p = i * 4 + c;
7385
+ out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
7386
+ }
7387
+ }
7388
+ if (output === "new-layer") {
7389
+ const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
7390
+ const data = new Uint8ClampedArray(b.width * b.height * 4);
7391
+ 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);
7392
+ 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 });
7393
+ return { layerId: id, bounds: { ...b }, output };
7394
+ }
7395
+ const surface = writeRegion(target.surface, layerRect, out);
7396
+ this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
7397
+ return { layerId: target.id, bounds: { ...b }, output };
7398
+ }
7399
+ /** The pixel clipboard (Copy / Cut), or null. */
7400
+ clipboard = null;
7401
+ /** What Copy or Cut last put on the editor's clipboard, or null. */
7402
+ getClipboard() {
7403
+ return this.clipboard;
7404
+ }
7405
+ /** Puts pixels on the editor's clipboard (e.g. an image pasted from the system clipboard). */
7406
+ setClipboard(image) {
7407
+ if (image && (image.data.length !== image.width * image.height * 4 || image.width < 1 || image.height < 1)) {
7408
+ throw new LayerOperationError("Clipboard pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
7409
+ }
7410
+ this.clipboard = image;
7411
+ }
7412
+ /**
7413
+ * Edit › Copy (or Copy Merged with `merged`): the selected pixels of a pixel layer (default the
7414
+ * active one) or of the whole visible image, cropped to the selection's bounds, with the
7415
+ * selection's soft edge in their alpha. Without a selection, the whole canvas. Not an undo step.
7416
+ */
7417
+ copyPixels(options = {}) {
7418
+ const { width, height } = this.naturalSize;
7419
+ const sel = this.state.selection?.bounds ? this.state.selection : null;
7420
+ const rect = sel?.bounds ?? { x: 0, y: 0, width, height };
7421
+ let data;
7422
+ if (options.merged) {
7423
+ data = new Uint8ClampedArray(this.compositePixels(rect).data);
7424
+ } else {
7425
+ const id = options.layerId ?? this.state.activeLayerId;
7426
+ if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
7427
+ const layer = requireLayer(this.state.layers, id).layer;
7428
+ if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is not a pixel layer; copy merged, or rasterize it first.`);
7429
+ data = readRegion(this.surfaceOf(layer), { x: rect.x - layer.offset.x, y: rect.y - layer.offset.y, width: rect.width, height: rect.height });
7430
+ }
7431
+ if (sel) {
7432
+ for (let y = 0; y < rect.height; y++) {
7433
+ for (let x = 0; x < rect.width; x++) {
7434
+ const k = sel.data[(rect.y + y) * sel.width + rect.x + x];
7435
+ const p = (y * rect.width + x) * 4 + 3;
7436
+ data[p] = Math.round(data[p] * k / 255);
7437
+ }
7438
+ }
7439
+ }
7440
+ this.clipboard = { width: rect.width, height: rect.height, x: rect.x, y: rect.y, data };
7441
+ return this.clipboard;
7442
+ }
7443
+ /** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
7444
+ cutPixels(options = {}) {
7445
+ return this.asOneStep(() => {
7446
+ const clip = this.copyPixels({ layerId: options.layerId });
7447
+ this.clearPixels({ layerId: options.layerId, channel: "pixels" });
7448
+ return clip;
7449
+ });
7450
+ }
7451
+ /**
7452
+ * Edit › Paste as a new pixel layer above the active one (one undo step): `new-layer` centred
7453
+ * on the selection (or the canvas), `in-place` where it was copied, `into` centred in the
7454
+ * selection with the selection as its mask (Paste Into), or `outside` with the inverse
7455
+ * (Paste Outside). Returns the new layer id and where it went.
7456
+ */
7457
+ pastePixels(mode = "new-layer", image = this.clipboard) {
7458
+ if (!image) throw new LayerOperationError("The clipboard is empty; copy_pixels first.");
7459
+ if (!PASTE_MODES.includes(mode)) throw new LayerOperationError(`Unknown paste mode "${String(mode)}"; use ${PASTE_MODES.join(", ")}.`);
7460
+ const sel = this.state.selection?.bounds ? this.state.selection : null;
7461
+ if ((mode === "into" || mode === "outside") && !sel) throw new LayerOperationError(`Paste ${mode === "into" ? "Into" : "Outside"} needs a selection; select where it goes first.`);
7462
+ const { width, height } = this.naturalSize;
7463
+ let x;
7464
+ let y;
7465
+ if (mode === "in-place" && image.x !== void 0 && image.y !== void 0) {
7466
+ x = image.x;
7467
+ y = image.y;
7468
+ } else {
7469
+ const frame = sel?.bounds ?? { x: 0, y: 0, width, height };
7470
+ x = Math.round(frame.x + (frame.width - image.width) / 2);
7471
+ y = Math.round(frame.y + (frame.height - image.height) / 2);
7472
+ }
7473
+ return this.asOneStep(() => {
7474
+ const id = this.addPixelLayer({ name: "Pasted", pixels: { width: image.width, height: image.height, data: new Uint8ClampedArray(image.data) }, x, y });
7475
+ if (mode === "into" || mode === "outside") this.addLayerMask(id, mode === "into" ? "selection" : "hide-selection");
7476
+ return { layerId: id, bounds: { x, y, width: image.width, height: image.height }, masked: mode === "into" || mode === "outside" };
7477
+ });
7478
+ }
7479
+ fillOrClear(options, mode, coverageMask) {
7480
+ const target = this.paintTarget(options.layerId, options.channel, mode === "paint" ? coverageMask ? "stroke" : "fill" : "clear");
7150
7481
  const { width, height } = this.naturalSize;
7151
7482
  const opacity = options.opacity ?? 1;
7152
7483
  if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
7153
7484
  const blend = parseBlend(options.blend) ?? "normal";
7154
- const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;
7485
+ const sel = coverageMask ?? (options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null);
7155
7486
  const region = sel?.bounds ?? { x: 0, y: 0, width, height };
7156
7487
  const coverage = new CoverageMap();
7157
7488
  const ones = new Float32Array(region.width * region.height);
@@ -7325,7 +7656,7 @@ var ImageEditorController = class {
7325
7656
  enabled: spec.enabled,
7326
7657
  opacity: spec.opacity,
7327
7658
  blend: spec.blend,
7328
- mask: spec.mask ?? void 0
7659
+ mask: spec.mask ?? (spec.useSelection && this.state.selection?.bounds ? layerMaskFromSelection(this.state.selection) : void 0)
7329
7660
  });
7330
7661
  while (ids.has(layer.id)) layer.id = newLayerId("adj");
7331
7662
  const { width, height } = this.naturalSize;
@@ -7535,6 +7866,7 @@ var ImageEditorController = class {
7535
7866
  const prev = normalizeSelection(this.state.selection ?? null);
7536
7867
  if (prev === next || prev && next && sameSelection(prev, next)) return false;
7537
7868
  this.recordHistory();
7869
+ if (!next && prev?.bounds) this.lastSelection = prev;
7538
7870
  this.state.selection = next;
7539
7871
  this.notify();
7540
7872
  return true;
@@ -7548,6 +7880,74 @@ var ImageEditorController = class {
7548
7880
  deselect() {
7549
7881
  return this.setSelection(null);
7550
7882
  }
7883
+ /** The selection a Deselect cleared, for `reselect` (not saved, not part of history). */
7884
+ lastSelection = null;
7885
+ /** Whether `reselect` has a selection to bring back. */
7886
+ canReselect() {
7887
+ const { width, height } = this.naturalSize;
7888
+ return Boolean(this.lastSelection && this.lastSelection.width === width && this.lastSelection.height === height);
7889
+ }
7890
+ /** Select › Reselect: brings back the last selection a Deselect cleared, as one undo step. */
7891
+ reselect() {
7892
+ if (!this.canReselect()) return false;
7893
+ return this.setSelection(this.lastSelection);
7894
+ }
7895
+ /**
7896
+ * Single Row / Single Column Marquee: a 1 px line across the whole image at `position`
7897
+ * (natural pixels; y for a row, x for a column).
7898
+ */
7899
+ selectLine(orientation, position, options = {}) {
7900
+ const { width, height } = this.naturalSize;
7901
+ const max = orientation === "row" ? height : width;
7902
+ const at = Math.floor(position);
7903
+ if (!Number.isFinite(position) || at < 0 || at >= max) {
7904
+ throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
7905
+ }
7906
+ const rect = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
7907
+ return this.shapeCommand(rectSelection(width, height, rect, { antiAlias: false }), options);
7908
+ }
7909
+ /** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
7910
+ async growSelection(options = {}) {
7911
+ const sel = this.state.selection;
7912
+ if (!sel?.bounds) return false;
7913
+ const pixels = await this.requirePixels(options);
7914
+ return this.setSelection(growSelection(pixels, sel, options));
7915
+ }
7916
+ /** Select › Similar: adds every pixel within `tolerance` of a selected colour (one undo step). */
7917
+ async selectSimilar(options = {}) {
7918
+ const sel = this.state.selection;
7919
+ if (!sel?.bounds) return false;
7920
+ const pixels = await this.requirePixels(options);
7921
+ return this.setSelection(selectSimilar(pixels, sel, options));
7922
+ }
7923
+ /**
7924
+ * Load Selection from a layer's transparency (Ctrl/⌘-click its thumbnail): its alpha in
7925
+ * document space, combined by `mode`. Works on pixel, shape, type and smart object layers.
7926
+ */
7927
+ selectLayerTransparency(id, options = {}) {
7928
+ const mask = this.layerTransparency(id);
7929
+ return this.shapeCommand(mask, options);
7930
+ }
7931
+ layerTransparency(id) {
7932
+ const layer = requireLayer(this.state.layers, id).layer;
7933
+ if (!hasSurface(layer)) {
7934
+ throw new LayerOperationError(`Layer "${layer.name || id}" has no pixels of its own; only pixel, shape, type and smart object layers do.`);
7935
+ }
7936
+ const { width, height } = this.naturalSize;
7937
+ const out = new Uint8Array(width * height);
7938
+ const pixels = this.getLayerPixels(id);
7939
+ if (!pixels) return createSelectionMask(width, height, out);
7940
+ for (let y = 0; y < pixels.height; y++) {
7941
+ const dy = y + pixels.y;
7942
+ if (dy < 0 || dy >= height) continue;
7943
+ for (let x = 0; x < pixels.width; x++) {
7944
+ const dx = x + pixels.x;
7945
+ if (dx < 0 || dx >= width) continue;
7946
+ out[dy * width + dx] = pixels.data[(y * pixels.width + x) * 4 + 3];
7947
+ }
7948
+ }
7949
+ return createSelectionMask(width, height, out);
7950
+ }
7551
7951
  /** Select › Inverse (with nothing selected, selects everything). */
7552
7952
  invertSelection() {
7553
7953
  const { width, height } = this.naturalSize;
@@ -7555,7 +7955,8 @@ var ImageEditorController = class {
7555
7955
  return this.setSelection(invertSelection(current));
7556
7956
  }
7557
7957
  shapeCommand(mask, options) {
7558
- const shaped = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
7958
+ const feathered = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
7959
+ const shaped = options.invert ? invertSelection(feathered) : feathered;
7559
7960
  return this.setSelection(shaped, options.mode ?? "replace");
7560
7961
  }
7561
7962
  /** Rectangular marquee in natural image pixels. */
@@ -7657,6 +8058,15 @@ var ImageEditorController = class {
7657
8058
  this.shapeCommand(colorRange(pixels, samples, options), options);
7658
8059
  return this.getSelection();
7659
8060
  }
8061
+ /**
8062
+ * Select › Color Range with a preset: `reds` … `magentas`, `highlights` / `midtones` /
8063
+ * `shadows` (with `falloff` levels, default 20) or `skinTones`. One undo step.
8064
+ */
8065
+ async selectColorRangePreset(preset, options = {}) {
8066
+ const pixels = await this.requirePixels(options);
8067
+ this.shapeCommand(colorRangePreset(pixels, preset, options), options);
8068
+ return this.getSelection();
8069
+ }
7660
8070
  /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
7661
8071
  modifySelection(op, radius) {
7662
8072
  const sel = this.state.selection;
@@ -7733,8 +8143,23 @@ var ImageEditorController = class {
7733
8143
  }
7734
8144
  /** Quick Mask view (tints the unselected area). View state: no undo step. */
7735
8145
  setQuickMask(on) {
7736
- if (Boolean(this.state.quickMask) === on) return;
7737
- this.state.quickMask = on;
8146
+ this.setSelectionDisplay(on ? "quickMask" : "ants");
8147
+ }
8148
+ /** How the canvas shows the selection (`state.selectionDisplay`). */
8149
+ getSelectionDisplay() {
8150
+ return this.state.selectionDisplay ?? (this.state.quickMask ? "quickMask" : "ants");
8151
+ }
8152
+ /**
8153
+ * Shows the selection as marching ants, as a Quick Mask tint over the unselected area, or hides
8154
+ * it (the selection still applies). View state: no undo step, not saved.
8155
+ */
8156
+ setSelectionDisplay(display) {
8157
+ if (display !== "ants" && display !== "quickMask" && display !== "none") {
8158
+ throw new Error(`Unknown selection display "${String(display)}". Use ants, quickMask or none.`);
8159
+ }
8160
+ if (this.getSelectionDisplay() === display) return;
8161
+ this.state.selectionDisplay = display;
8162
+ this.state.quickMask = display === "quickMask";
7738
8163
  this.notify();
7739
8164
  }
7740
8165
  // --- Layer masks ---
@@ -7778,8 +8203,11 @@ var ImageEditorController = class {
7778
8203
  if (!layer || layer.type !== "adjustment" || !layer.mask) return false;
7779
8204
  return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);
7780
8205
  }
7781
- /** Load Selection from a layer mask (its pixels; density and feather are not applied). */
7782
- selectionFromLayerMask(layerId, mode = "replace") {
8206
+ /**
8207
+ * Load Selection from a layer mask (its pixels; density and feather are not applied), inverted
8208
+ * with `invert`. False without a painted mask (a depth mask is computed, not painted).
8209
+ */
8210
+ selectionFromLayerMask(layerId, mode = "replace", invert2 = false) {
7783
8211
  const layer = this.getLayer(layerId);
7784
8212
  if (!layer?.mask || layer.mask.source) return false;
7785
8213
  const { width, height } = this.naturalSize;
@@ -7787,7 +8215,7 @@ var ImageEditorController = class {
7787
8215
  const oy = layer.type === "pixel" ? layer.offset.y : 0;
7788
8216
  const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });
7789
8217
  const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));
7790
- return this.setSelection(sel, mode);
8218
+ return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
7791
8219
  }
7792
8220
  // --- Histogram ---
7793
8221
  /**
@@ -10121,9 +10549,9 @@ var DEPTH_MASK_CONTROLS = [
10121
10549
  // src/image-editor/playback.ts
10122
10550
  var IMAGE_PLAYBACK_KIND = "layer";
10123
10551
  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)$/;
10552
+ 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)$/;
10125
10553
  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)$/;
10554
+ var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
10127
10555
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
10128
10556
  function imagePlaybackPanel(tool) {
10129
10557
  if (ADJUST.test(tool)) return "adjust";
@@ -10238,8 +10666,24 @@ var SELECTION_TOOLS = [
10238
10666
  { id: "wand", label: "Magic Wand" },
10239
10667
  { id: "rect", label: "Rectangle" },
10240
10668
  { id: "ellipse", label: "Ellipse" },
10241
- { id: "lasso", label: "Lasso" }
10669
+ { id: "row", label: "Single Row" },
10670
+ { id: "column", label: "Single Column" },
10671
+ { id: "lasso", label: "Lasso" },
10672
+ { id: "polygon", label: "Polygonal Lasso" },
10673
+ { id: "range", label: "Color Range" }
10242
10674
  ];
10675
+ var COLOR_RANGE_PRESET_LABELS = {
10676
+ reds: "Reds",
10677
+ yellows: "Yellows",
10678
+ greens: "Greens",
10679
+ cyans: "Cyans",
10680
+ blues: "Blues",
10681
+ magentas: "Magentas",
10682
+ highlights: "Highlights",
10683
+ midtones: "Midtones",
10684
+ shadows: "Shadows",
10685
+ skinTones: "Skin Tones"
10686
+ };
10243
10687
  var SELECTION_MODE_LABELS = {
10244
10688
  replace: "New",
10245
10689
  add: "Add",
@@ -10253,7 +10697,11 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
10253
10697
  contiguous: true,
10254
10698
  antiAlias: true,
10255
10699
  sampleMerged: false,
10256
- distance: "rgb"
10700
+ distance: "rgb",
10701
+ feather: 0,
10702
+ fuzziness: 40,
10703
+ localized: 0,
10704
+ rangeInvert: false
10257
10705
  };
10258
10706
  function selectionModeFromModifiers(modifiers, fallback) {
10259
10707
  if (modifiers.shiftKey && modifiers.altKey) return "intersect";
@@ -10284,6 +10732,41 @@ var modeSchema = {
10284
10732
  description: "How the result combines with the current selection (default replace)."
10285
10733
  };
10286
10734
  var num2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
10735
+ var featherSchema = { type: "number", minimum: 0, maximum: 1e3, description: "Feather the new shape by this many pixels before combining (default 0)." };
10736
+ var antiAliasSchema = { type: "boolean", description: "Soft one-pixel edge (default true)." };
10737
+ var sampleMergedSchema = { type: "boolean", description: "Sample the adjusted image (adjustment layers and filters applied) rather than the pixels as painted (default false)." };
10738
+ var sampleSchema = {
10739
+ type: "string",
10740
+ enum: ["composite", "active-layer"],
10741
+ description: "composite (default): all layers; active-layer: only the active pixel layer (see set_active_layer)."
10742
+ };
10743
+ var pointSchema = {
10744
+ type: "object",
10745
+ properties: { x: { type: "number", description: "x in natural image pixels." }, y: { type: "number", description: "y in natural image pixels." } },
10746
+ required: ["x", "y"],
10747
+ additionalProperties: false
10748
+ };
10749
+ var rectProperties = {
10750
+ x: { type: "number", description: "Left edge in natural image pixels." },
10751
+ y: { type: "number", description: "Top edge in natural image pixels." },
10752
+ width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
10753
+ height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
10754
+ };
10755
+ function shapeOptions(args) {
10756
+ return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
10757
+ }
10758
+ function sampleOptions(args) {
10759
+ if (args.sample !== void 0 && args.sample !== "composite" && args.sample !== "active-layer") {
10760
+ throw new Error(`Unknown sample "${String(args.sample)}". Use composite or active-layer.`);
10761
+ }
10762
+ return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
10763
+ }
10764
+ function rectOf(args) {
10765
+ const rect = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
10766
+ 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.");
10767
+ if (rect.width <= 0 || rect.height <= 0) throw new Error("width and height must be greater than 0.");
10768
+ return rect;
10769
+ }
10287
10770
  function summary(controller) {
10288
10771
  const sel = controller.getSelection();
10289
10772
  const { width, height } = controller.getState().imageDimensions;
@@ -10316,7 +10799,7 @@ function createImageSelectionTools(controller) {
10316
10799
  const selectRegion = {
10317
10800
  name: "select_region",
10318
10801
  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.',
10802
+ 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
10803
  inputSchema: {
10321
10804
  type: "object",
10322
10805
  properties: {
@@ -10382,7 +10865,7 @@ function createImageSelectionTools(controller) {
10382
10865
  const selectByColor = {
10383
10866
  name: "select_by_color",
10384
10867
  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.',
10868
+ 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
10869
  inputSchema: {
10387
10870
  type: "object",
10388
10871
  properties: {
@@ -10422,7 +10905,7 @@ function createImageSelectionTools(controller) {
10422
10905
  const modify = {
10423
10906
  name: "modify_selection",
10424
10907
  title: "Modify the selection",
10425
- description: "Select \u203A Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.",
10908
+ 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
10909
  inputSchema: {
10427
10910
  type: "object",
10428
10911
  properties: {
@@ -10512,7 +10995,499 @@ function createImageSelectionTools(controller) {
10512
10995
  return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };
10513
10996
  }
10514
10997
  };
10515
- return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];
10998
+ const selectAll = {
10999
+ name: "select_all",
11000
+ title: "Select all",
11001
+ description: "Select \u203A All: selects the whole image. One undoable step. Returns the selection.",
11002
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11003
+ annotations: writeAnnotations("Select all", true),
11004
+ handler: () => {
11005
+ controller.selectAll();
11006
+ return summary(controller);
11007
+ }
11008
+ };
11009
+ const deselect = {
11010
+ name: "deselect",
11011
+ title: "Deselect",
11012
+ description: "Select \u203A Deselect: clears the selection, so later edits apply to the whole layer. One undoable step. Returns the (empty) selection.",
11013
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11014
+ annotations: writeAnnotations("Deselect", true),
11015
+ handler: () => {
11016
+ controller.deselect();
11017
+ return summary(controller);
11018
+ }
11019
+ };
11020
+ const invert2 = {
11021
+ name: "invert_selection",
11022
+ title: "Invert the selection",
11023
+ description: "Select \u203A Inverse: selects what was not selected (soft edges stay soft). With nothing selected, selects everything. One undoable step. Returns the selection.",
11024
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11025
+ annotations: writeAnnotations("Invert the selection"),
11026
+ handler: () => {
11027
+ controller.invertSelection();
11028
+ return summary(controller);
11029
+ }
11030
+ };
11031
+ const selectRect = {
11032
+ name: "select_rect",
11033
+ title: "Select a rectangle",
11034
+ 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.",
11035
+ inputSchema: {
11036
+ type: "object",
11037
+ properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
11038
+ required: ["x", "y", "width", "height"],
11039
+ additionalProperties: false
11040
+ },
11041
+ annotations: writeAnnotations("Select a rectangle", true),
11042
+ handler: (args) => {
11043
+ controller.selectRect(rectOf(args), shapeOptions(args));
11044
+ return summary(controller);
11045
+ }
11046
+ };
11047
+ const selectEllipse = {
11048
+ name: "select_ellipse",
11049
+ title: "Select an ellipse",
11050
+ 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.",
11051
+ inputSchema: {
11052
+ type: "object",
11053
+ properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
11054
+ required: ["x", "y", "width", "height"],
11055
+ additionalProperties: false
11056
+ },
11057
+ annotations: writeAnnotations("Select an ellipse", true),
11058
+ handler: (args) => {
11059
+ controller.selectEllipse(rectOf(args), shapeOptions(args));
11060
+ return summary(controller);
11061
+ }
11062
+ };
11063
+ const selectPolygonTool = {
11064
+ name: "select_polygon",
11065
+ title: "Select a polygon (lasso)",
11066
+ 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.",
11067
+ inputSchema: {
11068
+ type: "object",
11069
+ properties: {
11070
+ points: { type: "array", minItems: 3, items: pointSchema, description: "At least 3 vertices along the outline, in order, in natural image pixels." },
11071
+ mode: modeSchema,
11072
+ feather: featherSchema,
11073
+ antiAlias: antiAliasSchema
11074
+ },
11075
+ required: ["points"],
11076
+ additionalProperties: false
11077
+ },
11078
+ annotations: writeAnnotations("Select a polygon (lasso)", true),
11079
+ handler: (args) => {
11080
+ const list = Array.isArray(args.points) ? args.points.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : [];
11081
+ if (list.length < 3 || !list.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) {
11082
+ throw new Error("points needs at least 3 { x, y } vertices in natural image pixels.");
11083
+ }
11084
+ controller.selectPolygon(list, shapeOptions(args));
11085
+ return summary(controller);
11086
+ }
11087
+ };
11088
+ const magicWandTool = {
11089
+ name: "magic_wand",
11090
+ title: "Magic wand",
11091
+ 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.",
11092
+ inputSchema: {
11093
+ type: "object",
11094
+ properties: {
11095
+ x: { type: "number", description: "Seed x in natural image pixels." },
11096
+ y: { type: "number", description: "Seed y in natural image pixels." },
11097
+ tolerance: { type: "number", minimum: 0, maximum: 255, description: "Colour tolerance 0..255 (default 32)." },
11098
+ contiguous: { type: "boolean", description: "Only the region connected to the seed (default true); false selects every similar pixel." },
11099
+ distance: { type: "string", enum: ["rgb", "perceptual"], description: "How colour difference is measured: rgb (default, as Photoshop) or perceptual (CIE \u0394E)." },
11100
+ antiAlias: antiAliasSchema,
11101
+ sampleMerged: sampleMergedSchema,
11102
+ sample: sampleSchema,
11103
+ mode: modeSchema,
11104
+ feather: featherSchema
11105
+ },
11106
+ required: ["x", "y"],
11107
+ additionalProperties: false
11108
+ },
11109
+ annotations: writeAnnotations("Magic wand", true),
11110
+ handler: async (args) => {
11111
+ const x = num2(args.x, NaN);
11112
+ const y = num2(args.y, NaN);
11113
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("Pass the seed point x and y in natural image pixels.");
11114
+ const { width, height } = controller.getState().imageDimensions;
11115
+ if (x < 0 || y < 0 || x >= width || y >= height) throw new Error(`(${x}, ${y}) is outside the image (0..${width - 1}, 0..${height - 1}).`);
11116
+ if (args.distance !== void 0 && args.distance !== "rgb" && args.distance !== "perceptual") {
11117
+ throw new Error(`Unknown distance "${String(args.distance)}". Use rgb or perceptual.`);
11118
+ }
11119
+ await controller.magicWand(
11120
+ { x, y },
11121
+ {
11122
+ ...shapeOptions(args),
11123
+ ...sampleOptions(args),
11124
+ tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))),
11125
+ contiguous: args.contiguous !== false,
11126
+ distance: args.distance ?? "rgb"
11127
+ }
11128
+ );
11129
+ return summary(controller);
11130
+ }
11131
+ };
11132
+ const colorRangeTool = {
11133
+ name: "select_color_range",
11134
+ title: "Select a colour range",
11135
+ 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.",
11136
+ inputSchema: {
11137
+ type: "object",
11138
+ properties: {
11139
+ samples: {
11140
+ type: "array",
11141
+ minItems: 1,
11142
+ maxItems: 32,
11143
+ items: {
11144
+ type: "object",
11145
+ properties: {
11146
+ color: { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "The sampled colour, #rrggbb." },
11147
+ x: { type: "number", description: "Where it was picked (natural image pixels), for localized." },
11148
+ y: { type: "number", description: "Where it was picked (natural image pixels), for localized." }
11149
+ },
11150
+ required: ["color"],
11151
+ additionalProperties: false
11152
+ },
11153
+ description: "Colours to select (the + eyedropper adds one). Read colours from capture_view or a seed pixel."
11154
+ },
11155
+ preset: { type: "string", enum: [...COLOR_RANGE_PRESETS], description: "A preset range instead of samples." },
11156
+ fuzziness: {
11157
+ type: "number",
11158
+ minimum: 0,
11159
+ maximum: 200,
11160
+ 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."
11161
+ },
11162
+ 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)." },
11163
+ invert: { type: "boolean", description: "Select everything except the range (default false)." },
11164
+ sampleMerged: sampleMergedSchema,
11165
+ sample: sampleSchema,
11166
+ mode: modeSchema,
11167
+ feather: featherSchema
11168
+ },
11169
+ additionalProperties: false
11170
+ },
11171
+ annotations: writeAnnotations("Select a colour range", true),
11172
+ handler: async (args) => {
11173
+ const hasSamples = Array.isArray(args.samples) && args.samples.length > 0;
11174
+ if (hasSamples === (args.preset !== void 0)) throw new Error("Pass samples (colours to match) or preset, not both.");
11175
+ const common = { ...sampleOptions(args), mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), invert: args.invert === true };
11176
+ if (!hasSamples) {
11177
+ if (!isColorRangePreset(args.preset)) throw new Error(`Unknown preset "${String(args.preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
11178
+ await controller.selectColorRangePreset(args.preset, { ...common, falloff: num2(args.fuzziness, 20) });
11179
+ } else {
11180
+ const samples = args.samples.map((s, i) => {
11181
+ if (typeof s?.color !== "string" || !/^#?[0-9a-f]{6}$/i.test(s.color.trim())) throw new Error(`samples[${i}].color must be #rrggbb.`);
11182
+ const at = typeof s.x === "number" && typeof s.y === "number" ? { x: s.x, y: s.y } : void 0;
11183
+ return at ? { color: s.color, at } : { color: s.color };
11184
+ });
11185
+ const localized = typeof args.localized === "number" && args.localized > 0 ? args.localized : void 0;
11186
+ await controller.selectColorRange(samples, { ...common, fuzziness: num2(args.fuzziness, 40), localized });
11187
+ }
11188
+ return summary(controller);
11189
+ }
11190
+ };
11191
+ const loadFromImage = {
11192
+ name: "load_selection_from_image",
11193
+ title: "Load a selection from a mask image",
11194
+ 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.",
11195
+ inputSchema: {
11196
+ type: "object",
11197
+ properties: {
11198
+ image: { type: "string", minLength: 1, description: "The mask as a data URL (data:image/png;base64,\u2026) or an image URL." },
11199
+ channel: { type: "string", enum: ["luminance", "alpha", "red", "green", "blue"], description: "What to read as coverage (default luminance; alpha for a cutout PNG)." },
11200
+ threshold: { type: "number", minimum: 0, maximum: 255, description: "Values at or above this are selected, the rest not (default: keep soft values)." },
11201
+ invert: { type: "boolean", description: "Black = selected instead (default false)." },
11202
+ mode: modeSchema
11203
+ },
11204
+ required: ["image"],
11205
+ additionalProperties: false
11206
+ },
11207
+ annotations: { ...writeAnnotations("Load a selection from a mask image", true), openWorldHint: true },
11208
+ handler: async (args) => {
11209
+ if (typeof args.image !== "string" || !args.image.trim()) throw new Error("image must be a data URL or image URL.");
11210
+ const channel = args.channel ?? "luminance";
11211
+ if (!["luminance", "alpha", "red", "green", "blue"].includes(channel)) throw new Error(`Unknown channel "${String(channel)}". Use luminance, alpha, red, green or blue.`);
11212
+ await controller.loadSelectionFromImage(args.image, {
11213
+ channel,
11214
+ invert: args.invert === true,
11215
+ threshold: typeof args.threshold === "number" ? Math.min(255, Math.max(0, args.threshold)) : void 0,
11216
+ mode: parseSelectionMode(args.mode)
11217
+ });
11218
+ return summary(controller);
11219
+ }
11220
+ };
11221
+ const loadFromMask = {
11222
+ name: "load_selection_from_mask",
11223
+ title: "Load a selection from a layer mask",
11224
+ 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.",
11225
+ inputSchema: {
11226
+ type: "object",
11227
+ properties: {
11228
+ layerId: { type: "string", minLength: 1, description: "The layer whose mask to load (see list_layers)." },
11229
+ invert: { type: "boolean", description: "Select what the mask hides instead (default false)." },
11230
+ mode: modeSchema
11231
+ },
11232
+ required: ["layerId"],
11233
+ additionalProperties: false
11234
+ },
11235
+ annotations: writeAnnotations("Load a selection from a layer mask", true),
11236
+ handler: (args) => {
11237
+ const id = String(args.layerId ?? "");
11238
+ const layer = controller.getLayer(id);
11239
+ if (!layer) throw new Error(`No layer "${id}". Call list_layers for ids.`);
11240
+ if (!layer.mask) throw new Error(`Layer "${id}" has no mask. Add one with add_layer_mask, or pick a layer with a mask.`);
11241
+ 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.`);
11242
+ controller.selectionFromLayerMask(id, parseSelectionMode(args.mode), args.invert === true);
11243
+ return summary(controller);
11244
+ }
11245
+ };
11246
+ const display = {
11247
+ name: "set_selection_display",
11248
+ title: "Show or hide the selection",
11249
+ 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.",
11250
+ inputSchema: {
11251
+ type: "object",
11252
+ properties: { display: { type: "string", enum: ["ants", "quickMask", "none"], description: "ants, quickMask or none." } },
11253
+ required: ["display"],
11254
+ additionalProperties: false
11255
+ },
11256
+ annotations: writeAnnotations("Show or hide the selection", true),
11257
+ handler: (args) => {
11258
+ controller.setSelectionDisplay(args.display);
11259
+ return { display: controller.getSelectionDisplay(), selection: summary(controller) };
11260
+ }
11261
+ };
11262
+ const sampleTolerance = {
11263
+ tolerance: { type: "number", minimum: 0, maximum: 255, description: "Largest per-channel difference from a selected colour, 0..255 (default 32, as magic_wand)." },
11264
+ sampleMerged: sampleMergedSchema,
11265
+ sample: sampleSchema
11266
+ };
11267
+ const growTool = {
11268
+ name: "grow_selection",
11269
+ title: "Grow the selection by colour",
11270
+ 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.",
11271
+ inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
11272
+ annotations: writeAnnotations("Grow the selection by colour"),
11273
+ handler: async (args) => {
11274
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select part of the area first (magic_wand, select_rect).");
11275
+ await controller.growSelection({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
11276
+ return summary(controller);
11277
+ }
11278
+ };
11279
+ const similarTool = {
11280
+ name: "select_similar",
11281
+ title: "Select similar colours",
11282
+ 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.",
11283
+ inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
11284
+ annotations: writeAnnotations("Select similar colours"),
11285
+ handler: async (args) => {
11286
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select a sample of the colour first.");
11287
+ await controller.selectSimilar({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
11288
+ return summary(controller);
11289
+ }
11290
+ };
11291
+ const lineTool = {
11292
+ name: "select_line",
11293
+ title: "Select a single row or column",
11294
+ 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.",
11295
+ inputSchema: {
11296
+ type: "object",
11297
+ properties: {
11298
+ orientation: { type: "string", enum: ["row", "column"], description: "row (horizontal, at y) or column (vertical, at x)." },
11299
+ position: { type: "number", minimum: 0, description: "y of the row or x of the column, in natural image pixels." },
11300
+ mode: modeSchema
11301
+ },
11302
+ required: ["orientation", "position"],
11303
+ additionalProperties: false
11304
+ },
11305
+ annotations: writeAnnotations("Select a single row or column", true),
11306
+ handler: (args) => {
11307
+ if (args.orientation !== "row" && args.orientation !== "column") throw new Error("orientation must be row or column.");
11308
+ controller.selectLine(args.orientation, num2(args.position, NaN), { mode: parseSelectionMode(args.mode) });
11309
+ return summary(controller);
11310
+ }
11311
+ };
11312
+ const reselectTool = {
11313
+ name: "reselect",
11314
+ title: "Reselect",
11315
+ 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.",
11316
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11317
+ annotations: writeAnnotations("Reselect", true),
11318
+ handler: () => {
11319
+ if (!controller.canReselect()) throw new Error("There is no cleared selection to bring back.");
11320
+ controller.reselect();
11321
+ return summary(controller);
11322
+ }
11323
+ };
11324
+ const transparencyTool = {
11325
+ name: "select_layer_transparency",
11326
+ title: "Select a layer's pixels",
11327
+ 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.",
11328
+ inputSchema: {
11329
+ type: "object",
11330
+ properties: {
11331
+ layerId: { type: "string", minLength: 1, description: "The layer whose pixels to select (see list_layers)." },
11332
+ mode: modeSchema,
11333
+ feather: featherSchema
11334
+ },
11335
+ required: ["layerId"],
11336
+ additionalProperties: false
11337
+ },
11338
+ annotations: writeAnnotations("Select a layer's pixels", true),
11339
+ handler: (args) => {
11340
+ const id = String(args.layerId ?? "");
11341
+ if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
11342
+ controller.selectLayerTransparency(id, { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0) });
11343
+ return summary(controller);
11344
+ }
11345
+ };
11346
+ const colorSchema = { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "Colour #rrggbb (default the foreground colour)." };
11347
+ const layerIdSchema = { type: "string", minLength: 1, description: "The layer (see list_layers; default the active layer)." };
11348
+ const requireLayerId = (v) => {
11349
+ if (v === void 0) return void 0;
11350
+ const id = String(v);
11351
+ if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
11352
+ return id;
11353
+ };
11354
+ const strokeTool = {
11355
+ name: "stroke_selection",
11356
+ title: "Stroke the selection",
11357
+ 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.",
11358
+ inputSchema: {
11359
+ type: "object",
11360
+ properties: {
11361
+ width: { type: "number", exclusiveMinimum: 0, maximum: 250, description: "Band width in pixels." },
11362
+ location: { type: "string", enum: ["outside", "inside", "center"], description: "Where the band lies against the edge (default outside)." },
11363
+ color: colorSchema,
11364
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
11365
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
11366
+ layerId: layerIdSchema,
11367
+ channel: { type: "string", enum: ["pixels", "mask"], description: "Paint the pixels (default) or the layer mask." }
11368
+ },
11369
+ required: ["width"],
11370
+ additionalProperties: false
11371
+ },
11372
+ annotations: writeAnnotations("Stroke the selection"),
11373
+ handler: (args) => {
11374
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select the shape to outline first.");
11375
+ return controller.strokeSelection({
11376
+ width: num2(args.width, NaN),
11377
+ location: args.location,
11378
+ color: args.color,
11379
+ opacity: args.opacity,
11380
+ blend: args.blend,
11381
+ layerId: requireLayerId(args.layerId),
11382
+ channel: args.channel
11383
+ });
11384
+ }
11385
+ };
11386
+ const cropTool = {
11387
+ name: "crop_to_selection",
11388
+ title: "Crop to the selection",
11389
+ 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.",
11390
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
11391
+ annotations: writeAnnotations("Crop to the selection", true),
11392
+ handler: () => {
11393
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select the area to keep first.");
11394
+ return { crop: controller.cropToSelection() };
11395
+ }
11396
+ };
11397
+ const fillTool = {
11398
+ name: "content_aware_fill",
11399
+ title: "Content-Aware Fill",
11400
+ 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.",
11401
+ inputSchema: {
11402
+ type: "object",
11403
+ properties: {
11404
+ output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
11405
+ sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
11406
+ seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
11407
+ layerId: layerIdSchema
11408
+ },
11409
+ additionalProperties: false
11410
+ },
11411
+ annotations: writeAnnotations("Content-Aware Fill"),
11412
+ handler: (args) => {
11413
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
11414
+ return controller.contentAwareFill({
11415
+ output: args.output,
11416
+ sampleRadius: args.sampleRadius,
11417
+ seed: args.seed,
11418
+ layerId: requireLayerId(args.layerId)
11419
+ });
11420
+ }
11421
+ };
11422
+ const copyTool = {
11423
+ name: "copy_pixels",
11424
+ title: "Copy pixels",
11425
+ 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.",
11426
+ inputSchema: {
11427
+ type: "object",
11428
+ properties: { layerId: layerIdSchema, merged: { type: "boolean", description: "Copy the visible image of all layers (default false)." } },
11429
+ additionalProperties: false
11430
+ },
11431
+ annotations: { title: "Copy pixels", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
11432
+ handler: (args) => {
11433
+ const { width, height, x, y } = controller.copyPixels({ layerId: requireLayerId(args.layerId), merged: args.merged === true });
11434
+ return { width, height, x, y };
11435
+ }
11436
+ };
11437
+ const cutTool = {
11438
+ name: "cut_pixels",
11439
+ title: "Cut pixels",
11440
+ 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.",
11441
+ inputSchema: { type: "object", properties: { layerId: layerIdSchema }, additionalProperties: false },
11442
+ annotations: { title: "Cut pixels", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
11443
+ handler: (args) => {
11444
+ if (!controller.getSelection()) throw new Error("Nothing is selected; select what to cut first (cutting everything would empty the layer).");
11445
+ const { width, height, x, y } = controller.cutPixels({ layerId: requireLayerId(args.layerId) });
11446
+ return { width, height, x, y };
11447
+ }
11448
+ };
11449
+ const pasteTool = {
11450
+ name: "paste_pixels",
11451
+ title: "Paste pixels",
11452
+ 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.",
11453
+ inputSchema: {
11454
+ type: "object",
11455
+ properties: { mode: { type: "string", enum: ["new-layer", "in-place", "into", "outside"], description: "Where and how to paste (default new-layer)." } },
11456
+ additionalProperties: false
11457
+ },
11458
+ annotations: writeAnnotations("Paste pixels"),
11459
+ handler: (args) => controller.pastePixels(args.mode ?? "new-layer")
11460
+ };
11461
+ return [
11462
+ getSelection,
11463
+ selectAll,
11464
+ deselect,
11465
+ invert2,
11466
+ selectRect,
11467
+ selectEllipse,
11468
+ selectPolygonTool,
11469
+ lineTool,
11470
+ magicWandTool,
11471
+ colorRangeTool,
11472
+ loadFromImage,
11473
+ loadFromMask,
11474
+ transparencyTool,
11475
+ growTool,
11476
+ similarTool,
11477
+ reselectTool,
11478
+ display,
11479
+ selectRegion,
11480
+ selectByColor,
11481
+ modify,
11482
+ refine,
11483
+ applyMask,
11484
+ strokeTool,
11485
+ cropTool,
11486
+ fillTool,
11487
+ copyTool,
11488
+ cutTool,
11489
+ pasteTool
11490
+ ];
10516
11491
  }
10517
11492
  function createImageSubjectSelectionTool(controller) {
10518
11493
  return {
@@ -11272,12 +12247,12 @@ function createImageLayerTools(controller) {
11272
12247
  const addMask = {
11273
12248
  name: "add_layer_mask",
11274
12249
  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.",
12250
+ 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
12251
  inputSchema: {
11277
12252
  type: "object",
11278
12253
  properties: {
11279
12254
  layerId: LAYER_ID2,
11280
- from: { type: "string", enum: ["reveal-all", "hide-all", "selection"], description: "What the mask starts as (default reveal-all)." },
12255
+ from: { type: "string", enum: ["reveal-all", "hide-all", "selection", "hide-selection", "transparency"], description: "What the mask starts as (default reveal-all)." },
11281
12256
  density: { type: "number", minimum: 0, maximum: 1, description: "Mask density 0..1 (default 1)." },
11282
12257
  feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px (default 0)." }
11283
12258
  },
@@ -13331,13 +14306,14 @@ function createImageSmartObjectTools(controller) {
13331
14306
  const addFilter = {
13332
14307
  name: "add_smart_filter",
13333
14308
  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.",
14309
+ 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
14310
  inputSchema: {
13336
14311
  type: "object",
13337
14312
  properties: {
13338
14313
  layerId: LAYER_ID4,
13339
14314
  kind: { type: "string", enum: ["develop", "blur"], description: "Filter kind." },
13340
14315
  index: { type: "integer", minimum: 0, description: "Position in the stack, 0 = bottom, applied first (default the top)." },
14316
+ useSelection: { type: "boolean", description: "With a selection, set the filter mask from it so the filter only changes the selected area (default true)." },
13341
14317
  ...FILTER_SETTINGS
13342
14318
  },
13343
14319
  required: ["layerId", "kind"],
@@ -13346,8 +14322,9 @@ function createImageSmartObjectTools(controller) {
13346
14322
  annotations: { title: "Add a smart filter", ...WRITE7 },
13347
14323
  handler: (args) => {
13348
14324
  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 };
14325
+ const masked = args.useSelection !== false && controller.hasSelection;
14326
+ const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index, { useSelection: args.useSelection !== false });
14327
+ return { layerId: id, filter, smartFilters: info(id).smartFilters, masked };
13351
14328
  }
13352
14329
  };
13353
14330
  const updateFilter = {
@@ -14160,7 +15137,7 @@ function createImageEditorToolProvider(controller, options = {}) {
14160
15137
  const addLayer = {
14161
15138
  name: "add_adjustment_layer",
14162
15139
  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.",
15140
+ 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
15141
  inputSchema: {
14165
15142
  type: "object",
14166
15143
  properties: {
@@ -14170,7 +15147,8 @@ function createImageEditorToolProvider(controller, options = {}) {
14170
15147
  blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
14171
15148
  name: { type: "string", description: "Layer name shown in the layers panel." },
14172
15149
  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)." }
15150
+ parentId: { type: "string", description: "Put the layer at the top of this group (from list_layers)." },
15151
+ 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
15152
  },
14175
15153
  required: ["kind"],
14176
15154
  additionalProperties: false
@@ -14182,7 +15160,7 @@ function createImageEditorToolProvider(controller, options = {}) {
14182
15160
  idempotentHint: false,
14183
15161
  openWorldHint: false
14184
15162
  },
14185
- handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {
15163
+ handler: ({ kind, params, opacity, blend, name, aboveId, parentId, useSelection }) => {
14186
15164
  if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind "${kind}".`);
14187
15165
  if (blend !== void 0 && !isBlendMode(blend)) throw new Error(`Unknown blend mode "${blend}".`);
14188
15166
  if (aboveId !== void 0 && parentId !== void 0) throw new Error("Give aboveId or parentId, not both.");
@@ -14190,9 +15168,9 @@ function createImageEditorToolProvider(controller, options = {}) {
14190
15168
  if (ref !== void 0 && !controller.getLayer(ref)) throw new Error(`Unknown layer "${ref}". Call list_layers for ids.`);
14191
15169
  }
14192
15170
  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 });
15171
+ const id = controller.addAdjustmentLayer({ kind, params, opacity, blend, name, aboveId, parentId, useSelection: useSelection !== false });
14194
15172
  const layer = controller.getAdjustmentLayer(id);
14195
- return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };
15173
+ return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id), masked: Boolean(layer.mask) };
14196
15174
  }
14197
15175
  };
14198
15176
  const renderPreview = {
@@ -14326,6 +15304,14 @@ function removeCurvePoint(points, index) {
14326
15304
  var round22 = (v) => Math.round(v * 100) / 100;
14327
15305
 
14328
15306
  // src/image-editor/selection/gesture.ts
15307
+ function snapTo45(from, point) {
15308
+ const dx = point.x - from.x;
15309
+ const dy = point.y - from.y;
15310
+ const len = Math.hypot(dx, dy);
15311
+ if (len === 0) return point;
15312
+ const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
15313
+ return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
15314
+ }
14329
15315
  var SelectionGesture = class {
14330
15316
  constructor(controller, getOptions) {
14331
15317
  this.controller = controller;
@@ -14337,13 +15323,20 @@ var SelectionGesture = class {
14337
15323
  points = [];
14338
15324
  current = null;
14339
15325
  modifiers = {};
15326
+ /** Polygonal lasso corners while one is being drawn. */
15327
+ polygon = null;
15328
+ pressedAt = null;
15329
+ freehand = false;
15330
+ /** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
15331
+ closeRadius = 8;
14340
15332
  /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
14341
15333
  error = null;
14342
15334
  get active() {
14343
- return this.start !== null;
15335
+ return this.start !== null || this.polygon !== null;
14344
15336
  }
14345
15337
  /** The outline of the shape being drawn, or `null`. */
14346
15338
  get draftPath() {
15339
+ if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
14347
15340
  if (!this.start || !this.current) return null;
14348
15341
  const { tool } = this.getOptions();
14349
15342
  if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
@@ -14366,18 +15359,71 @@ var SelectionGesture = class {
14366
15359
  async begin(point, modifiers = {}) {
14367
15360
  const options = this.getOptions();
14368
15361
  this.error = null;
15362
+ if (this.polygon && options.tool !== "polygon") this.cancel();
15363
+ if (options.tool === "polygon") {
15364
+ this.pressedAt = point;
15365
+ this.freehand = false;
15366
+ if (!this.polygon) {
15367
+ this.modifiers = modifiers;
15368
+ this.polygon = [point];
15369
+ this.current = point;
15370
+ return;
15371
+ }
15372
+ const last = this.polygon[this.polygon.length - 1];
15373
+ const p = modifiers.shiftKey ? snapTo45(last, point) : point;
15374
+ const first = this.polygon[0];
15375
+ if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
15376
+ this.close();
15377
+ return;
15378
+ }
15379
+ if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
15380
+ this.current = p;
15381
+ if ((modifiers.detail ?? 1) >= 2) this.close();
15382
+ return;
15383
+ }
14369
15384
  this.modifiers = modifiers;
14370
- if (options.tool === "wand") {
15385
+ if (options.tool === "row" || options.tool === "column") {
15386
+ this.start = null;
15387
+ try {
15388
+ this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
15389
+ mode: selectionModeFromModifiers(modifiers, options.mode),
15390
+ feather: options.feather ?? 0
15391
+ });
15392
+ } catch (err) {
15393
+ this.error = err instanceof Error ? err.message : String(err);
15394
+ }
15395
+ return;
15396
+ }
15397
+ if (options.tool === "wand" || options.tool === "range") {
14371
15398
  this.start = null;
15399
+ const common = {
15400
+ sampleMerged: options.sampleMerged,
15401
+ sample: options.sample,
15402
+ feather: options.feather ?? 0,
15403
+ mode: selectionModeFromModifiers(modifiers, options.mode)
15404
+ };
14372
15405
  try {
15406
+ if (options.tool === "range") {
15407
+ const pixels = await this.controller.getSamplePixels(common);
15408
+ if (!pixels) throw new Error("The image pixels are not available to sample.");
15409
+ const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
15410
+ const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
15411
+ const p = (y * pixels.width + x) * 4;
15412
+ const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
15413
+ await this.controller.selectColorRange([{ color, at: point }], {
15414
+ ...common,
15415
+ fuzziness: options.fuzziness ?? 40,
15416
+ localized: options.localized ? options.localized : void 0,
15417
+ invert: options.rangeInvert ?? false
15418
+ });
15419
+ return;
15420
+ }
14373
15421
  await this.controller.magicWand(point, {
15422
+ ...common,
14374
15423
  tolerance: options.tolerance,
14375
15424
  contiguous: options.contiguous,
14376
15425
  antiAlias: options.antiAlias,
14377
- distance: options.distance,
14378
- sampleMerged: options.sampleMerged,
14379
- sample: options.sample,
14380
- mode: selectionModeFromModifiers(modifiers, options.mode)
15426
+ distance: options.distance
14381
15427
  });
14382
15428
  } catch (err) {
14383
15429
  this.error = err instanceof Error ? err.message : String(err);
@@ -14389,7 +15435,18 @@ var SelectionGesture = class {
14389
15435
  this.points = [point];
14390
15436
  }
14391
15437
  /** Pointer move; returns true when the draft changed (re-render). */
14392
- move(point) {
15438
+ move(point, modifiers = {}) {
15439
+ if (this.polygon) {
15440
+ const last = this.polygon[this.polygon.length - 1];
15441
+ if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
15442
+ this.freehand = true;
15443
+ if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
15444
+ this.current = null;
15445
+ return true;
15446
+ }
15447
+ this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
15448
+ return true;
15449
+ }
14393
15450
  if (!this.start) return false;
14394
15451
  this.current = point;
14395
15452
  if (this.getOptions().tool === "lasso") {
@@ -14401,6 +15458,12 @@ var SelectionGesture = class {
14401
15458
  }
14402
15459
  /** Pointer up: commits the shape. Returns true when the selection changed. */
14403
15460
  end(point, modifiers) {
15461
+ if (this.polygon) {
15462
+ if (point && this.freehand) this.move(point);
15463
+ this.pressedAt = null;
15464
+ this.freehand = false;
15465
+ return false;
15466
+ }
14404
15467
  if (!this.start) return false;
14405
15468
  if (point) this.move(point);
14406
15469
  const options = this.getOptions();
@@ -14416,17 +15479,65 @@ var SelectionGesture = class {
14416
15479
  this.current = null;
14417
15480
  this.points = [];
14418
15481
  if (!path) return mode === "replace" ? this.controller.deselect() : false;
14419
- const opts = { mode, antiAlias: options.antiAlias };
15482
+ const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
14420
15483
  if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
14421
15484
  return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
14422
15485
  }
14423
- /** Abandons a drag without changing the selection. */
15486
+ /** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
15487
+ close() {
15488
+ const polygon = this.polygon;
15489
+ if (!polygon) return false;
15490
+ const options = this.getOptions();
15491
+ this.cancel();
15492
+ if (polygon.length < 3) return false;
15493
+ return this.controller.selectPolygon(polygon, {
15494
+ mode: selectionModeFromModifiers(this.modifiers, options.mode),
15495
+ antiAlias: options.antiAlias,
15496
+ feather: options.feather ?? 0
15497
+ });
15498
+ }
15499
+ /**
15500
+ * Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
15501
+ * removes the last corner. Returns true when the key was used (the host prevents its default).
15502
+ */
15503
+ key(key) {
15504
+ if (!this.polygon) return false;
15505
+ if (key === "Enter") this.close();
15506
+ else if (key === "Escape") this.cancel();
15507
+ else if (key === "Backspace" || key === "Delete") {
15508
+ this.polygon.pop();
15509
+ if (!this.polygon.length) this.cancel();
15510
+ } else return false;
15511
+ return true;
15512
+ }
15513
+ /** Abandons a drag (or a polygonal lasso) without changing the selection. */
14424
15514
  cancel() {
14425
15515
  this.start = null;
14426
15516
  this.current = null;
14427
15517
  this.points = [];
15518
+ this.polygon = null;
15519
+ this.pressedAt = null;
15520
+ this.freehand = false;
14428
15521
  }
14429
15522
  };
15523
+ function handleSelectionShortcut(controller, e, onError) {
15524
+ if (!(e.ctrlKey || e.metaKey)) return false;
15525
+ const key = e.key.toLowerCase();
15526
+ const run = (fn) => {
15527
+ try {
15528
+ fn();
15529
+ } catch (err) {
15530
+ onError?.(err instanceof Error ? err.message : String(err));
15531
+ }
15532
+ return true;
15533
+ };
15534
+ if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
15535
+ if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
15536
+ if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
15537
+ if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
15538
+ if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
15539
+ return false;
15540
+ }
14430
15541
 
14431
15542
  // src/image-editor/mosaic-filter.ts
14432
15543
  function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
@@ -14566,6 +15677,21 @@ function applyRegionBlur(imageData, regions, radius = 10) {
14566
15677
  }
14567
15678
 
14568
15679
  // src/image-editor/layers/panel.ts
15680
+ function maskThumbnailClick(controller, layer, e) {
15681
+ if (!layer.mask) return;
15682
+ if (e.ctrlKey || e.metaKey) {
15683
+ controller.selectionFromLayerMask(layer.id, selectionModeFromModifiers(e, "replace"));
15684
+ } else if (e.shiftKey) {
15685
+ controller.setLayerMaskProps(layer.id, { enabled: layer.mask.enabled === false });
15686
+ } else {
15687
+ controller.setActiveLayer(layer.id, "mask");
15688
+ }
15689
+ }
15690
+ function layerThumbnailClick(controller, layer, e) {
15691
+ if (!(e.ctrlKey || e.metaKey)) return false;
15692
+ controller.selectLayerTransparency(layer.id, { mode: selectionModeFromModifiers(e, "replace") });
15693
+ return true;
15694
+ }
14569
15695
  function layerPanelRows(layers, activeId) {
14570
15696
  const rows = [];
14571
15697
  const walk = (list, depth, parentId) => {
@@ -14637,6 +15763,7 @@ function layerMenu(controller) {
14637
15763
  const below = loc ? siblings[loc.index - 1] : void 0;
14638
15764
  const hasSel = controller.hasSelection;
14639
15765
  const isPixel = active?.type === "pixel";
15766
+ const hasOwnPixels = active?.type === "pixel" || active?.type === "shape" || active?.type === "type" || active?.type === "smart";
14640
15767
  return [
14641
15768
  { action: "new-layer", label: "New Layer", enabled: true },
14642
15769
  { action: "new-group", label: "New Group", enabled: true },
@@ -14651,6 +15778,9 @@ function layerMenu(controller) {
14651
15778
  { action: "via-cut", label: "Layer via Cut", enabled: isPixel && hasSel },
14652
15779
  { action: "add-mask", label: "Add Layer Mask", enabled: Boolean(active) },
14653
15780
  { action: "add-mask-selection", label: "Add Mask from Selection", enabled: Boolean(active) && hasSel },
15781
+ { action: "add-mask-hide-selection", label: "Add Mask Hiding Selection", enabled: Boolean(active) && hasSel },
15782
+ { action: "add-mask-transparency", label: "Add Mask from Transparency", enabled: hasOwnPixels },
15783
+ { action: "select-transparency", label: "Select Layer Pixels", enabled: hasOwnPixels },
14654
15784
  { action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
14655
15785
  { action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
14656
15786
  { action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
@@ -14702,6 +15832,15 @@ async function runLayerMenuAction(controller, action) {
14702
15832
  case "add-mask":
14703
15833
  if (id) controller.addLayerMask(id, "reveal-all");
14704
15834
  break;
15835
+ case "add-mask-hide-selection":
15836
+ if (id) controller.addLayerMask(id, "hide-selection");
15837
+ break;
15838
+ case "add-mask-transparency":
15839
+ if (id) controller.addLayerMask(id, "transparency");
15840
+ break;
15841
+ case "select-transparency":
15842
+ if (id) controller.selectLayerTransparency(id);
15843
+ break;
14705
15844
  case "add-mask-selection":
14706
15845
  if (id) controller.addLayerMask(id, "selection");
14707
15846
  break;
@@ -14886,6 +16025,7 @@ async function addLayerOfKind(controller, id, init) {
14886
16025
  if (isAdjustmentKind(kind)) {
14887
16026
  return controller.addAdjustmentLayer({
14888
16027
  kind,
16028
+ useSelection: true,
14889
16029
  ...init
14890
16030
  });
14891
16031
  }
@@ -15834,7 +16974,7 @@ function createImageEffectsToolProvider(controller, options = {}) {
15834
16974
  skill: "pixerate-image-editor",
15835
16975
  target: { noun: "layer", id: "A layer id from list_layers." },
15836
16976
  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.",
16977
+ 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
16978
  provider: () => controller.effectsProvider,
15839
16979
  readOnlyReason,
15840
16980
  list: (target) => {
@@ -15842,8 +16982,8 @@ function createImageEffectsToolProvider(controller, options = {}) {
15842
16982
  return layers.map((l) => ({ target: l.id, ...l.name ? { name: l.name } : {}, effects: imageLayerAppliedEffects(l) })).filter((t) => target || t.effects.length);
15843
16983
  },
15844
16984
  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 });
16985
+ const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity, useSelection: true });
16986
+ return result(r.layerId, r.filter.id, { converted: r.converted, masked: r.masked });
15847
16987
  },
15848
16988
  update: async (target, instance, changes) => {
15849
16989
  const layer = layerOf(target);
@@ -15974,6 +17114,8 @@ export {
15974
17114
  CANVAS_SIZE_PRESETS,
15975
17115
  CLONE_TRANSFORM_CONTROLS,
15976
17116
  COLOR_MODE_OPTIONS,
17117
+ COLOR_RANGE_PRESETS,
17118
+ COLOR_RANGE_PRESET_LABELS,
15977
17119
  CoverageMap,
15978
17120
  D50,
15979
17121
  D65,
@@ -16030,6 +17172,7 @@ export {
16030
17172
  HIDE,
16031
17173
  HISTOGRAM_BINS,
16032
17174
  HUE_RANGE_LABELS,
17175
+ HUE_RANGE_PRESETS,
16033
17176
  IDENTITY_CLONE_TRANSFORM,
16034
17177
  IDENTITY_MATRIX3,
16035
17178
  IMAGE_ADJUSTMENT_CONTROLS,
@@ -16093,6 +17236,7 @@ export {
16093
17236
  PARAGRAPH_STYLE_FIELDS,
16094
17237
  PARAM,
16095
17238
  PARAM_FLOATS,
17239
+ PASTE_MODES,
16096
17240
  PATTERN_BLOCK_KEYS,
16097
17241
  PIGMENT_BANDS,
16098
17242
  PIGMENT_CHANNELS,
@@ -16296,6 +17440,7 @@ export {
16296
17440
  colorModePlanes16,
16297
17441
  colorPanelView,
16298
17442
  colorRange,
17443
+ colorRangePreset,
16299
17444
  combineSelections,
16300
17445
  compileAdjustmentLayer,
16301
17446
  componentPath,
@@ -16468,9 +17613,11 @@ export {
16468
17613
  grainHeights,
16469
17614
  greyMorphology,
16470
17615
  groupLayers,
17616
+ growSelection,
16471
17617
  guidePlanes,
16472
17618
  guidedFilterColor,
16473
17619
  handlePaintShortcut,
17620
+ handleSelectionShortcut,
16474
17621
  hasActiveEffects,
16475
17622
  hasLayersBelow,
16476
17623
  hasResampledLegacyMask,
@@ -16515,6 +17662,7 @@ export {
16515
17662
  isBitDepth,
16516
17663
  isBlendMode,
16517
17664
  isBrushstrokeBrush,
17665
+ isColorRangePreset,
16518
17666
  isContentsDecoded,
16519
17667
  isDefaultDepthMask,
16520
17668
  isGradientPaint,
@@ -16561,6 +17709,7 @@ export {
16561
17709
  layerName,
16562
17710
  layerPanelRows,
16563
17711
  layerPanels,
17712
+ layerThumbnailClick,
16564
17713
  layerTree,
16565
17714
  layerTypeLabel,
16566
17715
  layerWarning,
@@ -16589,6 +17738,7 @@ export {
16589
17738
  mapSubpaths,
16590
17739
  maskBounds,
16591
17740
  maskSummary,
17741
+ maskThumbnailClick,
16592
17742
  matrixFromCorners,
16593
17743
  matrixFromParams,
16594
17744
  measureCanvasViewport,
@@ -16784,11 +17934,13 @@ export {
16784
17934
  segmentToSelection,
16785
17935
  segmentationOutlines,
16786
17936
  segmentsToSelection,
17937
+ selectSimilar,
16787
17938
  selectionArea,
16788
17939
  selectionFromCoverage,
16789
17940
  selectionFromPixels,
16790
17941
  selectionModeFromModifiers,
16791
17942
  selectionOutline,
17943
+ selectionRing,
16792
17944
  selectionToCoverage,
16793
17945
  selectionToPixels,
16794
17946
  selectionValueAt,
@@ -16821,11 +17973,13 @@ export {
16821
17973
  shapeOutline,
16822
17974
  shapeRenderClip,
16823
17975
  shiftCoverage,
17976
+ skinToneRange,
16824
17977
  smartFilterRows,
16825
17978
  smartObjectLayers,
16826
17979
  smoothCoverage,
16827
17980
  smoothPoints,
16828
17981
  smudgeEffect,
17982
+ snapTo45,
16829
17983
  solidPaint,
16830
17984
  solidSurface,
16831
17985
  solveMembrane,