@pixerate/editor 0.220.0 → 0.221.1

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 (29) hide show
  1. package/dist/image-editor/controller.d.cts +27 -4
  2. package/dist/image-editor/controller.d.ts +27 -4
  3. package/dist/image-editor/index.cjs +2276 -1864
  4. package/dist/image-editor/index.js +1240 -828
  5. package/dist/image-editor/layers/serialize.d.cts +2 -0
  6. package/dist/image-editor/layers/serialize.d.ts +2 -0
  7. package/dist/image-editor/selection/index.d.cts +1 -0
  8. package/dist/image-editor/selection/index.d.ts +1 -0
  9. package/dist/image-editor/selection/inspect.d.cts +1 -1
  10. package/dist/image-editor/selection/inspect.d.ts +1 -1
  11. package/dist/image-editor/selection/recipe.d.cts +89 -0
  12. package/dist/image-editor/selection/recipe.d.ts +89 -0
  13. package/dist/image-editor/types.d.cts +9 -0
  14. package/dist/image-editor/types.d.ts +9 -0
  15. package/package.json +1 -1
  16. package/skills/README.md +40 -9
  17. package/skills/WORKFLOW_TEMPLATE.md +50 -0
  18. package/skills/pixerate-image-editor/SKILL.md +14 -11
  19. package/skills/pixerate-image-editor/references/painting.md +17 -1
  20. package/skills/pixerate-image-selection-masking/SKILL.md +3 -3
  21. package/skills/pixerate-image-selection-masking/evals/local-adjustment.json +22 -0
  22. package/skills/pixerate-image-selection-masking/evals/mask-out-background.json +24 -0
  23. package/skills/pixerate-image-selection-masking/evals/select-one-of-two.json +22 -0
  24. package/skills/pixerate-spreadsheet/SKILL.md +7 -0
  25. package/skills/pixerate-spreadsheet-clean-import/SKILL.md +154 -0
  26. package/skills/pixerate-spreadsheet-clean-import/evals/clean-in-place.json +43 -0
  27. package/skills/pixerate-spreadsheet-clean-import/evals/dedupe-keep-latest.json +25 -0
  28. package/skills/pixerate-spreadsheet-clean-import/evals/fix-types-only.json +24 -0
  29. package/skills/pixerate-video-editor/SKILL.md +8 -0
@@ -1338,10 +1338,10 @@ function emptyHistogram() {
1338
1338
  max: 0
1339
1339
  };
1340
1340
  }
1341
- function addToHistogram(h, data, stride, rect2) {
1342
- for (let y = rect2.y; y < rect2.y + rect2.height; y++) {
1343
- let i = (y * stride + rect2.x) * 4;
1344
- for (let x = 0; x < rect2.width; x++, i += 4) {
1341
+ function addToHistogram(h, data, stride, rect3) {
1342
+ for (let y = rect3.y; y < rect3.y + rect3.height; y++) {
1343
+ let i = (y * stride + rect3.x) * 4;
1344
+ for (let x = 0; x < rect3.width; x++, i += 4) {
1345
1345
  if (!(data[i + 3] > 0)) {
1346
1346
  h.transparent++;
1347
1347
  continue;
@@ -1987,7 +1987,7 @@ var ImageEditorRenderer = class {
1987
1987
  if (!source.enabled || !source.maskSource) return null;
1988
1988
  return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;
1989
1989
  },
1990
- rasterizeAnnotation: (annotation, rect2) => this.rasterizeAnnotation(annotation, rect2),
1990
+ rasterizeAnnotation: (annotation, rect3) => this.rasterizeAnnotation(annotation, rect3),
1991
1991
  version: `${host?.version() ?? ""}:${this.assetVersion}`,
1992
1992
  globalLight: host?.globalLight?.(),
1993
1993
  pattern: host?.pattern ? (id) => host.pattern(id) : void 0
@@ -2028,34 +2028,34 @@ var ImageEditorRenderer = class {
2028
2028
  return Boolean(ctx && typeof ctx.getImageData === "function");
2029
2029
  }
2030
2030
  /** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */
2031
- rasterizeAnnotation(annotation, rect2) {
2032
- if (!this.scratch) this.scratch = this.createCanvas(rect2.width, rect2.height);
2031
+ rasterizeAnnotation(annotation, rect3) {
2032
+ if (!this.scratch) this.scratch = this.createCanvas(rect3.width, rect3.height);
2033
2033
  const canvas = this.scratch;
2034
- if (canvas.width !== rect2.width || canvas.height !== rect2.height) {
2035
- canvas.width = rect2.width;
2036
- canvas.height = rect2.height;
2034
+ if (canvas.width !== rect3.width || canvas.height !== rect3.height) {
2035
+ canvas.width = rect3.width;
2036
+ canvas.height = rect3.height;
2037
2037
  }
2038
2038
  const ctx = canvas.getContext("2d");
2039
2039
  if (!ctx || typeof ctx.getImageData !== "function") return null;
2040
2040
  try {
2041
2041
  ctx.setTransform(1, 0, 0, 1, 0, 0);
2042
- ctx.clearRect(0, 0, rect2.width, rect2.height);
2043
- ctx.setTransform(1, 0, 0, 1, -rect2.x, -rect2.y);
2042
+ ctx.clearRect(0, 0, rect3.width, rect3.height);
2043
+ ctx.setTransform(1, 0, 0, 1, -rect3.x, -rect3.y);
2044
2044
  const img = annotation.type === "image" ? this.imageCache.peek(annotation.src) ?? null : null;
2045
2045
  this.drawAnnotationShape(ctx, annotation, img);
2046
2046
  ctx.setTransform(1, 0, 0, 1, 0, 0);
2047
- return new Uint8ClampedArray(ctx.getImageData(0, 0, rect2.width, rect2.height).data);
2047
+ return new Uint8ClampedArray(ctx.getImageData(0, 0, rect3.width, rect3.height).data);
2048
2048
  } catch {
2049
2049
  return null;
2050
2050
  }
2051
2051
  }
2052
- putTile(ctx, data, tx, ty, rect2) {
2053
- const ox = tx * TILE - rect2.x;
2054
- const oy = ty * TILE - rect2.y;
2052
+ putTile(ctx, data, tx, ty, rect3) {
2053
+ const ox = tx * TILE - rect3.x;
2054
+ const oy = ty * TILE - rect3.y;
2055
2055
  const x0 = Math.max(0, -ox);
2056
2056
  const y0 = Math.max(0, -oy);
2057
- const x1 = Math.min(TILE, rect2.width - ox);
2058
- const y1 = Math.min(TILE, rect2.height - oy);
2057
+ const x1 = Math.min(TILE, rect3.width - ox);
2058
+ const y1 = Math.min(TILE, rect3.height - oy);
2059
2059
  if (x1 <= x0 || y1 <= y0) return;
2060
2060
  const img = ctx.createImageData(TILE, TILE);
2061
2061
  img.data.set(data);
@@ -2067,23 +2067,23 @@ var ImageEditorRenderer = class {
2067
2067
  * uploading only the tiles that changed since the last render.
2068
2068
  */
2069
2069
  updateComposite(layers, region, width, height, options = {}) {
2070
- const rect2 = coveringRect(region);
2070
+ const rect3 = coveringRect(region);
2071
2071
  let comp = this.comp;
2072
2072
  let fresh = false;
2073
- if (!comp || comp.rect.x !== rect2.x || comp.rect.y !== rect2.y || comp.rect.width !== rect2.width || comp.rect.height !== rect2.height) {
2074
- const canvas = this.createCanvas(rect2.width, rect2.height);
2075
- comp = this.comp = new CompositeCanvas(canvas, rect2);
2073
+ if (!comp || comp.rect.x !== rect3.x || comp.rect.y !== rect3.y || comp.rect.width !== rect3.width || comp.rect.height !== rect3.height) {
2074
+ const canvas = this.createCanvas(rect3.width, rect3.height);
2075
+ comp = this.comp = new CompositeCanvas(canvas, rect3);
2076
2076
  fresh = true;
2077
2077
  }
2078
2078
  const ctx = comp.canvas.getContext("2d");
2079
2079
  if (!ctx || typeof ctx.createImageData !== "function") return null;
2080
2080
  let changed = false;
2081
- this.compositor.compositeTiles(layers, rect2, this.compositorSources(width, height), (tx, ty, data) => {
2081
+ this.compositor.compositeTiles(layers, rect3, this.compositorSources(width, height), (tx, ty, data) => {
2082
2082
  const key2 = tileKey(tx, ty);
2083
2083
  if (comp.tiles.get(key2) === data) return;
2084
2084
  comp.tiles.set(key2, data);
2085
2085
  comp.histograms.delete(key2);
2086
- this.putTile(ctx, data, tx, ty, rect2);
2086
+ this.putTile(ctx, data, tx, ty, rect3);
2087
2087
  changed = true;
2088
2088
  }, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? void 0 : options.dirty });
2089
2089
  if (changed) comp.version++;
@@ -2363,15 +2363,15 @@ var ImageEditorRenderer = class {
2363
2363
  break;
2364
2364
  }
2365
2365
  case "rect": {
2366
- const rect2 = ann;
2367
- ctx.strokeStyle = rect2.strokeColor;
2368
- ctx.lineWidth = rect2.strokeWidth;
2369
- if (rect2.fillColor) {
2370
- ctx.fillStyle = rect2.fillColor;
2371
- ctx.fillRect(rect2.x, rect2.y, rect2.width, rect2.height);
2366
+ const rect3 = ann;
2367
+ ctx.strokeStyle = rect3.strokeColor;
2368
+ ctx.lineWidth = rect3.strokeWidth;
2369
+ if (rect3.fillColor) {
2370
+ ctx.fillStyle = rect3.fillColor;
2371
+ ctx.fillRect(rect3.x, rect3.y, rect3.width, rect3.height);
2372
2372
  }
2373
- if (rect2.strokeWidth > 0) {
2374
- ctx.strokeRect(rect2.x, rect2.y, rect2.width, rect2.height);
2373
+ if (rect3.strokeWidth > 0) {
2374
+ ctx.strokeRect(rect3.x, rect3.y, rect3.width, rect3.height);
2375
2375
  }
2376
2376
  break;
2377
2377
  }
@@ -2562,12 +2562,12 @@ var ImageEditorRenderer = class {
2562
2562
  const split = splitOverlayAnnotations(layers);
2563
2563
  overlay = split.overlay.map((l) => l.annotation);
2564
2564
  await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);
2565
- const rect2 = coveringRect(region);
2566
- const comp = this.createCanvas(rect2.width, rect2.height);
2565
+ const rect3 = coveringRect(region);
2566
+ const comp = this.createCanvas(rect3.width, rect3.height);
2567
2567
  const cctx = comp.getContext("2d");
2568
2568
  if (cctx && typeof cctx.createImageData === "function") {
2569
- this.exportCompositor.compositeTiles(split.composite, rect2, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {
2570
- this.putTile(cctx, data, tx, ty, rect2);
2569
+ this.exportCompositor.compositeTiles(split.composite, rect3, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {
2570
+ this.putTile(cctx, data, tx, ty, rect3);
2571
2571
  });
2572
2572
  }
2573
2573
  baseCanvas = this.createCanvas(baseW, baseH);
@@ -2578,7 +2578,7 @@ var ImageEditorRenderer = class {
2578
2578
  bctx.beginPath();
2579
2579
  bctx.rect(region.x, region.y, region.width, region.height);
2580
2580
  bctx.clip();
2581
- bctx.drawImage(comp, rect2.x, rect2.y);
2581
+ bctx.drawImage(comp, rect3.x, rect3.y);
2582
2582
  bctx.restore();
2583
2583
  this.applyPost(bctx, baseW, baseH, state);
2584
2584
  }
@@ -2706,10 +2706,10 @@ function getBaseProjection(region, transform, baseW, baseH) {
2706
2706
  };
2707
2707
  }
2708
2708
  var CompositeCanvas = class {
2709
- constructor(canvas, rect2) {
2709
+ constructor(canvas, rect3) {
2710
2710
  this.canvas = canvas;
2711
- this.rect = rect2;
2712
- this.region = rect2;
2711
+ this.rect = rect3;
2712
+ this.region = rect3;
2713
2713
  }
2714
2714
  canvas;
2715
2715
  rect;
@@ -2807,6 +2807,21 @@ function inspectMaskEdge(mask, image, options = {}) {
2807
2807
  blk.x1 = Math.max(blk.x1, x);
2808
2808
  blk.y1 = Math.max(blk.y1, y);
2809
2809
  };
2810
+ const antiAliased = (x, y) => {
2811
+ let full = false;
2812
+ let none = false;
2813
+ for (let dy = -2; dy <= 2; dy++) {
2814
+ for (let dx = -2; dx <= 2; dx++) {
2815
+ const nx = x + dx;
2816
+ const ny = y + dy;
2817
+ if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
2818
+ const n = a[ny * W + nx];
2819
+ if (n === 255) full = true;
2820
+ else if (n === 0) none = true;
2821
+ }
2822
+ }
2823
+ return full && none;
2824
+ };
2810
2825
  const x0 = Math.max(0, b.x - 1);
2811
2826
  const y0 = Math.max(0, b.y - 1);
2812
2827
  const x1 = Math.min(W, b.x + b.width + 1);
@@ -2836,7 +2851,7 @@ function inspectMaskEdge(mask, image, options = {}) {
2836
2851
  const partial = v > 0 && v < 255;
2837
2852
  if (hi && lo || partial) {
2838
2853
  band++;
2839
- if (partial) soft++;
2854
+ if (partial && !antiAliased(x, y)) soft++;
2840
2855
  }
2841
2856
  if (v < 128) continue;
2842
2857
  const inCanvasLow = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && a[ny * W + nx] < 128;
@@ -3002,7 +3017,7 @@ function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
3002
3017
  };
3003
3018
  const on = (i, j) => f(i, j) >= 128;
3004
3019
  const V = w * h;
3005
- const point = (id) => {
3020
+ const point2 = (id) => {
3006
3021
  const vertical = id >= V;
3007
3022
  const k = vertical ? id - V : id;
3008
3023
  const i = k % w;
@@ -3087,7 +3102,7 @@ function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
3087
3102
  let from = segs[s * 2];
3088
3103
  while (!used[s]) {
3089
3104
  used[s] = 1;
3090
- loop.push(point(from));
3105
+ loop.push(point2(from));
3091
3106
  const to = segs[s * 2] === from ? segs[s * 2 + 1] : segs[s * 2];
3092
3107
  const next = touch.get(to).find((t) => !used[t]);
3093
3108
  from = to;
@@ -3176,136 +3191,651 @@ function regions(a, W, H, b, minArea) {
3176
3191
  };
3177
3192
  }
3178
3193
 
3179
- // src/image-editor/selection/uncertainty.ts
3180
- var BACKED = 0.2;
3181
- function selectionUncertainty(mask, image, options = {}) {
3182
- const W = mask.width;
3183
- const H = mask.height;
3184
- const empty = () => ({ unknown: createSelectionMask(W, H), regions: [], count: 0, confidence: 1 });
3185
- const b = mask.bounds;
3186
- if (!b) return empty();
3187
- const band = Math.max(1, Math.round(options.band ?? Math.max(2, Math.min(W, H) / 200)));
3188
- const minArea = Math.max(1, options.minArea ?? 16);
3189
- const maxRegions = Math.max(1, options.maxRegions ?? 20);
3190
- const a = mask.data;
3191
- const gradient = createGradientSampler(image, W);
3192
- const x0 = Math.max(0, b.x - band - 1);
3193
- const y0 = Math.max(0, b.y - band - 1);
3194
- const x1 = Math.min(W, b.x + b.width + band + 1);
3195
- const y1 = Math.min(H, b.y + b.height + band + 1);
3196
- const w = x1 - x0;
3197
- const h = y1 - y0;
3198
- const evidence = new Float32Array(w * h).fill(NaN);
3199
- const seeds = new Uint8Array(w * h);
3200
- let sum = 0;
3201
- let n = 0;
3202
- const on = (x, y) => a[y * W + x] >= 128;
3194
+ // src/image-editor/selection/wand.ts
3195
+ function checkPixels(p) {
3196
+ const n = p.width * p.height;
3197
+ if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {
3198
+ throw new Error("Invalid pixel buffer size");
3199
+ }
3200
+ if (p.data.length < n * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
3201
+ return n;
3202
+ }
3203
+ var SRGB_TO_LINEAR2 = (() => {
3204
+ const t = new Float32Array(256);
3205
+ for (let i = 0; i < 256; i++) {
3206
+ const v = i / 255;
3207
+ t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
3208
+ }
3209
+ return t;
3210
+ })();
3211
+ var labF = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
3212
+ function srgb8ToLab(r, g, b) {
3213
+ const rl = SRGB_TO_LINEAR2[r & 255];
3214
+ const gl = SRGB_TO_LINEAR2[g & 255];
3215
+ const bl = SRGB_TO_LINEAR2[b & 255];
3216
+ const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;
3217
+ const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
3218
+ const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;
3219
+ const fx = labF(x);
3220
+ const fy = labF(y);
3221
+ const fz = labF(z);
3222
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
3223
+ }
3224
+ function similarity(pixels, seed, tolerance, distance) {
3225
+ const d = pixels.data;
3226
+ const tr = d[seed * 4];
3227
+ const tg = d[seed * 4 + 1];
3228
+ const tb = d[seed * 4 + 2];
3229
+ const ta = d[seed * 4 + 3];
3230
+ const tol = Math.max(0, Math.floor(tolerance + 1e-3));
3231
+ if (distance === "perceptual") {
3232
+ const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);
3233
+ const de = Math.max(0, tolerance) * 100 / 255;
3234
+ const de2 = de * de + 1e-6;
3235
+ return (i) => {
3236
+ const p = i * 4;
3237
+ const a = d[p + 3];
3238
+ if (ta === 0 && a === 0) return true;
3239
+ if (Math.abs(a - ta) > tol) return false;
3240
+ const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);
3241
+ return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;
3242
+ };
3243
+ }
3244
+ return (i) => {
3245
+ const p = i * 4;
3246
+ const a = d[p + 3];
3247
+ if (ta === 0 && a === 0) return true;
3248
+ return Math.abs(d[p] - tr) <= tol && Math.abs(d[p + 1] - tg) <= tol && Math.abs(d[p + 2] - tb) <= tol && Math.abs(a - ta) <= tol;
3249
+ };
3250
+ }
3251
+ function antialiasMarks(marks, w, h, x0, y0, x1, y1) {
3252
+ const out = marks.slice();
3203
3253
  for (let y = y0; y < y1; y++) {
3204
3254
  for (let x = x0; x < x1; x++) {
3205
- const v = on(x, y);
3206
- const differs = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && on(nx, ny) !== v;
3207
- if (!(differs(x - 1, y) || differs(x + 1, y) || differs(x, y - 1) || differs(x, y + 1))) continue;
3208
- let g = 0;
3209
- for (let dy = -2; dy <= 2; dy++) {
3210
- for (let dx = -2; dx <= 2; dx++) {
3255
+ const i = y * w + x;
3256
+ const v = marks[i];
3257
+ let sum = 0;
3258
+ let edge = false;
3259
+ for (let dy = -1; dy <= 1; dy++) {
3260
+ const ny = y + dy;
3261
+ for (let dx = -1; dx <= 1; dx++) {
3211
3262
  const nx = x + dx;
3212
- const ny = y + dy;
3213
- if (nx >= 0 && ny >= 0 && nx < W && ny < H) g = Math.max(g, gradient(nx, ny));
3263
+ const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];
3264
+ if (n !== v) edge = true;
3265
+ sum += n;
3214
3266
  }
3215
3267
  }
3216
- const e = Math.min(1, g / BACKED);
3217
- const k = (y - y0) * w + (x - x0);
3218
- evidence[k] = e;
3219
- sum += e;
3220
- n++;
3221
- if (e < 0.5) seeds[k] = 1;
3268
+ if (edge) {
3269
+ const avg = sum / (9 * 255);
3270
+ out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);
3271
+ }
3222
3272
  }
3223
3273
  }
3224
- if (!n) return empty();
3225
- const rows = new Uint8Array(w * h);
3226
- for (let y = 0; y < h; y++) {
3227
- let last = -Infinity;
3228
- for (let x = 0; x < w; x++) {
3229
- if (seeds[y * w + x]) last = x;
3230
- if (x - last <= band) rows[y * w + x] = 1;
3231
- }
3232
- last = Infinity;
3233
- for (let x = w - 1; x >= 0; x--) {
3234
- if (seeds[y * w + x]) last = x;
3235
- if (last - x <= band) rows[y * w + x] = 1;
3274
+ return out;
3275
+ }
3276
+ function magicWand(pixels, seed, options = {}) {
3277
+ const n = checkPixels(pixels);
3278
+ const { width: w, height: h } = pixels;
3279
+ const sx = Math.floor(seed.x);
3280
+ const sy = Math.floor(seed.y);
3281
+ if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);
3282
+ const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? "rgb");
3283
+ const marks = new Uint8Array(n);
3284
+ let bx0 = w;
3285
+ let by0 = h;
3286
+ let bx1 = -1;
3287
+ let by1 = -1;
3288
+ if (options.contiguous ?? true) {
3289
+ const stack = [sy * w + sx];
3290
+ while (stack.length) {
3291
+ const idx = stack.pop();
3292
+ if (marks[idx]) continue;
3293
+ const y = idx / w | 0;
3294
+ const row = y * w;
3295
+ let l = idx - row;
3296
+ let r = l;
3297
+ while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;
3298
+ while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;
3299
+ marks.fill(255, row + l, row + r + 1);
3300
+ if (l < bx0) bx0 = l;
3301
+ if (r > bx1) bx1 = r;
3302
+ if (y < by0) by0 = y;
3303
+ if (y > by1) by1 = y;
3304
+ for (const ny of [y - 1, y + 1]) {
3305
+ if (ny < 0 || ny >= h) continue;
3306
+ const nrow = ny * w;
3307
+ let inRun = false;
3308
+ for (let nx = l; nx <= r; nx++) {
3309
+ const ok = !marks[nrow + nx] && similar(nrow + nx);
3310
+ if (ok && !inRun) stack.push(nrow + nx);
3311
+ inRun = ok;
3312
+ }
3313
+ }
3236
3314
  }
3237
- }
3238
- const grown = new Uint8Array(w * h);
3239
- for (let x = 0; x < w; x++) {
3240
- let last = -Infinity;
3315
+ } else {
3241
3316
  for (let y = 0; y < h; y++) {
3242
- if (rows[y * w + x]) last = y;
3243
- if (y - last <= band) grown[y * w + x] = 1;
3244
- }
3245
- last = Infinity;
3246
- for (let y = h - 1; y >= 0; y--) {
3247
- if (rows[y * w + x]) last = y;
3248
- if (last - y <= band) grown[y * w + x] = 1;
3249
- }
3250
- }
3251
- const label = new Int32Array(w * h);
3252
- const stack = new Int32Array(w * h);
3253
- const found = [];
3254
- for (let start = 0; start < w * h; start++) {
3255
- if (!grown[start] || label[start]) continue;
3256
- const r = { area: 0, e: 0, en: 0, bx0: w, by0: h, bx1: -1, by1: -1 };
3257
- const id = found.length + 1;
3258
- let top = 0;
3259
- stack[top++] = start;
3260
- label[start] = id;
3261
- while (top) {
3262
- const k = stack[--top];
3263
- const i = k % w;
3264
- const j = (k - i) / w;
3265
- r.area++;
3266
- if (!Number.isNaN(evidence[k])) {
3267
- r.e += evidence[k];
3268
- r.en++;
3269
- }
3270
- if (i < r.bx0) r.bx0 = i;
3271
- if (i > r.bx1) r.bx1 = i;
3272
- if (j < r.by0) r.by0 = j;
3273
- if (j > r.by1) r.by1 = j;
3274
- for (let dy = -1; dy <= 1; dy++) {
3275
- for (let dx = -1; dx <= 1; dx++) {
3276
- const ni = i + dx;
3277
- const nj = j + dy;
3278
- if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
3279
- const nk = nj * w + ni;
3280
- if (!grown[nk] || label[nk]) continue;
3281
- label[nk] = id;
3282
- stack[top++] = nk;
3317
+ const row = y * w;
3318
+ for (let x = 0; x < w; x++) {
3319
+ if (similar(row + x)) {
3320
+ marks[row + x] = 255;
3321
+ if (x < bx0) bx0 = x;
3322
+ if (x > bx1) bx1 = x;
3323
+ if (y < by0) by0 = y;
3324
+ if (y > by1) by1 = y;
3283
3325
  }
3284
3326
  }
3285
3327
  }
3286
- found.push(r);
3287
3328
  }
3288
- const unknownData = new Uint8Array(W * H);
3289
- for (let y = 0; y < h; y++) {
3290
- for (let x = 0; x < w; x++) {
3291
- const id = label[y * w + x];
3292
- if (id && found[id - 1].area >= minArea) unknownData[(y + y0) * W + x + x0] = 255;
3293
- }
3329
+ if (bx1 < 0) return createSelectionMask(w, h);
3330
+ if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);
3331
+ return createSelectionMask(
3332
+ w,
3333
+ h,
3334
+ antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))
3335
+ );
3336
+ }
3337
+ function sampleColor(c) {
3338
+ if (typeof c === "string") {
3339
+ const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
3340
+ if (!m) throw new Error(`Invalid colour "${c}"`);
3341
+ const v = parseInt(m[1], 16);
3342
+ return [v >> 16 & 255, v >> 8 & 255, v & 255];
3294
3343
  }
3295
- const kept = found.filter((r) => r.area >= minArea).map((r) => ({
3296
- bounds: { x: x0 + r.bx0, y: y0 + r.by0, width: r.bx1 - r.bx0 + 1, height: r.by1 - r.by0 + 1 },
3297
- area: r.area,
3298
- confidence: Math.round((r.en ? r.e / r.en : 0) * 1e3) / 1e3
3299
- })).sort((p, q) => p.confidence - q.confidence || q.area - p.area);
3300
- return {
3301
- unknown: createSelectionMask(W, H, unknownData),
3302
- regions: kept.slice(0, maxRegions),
3303
- count: kept.length,
3304
- confidence: Math.round(sum / n * 1e3) / 1e3
3305
- };
3344
+ return [c[0], c[1], c[2]];
3306
3345
  }
3307
-
3308
- // src/image-editor/selection/path.ts
3346
+ function colorRange(pixels, samples, options = {}) {
3347
+ const n = checkPixels(pixels);
3348
+ const { width: w, height: h, data } = pixels;
3349
+ const f = Math.max(0, options.fuzziness ?? 40) + 1;
3350
+ const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));
3351
+ const radius = options.localized !== void 0 ? Math.max(1, options.localized) : void 0;
3352
+ const out = new Uint8Array(n);
3353
+ if (!parsed.length) return createSelectionMask(w, h, out);
3354
+ for (let i = 0; i < n; i++) {
3355
+ const p = i * 4;
3356
+ if (data[p + 3] <= 0) continue;
3357
+ const x = i % w;
3358
+ const y = (i - x) / w;
3359
+ let best = 0;
3360
+ for (const s of parsed) {
3361
+ const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));
3362
+ let k = Math.min(1, Math.max(0, 1 - d / f));
3363
+ if (k > 0 && radius !== void 0 && s.at) {
3364
+ const dist2 = Math.hypot(x - s.at.x, y - s.at.y);
3365
+ k *= Math.min(1, Math.max(0, 1 - dist2 / radius));
3366
+ }
3367
+ if (k > best) best = k;
3368
+ }
3369
+ out[i] = coverageToByte(best);
3370
+ }
3371
+ return createSelectionMask(w, h, out);
3372
+ }
3373
+ var TONE_RANGE_PRESETS = {
3374
+ shadows: [0, 65],
3375
+ midtones: [105, 150],
3376
+ highlights: [190, 255]
3377
+ };
3378
+ function toneRange(pixels, range, falloff = 20) {
3379
+ const n = checkPixels(pixels);
3380
+ const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
3381
+ const ramp2 = Math.max(1e-3, falloff);
3382
+ const out = new Uint8Array(n);
3383
+ const d = pixels.data;
3384
+ for (let i = 0; i < n; i++) {
3385
+ const p = i * 4;
3386
+ if (d[p + 3] <= 0) continue;
3387
+ const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
3388
+ const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
3389
+ out[i] = coverageToByte(1 - outside / ramp2);
3390
+ }
3391
+ return createSelectionMask(pixels.width, pixels.height, out);
3392
+ }
3393
+ function hueRange(pixels, center) {
3394
+ const n = checkPixels(pixels);
3395
+ const out = new Uint8Array(n);
3396
+ const d = pixels.data;
3397
+ for (let i = 0; i < n; i++) {
3398
+ const p = i * 4;
3399
+ if (d[p + 3] <= 0) continue;
3400
+ const r = d[p] / 255;
3401
+ const g = d[p + 1] / 255;
3402
+ const b = d[p + 2] / 255;
3403
+ const mx = Math.max(r, g, b);
3404
+ const mn = Math.min(r, g, b);
3405
+ const chroma = mx - mn;
3406
+ if (chroma <= 0) continue;
3407
+ let hue;
3408
+ if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
3409
+ else if (mx === g) hue = (b - r) / chroma + 2;
3410
+ else hue = (r - g) / chroma + 4;
3411
+ hue *= 60;
3412
+ let dh = ((hue - center) % 360 + 360) % 360;
3413
+ dh = Math.min(dh, 360 - dh);
3414
+ out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));
3415
+ }
3416
+ return createSelectionMask(pixels.width, pixels.height, out);
3417
+ }
3418
+ var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
3419
+ var COLOR_RANGE_PRESETS = [
3420
+ ...Object.keys(HUE_RANGE_PRESETS),
3421
+ "highlights",
3422
+ "midtones",
3423
+ "shadows",
3424
+ "skinTones"
3425
+ ];
3426
+ function isColorRangePreset(v) {
3427
+ return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
3428
+ }
3429
+ 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;
3430
+ function skinToneRange(pixels) {
3431
+ const n = checkPixels(pixels);
3432
+ const out = new Uint8Array(n);
3433
+ const d = pixels.data;
3434
+ for (let i = 0; i < n; i++) {
3435
+ const p = i * 4;
3436
+ if (d[p + 3] <= 0) continue;
3437
+ const r = d[p] / 255;
3438
+ const g = d[p + 1] / 255;
3439
+ const b = d[p + 2] / 255;
3440
+ const mx = Math.max(r, g, b);
3441
+ const mn = Math.min(r, g, b);
3442
+ const chroma = mx - mn;
3443
+ if (chroma <= 0 || mx <= 0) continue;
3444
+ let hue;
3445
+ if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
3446
+ else if (mx === g) hue = (b - r) / chroma + 2;
3447
+ else hue = (r - g) / chroma + 4;
3448
+ hue *= 60;
3449
+ if (hue > 180) hue -= 360;
3450
+ const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
3451
+ out[i] = coverageToByte(k);
3452
+ }
3453
+ return createSelectionMask(pixels.width, pixels.height, out);
3454
+ }
3455
+ function colorRangePreset(pixels, preset, options = {}) {
3456
+ if (preset === "skinTones") return skinToneRange(pixels);
3457
+ if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
3458
+ if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
3459
+ throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
3460
+ }
3461
+ function selectedColorBins(pixels, sel, tolerance) {
3462
+ const S = 64;
3463
+ let bins = new Uint8Array(S * S * S);
3464
+ const d = pixels.data;
3465
+ const n = pixels.width * pixels.height;
3466
+ for (let i = 0; i < n; i++) {
3467
+ if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
3468
+ bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
3469
+ }
3470
+ const r = Math.round(Math.max(0, tolerance) / 4);
3471
+ if (r === 0) return bins;
3472
+ for (const stride of [1, S, S * S]) {
3473
+ const out = new Uint8Array(bins.length);
3474
+ for (let i = 0; i < bins.length; i++) {
3475
+ if (!bins[i]) continue;
3476
+ const c = Math.floor(i / stride) % S;
3477
+ const lo = Math.max(0, c - r);
3478
+ const hi = Math.min(S - 1, c + r);
3479
+ const base = i - c * stride;
3480
+ for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
3481
+ }
3482
+ bins = out;
3483
+ }
3484
+ return bins;
3485
+ }
3486
+ function matcher(pixels, sel, options) {
3487
+ if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
3488
+ const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
3489
+ const d = pixels.data;
3490
+ 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;
3491
+ }
3492
+ function growSelection(pixels, sel, options = {}) {
3493
+ checkPixels(pixels);
3494
+ const match = matcher(pixels, sel, options);
3495
+ const { width: w, height: h } = pixels;
3496
+ const out = new Uint8Array(sel.data);
3497
+ const stack = [];
3498
+ for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
3499
+ while (stack.length) {
3500
+ const i = stack.pop();
3501
+ const x = i % w;
3502
+ const y = (i - x) / w;
3503
+ const visit = (j) => {
3504
+ if (out[j] < 255 && match(j)) {
3505
+ out[j] = 255;
3506
+ stack.push(j);
3507
+ }
3508
+ };
3509
+ if (x > 0) visit(i - 1);
3510
+ if (x < w - 1) visit(i + 1);
3511
+ if (y > 0) visit(i - w);
3512
+ if (y < h - 1) visit(i + w);
3513
+ }
3514
+ return createSelectionMask(w, h, out);
3515
+ }
3516
+ function selectSimilar(pixels, sel, options = {}) {
3517
+ const n = checkPixels(pixels);
3518
+ const match = matcher(pixels, sel, options);
3519
+ const out = new Uint8Array(sel.data);
3520
+ for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
3521
+ return createSelectionMask(pixels.width, pixels.height, out);
3522
+ }
3523
+
3524
+ // src/image-editor/selection/recipe.ts
3525
+ var SELECTION_RECIPE_OPS = ["rect", "ellipse", "polygon", "magic-wand", "color-range", "color-range-preset", "all", "invert", "modify", "refine"];
3526
+ function isShapeStep(step) {
3527
+ return step.op !== "all" && step.op !== "invert" && step.op !== "modify" && step.op !== "refine";
3528
+ }
3529
+ function stepNeedsPixels(step) {
3530
+ return step.op === "magic-wand" || step.op === "color-range" || step.op === "color-range-preset" || step.op === "refine" && (step.params.radius ?? 8) > 0;
3531
+ }
3532
+ function samplingKey(step) {
3533
+ const s = step;
3534
+ return `${s.sample ?? "composite"}:${s.sampleMerged === true}`;
3535
+ }
3536
+ var fail = (msg) => {
3537
+ throw new Error(msg);
3538
+ };
3539
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
3540
+ var point = (v, at) => {
3541
+ const p = v;
3542
+ if (!p || !finite(p.x) || !finite(p.y)) fail(`${at} must be { x, y } in natural image pixels.`);
3543
+ return { x: p.x, y: p.y };
3544
+ };
3545
+ var rect = (v, at) => {
3546
+ const r = v;
3547
+ if (!r || !finite(r.x) || !finite(r.y) || !finite(r.width) || !finite(r.height) || r.width <= 0 || r.height <= 0) fail(`${at} must be { x, y, width, height } with a positive size.`);
3548
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
3549
+ };
3550
+ function readRecipeStep(raw, at = "step") {
3551
+ const s = raw;
3552
+ if (!s || typeof s !== "object") return fail(`${at} must be an object with an op.`);
3553
+ const combine = () => {
3554
+ const out = {};
3555
+ if (s.mode !== void 0) {
3556
+ if (s.mode !== "replace" && s.mode !== "add" && s.mode !== "subtract" && s.mode !== "intersect") fail(`${at}.mode must be replace, add, subtract or intersect.`);
3557
+ out.mode = s.mode;
3558
+ }
3559
+ if (s.feather !== void 0) {
3560
+ if (!finite(s.feather) || s.feather < 0 || s.feather > 1e3) fail(`${at}.feather must be 0 to 1000 px.`);
3561
+ if (s.feather > 0) out.feather = s.feather;
3562
+ }
3563
+ if (s.invert === true) out.invert = true;
3564
+ return out;
3565
+ };
3566
+ const sampling = () => {
3567
+ const out = {};
3568
+ if (s.sample !== void 0) {
3569
+ if (s.sample !== "composite" && s.sample !== "active-layer") fail(`${at}.sample must be composite or active-layer.`);
3570
+ out.sample = s.sample;
3571
+ }
3572
+ if (s.sampleMerged === true) out.sampleMerged = true;
3573
+ return out;
3574
+ };
3575
+ const options = () => s.options && typeof s.options === "object" ? { options: JSON.parse(JSON.stringify(s.options)) } : {};
3576
+ switch (s.op) {
3577
+ case "rect":
3578
+ case "ellipse":
3579
+ return { op: s.op, rect: rect(s.rect, `${at}.rect`), ...s.antiAlias === false ? { antiAlias: false } : {}, ...combine() };
3580
+ case "polygon": {
3581
+ if (!Array.isArray(s.points) || s.points.length < 3) fail(`${at}.points must be at least 3 { x, y } points.`);
3582
+ return { op: "polygon", points: s.points.map((p, i) => point(p, `${at}.points[${i}]`)), ...s.antiAlias === false ? { antiAlias: false } : {}, ...combine() };
3583
+ }
3584
+ case "magic-wand":
3585
+ return { op: "magic-wand", point: point(s.point, `${at}.point`), ...options(), ...combine(), ...sampling() };
3586
+ case "color-range":
3587
+ if (!Array.isArray(s.samples) || !s.samples.length) fail(`${at}.samples must be a non-empty list of colour samples.`);
3588
+ return { op: "color-range", samples: JSON.parse(JSON.stringify(s.samples)), ...options(), ...combine(), ...sampling() };
3589
+ case "color-range-preset":
3590
+ if (typeof s.preset !== "string" || !isColorRangePreset(s.preset)) fail(`${at}.preset is not a Color Range preset.`);
3591
+ return { op: "color-range-preset", preset: s.preset, ...finite(s.falloff) ? { falloff: s.falloff } : {}, ...combine(), ...sampling() };
3592
+ case "all":
3593
+ return { op: "all" };
3594
+ case "invert":
3595
+ return { op: "invert" };
3596
+ case "modify":
3597
+ if (!SELECTION_MODIFY_OPS.includes(s.modify)) fail(`${at}.modify must be one of ${SELECTION_MODIFY_OPS.join(", ")}.`);
3598
+ if (!finite(s.radius) || s.radius < 0 || s.radius > 500) fail(`${at}.radius must be 0 to 500 px.`);
3599
+ return { op: "modify", modify: s.modify, radius: s.radius };
3600
+ case "refine":
3601
+ if (!s.params || typeof s.params !== "object") fail(`${at}.params must be the refine settings (radius, smooth, feather, contrast, shiftEdge).`);
3602
+ return { op: "refine", params: JSON.parse(JSON.stringify(s.params)), ...sampling() };
3603
+ default:
3604
+ return fail(`${at}.op "${String(s.op)}" is not a recipe step; use ${SELECTION_RECIPE_OPS.join(", ")}.`);
3605
+ }
3606
+ }
3607
+ function replaySelectionRecipe(steps, width, height, pixelsFor) {
3608
+ let sel = null;
3609
+ steps.forEach((step, i) => {
3610
+ const shape = (mask) => {
3611
+ const st = step;
3612
+ let m = st.feather && st.feather > 0 ? modifySelection(mask, "feather", st.feather) : mask;
3613
+ if (st.invert) m = invertSelection(m);
3614
+ return combineSelections(sel, m, st.mode ?? "replace");
3615
+ };
3616
+ let next;
3617
+ switch (step.op) {
3618
+ case "rect":
3619
+ next = shape(rectSelection(width, height, step.rect, { antiAlias: step.antiAlias }));
3620
+ break;
3621
+ case "ellipse":
3622
+ next = shape(ellipseSelection(width, height, step.rect, { antiAlias: step.antiAlias }));
3623
+ break;
3624
+ case "polygon":
3625
+ next = shape(polygonSelection(width, height, step.points, { antiAlias: step.antiAlias }));
3626
+ break;
3627
+ case "magic-wand":
3628
+ next = shape(magicWand(pixelsFor(step), step.point, step.options ?? {}));
3629
+ break;
3630
+ case "color-range":
3631
+ next = shape(colorRange(pixelsFor(step), step.samples, step.options ?? {}));
3632
+ break;
3633
+ case "color-range-preset":
3634
+ next = shape(colorRangePreset(pixelsFor(step), step.preset, step.falloff !== void 0 ? { falloff: step.falloff } : {}));
3635
+ break;
3636
+ case "all":
3637
+ next = fullSelection(width, height);
3638
+ break;
3639
+ case "invert":
3640
+ next = sel ? invertSelection(sel) : fullSelection(width, height);
3641
+ break;
3642
+ case "modify":
3643
+ if (!sel?.bounds) throw new Error(`Step ${i} (modify) has nothing selected to ${step.modify}; an earlier step must select something.`);
3644
+ next = modifySelection(sel, step.modify, step.radius);
3645
+ break;
3646
+ case "refine":
3647
+ if (!sel?.bounds) throw new Error(`Step ${i} (refine) has nothing selected to refine; an earlier step must select something.`);
3648
+ next = refineSelection(stepNeedsPixels(step) ? pixelsFor(step) : null, sel, step.params);
3649
+ break;
3650
+ }
3651
+ sel = next?.bounds ? next : null;
3652
+ });
3653
+ return sel;
3654
+ }
3655
+ function describeRecipe(steps) {
3656
+ return steps.map((s, index) => ({ index, ...JSON.parse(JSON.stringify(s)) }));
3657
+ }
3658
+ var SHAPE_LABELS = { rect: "Rectangle", ellipse: "Ellipse" };
3659
+ var MODIFY_LABELS = { expand: "Expand", grow: "Expand", contract: "Contract", shrink: "Contract", feather: "Feather", border: "Border", smooth: "Smooth" };
3660
+ function describeRecipeStep(step) {
3661
+ const r = (n) => Math.round(n);
3662
+ switch (step.op) {
3663
+ case "rect":
3664
+ case "ellipse":
3665
+ return `${SHAPE_LABELS[step.op]} ${r(step.rect.width)} \xD7 ${r(step.rect.height)} at ${r(step.rect.x)}, ${r(step.rect.y)}`;
3666
+ case "polygon":
3667
+ return `Polygon, ${step.points.length} points`;
3668
+ case "magic-wand":
3669
+ return `Magic Wand at ${r(step.point.x)}, ${r(step.point.y)}`;
3670
+ case "color-range":
3671
+ return `Color Range, ${step.samples.length} sample${step.samples.length === 1 ? "" : "s"}`;
3672
+ case "color-range-preset":
3673
+ return `Color Range: ${step.preset}`;
3674
+ case "all":
3675
+ return "Select All";
3676
+ case "invert":
3677
+ return "Inverse";
3678
+ case "modify":
3679
+ return `${MODIFY_LABELS[step.modify] ?? step.modify} ${step.radius} px`;
3680
+ case "refine":
3681
+ return "Refine Edge";
3682
+ }
3683
+ }
3684
+ function recipeStepSetting(step) {
3685
+ switch (step.op) {
3686
+ case "rect":
3687
+ case "ellipse":
3688
+ case "polygon":
3689
+ return { label: "feather", value: step.feather ?? 0, min: 0, max: 1e3, change: (v) => ({ feather: v }) };
3690
+ case "magic-wand": {
3691
+ const options = step.options ?? {};
3692
+ return { label: "tolerance", value: Number(options.tolerance ?? 32), min: 0, max: 255, change: (v) => ({ options: { ...options, tolerance: v } }) };
3693
+ }
3694
+ case "color-range": {
3695
+ const options = step.options ?? {};
3696
+ return { label: "fuzziness", value: Number(options.fuzziness ?? 40), min: 0, max: 200, change: (v) => ({ options: { ...options, fuzziness: v } }) };
3697
+ }
3698
+ case "color-range-preset":
3699
+ return step.preset === "highlights" || step.preset === "midtones" || step.preset === "shadows" ? { label: "falloff", value: step.falloff ?? 20, min: 0, max: 255, change: (v) => ({ falloff: v }) } : null;
3700
+ case "modify":
3701
+ return { label: "radius", value: step.radius, min: 0, max: 500, change: (v) => ({ radius: v }) };
3702
+ case "refine":
3703
+ return { label: "radius", value: step.params.radius ?? 8, min: 0, max: 64, change: (v) => ({ params: { ...step.params, radius: v } }) };
3704
+ default:
3705
+ return null;
3706
+ }
3707
+ }
3708
+
3709
+ // src/image-editor/selection/uncertainty.ts
3710
+ var BACKED = 0.2;
3711
+ function selectionUncertainty(mask, image, options = {}) {
3712
+ const W = mask.width;
3713
+ const H = mask.height;
3714
+ const empty = () => ({ unknown: createSelectionMask(W, H), regions: [], count: 0, confidence: 1 });
3715
+ const b = mask.bounds;
3716
+ if (!b) return empty();
3717
+ const band = Math.max(1, Math.round(options.band ?? Math.max(2, Math.min(W, H) / 200)));
3718
+ const minArea = Math.max(1, options.minArea ?? 16);
3719
+ const maxRegions = Math.max(1, options.maxRegions ?? 20);
3720
+ const a = mask.data;
3721
+ const gradient = createGradientSampler(image, W);
3722
+ const x0 = Math.max(0, b.x - band - 1);
3723
+ const y0 = Math.max(0, b.y - band - 1);
3724
+ const x1 = Math.min(W, b.x + b.width + band + 1);
3725
+ const y1 = Math.min(H, b.y + b.height + band + 1);
3726
+ const w = x1 - x0;
3727
+ const h = y1 - y0;
3728
+ const evidence = new Float32Array(w * h).fill(NaN);
3729
+ const seeds = new Uint8Array(w * h);
3730
+ let sum = 0;
3731
+ let n = 0;
3732
+ const on = (x, y) => a[y * W + x] >= 128;
3733
+ for (let y = y0; y < y1; y++) {
3734
+ for (let x = x0; x < x1; x++) {
3735
+ const v = on(x, y);
3736
+ const differs = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && on(nx, ny) !== v;
3737
+ if (!(differs(x - 1, y) || differs(x + 1, y) || differs(x, y - 1) || differs(x, y + 1))) continue;
3738
+ let g = 0;
3739
+ for (let dy = -2; dy <= 2; dy++) {
3740
+ for (let dx = -2; dx <= 2; dx++) {
3741
+ const nx = x + dx;
3742
+ const ny = y + dy;
3743
+ if (nx >= 0 && ny >= 0 && nx < W && ny < H) g = Math.max(g, gradient(nx, ny));
3744
+ }
3745
+ }
3746
+ const e = Math.min(1, g / BACKED);
3747
+ const k = (y - y0) * w + (x - x0);
3748
+ evidence[k] = e;
3749
+ sum += e;
3750
+ n++;
3751
+ if (e < 0.5) seeds[k] = 1;
3752
+ }
3753
+ }
3754
+ if (!n) return empty();
3755
+ const rows = new Uint8Array(w * h);
3756
+ for (let y = 0; y < h; y++) {
3757
+ let last = -Infinity;
3758
+ for (let x = 0; x < w; x++) {
3759
+ if (seeds[y * w + x]) last = x;
3760
+ if (x - last <= band) rows[y * w + x] = 1;
3761
+ }
3762
+ last = Infinity;
3763
+ for (let x = w - 1; x >= 0; x--) {
3764
+ if (seeds[y * w + x]) last = x;
3765
+ if (last - x <= band) rows[y * w + x] = 1;
3766
+ }
3767
+ }
3768
+ const grown = new Uint8Array(w * h);
3769
+ for (let x = 0; x < w; x++) {
3770
+ let last = -Infinity;
3771
+ for (let y = 0; y < h; y++) {
3772
+ if (rows[y * w + x]) last = y;
3773
+ if (y - last <= band) grown[y * w + x] = 1;
3774
+ }
3775
+ last = Infinity;
3776
+ for (let y = h - 1; y >= 0; y--) {
3777
+ if (rows[y * w + x]) last = y;
3778
+ if (last - y <= band) grown[y * w + x] = 1;
3779
+ }
3780
+ }
3781
+ const label = new Int32Array(w * h);
3782
+ const stack = new Int32Array(w * h);
3783
+ const found = [];
3784
+ for (let start = 0; start < w * h; start++) {
3785
+ if (!grown[start] || label[start]) continue;
3786
+ const r = { area: 0, e: 0, en: 0, bx0: w, by0: h, bx1: -1, by1: -1 };
3787
+ const id = found.length + 1;
3788
+ let top = 0;
3789
+ stack[top++] = start;
3790
+ label[start] = id;
3791
+ while (top) {
3792
+ const k = stack[--top];
3793
+ const i = k % w;
3794
+ const j = (k - i) / w;
3795
+ r.area++;
3796
+ if (!Number.isNaN(evidence[k])) {
3797
+ r.e += evidence[k];
3798
+ r.en++;
3799
+ }
3800
+ if (i < r.bx0) r.bx0 = i;
3801
+ if (i > r.bx1) r.bx1 = i;
3802
+ if (j < r.by0) r.by0 = j;
3803
+ if (j > r.by1) r.by1 = j;
3804
+ for (let dy = -1; dy <= 1; dy++) {
3805
+ for (let dx = -1; dx <= 1; dx++) {
3806
+ const ni = i + dx;
3807
+ const nj = j + dy;
3808
+ if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
3809
+ const nk = nj * w + ni;
3810
+ if (!grown[nk] || label[nk]) continue;
3811
+ label[nk] = id;
3812
+ stack[top++] = nk;
3813
+ }
3814
+ }
3815
+ }
3816
+ found.push(r);
3817
+ }
3818
+ const unknownData = new Uint8Array(W * H);
3819
+ for (let y = 0; y < h; y++) {
3820
+ for (let x = 0; x < w; x++) {
3821
+ const id = label[y * w + x];
3822
+ if (id && found[id - 1].area >= minArea) unknownData[(y + y0) * W + x + x0] = 255;
3823
+ }
3824
+ }
3825
+ const kept = found.filter((r) => r.area >= minArea).map((r) => ({
3826
+ bounds: { x: x0 + r.bx0, y: y0 + r.by0, width: r.bx1 - r.bx0 + 1, height: r.by1 - r.by0 + 1 },
3827
+ area: r.area,
3828
+ confidence: Math.round((r.en ? r.e / r.en : 0) * 1e3) / 1e3
3829
+ })).sort((p, q) => p.confidence - q.confidence || q.area - p.area);
3830
+ return {
3831
+ unknown: createSelectionMask(W, H, unknownData),
3832
+ regions: kept.slice(0, maxRegions),
3833
+ count: kept.length,
3834
+ confidence: Math.round(sum / n * 1e3) / 1e3
3835
+ };
3836
+ }
3837
+
3838
+ // src/image-editor/selection/path.ts
3309
3839
  function selectionToSubpaths(mask, options = {}) {
3310
3840
  const b = mask.bounds;
3311
3841
  if (!b) return [];
@@ -3356,12 +3886,12 @@ function pathToSelection(components, width, height, options = {}) {
3356
3886
  const rx1 = Math.min(width, Math.ceil(x1));
3357
3887
  const ry1 = Math.min(height, Math.ceil(y1));
3358
3888
  if (!(rx1 > rx0 && ry1 > ry0)) return createSelectionMask(width, height);
3359
- const rect2 = { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 };
3360
- const cov = componentsCoverage(components, rect2, options.fillRule ?? "nonzero");
3889
+ const rect3 = { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 };
3890
+ const cov = componentsCoverage(components, rect3, options.fillRule ?? "nonzero");
3361
3891
  const hard = options.antiAlias === false;
3362
3892
  const bytes = new Uint8Array(cov.length);
3363
3893
  for (let i = 0; i < cov.length; i++) bytes[i] = hard ? cov[i] >= 0.5 ? 255 : 0 : coverageToByte(cov[i]);
3364
- const mask = selectionFromRegion(width, height, rect2, bytes);
3894
+ const mask = selectionFromRegion(width, height, rect3, bytes);
3365
3895
  const feather = options.feather ?? 0;
3366
3896
  return feather > 0 && mask.bounds ? modifySelection(mask, "feather", feather) : mask;
3367
3897
  }
@@ -3471,9 +4001,9 @@ var EdgeMap = class {
3471
4001
  }
3472
4002
  };
3473
4003
  var clampPixel = (v, max) => Math.min(max - 1, Math.max(0, Math.floor(v)));
3474
- function snapToEdge(map, point, width = 10) {
3475
- const cx = clampPixel(point.x, map.width);
3476
- const cy = clampPixel(point.y, map.height);
4004
+ function snapToEdge(map, point2, width = 10) {
4005
+ const cx = clampPixel(point2.x, map.width);
4006
+ const cy = clampPixel(point2.y, map.height);
3477
4007
  const r = Math.max(0, Math.round(width));
3478
4008
  let best = 0;
3479
4009
  let bestD = Infinity;
@@ -3492,7 +4022,7 @@ function snapToEdge(map, point, width = 10) {
3492
4022
  }
3493
4023
  }
3494
4024
  }
3495
- return best > 0 ? { x: bx + 0.5, y: by + 0.5 } : point;
4025
+ return best > 0 ? { x: bx + 0.5, y: by + 0.5 } : point2;
3496
4026
  }
3497
4027
  var Heap = class {
3498
4028
  ids = new Int32Array(256);
@@ -4004,504 +4534,174 @@ var TextEditSession = class {
4004
4534
  else if (step === "next") this.move(mod2 ? "end" : "right", e.shiftKey);
4005
4535
  else if (step === "line-before") this.move(mod2 ? "start" : "up", e.shiftKey);
4006
4536
  else this.move(mod2 ? "finish" : "down", e.shiftKey);
4007
- return true;
4008
- }
4009
- case "Home":
4010
- this.move("home", e.shiftKey);
4011
- return true;
4012
- case "End":
4013
- this.move("end", e.shiftKey);
4014
- return true;
4015
- case "Tab":
4016
- this.insert(" ");
4017
- return true;
4018
- }
4019
- if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
4020
- this.insert(e.key);
4021
- return true;
4022
- }
4023
- return false;
4024
- }
4025
- /** Sets character style fields on the selection, or on the whole text when nothing is selected. */
4026
- setStyle(style) {
4027
- const s = this.start;
4028
- const e = this.end;
4029
- const range = s === e ? {} : { start: s, end: e };
4030
- this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
4031
- }
4032
- /** Sets paragraph fields on the paragraphs the selection touches. */
4033
- setParagraph(style) {
4034
- const s = this.start;
4035
- const e = this.end;
4036
- this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
4037
- }
4038
- /** The style at the caret (or the selection's start), for the options bar. */
4039
- styleHere() {
4040
- const layer = this.layer;
4041
- const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
4042
- return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
4043
- }
4044
- info() {
4045
- return this.controller.getTextLayer(this.layerId);
4046
- }
4047
- /** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
4048
- overlay() {
4049
- const layer = this.layer;
4050
- const layout = this.layout();
4051
- const m = textMatrix(layer);
4052
- const at = (x, y) => applyMatrix(m, { x, y });
4053
- let frame = null;
4054
- if (layer.frame.kind === "box") frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];
4055
- else if (layout) {
4056
- const b = layout.bounds;
4057
- frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];
4058
- }
4059
- if (!layout) return { caret: null, selection: [], frame };
4060
- const selection = textSelectionQuads(layout, this.start, this.end).map((q) => q.map((p) => at(p.x, p.y)));
4061
- const c = caretAt(layout, this.focus);
4062
- return { caret: [at(c.from.x, c.from.y), at(c.to.x, c.to.y)], selection, frame };
4063
- }
4064
- /** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
4065
- close() {
4066
- if (!this.alive) return false;
4067
- if (!this.text.trim().length) {
4068
- try {
4069
- this.controller.removeLayer(this.layerId);
4070
- } catch {
4071
- }
4072
- return false;
4073
- }
4074
- if (this.layer.name === "Text" && this.changed) {
4075
- try {
4076
- this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
4077
- } catch {
4078
- }
4079
- }
4080
- return true;
4081
- }
4082
- };
4083
- function typeLayerAt(controller, p, slop = 4) {
4084
- const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
4085
- for (const l of layers) {
4086
- if (!l.visible) continue;
4087
- const inv = invert(textMatrix(l));
4088
- const q = applyMatrix(inv, p);
4089
- let b = null;
4090
- if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
4091
- else {
4092
- const layout = controller.getTextLayout(l.id);
4093
- if (layout) b = layout.bounds;
4094
- }
4095
- if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;
4096
- }
4097
- return null;
4098
- }
4099
- function flattenTypeLayers(layers) {
4100
- const out = [];
4101
- const walk = (list) => {
4102
- for (const l of list) {
4103
- if (l.type === "type") out.push(l);
4104
- else if (l.type === "group") walk(l.children);
4105
- }
4106
- };
4107
- walk(layers);
4108
- return out;
4109
- }
4110
- function invert(m) {
4111
- const [a, b, c, d, e, f] = m;
4112
- const det = a * d - b * c || 1;
4113
- return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
4114
- }
4115
- var TypeToolGesture = class {
4116
- constructor(controller, options) {
4117
- this.controller = controller;
4118
- this.options = options;
4119
- }
4120
- controller;
4121
- options;
4122
- session = null;
4123
- dragStart = null;
4124
- error = null;
4125
- down(p, mods = {}) {
4126
- this.error = null;
4127
- const hit = typeLayerAt(this.controller, p);
4128
- if (this.session && this.session.alive && hit === this.session.layerId) {
4129
- this.session.pointAt(p, mods.shift);
4130
- this.dragStart = null;
4131
- return;
4132
- }
4133
- this.finish();
4134
- if (hit) {
4135
- this.controller.setActiveLayer(hit);
4136
- this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
4137
- this.dragStart = null;
4138
- return;
4139
- }
4140
- this.dragStart = { x: p.x, y: p.y };
4141
- }
4142
- /** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
4143
- up(p) {
4144
- const start = this.dragStart;
4145
- this.dragStart = null;
4146
- if (!start) return this.session;
4147
- const o = this.options();
4148
- const w = Math.abs(p.x - start.x);
4149
- const h = Math.abs(p.y - start.y);
4150
- try {
4151
- const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
4152
- const id = this.controller.addTextLayer({
4153
- text: "",
4154
- x: box ? Math.min(start.x, p.x) : start.x,
4155
- y: box ? Math.min(start.y, p.y) : start.y,
4156
- ...box ? { box } : {},
4157
- style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
4158
- paragraph: o.align !== "left" ? { align: o.align } : {},
4159
- ...o.orientation === "vertical" ? { orientation: "vertical" } : {},
4160
- name: "Text"
4161
- });
4162
- this.session = new TextEditSession(this.controller, id, 0);
4163
- } catch (e) {
4164
- this.error = e instanceof Error ? e.message : String(e);
4537
+ return true;
4538
+ }
4539
+ case "Home":
4540
+ this.move("home", e.shiftKey);
4541
+ return true;
4542
+ case "End":
4543
+ this.move("end", e.shiftKey);
4544
+ return true;
4545
+ case "Tab":
4546
+ this.insert(" ");
4547
+ return true;
4165
4548
  }
4166
- return this.session;
4549
+ if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
4550
+ this.insert(e.key);
4551
+ return true;
4552
+ }
4553
+ return false;
4167
4554
  }
4168
- /** Ends the open session (an empty new layer is removed). */
4169
- finish() {
4170
- const s = this.session;
4171
- this.session = null;
4172
- s?.close();
4555
+ /** Sets character style fields on the selection, or on the whole text when nothing is selected. */
4556
+ setStyle(style) {
4557
+ const s = this.start;
4558
+ const e = this.end;
4559
+ const range = s === e ? {} : { start: s, end: e };
4560
+ this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
4173
4561
  }
4174
- };
4175
-
4176
- // src/image-editor/selection/wand.ts
4177
- function checkPixels(p) {
4178
- const n = p.width * p.height;
4179
- if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {
4180
- throw new Error("Invalid pixel buffer size");
4562
+ /** Sets paragraph fields on the paragraphs the selection touches. */
4563
+ setParagraph(style) {
4564
+ const s = this.start;
4565
+ const e = this.end;
4566
+ this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
4181
4567
  }
4182
- if (p.data.length < n * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
4183
- return n;
4184
- }
4185
- var SRGB_TO_LINEAR2 = (() => {
4186
- const t = new Float32Array(256);
4187
- for (let i = 0; i < 256; i++) {
4188
- const v = i / 255;
4189
- t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
4568
+ /** The style at the caret (or the selection's start), for the options bar. */
4569
+ styleHere() {
4570
+ const layer = this.layer;
4571
+ const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
4572
+ return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
4190
4573
  }
4191
- return t;
4192
- })();
4193
- var labF = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
4194
- function srgb8ToLab(r, g, b) {
4195
- const rl = SRGB_TO_LINEAR2[r & 255];
4196
- const gl = SRGB_TO_LINEAR2[g & 255];
4197
- const bl = SRGB_TO_LINEAR2[b & 255];
4198
- const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;
4199
- const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
4200
- const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;
4201
- const fx = labF(x);
4202
- const fy = labF(y);
4203
- const fz = labF(z);
4204
- return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
4205
- }
4206
- function similarity(pixels, seed, tolerance, distance) {
4207
- const d = pixels.data;
4208
- const tr = d[seed * 4];
4209
- const tg = d[seed * 4 + 1];
4210
- const tb = d[seed * 4 + 2];
4211
- const ta = d[seed * 4 + 3];
4212
- const tol = Math.max(0, Math.floor(tolerance + 1e-3));
4213
- if (distance === "perceptual") {
4214
- const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);
4215
- const de = Math.max(0, tolerance) * 100 / 255;
4216
- const de2 = de * de + 1e-6;
4217
- return (i) => {
4218
- const p = i * 4;
4219
- const a = d[p + 3];
4220
- if (ta === 0 && a === 0) return true;
4221
- if (Math.abs(a - ta) > tol) return false;
4222
- const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);
4223
- return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;
4224
- };
4574
+ info() {
4575
+ return this.controller.getTextLayer(this.layerId);
4225
4576
  }
4226
- return (i) => {
4227
- const p = i * 4;
4228
- const a = d[p + 3];
4229
- if (ta === 0 && a === 0) return true;
4230
- return Math.abs(d[p] - tr) <= tol && Math.abs(d[p + 1] - tg) <= tol && Math.abs(d[p + 2] - tb) <= tol && Math.abs(a - ta) <= tol;
4231
- };
4232
- }
4233
- function antialiasMarks(marks, w, h, x0, y0, x1, y1) {
4234
- const out = marks.slice();
4235
- for (let y = y0; y < y1; y++) {
4236
- for (let x = x0; x < x1; x++) {
4237
- const i = y * w + x;
4238
- const v = marks[i];
4239
- let sum = 0;
4240
- let edge = false;
4241
- for (let dy = -1; dy <= 1; dy++) {
4242
- const ny = y + dy;
4243
- for (let dx = -1; dx <= 1; dx++) {
4244
- const nx = x + dx;
4245
- const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];
4246
- if (n !== v) edge = true;
4247
- sum += n;
4248
- }
4249
- }
4250
- if (edge) {
4251
- const avg = sum / (9 * 255);
4252
- out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);
4253
- }
4577
+ /** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
4578
+ overlay() {
4579
+ const layer = this.layer;
4580
+ const layout = this.layout();
4581
+ const m = textMatrix(layer);
4582
+ const at = (x, y) => applyMatrix(m, { x, y });
4583
+ let frame = null;
4584
+ if (layer.frame.kind === "box") frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];
4585
+ else if (layout) {
4586
+ const b = layout.bounds;
4587
+ frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];
4254
4588
  }
4589
+ if (!layout) return { caret: null, selection: [], frame };
4590
+ const selection = textSelectionQuads(layout, this.start, this.end).map((q) => q.map((p) => at(p.x, p.y)));
4591
+ const c = caretAt(layout, this.focus);
4592
+ return { caret: [at(c.from.x, c.from.y), at(c.to.x, c.to.y)], selection, frame };
4255
4593
  }
4256
- return out;
4257
- }
4258
- function magicWand(pixels, seed, options = {}) {
4259
- const n = checkPixels(pixels);
4260
- const { width: w, height: h } = pixels;
4261
- const sx = Math.floor(seed.x);
4262
- const sy = Math.floor(seed.y);
4263
- if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);
4264
- const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? "rgb");
4265
- const marks = new Uint8Array(n);
4266
- let bx0 = w;
4267
- let by0 = h;
4268
- let bx1 = -1;
4269
- let by1 = -1;
4270
- if (options.contiguous ?? true) {
4271
- const stack = [sy * w + sx];
4272
- while (stack.length) {
4273
- const idx = stack.pop();
4274
- if (marks[idx]) continue;
4275
- const y = idx / w | 0;
4276
- const row = y * w;
4277
- let l = idx - row;
4278
- let r = l;
4279
- while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;
4280
- while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;
4281
- marks.fill(255, row + l, row + r + 1);
4282
- if (l < bx0) bx0 = l;
4283
- if (r > bx1) bx1 = r;
4284
- if (y < by0) by0 = y;
4285
- if (y > by1) by1 = y;
4286
- for (const ny of [y - 1, y + 1]) {
4287
- if (ny < 0 || ny >= h) continue;
4288
- const nrow = ny * w;
4289
- let inRun = false;
4290
- for (let nx = l; nx <= r; nx++) {
4291
- const ok = !marks[nrow + nx] && similar(nrow + nx);
4292
- if (ok && !inRun) stack.push(nrow + nx);
4293
- inRun = ok;
4294
- }
4295
- }
4296
- }
4297
- } else {
4298
- for (let y = 0; y < h; y++) {
4299
- const row = y * w;
4300
- for (let x = 0; x < w; x++) {
4301
- if (similar(row + x)) {
4302
- marks[row + x] = 255;
4303
- if (x < bx0) bx0 = x;
4304
- if (x > bx1) bx1 = x;
4305
- if (y < by0) by0 = y;
4306
- if (y > by1) by1 = y;
4307
- }
4594
+ /** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
4595
+ close() {
4596
+ if (!this.alive) return false;
4597
+ if (!this.text.trim().length) {
4598
+ try {
4599
+ this.controller.removeLayer(this.layerId);
4600
+ } catch {
4308
4601
  }
4602
+ return false;
4309
4603
  }
4310
- }
4311
- if (bx1 < 0) return createSelectionMask(w, h);
4312
- if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);
4313
- return createSelectionMask(
4314
- w,
4315
- h,
4316
- antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))
4317
- );
4318
- }
4319
- function sampleColor(c) {
4320
- if (typeof c === "string") {
4321
- const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
4322
- if (!m) throw new Error(`Invalid colour "${c}"`);
4323
- const v = parseInt(m[1], 16);
4324
- return [v >> 16 & 255, v >> 8 & 255, v & 255];
4325
- }
4326
- return [c[0], c[1], c[2]];
4327
- }
4328
- function colorRange(pixels, samples, options = {}) {
4329
- const n = checkPixels(pixels);
4330
- const { width: w, height: h, data } = pixels;
4331
- const f = Math.max(0, options.fuzziness ?? 40) + 1;
4332
- const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));
4333
- const radius = options.localized !== void 0 ? Math.max(1, options.localized) : void 0;
4334
- const out = new Uint8Array(n);
4335
- if (!parsed.length) return createSelectionMask(w, h, out);
4336
- for (let i = 0; i < n; i++) {
4337
- const p = i * 4;
4338
- if (data[p + 3] <= 0) continue;
4339
- const x = i % w;
4340
- const y = (i - x) / w;
4341
- let best = 0;
4342
- for (const s of parsed) {
4343
- const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));
4344
- let k = Math.min(1, Math.max(0, 1 - d / f));
4345
- if (k > 0 && radius !== void 0 && s.at) {
4346
- const dist2 = Math.hypot(x - s.at.x, y - s.at.y);
4347
- k *= Math.min(1, Math.max(0, 1 - dist2 / radius));
4604
+ if (this.layer.name === "Text" && this.changed) {
4605
+ try {
4606
+ this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
4607
+ } catch {
4348
4608
  }
4349
- if (k > best) best = k;
4350
4609
  }
4351
- out[i] = coverageToByte(best);
4352
- }
4353
- return createSelectionMask(w, h, out);
4354
- }
4355
- var TONE_RANGE_PRESETS = {
4356
- shadows: [0, 65],
4357
- midtones: [105, 150],
4358
- highlights: [190, 255]
4359
- };
4360
- function toneRange(pixels, range, falloff = 20) {
4361
- const n = checkPixels(pixels);
4362
- const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
4363
- const ramp2 = Math.max(1e-3, falloff);
4364
- const out = new Uint8Array(n);
4365
- const d = pixels.data;
4366
- for (let i = 0; i < n; i++) {
4367
- const p = i * 4;
4368
- if (d[p + 3] <= 0) continue;
4369
- const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
4370
- const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
4371
- out[i] = coverageToByte(1 - outside / ramp2);
4610
+ return true;
4372
4611
  }
4373
- return createSelectionMask(pixels.width, pixels.height, out);
4374
- }
4375
- function hueRange(pixels, center) {
4376
- const n = checkPixels(pixels);
4377
- const out = new Uint8Array(n);
4378
- const d = pixels.data;
4379
- for (let i = 0; i < n; i++) {
4380
- const p = i * 4;
4381
- if (d[p + 3] <= 0) continue;
4382
- const r = d[p] / 255;
4383
- const g = d[p + 1] / 255;
4384
- const b = d[p + 2] / 255;
4385
- const mx = Math.max(r, g, b);
4386
- const mn = Math.min(r, g, b);
4387
- const chroma = mx - mn;
4388
- if (chroma <= 0) continue;
4389
- let hue;
4390
- if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
4391
- else if (mx === g) hue = (b - r) / chroma + 2;
4392
- else hue = (r - g) / chroma + 4;
4393
- hue *= 60;
4394
- let dh = ((hue - center) % 360 + 360) % 360;
4395
- dh = Math.min(dh, 360 - dh);
4396
- out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));
4612
+ };
4613
+ function typeLayerAt(controller, p, slop = 4) {
4614
+ const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
4615
+ for (const l of layers) {
4616
+ if (!l.visible) continue;
4617
+ const inv = invert(textMatrix(l));
4618
+ const q = applyMatrix(inv, p);
4619
+ let b = null;
4620
+ if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
4621
+ else {
4622
+ const layout = controller.getTextLayout(l.id);
4623
+ if (layout) b = layout.bounds;
4624
+ }
4625
+ if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;
4397
4626
  }
4398
- return createSelectionMask(pixels.width, pixels.height, out);
4399
- }
4400
- var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
4401
- var COLOR_RANGE_PRESETS = [
4402
- ...Object.keys(HUE_RANGE_PRESETS),
4403
- "highlights",
4404
- "midtones",
4405
- "shadows",
4406
- "skinTones"
4407
- ];
4408
- function isColorRangePreset(v) {
4409
- return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
4627
+ return null;
4410
4628
  }
4411
- 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;
4412
- function skinToneRange(pixels) {
4413
- const n = checkPixels(pixels);
4414
- const out = new Uint8Array(n);
4415
- const d = pixels.data;
4416
- for (let i = 0; i < n; i++) {
4417
- const p = i * 4;
4418
- if (d[p + 3] <= 0) continue;
4419
- const r = d[p] / 255;
4420
- const g = d[p + 1] / 255;
4421
- const b = d[p + 2] / 255;
4422
- const mx = Math.max(r, g, b);
4423
- const mn = Math.min(r, g, b);
4424
- const chroma = mx - mn;
4425
- if (chroma <= 0 || mx <= 0) continue;
4426
- let hue;
4427
- if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
4428
- else if (mx === g) hue = (b - r) / chroma + 2;
4429
- else hue = (r - g) / chroma + 4;
4430
- hue *= 60;
4431
- if (hue > 180) hue -= 360;
4432
- const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
4433
- out[i] = coverageToByte(k);
4434
- }
4435
- return createSelectionMask(pixels.width, pixels.height, out);
4629
+ function flattenTypeLayers(layers) {
4630
+ const out = [];
4631
+ const walk = (list) => {
4632
+ for (const l of list) {
4633
+ if (l.type === "type") out.push(l);
4634
+ else if (l.type === "group") walk(l.children);
4635
+ }
4636
+ };
4637
+ walk(layers);
4638
+ return out;
4436
4639
  }
4437
- function colorRangePreset(pixels, preset, options = {}) {
4438
- if (preset === "skinTones") return skinToneRange(pixels);
4439
- if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
4440
- if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
4441
- throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
4640
+ function invert(m) {
4641
+ const [a, b, c, d, e, f] = m;
4642
+ const det = a * d - b * c || 1;
4643
+ return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
4442
4644
  }
4443
- function selectedColorBins(pixels, sel, tolerance) {
4444
- const S = 64;
4445
- let bins = new Uint8Array(S * S * S);
4446
- const d = pixels.data;
4447
- const n = pixels.width * pixels.height;
4448
- for (let i = 0; i < n; i++) {
4449
- if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
4450
- bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
4645
+ var TypeToolGesture = class {
4646
+ constructor(controller, options) {
4647
+ this.controller = controller;
4648
+ this.options = options;
4451
4649
  }
4452
- const r = Math.round(Math.max(0, tolerance) / 4);
4453
- if (r === 0) return bins;
4454
- for (const stride of [1, S, S * S]) {
4455
- const out = new Uint8Array(bins.length);
4456
- for (let i = 0; i < bins.length; i++) {
4457
- if (!bins[i]) continue;
4458
- const c = Math.floor(i / stride) % S;
4459
- const lo = Math.max(0, c - r);
4460
- const hi = Math.min(S - 1, c + r);
4461
- const base = i - c * stride;
4462
- for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
4650
+ controller;
4651
+ options;
4652
+ session = null;
4653
+ dragStart = null;
4654
+ error = null;
4655
+ down(p, mods = {}) {
4656
+ this.error = null;
4657
+ const hit = typeLayerAt(this.controller, p);
4658
+ if (this.session && this.session.alive && hit === this.session.layerId) {
4659
+ this.session.pointAt(p, mods.shift);
4660
+ this.dragStart = null;
4661
+ return;
4463
4662
  }
4464
- bins = out;
4663
+ this.finish();
4664
+ if (hit) {
4665
+ this.controller.setActiveLayer(hit);
4666
+ this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
4667
+ this.dragStart = null;
4668
+ return;
4669
+ }
4670
+ this.dragStart = { x: p.x, y: p.y };
4465
4671
  }
4466
- return bins;
4467
- }
4468
- function matcher(pixels, sel, options) {
4469
- if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
4470
- const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
4471
- const d = pixels.data;
4472
- 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;
4473
- }
4474
- function growSelection(pixels, sel, options = {}) {
4475
- checkPixels(pixels);
4476
- const match = matcher(pixels, sel, options);
4477
- const { width: w, height: h } = pixels;
4478
- const out = new Uint8Array(sel.data);
4479
- const stack = [];
4480
- for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
4481
- while (stack.length) {
4482
- const i = stack.pop();
4483
- const x = i % w;
4484
- const y = (i - x) / w;
4485
- const visit = (j) => {
4486
- if (out[j] < 255 && match(j)) {
4487
- out[j] = 255;
4488
- stack.push(j);
4489
- }
4490
- };
4491
- if (x > 0) visit(i - 1);
4492
- if (x < w - 1) visit(i + 1);
4493
- if (y > 0) visit(i - w);
4494
- if (y < h - 1) visit(i + w);
4672
+ /** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
4673
+ up(p) {
4674
+ const start = this.dragStart;
4675
+ this.dragStart = null;
4676
+ if (!start) return this.session;
4677
+ const o = this.options();
4678
+ const w = Math.abs(p.x - start.x);
4679
+ const h = Math.abs(p.y - start.y);
4680
+ try {
4681
+ const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
4682
+ const id = this.controller.addTextLayer({
4683
+ text: "",
4684
+ x: box ? Math.min(start.x, p.x) : start.x,
4685
+ y: box ? Math.min(start.y, p.y) : start.y,
4686
+ ...box ? { box } : {},
4687
+ style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
4688
+ paragraph: o.align !== "left" ? { align: o.align } : {},
4689
+ ...o.orientation === "vertical" ? { orientation: "vertical" } : {},
4690
+ name: "Text"
4691
+ });
4692
+ this.session = new TextEditSession(this.controller, id, 0);
4693
+ } catch (e) {
4694
+ this.error = e instanceof Error ? e.message : String(e);
4695
+ }
4696
+ return this.session;
4495
4697
  }
4496
- return createSelectionMask(w, h, out);
4497
- }
4498
- function selectSimilar(pixels, sel, options = {}) {
4499
- const n = checkPixels(pixels);
4500
- const match = matcher(pixels, sel, options);
4501
- const out = new Uint8Array(sel.data);
4502
- for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
4503
- return createSelectionMask(pixels.width, pixels.height, out);
4504
- }
4698
+ /** Ends the open session (an empty new layer is removed). */
4699
+ finish() {
4700
+ const s = this.session;
4701
+ this.session = null;
4702
+ s?.close();
4703
+ }
4704
+ };
4505
4705
 
4506
4706
  // src/image-editor/selection/segmentation.ts
4507
4707
  var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
@@ -4786,7 +4986,7 @@ var ImageEffectsEngine = class {
4786
4986
 
4787
4987
  // src/image-editor/free-transform.ts
4788
4988
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
4789
- var fail = (message) => {
4989
+ var fail2 = (message) => {
4790
4990
  throw new LayerOperationError(message);
4791
4991
  };
4792
4992
  var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
@@ -4867,20 +5067,20 @@ function liftMask(mask, sel, o) {
4867
5067
  var FreeTransformSession = class {
4868
5068
  constructor(host, init = {}) {
4869
5069
  this.host = host;
4870
- if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
5070
+ if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail2(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
4871
5071
  this.interp = init.interpolation ?? "bicubic";
4872
5072
  const sel = host.selection();
4873
5073
  const selected = Boolean(sel?.bounds);
4874
5074
  let content = init.content ?? "auto";
4875
5075
  if (!["auto", "layer", "selection", "selection-copy", "selection-outline"].includes(content)) {
4876
- fail(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
5076
+ fail2(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
4877
5077
  }
4878
5078
  const targetId = init.layerId ?? host.activeLayerId();
4879
5079
  const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
4880
5080
  const smartTarget = (target?.type === "smart" || target?.type === "shape" || target?.type === "type") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
4881
5081
  if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
4882
5082
  this.content = content;
4883
- if (content !== "layer" && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
5083
+ if (content !== "layer" && !selected) fail2(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
4884
5084
  const layers = host.layers();
4885
5085
  this.startLayers = layers;
4886
5086
  this.startSelection = sel;
@@ -4895,17 +5095,17 @@ var FreeTransformSession = class {
4895
5095
  box = { ...sel.bounds };
4896
5096
  } else {
4897
5097
  const id = init.layerId ?? host.activeLayerId();
4898
- if (!id) fail("No layer given and none is active; pass layerId (see list_layers).");
5098
+ if (!id) fail2("No layer given and none is active; pass layerId (see list_layers).");
4899
5099
  const layer = requireLayer(layers, id).layer;
4900
5100
  this.layerId = layer.id;
4901
5101
  const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
4902
5102
  const channel = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : "pixels");
4903
- if (channel !== "pixels" && channel !== "mask") fail(`Unknown channel "${String(channel)}"; use pixels or mask.`);
5103
+ if (channel !== "pixels" && channel !== "mask") fail2(`Unknown channel "${String(channel)}"; use pixels or mask.`);
4904
5104
  const linked = mask?.linked !== false;
4905
5105
  const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
4906
- if (channel === "mask" && !mask) fail(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
4907
- if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type") fail(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape, type and smart object layers, layer masks (channel mask) and selections.`);
4908
- if (!layer.visible && content !== "selection-copy") fail(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
5106
+ if (channel === "mask" && !mask) fail2(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
5107
+ if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type") fail2(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel, shape, type and smart object layers, layer masks (channel mask) and selections.`);
5108
+ if (!layer.visible && content !== "selection-copy") fail2(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
4909
5109
  if (content !== "selection-copy") {
4910
5110
  requireUnlocked(layer, "position", "transform it");
4911
5111
  if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
@@ -4913,8 +5113,8 @@ var FreeTransformSession = class {
4913
5113
  const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
4914
5114
  const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
4915
5115
  if (layer.type === "smart" && !maskAlone) {
4916
- if (content !== "layer") fail(`Layer "${layer.name || id}" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
4917
- if (!isContentsDecoded(layer.contents)) fail(`The contents of "${layer.name || id}" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);
5116
+ if (content !== "layer") fail2(`Layer "${layer.name || id}" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
5117
+ if (!isContentsDecoded(layer.contents)) fail2(`The contents of "${layer.name || id}" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);
4918
5118
  this.smart = layer;
4919
5119
  this.smartObject = true;
4920
5120
  this.startGeometry = placementGeometry(layer);
@@ -4927,7 +5127,7 @@ var FreeTransformSession = class {
4927
5127
  return;
4928
5128
  }
4929
5129
  if (layer.type === "type" && !maskAlone) {
4930
- if (content !== "layer") fail(`Layer "${layer.name || id}" is a type layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
5130
+ if (content !== "layer") fail2(`Layer "${layer.name || id}" is a type layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
4931
5131
  const frame = typeFrame(layer, host.textEnv().shaper);
4932
5132
  const m = textMatrix(layer);
4933
5133
  const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
@@ -4947,9 +5147,9 @@ var FreeTransformSession = class {
4947
5147
  return;
4948
5148
  }
4949
5149
  if (layer.type === "shape" && !maskAlone) {
4950
- if (content !== "layer") fail(`Layer "${layer.name || id}" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
5150
+ if (content !== "layer") fail2(`Layer "${layer.name || id}" is a shape layer, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);
4951
5151
  const g = shapeGeometryBounds(layer);
4952
- if (!g) fail(`Shape layer "${layer.name || id}" has no path to transform.`);
5152
+ if (!g) fail2(`Shape layer "${layer.name || id}" has no path to transform.`);
4953
5153
  const w = Math.max(g.width, 1);
4954
5154
  const h = Math.max(g.height, 1);
4955
5155
  box = { x: g.x + (g.width - w) / 2, y: g.y + (g.height - h) / 2, width: w, height: h };
@@ -4961,7 +5161,7 @@ var FreeTransformSession = class {
4961
5161
  moves.push("mask");
4962
5162
  }
4963
5163
  } else if (maskAlone) {
4964
- if (content === "selection-copy") fail("Layer via Copy copies pixels; transform a mask with content layer or selection.");
5164
+ if (content === "selection-copy") fail2("Layer via Copy copies pixels; transform a mask with content layer or selection.");
4965
5165
  if (content === "selection") {
4966
5166
  const { lifted, rest } = liftMask(mask.surface, sel, o);
4967
5167
  this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
@@ -5016,8 +5216,8 @@ var FreeTransformSession = class {
5016
5216
  moves.push("selection");
5017
5217
  }
5018
5218
  }
5019
- if (!box && moves[0] === "mask") fail("The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).");
5020
- if (!box) fail(content === "layer" ? "The layer has no pixels to transform." : "The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).");
5219
+ if (!box && moves[0] === "mask") fail2("The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).");
5220
+ if (!box) fail2(content === "layer" ? "The layer has no pixels to transform." : "The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).");
5021
5221
  this.box = box;
5022
5222
  this.moves = moves;
5023
5223
  this.startGeometry = identityTransform(box);
@@ -5076,10 +5276,10 @@ var FreeTransformSession = class {
5076
5276
  return !this.closed;
5077
5277
  }
5078
5278
  check() {
5079
- if (this.closed) fail("This transform has already been committed or cancelled; begin a new one.");
5279
+ if (this.closed) fail2("This transform has already been committed or cancelled; begin a new one.");
5080
5280
  if (this.host.layers() !== this.shown) {
5081
5281
  this.closed = true;
5082
- fail("The document changed while the transform was open, so it was abandoned; begin a new one.");
5282
+ fail2("The document changed while the transform was open, so it was abandoned; begin a new one.");
5083
5283
  }
5084
5284
  }
5085
5285
  /**
@@ -5089,7 +5289,7 @@ var FreeTransformSession = class {
5089
5289
  update(changes, preview = {}) {
5090
5290
  this.check();
5091
5291
  if (changes.interpolation !== void 0) {
5092
- if (!isInterpolation(changes.interpolation)) fail(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
5292
+ if (!isInterpolation(changes.interpolation)) fail2(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
5093
5293
  this.interp = changes.interpolation;
5094
5294
  }
5095
5295
  const next = {
@@ -5097,10 +5297,10 @@ var FreeTransformSession = class {
5097
5297
  corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
5098
5298
  warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
5099
5299
  };
5100
- if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
5101
- if (!transformMap(next)) fail("These corners fold the frame over itself or through the horizon; keep the quad convex.");
5300
+ if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail2("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
5301
+ if (!transformMap(next)) fail2("These corners fold the frame over itself or through the horizon; keep the quad convex.");
5102
5302
  if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {
5103
- fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram, no warp); rasterize the layer (rasterize_layer) for distort, perspective or warp.");
5303
+ fail2("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram, no warp); rasterize the layer (rasterize_layer) for distort, perspective or warp.");
5104
5304
  }
5105
5305
  this.geometryValue = next;
5106
5306
  if (preview !== false) this.render(preview);
@@ -5168,7 +5368,7 @@ var FreeTransformSession = class {
5168
5368
  moved = transformSurface(src, hl, opts);
5169
5369
  }
5170
5370
  } catch (e) {
5171
- if (e instanceof TransformTooLargeError) fail(e.message);
5371
+ if (e instanceof TransformTooLargeError) fail2(e.message);
5172
5372
  throw e;
5173
5373
  }
5174
5374
  }
@@ -5214,9 +5414,9 @@ var FreeTransformSession = class {
5214
5414
  resampleType() {
5215
5415
  const layer = this.typeTarget;
5216
5416
  const g = this.geometryValue;
5217
- if (hasWarp(g)) fail("Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.");
5417
+ if (hasWarp(g)) fail2("Type layers cannot be warped here; rasterize the layer first (rasterize_layer), or transform it with scale, rotate and skew.");
5218
5418
  const m = matrixFromCorners(g.box, g.corners);
5219
- if (!m) fail("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram); rasterize the layer for distort or perspective.");
5419
+ if (!m) fail2("Type layers take scale, rotate, skew and flips only (the corners must stay a parallelogram); rasterize the layer for distort or perspective.");
5220
5420
  const next = { ...layer, transform: m };
5221
5421
  delete next.annotation;
5222
5422
  let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));
@@ -5244,7 +5444,7 @@ var FreeTransformSession = class {
5244
5444
  const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });
5245
5445
  return mapLayer(this.startLayers, layer.id, (l) => ({ ...l, placement, surface }));
5246
5446
  } catch (e) {
5247
- if (e instanceof TransformTooLargeError) fail(e.message);
5447
+ if (e instanceof TransformTooLargeError) fail2(e.message);
5248
5448
  throw e;
5249
5449
  }
5250
5450
  }
@@ -5791,9 +5991,9 @@ var PaintStrokeSession = class {
5791
5991
  this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);
5792
5992
  }
5793
5993
  };
5794
- function workRegionOf(surface, rect2, depth) {
5795
- if (!depth) return Float32Array.from(readRegion(surface, rect2));
5796
- const d = readDeepRegion(surface, rect2, depth);
5994
+ function workRegionOf(surface, rect3, depth) {
5995
+ if (!depth) return Float32Array.from(readRegion(surface, rect3));
5996
+ const d = readDeepRegion(surface, rect3, depth);
5797
5997
  const out = new Float32Array(d.length);
5798
5998
  if (d instanceof Uint16Array) {
5799
5999
  for (let i = 0; i < d.length; i++) out[i] = d[i] / 257;
@@ -5943,6 +6143,32 @@ function migrateToLayeredState(data, size) {
5943
6143
  var DEFAULT_TRANSFORM = DEFAULT_TRANSFORM_VALUES;
5944
6144
  var DEFAULT_DEPTH_MASK = DEFAULT_DEPTH_MASK_VALUES;
5945
6145
  var DEVELOP_KEYS = ["brightness", "contrast", "saturation", "exposure", "temperature", "tint"];
6146
+ function combineFields(o) {
6147
+ return { ...o.mode && o.mode !== "replace" ? { mode: o.mode } : {}, ...o.feather && o.feather > 0 ? { feather: o.feather } : {}, ...o.invert ? { invert: true } : {} };
6148
+ }
6149
+ function shapeStepFields(o) {
6150
+ return { ...o.antiAlias === false ? { antiAlias: false } : {}, ...combineFields(o) };
6151
+ }
6152
+ function samplingFields(o) {
6153
+ return { ...o.sample && o.sample !== "composite" ? { sample: o.sample } : {}, ...o.sampleMerged ? { sampleMerged: true } : {} };
6154
+ }
6155
+ function stepOptions(o) {
6156
+ const { mode: _m, feather: _f, invert: _i, sample: _s, sampleMerged: _sm, pixels: _p, ...rest } = o;
6157
+ return JSON.parse(JSON.stringify(rest));
6158
+ }
6159
+ function recipeAfter(prev, prevSel, step) {
6160
+ const replaces = step.op === "all" || isShapeStep(step) && (step.mode ?? "replace") === "replace";
6161
+ if (replaces || !prevSel?.bounds) return [step];
6162
+ return prev ? [...prev, step] : null;
6163
+ }
6164
+ function readStoredRecipe(raw) {
6165
+ if (!Array.isArray(raw) || !raw.length) return null;
6166
+ try {
6167
+ return raw.map((s, i) => readRecipeStep(s, `selectionRecipe[${i}]`));
6168
+ } catch {
6169
+ return null;
6170
+ }
6171
+ }
5946
6172
  var ImageEditorController = class {
5947
6173
  state;
5948
6174
  renderer;
@@ -6030,6 +6256,7 @@ var ImageEditorController = class {
6030
6256
  if (options.initialState?.selection) {
6031
6257
  try {
6032
6258
  this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));
6259
+ if (this.state.selection) this.state.selectionRecipe = readStoredRecipe(options.initialState.selectionRecipe);
6033
6260
  } catch {
6034
6261
  }
6035
6262
  }
@@ -6509,6 +6736,7 @@ var ImageEditorController = class {
6509
6736
  transform: { ...s.transform },
6510
6737
  outputOpacity: s.outputOpacity,
6511
6738
  selection: s.selection ?? null,
6739
+ ...s.selectionRecipe ? { selectionRecipe: s.selectionRecipe } : {},
6512
6740
  ...s.channels?.length ? { channels: s.channels } : {},
6513
6741
  ...s.paths?.length ? { paths: s.paths } : {},
6514
6742
  source: {
@@ -6546,6 +6774,7 @@ var ImageEditorController = class {
6546
6774
  if (entry.colorMode) s.colorMode = entry.colorMode;
6547
6775
  else delete s.colorMode;
6548
6776
  s.selection = entry.selection;
6777
+ s.selectionRecipe = entry.selectionRecipe ?? null;
6549
6778
  if (entry.channels?.length) s.channels = entry.channels;
6550
6779
  else delete s.channels;
6551
6780
  if (entry.paths?.length) s.paths = entry.paths;
@@ -6813,7 +7042,7 @@ var ImageEditorController = class {
6813
7042
  getLayerFrame(id) {
6814
7043
  const layer = this.getLayer(id);
6815
7044
  if (!layer) return null;
6816
- const rect2 = (b2) => [
7045
+ const rect3 = (b2) => [
6817
7046
  { x: b2.x, y: b2.y },
6818
7047
  { x: b2.x + b2.width, y: b2.y },
6819
7048
  { x: b2.x + b2.width, y: b2.y + b2.height },
@@ -6828,16 +7057,16 @@ var ImageEditorController = class {
6828
7057
  }
6829
7058
  if (layer.type === "shape") {
6830
7059
  const g = shapeGeometryBounds(layer);
6831
- return g ? rect2(g) : null;
7060
+ return g ? rect3(g) : null;
6832
7061
  }
6833
7062
  const b = this.getLayerBounds(id);
6834
- return b ? rect2(b) : null;
7063
+ return b ? rect3(b) : null;
6835
7064
  }
6836
7065
  /**
6837
7066
  * Hit-tests the layer stack at `point` (in document space) and returns the id of the topmost
6838
7067
  * matching visible layer, or `null`. Skips adjustment layers.
6839
7068
  */
6840
- layerAt(point, opts = {}) {
7069
+ layerAt(point2, opts = {}) {
6841
7070
  const { includeHidden = false, includeLocked = false, alphaThreshold = 1 } = opts;
6842
7071
  const layers = this.state.layers;
6843
7072
  const testLayer = (layer) => {
@@ -6854,20 +7083,20 @@ var ImageEditorController = class {
6854
7083
  return null;
6855
7084
  }
6856
7085
  if (layer.type === "type") {
6857
- const hitId = typeLayerAt(this, point);
7086
+ const hitId = typeLayerAt(this, point2);
6858
7087
  if (hitId === layer.id) return layer.id;
6859
7088
  return null;
6860
7089
  }
6861
7090
  if (layer.type === "annotation") {
6862
- if (hitTestAnnotation(point, layer.annotation)) return layer.id;
7091
+ if (hitTestAnnotation(point2, layer.annotation)) return layer.id;
6863
7092
  return null;
6864
7093
  }
6865
7094
  if (layer.type === "shape") {
6866
7095
  const bounds = this.getLayerBounds(layer.id);
6867
- if (bounds && point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) {
7096
+ if (bounds && point2.x >= bounds.x && point2.x <= bounds.x + bounds.width && point2.y >= bounds.y && point2.y <= bounds.y + bounds.height) {
6868
7097
  const surface = this.surfaceOf(layer);
6869
- const px2 = Math.floor(point.x - layer.offset.x);
6870
- const py = Math.floor(point.y - layer.offset.y);
7098
+ const px2 = Math.floor(point2.x - layer.offset.x);
7099
+ const py = Math.floor(point2.y - layer.offset.y);
6871
7100
  const pixel = readPixel(surface, px2, py);
6872
7101
  if (pixel.length >= 4 && pixel[3] >= alphaThreshold) return layer.id;
6873
7102
  }
@@ -6876,12 +7105,12 @@ var ImageEditorController = class {
6876
7105
  if (layer.type === "pixel" || layer.type === "smart") {
6877
7106
  const bounds = this.getLayerBounds(layer.id);
6878
7107
  if (!bounds) return null;
6879
- if (point.x < bounds.x || point.x > bounds.x + bounds.width || point.y < bounds.y || point.y > bounds.y + bounds.height) {
7108
+ if (point2.x < bounds.x || point2.x > bounds.x + bounds.width || point2.y < bounds.y || point2.y > bounds.y + bounds.height) {
6880
7109
  return null;
6881
7110
  }
6882
7111
  const surface = this.surfaceOf(layer);
6883
- const px2 = Math.floor(point.x - layer.offset.x);
6884
- const py = Math.floor(point.y - layer.offset.y);
7112
+ const px2 = Math.floor(point2.x - layer.offset.x);
7113
+ const py = Math.floor(point2.y - layer.offset.y);
6885
7114
  const pixel = readPixel(surface, px2, py);
6886
7115
  if (pixel.length >= 4 && pixel[3] >= alphaThreshold) {
6887
7116
  return layer.id;
@@ -6912,9 +7141,9 @@ var ImageEditorController = class {
6912
7141
  * and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and
6913
7142
  * output opacity are not applied. With `layerIds`, only those layers are composited.
6914
7143
  */
6915
- compositePixels(rect2, options = {}) {
7144
+ compositePixels(rect3, options = {}) {
6916
7145
  const { width, height } = this.naturalSize;
6917
- const r = rect2 ?? { x: 0, y: 0, width, height };
7146
+ const r = rect3 ?? { x: 0, y: 0, width, height };
6918
7147
  let layers = this.state.layers;
6919
7148
  if (options.layerIds) {
6920
7149
  const keep = new Set(options.layerIds);
@@ -8491,7 +8720,7 @@ var ImageEditorController = class {
8491
8720
  return this.getGlobalLight();
8492
8721
  }
8493
8722
  /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
8494
- async compositeToSurface(layers, rect2) {
8723
+ async compositeToSurface(layers, rect3) {
8495
8724
  const { width, height } = this.naturalSize;
8496
8725
  if (flattenLayers(layers).some((l) => l.type === "annotation" && l.visible) && !this.renderer.canRasterize()) {
8497
8726
  throw new LayerOperationError("Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.");
@@ -8500,20 +8729,20 @@ var ImageEditorController = class {
8500
8729
  const sources = this.renderer.compositorSources(width, height);
8501
8730
  const depth = this.bitDepth;
8502
8731
  if (depth !== 8) {
8503
- const samples = workingToSamples(compositeDeep(layers, rect2, sources, depth), depth);
8504
- return surfaceFromDeepPixels({ width: rect2.width, height: rect2.height, data: samples }, 4, rect2.x, rect2.y);
8732
+ const samples = workingToSamples(compositeDeep(layers, rect3, sources, depth), depth);
8733
+ return surfaceFromDeepPixels({ width: rect3.width, height: rect3.height, data: samples }, 4, rect3.x, rect3.y);
8505
8734
  }
8506
- const data = new LayerCompositor().composite(layers, rect2, sources);
8507
- return surfaceFromPixels({ width: rect2.width, height: rect2.height, data }, 4, rect2.x, rect2.y);
8735
+ const data = new LayerCompositor().composite(layers, rect3, sources);
8736
+ return surfaceFromPixels({ width: rect3.width, height: rect3.height, data }, 4, rect3.x, rect3.y);
8508
8737
  }
8509
8738
  /**
8510
8739
  * The composite of the layer stack at the document's bit depth (as `compositePixels`, no
8511
8740
  * canvas): 16-bit sRGB samples, or 32-bit linear float, over `rect` (default the canvas). At 8
8512
8741
  * bits, the 8-bit composite widened to 16 bits.
8513
8742
  */
8514
- compositeDeepPixels(rect2) {
8743
+ compositeDeepPixels(rect3) {
8515
8744
  const { width, height } = this.naturalSize;
8516
- const r = rect2 ?? { x: 0, y: 0, width, height };
8745
+ const r = rect3 ?? { x: 0, y: 0, width, height };
8517
8746
  const depth = this.bitDepth;
8518
8747
  if (depth === 8) return { width: r.width, height: r.height, depth: 16, data: to16From8(this.compositePixels(r).data) };
8519
8748
  const data = workingToSamples(compositeDeep(this.state.layers, r, this.compositorSources(), depth), depth);
@@ -8551,12 +8780,12 @@ var ImageEditorController = class {
8551
8780
  if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
8552
8781
  requireUnlocked(upper, "all", "merge it");
8553
8782
  requireUnlocked(lower, "pixels", "merge into it");
8554
- let rect2 = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
8783
+ let rect3 = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
8555
8784
  const reach = Math.max(effectsMargin(lower.effects), upper.type === "adjustment" ? 0 : effectsMargin(upper.effects));
8556
- if (reach) rect2 = { x: rect2.x - reach, y: rect2.y - reach, width: rect2.width + 2 * reach, height: rect2.height + 2 * reach };
8785
+ if (reach) rect3 = { x: rect3.x - reach, y: rect3.y - reach, width: rect3.width + 2 * reach, height: rect3.height + 2 * reach };
8557
8786
  const base = { ...lower, blend: "normal", opacity: 1 };
8558
8787
  delete base.clipped;
8559
- const surface = await this.compositeToSurface([base, upper], rect2);
8788
+ const surface = await this.compositeToSurface([base, upper], rect3);
8560
8789
  const merged = { ...lower, surface, offset: { x: 0, y: 0 } };
8561
8790
  delete merged.mask;
8562
8791
  delete merged.fillOpacity;
@@ -8729,9 +8958,9 @@ var ImageEditorController = class {
8729
8958
  * Sets (Alt-click) or clears the Clone Stamp and Healing Brush source point, in document
8730
8959
  * pixels. The next stroke fixes the offset from its first point. View state, no undo.
8731
8960
  */
8732
- setCloneSource(point) {
8733
- if (point && (!Number.isFinite(point.x) || !Number.isFinite(point.y))) throw new LayerOperationError("The clone source needs finite x and y (document pixels).");
8734
- const source = point ? { x: Math.round(point.x), y: Math.round(point.y), offset: null } : null;
8961
+ setCloneSource(point2) {
8962
+ if (point2 && (!Number.isFinite(point2.x) || !Number.isFinite(point2.y))) throw new LayerOperationError("The clone source needs finite x and y (document pixels).");
8963
+ const source = point2 ? { x: Math.round(point2.x), y: Math.round(point2.y), offset: null } : null;
8735
8964
  this.state.paint = { ...this.state.paint, cloneSource: source };
8736
8965
  this.notify();
8737
8966
  return source;
@@ -9299,27 +9528,27 @@ var ImageEditorController = class {
9299
9528
  copyPixels(options = {}) {
9300
9529
  const { width, height } = this.naturalSize;
9301
9530
  const sel = this.state.selection?.bounds ? this.state.selection : null;
9302
- const rect2 = sel?.bounds ?? { x: 0, y: 0, width, height };
9531
+ const rect3 = sel?.bounds ?? { x: 0, y: 0, width, height };
9303
9532
  let data;
9304
9533
  if (options.merged) {
9305
- data = new Uint8ClampedArray(this.compositePixels(rect2).data);
9534
+ data = new Uint8ClampedArray(this.compositePixels(rect3).data);
9306
9535
  } else {
9307
9536
  const id = options.layerId ?? this.state.activeLayerId;
9308
9537
  if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
9309
9538
  const layer = requireLayer(this.state.layers, id).layer;
9310
9539
  if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is not a pixel layer; copy merged, or rasterize it first.`);
9311
- data = readRegion(this.surfaceOf(layer), { x: rect2.x - layer.offset.x, y: rect2.y - layer.offset.y, width: rect2.width, height: rect2.height });
9540
+ data = readRegion(this.surfaceOf(layer), { x: rect3.x - layer.offset.x, y: rect3.y - layer.offset.y, width: rect3.width, height: rect3.height });
9312
9541
  }
9313
9542
  if (sel) {
9314
- for (let y = 0; y < rect2.height; y++) {
9315
- for (let x = 0; x < rect2.width; x++) {
9316
- const k = sel.data[(rect2.y + y) * sel.width + rect2.x + x];
9317
- const p = (y * rect2.width + x) * 4 + 3;
9543
+ for (let y = 0; y < rect3.height; y++) {
9544
+ for (let x = 0; x < rect3.width; x++) {
9545
+ const k = sel.data[(rect3.y + y) * sel.width + rect3.x + x];
9546
+ const p = (y * rect3.width + x) * 4 + 3;
9318
9547
  data[p] = Math.round(data[p] * k / 255);
9319
9548
  }
9320
9549
  }
9321
9550
  }
9322
- this.clipboard = { width: rect2.width, height: rect2.height, x: rect2.x, y: rect2.y, data };
9551
+ this.clipboard = { width: rect3.width, height: rect3.height, x: rect3.x, y: rect3.y, data };
9323
9552
  return this.clipboard;
9324
9553
  }
9325
9554
  /** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
@@ -9744,6 +9973,8 @@ var ImageEditorController = class {
9744
9973
  * Returns false when nothing changed.
9745
9974
  */
9746
9975
  setSelection(mask, mode = "replace") {
9976
+ const step = this.pendingStep;
9977
+ this.pendingStep = null;
9747
9978
  const { width, height } = this.naturalSize;
9748
9979
  let next = null;
9749
9980
  if (mask) {
@@ -9757,9 +9988,91 @@ var ImageEditorController = class {
9757
9988
  this.recordHistory();
9758
9989
  if (!next && prev?.bounds) this.lastSelection = prev;
9759
9990
  this.state.selection = next;
9991
+ this.state.selectionRecipe = step && next ? recipeAfter(this.state.selectionRecipe ?? null, prev, step) : null;
9992
+ this.notify();
9993
+ return true;
9994
+ }
9995
+ /** The recipe step of the selection command now running, taken by `setSelection`. */
9996
+ pendingStep = null;
9997
+ /** Runs a selection command whose change `step` describes, so the selection's recipe records it. */
9998
+ withRecipeStep(step, fn) {
9999
+ this.pendingStep = step;
10000
+ try {
10001
+ return fn();
10002
+ } finally {
10003
+ this.pendingStep = null;
10004
+ }
10005
+ }
10006
+ /** The selection's recipe as agents and panels read it (empty and not live when frozen or empty). */
10007
+ getSelectionRecipe() {
10008
+ const steps = this.state.selection?.bounds ? this.state.selectionRecipe ?? null : null;
10009
+ return { live: Boolean(steps), steps: describeRecipe(steps ?? []) };
10010
+ }
10011
+ requireRecipe(action) {
10012
+ const steps = this.state.selection?.bounds ? this.state.selectionRecipe : null;
10013
+ if (!steps) {
10014
+ throw new LayerOperationError(
10015
+ `The selection has no recipe to ${action}: it is frozen, or it was made or changed by a command recipes do not describe (quick, object, subject, sky and focus selections, Quick Mask painting, loaded masks and channels). Select again with marquees, polygons, the magic wand, Color Range, modify, invert or refine to start one.`
10016
+ );
10017
+ }
10018
+ return steps;
10019
+ }
10020
+ /**
10021
+ * Replaces the selection's recipe with `steps` (checked first) and the selection with what they
10022
+ * make, from nothing. One undo step; nothing changes on an error. Returns the recipe.
10023
+ */
10024
+ async setSelectionRecipe(steps) {
10025
+ if (!Array.isArray(steps) || !steps.length) throw new LayerOperationError("A recipe needs at least one step; use deselect to select nothing.");
10026
+ const parsed = steps.map((s, i) => readRecipeStep(s, `steps[${i}]`));
10027
+ const { width, height } = this.naturalSize;
10028
+ const images = /* @__PURE__ */ new Map();
10029
+ for (const step of parsed) {
10030
+ if (!stepNeedsPixels(step)) continue;
10031
+ const key2 = samplingKey(step);
10032
+ if (!images.has(key2)) images.set(key2, await this.requirePixels(step));
10033
+ }
10034
+ const result = replaySelectionRecipe(parsed, width, height, (step) => images.get(samplingKey(step)));
10035
+ this.recordHistory();
10036
+ if (!result && this.state.selection?.bounds) this.lastSelection = this.state.selection;
10037
+ this.state.selection = result;
10038
+ this.state.selectionRecipe = result ? parsed : null;
10039
+ this.notify();
10040
+ return this.getSelectionRecipe();
10041
+ }
10042
+ /** Changes step `index` of the selection's recipe (fields given replace its own; `op` replaces it whole) and replays. One undo step. */
10043
+ editSelectionStep(index, changes) {
10044
+ const steps = this.requireRecipe("edit");
10045
+ const i = this.recipeIndex(steps, index, false);
10046
+ const next = steps.map((s) => s);
10047
+ next[i] = changes.op !== void 0 && changes.op !== steps[i].op ? changes : { ...steps[i], ...changes };
10048
+ return this.setSelectionRecipe(next);
10049
+ }
10050
+ /** Inserts a step at `index` (0..length; length appends) and replays. One undo step. */
10051
+ insertSelectionStep(index, step) {
10052
+ const steps = this.requireRecipe("insert into");
10053
+ const i = this.recipeIndex(steps, index, true);
10054
+ return this.setSelectionRecipe([...steps.slice(0, i), step, ...steps.slice(i)]);
10055
+ }
10056
+ /** Removes step `index` and replays (the last step cannot go: deselect instead). One undo step. */
10057
+ removeSelectionStep(index) {
10058
+ const steps = this.requireRecipe("remove from");
10059
+ const i = this.recipeIndex(steps, index, false);
10060
+ if (steps.length === 1) throw new LayerOperationError("That is the recipe's only step; deselect to select nothing.");
10061
+ return this.setSelectionRecipe(steps.filter((_, k) => k !== i));
10062
+ }
10063
+ /** Drops the selection's recipe, keeping the selection as it is (a plain bitmap). One undo step. */
10064
+ freezeSelection() {
10065
+ if (!this.state.selectionRecipe) return false;
10066
+ this.recordHistory();
10067
+ this.state.selectionRecipe = null;
9760
10068
  this.notify();
9761
10069
  return true;
9762
10070
  }
10071
+ recipeIndex(steps, index, insert) {
10072
+ const max = insert ? steps.length : steps.length - 1;
10073
+ if (!Number.isInteger(index) || index < 0 || index > max) throw new LayerOperationError(`index must be 0..${max} (the recipe has ${steps.length} step${steps.length === 1 ? "" : "s"}; see get_selection_recipe), got ${String(index)}.`);
10074
+ return index;
10075
+ }
9763
10076
  /**
9764
10077
  * Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
9765
10078
  * moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo
@@ -9774,7 +10087,7 @@ var ImageEditorController = class {
9774
10087
  /** Select › All. */
9775
10088
  selectAll() {
9776
10089
  const { width, height } = this.naturalSize;
9777
- return this.setSelection(fullSelection(width, height));
10090
+ return this.withRecipeStep({ op: "all" }, () => this.setSelection(fullSelection(width, height)));
9778
10091
  }
9779
10092
  /** Select › Deselect. */
9780
10093
  deselect() {
@@ -9803,8 +10116,8 @@ var ImageEditorController = class {
9803
10116
  if (!Number.isFinite(position) || at < 0 || at >= max) {
9804
10117
  throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
9805
10118
  }
9806
- const rect2 = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
9807
- return this.shapeCommand(rectSelection(width, height, rect2, { antiAlias: false }), options);
10119
+ const rect3 = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
10120
+ return this.shapeCommand(rectSelection(width, height, rect3, { antiAlias: false }), options);
9808
10121
  }
9809
10122
  /** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
9810
10123
  async growSelection(options = {}) {
@@ -9852,7 +10165,7 @@ var ImageEditorController = class {
9852
10165
  invertSelection() {
9853
10166
  const { width, height } = this.naturalSize;
9854
10167
  const current = this.state.selection ?? createSelectionMask(width, height);
9855
- return this.setSelection(invertSelection(current));
10168
+ return this.withRecipeStep({ op: "invert" }, () => this.setSelection(invertSelection(current)));
9856
10169
  }
9857
10170
  shapeCommand(mask, options) {
9858
10171
  const feathered = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
@@ -9860,19 +10173,20 @@ var ImageEditorController = class {
9860
10173
  return this.setSelection(shaped, options.mode ?? "replace");
9861
10174
  }
9862
10175
  /** Rectangular marquee in natural image pixels. */
9863
- selectRect(rect2, options = {}) {
10176
+ selectRect(rect3, options = {}) {
9864
10177
  const { width, height } = this.naturalSize;
9865
- return this.shapeCommand(rectSelection(width, height, rect2, options), options);
10178
+ return this.withRecipeStep({ op: "rect", rect: { ...rect3 }, ...shapeStepFields(options) }, () => this.shapeCommand(rectSelection(width, height, rect3, options), options));
9866
10179
  }
9867
10180
  /** Elliptical marquee inscribed in `rect`. */
9868
- selectEllipse(rect2, options = {}) {
10181
+ selectEllipse(rect3, options = {}) {
9869
10182
  const { width, height } = this.naturalSize;
9870
- return this.shapeCommand(ellipseSelection(width, height, rect2, options), options);
10183
+ return this.withRecipeStep({ op: "ellipse", rect: { ...rect3 }, ...shapeStepFields(options) }, () => this.shapeCommand(ellipseSelection(width, height, rect3, options), options));
9871
10184
  }
9872
10185
  /** Lasso / polygonal lasso. */
9873
10186
  selectPolygon(points, options = {}) {
9874
10187
  const { width, height } = this.naturalSize;
9875
- return this.shapeCommand(polygonSelection(width, height, points, options), options);
10188
+ const step = { op: "polygon", points: points.map((p) => ({ x: p.x, y: p.y })), ...shapeStepFields(options) };
10189
+ return this.withRecipeStep(step, () => this.shapeCommand(polygonSelection(width, height, points, options), options));
9876
10190
  }
9877
10191
  /**
9878
10192
  * Magnetic Lasso for a given outline: each point snaps to the strongest edge within `width` px
@@ -10096,15 +10410,17 @@ var ImageEditorController = class {
10096
10410
  * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when
10097
10411
  * nothing is selected).
10098
10412
  */
10099
- async magicWand(point, options = {}) {
10413
+ async magicWand(point2, options = {}) {
10100
10414
  const pixels = await this.requirePixels(options);
10101
- this.shapeCommand(magicWand(pixels, point, options), options);
10415
+ const step = { op: "magic-wand", point: { x: point2.x, y: point2.y }, options: stepOptions(options), ...combineFields(options), ...samplingFields(options) };
10416
+ this.withRecipeStep(step, () => this.shapeCommand(magicWand(pixels, point2, options), options));
10102
10417
  return this.getSelection();
10103
10418
  }
10104
10419
  /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */
10105
10420
  async selectColorRange(samples, options = {}) {
10106
10421
  const pixels = await this.requirePixels(options);
10107
- this.shapeCommand(colorRange(pixels, samples, options), options);
10422
+ const step = { op: "color-range", samples: JSON.parse(JSON.stringify(samples)), options: stepOptions(options), ...combineFields(options), ...samplingFields(options) };
10423
+ this.withRecipeStep(step, () => this.shapeCommand(colorRange(pixels, samples, options), options));
10108
10424
  return this.getSelection();
10109
10425
  }
10110
10426
  /**
@@ -10113,14 +10429,15 @@ var ImageEditorController = class {
10113
10429
  */
10114
10430
  async selectColorRangePreset(preset, options = {}) {
10115
10431
  const pixels = await this.requirePixels(options);
10116
- this.shapeCommand(colorRangePreset(pixels, preset, options), options);
10432
+ const step = { op: "color-range-preset", preset, ...options.falloff !== void 0 ? { falloff: options.falloff } : {}, ...combineFields(options), ...samplingFields(options) };
10433
+ this.withRecipeStep(step, () => this.shapeCommand(colorRangePreset(pixels, preset, options), options));
10117
10434
  return this.getSelection();
10118
10435
  }
10119
10436
  /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
10120
10437
  modifySelection(op, radius) {
10121
10438
  const sel = this.state.selection;
10122
10439
  if (!sel?.bounds) return false;
10123
- return this.setSelection(modifySelection(sel, op, radius));
10440
+ return this.withRecipeStep({ op: "modify", modify: op, radius }, () => this.setSelection(modifySelection(sel, op, radius)));
10124
10441
  }
10125
10442
  /**
10126
10443
  * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)
@@ -10130,7 +10447,7 @@ var ImageEditorController = class {
10130
10447
  const sel = this.state.selection;
10131
10448
  if (!sel?.bounds) return false;
10132
10449
  const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
10133
- return this.setSelection(refineSelection(pixels, sel, params));
10450
+ return this.withRecipeStep({ op: "refine", params: { ...params }, ...samplingFields(options) }, () => this.setSelection(refineSelection(pixels, sel, params)));
10134
10451
  }
10135
10452
  /**
10136
10453
  * Select and Mask's output: refines the selection with `params`, then puts the result where
@@ -10200,7 +10517,9 @@ var ImageEditorController = class {
10200
10517
  return { output, layerId, refineMode, ...documentId ? { documentId } : {} };
10201
10518
  }
10202
10519
  return this.asOneStep(() => {
10203
- this.setSelection(refined);
10520
+ const plain3 = output === "selection" && !edits.length && !regions2 && refineMode === "color";
10521
+ if (plain3) this.withRecipeStep({ op: "refine", params: { ...params }, ...samplingFields(options) }, () => this.setSelection(refined));
10522
+ else this.setSelection(refined);
10204
10523
  if (output === "selection") return { output, refineMode };
10205
10524
  if (output === "layer-mask") {
10206
10525
  this.addLayerMask(layerId, "selection");
@@ -11220,15 +11539,15 @@ var ImageEditorController = class {
11220
11539
  let mask;
11221
11540
  if (!b || x1 <= x0 || y1 <= y0) mask = createSelectionMask(width, height);
11222
11541
  else {
11223
- const rect2 = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
11224
- const rgba = readRegion(layer.surface, rect2);
11225
- const bytes = new Uint8Array(rect2.width * rect2.height);
11542
+ const rect3 = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
11543
+ const rgba = readRegion(layer.surface, rect3);
11544
+ const bytes = new Uint8Array(rect3.width * rect3.height);
11226
11545
  const hard = init.antiAlias === false;
11227
11546
  for (let i = 0; i < bytes.length; i++) {
11228
11547
  const a = rgba[i * 4 + 3];
11229
11548
  bytes[i] = hard ? a >= 128 ? 255 : 0 : a;
11230
11549
  }
11231
- mask = selectionFromRegion(width, height, rect2, bytes);
11550
+ mask = selectionFromRegion(width, height, rect3, bytes);
11232
11551
  }
11233
11552
  if ((init.feather ?? 0) > 0 && mask.bounds) mask = modifySelection(mask, "feather", init.feather);
11234
11553
  return this.setSelection(mask.bounds ? mask : null, init.mode ?? "replace");
@@ -11406,10 +11725,10 @@ var ImageEditorController = class {
11406
11725
  return shaper ? layoutText(layer, shaper) : null;
11407
11726
  }
11408
11727
  /** The text index nearest a document point (for clicks on a type layer). */
11409
- textIndexAt(id, point) {
11728
+ textIndexAt(id, point2) {
11410
11729
  const layer = requireTypeLayer(this.state.layers, id);
11411
11730
  const layout = this.getTextLayout(id);
11412
- return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;
11731
+ return layout ? textIndexAt(layout, toTextSpace(layer, point2)) : layer.text.length;
11413
11732
  }
11414
11733
  /** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */
11415
11734
  textCaret(id, index) {
@@ -11587,8 +11906,8 @@ var ImageEditorController = class {
11587
11906
  delete content.mask;
11588
11907
  delete content.clipped;
11589
11908
  delete content.fillOpacity;
11590
- const rect2 = spatial ? canvas : this.getLayerBounds(id) ?? canvas;
11591
- const surface = await this.compositeToSurface([content], rect2);
11909
+ const rect3 = spatial ? canvas : this.getLayerBounds(id) ?? canvas;
11910
+ const surface = await this.compositeToSurface([content], rect3);
11592
11911
  next = { ...layer, surface, offset: { x: 0, y: 0 } };
11593
11912
  delete next.filters;
11594
11913
  delete next.source;
@@ -12166,6 +12485,7 @@ var ImageEditorController = class {
12166
12485
  try {
12167
12486
  out = this.serializeState();
12168
12487
  if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
12488
+ if (this.state.selection?.bounds && this.state.selectionRecipe) out.selectionRecipe = JSON.parse(JSON.stringify(this.state.selectionRecipe));
12169
12489
  if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
12170
12490
  if (this.state.paths?.length) out.paths = serializePaths(this.state.paths);
12171
12491
  } finally {
@@ -12207,6 +12527,7 @@ var ImageEditorController = class {
12207
12527
  }
12208
12528
  }
12209
12529
  this.state.selection = selection;
12530
+ this.state.selectionRecipe = selection ? readStoredRecipe(data.selectionRecipe) : null;
12210
12531
  this.dropMismatchedSelection();
12211
12532
  const problems = [];
12212
12533
  const channels = deserializeChannels(data.channels, this.naturalSize, problems);
@@ -13437,10 +13758,10 @@ function sampleOptions(args) {
13437
13758
  return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
13438
13759
  }
13439
13760
  function rectOf(args) {
13440
- const rect2 = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
13441
- if (![rect2.x, rect2.y, rect2.width, rect2.height].every(Number.isFinite)) throw new Error("Pass numeric x, y, width and height in natural image pixels.");
13442
- if (rect2.width <= 0 || rect2.height <= 0) throw new Error("width and height must be greater than 0.");
13443
- return rect2;
13761
+ const rect3 = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
13762
+ if (![rect3.x, rect3.y, rect3.width, rect3.height].every(Number.isFinite)) throw new Error("Pass numeric x, y, width and height in natural image pixels.");
13763
+ if (rect3.width <= 0 || rect3.height <= 0) throw new Error("width and height must be greater than 0.");
13764
+ return rect3;
13444
13765
  }
13445
13766
  function uncertainty(controller) {
13446
13767
  const u = controller.selectionUncertainty();
@@ -13478,7 +13799,7 @@ function createImageSelectionTools(controller) {
13478
13799
  const inspectEdge = {
13479
13800
  name: "inspect_mask_edge",
13480
13801
  title: "Inspect a mask edge",
13481
- description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels partly selected), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
13802
+ description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels in a transition wider than anti-aliasing), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
13482
13803
  inputSchema: {
13483
13804
  type: "object",
13484
13805
  properties: {
@@ -13554,6 +13875,83 @@ function createImageSelectionTools(controller) {
13554
13875
  show: show !== false
13555
13876
  })
13556
13877
  };
13878
+ const stepProperties = {
13879
+ op: { type: "string", enum: ["rect", "ellipse", "polygon", "magic-wand", "color-range", "color-range-preset", "all", "invert", "modify", "refine"], description: "The step: a marquee (rect / ellipse with rect), polygon (points), magic-wand (point, options), color-range (samples, options), color-range-preset (preset, falloff), all, invert, modify (modify, radius) or refine (params)." },
13880
+ rect: { ...rectSchemaStrict, description: "rect and ellipse: the box in natural image pixels." },
13881
+ points: { type: "array", minItems: 3, items: pointSchema, description: "polygon: the outline, natural image pixels." },
13882
+ point: { ...pointSchema, description: "magic-wand: where to click." },
13883
+ samples: { type: "array", minItems: 1, items: {}, description: "color-range: colour samples, as select_color_range takes them." },
13884
+ options: { type: "object", description: "magic-wand and color-range: their own options (tolerance, contiguous, fuzziness, \u2026) as those tools take them." },
13885
+ preset: { type: "string", description: "color-range-preset: the preset (reds \u2026 magentas, highlights, midtones, shadows, skinTones)." },
13886
+ falloff: { type: "number", minimum: 0, maximum: 255, description: "color-range-preset (tones): falloff in levels (default 20)." },
13887
+ modify: { type: "string", enum: [...SELECTION_MODIFY_OPS], description: "modify: expand, contract, feather, border or smooth (grow and shrink are older names)." },
13888
+ radius: { type: "number", minimum: 0, maximum: 500, description: "modify: radius in pixels." },
13889
+ params: { type: "object", description: "refine: radius, smartRadius, smooth, feather, contrast, shiftEdge as refine_selection takes them." },
13890
+ mode: modeSchema,
13891
+ feather: featherSchema,
13892
+ invert: { type: "boolean", description: "Shape steps: invert the new shape before combining (default false)." },
13893
+ antiAlias: antiAliasSchema,
13894
+ sample: sampleSchema,
13895
+ sampleMerged: sampleMergedSchema
13896
+ };
13897
+ const RECIPE_RETURNS = "Returns the selection summary and the recipe { live, steps (each with its index) }.";
13898
+ const withRecipe = async (p) => {
13899
+ await p;
13900
+ return { ...summary(controller), recipe: controller.getSelectionRecipe() };
13901
+ };
13902
+ const getRecipeTool = {
13903
+ name: "get_selection_recipe",
13904
+ title: "Read the selection recipe",
13905
+ description: `Live selections: the steps that made the current selection (marquees, polygons, magic wand, Color Range, modify, invert, refine), so one can be changed afterwards with edit_selection_step, insert_selection_step or remove_selection_step instead of selecting again. live false means it is frozen (made or changed by a command a recipe cannot describe, e.g. quick_select, select_subject, Quick Mask painting). ${RECIPE_RETURNS}`,
13906
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
13907
+ annotations: { title: "Read the selection recipe", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13908
+ handler: () => ({ ...summary(controller), recipe: controller.getSelectionRecipe() })
13909
+ };
13910
+ const editStepTool = {
13911
+ name: "edit_selection_step",
13912
+ title: "Edit a selection recipe step",
13913
+ description: `Changes one step of the selection's recipe (index from get_selection_recipe) and replays the recipe: the fields given replace the step's own (e.g. tolerance in options, the rect, the modify radius, mode); giving a different op replaces the step whole. One undo step; nothing changes on an error. ${RECIPE_RETURNS}`,
13914
+ inputSchema: {
13915
+ type: "object",
13916
+ properties: { index: { type: "integer", minimum: 0, description: "The step to change (see get_selection_recipe)." }, changes: { type: "object", properties: stepProperties, additionalProperties: false, description: "The step fields to set." } },
13917
+ required: ["index", "changes"],
13918
+ additionalProperties: false
13919
+ },
13920
+ annotations: writeAnnotations("Edit a selection recipe step"),
13921
+ handler: ({ index, changes }) => withRecipe(controller.editSelectionStep(index, changes))
13922
+ };
13923
+ const insertStepTool = {
13924
+ name: "insert_selection_step",
13925
+ title: "Insert a selection recipe step",
13926
+ description: `Inserts a step into the selection's recipe at index (its length appends) and replays it, e.g. a subtract rectangle before a refine, or a modify smooth after a magic wand. One undo step; nothing changes on an error. ${RECIPE_RETURNS}`,
13927
+ inputSchema: {
13928
+ type: "object",
13929
+ properties: { index: { type: "integer", minimum: 0, description: "Where to insert (0..number of steps)." }, step: { type: "object", properties: stepProperties, required: ["op"], additionalProperties: false, description: "The step to insert." } },
13930
+ required: ["index", "step"],
13931
+ additionalProperties: false
13932
+ },
13933
+ annotations: writeAnnotations("Insert a selection recipe step"),
13934
+ handler: ({ index, step }) => withRecipe(controller.insertSelectionStep(index, step))
13935
+ };
13936
+ const removeStepTool = {
13937
+ name: "remove_selection_step",
13938
+ title: "Remove a selection recipe step",
13939
+ description: `Removes one step of the selection's recipe and replays the rest (the only step cannot go: deselect instead). One undo step. ${RECIPE_RETURNS}`,
13940
+ inputSchema: { type: "object", properties: { index: { type: "integer", minimum: 0, description: "The step to remove (see get_selection_recipe)." } }, required: ["index"], additionalProperties: false },
13941
+ annotations: writeAnnotations("Remove a selection recipe step"),
13942
+ handler: ({ index }) => withRecipe(controller.removeSelectionStep(index))
13943
+ };
13944
+ const freezeTool = {
13945
+ name: "freeze_selection",
13946
+ title: "Freeze the selection",
13947
+ description: `Drops the selection's recipe and keeps the selection exactly as it is (a plain bitmap), so later edits start from it. One undo step; a frozen or empty selection is unchanged. ${RECIPE_RETURNS}`,
13948
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
13949
+ annotations: { title: "Freeze the selection", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13950
+ handler: () => {
13951
+ controller.freezeSelection();
13952
+ return { ...summary(controller), recipe: controller.getSelectionRecipe() };
13953
+ }
13954
+ };
13557
13955
  const selectRegion = {
13558
13956
  name: "select_region",
13559
13957
  title: "Select a region",
@@ -13598,12 +13996,12 @@ function createImageSelectionTools(controller) {
13598
13996
  break;
13599
13997
  case "rect":
13600
13998
  case "ellipse": {
13601
- const rect2 = { x: num2(x, NaN), y: num2(y, NaN), width: num2(width, NaN), height: num2(height, NaN) };
13602
- if (![rect2.x, rect2.y, rect2.width, rect2.height].every(Number.isFinite)) {
13999
+ const rect3 = { x: num2(x, NaN), y: num2(y, NaN), width: num2(width, NaN), height: num2(height, NaN) };
14000
+ if (![rect3.x, rect3.y, rect3.width, rect3.height].every(Number.isFinite)) {
13603
14001
  throw new Error("rect and ellipse need numeric x, y, width and height.");
13604
14002
  }
13605
- if (shape === "rect") controller.selectRect(rect2, opts);
13606
- else controller.selectEllipse(rect2, opts);
14003
+ if (shape === "rect") controller.selectRect(rect3, opts);
14004
+ else controller.selectEllipse(rect3, opts);
13607
14005
  break;
13608
14006
  }
13609
14007
  case "polygon": {
@@ -13982,10 +14380,10 @@ function createImageSelectionTools(controller) {
13982
14380
  },
13983
14381
  annotations: { ...writeAnnotations("Object Selection"), openWorldHint: true },
13984
14382
  handler: async (args, context) => {
13985
- const rect2 = args.rect;
13986
- const point = args.point;
13987
- if (!rect2 && !point) throw new Error("Pass rect (a box around the object) or point (a point on it).");
13988
- if (rect2 && point) throw new Error("Pass rect or point, not both.");
14383
+ const rect3 = args.rect;
14384
+ const point2 = args.point;
14385
+ if (!rect3 && !point2) throw new Error("Pass rect (a box around the object) or point (a point on it).");
14386
+ if (rect3 && point2) throw new Error("Pass rect or point, not both.");
13989
14387
  const options = {
13990
14388
  mode: parseSelectionMode(args.mode),
13991
14389
  sampleAllLayers: args.sampleAllLayers,
@@ -13993,7 +14391,7 @@ function createImageSelectionTools(controller) {
13993
14391
  useModel: args.useModel,
13994
14392
  signal: context?.signal
13995
14393
  };
13996
- const { method } = rect2 ? await controller.selectObject({ rect: { x: num2(rect2.x, NaN), y: num2(rect2.y, NaN), width: num2(rect2.width, NaN), height: num2(rect2.height, NaN) } }, options) : await controller.selectObject({ point: { x: num2(point.x, NaN), y: num2(point.y, NaN) } }, options);
14394
+ const { method } = rect3 ? await controller.selectObject({ rect: { x: num2(rect3.x, NaN), y: num2(rect3.y, NaN), width: num2(rect3.width, NaN), height: num2(rect3.height, NaN) } }, options) : await controller.selectObject({ point: { x: num2(point2.x, NaN), y: num2(point2.y, NaN) } }, options);
13997
14395
  return { ...summary(controller), method, ...uncertainty(controller) };
13998
14396
  }
13999
14397
  };
@@ -14632,6 +15030,11 @@ function createImageSelectionTools(controller) {
14632
15030
  getSelection,
14633
15031
  inspectEdge,
14634
15032
  compareTool,
15033
+ getRecipeTool,
15034
+ editStepTool,
15035
+ insertStepTool,
15036
+ removeStepTool,
15037
+ freezeTool,
14635
15038
  selectAll,
14636
15039
  deselect,
14637
15040
  invert2,
@@ -14771,7 +15174,7 @@ async function renderImageRegion(controller, request) {
14771
15174
  out.height = request.height;
14772
15175
  const context = out.getContext("2d");
14773
15176
  if (!context) throw new Error(NO_CANVAS);
14774
- const { rect: rect2 } = request;
15177
+ const { rect: rect3 } = request;
14775
15178
  context.imageSmoothingEnabled = request.scale < 1;
14776
15179
  if (request.format === "jpeg") {
14777
15180
  context.fillStyle = "#ffffff";
@@ -14779,22 +15182,22 @@ async function renderImageRegion(controller, request) {
14779
15182
  }
14780
15183
  const kx = view.width / state.imageDimensions.width;
14781
15184
  const ky = view.height / state.imageDimensions.height;
14782
- context.drawImage(view, rect2.x * kx, rect2.y * ky, rect2.width * kx, rect2.height * ky, 0, 0, out.width, out.height);
15185
+ context.drawImage(view, rect3.x * kx, rect3.y * ky, rect3.width * kx, rect3.height * ky, 0, 0, out.width, out.height);
14783
15186
  if (request.includeOverlays) {
14784
15187
  const s = request.scale;
14785
15188
  const maskView = maskViewCanvas(controller, document);
14786
- if (maskView) context.drawImage(maskView, rect2.x, rect2.y, rect2.width, rect2.height, 0, 0, out.width, out.height);
15189
+ if (maskView) context.drawImage(maskView, rect3.x, rect3.y, rect3.width, rect3.height, 0, 0, out.width, out.height);
14787
15190
  const selection = state.selection;
14788
15191
  if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {
14789
15192
  const shade = selectionShade(selection, document);
14790
- if (shade) context.drawImage(shade, rect2.x, rect2.y, rect2.width, rect2.height, 0, 0, out.width, out.height);
15193
+ if (shade) context.drawImage(shade, rect3.x, rect3.y, rect3.width, rect3.height, 0, 0, out.width, out.height);
14791
15194
  }
14792
15195
  if (state.crop) {
14793
15196
  context.save();
14794
15197
  context.strokeStyle = "#ffd400";
14795
15198
  context.lineWidth = 2;
14796
15199
  context.setLineDash([6, 4]);
14797
- context.strokeRect((state.crop.x - rect2.x) * s, (state.crop.y - rect2.y) * s, state.crop.width * s, state.crop.height * s);
15200
+ context.strokeRect((state.crop.x - rect3.x) * s, (state.crop.y - rect3.y) * s, state.crop.width * s, state.crop.height * s);
14798
15201
  context.restore();
14799
15202
  }
14800
15203
  }
@@ -16051,11 +16454,11 @@ function createImageEffectTools(controller) {
16051
16454
  }
16052
16455
 
16053
16456
  // src/image-editor/selection/gesture.ts
16054
- function snapTo45(from, point) {
16055
- const dx = point.x - from.x;
16056
- const dy = point.y - from.y;
16457
+ function snapTo45(from, point2) {
16458
+ const dx = point2.x - from.x;
16459
+ const dy = point2.y - from.y;
16057
16460
  const len = Math.hypot(dx, dy);
16058
- if (len === 0) return point;
16461
+ if (len === 0) return point2;
16059
16462
  const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
16060
16463
  return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
16061
16464
  }
@@ -16143,38 +16546,38 @@ var SelectionGesture = class {
16143
16546
  const { tool } = this.getOptions();
16144
16547
  if (tool === "quick") return null;
16145
16548
  if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
16146
- const rect2 = normalizeRect({
16549
+ const rect3 = normalizeRect({
16147
16550
  x: this.start.x,
16148
16551
  y: this.start.y,
16149
16552
  width: this.current.x - this.start.x,
16150
16553
  height: this.current.y - this.start.y
16151
16554
  });
16152
- if (rect2.width < 1 && rect2.height < 1) return null;
16153
- if (tool === "ellipse") return ellipsePoints(rect2);
16555
+ if (rect3.width < 1 && rect3.height < 1) return null;
16556
+ if (tool === "ellipse") return ellipsePoints(rect3);
16154
16557
  return [
16155
- { x: rect2.x, y: rect2.y },
16156
- { x: rect2.x + rect2.width, y: rect2.y },
16157
- { x: rect2.x + rect2.width, y: rect2.y + rect2.height },
16158
- { x: rect2.x, y: rect2.y + rect2.height }
16558
+ { x: rect3.x, y: rect3.y },
16559
+ { x: rect3.x + rect3.width, y: rect3.y },
16560
+ { x: rect3.x + rect3.width, y: rect3.y + rect3.height },
16561
+ { x: rect3.x, y: rect3.y + rect3.height }
16159
16562
  ];
16160
16563
  }
16161
16564
  /** Pointer down. Resolves once a wand click has been applied. */
16162
- async begin(point, modifiers = {}) {
16565
+ async begin(point2, modifiers = {}) {
16163
16566
  const options = this.getOptions();
16164
16567
  this.error = null;
16165
16568
  if (this.polygon && options.tool !== "polygon" || this.magnetic && options.tool !== "magnetic") this.cancel();
16166
- if (options.tool === "magnetic") return this.magneticClick(point, modifiers, options);
16569
+ if (options.tool === "magnetic") return this.magneticClick(point2, modifiers, options);
16167
16570
  if (options.tool === "polygon") {
16168
- this.pressedAt = point;
16571
+ this.pressedAt = point2;
16169
16572
  this.freehand = false;
16170
16573
  if (!this.polygon) {
16171
16574
  this.modifiers = modifiers;
16172
- this.polygon = [point];
16173
- this.current = point;
16575
+ this.polygon = [point2];
16576
+ this.current = point2;
16174
16577
  return;
16175
16578
  }
16176
16579
  const last = this.polygon[this.polygon.length - 1];
16177
- const p = modifiers.shiftKey ? snapTo45(last, point) : point;
16580
+ const p = modifiers.shiftKey ? snapTo45(last, point2) : point2;
16178
16581
  const first = this.polygon[0];
16179
16582
  if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
16180
16583
  this.close();
@@ -16189,7 +16592,7 @@ var SelectionGesture = class {
16189
16592
  if (options.tool === "row" || options.tool === "column") {
16190
16593
  this.start = null;
16191
16594
  try {
16192
- this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
16595
+ this.controller.selectLine(options.tool, options.tool === "row" ? point2.y : point2.x, {
16193
16596
  mode: selectionModeFromModifiers(modifiers, options.mode),
16194
16597
  feather: options.feather ?? 0
16195
16598
  });
@@ -16203,8 +16606,8 @@ var SelectionGesture = class {
16203
16606
  try {
16204
16607
  this.controller.selectTextShape({
16205
16608
  text: options.typeText || "Type",
16206
- x: point.x,
16207
- y: point.y,
16609
+ x: point2.x,
16610
+ y: point2.y,
16208
16611
  orientation: options.tool === "type-vertical" ? "vertical" : "horizontal",
16209
16612
  style: { fontSize: options.typeSize ?? 72, fontFamily: options.typeFamily || "sans-serif" },
16210
16613
  mode: selectionModeFromModifiers(modifiers, options.mode),
@@ -16228,11 +16631,11 @@ var SelectionGesture = class {
16228
16631
  if (options.tool === "range") {
16229
16632
  const pixels = await this.controller.getSamplePixels(common);
16230
16633
  if (!pixels) throw new Error("The image pixels are not available to sample.");
16231
- const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
16232
- const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
16634
+ const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point2.x)));
16635
+ const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point2.y)));
16233
16636
  const p = (y * pixels.width + x) * 4;
16234
16637
  const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
16235
- await this.controller.selectColorRange([{ color, at: point }], {
16638
+ await this.controller.selectColorRange([{ color, at: point2 }], {
16236
16639
  ...common,
16237
16640
  fuzziness: options.fuzziness ?? 40,
16238
16641
  localized: options.localized ? options.localized : void 0,
@@ -16240,7 +16643,7 @@ var SelectionGesture = class {
16240
16643
  });
16241
16644
  return;
16242
16645
  }
16243
- await this.controller.magicWand(point, {
16646
+ await this.controller.magicWand(point2, {
16244
16647
  ...common,
16245
16648
  tolerance: options.tolerance,
16246
16649
  contiguous: options.contiguous,
@@ -16252,9 +16655,9 @@ var SelectionGesture = class {
16252
16655
  }
16253
16656
  return;
16254
16657
  }
16255
- this.start = point;
16256
- this.current = point;
16257
- this.points = [point];
16658
+ this.start = point2;
16659
+ this.current = point2;
16660
+ this.points = [point2];
16258
16661
  if (options.tool === "quick") {
16259
16662
  const picked = selectionModeFromModifiers(modifiers, options.mode);
16260
16663
  const base = this.controller.getSelection();
@@ -16269,53 +16672,53 @@ var SelectionGesture = class {
16269
16672
  }
16270
16673
  }
16271
16674
  /** Pointer move; returns true when the draft changed (re-render). */
16272
- move(point, modifiers = {}) {
16675
+ move(point2, modifiers = {}) {
16273
16676
  if (this.magnetic) {
16274
- this.current = point;
16275
- this.followEdges(point);
16677
+ this.current = point2;
16678
+ this.followEdges(point2);
16276
16679
  return true;
16277
16680
  }
16278
16681
  if (this.polygon) {
16279
16682
  const last = this.polygon[this.polygon.length - 1];
16280
- if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
16683
+ if (this.pressedAt && (this.freehand || Math.hypot(point2.x - this.pressedAt.x, point2.y - this.pressedAt.y) >= 3)) {
16281
16684
  this.freehand = true;
16282
- if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
16685
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) >= 0.5) this.polygon.push(point2);
16283
16686
  this.current = null;
16284
16687
  return true;
16285
16688
  }
16286
- this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
16689
+ this.current = modifiers.shiftKey ? snapTo45(last, point2) : point2;
16287
16690
  return true;
16288
16691
  }
16289
16692
  if (!this.start) return false;
16290
- this.current = point;
16693
+ this.current = point2;
16291
16694
  const drawn = this.getOptions().tool;
16292
16695
  if (drawn === "lasso" || drawn === "quick") {
16293
16696
  const last = this.points[this.points.length - 1];
16294
- if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
16295
- this.points.push(point);
16697
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) < 0.5) return false;
16698
+ this.points.push(point2);
16296
16699
  if (drawn === "quick") this.growPreview();
16297
16700
  }
16298
16701
  return true;
16299
16702
  }
16300
16703
  /** Pointer up: commits the shape. Returns true when the selection changed. */
16301
- end(point, modifiers) {
16704
+ end(point2, modifiers) {
16302
16705
  if (this.magnetic) return false;
16303
16706
  if (this.polygon) {
16304
- if (point && this.freehand) this.move(point);
16707
+ if (point2 && this.freehand) this.move(point2);
16305
16708
  this.pressedAt = null;
16306
16709
  this.freehand = false;
16307
16710
  return false;
16308
16711
  }
16309
16712
  if (!this.start) return false;
16310
- if (point) this.move(point);
16713
+ if (point2) this.move(point2);
16311
16714
  const options = this.getOptions();
16312
16715
  const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
16313
16716
  if (options.tool === "object") {
16314
- const rect3 = normalizeRect({ x: this.start.x, y: this.start.y, width: this.current.x - this.start.x, height: this.current.y - this.start.y });
16717
+ const rect4 = normalizeRect({ x: this.start.x, y: this.start.y, width: this.current.x - this.start.x, height: this.current.y - this.start.y });
16315
16718
  this.start = null;
16316
16719
  this.current = null;
16317
16720
  this.points = [];
16318
- const target = rect3.width >= 4 && rect3.height >= 4 ? { rect: rect3 } : { point: { x: rect3.x + rect3.width / 2, y: rect3.y + rect3.height / 2 } };
16721
+ const target = rect4.width >= 4 && rect4.height >= 4 ? { rect: rect4 } : { point: { x: rect4.x + rect4.width / 2, y: rect4.y + rect4.height / 2 } };
16319
16722
  this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
16320
16723
  () => void 0,
16321
16724
  (err) => {
@@ -16345,7 +16748,7 @@ var SelectionGesture = class {
16345
16748
  return false;
16346
16749
  }
16347
16750
  const path = this.draftPath;
16348
- const rect2 = normalizeRect({
16751
+ const rect3 = normalizeRect({
16349
16752
  x: this.start.x,
16350
16753
  y: this.start.y,
16351
16754
  width: this.current.x - this.start.x,
@@ -16357,7 +16760,7 @@ var SelectionGesture = class {
16357
16760
  if (!path) return mode === "replace" ? this.controller.deselect() : false;
16358
16761
  const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
16359
16762
  if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
16360
- return options.tool === "ellipse" ? this.controller.selectEllipse(rect2, opts) : this.controller.selectRect(rect2, opts);
16763
+ return options.tool === "ellipse" ? this.controller.selectEllipse(rect3, opts) : this.controller.selectRect(rect3, opts);
16361
16764
  }
16362
16765
  /** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
16363
16766
  close() {
@@ -16406,19 +16809,19 @@ var SelectionGesture = class {
16406
16809
  corridor(a, b) {
16407
16810
  return Math.max(2 * (this.getOptions().magneticWidth ?? 10), 16, Math.hypot(b.x - a.x, b.y - a.y) / 4);
16408
16811
  }
16409
- async magneticClick(point, modifiers, options) {
16812
+ async magneticClick(point2, modifiers, options) {
16410
16813
  const m = this.magnetic;
16411
16814
  if (!m) {
16412
16815
  this.modifiers = modifiers;
16413
- const draft = { fixed: [point], anchors: [0], live: [], edges: null };
16816
+ const draft = { fixed: [point2], anchors: [0], live: [], edges: null };
16414
16817
  this.magnetic = draft;
16415
- this.current = point;
16818
+ this.current = point2;
16416
16819
  try {
16417
16820
  const pixels = await this.controller.getSamplePixels({ sampleMerged: options.sampleMerged, sample: options.sample });
16418
16821
  if (!pixels) throw new Error("The image pixels are not available to find edges in.");
16419
16822
  if (this.magnetic !== draft) return;
16420
16823
  draft.edges = new EdgeMap(pixels, { contrast: (options.magneticContrast ?? 10) / 100 });
16421
- draft.fixed = [snapToEdge(draft.edges, point, options.magneticWidth ?? 10)];
16824
+ draft.fixed = [snapToEdge(draft.edges, point2, options.magneticWidth ?? 10)];
16422
16825
  if (this.current) this.followEdges(this.current);
16423
16826
  } catch (err) {
16424
16827
  this.error = err instanceof Error ? err.message : String(err);
@@ -16427,12 +16830,12 @@ var SelectionGesture = class {
16427
16830
  return;
16428
16831
  }
16429
16832
  const first = m.fixed[0];
16430
- if (m.anchors.length >= 2 && Math.hypot(point.x - first.x, point.y - first.y) <= this.closeRadius) {
16833
+ if (m.anchors.length >= 2 && Math.hypot(point2.x - first.x, point2.y - first.y) <= this.closeRadius) {
16431
16834
  this.close();
16432
16835
  return;
16433
16836
  }
16434
- this.current = point;
16435
- this.followEdges(point);
16837
+ this.current = point2;
16838
+ this.followEdges(point2);
16436
16839
  if (m.live.length > 1) {
16437
16840
  m.fixed.push(...m.live.slice(1));
16438
16841
  m.anchors.push(m.fixed.length - 1);
@@ -16441,15 +16844,15 @@ var SelectionGesture = class {
16441
16844
  if ((modifiers.detail ?? 1) >= 2) this.close();
16442
16845
  }
16443
16846
  /** Updates the live path to `point`, fixing anchors along it every `frequency` px (with `place`). */
16444
- followEdges(point, place = true) {
16847
+ followEdges(point2, place = true) {
16445
16848
  const m = this.magnetic;
16446
16849
  const last = () => m.fixed[m.fixed.length - 1];
16447
16850
  if (!m.edges) {
16448
- m.live = [last(), point];
16851
+ m.live = [last(), point2];
16449
16852
  return;
16450
16853
  }
16451
16854
  const options = this.getOptions();
16452
- const target = snapToEdge(m.edges, point, options.magneticWidth ?? 10);
16855
+ const target = snapToEdge(m.edges, point2, options.magneticWidth ?? 10);
16453
16856
  const spacing = magneticAnchorSpacing(options.magneticFrequency ?? 57);
16454
16857
  for (let guard = 0; guard < 64; guard++) {
16455
16858
  const path = liveWire(m.edges, last(), target, this.corridor(last(), target));
@@ -19422,11 +19825,11 @@ function hitTestCurvePoint(points, x, y, width, height, radius = 8) {
19422
19825
  return best;
19423
19826
  }
19424
19827
  function insertCurvePoint(points, p) {
19425
- const point = { input: clampLevel(p.input), output: clampLevel(p.output) };
19426
- const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));
19427
- let index = out.findIndex((q) => q.input > point.input);
19828
+ const point2 = { input: clampLevel(p.input), output: clampLevel(p.output) };
19829
+ const out = points.filter((q) => q.input !== point2.input).map((q) => ({ ...q }));
19830
+ let index = out.findIndex((q) => q.input > point2.input);
19428
19831
  if (index === -1) index = out.length;
19429
- out.splice(index, 0, point);
19832
+ out.splice(index, 0, point2);
19430
19833
  return { points: out, index };
19431
19834
  }
19432
19835
  function moveCurvePoint(points, index, p) {
@@ -19795,25 +20198,25 @@ var SelectAndMaskSession = class {
19795
20198
  * Selection, brushes, lasso) or a box (Object Selection). Alt (or the subtract mode) removes.
19796
20199
  * Hand and Zoom are the UI's own; they return false here.
19797
20200
  */
19798
- begin(point, modifiers = {}) {
20201
+ begin(point2, modifiers = {}) {
19799
20202
  const s = this.settingsValue;
19800
20203
  const mode = modifiers.altKey ? s.brushMode === "add" ? "subtract" : "add" : s.brushMode;
19801
- this.dragStart = point;
20204
+ this.dragStart = point2;
19802
20205
  switch (s.tool) {
19803
20206
  case "quick-select":
19804
- this.live = { kind: "quick-select", points: [point], size: s.brushSize, mode };
20207
+ this.live = { kind: "quick-select", points: [point2], size: s.brushSize, mode };
19805
20208
  break;
19806
20209
  case "brush":
19807
- this.live = { kind: "brush", points: [point], size: s.brushSize, mode };
20210
+ this.live = { kind: "brush", points: [point2], size: s.brushSize, mode };
19808
20211
  break;
19809
20212
  case "refine-brush":
19810
- this.live = { kind: "refine-brush", points: [point], size: s.brushSize };
20213
+ this.live = { kind: "refine-brush", points: [point2], size: s.brushSize };
19811
20214
  break;
19812
20215
  case "lasso":
19813
- this.live = { kind: "lasso", points: [point], mode };
20216
+ this.live = { kind: "lasso", points: [point2], mode };
19814
20217
  break;
19815
20218
  case "object":
19816
- this.live = { kind: "object", point, mode };
20219
+ this.live = { kind: "object", point: point2, mode };
19817
20220
  break;
19818
20221
  default:
19819
20222
  this.dragStart = null;
@@ -19823,18 +20226,18 @@ var SelectAndMaskSession = class {
19823
20226
  return true;
19824
20227
  }
19825
20228
  /** Pointer move while a stroke is down. */
19826
- move(point) {
20229
+ move(point2) {
19827
20230
  const live = this.live;
19828
20231
  if (!live || !this.dragStart) return;
19829
20232
  if (live.kind === "object") {
19830
20233
  const a = this.dragStart;
19831
- const rect2 = { x: Math.min(a.x, point.x), y: Math.min(a.y, point.y), width: Math.abs(point.x - a.x), height: Math.abs(point.y - a.y) };
19832
- this.live = rect2.width >= 4 && rect2.height >= 4 ? { kind: "object", rect: rect2, mode: live.mode } : { kind: "object", point: a, mode: live.mode };
20234
+ const rect3 = { x: Math.min(a.x, point2.x), y: Math.min(a.y, point2.y), width: Math.abs(point2.x - a.x), height: Math.abs(point2.y - a.y) };
20235
+ this.live = rect3.width >= 4 && rect3.height >= 4 ? { kind: "object", rect: rect3, mode: live.mode } : { kind: "object", point: a, mode: live.mode };
19833
20236
  return;
19834
20237
  }
19835
20238
  const last = live.points[live.points.length - 1];
19836
- if (Math.hypot(point.x - last.x, point.y - last.y) < 1) return;
19837
- live.points.push(point);
20239
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) < 1) return;
20240
+ live.points.push(point2);
19838
20241
  if (this.settingsValue.realTime && live.kind !== "lasso") this.recompute();
19839
20242
  }
19840
20243
  /** Pointer up: the stroke becomes an edit (one step of the workspace's own undo). */
@@ -21866,7 +22269,7 @@ var PathEditGesture = class {
21866
22269
  if (!g) return;
21867
22270
  if (g.kind === "marquee") {
21868
22271
  g.end = pt(p);
21869
- const box = rect(g.start, g.end);
22272
+ const box = rect2(g.start, g.end);
21870
22273
  const cs = this.target ? this.controller.getPathEditComponents(this.target) : [];
21871
22274
  this.selected = new Set(g.keep);
21872
22275
  cs.forEach(
@@ -22001,11 +22404,11 @@ var PathEditGesture = class {
22001
22404
  const last = sp?.anchors[sp.anchors.length - 1];
22002
22405
  if (last) preview = last.out ? [pt(last), pt(last.out), this.pointer, this.pointer] : [pt(last), this.pointer];
22003
22406
  }
22004
- const marquee = this.drag?.kind === "marquee" ? rect(this.drag.start, this.drag.end) : null;
22407
+ const marquee = this.drag?.kind === "marquee" ? rect2(this.drag.start, this.drag.end) : null;
22005
22408
  return { curves, ...edges.length ? { curveModes } : {}, anchors, handles, marquee, preview };
22006
22409
  }
22007
22410
  };
22008
- function rect(a, b) {
22411
+ function rect2(a, b) {
22009
22412
  return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
22010
22413
  }
22011
22414
  function prune(cs) {
@@ -22206,21 +22609,21 @@ var ContentAwareFillSession = class _ContentAwareFillSession {
22206
22609
  }
22207
22610
  }
22208
22611
  /** Pointer down with the Sampling Brush (Alt flips add and subtract). Returns false for other tools. */
22209
- begin(point, modifiers = {}) {
22612
+ begin(point2, modifiers = {}) {
22210
22613
  if (this.settingsValue.tool !== "sampling-brush" || !this.win) return false;
22211
22614
  const add2 = this.settingsValue.brushMode === "add" !== Boolean(modifiers.altKey);
22212
22615
  this.settingsValue = { ...this.settingsValue, sampling: "custom" };
22213
- this.lastPoint = point;
22616
+ this.lastPoint = point2;
22214
22617
  this.strokeAdds = add2;
22215
- this.paint(point, point, add2);
22618
+ this.paint(point2, point2, add2);
22216
22619
  this.emit();
22217
22620
  return true;
22218
22621
  }
22219
22622
  strokeAdds = true;
22220
- move(point) {
22623
+ move(point2) {
22221
22624
  if (!this.lastPoint) return;
22222
- this.paint(this.lastPoint, point, this.strokeAdds);
22223
- this.lastPoint = point;
22625
+ this.paint(this.lastPoint, point2, this.strokeAdds);
22626
+ this.lastPoint = point2;
22224
22627
  this.emit();
22225
22628
  }
22226
22629
  /** Pointer up: the preview follows the new area when live. */
@@ -22534,6 +22937,7 @@ export {
22534
22937
  SELECTION_MODES,
22535
22938
  SELECTION_MODE_LABELS,
22536
22939
  SELECTION_MODIFY_OPS,
22940
+ SELECTION_RECIPE_OPS,
22537
22941
  SELECTION_TOOLS,
22538
22942
  SELECT_AND_MASK_TOOLS,
22539
22943
  SELECT_AND_MASK_VIEWS,
@@ -22799,6 +23203,8 @@ export {
22799
23203
  describeChannel,
22800
23204
  describeLayer,
22801
23205
  describeLayerEffects,
23206
+ describeRecipe,
23207
+ describeRecipeStep,
22802
23208
  describeSelection,
22803
23209
  describeTransform,
22804
23210
  deserializeChannels,
@@ -22983,6 +23389,7 @@ export {
22983
23389
  isSelectionEmpty,
22984
23390
  isSelectionMode,
22985
23391
  isSeparableBlendMode,
23392
+ isShapeStep,
22986
23393
  isSmartSurfaceIncomplete,
22987
23394
  isSpatialFilter,
22988
23395
  isSrgb,
@@ -23190,9 +23597,11 @@ export {
23190
23597
  readPaths,
23191
23598
  readPixel,
23192
23599
  readPsd,
23600
+ readRecipeStep,
23193
23601
  readRegion,
23194
23602
  readShapeComponents,
23195
23603
  readSourceRegion,
23604
+ recipeStepSetting,
23196
23605
  rectSelection,
23197
23606
  rectToQuad,
23198
23607
  reduceSurface,
@@ -23217,6 +23626,7 @@ export {
23217
23626
  renderText,
23218
23627
  replaceContents,
23219
23628
  replaceTextRange,
23629
+ replaySelectionRecipe,
23220
23630
  requireLayer,
23221
23631
  requireOperation,
23222
23632
  requireShapeLayer,
@@ -23249,6 +23659,7 @@ export {
23249
23659
  sameCanvasViewport,
23250
23660
  sampleLut,
23251
23661
  sampleToneLut,
23662
+ samplingKey,
23252
23663
  saturationOf,
23253
23664
  scaleLayerEffect,
23254
23665
  scalePaintGradient,
@@ -23336,6 +23747,7 @@ export {
23336
23747
  standardBlendMode,
23337
23748
  startMarchingAnts,
23338
23749
  startSelectionWorker,
23750
+ stepNeedsPixels,
23339
23751
  stepSlotTs,
23340
23752
  strokeGrainCatch,
23341
23753
  styleAt,