@pixerate/editor 0.220.0 → 0.221.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -3002,7 +3002,7 @@ function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
3002
3002
  };
3003
3003
  const on = (i, j) => f(i, j) >= 128;
3004
3004
  const V = w * h;
3005
- const point = (id) => {
3005
+ const point2 = (id) => {
3006
3006
  const vertical = id >= V;
3007
3007
  const k = vertical ? id - V : id;
3008
3008
  const i = k % w;
@@ -3087,7 +3087,7 @@ function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
3087
3087
  let from = segs[s * 2];
3088
3088
  while (!used[s]) {
3089
3089
  used[s] = 1;
3090
- loop.push(point(from));
3090
+ loop.push(point2(from));
3091
3091
  const to = segs[s * 2] === from ? segs[s * 2 + 1] : segs[s * 2];
3092
3092
  const next = touch.get(to).find((t) => !used[t]);
3093
3093
  from = to;
@@ -3176,140 +3176,655 @@ function regions(a, W, H, b, minArea) {
3176
3176
  };
3177
3177
  }
3178
3178
 
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;
3179
+ // src/image-editor/selection/wand.ts
3180
+ function checkPixels(p) {
3181
+ const n = p.width * p.height;
3182
+ if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {
3183
+ throw new Error("Invalid pixel buffer size");
3184
+ }
3185
+ if (p.data.length < n * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
3186
+ return n;
3187
+ }
3188
+ var SRGB_TO_LINEAR2 = (() => {
3189
+ const t = new Float32Array(256);
3190
+ for (let i = 0; i < 256; i++) {
3191
+ const v = i / 255;
3192
+ t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
3193
+ }
3194
+ return t;
3195
+ })();
3196
+ var labF = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
3197
+ function srgb8ToLab(r, g, b) {
3198
+ const rl = SRGB_TO_LINEAR2[r & 255];
3199
+ const gl = SRGB_TO_LINEAR2[g & 255];
3200
+ const bl = SRGB_TO_LINEAR2[b & 255];
3201
+ const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;
3202
+ const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
3203
+ const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;
3204
+ const fx = labF(x);
3205
+ const fy = labF(y);
3206
+ const fz = labF(z);
3207
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
3208
+ }
3209
+ function similarity(pixels, seed, tolerance, distance) {
3210
+ const d = pixels.data;
3211
+ const tr = d[seed * 4];
3212
+ const tg = d[seed * 4 + 1];
3213
+ const tb = d[seed * 4 + 2];
3214
+ const ta = d[seed * 4 + 3];
3215
+ const tol = Math.max(0, Math.floor(tolerance + 1e-3));
3216
+ if (distance === "perceptual") {
3217
+ const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);
3218
+ const de = Math.max(0, tolerance) * 100 / 255;
3219
+ const de2 = de * de + 1e-6;
3220
+ return (i) => {
3221
+ const p = i * 4;
3222
+ const a = d[p + 3];
3223
+ if (ta === 0 && a === 0) return true;
3224
+ if (Math.abs(a - ta) > tol) return false;
3225
+ const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);
3226
+ return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;
3227
+ };
3228
+ }
3229
+ return (i) => {
3230
+ const p = i * 4;
3231
+ const a = d[p + 3];
3232
+ if (ta === 0 && a === 0) return true;
3233
+ 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;
3234
+ };
3235
+ }
3236
+ function antialiasMarks(marks, w, h, x0, y0, x1, y1) {
3237
+ const out = marks.slice();
3203
3238
  for (let y = y0; y < y1; y++) {
3204
3239
  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++) {
3240
+ const i = y * w + x;
3241
+ const v = marks[i];
3242
+ let sum = 0;
3243
+ let edge = false;
3244
+ for (let dy = -1; dy <= 1; dy++) {
3245
+ const ny = y + dy;
3246
+ for (let dx = -1; dx <= 1; dx++) {
3211
3247
  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));
3248
+ const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];
3249
+ if (n !== v) edge = true;
3250
+ sum += n;
3214
3251
  }
3215
3252
  }
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;
3253
+ if (edge) {
3254
+ const avg = sum / (9 * 255);
3255
+ out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);
3256
+ }
3222
3257
  }
3223
3258
  }
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;
3259
+ return out;
3260
+ }
3261
+ function magicWand(pixels, seed, options = {}) {
3262
+ const n = checkPixels(pixels);
3263
+ const { width: w, height: h } = pixels;
3264
+ const sx = Math.floor(seed.x);
3265
+ const sy = Math.floor(seed.y);
3266
+ if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);
3267
+ const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? "rgb");
3268
+ const marks = new Uint8Array(n);
3269
+ let bx0 = w;
3270
+ let by0 = h;
3271
+ let bx1 = -1;
3272
+ let by1 = -1;
3273
+ if (options.contiguous ?? true) {
3274
+ const stack = [sy * w + sx];
3275
+ while (stack.length) {
3276
+ const idx = stack.pop();
3277
+ if (marks[idx]) continue;
3278
+ const y = idx / w | 0;
3279
+ const row = y * w;
3280
+ let l = idx - row;
3281
+ let r = l;
3282
+ while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;
3283
+ while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;
3284
+ marks.fill(255, row + l, row + r + 1);
3285
+ if (l < bx0) bx0 = l;
3286
+ if (r > bx1) bx1 = r;
3287
+ if (y < by0) by0 = y;
3288
+ if (y > by1) by1 = y;
3289
+ for (const ny of [y - 1, y + 1]) {
3290
+ if (ny < 0 || ny >= h) continue;
3291
+ const nrow = ny * w;
3292
+ let inRun = false;
3293
+ for (let nx = l; nx <= r; nx++) {
3294
+ const ok = !marks[nrow + nx] && similar(nrow + nx);
3295
+ if (ok && !inRun) stack.push(nrow + nx);
3296
+ inRun = ok;
3297
+ }
3298
+ }
3236
3299
  }
3237
- }
3238
- const grown = new Uint8Array(w * h);
3239
- for (let x = 0; x < w; x++) {
3240
- let last = -Infinity;
3300
+ } else {
3241
3301
  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;
3302
+ const row = y * w;
3303
+ for (let x = 0; x < w; x++) {
3304
+ if (similar(row + x)) {
3305
+ marks[row + x] = 255;
3306
+ if (x < bx0) bx0 = x;
3307
+ if (x > bx1) bx1 = x;
3308
+ if (y < by0) by0 = y;
3309
+ if (y > by1) by1 = y;
3283
3310
  }
3284
3311
  }
3285
3312
  }
3286
- found.push(r);
3287
3313
  }
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
- }
3314
+ if (bx1 < 0) return createSelectionMask(w, h);
3315
+ if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);
3316
+ return createSelectionMask(
3317
+ w,
3318
+ h,
3319
+ antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))
3320
+ );
3321
+ }
3322
+ function sampleColor(c) {
3323
+ if (typeof c === "string") {
3324
+ const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
3325
+ if (!m) throw new Error(`Invalid colour "${c}"`);
3326
+ const v = parseInt(m[1], 16);
3327
+ return [v >> 16 & 255, v >> 8 & 255, v & 255];
3294
3328
  }
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
- };
3329
+ return [c[0], c[1], c[2]];
3306
3330
  }
3307
-
3308
- // src/image-editor/selection/path.ts
3309
- function selectionToSubpaths(mask, options = {}) {
3310
- const b = mask.bounds;
3311
- if (!b) return [];
3312
- const x0 = Math.max(0, b.x - 1);
3331
+ function colorRange(pixels, samples, options = {}) {
3332
+ const n = checkPixels(pixels);
3333
+ const { width: w, height: h, data } = pixels;
3334
+ const f = Math.max(0, options.fuzziness ?? 40) + 1;
3335
+ const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));
3336
+ const radius = options.localized !== void 0 ? Math.max(1, options.localized) : void 0;
3337
+ const out = new Uint8Array(n);
3338
+ if (!parsed.length) return createSelectionMask(w, h, out);
3339
+ for (let i = 0; i < n; i++) {
3340
+ const p = i * 4;
3341
+ if (data[p + 3] <= 0) continue;
3342
+ const x = i % w;
3343
+ const y = (i - x) / w;
3344
+ let best = 0;
3345
+ for (const s of parsed) {
3346
+ 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]));
3347
+ let k = Math.min(1, Math.max(0, 1 - d / f));
3348
+ if (k > 0 && radius !== void 0 && s.at) {
3349
+ const dist2 = Math.hypot(x - s.at.x, y - s.at.y);
3350
+ k *= Math.min(1, Math.max(0, 1 - dist2 / radius));
3351
+ }
3352
+ if (k > best) best = k;
3353
+ }
3354
+ out[i] = coverageToByte(best);
3355
+ }
3356
+ return createSelectionMask(w, h, out);
3357
+ }
3358
+ var TONE_RANGE_PRESETS = {
3359
+ shadows: [0, 65],
3360
+ midtones: [105, 150],
3361
+ highlights: [190, 255]
3362
+ };
3363
+ function toneRange(pixels, range, falloff = 20) {
3364
+ const n = checkPixels(pixels);
3365
+ const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
3366
+ const ramp2 = Math.max(1e-3, falloff);
3367
+ const out = new Uint8Array(n);
3368
+ const d = pixels.data;
3369
+ for (let i = 0; i < n; i++) {
3370
+ const p = i * 4;
3371
+ if (d[p + 3] <= 0) continue;
3372
+ const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
3373
+ const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
3374
+ out[i] = coverageToByte(1 - outside / ramp2);
3375
+ }
3376
+ return createSelectionMask(pixels.width, pixels.height, out);
3377
+ }
3378
+ function hueRange(pixels, center) {
3379
+ const n = checkPixels(pixels);
3380
+ const out = new Uint8Array(n);
3381
+ const d = pixels.data;
3382
+ for (let i = 0; i < n; i++) {
3383
+ const p = i * 4;
3384
+ if (d[p + 3] <= 0) continue;
3385
+ const r = d[p] / 255;
3386
+ const g = d[p + 1] / 255;
3387
+ const b = d[p + 2] / 255;
3388
+ const mx = Math.max(r, g, b);
3389
+ const mn = Math.min(r, g, b);
3390
+ const chroma = mx - mn;
3391
+ if (chroma <= 0) continue;
3392
+ let hue;
3393
+ if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
3394
+ else if (mx === g) hue = (b - r) / chroma + 2;
3395
+ else hue = (r - g) / chroma + 4;
3396
+ hue *= 60;
3397
+ let dh = ((hue - center) % 360 + 360) % 360;
3398
+ dh = Math.min(dh, 360 - dh);
3399
+ out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));
3400
+ }
3401
+ return createSelectionMask(pixels.width, pixels.height, out);
3402
+ }
3403
+ var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
3404
+ var COLOR_RANGE_PRESETS = [
3405
+ ...Object.keys(HUE_RANGE_PRESETS),
3406
+ "highlights",
3407
+ "midtones",
3408
+ "shadows",
3409
+ "skinTones"
3410
+ ];
3411
+ function isColorRangePreset(v) {
3412
+ return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
3413
+ }
3414
+ 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;
3415
+ function skinToneRange(pixels) {
3416
+ const n = checkPixels(pixels);
3417
+ const out = new Uint8Array(n);
3418
+ const d = pixels.data;
3419
+ for (let i = 0; i < n; i++) {
3420
+ const p = i * 4;
3421
+ if (d[p + 3] <= 0) continue;
3422
+ const r = d[p] / 255;
3423
+ const g = d[p + 1] / 255;
3424
+ const b = d[p + 2] / 255;
3425
+ const mx = Math.max(r, g, b);
3426
+ const mn = Math.min(r, g, b);
3427
+ const chroma = mx - mn;
3428
+ if (chroma <= 0 || mx <= 0) continue;
3429
+ let hue;
3430
+ if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
3431
+ else if (mx === g) hue = (b - r) / chroma + 2;
3432
+ else hue = (r - g) / chroma + 4;
3433
+ hue *= 60;
3434
+ if (hue > 180) hue -= 360;
3435
+ const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
3436
+ out[i] = coverageToByte(k);
3437
+ }
3438
+ return createSelectionMask(pixels.width, pixels.height, out);
3439
+ }
3440
+ function colorRangePreset(pixels, preset, options = {}) {
3441
+ if (preset === "skinTones") return skinToneRange(pixels);
3442
+ if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
3443
+ if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
3444
+ throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
3445
+ }
3446
+ function selectedColorBins(pixels, sel, tolerance) {
3447
+ const S = 64;
3448
+ let bins = new Uint8Array(S * S * S);
3449
+ const d = pixels.data;
3450
+ const n = pixels.width * pixels.height;
3451
+ for (let i = 0; i < n; i++) {
3452
+ if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
3453
+ bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
3454
+ }
3455
+ const r = Math.round(Math.max(0, tolerance) / 4);
3456
+ if (r === 0) return bins;
3457
+ for (const stride of [1, S, S * S]) {
3458
+ const out = new Uint8Array(bins.length);
3459
+ for (let i = 0; i < bins.length; i++) {
3460
+ if (!bins[i]) continue;
3461
+ const c = Math.floor(i / stride) % S;
3462
+ const lo = Math.max(0, c - r);
3463
+ const hi = Math.min(S - 1, c + r);
3464
+ const base = i - c * stride;
3465
+ for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
3466
+ }
3467
+ bins = out;
3468
+ }
3469
+ return bins;
3470
+ }
3471
+ function matcher(pixels, sel, options) {
3472
+ if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
3473
+ const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
3474
+ const d = pixels.data;
3475
+ 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;
3476
+ }
3477
+ function growSelection(pixels, sel, options = {}) {
3478
+ checkPixels(pixels);
3479
+ const match = matcher(pixels, sel, options);
3480
+ const { width: w, height: h } = pixels;
3481
+ const out = new Uint8Array(sel.data);
3482
+ const stack = [];
3483
+ for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
3484
+ while (stack.length) {
3485
+ const i = stack.pop();
3486
+ const x = i % w;
3487
+ const y = (i - x) / w;
3488
+ const visit = (j) => {
3489
+ if (out[j] < 255 && match(j)) {
3490
+ out[j] = 255;
3491
+ stack.push(j);
3492
+ }
3493
+ };
3494
+ if (x > 0) visit(i - 1);
3495
+ if (x < w - 1) visit(i + 1);
3496
+ if (y > 0) visit(i - w);
3497
+ if (y < h - 1) visit(i + w);
3498
+ }
3499
+ return createSelectionMask(w, h, out);
3500
+ }
3501
+ function selectSimilar(pixels, sel, options = {}) {
3502
+ const n = checkPixels(pixels);
3503
+ const match = matcher(pixels, sel, options);
3504
+ const out = new Uint8Array(sel.data);
3505
+ for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
3506
+ return createSelectionMask(pixels.width, pixels.height, out);
3507
+ }
3508
+
3509
+ // src/image-editor/selection/recipe.ts
3510
+ var SELECTION_RECIPE_OPS = ["rect", "ellipse", "polygon", "magic-wand", "color-range", "color-range-preset", "all", "invert", "modify", "refine"];
3511
+ function isShapeStep(step) {
3512
+ return step.op !== "all" && step.op !== "invert" && step.op !== "modify" && step.op !== "refine";
3513
+ }
3514
+ function stepNeedsPixels(step) {
3515
+ return step.op === "magic-wand" || step.op === "color-range" || step.op === "color-range-preset" || step.op === "refine" && (step.params.radius ?? 8) > 0;
3516
+ }
3517
+ function samplingKey(step) {
3518
+ const s = step;
3519
+ return `${s.sample ?? "composite"}:${s.sampleMerged === true}`;
3520
+ }
3521
+ var fail = (msg) => {
3522
+ throw new Error(msg);
3523
+ };
3524
+ var finite = (v) => typeof v === "number" && Number.isFinite(v);
3525
+ var point = (v, at) => {
3526
+ const p = v;
3527
+ if (!p || !finite(p.x) || !finite(p.y)) fail(`${at} must be { x, y } in natural image pixels.`);
3528
+ return { x: p.x, y: p.y };
3529
+ };
3530
+ var rect = (v, at) => {
3531
+ const r = v;
3532
+ 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.`);
3533
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
3534
+ };
3535
+ function readRecipeStep(raw, at = "step") {
3536
+ const s = raw;
3537
+ if (!s || typeof s !== "object") return fail(`${at} must be an object with an op.`);
3538
+ const combine = () => {
3539
+ const out = {};
3540
+ if (s.mode !== void 0) {
3541
+ if (s.mode !== "replace" && s.mode !== "add" && s.mode !== "subtract" && s.mode !== "intersect") fail(`${at}.mode must be replace, add, subtract or intersect.`);
3542
+ out.mode = s.mode;
3543
+ }
3544
+ if (s.feather !== void 0) {
3545
+ if (!finite(s.feather) || s.feather < 0 || s.feather > 1e3) fail(`${at}.feather must be 0 to 1000 px.`);
3546
+ if (s.feather > 0) out.feather = s.feather;
3547
+ }
3548
+ if (s.invert === true) out.invert = true;
3549
+ return out;
3550
+ };
3551
+ const sampling = () => {
3552
+ const out = {};
3553
+ if (s.sample !== void 0) {
3554
+ if (s.sample !== "composite" && s.sample !== "active-layer") fail(`${at}.sample must be composite or active-layer.`);
3555
+ out.sample = s.sample;
3556
+ }
3557
+ if (s.sampleMerged === true) out.sampleMerged = true;
3558
+ return out;
3559
+ };
3560
+ const options = () => s.options && typeof s.options === "object" ? { options: JSON.parse(JSON.stringify(s.options)) } : {};
3561
+ switch (s.op) {
3562
+ case "rect":
3563
+ case "ellipse":
3564
+ return { op: s.op, rect: rect(s.rect, `${at}.rect`), ...s.antiAlias === false ? { antiAlias: false } : {}, ...combine() };
3565
+ case "polygon": {
3566
+ if (!Array.isArray(s.points) || s.points.length < 3) fail(`${at}.points must be at least 3 { x, y } points.`);
3567
+ return { op: "polygon", points: s.points.map((p, i) => point(p, `${at}.points[${i}]`)), ...s.antiAlias === false ? { antiAlias: false } : {}, ...combine() };
3568
+ }
3569
+ case "magic-wand":
3570
+ return { op: "magic-wand", point: point(s.point, `${at}.point`), ...options(), ...combine(), ...sampling() };
3571
+ case "color-range":
3572
+ if (!Array.isArray(s.samples) || !s.samples.length) fail(`${at}.samples must be a non-empty list of colour samples.`);
3573
+ return { op: "color-range", samples: JSON.parse(JSON.stringify(s.samples)), ...options(), ...combine(), ...sampling() };
3574
+ case "color-range-preset":
3575
+ if (typeof s.preset !== "string" || !isColorRangePreset(s.preset)) fail(`${at}.preset is not a Color Range preset.`);
3576
+ return { op: "color-range-preset", preset: s.preset, ...finite(s.falloff) ? { falloff: s.falloff } : {}, ...combine(), ...sampling() };
3577
+ case "all":
3578
+ return { op: "all" };
3579
+ case "invert":
3580
+ return { op: "invert" };
3581
+ case "modify":
3582
+ if (!SELECTION_MODIFY_OPS.includes(s.modify)) fail(`${at}.modify must be one of ${SELECTION_MODIFY_OPS.join(", ")}.`);
3583
+ if (!finite(s.radius) || s.radius < 0 || s.radius > 500) fail(`${at}.radius must be 0 to 500 px.`);
3584
+ return { op: "modify", modify: s.modify, radius: s.radius };
3585
+ case "refine":
3586
+ if (!s.params || typeof s.params !== "object") fail(`${at}.params must be the refine settings (radius, smooth, feather, contrast, shiftEdge).`);
3587
+ return { op: "refine", params: JSON.parse(JSON.stringify(s.params)), ...sampling() };
3588
+ default:
3589
+ return fail(`${at}.op "${String(s.op)}" is not a recipe step; use ${SELECTION_RECIPE_OPS.join(", ")}.`);
3590
+ }
3591
+ }
3592
+ function replaySelectionRecipe(steps, width, height, pixelsFor) {
3593
+ let sel = null;
3594
+ steps.forEach((step, i) => {
3595
+ const shape = (mask) => {
3596
+ const st = step;
3597
+ let m = st.feather && st.feather > 0 ? modifySelection(mask, "feather", st.feather) : mask;
3598
+ if (st.invert) m = invertSelection(m);
3599
+ return combineSelections(sel, m, st.mode ?? "replace");
3600
+ };
3601
+ let next;
3602
+ switch (step.op) {
3603
+ case "rect":
3604
+ next = shape(rectSelection(width, height, step.rect, { antiAlias: step.antiAlias }));
3605
+ break;
3606
+ case "ellipse":
3607
+ next = shape(ellipseSelection(width, height, step.rect, { antiAlias: step.antiAlias }));
3608
+ break;
3609
+ case "polygon":
3610
+ next = shape(polygonSelection(width, height, step.points, { antiAlias: step.antiAlias }));
3611
+ break;
3612
+ case "magic-wand":
3613
+ next = shape(magicWand(pixelsFor(step), step.point, step.options ?? {}));
3614
+ break;
3615
+ case "color-range":
3616
+ next = shape(colorRange(pixelsFor(step), step.samples, step.options ?? {}));
3617
+ break;
3618
+ case "color-range-preset":
3619
+ next = shape(colorRangePreset(pixelsFor(step), step.preset, step.falloff !== void 0 ? { falloff: step.falloff } : {}));
3620
+ break;
3621
+ case "all":
3622
+ next = fullSelection(width, height);
3623
+ break;
3624
+ case "invert":
3625
+ next = sel ? invertSelection(sel) : fullSelection(width, height);
3626
+ break;
3627
+ case "modify":
3628
+ if (!sel?.bounds) throw new Error(`Step ${i} (modify) has nothing selected to ${step.modify}; an earlier step must select something.`);
3629
+ next = modifySelection(sel, step.modify, step.radius);
3630
+ break;
3631
+ case "refine":
3632
+ if (!sel?.bounds) throw new Error(`Step ${i} (refine) has nothing selected to refine; an earlier step must select something.`);
3633
+ next = refineSelection(stepNeedsPixels(step) ? pixelsFor(step) : null, sel, step.params);
3634
+ break;
3635
+ }
3636
+ sel = next?.bounds ? next : null;
3637
+ });
3638
+ return sel;
3639
+ }
3640
+ function describeRecipe(steps) {
3641
+ return steps.map((s, index) => ({ index, ...JSON.parse(JSON.stringify(s)) }));
3642
+ }
3643
+ var SHAPE_LABELS = { rect: "Rectangle", ellipse: "Ellipse" };
3644
+ var MODIFY_LABELS = { expand: "Expand", grow: "Expand", contract: "Contract", shrink: "Contract", feather: "Feather", border: "Border", smooth: "Smooth" };
3645
+ function describeRecipeStep(step) {
3646
+ const r = (n) => Math.round(n);
3647
+ switch (step.op) {
3648
+ case "rect":
3649
+ case "ellipse":
3650
+ return `${SHAPE_LABELS[step.op]} ${r(step.rect.width)} \xD7 ${r(step.rect.height)} at ${r(step.rect.x)}, ${r(step.rect.y)}`;
3651
+ case "polygon":
3652
+ return `Polygon, ${step.points.length} points`;
3653
+ case "magic-wand":
3654
+ return `Magic Wand at ${r(step.point.x)}, ${r(step.point.y)}`;
3655
+ case "color-range":
3656
+ return `Color Range, ${step.samples.length} sample${step.samples.length === 1 ? "" : "s"}`;
3657
+ case "color-range-preset":
3658
+ return `Color Range: ${step.preset}`;
3659
+ case "all":
3660
+ return "Select All";
3661
+ case "invert":
3662
+ return "Inverse";
3663
+ case "modify":
3664
+ return `${MODIFY_LABELS[step.modify] ?? step.modify} ${step.radius} px`;
3665
+ case "refine":
3666
+ return "Refine Edge";
3667
+ }
3668
+ }
3669
+ function recipeStepSetting(step) {
3670
+ switch (step.op) {
3671
+ case "rect":
3672
+ case "ellipse":
3673
+ case "polygon":
3674
+ return { label: "feather", value: step.feather ?? 0, min: 0, max: 1e3, change: (v) => ({ feather: v }) };
3675
+ case "magic-wand": {
3676
+ const options = step.options ?? {};
3677
+ return { label: "tolerance", value: Number(options.tolerance ?? 32), min: 0, max: 255, change: (v) => ({ options: { ...options, tolerance: v } }) };
3678
+ }
3679
+ case "color-range": {
3680
+ const options = step.options ?? {};
3681
+ return { label: "fuzziness", value: Number(options.fuzziness ?? 40), min: 0, max: 200, change: (v) => ({ options: { ...options, fuzziness: v } }) };
3682
+ }
3683
+ case "color-range-preset":
3684
+ 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;
3685
+ case "modify":
3686
+ return { label: "radius", value: step.radius, min: 0, max: 500, change: (v) => ({ radius: v }) };
3687
+ case "refine":
3688
+ return { label: "radius", value: step.params.radius ?? 8, min: 0, max: 64, change: (v) => ({ params: { ...step.params, radius: v } }) };
3689
+ default:
3690
+ return null;
3691
+ }
3692
+ }
3693
+
3694
+ // src/image-editor/selection/uncertainty.ts
3695
+ var BACKED = 0.2;
3696
+ function selectionUncertainty(mask, image, options = {}) {
3697
+ const W = mask.width;
3698
+ const H = mask.height;
3699
+ const empty = () => ({ unknown: createSelectionMask(W, H), regions: [], count: 0, confidence: 1 });
3700
+ const b = mask.bounds;
3701
+ if (!b) return empty();
3702
+ const band = Math.max(1, Math.round(options.band ?? Math.max(2, Math.min(W, H) / 200)));
3703
+ const minArea = Math.max(1, options.minArea ?? 16);
3704
+ const maxRegions = Math.max(1, options.maxRegions ?? 20);
3705
+ const a = mask.data;
3706
+ const gradient = createGradientSampler(image, W);
3707
+ const x0 = Math.max(0, b.x - band - 1);
3708
+ const y0 = Math.max(0, b.y - band - 1);
3709
+ const x1 = Math.min(W, b.x + b.width + band + 1);
3710
+ const y1 = Math.min(H, b.y + b.height + band + 1);
3711
+ const w = x1 - x0;
3712
+ const h = y1 - y0;
3713
+ const evidence = new Float32Array(w * h).fill(NaN);
3714
+ const seeds = new Uint8Array(w * h);
3715
+ let sum = 0;
3716
+ let n = 0;
3717
+ const on = (x, y) => a[y * W + x] >= 128;
3718
+ for (let y = y0; y < y1; y++) {
3719
+ for (let x = x0; x < x1; x++) {
3720
+ const v = on(x, y);
3721
+ const differs = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && on(nx, ny) !== v;
3722
+ if (!(differs(x - 1, y) || differs(x + 1, y) || differs(x, y - 1) || differs(x, y + 1))) continue;
3723
+ let g = 0;
3724
+ for (let dy = -2; dy <= 2; dy++) {
3725
+ for (let dx = -2; dx <= 2; dx++) {
3726
+ const nx = x + dx;
3727
+ const ny = y + dy;
3728
+ if (nx >= 0 && ny >= 0 && nx < W && ny < H) g = Math.max(g, gradient(nx, ny));
3729
+ }
3730
+ }
3731
+ const e = Math.min(1, g / BACKED);
3732
+ const k = (y - y0) * w + (x - x0);
3733
+ evidence[k] = e;
3734
+ sum += e;
3735
+ n++;
3736
+ if (e < 0.5) seeds[k] = 1;
3737
+ }
3738
+ }
3739
+ if (!n) return empty();
3740
+ const rows = new Uint8Array(w * h);
3741
+ for (let y = 0; y < h; y++) {
3742
+ let last = -Infinity;
3743
+ for (let x = 0; x < w; x++) {
3744
+ if (seeds[y * w + x]) last = x;
3745
+ if (x - last <= band) rows[y * w + x] = 1;
3746
+ }
3747
+ last = Infinity;
3748
+ for (let x = w - 1; x >= 0; x--) {
3749
+ if (seeds[y * w + x]) last = x;
3750
+ if (last - x <= band) rows[y * w + x] = 1;
3751
+ }
3752
+ }
3753
+ const grown = new Uint8Array(w * h);
3754
+ for (let x = 0; x < w; x++) {
3755
+ let last = -Infinity;
3756
+ for (let y = 0; y < h; y++) {
3757
+ if (rows[y * w + x]) last = y;
3758
+ if (y - last <= band) grown[y * w + x] = 1;
3759
+ }
3760
+ last = Infinity;
3761
+ for (let y = h - 1; y >= 0; y--) {
3762
+ if (rows[y * w + x]) last = y;
3763
+ if (last - y <= band) grown[y * w + x] = 1;
3764
+ }
3765
+ }
3766
+ const label = new Int32Array(w * h);
3767
+ const stack = new Int32Array(w * h);
3768
+ const found = [];
3769
+ for (let start = 0; start < w * h; start++) {
3770
+ if (!grown[start] || label[start]) continue;
3771
+ const r = { area: 0, e: 0, en: 0, bx0: w, by0: h, bx1: -1, by1: -1 };
3772
+ const id = found.length + 1;
3773
+ let top = 0;
3774
+ stack[top++] = start;
3775
+ label[start] = id;
3776
+ while (top) {
3777
+ const k = stack[--top];
3778
+ const i = k % w;
3779
+ const j = (k - i) / w;
3780
+ r.area++;
3781
+ if (!Number.isNaN(evidence[k])) {
3782
+ r.e += evidence[k];
3783
+ r.en++;
3784
+ }
3785
+ if (i < r.bx0) r.bx0 = i;
3786
+ if (i > r.bx1) r.bx1 = i;
3787
+ if (j < r.by0) r.by0 = j;
3788
+ if (j > r.by1) r.by1 = j;
3789
+ for (let dy = -1; dy <= 1; dy++) {
3790
+ for (let dx = -1; dx <= 1; dx++) {
3791
+ const ni = i + dx;
3792
+ const nj = j + dy;
3793
+ if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
3794
+ const nk = nj * w + ni;
3795
+ if (!grown[nk] || label[nk]) continue;
3796
+ label[nk] = id;
3797
+ stack[top++] = nk;
3798
+ }
3799
+ }
3800
+ }
3801
+ found.push(r);
3802
+ }
3803
+ const unknownData = new Uint8Array(W * H);
3804
+ for (let y = 0; y < h; y++) {
3805
+ for (let x = 0; x < w; x++) {
3806
+ const id = label[y * w + x];
3807
+ if (id && found[id - 1].area >= minArea) unknownData[(y + y0) * W + x + x0] = 255;
3808
+ }
3809
+ }
3810
+ const kept = found.filter((r) => r.area >= minArea).map((r) => ({
3811
+ bounds: { x: x0 + r.bx0, y: y0 + r.by0, width: r.bx1 - r.bx0 + 1, height: r.by1 - r.by0 + 1 },
3812
+ area: r.area,
3813
+ confidence: Math.round((r.en ? r.e / r.en : 0) * 1e3) / 1e3
3814
+ })).sort((p, q) => p.confidence - q.confidence || q.area - p.area);
3815
+ return {
3816
+ unknown: createSelectionMask(W, H, unknownData),
3817
+ regions: kept.slice(0, maxRegions),
3818
+ count: kept.length,
3819
+ confidence: Math.round(sum / n * 1e3) / 1e3
3820
+ };
3821
+ }
3822
+
3823
+ // src/image-editor/selection/path.ts
3824
+ function selectionToSubpaths(mask, options = {}) {
3825
+ const b = mask.bounds;
3826
+ if (!b) return [];
3827
+ const x0 = Math.max(0, b.x - 1);
3313
3828
  const y0 = Math.max(0, b.y - 1);
3314
3829
  const x1 = Math.min(mask.width, b.x + b.width + 1);
3315
3830
  const y1 = Math.min(mask.height, b.y + b.height + 1);
@@ -3356,12 +3871,12 @@ function pathToSelection(components, width, height, options = {}) {
3356
3871
  const rx1 = Math.min(width, Math.ceil(x1));
3357
3872
  const ry1 = Math.min(height, Math.ceil(y1));
3358
3873
  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");
3874
+ const rect3 = { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 };
3875
+ const cov = componentsCoverage(components, rect3, options.fillRule ?? "nonzero");
3361
3876
  const hard = options.antiAlias === false;
3362
3877
  const bytes = new Uint8Array(cov.length);
3363
3878
  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);
3879
+ const mask = selectionFromRegion(width, height, rect3, bytes);
3365
3880
  const feather = options.feather ?? 0;
3366
3881
  return feather > 0 && mask.bounds ? modifySelection(mask, "feather", feather) : mask;
3367
3882
  }
@@ -3471,9 +3986,9 @@ var EdgeMap = class {
3471
3986
  }
3472
3987
  };
3473
3988
  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);
3989
+ function snapToEdge(map, point2, width = 10) {
3990
+ const cx = clampPixel(point2.x, map.width);
3991
+ const cy = clampPixel(point2.y, map.height);
3477
3992
  const r = Math.max(0, Math.round(width));
3478
3993
  let best = 0;
3479
3994
  let bestD = Infinity;
@@ -3492,7 +4007,7 @@ function snapToEdge(map, point, width = 10) {
3492
4007
  }
3493
4008
  }
3494
4009
  }
3495
- return best > 0 ? { x: bx + 0.5, y: by + 0.5 } : point;
4010
+ return best > 0 ? { x: bx + 0.5, y: by + 0.5 } : point2;
3496
4011
  }
3497
4012
  var Heap = class {
3498
4013
  ids = new Int32Array(256);
@@ -4004,504 +4519,174 @@ var TextEditSession = class {
4004
4519
  else if (step === "next") this.move(mod2 ? "end" : "right", e.shiftKey);
4005
4520
  else if (step === "line-before") this.move(mod2 ? "start" : "up", e.shiftKey);
4006
4521
  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);
4522
+ return true;
4523
+ }
4524
+ case "Home":
4525
+ this.move("home", e.shiftKey);
4526
+ return true;
4527
+ case "End":
4528
+ this.move("end", e.shiftKey);
4529
+ return true;
4530
+ case "Tab":
4531
+ this.insert(" ");
4532
+ return true;
4165
4533
  }
4166
- return this.session;
4534
+ if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
4535
+ this.insert(e.key);
4536
+ return true;
4537
+ }
4538
+ return false;
4167
4539
  }
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();
4540
+ /** Sets character style fields on the selection, or on the whole text when nothing is selected. */
4541
+ setStyle(style) {
4542
+ const s = this.start;
4543
+ const e = this.end;
4544
+ const range = s === e ? {} : { start: s, end: e };
4545
+ this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
4173
4546
  }
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");
4547
+ /** Sets paragraph fields on the paragraphs the selection touches. */
4548
+ setParagraph(style) {
4549
+ const s = this.start;
4550
+ const e = this.end;
4551
+ this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
4181
4552
  }
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);
4553
+ /** The style at the caret (or the selection's start), for the options bar. */
4554
+ styleHere() {
4555
+ const layer = this.layer;
4556
+ const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
4557
+ return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
4190
4558
  }
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
- };
4559
+ info() {
4560
+ return this.controller.getTextLayer(this.layerId);
4225
4561
  }
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
- }
4562
+ /** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
4563
+ overlay() {
4564
+ const layer = this.layer;
4565
+ const layout = this.layout();
4566
+ const m = textMatrix(layer);
4567
+ const at = (x, y) => applyMatrix(m, { x, y });
4568
+ let frame = null;
4569
+ 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)];
4570
+ else if (layout) {
4571
+ const b = layout.bounds;
4572
+ 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
4573
  }
4574
+ if (!layout) return { caret: null, selection: [], frame };
4575
+ const selection = textSelectionQuads(layout, this.start, this.end).map((q) => q.map((p) => at(p.x, p.y)));
4576
+ const c = caretAt(layout, this.focus);
4577
+ return { caret: [at(c.from.x, c.from.y), at(c.to.x, c.to.y)], selection, frame };
4255
4578
  }
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
- }
4579
+ /** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
4580
+ close() {
4581
+ if (!this.alive) return false;
4582
+ if (!this.text.trim().length) {
4583
+ try {
4584
+ this.controller.removeLayer(this.layerId);
4585
+ } catch {
4308
4586
  }
4587
+ return false;
4309
4588
  }
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));
4589
+ if (this.layer.name === "Text" && this.changed) {
4590
+ try {
4591
+ this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
4592
+ } catch {
4348
4593
  }
4349
- if (k > best) best = k;
4350
4594
  }
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);
4595
+ return true;
4372
4596
  }
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));
4597
+ };
4598
+ function typeLayerAt(controller, p, slop = 4) {
4599
+ const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
4600
+ for (const l of layers) {
4601
+ if (!l.visible) continue;
4602
+ const inv = invert(textMatrix(l));
4603
+ const q = applyMatrix(inv, p);
4604
+ let b = null;
4605
+ if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
4606
+ else {
4607
+ const layout = controller.getTextLayout(l.id);
4608
+ if (layout) b = layout.bounds;
4609
+ }
4610
+ 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
4611
  }
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);
4612
+ return null;
4410
4613
  }
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);
4614
+ function flattenTypeLayers(layers) {
4615
+ const out = [];
4616
+ const walk = (list) => {
4617
+ for (const l of list) {
4618
+ if (l.type === "type") out.push(l);
4619
+ else if (l.type === "group") walk(l.children);
4620
+ }
4621
+ };
4622
+ walk(layers);
4623
+ return out;
4436
4624
  }
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(", ")}.`);
4625
+ function invert(m) {
4626
+ const [a, b, c, d, e, f] = m;
4627
+ const det = a * d - b * c || 1;
4628
+ return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
4442
4629
  }
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;
4630
+ var TypeToolGesture = class {
4631
+ constructor(controller, options) {
4632
+ this.controller = controller;
4633
+ this.options = options;
4451
4634
  }
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;
4635
+ controller;
4636
+ options;
4637
+ session = null;
4638
+ dragStart = null;
4639
+ error = null;
4640
+ down(p, mods = {}) {
4641
+ this.error = null;
4642
+ const hit = typeLayerAt(this.controller, p);
4643
+ if (this.session && this.session.alive && hit === this.session.layerId) {
4644
+ this.session.pointAt(p, mods.shift);
4645
+ this.dragStart = null;
4646
+ return;
4463
4647
  }
4464
- bins = out;
4648
+ this.finish();
4649
+ if (hit) {
4650
+ this.controller.setActiveLayer(hit);
4651
+ this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
4652
+ this.dragStart = null;
4653
+ return;
4654
+ }
4655
+ this.dragStart = { x: p.x, y: p.y };
4465
4656
  }
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);
4657
+ /** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
4658
+ up(p) {
4659
+ const start = this.dragStart;
4660
+ this.dragStart = null;
4661
+ if (!start) return this.session;
4662
+ const o = this.options();
4663
+ const w = Math.abs(p.x - start.x);
4664
+ const h = Math.abs(p.y - start.y);
4665
+ try {
4666
+ const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
4667
+ const id = this.controller.addTextLayer({
4668
+ text: "",
4669
+ x: box ? Math.min(start.x, p.x) : start.x,
4670
+ y: box ? Math.min(start.y, p.y) : start.y,
4671
+ ...box ? { box } : {},
4672
+ style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
4673
+ paragraph: o.align !== "left" ? { align: o.align } : {},
4674
+ ...o.orientation === "vertical" ? { orientation: "vertical" } : {},
4675
+ name: "Text"
4676
+ });
4677
+ this.session = new TextEditSession(this.controller, id, 0);
4678
+ } catch (e) {
4679
+ this.error = e instanceof Error ? e.message : String(e);
4680
+ }
4681
+ return this.session;
4495
4682
  }
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
- }
4683
+ /** Ends the open session (an empty new layer is removed). */
4684
+ finish() {
4685
+ const s = this.session;
4686
+ this.session = null;
4687
+ s?.close();
4688
+ }
4689
+ };
4505
4690
 
4506
4691
  // src/image-editor/selection/segmentation.ts
4507
4692
  var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
@@ -4786,7 +4971,7 @@ var ImageEffectsEngine = class {
4786
4971
 
4787
4972
  // src/image-editor/free-transform.ts
4788
4973
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
4789
- var fail = (message) => {
4974
+ var fail2 = (message) => {
4790
4975
  throw new LayerOperationError(message);
4791
4976
  };
4792
4977
  var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
@@ -4867,20 +5052,20 @@ function liftMask(mask, sel, o) {
4867
5052
  var FreeTransformSession = class {
4868
5053
  constructor(host, init = {}) {
4869
5054
  this.host = host;
4870
- if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
5055
+ if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail2(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
4871
5056
  this.interp = init.interpolation ?? "bicubic";
4872
5057
  const sel = host.selection();
4873
5058
  const selected = Boolean(sel?.bounds);
4874
5059
  let content = init.content ?? "auto";
4875
5060
  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.`);
5061
+ fail2(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
4877
5062
  }
4878
5063
  const targetId = init.layerId ?? host.activeLayerId();
4879
5064
  const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
4880
5065
  const smartTarget = (target?.type === "smart" || target?.type === "shape" || target?.type === "type") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
4881
5066
  if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
4882
5067
  this.content = content;
4883
- if (content !== "layer" && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
5068
+ if (content !== "layer" && !selected) fail2(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
4884
5069
  const layers = host.layers();
4885
5070
  this.startLayers = layers;
4886
5071
  this.startSelection = sel;
@@ -4895,17 +5080,17 @@ var FreeTransformSession = class {
4895
5080
  box = { ...sel.bounds };
4896
5081
  } else {
4897
5082
  const id = init.layerId ?? host.activeLayerId();
4898
- if (!id) fail("No layer given and none is active; pass layerId (see list_layers).");
5083
+ if (!id) fail2("No layer given and none is active; pass layerId (see list_layers).");
4899
5084
  const layer = requireLayer(layers, id).layer;
4900
5085
  this.layerId = layer.id;
4901
5086
  const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
4902
5087
  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.`);
5088
+ if (channel !== "pixels" && channel !== "mask") fail2(`Unknown channel "${String(channel)}"; use pixels or mask.`);
4904
5089
  const linked = mask?.linked !== false;
4905
5090
  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.`);
5091
+ 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.`);
5092
+ 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.`);
5093
+ if (!layer.visible && content !== "selection-copy") fail2(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
4909
5094
  if (content !== "selection-copy") {
4910
5095
  requireUnlocked(layer, "position", "transform it");
4911
5096
  if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
@@ -4913,8 +5098,8 @@ var FreeTransformSession = class {
4913
5098
  const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
4914
5099
  const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
4915
5100
  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.`);
5101
+ 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).`);
5102
+ if (!isContentsDecoded(layer.contents)) fail2(`The contents of "${layer.name || id}" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);
4918
5103
  this.smart = layer;
4919
5104
  this.smartObject = true;
4920
5105
  this.startGeometry = placementGeometry(layer);
@@ -4927,7 +5112,7 @@ var FreeTransformSession = class {
4927
5112
  return;
4928
5113
  }
4929
5114
  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).`);
5115
+ 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
5116
  const frame = typeFrame(layer, host.textEnv().shaper);
4932
5117
  const m = textMatrix(layer);
4933
5118
  const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
@@ -4947,9 +5132,9 @@ var FreeTransformSession = class {
4947
5132
  return;
4948
5133
  }
4949
5134
  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).`);
5135
+ 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
5136
  const g = shapeGeometryBounds(layer);
4952
- if (!g) fail(`Shape layer "${layer.name || id}" has no path to transform.`);
5137
+ if (!g) fail2(`Shape layer "${layer.name || id}" has no path to transform.`);
4953
5138
  const w = Math.max(g.width, 1);
4954
5139
  const h = Math.max(g.height, 1);
4955
5140
  box = { x: g.x + (g.width - w) / 2, y: g.y + (g.height - h) / 2, width: w, height: h };
@@ -4961,7 +5146,7 @@ var FreeTransformSession = class {
4961
5146
  moves.push("mask");
4962
5147
  }
4963
5148
  } else if (maskAlone) {
4964
- if (content === "selection-copy") fail("Layer via Copy copies pixels; transform a mask with content layer or selection.");
5149
+ if (content === "selection-copy") fail2("Layer via Copy copies pixels; transform a mask with content layer or selection.");
4965
5150
  if (content === "selection") {
4966
5151
  const { lifted, rest } = liftMask(mask.surface, sel, o);
4967
5152
  this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
@@ -5016,8 +5201,8 @@ var FreeTransformSession = class {
5016
5201
  moves.push("selection");
5017
5202
  }
5018
5203
  }
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).");
5204
+ 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).");
5205
+ 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
5206
  this.box = box;
5022
5207
  this.moves = moves;
5023
5208
  this.startGeometry = identityTransform(box);
@@ -5076,10 +5261,10 @@ var FreeTransformSession = class {
5076
5261
  return !this.closed;
5077
5262
  }
5078
5263
  check() {
5079
- if (this.closed) fail("This transform has already been committed or cancelled; begin a new one.");
5264
+ if (this.closed) fail2("This transform has already been committed or cancelled; begin a new one.");
5080
5265
  if (this.host.layers() !== this.shown) {
5081
5266
  this.closed = true;
5082
- fail("The document changed while the transform was open, so it was abandoned; begin a new one.");
5267
+ fail2("The document changed while the transform was open, so it was abandoned; begin a new one.");
5083
5268
  }
5084
5269
  }
5085
5270
  /**
@@ -5089,7 +5274,7 @@ var FreeTransformSession = class {
5089
5274
  update(changes, preview = {}) {
5090
5275
  this.check();
5091
5276
  if (changes.interpolation !== void 0) {
5092
- if (!isInterpolation(changes.interpolation)) fail(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
5277
+ if (!isInterpolation(changes.interpolation)) fail2(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
5093
5278
  this.interp = changes.interpolation;
5094
5279
  }
5095
5280
  const next = {
@@ -5097,10 +5282,10 @@ var FreeTransformSession = class {
5097
5282
  corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
5098
5283
  warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
5099
5284
  };
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.");
5285
+ 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.");
5286
+ if (!transformMap(next)) fail2("These corners fold the frame over itself or through the horizon; keep the quad convex.");
5102
5287
  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.");
5288
+ 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
5289
  }
5105
5290
  this.geometryValue = next;
5106
5291
  if (preview !== false) this.render(preview);
@@ -5168,7 +5353,7 @@ var FreeTransformSession = class {
5168
5353
  moved = transformSurface(src, hl, opts);
5169
5354
  }
5170
5355
  } catch (e) {
5171
- if (e instanceof TransformTooLargeError) fail(e.message);
5356
+ if (e instanceof TransformTooLargeError) fail2(e.message);
5172
5357
  throw e;
5173
5358
  }
5174
5359
  }
@@ -5214,9 +5399,9 @@ var FreeTransformSession = class {
5214
5399
  resampleType() {
5215
5400
  const layer = this.typeTarget;
5216
5401
  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.");
5402
+ 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
5403
  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.");
5404
+ 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
5405
  const next = { ...layer, transform: m };
5221
5406
  delete next.annotation;
5222
5407
  let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));
@@ -5244,7 +5429,7 @@ var FreeTransformSession = class {
5244
5429
  const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });
5245
5430
  return mapLayer(this.startLayers, layer.id, (l) => ({ ...l, placement, surface }));
5246
5431
  } catch (e) {
5247
- if (e instanceof TransformTooLargeError) fail(e.message);
5432
+ if (e instanceof TransformTooLargeError) fail2(e.message);
5248
5433
  throw e;
5249
5434
  }
5250
5435
  }
@@ -5791,9 +5976,9 @@ var PaintStrokeSession = class {
5791
5976
  this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);
5792
5977
  }
5793
5978
  };
5794
- function workRegionOf(surface, rect2, depth) {
5795
- if (!depth) return Float32Array.from(readRegion(surface, rect2));
5796
- const d = readDeepRegion(surface, rect2, depth);
5979
+ function workRegionOf(surface, rect3, depth) {
5980
+ if (!depth) return Float32Array.from(readRegion(surface, rect3));
5981
+ const d = readDeepRegion(surface, rect3, depth);
5797
5982
  const out = new Float32Array(d.length);
5798
5983
  if (d instanceof Uint16Array) {
5799
5984
  for (let i = 0; i < d.length; i++) out[i] = d[i] / 257;
@@ -5943,6 +6128,32 @@ function migrateToLayeredState(data, size) {
5943
6128
  var DEFAULT_TRANSFORM = DEFAULT_TRANSFORM_VALUES;
5944
6129
  var DEFAULT_DEPTH_MASK = DEFAULT_DEPTH_MASK_VALUES;
5945
6130
  var DEVELOP_KEYS = ["brightness", "contrast", "saturation", "exposure", "temperature", "tint"];
6131
+ function combineFields(o) {
6132
+ return { ...o.mode && o.mode !== "replace" ? { mode: o.mode } : {}, ...o.feather && o.feather > 0 ? { feather: o.feather } : {}, ...o.invert ? { invert: true } : {} };
6133
+ }
6134
+ function shapeStepFields(o) {
6135
+ return { ...o.antiAlias === false ? { antiAlias: false } : {}, ...combineFields(o) };
6136
+ }
6137
+ function samplingFields(o) {
6138
+ return { ...o.sample && o.sample !== "composite" ? { sample: o.sample } : {}, ...o.sampleMerged ? { sampleMerged: true } : {} };
6139
+ }
6140
+ function stepOptions(o) {
6141
+ const { mode: _m, feather: _f, invert: _i, sample: _s, sampleMerged: _sm, pixels: _p, ...rest } = o;
6142
+ return JSON.parse(JSON.stringify(rest));
6143
+ }
6144
+ function recipeAfter(prev, prevSel, step) {
6145
+ const replaces = step.op === "all" || isShapeStep(step) && (step.mode ?? "replace") === "replace";
6146
+ if (replaces || !prevSel?.bounds) return [step];
6147
+ return prev ? [...prev, step] : null;
6148
+ }
6149
+ function readStoredRecipe(raw) {
6150
+ if (!Array.isArray(raw) || !raw.length) return null;
6151
+ try {
6152
+ return raw.map((s, i) => readRecipeStep(s, `selectionRecipe[${i}]`));
6153
+ } catch {
6154
+ return null;
6155
+ }
6156
+ }
5946
6157
  var ImageEditorController = class {
5947
6158
  state;
5948
6159
  renderer;
@@ -6030,6 +6241,7 @@ var ImageEditorController = class {
6030
6241
  if (options.initialState?.selection) {
6031
6242
  try {
6032
6243
  this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));
6244
+ if (this.state.selection) this.state.selectionRecipe = readStoredRecipe(options.initialState.selectionRecipe);
6033
6245
  } catch {
6034
6246
  }
6035
6247
  }
@@ -6509,6 +6721,7 @@ var ImageEditorController = class {
6509
6721
  transform: { ...s.transform },
6510
6722
  outputOpacity: s.outputOpacity,
6511
6723
  selection: s.selection ?? null,
6724
+ ...s.selectionRecipe ? { selectionRecipe: s.selectionRecipe } : {},
6512
6725
  ...s.channels?.length ? { channels: s.channels } : {},
6513
6726
  ...s.paths?.length ? { paths: s.paths } : {},
6514
6727
  source: {
@@ -6546,6 +6759,7 @@ var ImageEditorController = class {
6546
6759
  if (entry.colorMode) s.colorMode = entry.colorMode;
6547
6760
  else delete s.colorMode;
6548
6761
  s.selection = entry.selection;
6762
+ s.selectionRecipe = entry.selectionRecipe ?? null;
6549
6763
  if (entry.channels?.length) s.channels = entry.channels;
6550
6764
  else delete s.channels;
6551
6765
  if (entry.paths?.length) s.paths = entry.paths;
@@ -6813,7 +7027,7 @@ var ImageEditorController = class {
6813
7027
  getLayerFrame(id) {
6814
7028
  const layer = this.getLayer(id);
6815
7029
  if (!layer) return null;
6816
- const rect2 = (b2) => [
7030
+ const rect3 = (b2) => [
6817
7031
  { x: b2.x, y: b2.y },
6818
7032
  { x: b2.x + b2.width, y: b2.y },
6819
7033
  { x: b2.x + b2.width, y: b2.y + b2.height },
@@ -6828,16 +7042,16 @@ var ImageEditorController = class {
6828
7042
  }
6829
7043
  if (layer.type === "shape") {
6830
7044
  const g = shapeGeometryBounds(layer);
6831
- return g ? rect2(g) : null;
7045
+ return g ? rect3(g) : null;
6832
7046
  }
6833
7047
  const b = this.getLayerBounds(id);
6834
- return b ? rect2(b) : null;
7048
+ return b ? rect3(b) : null;
6835
7049
  }
6836
7050
  /**
6837
7051
  * Hit-tests the layer stack at `point` (in document space) and returns the id of the topmost
6838
7052
  * matching visible layer, or `null`. Skips adjustment layers.
6839
7053
  */
6840
- layerAt(point, opts = {}) {
7054
+ layerAt(point2, opts = {}) {
6841
7055
  const { includeHidden = false, includeLocked = false, alphaThreshold = 1 } = opts;
6842
7056
  const layers = this.state.layers;
6843
7057
  const testLayer = (layer) => {
@@ -6854,20 +7068,20 @@ var ImageEditorController = class {
6854
7068
  return null;
6855
7069
  }
6856
7070
  if (layer.type === "type") {
6857
- const hitId = typeLayerAt(this, point);
7071
+ const hitId = typeLayerAt(this, point2);
6858
7072
  if (hitId === layer.id) return layer.id;
6859
7073
  return null;
6860
7074
  }
6861
7075
  if (layer.type === "annotation") {
6862
- if (hitTestAnnotation(point, layer.annotation)) return layer.id;
7076
+ if (hitTestAnnotation(point2, layer.annotation)) return layer.id;
6863
7077
  return null;
6864
7078
  }
6865
7079
  if (layer.type === "shape") {
6866
7080
  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) {
7081
+ if (bounds && point2.x >= bounds.x && point2.x <= bounds.x + bounds.width && point2.y >= bounds.y && point2.y <= bounds.y + bounds.height) {
6868
7082
  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);
7083
+ const px2 = Math.floor(point2.x - layer.offset.x);
7084
+ const py = Math.floor(point2.y - layer.offset.y);
6871
7085
  const pixel = readPixel(surface, px2, py);
6872
7086
  if (pixel.length >= 4 && pixel[3] >= alphaThreshold) return layer.id;
6873
7087
  }
@@ -6876,12 +7090,12 @@ var ImageEditorController = class {
6876
7090
  if (layer.type === "pixel" || layer.type === "smart") {
6877
7091
  const bounds = this.getLayerBounds(layer.id);
6878
7092
  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) {
7093
+ if (point2.x < bounds.x || point2.x > bounds.x + bounds.width || point2.y < bounds.y || point2.y > bounds.y + bounds.height) {
6880
7094
  return null;
6881
7095
  }
6882
7096
  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);
7097
+ const px2 = Math.floor(point2.x - layer.offset.x);
7098
+ const py = Math.floor(point2.y - layer.offset.y);
6885
7099
  const pixel = readPixel(surface, px2, py);
6886
7100
  if (pixel.length >= 4 && pixel[3] >= alphaThreshold) {
6887
7101
  return layer.id;
@@ -6912,9 +7126,9 @@ var ImageEditorController = class {
6912
7126
  * and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and
6913
7127
  * output opacity are not applied. With `layerIds`, only those layers are composited.
6914
7128
  */
6915
- compositePixels(rect2, options = {}) {
7129
+ compositePixels(rect3, options = {}) {
6916
7130
  const { width, height } = this.naturalSize;
6917
- const r = rect2 ?? { x: 0, y: 0, width, height };
7131
+ const r = rect3 ?? { x: 0, y: 0, width, height };
6918
7132
  let layers = this.state.layers;
6919
7133
  if (options.layerIds) {
6920
7134
  const keep = new Set(options.layerIds);
@@ -8491,7 +8705,7 @@ var ImageEditorController = class {
8491
8705
  return this.getGlobalLight();
8492
8706
  }
8493
8707
  /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
8494
- async compositeToSurface(layers, rect2) {
8708
+ async compositeToSurface(layers, rect3) {
8495
8709
  const { width, height } = this.naturalSize;
8496
8710
  if (flattenLayers(layers).some((l) => l.type === "annotation" && l.visible) && !this.renderer.canRasterize()) {
8497
8711
  throw new LayerOperationError("Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.");
@@ -8500,20 +8714,20 @@ var ImageEditorController = class {
8500
8714
  const sources = this.renderer.compositorSources(width, height);
8501
8715
  const depth = this.bitDepth;
8502
8716
  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);
8717
+ const samples = workingToSamples(compositeDeep(layers, rect3, sources, depth), depth);
8718
+ return surfaceFromDeepPixels({ width: rect3.width, height: rect3.height, data: samples }, 4, rect3.x, rect3.y);
8505
8719
  }
8506
- const data = new LayerCompositor().composite(layers, rect2, sources);
8507
- return surfaceFromPixels({ width: rect2.width, height: rect2.height, data }, 4, rect2.x, rect2.y);
8720
+ const data = new LayerCompositor().composite(layers, rect3, sources);
8721
+ return surfaceFromPixels({ width: rect3.width, height: rect3.height, data }, 4, rect3.x, rect3.y);
8508
8722
  }
8509
8723
  /**
8510
8724
  * The composite of the layer stack at the document's bit depth (as `compositePixels`, no
8511
8725
  * canvas): 16-bit sRGB samples, or 32-bit linear float, over `rect` (default the canvas). At 8
8512
8726
  * bits, the 8-bit composite widened to 16 bits.
8513
8727
  */
8514
- compositeDeepPixels(rect2) {
8728
+ compositeDeepPixels(rect3) {
8515
8729
  const { width, height } = this.naturalSize;
8516
- const r = rect2 ?? { x: 0, y: 0, width, height };
8730
+ const r = rect3 ?? { x: 0, y: 0, width, height };
8517
8731
  const depth = this.bitDepth;
8518
8732
  if (depth === 8) return { width: r.width, height: r.height, depth: 16, data: to16From8(this.compositePixels(r).data) };
8519
8733
  const data = workingToSamples(compositeDeep(this.state.layers, r, this.compositorSources(), depth), depth);
@@ -8551,12 +8765,12 @@ var ImageEditorController = class {
8551
8765
  if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
8552
8766
  requireUnlocked(upper, "all", "merge it");
8553
8767
  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 };
8768
+ let rect3 = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
8555
8769
  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 };
8770
+ if (reach) rect3 = { x: rect3.x - reach, y: rect3.y - reach, width: rect3.width + 2 * reach, height: rect3.height + 2 * reach };
8557
8771
  const base = { ...lower, blend: "normal", opacity: 1 };
8558
8772
  delete base.clipped;
8559
- const surface = await this.compositeToSurface([base, upper], rect2);
8773
+ const surface = await this.compositeToSurface([base, upper], rect3);
8560
8774
  const merged = { ...lower, surface, offset: { x: 0, y: 0 } };
8561
8775
  delete merged.mask;
8562
8776
  delete merged.fillOpacity;
@@ -8729,9 +8943,9 @@ var ImageEditorController = class {
8729
8943
  * Sets (Alt-click) or clears the Clone Stamp and Healing Brush source point, in document
8730
8944
  * pixels. The next stroke fixes the offset from its first point. View state, no undo.
8731
8945
  */
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;
8946
+ setCloneSource(point2) {
8947
+ if (point2 && (!Number.isFinite(point2.x) || !Number.isFinite(point2.y))) throw new LayerOperationError("The clone source needs finite x and y (document pixels).");
8948
+ const source = point2 ? { x: Math.round(point2.x), y: Math.round(point2.y), offset: null } : null;
8735
8949
  this.state.paint = { ...this.state.paint, cloneSource: source };
8736
8950
  this.notify();
8737
8951
  return source;
@@ -9299,27 +9513,27 @@ var ImageEditorController = class {
9299
9513
  copyPixels(options = {}) {
9300
9514
  const { width, height } = this.naturalSize;
9301
9515
  const sel = this.state.selection?.bounds ? this.state.selection : null;
9302
- const rect2 = sel?.bounds ?? { x: 0, y: 0, width, height };
9516
+ const rect3 = sel?.bounds ?? { x: 0, y: 0, width, height };
9303
9517
  let data;
9304
9518
  if (options.merged) {
9305
- data = new Uint8ClampedArray(this.compositePixels(rect2).data);
9519
+ data = new Uint8ClampedArray(this.compositePixels(rect3).data);
9306
9520
  } else {
9307
9521
  const id = options.layerId ?? this.state.activeLayerId;
9308
9522
  if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
9309
9523
  const layer = requireLayer(this.state.layers, id).layer;
9310
9524
  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 });
9525
+ data = readRegion(this.surfaceOf(layer), { x: rect3.x - layer.offset.x, y: rect3.y - layer.offset.y, width: rect3.width, height: rect3.height });
9312
9526
  }
9313
9527
  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;
9528
+ for (let y = 0; y < rect3.height; y++) {
9529
+ for (let x = 0; x < rect3.width; x++) {
9530
+ const k = sel.data[(rect3.y + y) * sel.width + rect3.x + x];
9531
+ const p = (y * rect3.width + x) * 4 + 3;
9318
9532
  data[p] = Math.round(data[p] * k / 255);
9319
9533
  }
9320
9534
  }
9321
9535
  }
9322
- this.clipboard = { width: rect2.width, height: rect2.height, x: rect2.x, y: rect2.y, data };
9536
+ this.clipboard = { width: rect3.width, height: rect3.height, x: rect3.x, y: rect3.y, data };
9323
9537
  return this.clipboard;
9324
9538
  }
9325
9539
  /** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
@@ -9744,6 +9958,8 @@ var ImageEditorController = class {
9744
9958
  * Returns false when nothing changed.
9745
9959
  */
9746
9960
  setSelection(mask, mode = "replace") {
9961
+ const step = this.pendingStep;
9962
+ this.pendingStep = null;
9747
9963
  const { width, height } = this.naturalSize;
9748
9964
  let next = null;
9749
9965
  if (mask) {
@@ -9757,9 +9973,91 @@ var ImageEditorController = class {
9757
9973
  this.recordHistory();
9758
9974
  if (!next && prev?.bounds) this.lastSelection = prev;
9759
9975
  this.state.selection = next;
9976
+ this.state.selectionRecipe = step && next ? recipeAfter(this.state.selectionRecipe ?? null, prev, step) : null;
9977
+ this.notify();
9978
+ return true;
9979
+ }
9980
+ /** The recipe step of the selection command now running, taken by `setSelection`. */
9981
+ pendingStep = null;
9982
+ /** Runs a selection command whose change `step` describes, so the selection's recipe records it. */
9983
+ withRecipeStep(step, fn) {
9984
+ this.pendingStep = step;
9985
+ try {
9986
+ return fn();
9987
+ } finally {
9988
+ this.pendingStep = null;
9989
+ }
9990
+ }
9991
+ /** The selection's recipe as agents and panels read it (empty and not live when frozen or empty). */
9992
+ getSelectionRecipe() {
9993
+ const steps = this.state.selection?.bounds ? this.state.selectionRecipe ?? null : null;
9994
+ return { live: Boolean(steps), steps: describeRecipe(steps ?? []) };
9995
+ }
9996
+ requireRecipe(action) {
9997
+ const steps = this.state.selection?.bounds ? this.state.selectionRecipe : null;
9998
+ if (!steps) {
9999
+ throw new LayerOperationError(
10000
+ `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.`
10001
+ );
10002
+ }
10003
+ return steps;
10004
+ }
10005
+ /**
10006
+ * Replaces the selection's recipe with `steps` (checked first) and the selection with what they
10007
+ * make, from nothing. One undo step; nothing changes on an error. Returns the recipe.
10008
+ */
10009
+ async setSelectionRecipe(steps) {
10010
+ if (!Array.isArray(steps) || !steps.length) throw new LayerOperationError("A recipe needs at least one step; use deselect to select nothing.");
10011
+ const parsed = steps.map((s, i) => readRecipeStep(s, `steps[${i}]`));
10012
+ const { width, height } = this.naturalSize;
10013
+ const images = /* @__PURE__ */ new Map();
10014
+ for (const step of parsed) {
10015
+ if (!stepNeedsPixels(step)) continue;
10016
+ const key2 = samplingKey(step);
10017
+ if (!images.has(key2)) images.set(key2, await this.requirePixels(step));
10018
+ }
10019
+ const result = replaySelectionRecipe(parsed, width, height, (step) => images.get(samplingKey(step)));
10020
+ this.recordHistory();
10021
+ if (!result && this.state.selection?.bounds) this.lastSelection = this.state.selection;
10022
+ this.state.selection = result;
10023
+ this.state.selectionRecipe = result ? parsed : null;
10024
+ this.notify();
10025
+ return this.getSelectionRecipe();
10026
+ }
10027
+ /** Changes step `index` of the selection's recipe (fields given replace its own; `op` replaces it whole) and replays. One undo step. */
10028
+ editSelectionStep(index, changes) {
10029
+ const steps = this.requireRecipe("edit");
10030
+ const i = this.recipeIndex(steps, index, false);
10031
+ const next = steps.map((s) => s);
10032
+ next[i] = changes.op !== void 0 && changes.op !== steps[i].op ? changes : { ...steps[i], ...changes };
10033
+ return this.setSelectionRecipe(next);
10034
+ }
10035
+ /** Inserts a step at `index` (0..length; length appends) and replays. One undo step. */
10036
+ insertSelectionStep(index, step) {
10037
+ const steps = this.requireRecipe("insert into");
10038
+ const i = this.recipeIndex(steps, index, true);
10039
+ return this.setSelectionRecipe([...steps.slice(0, i), step, ...steps.slice(i)]);
10040
+ }
10041
+ /** Removes step `index` and replays (the last step cannot go: deselect instead). One undo step. */
10042
+ removeSelectionStep(index) {
10043
+ const steps = this.requireRecipe("remove from");
10044
+ const i = this.recipeIndex(steps, index, false);
10045
+ if (steps.length === 1) throw new LayerOperationError("That is the recipe's only step; deselect to select nothing.");
10046
+ return this.setSelectionRecipe(steps.filter((_, k) => k !== i));
10047
+ }
10048
+ /** Drops the selection's recipe, keeping the selection as it is (a plain bitmap). One undo step. */
10049
+ freezeSelection() {
10050
+ if (!this.state.selectionRecipe) return false;
10051
+ this.recordHistory();
10052
+ this.state.selectionRecipe = null;
9760
10053
  this.notify();
9761
10054
  return true;
9762
10055
  }
10056
+ recipeIndex(steps, index, insert) {
10057
+ const max = insert ? steps.length : steps.length - 1;
10058
+ 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)}.`);
10059
+ return index;
10060
+ }
9763
10061
  /**
9764
10062
  * Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
9765
10063
  * moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo
@@ -9774,7 +10072,7 @@ var ImageEditorController = class {
9774
10072
  /** Select › All. */
9775
10073
  selectAll() {
9776
10074
  const { width, height } = this.naturalSize;
9777
- return this.setSelection(fullSelection(width, height));
10075
+ return this.withRecipeStep({ op: "all" }, () => this.setSelection(fullSelection(width, height)));
9778
10076
  }
9779
10077
  /** Select › Deselect. */
9780
10078
  deselect() {
@@ -9803,8 +10101,8 @@ var ImageEditorController = class {
9803
10101
  if (!Number.isFinite(position) || at < 0 || at >= max) {
9804
10102
  throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
9805
10103
  }
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);
10104
+ const rect3 = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
10105
+ return this.shapeCommand(rectSelection(width, height, rect3, { antiAlias: false }), options);
9808
10106
  }
9809
10107
  /** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
9810
10108
  async growSelection(options = {}) {
@@ -9852,7 +10150,7 @@ var ImageEditorController = class {
9852
10150
  invertSelection() {
9853
10151
  const { width, height } = this.naturalSize;
9854
10152
  const current = this.state.selection ?? createSelectionMask(width, height);
9855
- return this.setSelection(invertSelection(current));
10153
+ return this.withRecipeStep({ op: "invert" }, () => this.setSelection(invertSelection(current)));
9856
10154
  }
9857
10155
  shapeCommand(mask, options) {
9858
10156
  const feathered = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
@@ -9860,19 +10158,20 @@ var ImageEditorController = class {
9860
10158
  return this.setSelection(shaped, options.mode ?? "replace");
9861
10159
  }
9862
10160
  /** Rectangular marquee in natural image pixels. */
9863
- selectRect(rect2, options = {}) {
10161
+ selectRect(rect3, options = {}) {
9864
10162
  const { width, height } = this.naturalSize;
9865
- return this.shapeCommand(rectSelection(width, height, rect2, options), options);
10163
+ return this.withRecipeStep({ op: "rect", rect: { ...rect3 }, ...shapeStepFields(options) }, () => this.shapeCommand(rectSelection(width, height, rect3, options), options));
9866
10164
  }
9867
10165
  /** Elliptical marquee inscribed in `rect`. */
9868
- selectEllipse(rect2, options = {}) {
10166
+ selectEllipse(rect3, options = {}) {
9869
10167
  const { width, height } = this.naturalSize;
9870
- return this.shapeCommand(ellipseSelection(width, height, rect2, options), options);
10168
+ return this.withRecipeStep({ op: "ellipse", rect: { ...rect3 }, ...shapeStepFields(options) }, () => this.shapeCommand(ellipseSelection(width, height, rect3, options), options));
9871
10169
  }
9872
10170
  /** Lasso / polygonal lasso. */
9873
10171
  selectPolygon(points, options = {}) {
9874
10172
  const { width, height } = this.naturalSize;
9875
- return this.shapeCommand(polygonSelection(width, height, points, options), options);
10173
+ const step = { op: "polygon", points: points.map((p) => ({ x: p.x, y: p.y })), ...shapeStepFields(options) };
10174
+ return this.withRecipeStep(step, () => this.shapeCommand(polygonSelection(width, height, points, options), options));
9876
10175
  }
9877
10176
  /**
9878
10177
  * Magnetic Lasso for a given outline: each point snaps to the strongest edge within `width` px
@@ -10096,15 +10395,17 @@ var ImageEditorController = class {
10096
10395
  * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when
10097
10396
  * nothing is selected).
10098
10397
  */
10099
- async magicWand(point, options = {}) {
10398
+ async magicWand(point2, options = {}) {
10100
10399
  const pixels = await this.requirePixels(options);
10101
- this.shapeCommand(magicWand(pixels, point, options), options);
10400
+ const step = { op: "magic-wand", point: { x: point2.x, y: point2.y }, options: stepOptions(options), ...combineFields(options), ...samplingFields(options) };
10401
+ this.withRecipeStep(step, () => this.shapeCommand(magicWand(pixels, point2, options), options));
10102
10402
  return this.getSelection();
10103
10403
  }
10104
10404
  /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */
10105
10405
  async selectColorRange(samples, options = {}) {
10106
10406
  const pixels = await this.requirePixels(options);
10107
- this.shapeCommand(colorRange(pixels, samples, options), options);
10407
+ const step = { op: "color-range", samples: JSON.parse(JSON.stringify(samples)), options: stepOptions(options), ...combineFields(options), ...samplingFields(options) };
10408
+ this.withRecipeStep(step, () => this.shapeCommand(colorRange(pixels, samples, options), options));
10108
10409
  return this.getSelection();
10109
10410
  }
10110
10411
  /**
@@ -10113,14 +10414,15 @@ var ImageEditorController = class {
10113
10414
  */
10114
10415
  async selectColorRangePreset(preset, options = {}) {
10115
10416
  const pixels = await this.requirePixels(options);
10116
- this.shapeCommand(colorRangePreset(pixels, preset, options), options);
10417
+ const step = { op: "color-range-preset", preset, ...options.falloff !== void 0 ? { falloff: options.falloff } : {}, ...combineFields(options), ...samplingFields(options) };
10418
+ this.withRecipeStep(step, () => this.shapeCommand(colorRangePreset(pixels, preset, options), options));
10117
10419
  return this.getSelection();
10118
10420
  }
10119
10421
  /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
10120
10422
  modifySelection(op, radius) {
10121
10423
  const sel = this.state.selection;
10122
10424
  if (!sel?.bounds) return false;
10123
- return this.setSelection(modifySelection(sel, op, radius));
10425
+ return this.withRecipeStep({ op: "modify", modify: op, radius }, () => this.setSelection(modifySelection(sel, op, radius)));
10124
10426
  }
10125
10427
  /**
10126
10428
  * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)
@@ -10130,7 +10432,7 @@ var ImageEditorController = class {
10130
10432
  const sel = this.state.selection;
10131
10433
  if (!sel?.bounds) return false;
10132
10434
  const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
10133
- return this.setSelection(refineSelection(pixels, sel, params));
10435
+ return this.withRecipeStep({ op: "refine", params: { ...params }, ...samplingFields(options) }, () => this.setSelection(refineSelection(pixels, sel, params)));
10134
10436
  }
10135
10437
  /**
10136
10438
  * Select and Mask's output: refines the selection with `params`, then puts the result where
@@ -10200,7 +10502,9 @@ var ImageEditorController = class {
10200
10502
  return { output, layerId, refineMode, ...documentId ? { documentId } : {} };
10201
10503
  }
10202
10504
  return this.asOneStep(() => {
10203
- this.setSelection(refined);
10505
+ const plain3 = output === "selection" && !edits.length && !regions2 && refineMode === "color";
10506
+ if (plain3) this.withRecipeStep({ op: "refine", params: { ...params }, ...samplingFields(options) }, () => this.setSelection(refined));
10507
+ else this.setSelection(refined);
10204
10508
  if (output === "selection") return { output, refineMode };
10205
10509
  if (output === "layer-mask") {
10206
10510
  this.addLayerMask(layerId, "selection");
@@ -11220,15 +11524,15 @@ var ImageEditorController = class {
11220
11524
  let mask;
11221
11525
  if (!b || x1 <= x0 || y1 <= y0) mask = createSelectionMask(width, height);
11222
11526
  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);
11527
+ const rect3 = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
11528
+ const rgba = readRegion(layer.surface, rect3);
11529
+ const bytes = new Uint8Array(rect3.width * rect3.height);
11226
11530
  const hard = init.antiAlias === false;
11227
11531
  for (let i = 0; i < bytes.length; i++) {
11228
11532
  const a = rgba[i * 4 + 3];
11229
11533
  bytes[i] = hard ? a >= 128 ? 255 : 0 : a;
11230
11534
  }
11231
- mask = selectionFromRegion(width, height, rect2, bytes);
11535
+ mask = selectionFromRegion(width, height, rect3, bytes);
11232
11536
  }
11233
11537
  if ((init.feather ?? 0) > 0 && mask.bounds) mask = modifySelection(mask, "feather", init.feather);
11234
11538
  return this.setSelection(mask.bounds ? mask : null, init.mode ?? "replace");
@@ -11406,10 +11710,10 @@ var ImageEditorController = class {
11406
11710
  return shaper ? layoutText(layer, shaper) : null;
11407
11711
  }
11408
11712
  /** The text index nearest a document point (for clicks on a type layer). */
11409
- textIndexAt(id, point) {
11713
+ textIndexAt(id, point2) {
11410
11714
  const layer = requireTypeLayer(this.state.layers, id);
11411
11715
  const layout = this.getTextLayout(id);
11412
- return layout ? textIndexAt(layout, toTextSpace(layer, point)) : layer.text.length;
11716
+ return layout ? textIndexAt(layout, toTextSpace(layer, point2)) : layer.text.length;
11413
11717
  }
11414
11718
  /** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */
11415
11719
  textCaret(id, index) {
@@ -11587,8 +11891,8 @@ var ImageEditorController = class {
11587
11891
  delete content.mask;
11588
11892
  delete content.clipped;
11589
11893
  delete content.fillOpacity;
11590
- const rect2 = spatial ? canvas : this.getLayerBounds(id) ?? canvas;
11591
- const surface = await this.compositeToSurface([content], rect2);
11894
+ const rect3 = spatial ? canvas : this.getLayerBounds(id) ?? canvas;
11895
+ const surface = await this.compositeToSurface([content], rect3);
11592
11896
  next = { ...layer, surface, offset: { x: 0, y: 0 } };
11593
11897
  delete next.filters;
11594
11898
  delete next.source;
@@ -12166,6 +12470,7 @@ var ImageEditorController = class {
12166
12470
  try {
12167
12471
  out = this.serializeState();
12168
12472
  if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
12473
+ if (this.state.selection?.bounds && this.state.selectionRecipe) out.selectionRecipe = JSON.parse(JSON.stringify(this.state.selectionRecipe));
12169
12474
  if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
12170
12475
  if (this.state.paths?.length) out.paths = serializePaths(this.state.paths);
12171
12476
  } finally {
@@ -12207,6 +12512,7 @@ var ImageEditorController = class {
12207
12512
  }
12208
12513
  }
12209
12514
  this.state.selection = selection;
12515
+ this.state.selectionRecipe = selection ? readStoredRecipe(data.selectionRecipe) : null;
12210
12516
  this.dropMismatchedSelection();
12211
12517
  const problems = [];
12212
12518
  const channels = deserializeChannels(data.channels, this.naturalSize, problems);
@@ -13437,10 +13743,10 @@ function sampleOptions(args) {
13437
13743
  return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
13438
13744
  }
13439
13745
  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;
13746
+ const rect3 = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
13747
+ 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.");
13748
+ if (rect3.width <= 0 || rect3.height <= 0) throw new Error("width and height must be greater than 0.");
13749
+ return rect3;
13444
13750
  }
13445
13751
  function uncertainty(controller) {
13446
13752
  const u = controller.selectionUncertainty();
@@ -13554,6 +13860,83 @@ function createImageSelectionTools(controller) {
13554
13860
  show: show !== false
13555
13861
  })
13556
13862
  };
13863
+ const stepProperties = {
13864
+ 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)." },
13865
+ rect: { ...rectSchemaStrict, description: "rect and ellipse: the box in natural image pixels." },
13866
+ points: { type: "array", minItems: 3, items: pointSchema, description: "polygon: the outline, natural image pixels." },
13867
+ point: { ...pointSchema, description: "magic-wand: where to click." },
13868
+ samples: { type: "array", minItems: 1, items: {}, description: "color-range: colour samples, as select_color_range takes them." },
13869
+ options: { type: "object", description: "magic-wand and color-range: their own options (tolerance, contiguous, fuzziness, \u2026) as those tools take them." },
13870
+ preset: { type: "string", description: "color-range-preset: the preset (reds \u2026 magentas, highlights, midtones, shadows, skinTones)." },
13871
+ falloff: { type: "number", minimum: 0, maximum: 255, description: "color-range-preset (tones): falloff in levels (default 20)." },
13872
+ modify: { type: "string", enum: [...SELECTION_MODIFY_OPS], description: "modify: expand, contract, feather, border or smooth (grow and shrink are older names)." },
13873
+ radius: { type: "number", minimum: 0, maximum: 500, description: "modify: radius in pixels." },
13874
+ params: { type: "object", description: "refine: radius, smartRadius, smooth, feather, contrast, shiftEdge as refine_selection takes them." },
13875
+ mode: modeSchema,
13876
+ feather: featherSchema,
13877
+ invert: { type: "boolean", description: "Shape steps: invert the new shape before combining (default false)." },
13878
+ antiAlias: antiAliasSchema,
13879
+ sample: sampleSchema,
13880
+ sampleMerged: sampleMergedSchema
13881
+ };
13882
+ const RECIPE_RETURNS = "Returns the selection summary and the recipe { live, steps (each with its index) }.";
13883
+ const withRecipe = async (p) => {
13884
+ await p;
13885
+ return { ...summary(controller), recipe: controller.getSelectionRecipe() };
13886
+ };
13887
+ const getRecipeTool = {
13888
+ name: "get_selection_recipe",
13889
+ title: "Read the selection recipe",
13890
+ 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}`,
13891
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
13892
+ annotations: { title: "Read the selection recipe", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13893
+ handler: () => ({ ...summary(controller), recipe: controller.getSelectionRecipe() })
13894
+ };
13895
+ const editStepTool = {
13896
+ name: "edit_selection_step",
13897
+ title: "Edit a selection recipe step",
13898
+ 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}`,
13899
+ inputSchema: {
13900
+ type: "object",
13901
+ 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." } },
13902
+ required: ["index", "changes"],
13903
+ additionalProperties: false
13904
+ },
13905
+ annotations: writeAnnotations("Edit a selection recipe step"),
13906
+ handler: ({ index, changes }) => withRecipe(controller.editSelectionStep(index, changes))
13907
+ };
13908
+ const insertStepTool = {
13909
+ name: "insert_selection_step",
13910
+ title: "Insert a selection recipe step",
13911
+ 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}`,
13912
+ inputSchema: {
13913
+ type: "object",
13914
+ 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." } },
13915
+ required: ["index", "step"],
13916
+ additionalProperties: false
13917
+ },
13918
+ annotations: writeAnnotations("Insert a selection recipe step"),
13919
+ handler: ({ index, step }) => withRecipe(controller.insertSelectionStep(index, step))
13920
+ };
13921
+ const removeStepTool = {
13922
+ name: "remove_selection_step",
13923
+ title: "Remove a selection recipe step",
13924
+ 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}`,
13925
+ inputSchema: { type: "object", properties: { index: { type: "integer", minimum: 0, description: "The step to remove (see get_selection_recipe)." } }, required: ["index"], additionalProperties: false },
13926
+ annotations: writeAnnotations("Remove a selection recipe step"),
13927
+ handler: ({ index }) => withRecipe(controller.removeSelectionStep(index))
13928
+ };
13929
+ const freezeTool = {
13930
+ name: "freeze_selection",
13931
+ title: "Freeze the selection",
13932
+ 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}`,
13933
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
13934
+ annotations: { title: "Freeze the selection", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13935
+ handler: () => {
13936
+ controller.freezeSelection();
13937
+ return { ...summary(controller), recipe: controller.getSelectionRecipe() };
13938
+ }
13939
+ };
13557
13940
  const selectRegion = {
13558
13941
  name: "select_region",
13559
13942
  title: "Select a region",
@@ -13598,12 +13981,12 @@ function createImageSelectionTools(controller) {
13598
13981
  break;
13599
13982
  case "rect":
13600
13983
  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)) {
13984
+ const rect3 = { x: num2(x, NaN), y: num2(y, NaN), width: num2(width, NaN), height: num2(height, NaN) };
13985
+ if (![rect3.x, rect3.y, rect3.width, rect3.height].every(Number.isFinite)) {
13603
13986
  throw new Error("rect and ellipse need numeric x, y, width and height.");
13604
13987
  }
13605
- if (shape === "rect") controller.selectRect(rect2, opts);
13606
- else controller.selectEllipse(rect2, opts);
13988
+ if (shape === "rect") controller.selectRect(rect3, opts);
13989
+ else controller.selectEllipse(rect3, opts);
13607
13990
  break;
13608
13991
  }
13609
13992
  case "polygon": {
@@ -13982,10 +14365,10 @@ function createImageSelectionTools(controller) {
13982
14365
  },
13983
14366
  annotations: { ...writeAnnotations("Object Selection"), openWorldHint: true },
13984
14367
  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.");
14368
+ const rect3 = args.rect;
14369
+ const point2 = args.point;
14370
+ if (!rect3 && !point2) throw new Error("Pass rect (a box around the object) or point (a point on it).");
14371
+ if (rect3 && point2) throw new Error("Pass rect or point, not both.");
13989
14372
  const options = {
13990
14373
  mode: parseSelectionMode(args.mode),
13991
14374
  sampleAllLayers: args.sampleAllLayers,
@@ -13993,7 +14376,7 @@ function createImageSelectionTools(controller) {
13993
14376
  useModel: args.useModel,
13994
14377
  signal: context?.signal
13995
14378
  };
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);
14379
+ 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
14380
  return { ...summary(controller), method, ...uncertainty(controller) };
13998
14381
  }
13999
14382
  };
@@ -14632,6 +15015,11 @@ function createImageSelectionTools(controller) {
14632
15015
  getSelection,
14633
15016
  inspectEdge,
14634
15017
  compareTool,
15018
+ getRecipeTool,
15019
+ editStepTool,
15020
+ insertStepTool,
15021
+ removeStepTool,
15022
+ freezeTool,
14635
15023
  selectAll,
14636
15024
  deselect,
14637
15025
  invert2,
@@ -14771,7 +15159,7 @@ async function renderImageRegion(controller, request) {
14771
15159
  out.height = request.height;
14772
15160
  const context = out.getContext("2d");
14773
15161
  if (!context) throw new Error(NO_CANVAS);
14774
- const { rect: rect2 } = request;
15162
+ const { rect: rect3 } = request;
14775
15163
  context.imageSmoothingEnabled = request.scale < 1;
14776
15164
  if (request.format === "jpeg") {
14777
15165
  context.fillStyle = "#ffffff";
@@ -14779,22 +15167,22 @@ async function renderImageRegion(controller, request) {
14779
15167
  }
14780
15168
  const kx = view.width / state.imageDimensions.width;
14781
15169
  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);
15170
+ context.drawImage(view, rect3.x * kx, rect3.y * ky, rect3.width * kx, rect3.height * ky, 0, 0, out.width, out.height);
14783
15171
  if (request.includeOverlays) {
14784
15172
  const s = request.scale;
14785
15173
  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);
15174
+ if (maskView) context.drawImage(maskView, rect3.x, rect3.y, rect3.width, rect3.height, 0, 0, out.width, out.height);
14787
15175
  const selection = state.selection;
14788
15176
  if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {
14789
15177
  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);
15178
+ if (shade) context.drawImage(shade, rect3.x, rect3.y, rect3.width, rect3.height, 0, 0, out.width, out.height);
14791
15179
  }
14792
15180
  if (state.crop) {
14793
15181
  context.save();
14794
15182
  context.strokeStyle = "#ffd400";
14795
15183
  context.lineWidth = 2;
14796
15184
  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);
15185
+ context.strokeRect((state.crop.x - rect3.x) * s, (state.crop.y - rect3.y) * s, state.crop.width * s, state.crop.height * s);
14798
15186
  context.restore();
14799
15187
  }
14800
15188
  }
@@ -16051,11 +16439,11 @@ function createImageEffectTools(controller) {
16051
16439
  }
16052
16440
 
16053
16441
  // 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;
16442
+ function snapTo45(from, point2) {
16443
+ const dx = point2.x - from.x;
16444
+ const dy = point2.y - from.y;
16057
16445
  const len = Math.hypot(dx, dy);
16058
- if (len === 0) return point;
16446
+ if (len === 0) return point2;
16059
16447
  const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
16060
16448
  return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
16061
16449
  }
@@ -16143,38 +16531,38 @@ var SelectionGesture = class {
16143
16531
  const { tool } = this.getOptions();
16144
16532
  if (tool === "quick") return null;
16145
16533
  if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
16146
- const rect2 = normalizeRect({
16534
+ const rect3 = normalizeRect({
16147
16535
  x: this.start.x,
16148
16536
  y: this.start.y,
16149
16537
  width: this.current.x - this.start.x,
16150
16538
  height: this.current.y - this.start.y
16151
16539
  });
16152
- if (rect2.width < 1 && rect2.height < 1) return null;
16153
- if (tool === "ellipse") return ellipsePoints(rect2);
16540
+ if (rect3.width < 1 && rect3.height < 1) return null;
16541
+ if (tool === "ellipse") return ellipsePoints(rect3);
16154
16542
  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 }
16543
+ { x: rect3.x, y: rect3.y },
16544
+ { x: rect3.x + rect3.width, y: rect3.y },
16545
+ { x: rect3.x + rect3.width, y: rect3.y + rect3.height },
16546
+ { x: rect3.x, y: rect3.y + rect3.height }
16159
16547
  ];
16160
16548
  }
16161
16549
  /** Pointer down. Resolves once a wand click has been applied. */
16162
- async begin(point, modifiers = {}) {
16550
+ async begin(point2, modifiers = {}) {
16163
16551
  const options = this.getOptions();
16164
16552
  this.error = null;
16165
16553
  if (this.polygon && options.tool !== "polygon" || this.magnetic && options.tool !== "magnetic") this.cancel();
16166
- if (options.tool === "magnetic") return this.magneticClick(point, modifiers, options);
16554
+ if (options.tool === "magnetic") return this.magneticClick(point2, modifiers, options);
16167
16555
  if (options.tool === "polygon") {
16168
- this.pressedAt = point;
16556
+ this.pressedAt = point2;
16169
16557
  this.freehand = false;
16170
16558
  if (!this.polygon) {
16171
16559
  this.modifiers = modifiers;
16172
- this.polygon = [point];
16173
- this.current = point;
16560
+ this.polygon = [point2];
16561
+ this.current = point2;
16174
16562
  return;
16175
16563
  }
16176
16564
  const last = this.polygon[this.polygon.length - 1];
16177
- const p = modifiers.shiftKey ? snapTo45(last, point) : point;
16565
+ const p = modifiers.shiftKey ? snapTo45(last, point2) : point2;
16178
16566
  const first = this.polygon[0];
16179
16567
  if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
16180
16568
  this.close();
@@ -16189,7 +16577,7 @@ var SelectionGesture = class {
16189
16577
  if (options.tool === "row" || options.tool === "column") {
16190
16578
  this.start = null;
16191
16579
  try {
16192
- this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
16580
+ this.controller.selectLine(options.tool, options.tool === "row" ? point2.y : point2.x, {
16193
16581
  mode: selectionModeFromModifiers(modifiers, options.mode),
16194
16582
  feather: options.feather ?? 0
16195
16583
  });
@@ -16203,8 +16591,8 @@ var SelectionGesture = class {
16203
16591
  try {
16204
16592
  this.controller.selectTextShape({
16205
16593
  text: options.typeText || "Type",
16206
- x: point.x,
16207
- y: point.y,
16594
+ x: point2.x,
16595
+ y: point2.y,
16208
16596
  orientation: options.tool === "type-vertical" ? "vertical" : "horizontal",
16209
16597
  style: { fontSize: options.typeSize ?? 72, fontFamily: options.typeFamily || "sans-serif" },
16210
16598
  mode: selectionModeFromModifiers(modifiers, options.mode),
@@ -16228,11 +16616,11 @@ var SelectionGesture = class {
16228
16616
  if (options.tool === "range") {
16229
16617
  const pixels = await this.controller.getSamplePixels(common);
16230
16618
  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)));
16619
+ const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point2.x)));
16620
+ const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point2.y)));
16233
16621
  const p = (y * pixels.width + x) * 4;
16234
16622
  const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
16235
- await this.controller.selectColorRange([{ color, at: point }], {
16623
+ await this.controller.selectColorRange([{ color, at: point2 }], {
16236
16624
  ...common,
16237
16625
  fuzziness: options.fuzziness ?? 40,
16238
16626
  localized: options.localized ? options.localized : void 0,
@@ -16240,7 +16628,7 @@ var SelectionGesture = class {
16240
16628
  });
16241
16629
  return;
16242
16630
  }
16243
- await this.controller.magicWand(point, {
16631
+ await this.controller.magicWand(point2, {
16244
16632
  ...common,
16245
16633
  tolerance: options.tolerance,
16246
16634
  contiguous: options.contiguous,
@@ -16252,9 +16640,9 @@ var SelectionGesture = class {
16252
16640
  }
16253
16641
  return;
16254
16642
  }
16255
- this.start = point;
16256
- this.current = point;
16257
- this.points = [point];
16643
+ this.start = point2;
16644
+ this.current = point2;
16645
+ this.points = [point2];
16258
16646
  if (options.tool === "quick") {
16259
16647
  const picked = selectionModeFromModifiers(modifiers, options.mode);
16260
16648
  const base = this.controller.getSelection();
@@ -16269,53 +16657,53 @@ var SelectionGesture = class {
16269
16657
  }
16270
16658
  }
16271
16659
  /** Pointer move; returns true when the draft changed (re-render). */
16272
- move(point, modifiers = {}) {
16660
+ move(point2, modifiers = {}) {
16273
16661
  if (this.magnetic) {
16274
- this.current = point;
16275
- this.followEdges(point);
16662
+ this.current = point2;
16663
+ this.followEdges(point2);
16276
16664
  return true;
16277
16665
  }
16278
16666
  if (this.polygon) {
16279
16667
  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)) {
16668
+ if (this.pressedAt && (this.freehand || Math.hypot(point2.x - this.pressedAt.x, point2.y - this.pressedAt.y) >= 3)) {
16281
16669
  this.freehand = true;
16282
- if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
16670
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) >= 0.5) this.polygon.push(point2);
16283
16671
  this.current = null;
16284
16672
  return true;
16285
16673
  }
16286
- this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
16674
+ this.current = modifiers.shiftKey ? snapTo45(last, point2) : point2;
16287
16675
  return true;
16288
16676
  }
16289
16677
  if (!this.start) return false;
16290
- this.current = point;
16678
+ this.current = point2;
16291
16679
  const drawn = this.getOptions().tool;
16292
16680
  if (drawn === "lasso" || drawn === "quick") {
16293
16681
  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);
16682
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) < 0.5) return false;
16683
+ this.points.push(point2);
16296
16684
  if (drawn === "quick") this.growPreview();
16297
16685
  }
16298
16686
  return true;
16299
16687
  }
16300
16688
  /** Pointer up: commits the shape. Returns true when the selection changed. */
16301
- end(point, modifiers) {
16689
+ end(point2, modifiers) {
16302
16690
  if (this.magnetic) return false;
16303
16691
  if (this.polygon) {
16304
- if (point && this.freehand) this.move(point);
16692
+ if (point2 && this.freehand) this.move(point2);
16305
16693
  this.pressedAt = null;
16306
16694
  this.freehand = false;
16307
16695
  return false;
16308
16696
  }
16309
16697
  if (!this.start) return false;
16310
- if (point) this.move(point);
16698
+ if (point2) this.move(point2);
16311
16699
  const options = this.getOptions();
16312
16700
  const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
16313
16701
  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 });
16702
+ 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
16703
  this.start = null;
16316
16704
  this.current = null;
16317
16705
  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 } };
16706
+ const target = rect4.width >= 4 && rect4.height >= 4 ? { rect: rect4 } : { point: { x: rect4.x + rect4.width / 2, y: rect4.y + rect4.height / 2 } };
16319
16707
  this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
16320
16708
  () => void 0,
16321
16709
  (err) => {
@@ -16345,7 +16733,7 @@ var SelectionGesture = class {
16345
16733
  return false;
16346
16734
  }
16347
16735
  const path = this.draftPath;
16348
- const rect2 = normalizeRect({
16736
+ const rect3 = normalizeRect({
16349
16737
  x: this.start.x,
16350
16738
  y: this.start.y,
16351
16739
  width: this.current.x - this.start.x,
@@ -16357,7 +16745,7 @@ var SelectionGesture = class {
16357
16745
  if (!path) return mode === "replace" ? this.controller.deselect() : false;
16358
16746
  const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
16359
16747
  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);
16748
+ return options.tool === "ellipse" ? this.controller.selectEllipse(rect3, opts) : this.controller.selectRect(rect3, opts);
16361
16749
  }
16362
16750
  /** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
16363
16751
  close() {
@@ -16406,19 +16794,19 @@ var SelectionGesture = class {
16406
16794
  corridor(a, b) {
16407
16795
  return Math.max(2 * (this.getOptions().magneticWidth ?? 10), 16, Math.hypot(b.x - a.x, b.y - a.y) / 4);
16408
16796
  }
16409
- async magneticClick(point, modifiers, options) {
16797
+ async magneticClick(point2, modifiers, options) {
16410
16798
  const m = this.magnetic;
16411
16799
  if (!m) {
16412
16800
  this.modifiers = modifiers;
16413
- const draft = { fixed: [point], anchors: [0], live: [], edges: null };
16801
+ const draft = { fixed: [point2], anchors: [0], live: [], edges: null };
16414
16802
  this.magnetic = draft;
16415
- this.current = point;
16803
+ this.current = point2;
16416
16804
  try {
16417
16805
  const pixels = await this.controller.getSamplePixels({ sampleMerged: options.sampleMerged, sample: options.sample });
16418
16806
  if (!pixels) throw new Error("The image pixels are not available to find edges in.");
16419
16807
  if (this.magnetic !== draft) return;
16420
16808
  draft.edges = new EdgeMap(pixels, { contrast: (options.magneticContrast ?? 10) / 100 });
16421
- draft.fixed = [snapToEdge(draft.edges, point, options.magneticWidth ?? 10)];
16809
+ draft.fixed = [snapToEdge(draft.edges, point2, options.magneticWidth ?? 10)];
16422
16810
  if (this.current) this.followEdges(this.current);
16423
16811
  } catch (err) {
16424
16812
  this.error = err instanceof Error ? err.message : String(err);
@@ -16427,12 +16815,12 @@ var SelectionGesture = class {
16427
16815
  return;
16428
16816
  }
16429
16817
  const first = m.fixed[0];
16430
- if (m.anchors.length >= 2 && Math.hypot(point.x - first.x, point.y - first.y) <= this.closeRadius) {
16818
+ if (m.anchors.length >= 2 && Math.hypot(point2.x - first.x, point2.y - first.y) <= this.closeRadius) {
16431
16819
  this.close();
16432
16820
  return;
16433
16821
  }
16434
- this.current = point;
16435
- this.followEdges(point);
16822
+ this.current = point2;
16823
+ this.followEdges(point2);
16436
16824
  if (m.live.length > 1) {
16437
16825
  m.fixed.push(...m.live.slice(1));
16438
16826
  m.anchors.push(m.fixed.length - 1);
@@ -16441,15 +16829,15 @@ var SelectionGesture = class {
16441
16829
  if ((modifiers.detail ?? 1) >= 2) this.close();
16442
16830
  }
16443
16831
  /** Updates the live path to `point`, fixing anchors along it every `frequency` px (with `place`). */
16444
- followEdges(point, place = true) {
16832
+ followEdges(point2, place = true) {
16445
16833
  const m = this.magnetic;
16446
16834
  const last = () => m.fixed[m.fixed.length - 1];
16447
16835
  if (!m.edges) {
16448
- m.live = [last(), point];
16836
+ m.live = [last(), point2];
16449
16837
  return;
16450
16838
  }
16451
16839
  const options = this.getOptions();
16452
- const target = snapToEdge(m.edges, point, options.magneticWidth ?? 10);
16840
+ const target = snapToEdge(m.edges, point2, options.magneticWidth ?? 10);
16453
16841
  const spacing = magneticAnchorSpacing(options.magneticFrequency ?? 57);
16454
16842
  for (let guard = 0; guard < 64; guard++) {
16455
16843
  const path = liveWire(m.edges, last(), target, this.corridor(last(), target));
@@ -19422,11 +19810,11 @@ function hitTestCurvePoint(points, x, y, width, height, radius = 8) {
19422
19810
  return best;
19423
19811
  }
19424
19812
  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);
19813
+ const point2 = { input: clampLevel(p.input), output: clampLevel(p.output) };
19814
+ const out = points.filter((q) => q.input !== point2.input).map((q) => ({ ...q }));
19815
+ let index = out.findIndex((q) => q.input > point2.input);
19428
19816
  if (index === -1) index = out.length;
19429
- out.splice(index, 0, point);
19817
+ out.splice(index, 0, point2);
19430
19818
  return { points: out, index };
19431
19819
  }
19432
19820
  function moveCurvePoint(points, index, p) {
@@ -19795,25 +20183,25 @@ var SelectAndMaskSession = class {
19795
20183
  * Selection, brushes, lasso) or a box (Object Selection). Alt (or the subtract mode) removes.
19796
20184
  * Hand and Zoom are the UI's own; they return false here.
19797
20185
  */
19798
- begin(point, modifiers = {}) {
20186
+ begin(point2, modifiers = {}) {
19799
20187
  const s = this.settingsValue;
19800
20188
  const mode = modifiers.altKey ? s.brushMode === "add" ? "subtract" : "add" : s.brushMode;
19801
- this.dragStart = point;
20189
+ this.dragStart = point2;
19802
20190
  switch (s.tool) {
19803
20191
  case "quick-select":
19804
- this.live = { kind: "quick-select", points: [point], size: s.brushSize, mode };
20192
+ this.live = { kind: "quick-select", points: [point2], size: s.brushSize, mode };
19805
20193
  break;
19806
20194
  case "brush":
19807
- this.live = { kind: "brush", points: [point], size: s.brushSize, mode };
20195
+ this.live = { kind: "brush", points: [point2], size: s.brushSize, mode };
19808
20196
  break;
19809
20197
  case "refine-brush":
19810
- this.live = { kind: "refine-brush", points: [point], size: s.brushSize };
20198
+ this.live = { kind: "refine-brush", points: [point2], size: s.brushSize };
19811
20199
  break;
19812
20200
  case "lasso":
19813
- this.live = { kind: "lasso", points: [point], mode };
20201
+ this.live = { kind: "lasso", points: [point2], mode };
19814
20202
  break;
19815
20203
  case "object":
19816
- this.live = { kind: "object", point, mode };
20204
+ this.live = { kind: "object", point: point2, mode };
19817
20205
  break;
19818
20206
  default:
19819
20207
  this.dragStart = null;
@@ -19823,18 +20211,18 @@ var SelectAndMaskSession = class {
19823
20211
  return true;
19824
20212
  }
19825
20213
  /** Pointer move while a stroke is down. */
19826
- move(point) {
20214
+ move(point2) {
19827
20215
  const live = this.live;
19828
20216
  if (!live || !this.dragStart) return;
19829
20217
  if (live.kind === "object") {
19830
20218
  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 };
20219
+ 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) };
20220
+ this.live = rect3.width >= 4 && rect3.height >= 4 ? { kind: "object", rect: rect3, mode: live.mode } : { kind: "object", point: a, mode: live.mode };
19833
20221
  return;
19834
20222
  }
19835
20223
  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);
20224
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) < 1) return;
20225
+ live.points.push(point2);
19838
20226
  if (this.settingsValue.realTime && live.kind !== "lasso") this.recompute();
19839
20227
  }
19840
20228
  /** Pointer up: the stroke becomes an edit (one step of the workspace's own undo). */
@@ -21866,7 +22254,7 @@ var PathEditGesture = class {
21866
22254
  if (!g) return;
21867
22255
  if (g.kind === "marquee") {
21868
22256
  g.end = pt(p);
21869
- const box = rect(g.start, g.end);
22257
+ const box = rect2(g.start, g.end);
21870
22258
  const cs = this.target ? this.controller.getPathEditComponents(this.target) : [];
21871
22259
  this.selected = new Set(g.keep);
21872
22260
  cs.forEach(
@@ -22001,11 +22389,11 @@ var PathEditGesture = class {
22001
22389
  const last = sp?.anchors[sp.anchors.length - 1];
22002
22390
  if (last) preview = last.out ? [pt(last), pt(last.out), this.pointer, this.pointer] : [pt(last), this.pointer];
22003
22391
  }
22004
- const marquee = this.drag?.kind === "marquee" ? rect(this.drag.start, this.drag.end) : null;
22392
+ const marquee = this.drag?.kind === "marquee" ? rect2(this.drag.start, this.drag.end) : null;
22005
22393
  return { curves, ...edges.length ? { curveModes } : {}, anchors, handles, marquee, preview };
22006
22394
  }
22007
22395
  };
22008
- function rect(a, b) {
22396
+ function rect2(a, b) {
22009
22397
  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
22398
  }
22011
22399
  function prune(cs) {
@@ -22206,21 +22594,21 @@ var ContentAwareFillSession = class _ContentAwareFillSession {
22206
22594
  }
22207
22595
  }
22208
22596
  /** Pointer down with the Sampling Brush (Alt flips add and subtract). Returns false for other tools. */
22209
- begin(point, modifiers = {}) {
22597
+ begin(point2, modifiers = {}) {
22210
22598
  if (this.settingsValue.tool !== "sampling-brush" || !this.win) return false;
22211
22599
  const add2 = this.settingsValue.brushMode === "add" !== Boolean(modifiers.altKey);
22212
22600
  this.settingsValue = { ...this.settingsValue, sampling: "custom" };
22213
- this.lastPoint = point;
22601
+ this.lastPoint = point2;
22214
22602
  this.strokeAdds = add2;
22215
- this.paint(point, point, add2);
22603
+ this.paint(point2, point2, add2);
22216
22604
  this.emit();
22217
22605
  return true;
22218
22606
  }
22219
22607
  strokeAdds = true;
22220
- move(point) {
22608
+ move(point2) {
22221
22609
  if (!this.lastPoint) return;
22222
- this.paint(this.lastPoint, point, this.strokeAdds);
22223
- this.lastPoint = point;
22610
+ this.paint(this.lastPoint, point2, this.strokeAdds);
22611
+ this.lastPoint = point2;
22224
22612
  this.emit();
22225
22613
  }
22226
22614
  /** Pointer up: the preview follows the new area when live. */
@@ -22534,6 +22922,7 @@ export {
22534
22922
  SELECTION_MODES,
22535
22923
  SELECTION_MODE_LABELS,
22536
22924
  SELECTION_MODIFY_OPS,
22925
+ SELECTION_RECIPE_OPS,
22537
22926
  SELECTION_TOOLS,
22538
22927
  SELECT_AND_MASK_TOOLS,
22539
22928
  SELECT_AND_MASK_VIEWS,
@@ -22799,6 +23188,8 @@ export {
22799
23188
  describeChannel,
22800
23189
  describeLayer,
22801
23190
  describeLayerEffects,
23191
+ describeRecipe,
23192
+ describeRecipeStep,
22802
23193
  describeSelection,
22803
23194
  describeTransform,
22804
23195
  deserializeChannels,
@@ -22983,6 +23374,7 @@ export {
22983
23374
  isSelectionEmpty,
22984
23375
  isSelectionMode,
22985
23376
  isSeparableBlendMode,
23377
+ isShapeStep,
22986
23378
  isSmartSurfaceIncomplete,
22987
23379
  isSpatialFilter,
22988
23380
  isSrgb,
@@ -23190,9 +23582,11 @@ export {
23190
23582
  readPaths,
23191
23583
  readPixel,
23192
23584
  readPsd,
23585
+ readRecipeStep,
23193
23586
  readRegion,
23194
23587
  readShapeComponents,
23195
23588
  readSourceRegion,
23589
+ recipeStepSetting,
23196
23590
  rectSelection,
23197
23591
  rectToQuad,
23198
23592
  reduceSurface,
@@ -23217,6 +23611,7 @@ export {
23217
23611
  renderText,
23218
23612
  replaceContents,
23219
23613
  replaceTextRange,
23614
+ replaySelectionRecipe,
23220
23615
  requireLayer,
23221
23616
  requireOperation,
23222
23617
  requireShapeLayer,
@@ -23249,6 +23644,7 @@ export {
23249
23644
  sameCanvasViewport,
23250
23645
  sampleLut,
23251
23646
  sampleToneLut,
23647
+ samplingKey,
23252
23648
  saturationOf,
23253
23649
  scaleLayerEffect,
23254
23650
  scalePaintGradient,
@@ -23336,6 +23732,7 @@ export {
23336
23732
  standardBlendMode,
23337
23733
  startMarchingAnts,
23338
23734
  startSelectionWorker,
23735
+ stepNeedsPixels,
23339
23736
  stepSlotTs,
23340
23737
  strokeGrainCatch,
23341
23738
  styleAt,