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