@pixerate/editor 0.219.2 → 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/{chunk-S55JFWDA.cjs → chunk-R35YOUP2.cjs} +407 -161
- package/dist/{chunk-F7Z2MKY2.js → chunk-S6KKLFZO.js} +261 -15
- package/dist/image-editor/controller.d.cts +52 -5
- package/dist/image-editor/controller.d.ts +52 -5
- package/dist/image-editor/index.cjs +3065 -2536
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +1528 -999
- package/dist/image-editor/layers/hybrid-mask.d.cts +31 -0
- package/dist/image-editor/layers/hybrid-mask.d.ts +31 -0
- package/dist/image-editor/layers/model.d.cts +17 -0
- package/dist/image-editor/layers/model.d.ts +17 -0
- package/dist/image-editor/layers/serialize.d.cts +4 -0
- package/dist/image-editor/layers/serialize.d.ts +4 -0
- package/dist/image-editor/layers/vector-mask.d.cts +10 -5
- package/dist/image-editor/layers/vector-mask.d.ts +10 -5
- package/dist/image-editor/path-edit.d.cts +4 -0
- package/dist/image-editor/path-edit.d.ts +4 -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/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- 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
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
TINT_SCALE,
|
|
92
92
|
TONE_LUT_SIZE,
|
|
93
93
|
TRANSFORM_ANCHORS,
|
|
94
|
+
VECTOR_MASK_EDGE_DEFAULTS,
|
|
94
95
|
VIBRANCE_GREY,
|
|
95
96
|
XYZ_D50_TO_SRGB,
|
|
96
97
|
activeAdjustmentLayers,
|
|
@@ -201,6 +202,7 @@ import {
|
|
|
201
202
|
hueRangeTables,
|
|
202
203
|
hueRangeWeight,
|
|
203
204
|
hueSaturation,
|
|
205
|
+
hybridVectorMaskCoverage,
|
|
204
206
|
identityCurve,
|
|
205
207
|
identityLevels,
|
|
206
208
|
identityTransform,
|
|
@@ -224,6 +226,7 @@ import {
|
|
|
224
226
|
isValidWarp,
|
|
225
227
|
kelvinToTemperature,
|
|
226
228
|
layerEffectsOf,
|
|
229
|
+
layerGuide,
|
|
227
230
|
layerMaskCoverage,
|
|
228
231
|
layerMaskFromSelection,
|
|
229
232
|
layerMaskKey,
|
|
@@ -383,8 +386,10 @@ import {
|
|
|
383
386
|
updateTypeLayer,
|
|
384
387
|
userErrorMessage,
|
|
385
388
|
uvToXy,
|
|
389
|
+
validVectorMaskEdges,
|
|
386
390
|
vectorMaskBounds,
|
|
387
391
|
vectorMaskCoverage,
|
|
392
|
+
vectorMaskSegments,
|
|
388
393
|
vectorMaskThumbnail,
|
|
389
394
|
versionedDescriptorBytes,
|
|
390
395
|
vibranceLinear,
|
|
@@ -400,7 +405,7 @@ import {
|
|
|
400
405
|
writePsd,
|
|
401
406
|
xyToTempTint,
|
|
402
407
|
xyToUv
|
|
403
|
-
} from "../chunk-
|
|
408
|
+
} from "../chunk-S6KKLFZO.js";
|
|
404
409
|
import {
|
|
405
410
|
parseCssColor
|
|
406
411
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -1333,10 +1338,10 @@ function emptyHistogram() {
|
|
|
1333
1338
|
max: 0
|
|
1334
1339
|
};
|
|
1335
1340
|
}
|
|
1336
|
-
function addToHistogram(h, data, stride,
|
|
1337
|
-
for (let y =
|
|
1338
|
-
let i = (y * stride +
|
|
1339
|
-
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) {
|
|
1340
1345
|
if (!(data[i + 3] > 0)) {
|
|
1341
1346
|
h.transparent++;
|
|
1342
1347
|
continue;
|
|
@@ -1822,10 +1827,22 @@ var ImageEditorRenderer = class {
|
|
|
1822
1827
|
else ctx.lineTo(b.x, b.y);
|
|
1823
1828
|
};
|
|
1824
1829
|
ctx.beginPath();
|
|
1825
|
-
e.curves.forEach(trace2);
|
|
1830
|
+
e.curves.forEach((c, i) => (e.curveModes?.[i] ?? "hard") === "hard" && trace2(c));
|
|
1826
1831
|
ctx.lineWidth = 1.5 * u;
|
|
1827
1832
|
ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
|
|
1828
1833
|
ctx.stroke();
|
|
1834
|
+
for (const [mode, color, width, dash] of [["feather", "#f59e0b", 2, [5, 3]], ["matte", "#d946ef", 3, []]]) {
|
|
1835
|
+
if (!e.curveModes?.includes(mode)) continue;
|
|
1836
|
+
ctx.beginPath();
|
|
1837
|
+
e.curves.forEach((c, i) => e.curveModes[i] === mode && trace2(c));
|
|
1838
|
+
ctx.lineWidth = width * u;
|
|
1839
|
+
ctx.strokeStyle = color;
|
|
1840
|
+
ctx.setLineDash(dash.map((d) => d * u));
|
|
1841
|
+
ctx.stroke();
|
|
1842
|
+
ctx.setLineDash([]);
|
|
1843
|
+
}
|
|
1844
|
+
ctx.lineWidth = 1.5 * u;
|
|
1845
|
+
ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
|
|
1829
1846
|
if (e.preview) {
|
|
1830
1847
|
ctx.beginPath();
|
|
1831
1848
|
trace2(e.preview);
|
|
@@ -1970,7 +1987,7 @@ var ImageEditorRenderer = class {
|
|
|
1970
1987
|
if (!source.enabled || !source.maskSource) return null;
|
|
1971
1988
|
return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;
|
|
1972
1989
|
},
|
|
1973
|
-
rasterizeAnnotation: (annotation,
|
|
1990
|
+
rasterizeAnnotation: (annotation, rect3) => this.rasterizeAnnotation(annotation, rect3),
|
|
1974
1991
|
version: `${host?.version() ?? ""}:${this.assetVersion}`,
|
|
1975
1992
|
globalLight: host?.globalLight?.(),
|
|
1976
1993
|
pattern: host?.pattern ? (id) => host.pattern(id) : void 0
|
|
@@ -2011,34 +2028,34 @@ var ImageEditorRenderer = class {
|
|
|
2011
2028
|
return Boolean(ctx && typeof ctx.getImageData === "function");
|
|
2012
2029
|
}
|
|
2013
2030
|
/** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */
|
|
2014
|
-
rasterizeAnnotation(annotation,
|
|
2015
|
-
if (!this.scratch) this.scratch = this.createCanvas(
|
|
2031
|
+
rasterizeAnnotation(annotation, rect3) {
|
|
2032
|
+
if (!this.scratch) this.scratch = this.createCanvas(rect3.width, rect3.height);
|
|
2016
2033
|
const canvas = this.scratch;
|
|
2017
|
-
if (canvas.width !==
|
|
2018
|
-
canvas.width =
|
|
2019
|
-
canvas.height =
|
|
2034
|
+
if (canvas.width !== rect3.width || canvas.height !== rect3.height) {
|
|
2035
|
+
canvas.width = rect3.width;
|
|
2036
|
+
canvas.height = rect3.height;
|
|
2020
2037
|
}
|
|
2021
2038
|
const ctx = canvas.getContext("2d");
|
|
2022
2039
|
if (!ctx || typeof ctx.getImageData !== "function") return null;
|
|
2023
2040
|
try {
|
|
2024
2041
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
2025
|
-
ctx.clearRect(0, 0,
|
|
2026
|
-
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);
|
|
2027
2044
|
const img = annotation.type === "image" ? this.imageCache.peek(annotation.src) ?? null : null;
|
|
2028
2045
|
this.drawAnnotationShape(ctx, annotation, img);
|
|
2029
2046
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
2030
|
-
return new Uint8ClampedArray(ctx.getImageData(0, 0,
|
|
2047
|
+
return new Uint8ClampedArray(ctx.getImageData(0, 0, rect3.width, rect3.height).data);
|
|
2031
2048
|
} catch {
|
|
2032
2049
|
return null;
|
|
2033
2050
|
}
|
|
2034
2051
|
}
|
|
2035
|
-
putTile(ctx, data, tx, ty,
|
|
2036
|
-
const ox = tx * TILE -
|
|
2037
|
-
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;
|
|
2038
2055
|
const x0 = Math.max(0, -ox);
|
|
2039
2056
|
const y0 = Math.max(0, -oy);
|
|
2040
|
-
const x1 = Math.min(TILE,
|
|
2041
|
-
const y1 = Math.min(TILE,
|
|
2057
|
+
const x1 = Math.min(TILE, rect3.width - ox);
|
|
2058
|
+
const y1 = Math.min(TILE, rect3.height - oy);
|
|
2042
2059
|
if (x1 <= x0 || y1 <= y0) return;
|
|
2043
2060
|
const img = ctx.createImageData(TILE, TILE);
|
|
2044
2061
|
img.data.set(data);
|
|
@@ -2050,23 +2067,23 @@ var ImageEditorRenderer = class {
|
|
|
2050
2067
|
* uploading only the tiles that changed since the last render.
|
|
2051
2068
|
*/
|
|
2052
2069
|
updateComposite(layers, region, width, height, options = {}) {
|
|
2053
|
-
const
|
|
2070
|
+
const rect3 = coveringRect(region);
|
|
2054
2071
|
let comp = this.comp;
|
|
2055
2072
|
let fresh = false;
|
|
2056
|
-
if (!comp || comp.rect.x !==
|
|
2057
|
-
const canvas = this.createCanvas(
|
|
2058
|
-
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);
|
|
2059
2076
|
fresh = true;
|
|
2060
2077
|
}
|
|
2061
2078
|
const ctx = comp.canvas.getContext("2d");
|
|
2062
2079
|
if (!ctx || typeof ctx.createImageData !== "function") return null;
|
|
2063
2080
|
let changed = false;
|
|
2064
|
-
this.compositor.compositeTiles(layers,
|
|
2081
|
+
this.compositor.compositeTiles(layers, rect3, this.compositorSources(width, height), (tx, ty, data) => {
|
|
2065
2082
|
const key2 = tileKey(tx, ty);
|
|
2066
2083
|
if (comp.tiles.get(key2) === data) return;
|
|
2067
2084
|
comp.tiles.set(key2, data);
|
|
2068
2085
|
comp.histograms.delete(key2);
|
|
2069
|
-
this.putTile(ctx, data, tx, ty,
|
|
2086
|
+
this.putTile(ctx, data, tx, ty, rect3);
|
|
2070
2087
|
changed = true;
|
|
2071
2088
|
}, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? void 0 : options.dirty });
|
|
2072
2089
|
if (changed) comp.version++;
|
|
@@ -2346,15 +2363,15 @@ var ImageEditorRenderer = class {
|
|
|
2346
2363
|
break;
|
|
2347
2364
|
}
|
|
2348
2365
|
case "rect": {
|
|
2349
|
-
const
|
|
2350
|
-
ctx.strokeStyle =
|
|
2351
|
-
ctx.lineWidth =
|
|
2352
|
-
if (
|
|
2353
|
-
ctx.fillStyle =
|
|
2354
|
-
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);
|
|
2355
2372
|
}
|
|
2356
|
-
if (
|
|
2357
|
-
ctx.strokeRect(
|
|
2373
|
+
if (rect3.strokeWidth > 0) {
|
|
2374
|
+
ctx.strokeRect(rect3.x, rect3.y, rect3.width, rect3.height);
|
|
2358
2375
|
}
|
|
2359
2376
|
break;
|
|
2360
2377
|
}
|
|
@@ -2545,12 +2562,12 @@ var ImageEditorRenderer = class {
|
|
|
2545
2562
|
const split = splitOverlayAnnotations(layers);
|
|
2546
2563
|
overlay = split.overlay.map((l) => l.annotation);
|
|
2547
2564
|
await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);
|
|
2548
|
-
const
|
|
2549
|
-
const comp = this.createCanvas(
|
|
2565
|
+
const rect3 = coveringRect(region);
|
|
2566
|
+
const comp = this.createCanvas(rect3.width, rect3.height);
|
|
2550
2567
|
const cctx = comp.getContext("2d");
|
|
2551
2568
|
if (cctx && typeof cctx.createImageData === "function") {
|
|
2552
|
-
this.exportCompositor.compositeTiles(split.composite,
|
|
2553
|
-
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);
|
|
2554
2571
|
});
|
|
2555
2572
|
}
|
|
2556
2573
|
baseCanvas = this.createCanvas(baseW, baseH);
|
|
@@ -2561,7 +2578,7 @@ var ImageEditorRenderer = class {
|
|
|
2561
2578
|
bctx.beginPath();
|
|
2562
2579
|
bctx.rect(region.x, region.y, region.width, region.height);
|
|
2563
2580
|
bctx.clip();
|
|
2564
|
-
bctx.drawImage(comp,
|
|
2581
|
+
bctx.drawImage(comp, rect3.x, rect3.y);
|
|
2565
2582
|
bctx.restore();
|
|
2566
2583
|
this.applyPost(bctx, baseW, baseH, state);
|
|
2567
2584
|
}
|
|
@@ -2689,10 +2706,10 @@ function getBaseProjection(region, transform, baseW, baseH) {
|
|
|
2689
2706
|
};
|
|
2690
2707
|
}
|
|
2691
2708
|
var CompositeCanvas = class {
|
|
2692
|
-
constructor(canvas,
|
|
2709
|
+
constructor(canvas, rect3) {
|
|
2693
2710
|
this.canvas = canvas;
|
|
2694
|
-
this.rect =
|
|
2695
|
-
this.region =
|
|
2711
|
+
this.rect = rect3;
|
|
2712
|
+
this.region = rect3;
|
|
2696
2713
|
}
|
|
2697
2714
|
canvas;
|
|
2698
2715
|
rect;
|
|
@@ -2985,7 +3002,7 @@ function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
|
|
|
2985
3002
|
};
|
|
2986
3003
|
const on = (i, j) => f(i, j) >= 128;
|
|
2987
3004
|
const V = w * h;
|
|
2988
|
-
const
|
|
3005
|
+
const point2 = (id) => {
|
|
2989
3006
|
const vertical = id >= V;
|
|
2990
3007
|
const k = vertical ? id - V : id;
|
|
2991
3008
|
const i = k % w;
|
|
@@ -3070,7 +3087,7 @@ function traceLoops(a, W, H, rx0, ry0, rx1, ry1) {
|
|
|
3070
3087
|
let from = segs[s * 2];
|
|
3071
3088
|
while (!used[s]) {
|
|
3072
3089
|
used[s] = 1;
|
|
3073
|
-
loop.push(
|
|
3090
|
+
loop.push(point2(from));
|
|
3074
3091
|
const to = segs[s * 2] === from ? segs[s * 2 + 1] : segs[s * 2];
|
|
3075
3092
|
const next = touch.get(to).find((t) => !used[t]);
|
|
3076
3093
|
from = to;
|
|
@@ -3159,133 +3176,648 @@ function regions(a, W, H, b, minArea) {
|
|
|
3159
3176
|
};
|
|
3160
3177
|
}
|
|
3161
3178
|
|
|
3162
|
-
// src/image-editor/selection/
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
const
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
const
|
|
3182
|
-
const
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
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();
|
|
3186
3238
|
for (let y = y0; y < y1; y++) {
|
|
3187
3239
|
for (let x = x0; x < x1; x++) {
|
|
3188
|
-
const
|
|
3189
|
-
const
|
|
3190
|
-
|
|
3191
|
-
let
|
|
3192
|
-
for (let dy = -
|
|
3193
|
-
|
|
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++) {
|
|
3194
3247
|
const nx = x + dx;
|
|
3195
|
-
const
|
|
3196
|
-
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;
|
|
3197
3251
|
}
|
|
3198
3252
|
}
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
n++;
|
|
3204
|
-
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
|
+
}
|
|
3205
3257
|
}
|
|
3206
3258
|
}
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
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
|
+
}
|
|
3219
3299
|
}
|
|
3220
|
-
}
|
|
3221
|
-
const grown = new Uint8Array(w * h);
|
|
3222
|
-
for (let x = 0; x < w; x++) {
|
|
3223
|
-
let last = -Infinity;
|
|
3300
|
+
} else {
|
|
3224
3301
|
for (let y = 0; y < h; y++) {
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
}
|
|
3234
|
-
const label = new Int32Array(w * h);
|
|
3235
|
-
const stack = new Int32Array(w * h);
|
|
3236
|
-
const found = [];
|
|
3237
|
-
for (let start = 0; start < w * h; start++) {
|
|
3238
|
-
if (!grown[start] || label[start]) continue;
|
|
3239
|
-
const r = { area: 0, e: 0, en: 0, bx0: w, by0: h, bx1: -1, by1: -1 };
|
|
3240
|
-
const id = found.length + 1;
|
|
3241
|
-
let top = 0;
|
|
3242
|
-
stack[top++] = start;
|
|
3243
|
-
label[start] = id;
|
|
3244
|
-
while (top) {
|
|
3245
|
-
const k = stack[--top];
|
|
3246
|
-
const i = k % w;
|
|
3247
|
-
const j = (k - i) / w;
|
|
3248
|
-
r.area++;
|
|
3249
|
-
if (!Number.isNaN(evidence[k])) {
|
|
3250
|
-
r.e += evidence[k];
|
|
3251
|
-
r.en++;
|
|
3252
|
-
}
|
|
3253
|
-
if (i < r.bx0) r.bx0 = i;
|
|
3254
|
-
if (i > r.bx1) r.bx1 = i;
|
|
3255
|
-
if (j < r.by0) r.by0 = j;
|
|
3256
|
-
if (j > r.by1) r.by1 = j;
|
|
3257
|
-
for (let dy = -1; dy <= 1; dy++) {
|
|
3258
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
3259
|
-
const ni = i + dx;
|
|
3260
|
-
const nj = j + dy;
|
|
3261
|
-
if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
|
|
3262
|
-
const nk = nj * w + ni;
|
|
3263
|
-
if (!grown[nk] || label[nk]) continue;
|
|
3264
|
-
label[nk] = id;
|
|
3265
|
-
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;
|
|
3266
3310
|
}
|
|
3267
3311
|
}
|
|
3268
3312
|
}
|
|
3269
|
-
found.push(r);
|
|
3270
3313
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
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];
|
|
3277
3328
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3329
|
+
return [c[0], c[1], c[2]];
|
|
3330
|
+
}
|
|
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
|
+
};
|
|
3289
3821
|
}
|
|
3290
3822
|
|
|
3291
3823
|
// src/image-editor/selection/path.ts
|
|
@@ -3339,12 +3871,12 @@ function pathToSelection(components, width, height, options = {}) {
|
|
|
3339
3871
|
const rx1 = Math.min(width, Math.ceil(x1));
|
|
3340
3872
|
const ry1 = Math.min(height, Math.ceil(y1));
|
|
3341
3873
|
if (!(rx1 > rx0 && ry1 > ry0)) return createSelectionMask(width, height);
|
|
3342
|
-
const
|
|
3343
|
-
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");
|
|
3344
3876
|
const hard = options.antiAlias === false;
|
|
3345
3877
|
const bytes = new Uint8Array(cov.length);
|
|
3346
3878
|
for (let i = 0; i < cov.length; i++) bytes[i] = hard ? cov[i] >= 0.5 ? 255 : 0 : coverageToByte(cov[i]);
|
|
3347
|
-
const mask = selectionFromRegion(width, height,
|
|
3879
|
+
const mask = selectionFromRegion(width, height, rect3, bytes);
|
|
3348
3880
|
const feather = options.feather ?? 0;
|
|
3349
3881
|
return feather > 0 && mask.bounds ? modifySelection(mask, "feather", feather) : mask;
|
|
3350
3882
|
}
|
|
@@ -3454,9 +3986,9 @@ var EdgeMap = class {
|
|
|
3454
3986
|
}
|
|
3455
3987
|
};
|
|
3456
3988
|
var clampPixel = (v, max) => Math.min(max - 1, Math.max(0, Math.floor(v)));
|
|
3457
|
-
function snapToEdge(map,
|
|
3458
|
-
const cx = clampPixel(
|
|
3459
|
-
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);
|
|
3460
3992
|
const r = Math.max(0, Math.round(width));
|
|
3461
3993
|
let best = 0;
|
|
3462
3994
|
let bestD = Infinity;
|
|
@@ -3475,7 +4007,7 @@ function snapToEdge(map, point, width = 10) {
|
|
|
3475
4007
|
}
|
|
3476
4008
|
}
|
|
3477
4009
|
}
|
|
3478
|
-
return best > 0 ? { x: bx + 0.5, y: by + 0.5 } :
|
|
4010
|
+
return best > 0 ? { x: bx + 0.5, y: by + 0.5 } : point2;
|
|
3479
4011
|
}
|
|
3480
4012
|
var Heap = class {
|
|
3481
4013
|
ids = new Int32Array(256);
|
|
@@ -3810,681 +4342,351 @@ var ShapeDrawGesture = class {
|
|
|
3810
4342
|
this.start = null;
|
|
3811
4343
|
return this.layerId;
|
|
3812
4344
|
}
|
|
3813
|
-
cancel() {
|
|
3814
|
-
this.start = null;
|
|
3815
|
-
}
|
|
3816
|
-
};
|
|
3817
|
-
function applyShapeOptions(controller, layerId, change) {
|
|
3818
|
-
const info = controller.getShapeLayer(layerId);
|
|
3819
|
-
const style = {};
|
|
3820
|
-
if ("fill" in change) style.fill = change.fill ?? null;
|
|
3821
|
-
if ("stroke" in change || "strokeWidth" in change) {
|
|
3822
|
-
const color = "stroke" in change ? change.stroke : info.stroke?.paint.type === "color" ? info.stroke.paint.color : null;
|
|
3823
|
-
style.stroke = color ? { width: change.strokeWidth ?? info.stroke?.width ?? 2, paint: color } : null;
|
|
3824
|
-
}
|
|
3825
|
-
if (Object.keys(style).length) controller.setShapeStyle(layerId, style);
|
|
3826
|
-
const live = info.components[0]?.live;
|
|
3827
|
-
if (!live) return;
|
|
3828
|
-
const shape = {};
|
|
3829
|
-
if (live.kind === "rect" && change.radius !== void 0) shape.radius = change.radius;
|
|
3830
|
-
if (live.kind === "polygon" && change.sides !== void 0) shape.sides = change.sides;
|
|
3831
|
-
if (live.kind === "star" && change.points !== void 0) shape.points = change.points;
|
|
3832
|
-
if (live.kind === "star" && change.innerRatio !== void 0) shape.innerRatio = change.innerRatio;
|
|
3833
|
-
if (live.kind === "line" && change.weight !== void 0) shape.weight = change.weight;
|
|
3834
|
-
if (live.kind === "line" && change.arrow !== void 0) shape.endArrow = change.arrow ? { width: (live.weight || 1) * 4, length: (live.weight || 1) * 4 } : null;
|
|
3835
|
-
if (Object.keys(shape).length) controller.setShapeComponent(layerId, 0, { shape });
|
|
3836
|
-
}
|
|
3837
|
-
var TextEditSession = class {
|
|
3838
|
-
constructor(controller, layerId, index) {
|
|
3839
|
-
this.controller = controller;
|
|
3840
|
-
this.layerId = layerId;
|
|
3841
|
-
const n = this.text.length;
|
|
3842
|
-
this.anchor = this.focus = Math.max(0, Math.min(n, index ?? n));
|
|
3843
|
-
}
|
|
3844
|
-
controller;
|
|
3845
|
-
layerId;
|
|
3846
|
-
anchor;
|
|
3847
|
-
focus;
|
|
3848
|
-
changed = false;
|
|
3849
|
-
listeners = /* @__PURE__ */ new Set();
|
|
3850
|
-
error = null;
|
|
3851
|
-
get layer() {
|
|
3852
|
-
const l = findLayer(this.controller.getLayers(), this.layerId)?.layer;
|
|
3853
|
-
if (!l || l.type !== "type") throw new Error("The type layer being edited is gone.");
|
|
3854
|
-
return l;
|
|
3855
|
-
}
|
|
3856
|
-
get text() {
|
|
3857
|
-
return this.layer.text;
|
|
3858
|
-
}
|
|
3859
|
-
get start() {
|
|
3860
|
-
return Math.min(this.anchor, this.focus);
|
|
3861
|
-
}
|
|
3862
|
-
get end() {
|
|
3863
|
-
return Math.max(this.anchor, this.focus);
|
|
3864
|
-
}
|
|
3865
|
-
/** Whether the layer still exists (it can be deleted or undone away while editing). */
|
|
3866
|
-
get alive() {
|
|
3867
|
-
return findLayer(this.controller.getLayers(), this.layerId)?.layer.type === "type";
|
|
3868
|
-
}
|
|
3869
|
-
subscribe(fn) {
|
|
3870
|
-
this.listeners.add(fn);
|
|
3871
|
-
return () => this.listeners.delete(fn);
|
|
3872
|
-
}
|
|
3873
|
-
emit() {
|
|
3874
|
-
for (const l of this.listeners) l();
|
|
3875
|
-
}
|
|
3876
|
-
layout() {
|
|
3877
|
-
return this.controller.getTextLayout(this.layerId);
|
|
3878
|
-
}
|
|
3879
|
-
run(fn) {
|
|
3880
|
-
this.error = null;
|
|
3881
|
-
try {
|
|
3882
|
-
const out = fn(!this.changed);
|
|
3883
|
-
this.changed = true;
|
|
3884
|
-
return out;
|
|
3885
|
-
} catch (e) {
|
|
3886
|
-
this.error = e instanceof Error ? e.message : String(e);
|
|
3887
|
-
return void 0;
|
|
3888
|
-
} finally {
|
|
3889
|
-
this.emit();
|
|
3890
|
-
}
|
|
3891
|
-
}
|
|
3892
|
-
/** Replaces the selection with `text` (typing and pasting). */
|
|
3893
|
-
insert(text) {
|
|
3894
|
-
if (!text) return;
|
|
3895
|
-
const s = this.start;
|
|
3896
|
-
const e = this.end;
|
|
3897
|
-
const done = this.run((commit) => this.controller.setText(this.layerId, text, { start: s, end: e }, commit));
|
|
3898
|
-
if (done) this.anchor = this.focus = s + text.replace(/\r\n?/g, "\n").length;
|
|
3899
|
-
}
|
|
3900
|
-
/** Backspace: the selection, else the character before the caret. */
|
|
3901
|
-
deleteBackward() {
|
|
3902
|
-
if (this.start === this.end && this.start === 0) return;
|
|
3903
|
-
const s = this.start === this.end ? Math.max(0, this.start - this.surrogate(this.start - 1, -1)) : this.start;
|
|
3904
|
-
const e = this.end;
|
|
3905
|
-
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
3906
|
-
}
|
|
3907
|
-
/** Delete: the selection, else the character after the caret. */
|
|
3908
|
-
deleteForward() {
|
|
3909
|
-
const n = this.text.length;
|
|
3910
|
-
if (this.start === this.end && this.end >= n) return;
|
|
3911
|
-
const s = this.start;
|
|
3912
|
-
const e = this.start === this.end ? Math.min(n, this.end + this.surrogate(this.end, 1)) : this.end;
|
|
3913
|
-
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
3914
|
-
}
|
|
3915
|
-
/** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */
|
|
3916
|
-
surrogate(i, dir) {
|
|
3917
|
-
const t = this.text;
|
|
3918
|
-
const c = t.charCodeAt(i);
|
|
3919
|
-
if (dir < 0 && c >= 56320 && c <= 57343 && i > 0) return 2;
|
|
3920
|
-
if (dir > 0 && c >= 55296 && c <= 56319 && i + 1 < t.length) return 2;
|
|
3921
|
-
return 1;
|
|
3922
|
-
}
|
|
3923
|
-
/**
|
|
3924
|
-
* Moves the caret (Shift extends the selection): `left` / `right` along the line, `up` / `down`
|
|
3925
|
-
* between lines, `home` / `end` of the line, `start` / `finish` of the text. Arrow keys of vertical
|
|
3926
|
-
* text map onto these through `textArrowStep` in `handleKey`.
|
|
3927
|
-
*/
|
|
3928
|
-
move(to, extend = false) {
|
|
3929
|
-
const n = this.text.length;
|
|
3930
|
-
let f = this.focus;
|
|
3931
|
-
const collapsedJump = !extend && this.start !== this.end;
|
|
3932
|
-
if (to === "left") f = collapsedJump ? this.start : Math.max(0, f - this.surrogate(f - 1, -1));
|
|
3933
|
-
else if (to === "right") f = collapsedJump ? this.end : Math.min(n, f + this.surrogate(f, 1));
|
|
3934
|
-
else if (to === "start") f = 0;
|
|
3935
|
-
else if (to === "finish") f = n;
|
|
3936
|
-
else {
|
|
3937
|
-
const layout = this.layout();
|
|
3938
|
-
if (layout && layout.lines.length) {
|
|
3939
|
-
const c = caretAt(layout, f);
|
|
3940
|
-
const line = layout.lines[c.line];
|
|
3941
|
-
if (to === "home") f = line.start;
|
|
3942
|
-
else if (to === "end") {
|
|
3943
|
-
const last = line.chars[line.chars.length - 1];
|
|
3944
|
-
f = last ? Math.min(n, last.index + 1) : line.start;
|
|
3945
|
-
if (last && this.text[last.index] === " " && c.line < layout.lines.length - 1) f = last.index;
|
|
3946
|
-
} else f = textLineMove(layout, f, to === "up" ? -1 : 1, n);
|
|
3947
|
-
}
|
|
3948
|
-
}
|
|
3949
|
-
this.focus = f;
|
|
3950
|
-
if (!extend) this.anchor = f;
|
|
3951
|
-
this.emit();
|
|
3952
|
-
}
|
|
3953
|
-
selectAll() {
|
|
3954
|
-
this.anchor = 0;
|
|
3955
|
-
this.focus = this.text.length;
|
|
3956
|
-
this.emit();
|
|
3957
|
-
}
|
|
3958
|
-
/** Places the caret at a document point (Shift-click extends). */
|
|
3959
|
-
pointAt(p, extend = false) {
|
|
3960
|
-
this.focus = this.controller.textIndexAt(this.layerId, p);
|
|
3961
|
-
if (!extend) this.anchor = this.focus;
|
|
3962
|
-
this.emit();
|
|
3963
|
-
}
|
|
3964
|
-
/** Handles a key; returns `true` when it was the session's (the caller then prevents the default). */
|
|
3965
|
-
handleKey(e) {
|
|
3966
|
-
const mod2 = e.metaKey || e.ctrlKey;
|
|
3967
|
-
if (mod2 && e.key.toLowerCase() === "a") {
|
|
3968
|
-
this.selectAll();
|
|
3969
|
-
return true;
|
|
3970
|
-
}
|
|
3971
|
-
switch (e.key) {
|
|
3972
|
-
case "Backspace":
|
|
3973
|
-
this.deleteBackward();
|
|
3974
|
-
return true;
|
|
3975
|
-
case "Delete":
|
|
3976
|
-
this.deleteForward();
|
|
3977
|
-
return true;
|
|
3978
|
-
case "Enter":
|
|
3979
|
-
this.insert("\n");
|
|
3980
|
-
return true;
|
|
3981
|
-
case "ArrowLeft":
|
|
3982
|
-
case "ArrowRight":
|
|
3983
|
-
case "ArrowUp":
|
|
3984
|
-
case "ArrowDown": {
|
|
3985
|
-
const step = textArrowStep(this.layer.orientation, e.key);
|
|
3986
|
-
if (step === "previous") this.move(mod2 ? "home" : "left", e.shiftKey);
|
|
3987
|
-
else if (step === "next") this.move(mod2 ? "end" : "right", e.shiftKey);
|
|
3988
|
-
else if (step === "line-before") this.move(mod2 ? "start" : "up", e.shiftKey);
|
|
3989
|
-
else this.move(mod2 ? "finish" : "down", e.shiftKey);
|
|
3990
|
-
return true;
|
|
3991
|
-
}
|
|
3992
|
-
case "Home":
|
|
3993
|
-
this.move("home", e.shiftKey);
|
|
3994
|
-
return true;
|
|
3995
|
-
case "End":
|
|
3996
|
-
this.move("end", e.shiftKey);
|
|
3997
|
-
return true;
|
|
3998
|
-
case "Tab":
|
|
3999
|
-
this.insert(" ");
|
|
4000
|
-
return true;
|
|
4001
|
-
}
|
|
4002
|
-
if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
|
|
4003
|
-
this.insert(e.key);
|
|
4004
|
-
return true;
|
|
4005
|
-
}
|
|
4006
|
-
return false;
|
|
4007
|
-
}
|
|
4008
|
-
/** Sets character style fields on the selection, or on the whole text when nothing is selected. */
|
|
4009
|
-
setStyle(style) {
|
|
4010
|
-
const s = this.start;
|
|
4011
|
-
const e = this.end;
|
|
4012
|
-
const range = s === e ? {} : { start: s, end: e };
|
|
4013
|
-
this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
|
|
4014
|
-
}
|
|
4015
|
-
/** Sets paragraph fields on the paragraphs the selection touches. */
|
|
4016
|
-
setParagraph(style) {
|
|
4017
|
-
const s = this.start;
|
|
4018
|
-
const e = this.end;
|
|
4019
|
-
this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
|
|
4020
|
-
}
|
|
4021
|
-
/** The style at the caret (or the selection's start), for the options bar. */
|
|
4022
|
-
styleHere() {
|
|
4023
|
-
const layer = this.layer;
|
|
4024
|
-
const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
|
|
4025
|
-
return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
|
|
4026
|
-
}
|
|
4027
|
-
info() {
|
|
4028
|
-
return this.controller.getTextLayer(this.layerId);
|
|
4029
|
-
}
|
|
4030
|
-
/** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
|
|
4031
|
-
overlay() {
|
|
4032
|
-
const layer = this.layer;
|
|
4033
|
-
const layout = this.layout();
|
|
4034
|
-
const m = textMatrix(layer);
|
|
4035
|
-
const at = (x, y) => applyMatrix(m, { x, y });
|
|
4036
|
-
let frame = null;
|
|
4037
|
-
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)];
|
|
4038
|
-
else if (layout) {
|
|
4039
|
-
const b = layout.bounds;
|
|
4040
|
-
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)];
|
|
4041
|
-
}
|
|
4042
|
-
if (!layout) return { caret: null, selection: [], frame };
|
|
4043
|
-
const selection = textSelectionQuads(layout, this.start, this.end).map((q) => q.map((p) => at(p.x, p.y)));
|
|
4044
|
-
const c = caretAt(layout, this.focus);
|
|
4045
|
-
return { caret: [at(c.from.x, c.from.y), at(c.to.x, c.to.y)], selection, frame };
|
|
4046
|
-
}
|
|
4047
|
-
/** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
|
|
4048
|
-
close() {
|
|
4049
|
-
if (!this.alive) return false;
|
|
4050
|
-
if (!this.text.trim().length) {
|
|
4051
|
-
try {
|
|
4052
|
-
this.controller.removeLayer(this.layerId);
|
|
4053
|
-
} catch {
|
|
4054
|
-
}
|
|
4055
|
-
return false;
|
|
4056
|
-
}
|
|
4057
|
-
if (this.layer.name === "Text" && this.changed) {
|
|
4058
|
-
try {
|
|
4059
|
-
this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
|
|
4060
|
-
} catch {
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
return true;
|
|
4345
|
+
cancel() {
|
|
4346
|
+
this.start = null;
|
|
4064
4347
|
}
|
|
4065
4348
|
};
|
|
4066
|
-
function
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
const
|
|
4072
|
-
|
|
4073
|
-
if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
|
|
4074
|
-
else {
|
|
4075
|
-
const layout = controller.getTextLayout(l.id);
|
|
4076
|
-
if (layout) b = layout.bounds;
|
|
4077
|
-
}
|
|
4078
|
-
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;
|
|
4349
|
+
function applyShapeOptions(controller, layerId, change) {
|
|
4350
|
+
const info = controller.getShapeLayer(layerId);
|
|
4351
|
+
const style = {};
|
|
4352
|
+
if ("fill" in change) style.fill = change.fill ?? null;
|
|
4353
|
+
if ("stroke" in change || "strokeWidth" in change) {
|
|
4354
|
+
const color = "stroke" in change ? change.stroke : info.stroke?.paint.type === "color" ? info.stroke.paint.color : null;
|
|
4355
|
+
style.stroke = color ? { width: change.strokeWidth ?? info.stroke?.width ?? 2, paint: color } : null;
|
|
4079
4356
|
}
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
const
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
};
|
|
4090
|
-
|
|
4091
|
-
return out;
|
|
4092
|
-
}
|
|
4093
|
-
function invert(m) {
|
|
4094
|
-
const [a, b, c, d, e, f] = m;
|
|
4095
|
-
const det = a * d - b * c || 1;
|
|
4096
|
-
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
4357
|
+
if (Object.keys(style).length) controller.setShapeStyle(layerId, style);
|
|
4358
|
+
const live = info.components[0]?.live;
|
|
4359
|
+
if (!live) return;
|
|
4360
|
+
const shape = {};
|
|
4361
|
+
if (live.kind === "rect" && change.radius !== void 0) shape.radius = change.radius;
|
|
4362
|
+
if (live.kind === "polygon" && change.sides !== void 0) shape.sides = change.sides;
|
|
4363
|
+
if (live.kind === "star" && change.points !== void 0) shape.points = change.points;
|
|
4364
|
+
if (live.kind === "star" && change.innerRatio !== void 0) shape.innerRatio = change.innerRatio;
|
|
4365
|
+
if (live.kind === "line" && change.weight !== void 0) shape.weight = change.weight;
|
|
4366
|
+
if (live.kind === "line" && change.arrow !== void 0) shape.endArrow = change.arrow ? { width: (live.weight || 1) * 4, length: (live.weight || 1) * 4 } : null;
|
|
4367
|
+
if (Object.keys(shape).length) controller.setShapeComponent(layerId, 0, { shape });
|
|
4097
4368
|
}
|
|
4098
|
-
var
|
|
4099
|
-
constructor(controller,
|
|
4369
|
+
var TextEditSession = class {
|
|
4370
|
+
constructor(controller, layerId, index) {
|
|
4100
4371
|
this.controller = controller;
|
|
4101
|
-
this.
|
|
4372
|
+
this.layerId = layerId;
|
|
4373
|
+
const n = this.text.length;
|
|
4374
|
+
this.anchor = this.focus = Math.max(0, Math.min(n, index ?? n));
|
|
4102
4375
|
}
|
|
4103
4376
|
controller;
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4377
|
+
layerId;
|
|
4378
|
+
anchor;
|
|
4379
|
+
focus;
|
|
4380
|
+
changed = false;
|
|
4381
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4107
4382
|
error = null;
|
|
4108
|
-
|
|
4109
|
-
this.
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
this.session.pointAt(p, mods.shift);
|
|
4113
|
-
this.dragStart = null;
|
|
4114
|
-
return;
|
|
4115
|
-
}
|
|
4116
|
-
this.finish();
|
|
4117
|
-
if (hit) {
|
|
4118
|
-
this.controller.setActiveLayer(hit);
|
|
4119
|
-
this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
|
|
4120
|
-
this.dragStart = null;
|
|
4121
|
-
return;
|
|
4122
|
-
}
|
|
4123
|
-
this.dragStart = { x: p.x, y: p.y };
|
|
4383
|
+
get layer() {
|
|
4384
|
+
const l = findLayer(this.controller.getLayers(), this.layerId)?.layer;
|
|
4385
|
+
if (!l || l.type !== "type") throw new Error("The type layer being edited is gone.");
|
|
4386
|
+
return l;
|
|
4124
4387
|
}
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4388
|
+
get text() {
|
|
4389
|
+
return this.layer.text;
|
|
4390
|
+
}
|
|
4391
|
+
get start() {
|
|
4392
|
+
return Math.min(this.anchor, this.focus);
|
|
4393
|
+
}
|
|
4394
|
+
get end() {
|
|
4395
|
+
return Math.max(this.anchor, this.focus);
|
|
4396
|
+
}
|
|
4397
|
+
/** Whether the layer still exists (it can be deleted or undone away while editing). */
|
|
4398
|
+
get alive() {
|
|
4399
|
+
return findLayer(this.controller.getLayers(), this.layerId)?.layer.type === "type";
|
|
4400
|
+
}
|
|
4401
|
+
subscribe(fn) {
|
|
4402
|
+
this.listeners.add(fn);
|
|
4403
|
+
return () => this.listeners.delete(fn);
|
|
4404
|
+
}
|
|
4405
|
+
emit() {
|
|
4406
|
+
for (const l of this.listeners) l();
|
|
4407
|
+
}
|
|
4408
|
+
layout() {
|
|
4409
|
+
return this.controller.getTextLayout(this.layerId);
|
|
4410
|
+
}
|
|
4411
|
+
run(fn) {
|
|
4412
|
+
this.error = null;
|
|
4133
4413
|
try {
|
|
4134
|
-
const
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
x: box ? Math.min(start.x, p.x) : start.x,
|
|
4138
|
-
y: box ? Math.min(start.y, p.y) : start.y,
|
|
4139
|
-
...box ? { box } : {},
|
|
4140
|
-
style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
|
|
4141
|
-
paragraph: o.align !== "left" ? { align: o.align } : {},
|
|
4142
|
-
...o.orientation === "vertical" ? { orientation: "vertical" } : {},
|
|
4143
|
-
name: "Text"
|
|
4144
|
-
});
|
|
4145
|
-
this.session = new TextEditSession(this.controller, id, 0);
|
|
4414
|
+
const out = fn(!this.changed);
|
|
4415
|
+
this.changed = true;
|
|
4416
|
+
return out;
|
|
4146
4417
|
} catch (e) {
|
|
4147
4418
|
this.error = e instanceof Error ? e.message : String(e);
|
|
4419
|
+
return void 0;
|
|
4420
|
+
} finally {
|
|
4421
|
+
this.emit();
|
|
4148
4422
|
}
|
|
4149
|
-
return this.session;
|
|
4150
4423
|
}
|
|
4151
|
-
/**
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4424
|
+
/** Replaces the selection with `text` (typing and pasting). */
|
|
4425
|
+
insert(text) {
|
|
4426
|
+
if (!text) return;
|
|
4427
|
+
const s = this.start;
|
|
4428
|
+
const e = this.end;
|
|
4429
|
+
const done = this.run((commit) => this.controller.setText(this.layerId, text, { start: s, end: e }, commit));
|
|
4430
|
+
if (done) this.anchor = this.focus = s + text.replace(/\r\n?/g, "\n").length;
|
|
4156
4431
|
}
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
throw new Error("Invalid pixel buffer size");
|
|
4432
|
+
/** Backspace: the selection, else the character before the caret. */
|
|
4433
|
+
deleteBackward() {
|
|
4434
|
+
if (this.start === this.end && this.start === 0) return;
|
|
4435
|
+
const s = this.start === this.end ? Math.max(0, this.start - this.surrogate(this.start - 1, -1)) : this.start;
|
|
4436
|
+
const e = this.end;
|
|
4437
|
+
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
4164
4438
|
}
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
4439
|
+
/** Delete: the selection, else the character after the caret. */
|
|
4440
|
+
deleteForward() {
|
|
4441
|
+
const n = this.text.length;
|
|
4442
|
+
if (this.start === this.end && this.end >= n) return;
|
|
4443
|
+
const s = this.start;
|
|
4444
|
+
const e = this.start === this.end ? Math.min(n, this.end + this.surrogate(this.end, 1)) : this.end;
|
|
4445
|
+
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
4173
4446
|
}
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;
|
|
4182
|
-
const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
|
|
4183
|
-
const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;
|
|
4184
|
-
const fx = labF(x);
|
|
4185
|
-
const fy = labF(y);
|
|
4186
|
-
const fz = labF(z);
|
|
4187
|
-
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
4188
|
-
}
|
|
4189
|
-
function similarity(pixels, seed, tolerance, distance) {
|
|
4190
|
-
const d = pixels.data;
|
|
4191
|
-
const tr = d[seed * 4];
|
|
4192
|
-
const tg = d[seed * 4 + 1];
|
|
4193
|
-
const tb = d[seed * 4 + 2];
|
|
4194
|
-
const ta = d[seed * 4 + 3];
|
|
4195
|
-
const tol = Math.max(0, Math.floor(tolerance + 1e-3));
|
|
4196
|
-
if (distance === "perceptual") {
|
|
4197
|
-
const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);
|
|
4198
|
-
const de = Math.max(0, tolerance) * 100 / 255;
|
|
4199
|
-
const de2 = de * de + 1e-6;
|
|
4200
|
-
return (i) => {
|
|
4201
|
-
const p = i * 4;
|
|
4202
|
-
const a = d[p + 3];
|
|
4203
|
-
if (ta === 0 && a === 0) return true;
|
|
4204
|
-
if (Math.abs(a - ta) > tol) return false;
|
|
4205
|
-
const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);
|
|
4206
|
-
return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;
|
|
4207
|
-
};
|
|
4447
|
+
/** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */
|
|
4448
|
+
surrogate(i, dir) {
|
|
4449
|
+
const t = this.text;
|
|
4450
|
+
const c = t.charCodeAt(i);
|
|
4451
|
+
if (dir < 0 && c >= 56320 && c <= 57343 && i > 0) return 2;
|
|
4452
|
+
if (dir > 0 && c >= 55296 && c <= 56319 && i + 1 < t.length) return 2;
|
|
4453
|
+
return 1;
|
|
4208
4454
|
}
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
const
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
if (edge) {
|
|
4234
|
-
const avg = sum / (9 * 255);
|
|
4235
|
-
out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);
|
|
4455
|
+
/**
|
|
4456
|
+
* Moves the caret (Shift extends the selection): `left` / `right` along the line, `up` / `down`
|
|
4457
|
+
* between lines, `home` / `end` of the line, `start` / `finish` of the text. Arrow keys of vertical
|
|
4458
|
+
* text map onto these through `textArrowStep` in `handleKey`.
|
|
4459
|
+
*/
|
|
4460
|
+
move(to, extend = false) {
|
|
4461
|
+
const n = this.text.length;
|
|
4462
|
+
let f = this.focus;
|
|
4463
|
+
const collapsedJump = !extend && this.start !== this.end;
|
|
4464
|
+
if (to === "left") f = collapsedJump ? this.start : Math.max(0, f - this.surrogate(f - 1, -1));
|
|
4465
|
+
else if (to === "right") f = collapsedJump ? this.end : Math.min(n, f + this.surrogate(f, 1));
|
|
4466
|
+
else if (to === "start") f = 0;
|
|
4467
|
+
else if (to === "finish") f = n;
|
|
4468
|
+
else {
|
|
4469
|
+
const layout = this.layout();
|
|
4470
|
+
if (layout && layout.lines.length) {
|
|
4471
|
+
const c = caretAt(layout, f);
|
|
4472
|
+
const line = layout.lines[c.line];
|
|
4473
|
+
if (to === "home") f = line.start;
|
|
4474
|
+
else if (to === "end") {
|
|
4475
|
+
const last = line.chars[line.chars.length - 1];
|
|
4476
|
+
f = last ? Math.min(n, last.index + 1) : line.start;
|
|
4477
|
+
if (last && this.text[last.index] === " " && c.line < layout.lines.length - 1) f = last.index;
|
|
4478
|
+
} else f = textLineMove(layout, f, to === "up" ? -1 : 1, n);
|
|
4236
4479
|
}
|
|
4237
4480
|
}
|
|
4481
|
+
this.focus = f;
|
|
4482
|
+
if (!extend) this.anchor = f;
|
|
4483
|
+
this.emit();
|
|
4238
4484
|
}
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
const idx = stack.pop();
|
|
4257
|
-
if (marks[idx]) continue;
|
|
4258
|
-
const y = idx / w | 0;
|
|
4259
|
-
const row = y * w;
|
|
4260
|
-
let l = idx - row;
|
|
4261
|
-
let r = l;
|
|
4262
|
-
while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;
|
|
4263
|
-
while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;
|
|
4264
|
-
marks.fill(255, row + l, row + r + 1);
|
|
4265
|
-
if (l < bx0) bx0 = l;
|
|
4266
|
-
if (r > bx1) bx1 = r;
|
|
4267
|
-
if (y < by0) by0 = y;
|
|
4268
|
-
if (y > by1) by1 = y;
|
|
4269
|
-
for (const ny of [y - 1, y + 1]) {
|
|
4270
|
-
if (ny < 0 || ny >= h) continue;
|
|
4271
|
-
const nrow = ny * w;
|
|
4272
|
-
let inRun = false;
|
|
4273
|
-
for (let nx = l; nx <= r; nx++) {
|
|
4274
|
-
const ok = !marks[nrow + nx] && similar(nrow + nx);
|
|
4275
|
-
if (ok && !inRun) stack.push(nrow + nx);
|
|
4276
|
-
inRun = ok;
|
|
4277
|
-
}
|
|
4278
|
-
}
|
|
4485
|
+
selectAll() {
|
|
4486
|
+
this.anchor = 0;
|
|
4487
|
+
this.focus = this.text.length;
|
|
4488
|
+
this.emit();
|
|
4489
|
+
}
|
|
4490
|
+
/** Places the caret at a document point (Shift-click extends). */
|
|
4491
|
+
pointAt(p, extend = false) {
|
|
4492
|
+
this.focus = this.controller.textIndexAt(this.layerId, p);
|
|
4493
|
+
if (!extend) this.anchor = this.focus;
|
|
4494
|
+
this.emit();
|
|
4495
|
+
}
|
|
4496
|
+
/** Handles a key; returns `true` when it was the session's (the caller then prevents the default). */
|
|
4497
|
+
handleKey(e) {
|
|
4498
|
+
const mod2 = e.metaKey || e.ctrlKey;
|
|
4499
|
+
if (mod2 && e.key.toLowerCase() === "a") {
|
|
4500
|
+
this.selectAll();
|
|
4501
|
+
return true;
|
|
4279
4502
|
}
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4503
|
+
switch (e.key) {
|
|
4504
|
+
case "Backspace":
|
|
4505
|
+
this.deleteBackward();
|
|
4506
|
+
return true;
|
|
4507
|
+
case "Delete":
|
|
4508
|
+
this.deleteForward();
|
|
4509
|
+
return true;
|
|
4510
|
+
case "Enter":
|
|
4511
|
+
this.insert("\n");
|
|
4512
|
+
return true;
|
|
4513
|
+
case "ArrowLeft":
|
|
4514
|
+
case "ArrowRight":
|
|
4515
|
+
case "ArrowUp":
|
|
4516
|
+
case "ArrowDown": {
|
|
4517
|
+
const step = textArrowStep(this.layer.orientation, e.key);
|
|
4518
|
+
if (step === "previous") this.move(mod2 ? "home" : "left", e.shiftKey);
|
|
4519
|
+
else if (step === "next") this.move(mod2 ? "end" : "right", e.shiftKey);
|
|
4520
|
+
else if (step === "line-before") this.move(mod2 ? "start" : "up", e.shiftKey);
|
|
4521
|
+
else this.move(mod2 ? "finish" : "down", e.shiftKey);
|
|
4522
|
+
return true;
|
|
4291
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;
|
|
4533
|
+
}
|
|
4534
|
+
if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
|
|
4535
|
+
this.insert(e.key);
|
|
4536
|
+
return true;
|
|
4292
4537
|
}
|
|
4538
|
+
return false;
|
|
4293
4539
|
}
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
);
|
|
4301
|
-
}
|
|
4302
|
-
function sampleColor(c) {
|
|
4303
|
-
if (typeof c === "string") {
|
|
4304
|
-
const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
|
|
4305
|
-
if (!m) throw new Error(`Invalid colour "${c}"`);
|
|
4306
|
-
const v = parseInt(m[1], 16);
|
|
4307
|
-
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
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));
|
|
4308
4546
|
}
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
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));
|
|
4552
|
+
}
|
|
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);
|
|
4558
|
+
}
|
|
4559
|
+
info() {
|
|
4560
|
+
return this.controller.getTextLayer(this.layerId);
|
|
4561
|
+
}
|
|
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)];
|
|
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 };
|
|
4578
|
+
}
|
|
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 {
|
|
4331
4586
|
}
|
|
4332
|
-
|
|
4587
|
+
return false;
|
|
4333
4588
|
}
|
|
4334
|
-
|
|
4589
|
+
if (this.layer.name === "Text" && this.changed) {
|
|
4590
|
+
try {
|
|
4591
|
+
this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
|
|
4592
|
+
} catch {
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
return true;
|
|
4335
4596
|
}
|
|
4336
|
-
return createSelectionMask(w, h, out);
|
|
4337
|
-
}
|
|
4338
|
-
var TONE_RANGE_PRESETS = {
|
|
4339
|
-
shadows: [0, 65],
|
|
4340
|
-
midtones: [105, 150],
|
|
4341
|
-
highlights: [190, 255]
|
|
4342
4597
|
};
|
|
4343
|
-
function
|
|
4344
|
-
const
|
|
4345
|
-
const
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
return createSelectionMask(pixels.width, pixels.height, out);
|
|
4357
|
-
}
|
|
4358
|
-
function hueRange(pixels, center) {
|
|
4359
|
-
const n = checkPixels(pixels);
|
|
4360
|
-
const out = new Uint8Array(n);
|
|
4361
|
-
const d = pixels.data;
|
|
4362
|
-
for (let i = 0; i < n; i++) {
|
|
4363
|
-
const p = i * 4;
|
|
4364
|
-
if (d[p + 3] <= 0) continue;
|
|
4365
|
-
const r = d[p] / 255;
|
|
4366
|
-
const g = d[p + 1] / 255;
|
|
4367
|
-
const b = d[p + 2] / 255;
|
|
4368
|
-
const mx = Math.max(r, g, b);
|
|
4369
|
-
const mn = Math.min(r, g, b);
|
|
4370
|
-
const chroma = mx - mn;
|
|
4371
|
-
if (chroma <= 0) continue;
|
|
4372
|
-
let hue;
|
|
4373
|
-
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
4374
|
-
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
4375
|
-
else hue = (r - g) / chroma + 4;
|
|
4376
|
-
hue *= 60;
|
|
4377
|
-
let dh = ((hue - center) % 360 + 360) % 360;
|
|
4378
|
-
dh = Math.min(dh, 360 - dh);
|
|
4379
|
-
out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));
|
|
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;
|
|
4380
4611
|
}
|
|
4381
|
-
return
|
|
4382
|
-
}
|
|
4383
|
-
var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
|
|
4384
|
-
var COLOR_RANGE_PRESETS = [
|
|
4385
|
-
...Object.keys(HUE_RANGE_PRESETS),
|
|
4386
|
-
"highlights",
|
|
4387
|
-
"midtones",
|
|
4388
|
-
"shadows",
|
|
4389
|
-
"skinTones"
|
|
4390
|
-
];
|
|
4391
|
-
function isColorRangePreset(v) {
|
|
4392
|
-
return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
|
|
4612
|
+
return null;
|
|
4393
4613
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
const b = d[p + 2] / 255;
|
|
4405
|
-
const mx = Math.max(r, g, b);
|
|
4406
|
-
const mn = Math.min(r, g, b);
|
|
4407
|
-
const chroma = mx - mn;
|
|
4408
|
-
if (chroma <= 0 || mx <= 0) continue;
|
|
4409
|
-
let hue;
|
|
4410
|
-
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
4411
|
-
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
4412
|
-
else hue = (r - g) / chroma + 4;
|
|
4413
|
-
hue *= 60;
|
|
4414
|
-
if (hue > 180) hue -= 360;
|
|
4415
|
-
const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
|
|
4416
|
-
out[i] = coverageToByte(k);
|
|
4417
|
-
}
|
|
4418
|
-
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;
|
|
4419
4624
|
}
|
|
4420
|
-
function
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
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];
|
|
4425
4629
|
}
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
const n = pixels.width * pixels.height;
|
|
4431
|
-
for (let i = 0; i < n; i++) {
|
|
4432
|
-
if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
|
|
4433
|
-
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;
|
|
4434
4634
|
}
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
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;
|
|
4446
4647
|
}
|
|
4447
|
-
|
|
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 };
|
|
4448
4656
|
}
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
if (x > 0) visit(i - 1);
|
|
4475
|
-
if (x < w - 1) visit(i + 1);
|
|
4476
|
-
if (y > 0) visit(i - w);
|
|
4477
|
-
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;
|
|
4478
4682
|
}
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
return createSelectionMask(pixels.width, pixels.height, out);
|
|
4487
|
-
}
|
|
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
|
+
};
|
|
4488
4690
|
|
|
4489
4691
|
// src/image-editor/selection/segmentation.ts
|
|
4490
4692
|
var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
|
|
@@ -4769,7 +4971,7 @@ var ImageEffectsEngine = class {
|
|
|
4769
4971
|
|
|
4770
4972
|
// src/image-editor/free-transform.ts
|
|
4771
4973
|
var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
|
|
4772
|
-
var
|
|
4974
|
+
var fail2 = (message) => {
|
|
4773
4975
|
throw new LayerOperationError(message);
|
|
4774
4976
|
};
|
|
4775
4977
|
var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
|
|
@@ -4850,20 +5052,20 @@ function liftMask(mask, sel, o) {
|
|
|
4850
5052
|
var FreeTransformSession = class {
|
|
4851
5053
|
constructor(host, init = {}) {
|
|
4852
5054
|
this.host = host;
|
|
4853
|
-
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.`);
|
|
4854
5056
|
this.interp = init.interpolation ?? "bicubic";
|
|
4855
5057
|
const sel = host.selection();
|
|
4856
5058
|
const selected = Boolean(sel?.bounds);
|
|
4857
5059
|
let content = init.content ?? "auto";
|
|
4858
5060
|
if (!["auto", "layer", "selection", "selection-copy", "selection-outline"].includes(content)) {
|
|
4859
|
-
|
|
5061
|
+
fail2(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
|
|
4860
5062
|
}
|
|
4861
5063
|
const targetId = init.layerId ?? host.activeLayerId();
|
|
4862
5064
|
const target = targetId && content !== "selection-outline" ? findLayer(host.layers(), targetId)?.layer : void 0;
|
|
4863
5065
|
const smartTarget = (target?.type === "smart" || target?.type === "shape" || target?.type === "type") && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : "pixels")) !== "mask";
|
|
4864
5066
|
if (content === "auto") content = selected && !smartTarget ? "selection" : "layer";
|
|
4865
5067
|
this.content = content;
|
|
4866
|
-
if (content !== "layer" && !selected)
|
|
5068
|
+
if (content !== "layer" && !selected) fail2(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
|
|
4867
5069
|
const layers = host.layers();
|
|
4868
5070
|
this.startLayers = layers;
|
|
4869
5071
|
this.startSelection = sel;
|
|
@@ -4878,17 +5080,17 @@ var FreeTransformSession = class {
|
|
|
4878
5080
|
box = { ...sel.bounds };
|
|
4879
5081
|
} else {
|
|
4880
5082
|
const id = init.layerId ?? host.activeLayerId();
|
|
4881
|
-
if (!id)
|
|
5083
|
+
if (!id) fail2("No layer given and none is active; pass layerId (see list_layers).");
|
|
4882
5084
|
const layer = requireLayer(layers, id).layer;
|
|
4883
5085
|
this.layerId = layer.id;
|
|
4884
5086
|
const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
|
|
4885
5087
|
const channel = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : "pixels");
|
|
4886
|
-
if (channel !== "pixels" && channel !== "mask")
|
|
5088
|
+
if (channel !== "pixels" && channel !== "mask") fail2(`Unknown channel "${String(channel)}"; use pixels or mask.`);
|
|
4887
5089
|
const linked = mask?.linked !== false;
|
|
4888
5090
|
const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
|
|
4889
|
-
if (channel === "mask" && !mask)
|
|
4890
|
-
if (!maskAlone && layer.type !== "pixel" && layer.type !== "smart" && layer.type !== "shape" && layer.type !== "type")
|
|
4891
|
-
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.`);
|
|
4892
5094
|
if (content !== "selection-copy") {
|
|
4893
5095
|
requireUnlocked(layer, "position", "transform it");
|
|
4894
5096
|
if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
|
|
@@ -4896,8 +5098,8 @@ var FreeTransformSession = class {
|
|
|
4896
5098
|
const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
|
|
4897
5099
|
const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
|
|
4898
5100
|
if (layer.type === "smart" && !maskAlone) {
|
|
4899
|
-
if (content !== "layer")
|
|
4900
|
-
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.`);
|
|
4901
5103
|
this.smart = layer;
|
|
4902
5104
|
this.smartObject = true;
|
|
4903
5105
|
this.startGeometry = placementGeometry(layer);
|
|
@@ -4910,7 +5112,7 @@ var FreeTransformSession = class {
|
|
|
4910
5112
|
return;
|
|
4911
5113
|
}
|
|
4912
5114
|
if (layer.type === "type" && !maskAlone) {
|
|
4913
|
-
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).`);
|
|
4914
5116
|
const frame = typeFrame(layer, host.textEnv().shaper);
|
|
4915
5117
|
const m = textMatrix(layer);
|
|
4916
5118
|
const at = (x, y) => ({ x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] });
|
|
@@ -4930,9 +5132,9 @@ var FreeTransformSession = class {
|
|
|
4930
5132
|
return;
|
|
4931
5133
|
}
|
|
4932
5134
|
if (layer.type === "shape" && !maskAlone) {
|
|
4933
|
-
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).`);
|
|
4934
5136
|
const g = shapeGeometryBounds(layer);
|
|
4935
|
-
if (!g)
|
|
5137
|
+
if (!g) fail2(`Shape layer "${layer.name || id}" has no path to transform.`);
|
|
4936
5138
|
const w = Math.max(g.width, 1);
|
|
4937
5139
|
const h = Math.max(g.height, 1);
|
|
4938
5140
|
box = { x: g.x + (g.width - w) / 2, y: g.y + (g.height - h) / 2, width: w, height: h };
|
|
@@ -4944,7 +5146,7 @@ var FreeTransformSession = class {
|
|
|
4944
5146
|
moves.push("mask");
|
|
4945
5147
|
}
|
|
4946
5148
|
} else if (maskAlone) {
|
|
4947
|
-
if (content === "selection-copy")
|
|
5149
|
+
if (content === "selection-copy") fail2("Layer via Copy copies pixels; transform a mask with content layer or selection.");
|
|
4948
5150
|
if (content === "selection") {
|
|
4949
5151
|
const { lifted, rest } = liftMask(mask.surface, sel, o);
|
|
4950
5152
|
this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
|
|
@@ -4999,8 +5201,8 @@ var FreeTransformSession = class {
|
|
|
4999
5201
|
moves.push("selection");
|
|
5000
5202
|
}
|
|
5001
5203
|
}
|
|
5002
|
-
if (!box && moves[0] === "mask")
|
|
5003
|
-
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).");
|
|
5004
5206
|
this.box = box;
|
|
5005
5207
|
this.moves = moves;
|
|
5006
5208
|
this.startGeometry = identityTransform(box);
|
|
@@ -5059,10 +5261,10 @@ var FreeTransformSession = class {
|
|
|
5059
5261
|
return !this.closed;
|
|
5060
5262
|
}
|
|
5061
5263
|
check() {
|
|
5062
|
-
if (this.closed)
|
|
5264
|
+
if (this.closed) fail2("This transform has already been committed or cancelled; begin a new one.");
|
|
5063
5265
|
if (this.host.layers() !== this.shown) {
|
|
5064
5266
|
this.closed = true;
|
|
5065
|
-
|
|
5267
|
+
fail2("The document changed while the transform was open, so it was abandoned; begin a new one.");
|
|
5066
5268
|
}
|
|
5067
5269
|
}
|
|
5068
5270
|
/**
|
|
@@ -5072,7 +5274,7 @@ var FreeTransformSession = class {
|
|
|
5072
5274
|
update(changes, preview = {}) {
|
|
5073
5275
|
this.check();
|
|
5074
5276
|
if (changes.interpolation !== void 0) {
|
|
5075
|
-
if (!isInterpolation(changes.interpolation))
|
|
5277
|
+
if (!isInterpolation(changes.interpolation)) fail2(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
|
|
5076
5278
|
this.interp = changes.interpolation;
|
|
5077
5279
|
}
|
|
5078
5280
|
const next = {
|
|
@@ -5080,10 +5282,10 @@ var FreeTransformSession = class {
|
|
|
5080
5282
|
corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
|
|
5081
5283
|
warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
|
|
5082
5284
|
};
|
|
5083
|
-
if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)))
|
|
5084
|
-
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.");
|
|
5085
5287
|
if (this.typeTarget && (hasWarp(next) || !matrixFromCorners(next.box, next.corners))) {
|
|
5086
|
-
|
|
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.");
|
|
5087
5289
|
}
|
|
5088
5290
|
this.geometryValue = next;
|
|
5089
5291
|
if (preview !== false) this.render(preview);
|
|
@@ -5151,7 +5353,7 @@ var FreeTransformSession = class {
|
|
|
5151
5353
|
moved = transformSurface(src, hl, opts);
|
|
5152
5354
|
}
|
|
5153
5355
|
} catch (e) {
|
|
5154
|
-
if (e instanceof TransformTooLargeError)
|
|
5356
|
+
if (e instanceof TransformTooLargeError) fail2(e.message);
|
|
5155
5357
|
throw e;
|
|
5156
5358
|
}
|
|
5157
5359
|
}
|
|
@@ -5197,9 +5399,9 @@ var FreeTransformSession = class {
|
|
|
5197
5399
|
resampleType() {
|
|
5198
5400
|
const layer = this.typeTarget;
|
|
5199
5401
|
const g = this.geometryValue;
|
|
5200
|
-
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.");
|
|
5201
5403
|
const m = matrixFromCorners(g.box, g.corners);
|
|
5202
|
-
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.");
|
|
5203
5405
|
const next = { ...layer, transform: m };
|
|
5204
5406
|
delete next.annotation;
|
|
5205
5407
|
let layers = mapLayer(this.startLayers, layer.id, () => rerenderText(next, this.host.textEnv()));
|
|
@@ -5227,7 +5429,7 @@ var FreeTransformSession = class {
|
|
|
5227
5429
|
const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });
|
|
5228
5430
|
return mapLayer(this.startLayers, layer.id, (l) => ({ ...l, placement, surface }));
|
|
5229
5431
|
} catch (e) {
|
|
5230
|
-
if (e instanceof TransformTooLargeError)
|
|
5432
|
+
if (e instanceof TransformTooLargeError) fail2(e.message);
|
|
5231
5433
|
throw e;
|
|
5232
5434
|
}
|
|
5233
5435
|
}
|
|
@@ -5774,9 +5976,9 @@ var PaintStrokeSession = class {
|
|
|
5774
5976
|
this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);
|
|
5775
5977
|
}
|
|
5776
5978
|
};
|
|
5777
|
-
function workRegionOf(surface,
|
|
5778
|
-
if (!depth) return Float32Array.from(readRegion(surface,
|
|
5779
|
-
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);
|
|
5780
5982
|
const out = new Float32Array(d.length);
|
|
5781
5983
|
if (d instanceof Uint16Array) {
|
|
5782
5984
|
for (let i = 0; i < d.length; i++) out[i] = d[i] / 257;
|
|
@@ -5926,6 +6128,32 @@ function migrateToLayeredState(data, size) {
|
|
|
5926
6128
|
var DEFAULT_TRANSFORM = DEFAULT_TRANSFORM_VALUES;
|
|
5927
6129
|
var DEFAULT_DEPTH_MASK = DEFAULT_DEPTH_MASK_VALUES;
|
|
5928
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
|
+
}
|
|
5929
6157
|
var ImageEditorController = class {
|
|
5930
6158
|
state;
|
|
5931
6159
|
renderer;
|
|
@@ -6013,6 +6241,7 @@ var ImageEditorController = class {
|
|
|
6013
6241
|
if (options.initialState?.selection) {
|
|
6014
6242
|
try {
|
|
6015
6243
|
this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));
|
|
6244
|
+
if (this.state.selection) this.state.selectionRecipe = readStoredRecipe(options.initialState.selectionRecipe);
|
|
6016
6245
|
} catch {
|
|
6017
6246
|
}
|
|
6018
6247
|
}
|
|
@@ -6492,6 +6721,7 @@ var ImageEditorController = class {
|
|
|
6492
6721
|
transform: { ...s.transform },
|
|
6493
6722
|
outputOpacity: s.outputOpacity,
|
|
6494
6723
|
selection: s.selection ?? null,
|
|
6724
|
+
...s.selectionRecipe ? { selectionRecipe: s.selectionRecipe } : {},
|
|
6495
6725
|
...s.channels?.length ? { channels: s.channels } : {},
|
|
6496
6726
|
...s.paths?.length ? { paths: s.paths } : {},
|
|
6497
6727
|
source: {
|
|
@@ -6529,6 +6759,7 @@ var ImageEditorController = class {
|
|
|
6529
6759
|
if (entry.colorMode) s.colorMode = entry.colorMode;
|
|
6530
6760
|
else delete s.colorMode;
|
|
6531
6761
|
s.selection = entry.selection;
|
|
6762
|
+
s.selectionRecipe = entry.selectionRecipe ?? null;
|
|
6532
6763
|
if (entry.channels?.length) s.channels = entry.channels;
|
|
6533
6764
|
else delete s.channels;
|
|
6534
6765
|
if (entry.paths?.length) s.paths = entry.paths;
|
|
@@ -6796,7 +7027,7 @@ var ImageEditorController = class {
|
|
|
6796
7027
|
getLayerFrame(id) {
|
|
6797
7028
|
const layer = this.getLayer(id);
|
|
6798
7029
|
if (!layer) return null;
|
|
6799
|
-
const
|
|
7030
|
+
const rect3 = (b2) => [
|
|
6800
7031
|
{ x: b2.x, y: b2.y },
|
|
6801
7032
|
{ x: b2.x + b2.width, y: b2.y },
|
|
6802
7033
|
{ x: b2.x + b2.width, y: b2.y + b2.height },
|
|
@@ -6811,16 +7042,16 @@ var ImageEditorController = class {
|
|
|
6811
7042
|
}
|
|
6812
7043
|
if (layer.type === "shape") {
|
|
6813
7044
|
const g = shapeGeometryBounds(layer);
|
|
6814
|
-
return g ?
|
|
7045
|
+
return g ? rect3(g) : null;
|
|
6815
7046
|
}
|
|
6816
7047
|
const b = this.getLayerBounds(id);
|
|
6817
|
-
return b ?
|
|
7048
|
+
return b ? rect3(b) : null;
|
|
6818
7049
|
}
|
|
6819
7050
|
/**
|
|
6820
7051
|
* Hit-tests the layer stack at `point` (in document space) and returns the id of the topmost
|
|
6821
7052
|
* matching visible layer, or `null`. Skips adjustment layers.
|
|
6822
7053
|
*/
|
|
6823
|
-
layerAt(
|
|
7054
|
+
layerAt(point2, opts = {}) {
|
|
6824
7055
|
const { includeHidden = false, includeLocked = false, alphaThreshold = 1 } = opts;
|
|
6825
7056
|
const layers = this.state.layers;
|
|
6826
7057
|
const testLayer = (layer) => {
|
|
@@ -6837,20 +7068,20 @@ var ImageEditorController = class {
|
|
|
6837
7068
|
return null;
|
|
6838
7069
|
}
|
|
6839
7070
|
if (layer.type === "type") {
|
|
6840
|
-
const hitId = typeLayerAt(this,
|
|
7071
|
+
const hitId = typeLayerAt(this, point2);
|
|
6841
7072
|
if (hitId === layer.id) return layer.id;
|
|
6842
7073
|
return null;
|
|
6843
7074
|
}
|
|
6844
7075
|
if (layer.type === "annotation") {
|
|
6845
|
-
if (hitTestAnnotation(
|
|
7076
|
+
if (hitTestAnnotation(point2, layer.annotation)) return layer.id;
|
|
6846
7077
|
return null;
|
|
6847
7078
|
}
|
|
6848
7079
|
if (layer.type === "shape") {
|
|
6849
7080
|
const bounds = this.getLayerBounds(layer.id);
|
|
6850
|
-
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) {
|
|
6851
7082
|
const surface = this.surfaceOf(layer);
|
|
6852
|
-
const px2 = Math.floor(
|
|
6853
|
-
const py = Math.floor(
|
|
7083
|
+
const px2 = Math.floor(point2.x - layer.offset.x);
|
|
7084
|
+
const py = Math.floor(point2.y - layer.offset.y);
|
|
6854
7085
|
const pixel = readPixel(surface, px2, py);
|
|
6855
7086
|
if (pixel.length >= 4 && pixel[3] >= alphaThreshold) return layer.id;
|
|
6856
7087
|
}
|
|
@@ -6859,12 +7090,12 @@ var ImageEditorController = class {
|
|
|
6859
7090
|
if (layer.type === "pixel" || layer.type === "smart") {
|
|
6860
7091
|
const bounds = this.getLayerBounds(layer.id);
|
|
6861
7092
|
if (!bounds) return null;
|
|
6862
|
-
if (
|
|
7093
|
+
if (point2.x < bounds.x || point2.x > bounds.x + bounds.width || point2.y < bounds.y || point2.y > bounds.y + bounds.height) {
|
|
6863
7094
|
return null;
|
|
6864
7095
|
}
|
|
6865
7096
|
const surface = this.surfaceOf(layer);
|
|
6866
|
-
const px2 = Math.floor(
|
|
6867
|
-
const py = Math.floor(
|
|
7097
|
+
const px2 = Math.floor(point2.x - layer.offset.x);
|
|
7098
|
+
const py = Math.floor(point2.y - layer.offset.y);
|
|
6868
7099
|
const pixel = readPixel(surface, px2, py);
|
|
6869
7100
|
if (pixel.length >= 4 && pixel[3] >= alphaThreshold) {
|
|
6870
7101
|
return layer.id;
|
|
@@ -6895,9 +7126,9 @@ var ImageEditorController = class {
|
|
|
6895
7126
|
* and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and
|
|
6896
7127
|
* output opacity are not applied. With `layerIds`, only those layers are composited.
|
|
6897
7128
|
*/
|
|
6898
|
-
compositePixels(
|
|
7129
|
+
compositePixels(rect3, options = {}) {
|
|
6899
7130
|
const { width, height } = this.naturalSize;
|
|
6900
|
-
const r =
|
|
7131
|
+
const r = rect3 ?? { x: 0, y: 0, width, height };
|
|
6901
7132
|
let layers = this.state.layers;
|
|
6902
7133
|
if (options.layerIds) {
|
|
6903
7134
|
const keep = new Set(options.layerIds);
|
|
@@ -6931,6 +7162,8 @@ var ImageEditorController = class {
|
|
|
6931
7162
|
return String(id);
|
|
6932
7163
|
}
|
|
6933
7164
|
/** The pixels a pixel layer, smart object or shape composites (the decoded source for the Background). */
|
|
7165
|
+
/** A layer's real pixels for hybrid vector masks' matte guide (the Background's from the source). */
|
|
7166
|
+
guideSurfaceOf = (layer) => layer.type === "pixel" || layer.type === "smart" || layer.type === "shape" || layer.type === "type" ? this.surfaceOf(layer) : void 0;
|
|
6934
7167
|
surfaceOf(layer) {
|
|
6935
7168
|
if (layer.type !== "pixel" || layer.source !== "document") return layer.surface;
|
|
6936
7169
|
return this.sourceSurface() ?? layer.surface;
|
|
@@ -7149,7 +7382,7 @@ var ImageEditorController = class {
|
|
|
7149
7382
|
/** Whether the layer's mask (painted and vector, with feather) has a soft band. */
|
|
7150
7383
|
maskHasSoftBand(layer) {
|
|
7151
7384
|
const { width, height } = this.naturalSize;
|
|
7152
|
-
const mask = effectiveLayerMask(layer, width, height);
|
|
7385
|
+
const mask = effectiveLayerMask(layer, width, height, this.guideSurfaceOf);
|
|
7153
7386
|
if (!mask || mask.enabled === false || mask.source) return false;
|
|
7154
7387
|
const ox = layer.type === "pixel" ? layer.offset.x : 0;
|
|
7155
7388
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
@@ -7523,7 +7756,11 @@ var ImageEditorController = class {
|
|
|
7523
7756
|
this.commitLayers(layers, void 0, commitToHistory);
|
|
7524
7757
|
} else {
|
|
7525
7758
|
const { vm } = this.requireVectorMask(target.layerId);
|
|
7526
|
-
|
|
7759
|
+
const edges = validVectorMaskEdges(vm.edges, next);
|
|
7760
|
+
const nextVm = { ...vm, components: next };
|
|
7761
|
+
if (edges.length) nextVm.edges = edges;
|
|
7762
|
+
else delete nextVm.edges;
|
|
7763
|
+
this.commitLayers(mapLayer(this.state.layers, target.layerId, (l) => ({ ...l, vectorMask: nextVm })), void 0, commitToHistory);
|
|
7527
7764
|
}
|
|
7528
7765
|
}
|
|
7529
7766
|
/**
|
|
@@ -7618,6 +7855,9 @@ var ImageEditorController = class {
|
|
|
7618
7855
|
setVectorMaskPath(id, path) {
|
|
7619
7856
|
const { vm } = this.requireVectorMask(id);
|
|
7620
7857
|
const next = { ...vm, components: this.vectorMaskComponents(path) };
|
|
7858
|
+
const kept = validVectorMaskEdges(vm.edges, next.components);
|
|
7859
|
+
if (kept.length) next.edges = kept;
|
|
7860
|
+
else delete next.edges;
|
|
7621
7861
|
if (path.fillRule === "evenodd") next.fillRule = "evenodd";
|
|
7622
7862
|
else if (path.fillRule === "nonzero") delete next.fillRule;
|
|
7623
7863
|
this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: next })));
|
|
@@ -7667,7 +7907,7 @@ var ImageEditorController = class {
|
|
|
7667
7907
|
const { width, height } = this.canvasSize;
|
|
7668
7908
|
const raster = layer.mask;
|
|
7669
7909
|
const plain3 = { ...layer, vectorMask: { ...layer.vectorMask, enabled: true }, mask: raster ? { surface: raster.surface, defaultValue: raster.defaultValue } : void 0 };
|
|
7670
|
-
const baked = effectiveLayerMask(plain3, width, height);
|
|
7910
|
+
const baked = effectiveLayerMask(plain3, width, height, this.guideSurfaceOf);
|
|
7671
7911
|
const mask = raster ? { ...raster, surface: baked.surface, defaultValue: baked.defaultValue } : baked;
|
|
7672
7912
|
this.commitLayers(
|
|
7673
7913
|
mapLayer(this.state.layers, id, (l) => {
|
|
@@ -7703,8 +7943,55 @@ var ImageEditorController = class {
|
|
|
7703
7943
|
density: vm.density ?? 1,
|
|
7704
7944
|
feather: vm.feather ?? 0,
|
|
7705
7945
|
bounds: b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null,
|
|
7706
|
-
components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) }))
|
|
7946
|
+
components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) })),
|
|
7947
|
+
edges: validVectorMaskEdges(vm.edges, vm.components).map((e) => ({ ...e })),
|
|
7948
|
+
segmentCount: vectorMaskSegments(vm.components).length
|
|
7949
|
+
};
|
|
7950
|
+
}
|
|
7951
|
+
/**
|
|
7952
|
+
* Hybrid mask: a vector mask segment's edge mode. `hard` (the default) is the path's own sharp
|
|
7953
|
+
* edge; `feather` blurs across it by `radius` px (default 4); `matte` solves a corridor from
|
|
7954
|
+
* `inner` px inside to `outer` px outside (defaults 4 and 8) from the layer's colours, for hair
|
|
7955
|
+
* and fur. Modes blend where they meet. One undo step. Returns the vector mask's info.
|
|
7956
|
+
*/
|
|
7957
|
+
setVectorMaskEdge(id, at, mode, options = {}, commitToHistory = true) {
|
|
7958
|
+
const { layer, vm } = this.requireVectorMask(id);
|
|
7959
|
+
const name = layer.name || id;
|
|
7960
|
+
const sp = vm.components[at.component]?.subpaths[at.subpath];
|
|
7961
|
+
const count = sp ? subpathSegmentCount(sp) : 0;
|
|
7962
|
+
if (!sp || !Number.isInteger(at.segment) || at.segment < 0 || at.segment >= count) {
|
|
7963
|
+
const where = vm.components.map((c, ci) => c.subpaths.map((p, si) => `component ${ci} subpath ${si}: segments 0..${subpathSegmentCount(p) - 1}`).join("; ")).join("; ");
|
|
7964
|
+
throw new LayerOperationError(`The vector mask of "${name}" has no segment ${at.component}/${at.subpath}/${at.segment}; it has ${where || "no segments"} (see get_vector_mask).`);
|
|
7965
|
+
}
|
|
7966
|
+
if (mode !== "hard" && mode !== "feather" && mode !== "matte") throw new LayerOperationError(`mode must be hard, feather or matte, got "${String(mode)}".`);
|
|
7967
|
+
const num3 = (v, what, lo, hi) => {
|
|
7968
|
+
if (v === void 0) return void 0;
|
|
7969
|
+
if (!(typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi)) throw new LayerOperationError(`${what} must be ${lo} to ${hi} px, got ${String(v)}.`);
|
|
7970
|
+
return v;
|
|
7707
7971
|
};
|
|
7972
|
+
const radius = num3(options.radius, "radius", 0.5, 250);
|
|
7973
|
+
const inner = num3(options.inner, "inner", 0.5, 200);
|
|
7974
|
+
const outer = num3(options.outer, "outer", 0.5, 200);
|
|
7975
|
+
if (mode !== "feather" && radius !== void 0) throw new LayerOperationError("radius is for feather edges; matte edges take inner and outer.");
|
|
7976
|
+
if (mode !== "matte" && (inner !== void 0 || outer !== void 0)) throw new LayerOperationError("inner and outer are for matte edges; feather edges take radius.");
|
|
7977
|
+
const same = (e) => e.component === at.component && e.subpath === at.subpath && e.segment === at.segment;
|
|
7978
|
+
const edges = validVectorMaskEdges(vm.edges, vm.components).filter((e) => !same(e));
|
|
7979
|
+
if (mode === "feather") edges.push({ ...at, mode, radius: radius ?? VECTOR_MASK_EDGE_DEFAULTS.radius });
|
|
7980
|
+
else if (mode === "matte") edges.push({ ...at, mode, inner: inner ?? VECTOR_MASK_EDGE_DEFAULTS.inner, outer: outer ?? VECTOR_MASK_EDGE_DEFAULTS.outer });
|
|
7981
|
+
const next = { ...vm };
|
|
7982
|
+
if (edges.length) next.edges = edges;
|
|
7983
|
+
else delete next.edges;
|
|
7984
|
+
this.commitLayers(mapLayer(this.state.layers, layer.id, (l) => ({ ...l, vectorMask: next })), void 0, commitToHistory);
|
|
7985
|
+
return this.getVectorMask(layer.id);
|
|
7986
|
+
}
|
|
7987
|
+
/** A layer's vector mask over the canvas as a selection mask, hybrid edges, feather and density included. */
|
|
7988
|
+
vectorMaskSelection(layer) {
|
|
7989
|
+
const { width, height } = this.naturalSize;
|
|
7990
|
+
const vm = layer.vectorMask;
|
|
7991
|
+
const v = vectorMaskCoverage(vm, width, height, vm.edges?.length ? layerGuide(layer, width, height, this.guideSurfaceOf) : null);
|
|
7992
|
+
const bytes = new Uint8Array(v.length);
|
|
7993
|
+
for (let i = 0; i < v.length; i++) bytes[i] = Math.round(v[i] * 255);
|
|
7994
|
+
return createSelectionMask(width, height, bytes);
|
|
7708
7995
|
}
|
|
7709
7996
|
/** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
|
|
7710
7997
|
setLayerMaskProps(id, props, commitToHistory = true) {
|
|
@@ -8418,7 +8705,7 @@ var ImageEditorController = class {
|
|
|
8418
8705
|
return this.getGlobalLight();
|
|
8419
8706
|
}
|
|
8420
8707
|
/** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
|
|
8421
|
-
async compositeToSurface(layers,
|
|
8708
|
+
async compositeToSurface(layers, rect3) {
|
|
8422
8709
|
const { width, height } = this.naturalSize;
|
|
8423
8710
|
if (flattenLayers(layers).some((l) => l.type === "annotation" && l.visible) && !this.renderer.canRasterize()) {
|
|
8424
8711
|
throw new LayerOperationError("Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.");
|
|
@@ -8427,20 +8714,20 @@ var ImageEditorController = class {
|
|
|
8427
8714
|
const sources = this.renderer.compositorSources(width, height);
|
|
8428
8715
|
const depth = this.bitDepth;
|
|
8429
8716
|
if (depth !== 8) {
|
|
8430
|
-
const samples = workingToSamples(compositeDeep(layers,
|
|
8431
|
-
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);
|
|
8432
8719
|
}
|
|
8433
|
-
const data = new LayerCompositor().composite(layers,
|
|
8434
|
-
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);
|
|
8435
8722
|
}
|
|
8436
8723
|
/**
|
|
8437
8724
|
* The composite of the layer stack at the document's bit depth (as `compositePixels`, no
|
|
8438
8725
|
* canvas): 16-bit sRGB samples, or 32-bit linear float, over `rect` (default the canvas). At 8
|
|
8439
8726
|
* bits, the 8-bit composite widened to 16 bits.
|
|
8440
8727
|
*/
|
|
8441
|
-
compositeDeepPixels(
|
|
8728
|
+
compositeDeepPixels(rect3) {
|
|
8442
8729
|
const { width, height } = this.naturalSize;
|
|
8443
|
-
const r =
|
|
8730
|
+
const r = rect3 ?? { x: 0, y: 0, width, height };
|
|
8444
8731
|
const depth = this.bitDepth;
|
|
8445
8732
|
if (depth === 8) return { width: r.width, height: r.height, depth: 16, data: to16From8(this.compositePixels(r).data) };
|
|
8446
8733
|
const data = workingToSamples(compositeDeep(this.state.layers, r, this.compositorSources(), depth), depth);
|
|
@@ -8478,12 +8765,12 @@ var ImageEditorController = class {
|
|
|
8478
8765
|
if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
|
|
8479
8766
|
requireUnlocked(upper, "all", "merge it");
|
|
8480
8767
|
requireUnlocked(lower, "pixels", "merge into it");
|
|
8481
|
-
let
|
|
8768
|
+
let rect3 = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
8482
8769
|
const reach = Math.max(effectsMargin(lower.effects), upper.type === "adjustment" ? 0 : effectsMargin(upper.effects));
|
|
8483
|
-
if (reach)
|
|
8770
|
+
if (reach) rect3 = { x: rect3.x - reach, y: rect3.y - reach, width: rect3.width + 2 * reach, height: rect3.height + 2 * reach };
|
|
8484
8771
|
const base = { ...lower, blend: "normal", opacity: 1 };
|
|
8485
8772
|
delete base.clipped;
|
|
8486
|
-
const surface = await this.compositeToSurface([base, upper],
|
|
8773
|
+
const surface = await this.compositeToSurface([base, upper], rect3);
|
|
8487
8774
|
const merged = { ...lower, surface, offset: { x: 0, y: 0 } };
|
|
8488
8775
|
delete merged.mask;
|
|
8489
8776
|
delete merged.fillOpacity;
|
|
@@ -8656,9 +8943,9 @@ var ImageEditorController = class {
|
|
|
8656
8943
|
* Sets (Alt-click) or clears the Clone Stamp and Healing Brush source point, in document
|
|
8657
8944
|
* pixels. The next stroke fixes the offset from its first point. View state, no undo.
|
|
8658
8945
|
*/
|
|
8659
|
-
setCloneSource(
|
|
8660
|
-
if (
|
|
8661
|
-
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;
|
|
8662
8949
|
this.state.paint = { ...this.state.paint, cloneSource: source };
|
|
8663
8950
|
this.notify();
|
|
8664
8951
|
return source;
|
|
@@ -9226,27 +9513,27 @@ var ImageEditorController = class {
|
|
|
9226
9513
|
copyPixels(options = {}) {
|
|
9227
9514
|
const { width, height } = this.naturalSize;
|
|
9228
9515
|
const sel = this.state.selection?.bounds ? this.state.selection : null;
|
|
9229
|
-
const
|
|
9516
|
+
const rect3 = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
9230
9517
|
let data;
|
|
9231
9518
|
if (options.merged) {
|
|
9232
|
-
data = new Uint8ClampedArray(this.compositePixels(
|
|
9519
|
+
data = new Uint8ClampedArray(this.compositePixels(rect3).data);
|
|
9233
9520
|
} else {
|
|
9234
9521
|
const id = options.layerId ?? this.state.activeLayerId;
|
|
9235
9522
|
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
|
|
9236
9523
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
9237
9524
|
if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is not a pixel layer; copy merged, or rasterize it first.`);
|
|
9238
|
-
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 });
|
|
9239
9526
|
}
|
|
9240
9527
|
if (sel) {
|
|
9241
|
-
for (let y = 0; y <
|
|
9242
|
-
for (let x = 0; x <
|
|
9243
|
-
const k = sel.data[(
|
|
9244
|
-
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;
|
|
9245
9532
|
data[p] = Math.round(data[p] * k / 255);
|
|
9246
9533
|
}
|
|
9247
9534
|
}
|
|
9248
9535
|
}
|
|
9249
|
-
this.clipboard = { width:
|
|
9536
|
+
this.clipboard = { width: rect3.width, height: rect3.height, x: rect3.x, y: rect3.y, data };
|
|
9250
9537
|
return this.clipboard;
|
|
9251
9538
|
}
|
|
9252
9539
|
/** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
|
|
@@ -9671,6 +9958,8 @@ var ImageEditorController = class {
|
|
|
9671
9958
|
* Returns false when nothing changed.
|
|
9672
9959
|
*/
|
|
9673
9960
|
setSelection(mask, mode = "replace") {
|
|
9961
|
+
const step = this.pendingStep;
|
|
9962
|
+
this.pendingStep = null;
|
|
9674
9963
|
const { width, height } = this.naturalSize;
|
|
9675
9964
|
let next = null;
|
|
9676
9965
|
if (mask) {
|
|
@@ -9684,9 +9973,91 @@ var ImageEditorController = class {
|
|
|
9684
9973
|
this.recordHistory();
|
|
9685
9974
|
if (!next && prev?.bounds) this.lastSelection = prev;
|
|
9686
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;
|
|
9687
10053
|
this.notify();
|
|
9688
10054
|
return true;
|
|
9689
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
|
+
}
|
|
9690
10061
|
/**
|
|
9691
10062
|
* Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
|
|
9692
10063
|
* moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo
|
|
@@ -9701,7 +10072,7 @@ var ImageEditorController = class {
|
|
|
9701
10072
|
/** Select › All. */
|
|
9702
10073
|
selectAll() {
|
|
9703
10074
|
const { width, height } = this.naturalSize;
|
|
9704
|
-
return this.setSelection(fullSelection(width, height));
|
|
10075
|
+
return this.withRecipeStep({ op: "all" }, () => this.setSelection(fullSelection(width, height)));
|
|
9705
10076
|
}
|
|
9706
10077
|
/** Select › Deselect. */
|
|
9707
10078
|
deselect() {
|
|
@@ -9730,8 +10101,8 @@ var ImageEditorController = class {
|
|
|
9730
10101
|
if (!Number.isFinite(position) || at < 0 || at >= max) {
|
|
9731
10102
|
throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
|
|
9732
10103
|
}
|
|
9733
|
-
const
|
|
9734
|
-
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);
|
|
9735
10106
|
}
|
|
9736
10107
|
/** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
|
|
9737
10108
|
async growSelection(options = {}) {
|
|
@@ -9779,7 +10150,7 @@ var ImageEditorController = class {
|
|
|
9779
10150
|
invertSelection() {
|
|
9780
10151
|
const { width, height } = this.naturalSize;
|
|
9781
10152
|
const current = this.state.selection ?? createSelectionMask(width, height);
|
|
9782
|
-
return this.setSelection(invertSelection(current));
|
|
10153
|
+
return this.withRecipeStep({ op: "invert" }, () => this.setSelection(invertSelection(current)));
|
|
9783
10154
|
}
|
|
9784
10155
|
shapeCommand(mask, options) {
|
|
9785
10156
|
const feathered = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
|
|
@@ -9787,19 +10158,20 @@ var ImageEditorController = class {
|
|
|
9787
10158
|
return this.setSelection(shaped, options.mode ?? "replace");
|
|
9788
10159
|
}
|
|
9789
10160
|
/** Rectangular marquee in natural image pixels. */
|
|
9790
|
-
selectRect(
|
|
10161
|
+
selectRect(rect3, options = {}) {
|
|
9791
10162
|
const { width, height } = this.naturalSize;
|
|
9792
|
-
return this.shapeCommand(rectSelection(width, height,
|
|
10163
|
+
return this.withRecipeStep({ op: "rect", rect: { ...rect3 }, ...shapeStepFields(options) }, () => this.shapeCommand(rectSelection(width, height, rect3, options), options));
|
|
9793
10164
|
}
|
|
9794
10165
|
/** Elliptical marquee inscribed in `rect`. */
|
|
9795
|
-
selectEllipse(
|
|
10166
|
+
selectEllipse(rect3, options = {}) {
|
|
9796
10167
|
const { width, height } = this.naturalSize;
|
|
9797
|
-
return this.shapeCommand(ellipseSelection(width, height,
|
|
10168
|
+
return this.withRecipeStep({ op: "ellipse", rect: { ...rect3 }, ...shapeStepFields(options) }, () => this.shapeCommand(ellipseSelection(width, height, rect3, options), options));
|
|
9798
10169
|
}
|
|
9799
10170
|
/** Lasso / polygonal lasso. */
|
|
9800
10171
|
selectPolygon(points, options = {}) {
|
|
9801
10172
|
const { width, height } = this.naturalSize;
|
|
9802
|
-
|
|
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));
|
|
9803
10175
|
}
|
|
9804
10176
|
/**
|
|
9805
10177
|
* Magnetic Lasso for a given outline: each point snaps to the strongest edge within `width` px
|
|
@@ -10023,15 +10395,17 @@ var ImageEditorController = class {
|
|
|
10023
10395
|
* Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when
|
|
10024
10396
|
* nothing is selected).
|
|
10025
10397
|
*/
|
|
10026
|
-
async magicWand(
|
|
10398
|
+
async magicWand(point2, options = {}) {
|
|
10027
10399
|
const pixels = await this.requirePixels(options);
|
|
10028
|
-
|
|
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));
|
|
10029
10402
|
return this.getSelection();
|
|
10030
10403
|
}
|
|
10031
10404
|
/** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */
|
|
10032
10405
|
async selectColorRange(samples, options = {}) {
|
|
10033
10406
|
const pixels = await this.requirePixels(options);
|
|
10034
|
-
|
|
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));
|
|
10035
10409
|
return this.getSelection();
|
|
10036
10410
|
}
|
|
10037
10411
|
/**
|
|
@@ -10040,14 +10414,15 @@ var ImageEditorController = class {
|
|
|
10040
10414
|
*/
|
|
10041
10415
|
async selectColorRangePreset(preset, options = {}) {
|
|
10042
10416
|
const pixels = await this.requirePixels(options);
|
|
10043
|
-
|
|
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));
|
|
10044
10419
|
return this.getSelection();
|
|
10045
10420
|
}
|
|
10046
10421
|
/** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
|
|
10047
10422
|
modifySelection(op, radius) {
|
|
10048
10423
|
const sel = this.state.selection;
|
|
10049
10424
|
if (!sel?.bounds) return false;
|
|
10050
|
-
return this.setSelection(modifySelection(sel, op, radius));
|
|
10425
|
+
return this.withRecipeStep({ op: "modify", modify: op, radius }, () => this.setSelection(modifySelection(sel, op, radius)));
|
|
10051
10426
|
}
|
|
10052
10427
|
/**
|
|
10053
10428
|
* Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)
|
|
@@ -10057,7 +10432,7 @@ var ImageEditorController = class {
|
|
|
10057
10432
|
const sel = this.state.selection;
|
|
10058
10433
|
if (!sel?.bounds) return false;
|
|
10059
10434
|
const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
|
|
10060
|
-
return this.setSelection(refineSelection(pixels, sel, params));
|
|
10435
|
+
return this.withRecipeStep({ op: "refine", params: { ...params }, ...samplingFields(options) }, () => this.setSelection(refineSelection(pixels, sel, params)));
|
|
10061
10436
|
}
|
|
10062
10437
|
/**
|
|
10063
10438
|
* Select and Mask's output: refines the selection with `params`, then puts the result where
|
|
@@ -10127,7 +10502,9 @@ var ImageEditorController = class {
|
|
|
10127
10502
|
return { output, layerId, refineMode, ...documentId ? { documentId } : {} };
|
|
10128
10503
|
}
|
|
10129
10504
|
return this.asOneStep(() => {
|
|
10130
|
-
|
|
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);
|
|
10131
10508
|
if (output === "selection") return { output, refineMode };
|
|
10132
10509
|
if (output === "layer-mask") {
|
|
10133
10510
|
this.addLayerMask(layerId, "selection");
|
|
@@ -10546,9 +10923,8 @@ var ImageEditorController = class {
|
|
|
10546
10923
|
mask = this.layerMaskCoverage(layer.id);
|
|
10547
10924
|
if (!mask) throw new LayerOperationError(`Layer "${layer.name || layer.id}" has no painted mask; add one with add_layer_mask, or inspect its vector-mask.`);
|
|
10548
10925
|
} else {
|
|
10549
|
-
|
|
10550
|
-
mask =
|
|
10551
|
-
if (vm.inverted) mask = invertSelection(mask);
|
|
10926
|
+
this.requireVectorMask(layer.id);
|
|
10927
|
+
mask = this.vectorMaskSelection(layer);
|
|
10552
10928
|
}
|
|
10553
10929
|
if (!mask.bounds) throw new LayerOperationError(`The ${target} of "${layer.name || layer.id}" hides everything: there is no edge to inspect.`);
|
|
10554
10930
|
} else {
|
|
@@ -10622,9 +10998,8 @@ var ImageEditorController = class {
|
|
|
10622
10998
|
case "vector-mask": {
|
|
10623
10999
|
const layer = this.getLayer(source.layerId);
|
|
10624
11000
|
if (!layer) throw new LayerOperationError(`There is no layer "${source.layerId}" (see list_layers).`);
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
return vm.inverted ? invertSelection(mask) : mask;
|
|
11001
|
+
this.requireVectorMask(layer.id);
|
|
11002
|
+
return this.vectorMaskSelection(layer);
|
|
10628
11003
|
}
|
|
10629
11004
|
default:
|
|
10630
11005
|
throw new LayerOperationError(`Unknown selection source ${JSON.stringify(source)}. Use selection, history, channel, layer-mask or vector-mask.`);
|
|
@@ -11149,15 +11524,15 @@ var ImageEditorController = class {
|
|
|
11149
11524
|
let mask;
|
|
11150
11525
|
if (!b || x1 <= x0 || y1 <= y0) mask = createSelectionMask(width, height);
|
|
11151
11526
|
else {
|
|
11152
|
-
const
|
|
11153
|
-
const rgba = readRegion(layer.surface,
|
|
11154
|
-
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);
|
|
11155
11530
|
const hard = init.antiAlias === false;
|
|
11156
11531
|
for (let i = 0; i < bytes.length; i++) {
|
|
11157
11532
|
const a = rgba[i * 4 + 3];
|
|
11158
11533
|
bytes[i] = hard ? a >= 128 ? 255 : 0 : a;
|
|
11159
11534
|
}
|
|
11160
|
-
mask = selectionFromRegion(width, height,
|
|
11535
|
+
mask = selectionFromRegion(width, height, rect3, bytes);
|
|
11161
11536
|
}
|
|
11162
11537
|
if ((init.feather ?? 0) > 0 && mask.bounds) mask = modifySelection(mask, "feather", init.feather);
|
|
11163
11538
|
return this.setSelection(mask.bounds ? mask : null, init.mode ?? "replace");
|
|
@@ -11335,10 +11710,10 @@ var ImageEditorController = class {
|
|
|
11335
11710
|
return shaper ? layoutText(layer, shaper) : null;
|
|
11336
11711
|
}
|
|
11337
11712
|
/** The text index nearest a document point (for clicks on a type layer). */
|
|
11338
|
-
textIndexAt(id,
|
|
11713
|
+
textIndexAt(id, point2) {
|
|
11339
11714
|
const layer = requireTypeLayer(this.state.layers, id);
|
|
11340
11715
|
const layout = this.getTextLayout(id);
|
|
11341
|
-
return layout ? textIndexAt(layout, toTextSpace(layer,
|
|
11716
|
+
return layout ? textIndexAt(layout, toTextSpace(layer, point2)) : layer.text.length;
|
|
11342
11717
|
}
|
|
11343
11718
|
/** The caret at text index `index` as a document-space segment (top to bottom), for drawing it. */
|
|
11344
11719
|
textCaret(id, index) {
|
|
@@ -11516,8 +11891,8 @@ var ImageEditorController = class {
|
|
|
11516
11891
|
delete content.mask;
|
|
11517
11892
|
delete content.clipped;
|
|
11518
11893
|
delete content.fillOpacity;
|
|
11519
|
-
const
|
|
11520
|
-
const surface = await this.compositeToSurface([content],
|
|
11894
|
+
const rect3 = spatial ? canvas : this.getLayerBounds(id) ?? canvas;
|
|
11895
|
+
const surface = await this.compositeToSurface([content], rect3);
|
|
11521
11896
|
next = { ...layer, surface, offset: { x: 0, y: 0 } };
|
|
11522
11897
|
delete next.filters;
|
|
11523
11898
|
delete next.source;
|
|
@@ -11905,6 +12280,12 @@ var ImageEditorController = class {
|
|
|
11905
12280
|
if (layer.edge?.ambient) add2("The live edge cast (set_edge_ambient) has no PSD equivalent and was left out", layer.name);
|
|
11906
12281
|
const p = await prepareLayer(layer);
|
|
11907
12282
|
if (p?.edge) delete p.edge;
|
|
12283
|
+
if (p && isActiveVectorMask(layer.vectorMask) && layer.vectorMask.edges?.length) {
|
|
12284
|
+
add2("Hybrid vector masks (soft edge segments) were baked into pixel masks", layer.name);
|
|
12285
|
+
const baked = effectiveLayerMask(layer, width, height, this.guideSurfaceOf);
|
|
12286
|
+
if (baked) p.mask = baked;
|
|
12287
|
+
delete p.vectorMask;
|
|
12288
|
+
}
|
|
11908
12289
|
if (p?.blend === "pigment") add2("The Pigment blend mode has no PSD equivalent and was written as Normal", layer.name);
|
|
11909
12290
|
return p;
|
|
11910
12291
|
};
|
|
@@ -11980,7 +12361,7 @@ var ImageEditorController = class {
|
|
|
11980
12361
|
}
|
|
11981
12362
|
if (layer.filters?.some((f) => !f.enabled)) add2("Disabled layer filters were left out", layer.name);
|
|
11982
12363
|
delete out.filters;
|
|
11983
|
-
const unmixMask = hasEdgeUnmix(layer.edge) ? effectiveLayerMask(layer, width, height) : void 0;
|
|
12364
|
+
const unmixMask = hasEdgeUnmix(layer.edge) ? effectiveLayerMask(layer, width, height, this.guideSurfaceOf) : void 0;
|
|
11984
12365
|
if (unmixMask && !unmixMask.source && unmixMask.enabled !== false) {
|
|
11985
12366
|
add2("Edge decontamination and despill were baked into the pixels", layer.name);
|
|
11986
12367
|
const { x: ox, y: oy } = layer.offset;
|
|
@@ -12089,6 +12470,7 @@ var ImageEditorController = class {
|
|
|
12089
12470
|
try {
|
|
12090
12471
|
out = this.serializeState();
|
|
12091
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));
|
|
12092
12474
|
if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
|
|
12093
12475
|
if (this.state.paths?.length) out.paths = serializePaths(this.state.paths);
|
|
12094
12476
|
} finally {
|
|
@@ -12130,6 +12512,7 @@ var ImageEditorController = class {
|
|
|
12130
12512
|
}
|
|
12131
12513
|
}
|
|
12132
12514
|
this.state.selection = selection;
|
|
12515
|
+
this.state.selectionRecipe = selection ? readStoredRecipe(data.selectionRecipe) : null;
|
|
12133
12516
|
this.dropMismatchedSelection();
|
|
12134
12517
|
const problems = [];
|
|
12135
12518
|
const channels = deserializeChannels(data.channels, this.naturalSize, problems);
|
|
@@ -13360,10 +13743,10 @@ function sampleOptions(args) {
|
|
|
13360
13743
|
return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
|
|
13361
13744
|
}
|
|
13362
13745
|
function rectOf(args) {
|
|
13363
|
-
const
|
|
13364
|
-
if (![
|
|
13365
|
-
if (
|
|
13366
|
-
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;
|
|
13367
13750
|
}
|
|
13368
13751
|
function uncertainty(controller) {
|
|
13369
13752
|
const u = controller.selectionUncertainty();
|
|
@@ -13477,6 +13860,83 @@ function createImageSelectionTools(controller) {
|
|
|
13477
13860
|
show: show !== false
|
|
13478
13861
|
})
|
|
13479
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
|
+
};
|
|
13480
13940
|
const selectRegion = {
|
|
13481
13941
|
name: "select_region",
|
|
13482
13942
|
title: "Select a region",
|
|
@@ -13521,12 +13981,12 @@ function createImageSelectionTools(controller) {
|
|
|
13521
13981
|
break;
|
|
13522
13982
|
case "rect":
|
|
13523
13983
|
case "ellipse": {
|
|
13524
|
-
const
|
|
13525
|
-
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)) {
|
|
13526
13986
|
throw new Error("rect and ellipse need numeric x, y, width and height.");
|
|
13527
13987
|
}
|
|
13528
|
-
if (shape === "rect") controller.selectRect(
|
|
13529
|
-
else controller.selectEllipse(
|
|
13988
|
+
if (shape === "rect") controller.selectRect(rect3, opts);
|
|
13989
|
+
else controller.selectEllipse(rect3, opts);
|
|
13530
13990
|
break;
|
|
13531
13991
|
}
|
|
13532
13992
|
case "polygon": {
|
|
@@ -13905,10 +14365,10 @@ function createImageSelectionTools(controller) {
|
|
|
13905
14365
|
},
|
|
13906
14366
|
annotations: { ...writeAnnotations("Object Selection"), openWorldHint: true },
|
|
13907
14367
|
handler: async (args, context) => {
|
|
13908
|
-
const
|
|
13909
|
-
const
|
|
13910
|
-
if (!
|
|
13911
|
-
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.");
|
|
13912
14372
|
const options = {
|
|
13913
14373
|
mode: parseSelectionMode(args.mode),
|
|
13914
14374
|
sampleAllLayers: args.sampleAllLayers,
|
|
@@ -13916,7 +14376,7 @@ function createImageSelectionTools(controller) {
|
|
|
13916
14376
|
useModel: args.useModel,
|
|
13917
14377
|
signal: context?.signal
|
|
13918
14378
|
};
|
|
13919
|
-
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);
|
|
13920
14380
|
return { ...summary(controller), method, ...uncertainty(controller) };
|
|
13921
14381
|
}
|
|
13922
14382
|
};
|
|
@@ -14555,6 +15015,11 @@ function createImageSelectionTools(controller) {
|
|
|
14555
15015
|
getSelection,
|
|
14556
15016
|
inspectEdge,
|
|
14557
15017
|
compareTool,
|
|
15018
|
+
getRecipeTool,
|
|
15019
|
+
editStepTool,
|
|
15020
|
+
insertStepTool,
|
|
15021
|
+
removeStepTool,
|
|
15022
|
+
freezeTool,
|
|
14558
15023
|
selectAll,
|
|
14559
15024
|
deselect,
|
|
14560
15025
|
invert2,
|
|
@@ -14694,7 +15159,7 @@ async function renderImageRegion(controller, request) {
|
|
|
14694
15159
|
out.height = request.height;
|
|
14695
15160
|
const context = out.getContext("2d");
|
|
14696
15161
|
if (!context) throw new Error(NO_CANVAS);
|
|
14697
|
-
const { rect:
|
|
15162
|
+
const { rect: rect3 } = request;
|
|
14698
15163
|
context.imageSmoothingEnabled = request.scale < 1;
|
|
14699
15164
|
if (request.format === "jpeg") {
|
|
14700
15165
|
context.fillStyle = "#ffffff";
|
|
@@ -14702,22 +15167,22 @@ async function renderImageRegion(controller, request) {
|
|
|
14702
15167
|
}
|
|
14703
15168
|
const kx = view.width / state.imageDimensions.width;
|
|
14704
15169
|
const ky = view.height / state.imageDimensions.height;
|
|
14705
|
-
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);
|
|
14706
15171
|
if (request.includeOverlays) {
|
|
14707
15172
|
const s = request.scale;
|
|
14708
15173
|
const maskView = maskViewCanvas(controller, document);
|
|
14709
|
-
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);
|
|
14710
15175
|
const selection = state.selection;
|
|
14711
15176
|
if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {
|
|
14712
15177
|
const shade = selectionShade(selection, document);
|
|
14713
|
-
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);
|
|
14714
15179
|
}
|
|
14715
15180
|
if (state.crop) {
|
|
14716
15181
|
context.save();
|
|
14717
15182
|
context.strokeStyle = "#ffd400";
|
|
14718
15183
|
context.lineWidth = 2;
|
|
14719
15184
|
context.setLineDash([6, 4]);
|
|
14720
|
-
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);
|
|
14721
15186
|
context.restore();
|
|
14722
15187
|
}
|
|
14723
15188
|
}
|
|
@@ -15974,11 +16439,11 @@ function createImageEffectTools(controller) {
|
|
|
15974
16439
|
}
|
|
15975
16440
|
|
|
15976
16441
|
// src/image-editor/selection/gesture.ts
|
|
15977
|
-
function snapTo45(from,
|
|
15978
|
-
const dx =
|
|
15979
|
-
const dy =
|
|
16442
|
+
function snapTo45(from, point2) {
|
|
16443
|
+
const dx = point2.x - from.x;
|
|
16444
|
+
const dy = point2.y - from.y;
|
|
15980
16445
|
const len = Math.hypot(dx, dy);
|
|
15981
|
-
if (len === 0) return
|
|
16446
|
+
if (len === 0) return point2;
|
|
15982
16447
|
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
15983
16448
|
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
15984
16449
|
}
|
|
@@ -16066,38 +16531,38 @@ var SelectionGesture = class {
|
|
|
16066
16531
|
const { tool } = this.getOptions();
|
|
16067
16532
|
if (tool === "quick") return null;
|
|
16068
16533
|
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
16069
|
-
const
|
|
16534
|
+
const rect3 = normalizeRect({
|
|
16070
16535
|
x: this.start.x,
|
|
16071
16536
|
y: this.start.y,
|
|
16072
16537
|
width: this.current.x - this.start.x,
|
|
16073
16538
|
height: this.current.y - this.start.y
|
|
16074
16539
|
});
|
|
16075
|
-
if (
|
|
16076
|
-
if (tool === "ellipse") return ellipsePoints(
|
|
16540
|
+
if (rect3.width < 1 && rect3.height < 1) return null;
|
|
16541
|
+
if (tool === "ellipse") return ellipsePoints(rect3);
|
|
16077
16542
|
return [
|
|
16078
|
-
{ x:
|
|
16079
|
-
{ x:
|
|
16080
|
-
{ x:
|
|
16081
|
-
{ 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 }
|
|
16082
16547
|
];
|
|
16083
16548
|
}
|
|
16084
16549
|
/** Pointer down. Resolves once a wand click has been applied. */
|
|
16085
|
-
async begin(
|
|
16550
|
+
async begin(point2, modifiers = {}) {
|
|
16086
16551
|
const options = this.getOptions();
|
|
16087
16552
|
this.error = null;
|
|
16088
16553
|
if (this.polygon && options.tool !== "polygon" || this.magnetic && options.tool !== "magnetic") this.cancel();
|
|
16089
|
-
if (options.tool === "magnetic") return this.magneticClick(
|
|
16554
|
+
if (options.tool === "magnetic") return this.magneticClick(point2, modifiers, options);
|
|
16090
16555
|
if (options.tool === "polygon") {
|
|
16091
|
-
this.pressedAt =
|
|
16556
|
+
this.pressedAt = point2;
|
|
16092
16557
|
this.freehand = false;
|
|
16093
16558
|
if (!this.polygon) {
|
|
16094
16559
|
this.modifiers = modifiers;
|
|
16095
|
-
this.polygon = [
|
|
16096
|
-
this.current =
|
|
16560
|
+
this.polygon = [point2];
|
|
16561
|
+
this.current = point2;
|
|
16097
16562
|
return;
|
|
16098
16563
|
}
|
|
16099
16564
|
const last = this.polygon[this.polygon.length - 1];
|
|
16100
|
-
const p = modifiers.shiftKey ? snapTo45(last,
|
|
16565
|
+
const p = modifiers.shiftKey ? snapTo45(last, point2) : point2;
|
|
16101
16566
|
const first = this.polygon[0];
|
|
16102
16567
|
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
16103
16568
|
this.close();
|
|
@@ -16112,7 +16577,7 @@ var SelectionGesture = class {
|
|
|
16112
16577
|
if (options.tool === "row" || options.tool === "column") {
|
|
16113
16578
|
this.start = null;
|
|
16114
16579
|
try {
|
|
16115
|
-
this.controller.selectLine(options.tool, options.tool === "row" ?
|
|
16580
|
+
this.controller.selectLine(options.tool, options.tool === "row" ? point2.y : point2.x, {
|
|
16116
16581
|
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
16117
16582
|
feather: options.feather ?? 0
|
|
16118
16583
|
});
|
|
@@ -16126,8 +16591,8 @@ var SelectionGesture = class {
|
|
|
16126
16591
|
try {
|
|
16127
16592
|
this.controller.selectTextShape({
|
|
16128
16593
|
text: options.typeText || "Type",
|
|
16129
|
-
x:
|
|
16130
|
-
y:
|
|
16594
|
+
x: point2.x,
|
|
16595
|
+
y: point2.y,
|
|
16131
16596
|
orientation: options.tool === "type-vertical" ? "vertical" : "horizontal",
|
|
16132
16597
|
style: { fontSize: options.typeSize ?? 72, fontFamily: options.typeFamily || "sans-serif" },
|
|
16133
16598
|
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
@@ -16151,11 +16616,11 @@ var SelectionGesture = class {
|
|
|
16151
16616
|
if (options.tool === "range") {
|
|
16152
16617
|
const pixels = await this.controller.getSamplePixels(common);
|
|
16153
16618
|
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
16154
|
-
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(
|
|
16155
|
-
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)));
|
|
16156
16621
|
const p = (y * pixels.width + x) * 4;
|
|
16157
16622
|
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
16158
|
-
await this.controller.selectColorRange([{ color, at:
|
|
16623
|
+
await this.controller.selectColorRange([{ color, at: point2 }], {
|
|
16159
16624
|
...common,
|
|
16160
16625
|
fuzziness: options.fuzziness ?? 40,
|
|
16161
16626
|
localized: options.localized ? options.localized : void 0,
|
|
@@ -16163,7 +16628,7 @@ var SelectionGesture = class {
|
|
|
16163
16628
|
});
|
|
16164
16629
|
return;
|
|
16165
16630
|
}
|
|
16166
|
-
await this.controller.magicWand(
|
|
16631
|
+
await this.controller.magicWand(point2, {
|
|
16167
16632
|
...common,
|
|
16168
16633
|
tolerance: options.tolerance,
|
|
16169
16634
|
contiguous: options.contiguous,
|
|
@@ -16175,9 +16640,9 @@ var SelectionGesture = class {
|
|
|
16175
16640
|
}
|
|
16176
16641
|
return;
|
|
16177
16642
|
}
|
|
16178
|
-
this.start =
|
|
16179
|
-
this.current =
|
|
16180
|
-
this.points = [
|
|
16643
|
+
this.start = point2;
|
|
16644
|
+
this.current = point2;
|
|
16645
|
+
this.points = [point2];
|
|
16181
16646
|
if (options.tool === "quick") {
|
|
16182
16647
|
const picked = selectionModeFromModifiers(modifiers, options.mode);
|
|
16183
16648
|
const base = this.controller.getSelection();
|
|
@@ -16192,53 +16657,53 @@ var SelectionGesture = class {
|
|
|
16192
16657
|
}
|
|
16193
16658
|
}
|
|
16194
16659
|
/** Pointer move; returns true when the draft changed (re-render). */
|
|
16195
|
-
move(
|
|
16660
|
+
move(point2, modifiers = {}) {
|
|
16196
16661
|
if (this.magnetic) {
|
|
16197
|
-
this.current =
|
|
16198
|
-
this.followEdges(
|
|
16662
|
+
this.current = point2;
|
|
16663
|
+
this.followEdges(point2);
|
|
16199
16664
|
return true;
|
|
16200
16665
|
}
|
|
16201
16666
|
if (this.polygon) {
|
|
16202
16667
|
const last = this.polygon[this.polygon.length - 1];
|
|
16203
|
-
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)) {
|
|
16204
16669
|
this.freehand = true;
|
|
16205
|
-
if (Math.hypot(
|
|
16670
|
+
if (Math.hypot(point2.x - last.x, point2.y - last.y) >= 0.5) this.polygon.push(point2);
|
|
16206
16671
|
this.current = null;
|
|
16207
16672
|
return true;
|
|
16208
16673
|
}
|
|
16209
|
-
this.current = modifiers.shiftKey ? snapTo45(last,
|
|
16674
|
+
this.current = modifiers.shiftKey ? snapTo45(last, point2) : point2;
|
|
16210
16675
|
return true;
|
|
16211
16676
|
}
|
|
16212
16677
|
if (!this.start) return false;
|
|
16213
|
-
this.current =
|
|
16678
|
+
this.current = point2;
|
|
16214
16679
|
const drawn = this.getOptions().tool;
|
|
16215
16680
|
if (drawn === "lasso" || drawn === "quick") {
|
|
16216
16681
|
const last = this.points[this.points.length - 1];
|
|
16217
|
-
if (Math.hypot(
|
|
16218
|
-
this.points.push(
|
|
16682
|
+
if (Math.hypot(point2.x - last.x, point2.y - last.y) < 0.5) return false;
|
|
16683
|
+
this.points.push(point2);
|
|
16219
16684
|
if (drawn === "quick") this.growPreview();
|
|
16220
16685
|
}
|
|
16221
16686
|
return true;
|
|
16222
16687
|
}
|
|
16223
16688
|
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
16224
|
-
end(
|
|
16689
|
+
end(point2, modifiers) {
|
|
16225
16690
|
if (this.magnetic) return false;
|
|
16226
16691
|
if (this.polygon) {
|
|
16227
|
-
if (
|
|
16692
|
+
if (point2 && this.freehand) this.move(point2);
|
|
16228
16693
|
this.pressedAt = null;
|
|
16229
16694
|
this.freehand = false;
|
|
16230
16695
|
return false;
|
|
16231
16696
|
}
|
|
16232
16697
|
if (!this.start) return false;
|
|
16233
|
-
if (
|
|
16698
|
+
if (point2) this.move(point2);
|
|
16234
16699
|
const options = this.getOptions();
|
|
16235
16700
|
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
16236
16701
|
if (options.tool === "object") {
|
|
16237
|
-
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 });
|
|
16238
16703
|
this.start = null;
|
|
16239
16704
|
this.current = null;
|
|
16240
16705
|
this.points = [];
|
|
16241
|
-
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 } };
|
|
16242
16707
|
this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
|
|
16243
16708
|
() => void 0,
|
|
16244
16709
|
(err) => {
|
|
@@ -16268,7 +16733,7 @@ var SelectionGesture = class {
|
|
|
16268
16733
|
return false;
|
|
16269
16734
|
}
|
|
16270
16735
|
const path = this.draftPath;
|
|
16271
|
-
const
|
|
16736
|
+
const rect3 = normalizeRect({
|
|
16272
16737
|
x: this.start.x,
|
|
16273
16738
|
y: this.start.y,
|
|
16274
16739
|
width: this.current.x - this.start.x,
|
|
@@ -16280,7 +16745,7 @@ var SelectionGesture = class {
|
|
|
16280
16745
|
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
16281
16746
|
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
16282
16747
|
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
16283
|
-
return options.tool === "ellipse" ? this.controller.selectEllipse(
|
|
16748
|
+
return options.tool === "ellipse" ? this.controller.selectEllipse(rect3, opts) : this.controller.selectRect(rect3, opts);
|
|
16284
16749
|
}
|
|
16285
16750
|
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
16286
16751
|
close() {
|
|
@@ -16329,19 +16794,19 @@ var SelectionGesture = class {
|
|
|
16329
16794
|
corridor(a, b) {
|
|
16330
16795
|
return Math.max(2 * (this.getOptions().magneticWidth ?? 10), 16, Math.hypot(b.x - a.x, b.y - a.y) / 4);
|
|
16331
16796
|
}
|
|
16332
|
-
async magneticClick(
|
|
16797
|
+
async magneticClick(point2, modifiers, options) {
|
|
16333
16798
|
const m = this.magnetic;
|
|
16334
16799
|
if (!m) {
|
|
16335
16800
|
this.modifiers = modifiers;
|
|
16336
|
-
const draft = { fixed: [
|
|
16801
|
+
const draft = { fixed: [point2], anchors: [0], live: [], edges: null };
|
|
16337
16802
|
this.magnetic = draft;
|
|
16338
|
-
this.current =
|
|
16803
|
+
this.current = point2;
|
|
16339
16804
|
try {
|
|
16340
16805
|
const pixels = await this.controller.getSamplePixels({ sampleMerged: options.sampleMerged, sample: options.sample });
|
|
16341
16806
|
if (!pixels) throw new Error("The image pixels are not available to find edges in.");
|
|
16342
16807
|
if (this.magnetic !== draft) return;
|
|
16343
16808
|
draft.edges = new EdgeMap(pixels, { contrast: (options.magneticContrast ?? 10) / 100 });
|
|
16344
|
-
draft.fixed = [snapToEdge(draft.edges,
|
|
16809
|
+
draft.fixed = [snapToEdge(draft.edges, point2, options.magneticWidth ?? 10)];
|
|
16345
16810
|
if (this.current) this.followEdges(this.current);
|
|
16346
16811
|
} catch (err) {
|
|
16347
16812
|
this.error = err instanceof Error ? err.message : String(err);
|
|
@@ -16350,12 +16815,12 @@ var SelectionGesture = class {
|
|
|
16350
16815
|
return;
|
|
16351
16816
|
}
|
|
16352
16817
|
const first = m.fixed[0];
|
|
16353
|
-
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) {
|
|
16354
16819
|
this.close();
|
|
16355
16820
|
return;
|
|
16356
16821
|
}
|
|
16357
|
-
this.current =
|
|
16358
|
-
this.followEdges(
|
|
16822
|
+
this.current = point2;
|
|
16823
|
+
this.followEdges(point2);
|
|
16359
16824
|
if (m.live.length > 1) {
|
|
16360
16825
|
m.fixed.push(...m.live.slice(1));
|
|
16361
16826
|
m.anchors.push(m.fixed.length - 1);
|
|
@@ -16364,15 +16829,15 @@ var SelectionGesture = class {
|
|
|
16364
16829
|
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
16365
16830
|
}
|
|
16366
16831
|
/** Updates the live path to `point`, fixing anchors along it every `frequency` px (with `place`). */
|
|
16367
|
-
followEdges(
|
|
16832
|
+
followEdges(point2, place = true) {
|
|
16368
16833
|
const m = this.magnetic;
|
|
16369
16834
|
const last = () => m.fixed[m.fixed.length - 1];
|
|
16370
16835
|
if (!m.edges) {
|
|
16371
|
-
m.live = [last(),
|
|
16836
|
+
m.live = [last(), point2];
|
|
16372
16837
|
return;
|
|
16373
16838
|
}
|
|
16374
16839
|
const options = this.getOptions();
|
|
16375
|
-
const target = snapToEdge(m.edges,
|
|
16840
|
+
const target = snapToEdge(m.edges, point2, options.magneticWidth ?? 10);
|
|
16376
16841
|
const spacing = magneticAnchorSpacing(options.magneticFrequency ?? 57);
|
|
16377
16842
|
for (let guard = 0; guard < 64; guard++) {
|
|
16378
16843
|
const path = liveWire(m.edges, last(), target, this.corridor(last(), target));
|
|
@@ -18645,6 +19110,37 @@ function createImageShapeTools(controller) {
|
|
|
18645
19110
|
return { vectorMask: controller.getVectorMask(id) };
|
|
18646
19111
|
}
|
|
18647
19112
|
};
|
|
19113
|
+
const setVectorMaskEdge = {
|
|
19114
|
+
name: "set_vector_mask_edge",
|
|
19115
|
+
title: "Set a vector mask segment's edge",
|
|
19116
|
+
description: "Hybrid mask: gives one segment of a layer's vector mask its own edge, so one mask holds sharp edges and hair or fur. hard (default) is the path's sharp edge; feather blurs across it by radius px; matte solves a corridor from inner px inside the path to outer px outside it from the layer's colours (the colour-guided filter), so strands keep their detail. Modes blend where they meet. Address the segment with component, subpath and segment (the segment from anchor segment to the next; see get_vector_mask segmentCount and components). Check the result with inspect_mask_edge target vector-mask. One undo step. Returns the vector mask with its edges.",
|
|
19117
|
+
inputSchema: {
|
|
19118
|
+
type: "object",
|
|
19119
|
+
properties: {
|
|
19120
|
+
layerId: LAYER,
|
|
19121
|
+
component: { type: "integer", minimum: 0, description: "Component index of the path (0 for a single shape)." },
|
|
19122
|
+
subpath: { type: "integer", minimum: 0, description: "Subpath index in the component (0 for a single outline)." },
|
|
19123
|
+
segment: { type: "integer", minimum: 0, description: "Segment index in the subpath: from anchor segment to the next (the last closes back to anchor 0)." },
|
|
19124
|
+
mode: { type: "string", enum: ["hard", "feather", "matte"], description: "hard (sharp, the default), feather (a soft falloff) or matte (a solved corridor for hair and fur)." },
|
|
19125
|
+
radius: { type: "number", minimum: 0.5, maximum: 250, description: "feather only: Gaussian falloff in px across the curve (default 4)." },
|
|
19126
|
+
inner: { type: "number", minimum: 0.5, maximum: 200, description: "matte only: how far inside the path the unsure corridor reaches, in px (default 4)." },
|
|
19127
|
+
outer: { type: "number", minimum: 0.5, maximum: 200, description: "matte only: how far outside the path the unsure corridor reaches, in px (default 8; cover the longest strands)." }
|
|
19128
|
+
},
|
|
19129
|
+
required: ["layerId", "component", "subpath", "segment", "mode"],
|
|
19130
|
+
additionalProperties: false
|
|
19131
|
+
},
|
|
19132
|
+
annotations: { title: "Set a vector mask segment's edge", ...WRITE_IDEMPOTENT5 },
|
|
19133
|
+
handler: ({ layerId, component, subpath, segment, mode, radius, inner, outer }) => {
|
|
19134
|
+
const id = layerArg(layerId);
|
|
19135
|
+
const vectorMask = controller.setVectorMaskEdge(
|
|
19136
|
+
id,
|
|
19137
|
+
{ component, subpath, segment },
|
|
19138
|
+
mode,
|
|
19139
|
+
{ radius, inner, outer }
|
|
19140
|
+
);
|
|
19141
|
+
return { vectorMask };
|
|
19142
|
+
}
|
|
19143
|
+
};
|
|
18648
19144
|
const rasterizeVectorMask = {
|
|
18649
19145
|
name: "rasterize_vector_mask",
|
|
18650
19146
|
title: "Rasterize a vector mask",
|
|
@@ -18787,6 +19283,7 @@ function createImageShapeTools(controller) {
|
|
|
18787
19283
|
addVectorMask,
|
|
18788
19284
|
setVectorMaskPath,
|
|
18789
19285
|
setVectorMaskProps,
|
|
19286
|
+
setVectorMaskEdge,
|
|
18790
19287
|
rasterizeVectorMask,
|
|
18791
19288
|
removeVectorMask,
|
|
18792
19289
|
selectionToPath,
|
|
@@ -19313,11 +19810,11 @@ function hitTestCurvePoint(points, x, y, width, height, radius = 8) {
|
|
|
19313
19810
|
return best;
|
|
19314
19811
|
}
|
|
19315
19812
|
function insertCurvePoint(points, p) {
|
|
19316
|
-
const
|
|
19317
|
-
const out = points.filter((q) => q.input !==
|
|
19318
|
-
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);
|
|
19319
19816
|
if (index === -1) index = out.length;
|
|
19320
|
-
out.splice(index, 0,
|
|
19817
|
+
out.splice(index, 0, point2);
|
|
19321
19818
|
return { points: out, index };
|
|
19322
19819
|
}
|
|
19323
19820
|
function moveCurvePoint(points, index, p) {
|
|
@@ -19686,25 +20183,25 @@ var SelectAndMaskSession = class {
|
|
|
19686
20183
|
* Selection, brushes, lasso) or a box (Object Selection). Alt (or the subtract mode) removes.
|
|
19687
20184
|
* Hand and Zoom are the UI's own; they return false here.
|
|
19688
20185
|
*/
|
|
19689
|
-
begin(
|
|
20186
|
+
begin(point2, modifiers = {}) {
|
|
19690
20187
|
const s = this.settingsValue;
|
|
19691
20188
|
const mode = modifiers.altKey ? s.brushMode === "add" ? "subtract" : "add" : s.brushMode;
|
|
19692
|
-
this.dragStart =
|
|
20189
|
+
this.dragStart = point2;
|
|
19693
20190
|
switch (s.tool) {
|
|
19694
20191
|
case "quick-select":
|
|
19695
|
-
this.live = { kind: "quick-select", points: [
|
|
20192
|
+
this.live = { kind: "quick-select", points: [point2], size: s.brushSize, mode };
|
|
19696
20193
|
break;
|
|
19697
20194
|
case "brush":
|
|
19698
|
-
this.live = { kind: "brush", points: [
|
|
20195
|
+
this.live = { kind: "brush", points: [point2], size: s.brushSize, mode };
|
|
19699
20196
|
break;
|
|
19700
20197
|
case "refine-brush":
|
|
19701
|
-
this.live = { kind: "refine-brush", points: [
|
|
20198
|
+
this.live = { kind: "refine-brush", points: [point2], size: s.brushSize };
|
|
19702
20199
|
break;
|
|
19703
20200
|
case "lasso":
|
|
19704
|
-
this.live = { kind: "lasso", points: [
|
|
20201
|
+
this.live = { kind: "lasso", points: [point2], mode };
|
|
19705
20202
|
break;
|
|
19706
20203
|
case "object":
|
|
19707
|
-
this.live = { kind: "object", point, mode };
|
|
20204
|
+
this.live = { kind: "object", point: point2, mode };
|
|
19708
20205
|
break;
|
|
19709
20206
|
default:
|
|
19710
20207
|
this.dragStart = null;
|
|
@@ -19714,18 +20211,18 @@ var SelectAndMaskSession = class {
|
|
|
19714
20211
|
return true;
|
|
19715
20212
|
}
|
|
19716
20213
|
/** Pointer move while a stroke is down. */
|
|
19717
|
-
move(
|
|
20214
|
+
move(point2) {
|
|
19718
20215
|
const live = this.live;
|
|
19719
20216
|
if (!live || !this.dragStart) return;
|
|
19720
20217
|
if (live.kind === "object") {
|
|
19721
20218
|
const a = this.dragStart;
|
|
19722
|
-
const
|
|
19723
|
-
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 };
|
|
19724
20221
|
return;
|
|
19725
20222
|
}
|
|
19726
20223
|
const last = live.points[live.points.length - 1];
|
|
19727
|
-
if (Math.hypot(
|
|
19728
|
-
live.points.push(
|
|
20224
|
+
if (Math.hypot(point2.x - last.x, point2.y - last.y) < 1) return;
|
|
20225
|
+
live.points.push(point2);
|
|
19729
20226
|
if (this.settingsValue.realTime && live.kind !== "lasso") this.recompute();
|
|
19730
20227
|
}
|
|
19731
20228
|
/** Pointer up: the stroke becomes an edit (one step of the workspace's own undo). */
|
|
@@ -21649,6 +22146,7 @@ var PathEditGesture = class {
|
|
|
21649
22146
|
return;
|
|
21650
22147
|
}
|
|
21651
22148
|
const seg = this.segmentAt(cs, p);
|
|
22149
|
+
if (seg && mods.alt && mods.shift && this.target?.kind === "vector-mask") return this.cycleEdge(this.target.layerId, seg.ref);
|
|
21652
22150
|
if (seg) {
|
|
21653
22151
|
if (!mods.shift) this.selected.clear();
|
|
21654
22152
|
this.drag = { kind: "segment", ...seg, base: cloneComponents(cs), moved: false };
|
|
@@ -21657,6 +22155,17 @@ var PathEditGesture = class {
|
|
|
21657
22155
|
this.drag = { kind: "marquee", start: pt(p), end: pt(p), keep: mods.shift ? new Set(this.selected) : /* @__PURE__ */ new Set() };
|
|
21658
22156
|
if (!mods.shift) this.selected.clear();
|
|
21659
22157
|
}
|
|
22158
|
+
/** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
|
|
22159
|
+
cycleEdge(layerId, r) {
|
|
22160
|
+
const at = { component: r.component, subpath: r.subpath, segment: r.anchor };
|
|
22161
|
+
const now = this.controller.getVectorMask(layerId)?.edges.find((e) => e.component === at.component && e.subpath === at.subpath && e.segment === at.segment)?.mode ?? "hard";
|
|
22162
|
+
const next = now === "hard" ? "feather" : now === "feather" ? "matte" : "hard";
|
|
22163
|
+
try {
|
|
22164
|
+
this.controller.setVectorMaskEdge(layerId, at, next);
|
|
22165
|
+
} catch (e) {
|
|
22166
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
22167
|
+
}
|
|
22168
|
+
}
|
|
21660
22169
|
/** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
|
|
21661
22170
|
convert(cs, r) {
|
|
21662
22171
|
const next = cloneComponents(cs);
|
|
@@ -21745,7 +22254,7 @@ var PathEditGesture = class {
|
|
|
21745
22254
|
if (!g) return;
|
|
21746
22255
|
if (g.kind === "marquee") {
|
|
21747
22256
|
g.end = pt(p);
|
|
21748
|
-
const box =
|
|
22257
|
+
const box = rect2(g.start, g.end);
|
|
21749
22258
|
const cs = this.target ? this.controller.getPathEditComponents(this.target) : [];
|
|
21750
22259
|
this.selected = new Set(g.keep);
|
|
21751
22260
|
cs.forEach(
|
|
@@ -21852,12 +22361,18 @@ var PathEditGesture = class {
|
|
|
21852
22361
|
const cs = this.components();
|
|
21853
22362
|
if (!cs) return null;
|
|
21854
22363
|
const curves = [];
|
|
22364
|
+
const curveModes = [];
|
|
22365
|
+
const edges = this.target?.kind === "vector-mask" ? this.controller.getVectorMask(this.target.layerId)?.edges ?? [] : [];
|
|
22366
|
+
const modeOf = (ci, si, k) => edges.find((e) => e.component === ci && e.subpath === si && e.segment === k)?.mode ?? "hard";
|
|
21855
22367
|
const anchors = [];
|
|
21856
22368
|
const handles = [];
|
|
21857
22369
|
const selectedComponents = new Set(this.selection.map((r) => r.component));
|
|
21858
22370
|
cs.forEach(
|
|
21859
22371
|
(c, ci) => c.subpaths.forEach((sp, si) => {
|
|
21860
|
-
for (let i = 0; i < subpathSegmentCount(sp); i++)
|
|
22372
|
+
for (let i = 0; i < subpathSegmentCount(sp); i++) {
|
|
22373
|
+
curves.push(subpathSegment(sp, i).map(([x, y]) => ({ x, y })));
|
|
22374
|
+
curveModes.push(modeOf(ci, si, i));
|
|
22375
|
+
}
|
|
21861
22376
|
sp.anchors.forEach((a, ai) => {
|
|
21862
22377
|
const selected = this.selected.has(key({ component: ci, subpath: si, anchor: ai }));
|
|
21863
22378
|
if (tool === "path-select" && !selectedComponents.has(ci)) return;
|
|
@@ -21874,11 +22389,11 @@ var PathEditGesture = class {
|
|
|
21874
22389
|
const last = sp?.anchors[sp.anchors.length - 1];
|
|
21875
22390
|
if (last) preview = last.out ? [pt(last), pt(last.out), this.pointer, this.pointer] : [pt(last), this.pointer];
|
|
21876
22391
|
}
|
|
21877
|
-
const marquee = this.drag?.kind === "marquee" ?
|
|
21878
|
-
return { curves, anchors, handles, marquee, preview };
|
|
22392
|
+
const marquee = this.drag?.kind === "marquee" ? rect2(this.drag.start, this.drag.end) : null;
|
|
22393
|
+
return { curves, ...edges.length ? { curveModes } : {}, anchors, handles, marquee, preview };
|
|
21879
22394
|
}
|
|
21880
22395
|
};
|
|
21881
|
-
function
|
|
22396
|
+
function rect2(a, b) {
|
|
21882
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) };
|
|
21883
22398
|
}
|
|
21884
22399
|
function prune(cs) {
|
|
@@ -22079,21 +22594,21 @@ var ContentAwareFillSession = class _ContentAwareFillSession {
|
|
|
22079
22594
|
}
|
|
22080
22595
|
}
|
|
22081
22596
|
/** Pointer down with the Sampling Brush (Alt flips add and subtract). Returns false for other tools. */
|
|
22082
|
-
begin(
|
|
22597
|
+
begin(point2, modifiers = {}) {
|
|
22083
22598
|
if (this.settingsValue.tool !== "sampling-brush" || !this.win) return false;
|
|
22084
22599
|
const add2 = this.settingsValue.brushMode === "add" !== Boolean(modifiers.altKey);
|
|
22085
22600
|
this.settingsValue = { ...this.settingsValue, sampling: "custom" };
|
|
22086
|
-
this.lastPoint =
|
|
22601
|
+
this.lastPoint = point2;
|
|
22087
22602
|
this.strokeAdds = add2;
|
|
22088
|
-
this.paint(
|
|
22603
|
+
this.paint(point2, point2, add2);
|
|
22089
22604
|
this.emit();
|
|
22090
22605
|
return true;
|
|
22091
22606
|
}
|
|
22092
22607
|
strokeAdds = true;
|
|
22093
|
-
move(
|
|
22608
|
+
move(point2) {
|
|
22094
22609
|
if (!this.lastPoint) return;
|
|
22095
|
-
this.paint(this.lastPoint,
|
|
22096
|
-
this.lastPoint =
|
|
22610
|
+
this.paint(this.lastPoint, point2, this.strokeAdds);
|
|
22611
|
+
this.lastPoint = point2;
|
|
22097
22612
|
this.emit();
|
|
22098
22613
|
}
|
|
22099
22614
|
/** Pointer up: the preview follows the new area when live. */
|
|
@@ -22407,6 +22922,7 @@ export {
|
|
|
22407
22922
|
SELECTION_MODES,
|
|
22408
22923
|
SELECTION_MODE_LABELS,
|
|
22409
22924
|
SELECTION_MODIFY_OPS,
|
|
22925
|
+
SELECTION_RECIPE_OPS,
|
|
22410
22926
|
SELECTION_TOOLS,
|
|
22411
22927
|
SELECT_AND_MASK_TOOLS,
|
|
22412
22928
|
SELECT_AND_MASK_VIEWS,
|
|
@@ -22453,6 +22969,7 @@ export {
|
|
|
22453
22969
|
TypeToolGesture,
|
|
22454
22970
|
UNITS,
|
|
22455
22971
|
U_MAX,
|
|
22972
|
+
VECTOR_MASK_EDGE_DEFAULTS,
|
|
22456
22973
|
VIBRANCE_GREY,
|
|
22457
22974
|
W9,
|
|
22458
22975
|
WARP_GRID_OPTIONS,
|
|
@@ -22671,6 +23188,8 @@ export {
|
|
|
22671
23188
|
describeChannel,
|
|
22672
23189
|
describeLayer,
|
|
22673
23190
|
describeLayerEffects,
|
|
23191
|
+
describeRecipe,
|
|
23192
|
+
describeRecipeStep,
|
|
22674
23193
|
describeSelection,
|
|
22675
23194
|
describeTransform,
|
|
22676
23195
|
deserializeChannels,
|
|
@@ -22801,6 +23320,7 @@ export {
|
|
|
22801
23320
|
hueRangeTables,
|
|
22802
23321
|
hueRangeWeight,
|
|
22803
23322
|
hueSaturation,
|
|
23323
|
+
hybridVectorMaskCoverage,
|
|
22804
23324
|
identityCurve,
|
|
22805
23325
|
identityLevels,
|
|
22806
23326
|
identityTransform,
|
|
@@ -22854,6 +23374,7 @@ export {
|
|
|
22854
23374
|
isSelectionEmpty,
|
|
22855
23375
|
isSelectionMode,
|
|
22856
23376
|
isSeparableBlendMode,
|
|
23377
|
+
isShapeStep,
|
|
22857
23378
|
isSmartSurfaceIncomplete,
|
|
22858
23379
|
isSpatialFilter,
|
|
22859
23380
|
isSrgb,
|
|
@@ -22871,6 +23392,7 @@ export {
|
|
|
22871
23392
|
layerEffectsBadge,
|
|
22872
23393
|
layerEffectsOf,
|
|
22873
23394
|
layerEffectsPanel,
|
|
23395
|
+
layerGuide,
|
|
22874
23396
|
layerHasThumbnail,
|
|
22875
23397
|
layerLabel,
|
|
22876
23398
|
layerMaskCoverage,
|
|
@@ -23060,9 +23582,11 @@ export {
|
|
|
23060
23582
|
readPaths,
|
|
23061
23583
|
readPixel,
|
|
23062
23584
|
readPsd,
|
|
23585
|
+
readRecipeStep,
|
|
23063
23586
|
readRegion,
|
|
23064
23587
|
readShapeComponents,
|
|
23065
23588
|
readSourceRegion,
|
|
23589
|
+
recipeStepSetting,
|
|
23066
23590
|
rectSelection,
|
|
23067
23591
|
rectToQuad,
|
|
23068
23592
|
reduceSurface,
|
|
@@ -23087,6 +23611,7 @@ export {
|
|
|
23087
23611
|
renderText,
|
|
23088
23612
|
replaceContents,
|
|
23089
23613
|
replaceTextRange,
|
|
23614
|
+
replaySelectionRecipe,
|
|
23090
23615
|
requireLayer,
|
|
23091
23616
|
requireOperation,
|
|
23092
23617
|
requireShapeLayer,
|
|
@@ -23119,6 +23644,7 @@ export {
|
|
|
23119
23644
|
sameCanvasViewport,
|
|
23120
23645
|
sampleLut,
|
|
23121
23646
|
sampleToneLut,
|
|
23647
|
+
samplingKey,
|
|
23122
23648
|
saturationOf,
|
|
23123
23649
|
scaleLayerEffect,
|
|
23124
23650
|
scalePaintGradient,
|
|
@@ -23206,6 +23732,7 @@ export {
|
|
|
23206
23732
|
standardBlendMode,
|
|
23207
23733
|
startMarchingAnts,
|
|
23208
23734
|
startSelectionWorker,
|
|
23735
|
+
stepNeedsPixels,
|
|
23209
23736
|
stepSlotTs,
|
|
23210
23737
|
strokeGrainCatch,
|
|
23211
23738
|
styleAt,
|
|
@@ -23276,11 +23803,13 @@ export {
|
|
|
23276
23803
|
upscaleRegion,
|
|
23277
23804
|
userErrorMessage,
|
|
23278
23805
|
uvToXy,
|
|
23806
|
+
validVectorMaskEdges,
|
|
23279
23807
|
validateEffectGradient,
|
|
23280
23808
|
validateLayerEffect,
|
|
23281
23809
|
validateLayerEffects,
|
|
23282
23810
|
vectorMaskBounds,
|
|
23283
23811
|
vectorMaskCoverage,
|
|
23812
|
+
vectorMaskSegments,
|
|
23284
23813
|
vectorMaskThumbnail,
|
|
23285
23814
|
vectorMaskThumbnailClick,
|
|
23286
23815
|
versionedDescriptorBytes,
|