@pixerate/editor 0.213.0 → 0.215.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{chunk-UOGKSVRQ.cjs → chunk-B46UQUDC.cjs} +770 -733
  2. package/dist/{chunk-W3TOZTL7.js → chunk-F7Z2MKY2.js} +115 -4
  3. package/dist/{chunk-YBJXCQTR.js → chunk-QXY4YTRI.js} +738 -701
  4. package/dist/{chunk-H65IBXFI.cjs → chunk-S55JFWDA.cjs} +350 -239
  5. package/dist/{chunk-QYLWG22N.cjs → chunk-SS53NNZO.cjs} +304 -1
  6. package/dist/{chunk-LQ5SALHK.js → chunk-U7C655AM.js} +5 -311
  7. package/dist/{chunk-L52VMQZS.cjs → chunk-UC3PWFM3.cjs} +68 -374
  8. package/dist/{chunk-L3G23N5Y.js → chunk-ZNA3IQZF.js} +304 -1
  9. package/dist/image-editor/controller.d.cts +44 -0
  10. package/dist/image-editor/controller.d.ts +44 -0
  11. package/dist/image-editor/index.cjs +1768 -1140
  12. package/dist/image-editor/index.d.cts +1 -0
  13. package/dist/image-editor/index.d.ts +1 -0
  14. package/dist/image-editor/index.js +660 -32
  15. package/dist/image-editor/layers/edge-color.d.cts +27 -0
  16. package/dist/image-editor/layers/edge-color.d.ts +27 -0
  17. package/dist/image-editor/layers/serialize.d.cts +13 -0
  18. package/dist/image-editor/layers/serialize.d.ts +13 -0
  19. package/dist/image-editor/selection/index.d.cts +1 -0
  20. package/dist/image-editor/selection/index.d.ts +1 -0
  21. package/dist/image-editor/selection/inspect.d.cts +70 -0
  22. package/dist/image-editor/selection/inspect.d.ts +70 -0
  23. package/dist/image-editor/selection-worker.cjs +4 -4
  24. package/dist/image-editor/selection-worker.js +2 -2
  25. package/dist/paint-editor/index.cjs +79 -79
  26. package/dist/paint-editor/index.js +3 -3
  27. package/dist/raster/compositor.d.cts +10 -1
  28. package/dist/raster/compositor.d.ts +10 -1
  29. package/dist/raster/deep-compositor.d.cts +1 -1
  30. package/dist/raster/deep-compositor.d.ts +1 -1
  31. package/dist/raster/index.cjs +4 -2
  32. package/dist/raster/index.js +3 -1
  33. package/dist/raster/layers.d.cts +16 -0
  34. package/dist/raster/layers.d.ts +16 -0
  35. package/package.json +1 -1
  36. package/skills/pixerate-image-editor/SKILL.md +9 -6
  37. package/skills/pixerate-image-editor/references/paths.md +12 -0
  38. package/skills/pixerate-image-selection-masking/SKILL.md +17 -17
@@ -7,9 +7,7 @@ import {
7
7
  } from "../chunk-DJBYJOHM.js";
8
8
  import {
9
9
  ColorGmm,
10
- DEFAULT_REFINE_EDGE_PARAMS,
11
10
  GAMMA,
12
- GUIDED_FILTER_EPS,
13
11
  GridGraph,
14
12
  REFINE_EDIT_KINDS,
15
13
  SelectionWorkerClient,
@@ -17,31 +15,22 @@ import {
17
15
  WORK_SIZE,
18
16
  applyRefineEdits,
19
17
  boxBlur,
20
- boxMean,
21
18
  brushStrokeMask,
22
- decontaminateColors,
23
19
  deserializeMask,
24
- edgeWidth,
25
20
  ellipsePoints,
26
21
  ellipseSelection,
27
22
  findSky,
28
23
  findSubject,
29
24
  focusArea,
30
25
  grabCutLabels,
31
- guidePlanes,
32
- guidedFilterColor,
33
26
  keepMainPieces,
34
27
  normalizeRect,
35
- normalizeRefineEdgeParams,
36
28
  otsuThreshold,
37
29
  polygonCoverage,
38
30
  polygonCoverageBox,
39
31
  polygonSelection,
40
32
  quickSelectStroke,
41
33
  rectSelection,
42
- refineCoverage,
43
- refineEdgeHalo,
44
- refineSelection,
45
34
  runSelectionJob,
46
35
  selectObjectIn,
47
36
  serializeMask,
@@ -54,7 +43,7 @@ import {
54
43
  workImage,
55
44
  workLuma,
56
45
  workPlanes
57
- } from "../chunk-LQ5SALHK.js";
46
+ } from "../chunk-U7C655AM.js";
58
47
  import {
59
48
  ADJUSTMENT_KEYS,
60
49
  ADJUSTMENT_KINDS,
@@ -72,6 +61,7 @@ import {
72
61
  DEFAULT_MITER_LIMIT,
73
62
  DEFAULT_TRANSFORM_VALUES,
74
63
  DEVELOP_FILTER_ID,
64
+ EDGE_COLOR_RADIUS,
75
65
  FREE_TRANSFORM_MODES,
76
66
  HUE_RANGE_LABELS,
77
67
  IDENTITY_MATRIX3,
@@ -127,6 +117,7 @@ import {
127
117
  applyBoxBlur,
128
118
  applyBoxBlurFloat,
129
119
  applyDepthMask,
120
+ applyEdgeUnmix,
130
121
  applyImageProjectionToContext,
131
122
  applyLegacyAdjustments,
132
123
  applyLegacyFloat,
@@ -175,6 +166,7 @@ import {
175
166
  deserializeLayer,
176
167
  deserializeLayers,
177
168
  deserializePatterns,
169
+ despillEdge,
178
170
  developFilter,
179
171
  developFromAdjustments,
180
172
  developTarget,
@@ -197,7 +189,9 @@ import {
197
189
  getViewportMetrics,
198
190
  getViewportProjection,
199
191
  groupLayers,
192
+ hasEdgeUnmix,
200
193
  hasResampledLegacyMask,
194
+ hasSoftBand,
201
195
  hasSurface,
202
196
  hasWarp,
203
197
  hexToRgb,
@@ -248,6 +242,7 @@ import {
248
242
  liveShapeSubpaths,
249
243
  mapCropHandle,
250
244
  mapSubpaths,
245
+ maskCoverageOverCanvas,
251
246
  matrixFromCorners,
252
247
  matrixFromParams,
253
248
  mergeShapeComponents,
@@ -302,6 +297,7 @@ import {
302
297
  rasterMaskFromLegacy,
303
298
  rasterizeCoverage,
304
299
  rasterizeSmartObject,
300
+ readEdgeColor,
305
301
  readPsd,
306
302
  readShapeComponents,
307
303
  referencePoint,
@@ -404,34 +400,45 @@ import {
404
400
  writePsd,
405
401
  xyToTempTint,
406
402
  xyToUv
407
- } from "../chunk-W3TOZTL7.js";
403
+ } from "../chunk-F7Z2MKY2.js";
408
404
  import {
409
405
  parseCssColor
410
406
  } from "../chunk-3CI7FRMK.js";
411
407
  import {
408
+ DEFAULT_REFINE_EDGE_PARAMS,
412
409
  EMPTY_DISTANCE,
410
+ GUIDED_FILTER_EPS,
413
411
  SELECTION_MODES,
414
412
  SELECTION_MODIFY_OPS,
415
413
  borderCoverage,
414
+ boxMean,
416
415
  combineSelections,
417
416
  computeSelectionBounds,
418
417
  contractCoverage,
419
418
  coverageToByte,
420
419
  createSelectionMask,
421
420
  decodeSelectionRle,
421
+ decontaminateColors,
422
422
  deserializeSelection,
423
423
  distanceTransform,
424
+ edgeWidth,
424
425
  encodeSelectionRle,
425
426
  expandCoverage,
426
427
  featherCoverage,
427
428
  fullSelection,
428
429
  greyMorphology,
430
+ guidePlanes,
431
+ guidedFilterColor,
429
432
  hashMaskContent,
430
433
  invertSelection,
431
434
  isSelectionEmpty,
432
435
  isSelectionMode,
433
436
  modifySelection,
437
+ normalizeRefineEdgeParams,
434
438
  parseSelectionMode,
439
+ refineCoverage,
440
+ refineEdgeHalo,
441
+ refineSelection,
435
442
  resizeSelection,
436
443
  selectionArea,
437
444
  selectionFromCoverage,
@@ -444,7 +451,7 @@ import {
444
451
  serializeSelection,
445
452
  shiftCoverage,
446
453
  smoothCoverage
447
- } from "../chunk-L3G23N5Y.js";
454
+ } from "../chunk-ZNA3IQZF.js";
448
455
  import {
449
456
  EffectRendererPool
450
457
  } from "../chunk-EHLOCVWB.js";
@@ -500,7 +507,7 @@ import {
500
507
  validateLayerEffects,
501
508
  warpSurface,
502
509
  workingToSamples
503
- } from "../chunk-YBJXCQTR.js";
510
+ } from "../chunk-QXY4YTRI.js";
504
511
  import {
505
512
  applyHomography,
506
513
  boxQuad,
@@ -2566,6 +2573,423 @@ function hexRgb(hex) {
2566
2573
  return [v >> 16 & 255, v >> 8 & 255, v & 255];
2567
2574
  }
2568
2575
 
2576
+ // src/image-editor/selection/inspect.ts
2577
+ var MASK_EDGE_THRESHOLDS = { halo: 0.35, alignment: 0.5, jaggedness: 15, soft: 0.9 };
2578
+ var STRONG_EDGE = 0.1;
2579
+ function toLab(r, g, b) {
2580
+ const lin = (v) => {
2581
+ v /= 255;
2582
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
2583
+ };
2584
+ const R = lin(r);
2585
+ const G = lin(g);
2586
+ const B = lin(b);
2587
+ const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
2588
+ const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
2589
+ const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
2590
+ const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
2591
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
2592
+ }
2593
+ var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
2594
+ var r3 = (v) => Math.round(v * 1e3) / 1e3;
2595
+ function inspectMaskEdge(mask, image, options = {}) {
2596
+ const b = mask.bounds;
2597
+ if (!b) throw new Error("The mask is empty: there is no edge to inspect.");
2598
+ const minArea = Math.max(1, options.minArea ?? 16);
2599
+ const W = mask.width;
2600
+ const H = mask.height;
2601
+ const a = mask.data;
2602
+ const alpha = (x, y) => x < 0 || y < 0 || x >= W || y >= H ? 0 : a[y * W + x];
2603
+ const img = image.data;
2604
+ const rgb = (x, y) => {
2605
+ const ix = x - image.x;
2606
+ const iy = y - image.y;
2607
+ if (ix < 0 || iy < 0 || ix >= image.width || iy >= image.height) return null;
2608
+ const i = (iy * image.width + ix) * 4;
2609
+ if (img[i + 3] < 128) return null;
2610
+ return [img[i], img[i + 1], img[i + 2]];
2611
+ };
2612
+ const premul = (x, y, c) => {
2613
+ const ix = Math.min(image.width - 1, Math.max(0, x - image.x));
2614
+ const iy = Math.min(image.height - 1, Math.max(0, y - image.y));
2615
+ const i = (iy * image.width + ix) * 4;
2616
+ return img[i + c] * img[i + 3] / 255;
2617
+ };
2618
+ const gradCache = /* @__PURE__ */ new Map();
2619
+ const gradient = (x, y) => {
2620
+ const k = y * W + x;
2621
+ const hit = gradCache.get(k);
2622
+ if (hit !== void 0) return hit;
2623
+ let best = 0;
2624
+ for (let c = 0; c < 3; c++) {
2625
+ const p = (dx, dy) => premul(x + dx, y + dy, c);
2626
+ const gx = p(1, -1) + 2 * p(1, 0) + p(1, 1) - p(-1, -1) - 2 * p(-1, 0) - p(-1, 1);
2627
+ const gy = p(-1, 1) + 2 * p(0, 1) + p(1, 1) - p(-1, -1) - 2 * p(0, -1) - p(1, -1);
2628
+ best = Math.max(best, Math.hypot(gx, gy) / 1020);
2629
+ }
2630
+ gradCache.set(k, best);
2631
+ return best;
2632
+ };
2633
+ const B = Math.min(256, Math.max(24, Math.round(Math.max(W, H) / 12)));
2634
+ const BW = Math.ceil(W / B);
2635
+ const blocks = /* @__PURE__ */ new Map();
2636
+ const block = (x, y) => {
2637
+ const k = Math.floor(y / B) * BW + Math.floor(x / B);
2638
+ let blk = blocks.get(k);
2639
+ if (!blk) blocks.set(k, blk = { edge: 0, aligned: 0, halo: 0, haloN: 0, turn: 0, len: 0, x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity });
2640
+ return blk;
2641
+ };
2642
+ const grow = (blk, x, y) => {
2643
+ blk.x0 = Math.min(blk.x0, x);
2644
+ blk.y0 = Math.min(blk.y0, y);
2645
+ blk.x1 = Math.max(blk.x1, x);
2646
+ blk.y1 = Math.max(blk.y1, y);
2647
+ };
2648
+ const x0 = Math.max(0, b.x - 1);
2649
+ const y0 = Math.max(0, b.y - 1);
2650
+ const x1 = Math.min(W, b.x + b.width + 1);
2651
+ const y1 = Math.min(H, b.y + b.height + 1);
2652
+ let area = 0;
2653
+ let edgeLength = 0;
2654
+ let aligned = 0;
2655
+ let haloSum = 0;
2656
+ let haloN = 0;
2657
+ let band = 0;
2658
+ let soft = 0;
2659
+ for (let y = y0; y < y1; y++) {
2660
+ for (let x = x0; x < x1; x++) {
2661
+ const v = a[y * W + x];
2662
+ if (v >= 128) area++;
2663
+ let lo = false;
2664
+ let hi = false;
2665
+ for (let dy = -1; dy <= 1; dy++) {
2666
+ for (let dx = -1; dx <= 1; dx++) {
2667
+ const nx = x + dx;
2668
+ const ny = y + dy;
2669
+ if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
2670
+ if (a[ny * W + nx] >= 128) hi = true;
2671
+ else lo = true;
2672
+ }
2673
+ }
2674
+ const partial = v > 0 && v < 255;
2675
+ if (hi && lo || partial) {
2676
+ band++;
2677
+ if (partial) soft++;
2678
+ }
2679
+ if (v < 128) continue;
2680
+ const inCanvasLow = (nx, ny) => nx >= 0 && ny >= 0 && nx < W && ny < H && a[ny * W + nx] < 128;
2681
+ if (!(inCanvasLow(x - 1, y) || inCanvasLow(x + 1, y) || inCanvasLow(x, y - 1) || inCanvasLow(x, y + 1))) continue;
2682
+ edgeLength++;
2683
+ const blk = block(x, y);
2684
+ blk.edge++;
2685
+ grow(blk, x, y);
2686
+ let strong = false;
2687
+ for (let dy = -2; dy <= 2 && !strong; dy++) {
2688
+ for (let dx = -2; dx <= 2 && !strong; dx++) {
2689
+ const nx = x + dx;
2690
+ const ny = y + dy;
2691
+ if (nx >= 0 && ny >= 0 && nx < W && ny < H && gradient(nx, ny) >= STRONG_EDGE) strong = true;
2692
+ }
2693
+ }
2694
+ if (strong) {
2695
+ aligned++;
2696
+ blk.aligned++;
2697
+ }
2698
+ const s = (dx, dy) => alpha(Math.min(W - 1, Math.max(0, x + dx)), Math.min(H - 1, Math.max(0, y + dy)));
2699
+ const gx = s(1, -1) + 2 * s(1, 0) + s(1, 1) - s(-1, -1) - 2 * s(-1, 0) - s(-1, 1);
2700
+ const gy = s(-1, 1) + 2 * s(0, 1) + s(1, 1) - s(-1, -1) - 2 * s(0, -1) - s(1, -1);
2701
+ const len = Math.hypot(gx, gy);
2702
+ if (len < 1e-6) continue;
2703
+ const ux = gx / len;
2704
+ const uy = gy / len;
2705
+ const at = (d) => rgb(Math.round(x + ux * d), Math.round(y + uy * d));
2706
+ const ring = at(1.5);
2707
+ const deep = at(5);
2708
+ const out = at(-3);
2709
+ if (!ring || !deep || !out) continue;
2710
+ const labOut = toLab(...out);
2711
+ const span = dist(toLab(...deep), labOut);
2712
+ if (span < 10) continue;
2713
+ const h = Math.min(1, Math.max(0, 1 - dist(toLab(...ring), labOut) / span));
2714
+ haloSum += h;
2715
+ haloN++;
2716
+ blk.halo += h;
2717
+ blk.haloN++;
2718
+ }
2719
+ }
2720
+ let turnExcess = 0;
2721
+ let outline = 0;
2722
+ for (const loop of traceLoops(a, W, H, x0 - 1, y0 - 1, x1 + 1, y1 + 1)) {
2723
+ const pts = resample(loop, 2);
2724
+ const n = pts.length;
2725
+ if (n < 6) continue;
2726
+ const L = n * 2;
2727
+ let total = 0;
2728
+ let net = 0;
2729
+ const turns = [];
2730
+ for (let i = 0; i < n; i++) {
2731
+ const p0 = pts[(i + n - 1) % n];
2732
+ const p1 = pts[i];
2733
+ const p2 = pts[(i + 1) % n];
2734
+ let d = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) - Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
2735
+ while (d > Math.PI) d -= 2 * Math.PI;
2736
+ while (d <= -Math.PI) d += 2 * Math.PI;
2737
+ turns.push(d);
2738
+ total += Math.abs(d);
2739
+ net += d;
2740
+ }
2741
+ const closure = Math.abs(net);
2742
+ turnExcess += Math.max(0, total - closure);
2743
+ outline += L;
2744
+ for (let i = 0; i < n; i++) {
2745
+ const [px2, py] = pts[i];
2746
+ if (px2 < 0 || py < 0 || px2 >= W || py >= H) continue;
2747
+ const blk = block(Math.floor(px2), Math.floor(py));
2748
+ blk.turn += Math.abs(turns[i]) - closure / n;
2749
+ blk.len += 2;
2750
+ }
2751
+ }
2752
+ const jaggedness = outline ? 100 * turnExcess / outline : 0;
2753
+ const { holes, islands } = regions(a, W, H, b, minArea);
2754
+ const haloScore = haloN ? haloSum / haloN : 0;
2755
+ const edgeAlignment = edgeLength ? aligned / edgeLength : 0;
2756
+ const softFraction = band ? soft / band : 0;
2757
+ const T2 = MASK_EDGE_THRESHOLDS;
2758
+ const candidates = [];
2759
+ const pad = (r) => {
2760
+ const px0 = Math.max(0, r.x - 8);
2761
+ const py0 = Math.max(0, r.y - 8);
2762
+ return { x: px0, y: py0, width: Math.min(W, r.x + r.width + 8) - px0, height: Math.min(H, r.y + r.height + 8) - py0 };
2763
+ };
2764
+ for (const blk of blocks.values()) {
2765
+ if (blk.edge < 8) continue;
2766
+ const bounds = pad({ x: blk.x0, y: blk.y0, width: blk.x1 - blk.x0 + 1, height: blk.y1 - blk.y0 + 1 });
2767
+ const opts = [];
2768
+ if (blk.haloN >= 8) {
2769
+ const h = blk.halo / blk.haloN;
2770
+ if (h >= T2.halo) opts.push({ severity: h, reason: `halo ${r3(h)}: the colour just inside the edge matches the background` });
2771
+ }
2772
+ const al = blk.aligned / blk.edge;
2773
+ if (al < T2.alignment) opts.push({ severity: (1 - al) * 0.9, reason: `alignment ${r3(al)}: the edge is off the image's edges` });
2774
+ if (blk.len >= 16) {
2775
+ const j = 100 * Math.max(0, blk.turn) / blk.len;
2776
+ if (j >= T2.jaggedness) opts.push({ severity: Math.min(1, j / (4 * T2.jaggedness)), reason: `jaggedness ${r3(j)}: the outline zigzags` });
2777
+ }
2778
+ const top = opts.sort((p, q) => q.severity - p.severity)[0];
2779
+ if (top) candidates.push({ ...top, bounds });
2780
+ }
2781
+ const imageArea = W * H;
2782
+ for (const r of holes.largest) candidates.push({ severity: Math.min(0.95, 0.5 + r.area / (0.01 * imageArea)), bounds: pad(r.bounds), reason: `hole of ${r.area} px inside the selection` });
2783
+ for (const r of islands.largest) candidates.push({ severity: Math.min(0.95, 0.5 + r.area / (0.01 * imageArea)), bounds: pad(r.bounds), reason: `island of ${r.area} px apart from the main selection` });
2784
+ const worst = candidates.sort((p, q) => q.severity - p.severity).slice(0, 5).map(({ bounds, reason }) => ({ bounds, reason }));
2785
+ const issues = [];
2786
+ if (haloScore >= T2.halo)
2787
+ issues.push(`Halo (haloScore ${r3(haloScore)} \u2265 ${T2.halo}): background colour is left inside the edge. Contract the edge (modify_selection contract 1\u20132 px, or refine_selection shiftEdge -20 to -40), or on a masked cutout set_edge_decontamination 80\u2013100.`);
2788
+ if (edgeLength >= 16 && edgeAlignment < T2.alignment)
2789
+ issues.push(`Off the image's edges (edgeAlignment ${r3(edgeAlignment)} < ${T2.alignment}): much of the edge does not follow edges in the image. Rebuild it with select_subject, select_object or quick_select, or refine_selection with a larger radius.`);
2790
+ if (jaggedness >= T2.jaggedness) issues.push(`Jagged (jaggedness ${r3(jaggedness)} \u2265 ${T2.jaggedness}): the outline zigzags. Smooth it (refine_selection smooth 20\u201340, or modify_selection smooth).`);
2791
+ if (softFraction >= T2.soft && edgeAlignment >= T2.alignment)
2792
+ issues.push(`Very soft (softFraction ${r3(softFraction)} \u2265 ${T2.soft}): almost the whole edge is partly selected. If the subject has hard edges, raise refine_selection contrast or lower feather.`);
2793
+ if (holes.count) issues.push(`${holes.count} hole(s) (${holes.totalArea} px) inside the selection. If they belong to the subject, fill them (quick_select or select_polygon with mode add).`);
2794
+ if (islands.count) issues.push(`${islands.count} island(s) (${islands.totalArea} px) apart from the main selection. If they are stray, remove them (select_polygon or quick_select with mode subtract).`);
2795
+ return {
2796
+ area,
2797
+ bounds: { ...b },
2798
+ edgeLength,
2799
+ haloScore: r3(haloScore),
2800
+ edgeAlignment: r3(edgeAlignment),
2801
+ jaggedness: r3(jaggedness),
2802
+ softFraction: r3(softFraction),
2803
+ holes,
2804
+ islands,
2805
+ worst,
2806
+ issues
2807
+ };
2808
+ }
2809
+ function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
2810
+ const w = rx1 - rx0;
2811
+ const h = ry1 - ry0;
2812
+ const f = (i, j) => {
2813
+ const x = rx0 + i;
2814
+ const y = ry0 + j;
2815
+ return x < 0 || y < 0 || x >= W || y >= H ? 0 : a[y * W + x];
2816
+ };
2817
+ const on = (i, j) => f(i, j) >= 128;
2818
+ const V = w * h;
2819
+ const point = (id) => {
2820
+ const vertical = id >= V;
2821
+ const k = vertical ? id - V : id;
2822
+ const i = k % w;
2823
+ const j = (k - i) / w;
2824
+ const p = f(i, j);
2825
+ const q = vertical ? f(i, j + 1) : f(i + 1, j);
2826
+ const t = q === p ? 0.5 : Math.min(1, Math.max(0, (127.5 - p) / (q - p)));
2827
+ return vertical ? [rx0 + i, ry0 + j + t] : [rx0 + i + t, ry0 + j];
2828
+ };
2829
+ const segs = [];
2830
+ const touch = /* @__PURE__ */ new Map();
2831
+ const add2 = (e1, e2) => {
2832
+ const s = segs.length / 2;
2833
+ segs.push(e1, e2);
2834
+ for (const e of [e1, e2]) {
2835
+ const list = touch.get(e);
2836
+ if (list) list.push(s);
2837
+ else touch.set(e, [s]);
2838
+ }
2839
+ };
2840
+ for (let j = 0; j < h - 1; j++) {
2841
+ for (let i = 0; i < w - 1; i++) {
2842
+ const c = (on(i, j) ? 1 : 0) | (on(i + 1, j) ? 2 : 0) | (on(i + 1, j + 1) ? 4 : 0) | (on(i, j + 1) ? 8 : 0);
2843
+ if (c === 0 || c === 15) continue;
2844
+ const top = j * w + i;
2845
+ const bottom = (j + 1) * w + i;
2846
+ const left = V + j * w + i;
2847
+ const right = V + j * w + i + 1;
2848
+ const centre = (f(i, j) + f(i + 1, j) + f(i + 1, j + 1) + f(i, j + 1)) / 4 >= 128;
2849
+ switch (c) {
2850
+ case 1:
2851
+ case 14:
2852
+ add2(left, top);
2853
+ break;
2854
+ case 2:
2855
+ case 13:
2856
+ add2(top, right);
2857
+ break;
2858
+ case 3:
2859
+ case 12:
2860
+ add2(left, right);
2861
+ break;
2862
+ case 4:
2863
+ case 11:
2864
+ add2(right, bottom);
2865
+ break;
2866
+ case 6:
2867
+ case 9:
2868
+ add2(top, bottom);
2869
+ break;
2870
+ case 7:
2871
+ case 8:
2872
+ add2(left, bottom);
2873
+ break;
2874
+ case 5:
2875
+ if (centre) {
2876
+ add2(top, right);
2877
+ add2(left, bottom);
2878
+ } else {
2879
+ add2(left, top);
2880
+ add2(right, bottom);
2881
+ }
2882
+ break;
2883
+ case 10:
2884
+ if (centre) {
2885
+ add2(left, top);
2886
+ add2(right, bottom);
2887
+ } else {
2888
+ add2(top, right);
2889
+ add2(left, bottom);
2890
+ }
2891
+ break;
2892
+ }
2893
+ }
2894
+ }
2895
+ const used = new Uint8Array(segs.length / 2);
2896
+ const loops = [];
2897
+ for (let s0 = 0; s0 < used.length; s0++) {
2898
+ if (used[s0]) continue;
2899
+ const loop = [];
2900
+ let s = s0;
2901
+ let from = segs[s * 2];
2902
+ while (!used[s]) {
2903
+ used[s] = 1;
2904
+ loop.push(point(from));
2905
+ const to = segs[s * 2] === from ? segs[s * 2 + 1] : segs[s * 2];
2906
+ const next = touch.get(to).find((t) => !used[t]);
2907
+ from = to;
2908
+ if (next === void 0) break;
2909
+ s = next;
2910
+ }
2911
+ loops.push(loop);
2912
+ }
2913
+ return loops;
2914
+ }
2915
+ function resample(loop, step) {
2916
+ const n = loop.length;
2917
+ if (n < 2) return [];
2918
+ const out = [loop[0]];
2919
+ let carry = 0;
2920
+ for (let i = 0; i < n; i++) {
2921
+ const [ax, ay] = loop[i];
2922
+ const [bx, by] = loop[(i + 1) % n];
2923
+ const seg = Math.hypot(bx - ax, by - ay);
2924
+ let t = step - carry;
2925
+ while (t <= seg) {
2926
+ out.push([ax + (bx - ax) * t / seg, ay + (by - ay) * t / seg]);
2927
+ t += step;
2928
+ }
2929
+ carry = seg - (t - step);
2930
+ }
2931
+ if (out.length > 1 && Math.hypot(out[out.length - 1][0] - out[0][0], out[out.length - 1][1] - out[0][1]) < step / 2) out.pop();
2932
+ return out;
2933
+ }
2934
+ function regions(a, W, H, b, minArea) {
2935
+ const x0 = b.x - 1;
2936
+ const y0 = b.y - 1;
2937
+ const w = b.width + 2;
2938
+ const h = b.height + 2;
2939
+ const on = (i, j) => {
2940
+ const x = x0 + i;
2941
+ const y = y0 + j;
2942
+ return x >= 0 && y >= 0 && x < W && y < H && a[y * W + x] >= 128;
2943
+ };
2944
+ const label = new Int32Array(w * h);
2945
+ const stack = new Int32Array(w * h);
2946
+ const found = [];
2947
+ const N4 = [[1, 0], [-1, 0], [0, 1], [0, -1]];
2948
+ const N8 = [...N4, [1, 1], [1, -1], [-1, 1], [-1, -1]];
2949
+ for (let start = 0; start < w * h; start++) {
2950
+ if (label[start]) continue;
2951
+ const si = start % w;
2952
+ const sj = (start - si) / w;
2953
+ const fg = on(si, sj);
2954
+ const id = found.length + 1;
2955
+ const r = { fg, area: 0, bx0: si, by0: sj, bx1: si, by1: sj, outer: false };
2956
+ let top = 0;
2957
+ stack[top++] = start;
2958
+ label[start] = id;
2959
+ while (top) {
2960
+ const k = stack[--top];
2961
+ const i = k % w;
2962
+ const j = (k - i) / w;
2963
+ r.area++;
2964
+ if (i < r.bx0) r.bx0 = i;
2965
+ if (i > r.bx1) r.bx1 = i;
2966
+ if (j < r.by0) r.by0 = j;
2967
+ if (j > r.by1) r.by1 = j;
2968
+ if (i === 0 || j === 0 || i === w - 1 || j === h - 1) r.outer = true;
2969
+ for (const [dx, dy] of fg ? N8 : N4) {
2970
+ const ni = i + dx;
2971
+ const nj = j + dy;
2972
+ if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
2973
+ const nk = nj * w + ni;
2974
+ if (label[nk] || on(ni, nj) !== fg) continue;
2975
+ label[nk] = id;
2976
+ stack[top++] = nk;
2977
+ }
2978
+ }
2979
+ found.push(r);
2980
+ }
2981
+ const box = (r) => ({ x: x0 + r.bx0, y: y0 + r.by0, width: r.bx1 - r.bx0 + 1, height: r.by1 - r.by0 + 1 });
2982
+ const summarize = (list) => {
2983
+ const kept = list.filter((r) => r.area >= minArea).sort((p, q) => q.area - p.area);
2984
+ return { count: kept.length, totalArea: kept.reduce((s, r) => s + r.area, 0), largest: kept.slice(0, 10).map((r) => ({ area: r.area, bounds: box(r) })) };
2985
+ };
2986
+ const pieces = found.filter((r) => r.fg).sort((p, q) => q.area - p.area);
2987
+ return {
2988
+ holes: summarize(found.filter((r) => !r.fg && !r.outer)),
2989
+ islands: summarize(pieces.slice(1))
2990
+ };
2991
+ }
2992
+
2569
2993
  // src/image-editor/selection/path.ts
2570
2994
  function selectionToSubpaths(mask, options = {}) {
2571
2995
  const b = mask.bounds;
@@ -2854,19 +3278,19 @@ function liveWire(map, from, to, band) {
2854
3278
  }
2855
3279
  }
2856
3280
  const scale = maxStrength > 0 ? 1 / maxStrength : 0;
2857
- const dist = new Float64Array(n).fill(Infinity);
3281
+ const dist2 = new Float64Array(n).fill(Infinity);
2858
3282
  const prev = new Int32Array(n).fill(-1);
2859
3283
  const done = new Uint8Array(n);
2860
3284
  const start = (ay - ry0) * w + (ax - rx0);
2861
3285
  const goal = (by - ry0) * w + (bx - rx0);
2862
- dist[start] = 0;
3286
+ dist2[start] = 0;
2863
3287
  const heap = new Heap();
2864
3288
  heap.push(start, 0);
2865
3289
  while (heap.size) {
2866
3290
  const p = heap.topId;
2867
3291
  const d = heap.topKey;
2868
3292
  heap.pop();
2869
- if (done[p] || d > dist[p]) continue;
3293
+ if (done[p] || d > dist2[p]) continue;
2870
3294
  done[p] = 1;
2871
3295
  if (p === goal) break;
2872
3296
  const x = p % w;
@@ -2887,8 +3311,8 @@ function liveWire(map, from, to, band) {
2887
3311
  const fZ = 1 - zero[q];
2888
3312
  const cost = (W_ZERO * fZ + W_GRADIENT * fG + W_DIRECTION * fD + 0.01) * step;
2889
3313
  const nd = d + cost;
2890
- if (nd < dist[q]) {
2891
- dist[q] = nd;
3314
+ if (nd < dist2[q]) {
3315
+ dist2[q] = nd;
2892
3316
  prev[q] = p;
2893
3317
  heap.push(q, nd);
2894
3318
  }
@@ -3604,8 +4028,8 @@ function colorRange(pixels, samples, options = {}) {
3604
4028
  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]));
3605
4029
  let k = Math.min(1, Math.max(0, 1 - d / f));
3606
4030
  if (k > 0 && radius !== void 0 && s.at) {
3607
- const dist = Math.hypot(x - s.at.x, y - s.at.y);
3608
- k *= Math.min(1, Math.max(0, 1 - dist / radius));
4031
+ const dist2 = Math.hypot(x - s.at.x, y - s.at.y);
4032
+ k *= Math.min(1, Math.max(0, 1 - dist2 / radius));
3609
4033
  }
3610
4034
  if (k > best) best = k;
3611
4035
  }
@@ -4966,15 +5390,15 @@ var PaintStrokeSession = class {
4966
5390
  return;
4967
5391
  }
4968
5392
  const stroke = this.stroke;
4969
- const regions = /* @__PURE__ */ new Map();
5393
+ const regions2 = /* @__PURE__ */ new Map();
4970
5394
  for (const r of rects) {
4971
5395
  for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {
4972
5396
  const key2 = tileKey(t.tx, t.ty);
4973
- const cur = regions.get(key2);
4974
- regions.set(key2, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r) : r });
5397
+ const cur = regions2.get(key2);
5398
+ regions2.set(key2, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r) : r });
4975
5399
  }
4976
5400
  }
4977
- for (const [key2, { tx, ty, r }] of regions) {
5401
+ for (const [key2, { tx, ty, r }] of regions2) {
4978
5402
  if (this.deep) {
4979
5403
  const rt = this.retouch;
4980
5404
  const paint = rt?.kind === "coverage" ? rt.paint() : null;
@@ -6405,6 +6829,85 @@ var ImageEditorController = class {
6405
6829
  this.commitLayers(next);
6406
6830
  return { parentId: loc.parent?.id ?? null, index: loc.index };
6407
6831
  }
6832
+ /** A layer's edge colour as percentages (see `setEdgeColor`). */
6833
+ getEdgeColor(id) {
6834
+ const layer = this.requireEdgeLayer(id);
6835
+ const e = layer.edge ?? {};
6836
+ const pct2 = (v) => Math.round((v ?? 0) * 1e3) / 10;
6837
+ const hex = (c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("");
6838
+ return { layerId: layer.id, decontaminate: pct2(e.decontaminate), despill: pct2(e.despill), ambient: pct2(e.ambient), ambientSource: e.ambientColor ? hex(e.ambientColor) : "auto" };
6839
+ }
6840
+ requireEdgeLayer(id) {
6841
+ const layer = this.getLayer(id);
6842
+ if (!layer) throw new LayerOperationError(`There is no layer "${id}" (see list_layers).`);
6843
+ if (layer.type === "group" || layer.type === "adjustment" || layer.type === "annotation")
6844
+ throw new LayerOperationError(`Edge colour works on pixel, shape, type and smart object layers; "${layer.name || id}" is a ${layer.type} layer.`);
6845
+ return layer;
6846
+ }
6847
+ /** Whether the layer's mask (painted and vector, with feather) has a soft band. */
6848
+ maskHasSoftBand(layer) {
6849
+ const { width, height } = this.naturalSize;
6850
+ const mask = effectiveLayerMask(layer, width, height);
6851
+ if (!mask || mask.enabled === false || mask.source) return false;
6852
+ const ox = layer.type === "pixel" ? layer.offset.x : 0;
6853
+ const oy = layer.type === "pixel" ? layer.offset.y : 0;
6854
+ return hasSoftBand(maskCoverageOverCanvas(mask, ox, oy, width, height));
6855
+ }
6856
+ /**
6857
+ * Edge colour on a layer's soft edge, applied when compositing (nothing is baked); amounts in
6858
+ * percent, 0 turns one off. `decontaminate` unmixes the old background (the pixels the mask
6859
+ * hides) from the edge and `despill` limits its hue there: both need a pixel layer whose mask
6860
+ * (painted or vector) has a soft band. `ambient` pulls the edge toward what is behind the layer
6861
+ * (`ambientColor` null: sampled live) or a fixed `#rrggbb`; it needs a soft mask or soft
6862
+ * pixels at the edge. Validates everything before changing anything; one undo step (none with
6863
+ * `commitToHistory` false, for the later moves of a slider drag). Returns the effective settings.
6864
+ */
6865
+ setEdgeColor(id, change, commitToHistory = true) {
6866
+ const layer = this.requireEdgeLayer(id);
6867
+ const name = layer.name || id;
6868
+ const next = { ...layer.edge ?? {} };
6869
+ for (const k of ["decontaminate", "despill", "ambient"]) {
6870
+ const v = change[k];
6871
+ if (v === void 0) continue;
6872
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 100) throw new LayerOperationError(`${k} must be a percentage from 0 to 100, got ${String(v)}.`);
6873
+ if (v > 0) next[k] = v / 100;
6874
+ else delete next[k];
6875
+ }
6876
+ if (change.ambientColor !== void 0) {
6877
+ if (change.ambientColor === null || change.ambientColor === "auto") delete next.ambientColor;
6878
+ else {
6879
+ const m = /^#([0-9a-f]{6})$/i.exec(String(change.ambientColor));
6880
+ if (!m) throw new LayerOperationError(`ambient colour must be "auto" or #rrggbb, got "${String(change.ambientColor)}".`);
6881
+ const n = parseInt(m[1], 16);
6882
+ next.ambientColor = [n >> 16 & 255, n >> 8 & 255, n & 255];
6883
+ }
6884
+ }
6885
+ const unmix = (next.decontaminate ?? 0) > 0 || (next.despill ?? 0) > 0;
6886
+ const turningOn = (k) => (change[k] ?? 0) > 0;
6887
+ if (unmix && (turningOn("decontaminate") || turningOn("despill"))) {
6888
+ if (layer.type !== "pixel") throw new LayerOperationError(`Decontamination and despill need a pixel layer with a soft-edged mask; "${name}" is a ${layer.type} layer (rasterize it, or use ambient).`);
6889
+ if (!this.maskHasSoftBand(layer))
6890
+ throw new LayerOperationError(`"${name}" has no mask with a soft edge: decontamination and despill work in the partly hidden band of a layer mask. Add one with add_layer_mask from a refined selection (refine_selection), or feather the mask (set_layer_mask_properties feather).`);
6891
+ }
6892
+ if (turningOn("ambient") && !this.maskHasSoftBand(layer) && layer.type === "pixel") {
6893
+ const px2 = this.getLayerPixels(layer.id);
6894
+ let soft = false;
6895
+ if (px2) for (let i = 3; i < px2.data.length && !soft; i += 4) soft = px2.data[i] > 5 && px2.data[i] < 250;
6896
+ if (!soft) throw new LayerOperationError(`"${name}" has no soft edge (no soft-edged mask and no partly transparent pixels) for the edge cast to work on.`);
6897
+ }
6898
+ const edge = next.decontaminate || next.despill || next.ambient || next.ambientColor ? next : void 0;
6899
+ this.commitLayers(
6900
+ mapLayer(this.state.layers, layer.id, (l) => {
6901
+ const out = { ...l };
6902
+ if (edge) out.edge = edge;
6903
+ else delete out.edge;
6904
+ return out;
6905
+ }),
6906
+ void 0,
6907
+ commitToHistory
6908
+ );
6909
+ return this.getEdgeColor(layer.id);
6910
+ }
6408
6911
  /**
6409
6912
  * Changes name, visibility, opacity, fill, blend, clipping, locks or (groups) collapsed state, as
6410
6913
  * one undo step (`commitToHistory = false` for the moves of a slider drag). Returns the layer.
@@ -9668,6 +10171,44 @@ var ImageEditorController = class {
9668
10171
  if (!sel) return false;
9669
10172
  return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
9670
10173
  }
10174
+ /**
10175
+ * A measured report on a mask's edge (`inspect_mask_edge`): the selection (default), a layer's
10176
+ * painted mask or its vector mask (its path, without density or feather). Halo and alignment are
10177
+ * measured against the layer's own pixels for a pixel layer's masks, else the composite.
10178
+ * Read-only. Throws for an empty selection or a layer without that mask.
10179
+ */
10180
+ inspectMaskEdge(target = "selection", options = {}) {
10181
+ const { width, height } = this.naturalSize;
10182
+ let mask;
10183
+ let layer;
10184
+ if (target === "selection") {
10185
+ mask = this.getSelection();
10186
+ if (!mask?.bounds) throw new LayerOperationError("Nothing is selected: make a selection first, or inspect a layer-mask or vector-mask with layerId.");
10187
+ } else if (target === "layer-mask" || target === "vector-mask") {
10188
+ if (!options.layerId) throw new LayerOperationError(`A ${target} needs layerId (see list_layers).`);
10189
+ layer = this.getLayer(options.layerId);
10190
+ if (!layer) throw new LayerOperationError(`There is no layer "${options.layerId}" (see list_layers).`);
10191
+ if (target === "layer-mask") {
10192
+ mask = this.layerMaskCoverage(layer.id);
10193
+ if (!mask) throw new LayerOperationError(`Layer "${layer.name || layer.id}" has no painted mask; add one with add_layer_mask, or inspect its vector-mask.`);
10194
+ } else {
10195
+ const { vm } = this.requireVectorMask(layer.id);
10196
+ mask = pathToSelection(vm.components, width, height, { fillRule: vm.fillRule });
10197
+ if (vm.inverted) mask = invertSelection(mask);
10198
+ }
10199
+ if (!mask.bounds) throw new LayerOperationError(`The ${target} of "${layer.name || layer.id}" hides everything: there is no edge to inspect.`);
10200
+ } else {
10201
+ throw new LayerOperationError(`Unknown target "${String(target)}". Use selection, layer-mask or vector-mask.`);
10202
+ }
10203
+ const b = mask.bounds;
10204
+ const x = Math.max(0, b.x - 8);
10205
+ const y = Math.max(0, b.y - 8);
10206
+ const region = { x, y, width: Math.min(width, b.x + b.width + 8) - x, height: Math.min(height, b.y + b.height + 8) - y };
10207
+ const own = layer?.type === "pixel" ? this.getLayerPixels(layer.id) : null;
10208
+ const image = own ?? { ...this.compositePixels(region), x: region.x, y: region.y };
10209
+ const report = inspectMaskEdge(mask, image, options);
10210
+ return { target, ...layer ? { layerId: layer.id } : {}, ...report };
10211
+ }
9671
10212
  /** A layer's painted mask over the canvas as a selection mask; null without one (or for a depth mask). */
9672
10213
  layerMaskCoverage(layerId) {
9673
10214
  const layer = this.getLayer(layerId);
@@ -10717,7 +11258,7 @@ var ImageEditorController = class {
10717
11258
  }
10718
11259
  if (src.colorSpace !== "rgb") throw new LayerOperationError("Colours are given as #rrggbb, so the source profile must be RGB.");
10719
11260
  const t = Transform.create(src, dst, { intent: options.intent ?? "relative", bpc: options.bpc ?? true });
10720
- const toLab = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
11261
+ const toLab2 = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
10721
11262
  return colors.map((input) => {
10722
11263
  const m = /^#?([0-9a-f]{6})$/i.exec(input.trim());
10723
11264
  if (!m) throw new LayerOperationError(`Invalid colour "${input}"; use #rrggbb.`);
@@ -10727,7 +11268,7 @@ var ImageEditorController = class {
10727
11268
  t.eval(rgb, out);
10728
11269
  const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4);
10729
11270
  const l = new Float32Array(3);
10730
- toLab.eval(rgb, l);
11271
+ toLab2.eval(rgb, l);
10731
11272
  const lab = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
10732
11273
  const res = { input, values, lab };
10733
11274
  if (dst.colorSpace === "rgb") res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("")}`;
@@ -10917,7 +11458,9 @@ var ImageEditorController = class {
10917
11458
  return baked;
10918
11459
  };
10919
11460
  const prepare = async (layer) => {
11461
+ if (layer.edge?.ambient) add2("The live edge cast (set_edge_ambient) has no PSD equivalent and was left out", layer.name);
10920
11462
  const p = await prepareLayer(layer);
11463
+ if (p?.edge) delete p.edge;
10921
11464
  if (p?.blend === "pigment") add2("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
10922
11465
  return p;
10923
11466
  };
@@ -10993,6 +11536,14 @@ var ImageEditorController = class {
10993
11536
  }
10994
11537
  if (layer.filters?.some((f) => !f.enabled)) add2("Disabled layer filters were left out", layer.name);
10995
11538
  delete out.filters;
11539
+ const unmixMask = hasEdgeUnmix(layer.edge) ? effectiveLayerMask(layer, width, height) : void 0;
11540
+ if (unmixMask && !unmixMask.source && unmixMask.enabled !== false) {
11541
+ add2("Edge decontamination and despill were baked into the pixels", layer.name);
11542
+ const { x: ox, y: oy } = layer.offset;
11543
+ const data = readRegion(out.surface, { x: -ox, y: -oy, width, height });
11544
+ applyEdgeUnmix(data, maskCoverageOverCanvas(unmixMask, ox, oy, width, height), width, height, layer.edge);
11545
+ out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -ox, -oy) };
11546
+ }
10996
11547
  const mask = bakeMask(layer);
10997
11548
  if (mask) out.mask = mask;
10998
11549
  else delete out.mask;
@@ -12399,6 +12950,26 @@ function createImageSelectionTools(controller) {
12399
12950
  annotations: { title: "Read the selection", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12400
12951
  handler: () => summary(controller)
12401
12952
  };
12953
+ const inspectEdge = {
12954
+ name: "inspect_mask_edge",
12955
+ title: "Inspect a mask edge",
12956
+ description: "Measures the edge of the selection (default), a layer's painted mask (layer-mask) or its vector mask (vector-mask), so you can check it without looking. Returns haloScore 0-1 (background colour left just inside the edge; worse above 0.35), edgeAlignment 0-1 (share of the edge on strong image edges; worse below 0.5), jaggedness (outline turning in radians per 100 px beyond what closing it needs; smooth outlines score under 3, worse above 15), softFraction 0-1 (share of edge pixels partly selected), holes and islands (count, total area, the 10 largest with bounds), worst (up to 5 regions with a reason, ready for capture_view rect) and issues (findings with a suggested fix; empty when clean). Measures a pixel layer's masks against its own pixels, otherwise the composite. Read-only: inspect, fix the issues, inspect again.",
12957
+ inputSchema: {
12958
+ type: "object",
12959
+ properties: {
12960
+ target: { type: "string", enum: ["selection", "layer-mask", "vector-mask"], description: "What to inspect (default selection). layer-mask and vector-mask need layerId." },
12961
+ layerId: { type: "string", description: "The layer whose mask to inspect (see list_layers); only with layer-mask or vector-mask." },
12962
+ minArea: { type: "number", minimum: 1, maximum: 1e6, description: "Holes and islands smaller than this many pixels are left out (default 16)." }
12963
+ },
12964
+ additionalProperties: false
12965
+ },
12966
+ annotations: { title: "Inspect a mask edge", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
12967
+ handler: ({ target, layerId, minArea }) => {
12968
+ const t = target ?? "selection";
12969
+ if (t === "selection" && layerId !== void 0) throw new Error("layerId is only for target layer-mask or vector-mask; leave it out to inspect the selection.");
12970
+ return controller.inspectMaskEdge(t, { layerId, minArea });
12971
+ }
12972
+ };
12402
12973
  const selectRegion = {
12403
12974
  name: "select_region",
12404
12975
  title: "Select a region",
@@ -13444,6 +14015,7 @@ function createImageSelectionTools(controller) {
13444
14015
  };
13445
14016
  return [
13446
14017
  getSelection,
14018
+ inspectEdge,
13447
14019
  selectAll,
13448
14020
  deselect,
13449
14021
  invert2,
@@ -14333,6 +14905,49 @@ function createImageLayerTools(controller) {
14333
14905
  return { view: shown?.mode ?? "off", layerId: shown?.layerId ?? null };
14334
14906
  }
14335
14907
  };
14908
+ const AMOUNT = (what) => ({ type: "number", minimum: 0, maximum: 100, description: `${what}, in percent 0-100; 0 turns it off.` });
14909
+ const EDGE_RETURNS = "Returns the layer's effective edge colour: { layerId, decontaminate, despill, ambient (percent), ambientSource }.";
14910
+ const getEdge = {
14911
+ name: "get_edge_color",
14912
+ title: "Read edge colour",
14913
+ description: `A layer's edge colour settings, applied when compositing: decontaminate, despill and ambient (percent) and ambientSource (auto or #rrggbb). ${EDGE_RETURNS}`,
14914
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, required: ["layerId"], additionalProperties: false },
14915
+ annotations: { title: "Read edge colour", ...READ2 },
14916
+ handler: ({ layerId }) => controller.getEdgeColor(String(layerId))
14917
+ };
14918
+ const setDecontaminate = {
14919
+ name: "set_edge_decontamination",
14920
+ title: "Decontaminate edge colour",
14921
+ description: `Removes the old background's colour from a cutout's soft edge at composite time (nothing is baked; change or turn it off any time): each partly hidden edge pixel is unmixed from the background next to it (the pixels the layer's mask hides). Needs a pixel layer whose mask (painted or vector) has a soft band, e.g. add_layer_mask from a refined selection. Check with inspect_mask_edge (haloScore) and capture_view. One undo step. ${EDGE_RETURNS}`,
14922
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID2, amount: AMOUNT("How much to replace edge colour with the subject's own (80-100 typical)") }, required: ["layerId", "amount"], additionalProperties: false },
14923
+ annotations: { title: "Decontaminate edge colour", ...WRITE_IDEMPOTENT2 },
14924
+ handler: ({ layerId, amount }) => controller.setEdgeColor(String(layerId), { decontaminate: amount })
14925
+ };
14926
+ const setDespill = {
14927
+ name: "set_edge_despill",
14928
+ title: "Despill edge colour",
14929
+ description: `Limits how much of the old background's hue a cutout's soft edge carries (a green-screen or coloured-wall fringe), at composite time: the edge colour's component toward the background's hue is taken out, luminance kept, most at the outer edge. A heuristic: it does not remove light that bounced onto the subject itself. Needs a pixel layer whose mask has a soft band. Pixels fully inside the mask never change. One undo step. ${EDGE_RETURNS}`,
14930
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID2, amount: AMOUNT("How much of the background hue to take out (50-100 typical)") }, required: ["layerId", "amount"], additionalProperties: false },
14931
+ annotations: { title: "Despill edge colour", ...WRITE_IDEMPOTENT2 },
14932
+ handler: ({ layerId, amount }) => controller.setEdgeColor(String(layerId), { despill: amount })
14933
+ };
14934
+ const setAmbient = {
14935
+ name: "set_edge_ambient",
14936
+ title: "Cast the scene onto an edge",
14937
+ description: `Tints a cutout's soft edge with what is behind it now, live at composite time, so a subject moved onto a new background picks up its colour at the edge (more where the edge is more transparent). source auto samples the layers below as they change; #rrggbb uses a fixed colour. Works on soft-edged masks or partly transparent pixels (pixel, shape, type and smart object layers). One undo step. ${EDGE_RETURNS}`,
14938
+ inputSchema: {
14939
+ type: "object",
14940
+ properties: {
14941
+ layerId: LAYER_ID2,
14942
+ amount: AMOUNT("How far edge colour moves toward the background (20-50 typical)"),
14943
+ source: { type: "string", pattern: "^(auto|#[0-9a-fA-F]{6})$", description: "auto (default): what is behind the layer, sampled live; or a fixed #rrggbb." }
14944
+ },
14945
+ required: ["layerId", "amount"],
14946
+ additionalProperties: false
14947
+ },
14948
+ annotations: { title: "Cast the scene onto an edge", ...WRITE_IDEMPOTENT2 },
14949
+ handler: ({ layerId, amount, source }) => controller.setEdgeColor(String(layerId), { ambient: amount, ambientColor: source ?? "auto" })
14950
+ };
14336
14951
  const maskProps = {
14337
14952
  name: "set_layer_mask_properties",
14338
14953
  title: "Set layer mask properties",
@@ -14475,6 +15090,10 @@ function createImageLayerTools(controller) {
14475
15090
  removeMask,
14476
15091
  maskView,
14477
15092
  maskProps,
15093
+ getEdge,
15094
+ setDecontaminate,
15095
+ setDespill,
15096
+ setAmbient,
14478
15097
  viaCopy,
14479
15098
  rasterize,
14480
15099
  addFilter,
@@ -18677,10 +19296,10 @@ function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
18677
19296
  if (x1 <= x0 || y1 <= y0) return null;
18678
19297
  return { x0, y0, x1, y1 };
18679
19298
  }
18680
- function applyRegionMosaic(imageData, regions, blockSize = 12) {
19299
+ function applyRegionMosaic(imageData, regions2, blockSize = 12) {
18681
19300
  const { data, width, height } = imageData;
18682
19301
  const step = Math.max(2, Math.round(blockSize));
18683
- for (const region of regions) {
19302
+ for (const region of regions2) {
18684
19303
  const bounds = resolveMosaicRegionBounds(region, width, height);
18685
19304
  if (!bounds) continue;
18686
19305
  const { x0, y0, x1, y1 } = bounds;
@@ -18722,10 +19341,10 @@ function applyRegionMosaic(imageData, regions, blockSize = 12) {
18722
19341
  }
18723
19342
  return imageData;
18724
19343
  }
18725
- function applyRegionBlur(imageData, regions, radius = 10) {
19344
+ function applyRegionBlur(imageData, regions2, radius = 10) {
18726
19345
  const { data, width, height } = imageData;
18727
19346
  const rad = Math.max(1, Math.min(64, Math.round(radius)));
18728
- for (const region of regions) {
19347
+ for (const region of regions2) {
18729
19348
  const bounds = resolveMosaicRegionBounds(region, width, height);
18730
19349
  if (!bounds) continue;
18731
19350
  const { x0, y0, x1, y1 } = bounds;
@@ -21102,6 +21721,7 @@ export {
21102
21721
  DIRECT_GAUSSIAN_MAX_SIGMA,
21103
21722
  DRY_EPS,
21104
21723
  DabWalker,
21724
+ EDGE_COLOR_RADIUS,
21105
21725
  EFFECT_LAYER_USES,
21106
21726
  EMPTY_DISTANCE,
21107
21727
  EMPTY_MIXER_LOAD,
@@ -21159,6 +21779,7 @@ export {
21159
21779
  LayerEffectError,
21160
21780
  LayerOperationError,
21161
21781
  MARCHING_ANTS_STEP_MS,
21782
+ MASK_EDGE_THRESHOLDS,
21162
21783
  MASK_FILE_ACCEPT,
21163
21784
  MAX_BATCH,
21164
21785
  MAX_BRUSH_SIZE,
@@ -21343,6 +21964,7 @@ export {
21343
21964
  applyBoxBlur,
21344
21965
  applyBoxBlurFloat,
21345
21966
  applyDepthMask,
21967
+ applyEdgeUnmix,
21346
21968
  applyHomography,
21347
21969
  applyImageProjectionToContext,
21348
21970
  applyLegacyAdjustments,
@@ -21518,6 +22140,7 @@ export {
21518
22140
  deserializeMask,
21519
22141
  deserializePatterns,
21520
22142
  deserializeSelection,
22143
+ despillEdge,
21521
22144
  developFilter,
21522
22145
  developFromAdjustments,
21523
22146
  developTarget,
@@ -21613,8 +22236,10 @@ export {
21613
22236
  handleQuickMaskKey,
21614
22237
  handleSelectionShortcut,
21615
22238
  hasActiveEffects,
22239
+ hasEdgeUnmix,
21616
22240
  hasLayersBelow,
21617
22241
  hasResampledLegacyMask,
22242
+ hasSoftBand,
21618
22243
  hasSurface,
21619
22244
  hasWarp,
21620
22245
  hash2,
@@ -21653,6 +22278,7 @@ export {
21653
22278
  inflateRaw,
21654
22279
  insertCurvePoint,
21655
22280
  insertLayer,
22281
+ inspectMaskEdge,
21656
22282
  instancesOf,
21657
22283
  invertHomography,
21658
22284
  invertMatrix3,
@@ -21743,6 +22369,7 @@ export {
21743
22369
  mapLayer,
21744
22370
  mapSubpaths,
21745
22371
  maskBounds,
22372
+ maskCoverageOverCanvas,
21746
22373
  maskFileName,
21747
22374
  maskNameFromFile,
21748
22375
  maskSummary,
@@ -21885,6 +22512,7 @@ export {
21885
22512
  rasterizeSmartObject,
21886
22513
  readAdjustmentControls,
21887
22514
  readDeepRegion,
22515
+ readEdgeColor,
21888
22516
  readPaintSource,
21889
22517
  readPaths,
21890
22518
  readPixel,