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