@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.
- package/dist/{chunk-UOGKSVRQ.cjs → chunk-B46UQUDC.cjs} +770 -733
- package/dist/{chunk-W3TOZTL7.js → chunk-F7Z2MKY2.js} +115 -4
- package/dist/{chunk-YBJXCQTR.js → chunk-QXY4YTRI.js} +738 -701
- package/dist/{chunk-H65IBXFI.cjs → chunk-S55JFWDA.cjs} +350 -239
- package/dist/{chunk-QYLWG22N.cjs → chunk-SS53NNZO.cjs} +304 -1
- package/dist/{chunk-LQ5SALHK.js → chunk-U7C655AM.js} +5 -311
- package/dist/{chunk-L52VMQZS.cjs → chunk-UC3PWFM3.cjs} +68 -374
- package/dist/{chunk-L3G23N5Y.js → chunk-ZNA3IQZF.js} +304 -1
- package/dist/image-editor/controller.d.cts +44 -0
- package/dist/image-editor/controller.d.ts +44 -0
- package/dist/image-editor/index.cjs +1768 -1140
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +660 -32
- package/dist/image-editor/layers/edge-color.d.cts +27 -0
- package/dist/image-editor/layers/edge-color.d.ts +27 -0
- package/dist/image-editor/layers/serialize.d.cts +13 -0
- package/dist/image-editor/layers/serialize.d.ts +13 -0
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/inspect.d.cts +70 -0
- package/dist/image-editor/selection/inspect.d.ts +70 -0
- package/dist/image-editor/selection-worker.cjs +4 -4
- package/dist/image-editor/selection-worker.js +2 -2
- package/dist/paint-editor/index.cjs +79 -79
- package/dist/paint-editor/index.js +3 -3
- package/dist/raster/compositor.d.cts +10 -1
- package/dist/raster/compositor.d.ts +10 -1
- package/dist/raster/deep-compositor.d.cts +1 -1
- package/dist/raster/deep-compositor.d.ts +1 -1
- package/dist/raster/index.cjs +4 -2
- package/dist/raster/index.js +3 -1
- package/dist/raster/layers.d.cts +16 -0
- package/dist/raster/layers.d.ts +16 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +9 -6
- package/skills/pixerate-image-editor/references/paths.md +12 -0
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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 >
|
|
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 <
|
|
2891
|
-
|
|
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
|
|
3608
|
-
k *= Math.min(1, Math.max(0, 1 -
|
|
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
|
|
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 =
|
|
4974
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|