@pixerate/editor 0.183.0 → 0.186.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-Y46OMERN.js → chunk-FW7N64XK.js} +298 -189
- package/dist/{chunk-UHMSMUHK.cjs → chunk-X2KQQZBT.cjs} +358 -249
- package/dist/image-editor/controller.d.cts +208 -8
- package/dist/image-editor/controller.d.ts +208 -8
- package/dist/image-editor/index.cjs +2610 -1025
- package/dist/image-editor/index.js +1672 -87
- package/dist/image-editor/layers/model.d.cts +5 -1
- package/dist/image-editor/layers/model.d.ts +5 -1
- package/dist/image-editor/layers/paint-ui.d.cts +2 -1
- package/dist/image-editor/layers/paint-ui.d.ts +2 -1
- package/dist/image-editor/layers/panel.d.cts +23 -1
- package/dist/image-editor/layers/panel.d.ts +23 -1
- package/dist/image-editor/layers/serialize.d.cts +3 -0
- package/dist/image-editor/layers/serialize.d.ts +3 -0
- package/dist/image-editor/psd/channels.d.cts +24 -0
- package/dist/image-editor/psd/channels.d.ts +24 -0
- package/dist/image-editor/psd/export.d.cts +3 -0
- package/dist/image-editor/psd/export.d.ts +3 -0
- package/dist/image-editor/psd/import.d.cts +10 -0
- package/dist/image-editor/psd/import.d.ts +10 -0
- package/dist/image-editor/renderer.d.cts +6 -0
- package/dist/image-editor/renderer.d.ts +6 -0
- package/dist/image-editor/selection/channels.d.cts +52 -0
- package/dist/image-editor/selection/channels.d.ts +52 -0
- package/dist/image-editor/selection/gesture.d.cts +47 -2
- package/dist/image-editor/selection/gesture.d.ts +47 -2
- package/dist/image-editor/selection/index.d.cts +2 -0
- package/dist/image-editor/selection/index.d.ts +2 -0
- package/dist/image-editor/selection/modify.d.cts +3 -2
- package/dist/image-editor/selection/modify.d.ts +3 -2
- package/dist/image-editor/selection/overlay.d.cts +2 -0
- package/dist/image-editor/selection/overlay.d.ts +2 -0
- package/dist/image-editor/selection/stroke.d.cts +8 -0
- package/dist/image-editor/selection/stroke.d.ts +8 -0
- package/dist/image-editor/selection/tool-options.d.cts +12 -2
- package/dist/image-editor/selection/tool-options.d.ts +12 -2
- package/dist/image-editor/selection/wand.d.cts +37 -0
- package/dist/image-editor/selection/wand.d.ts +37 -0
- package/dist/image-editor/selection-tools.d.cts +8 -3
- package/dist/image-editor/selection-tools.d.ts +8 -3
- package/dist/image-editor/tools.d.cts +10 -2
- package/dist/image-editor/tools.d.ts +10 -2
- package/dist/image-editor/types.d.cts +12 -0
- package/dist/image-editor/types.d.ts +12 -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 +19 -21
- package/skills/pixerate-image-editor/references/free-transform.md +22 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +36 -3
|
@@ -370,7 +370,7 @@ import {
|
|
|
370
370
|
writePsd,
|
|
371
371
|
xyToTempTint,
|
|
372
372
|
xyToUv
|
|
373
|
-
} from "../chunk-
|
|
373
|
+
} from "../chunk-FW7N64XK.js";
|
|
374
374
|
import {
|
|
375
375
|
parseCssColor
|
|
376
376
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -939,12 +939,13 @@ function selectionOutline(mask, threshold = 128) {
|
|
|
939
939
|
function quickMaskPixels(mask, style = {}) {
|
|
940
940
|
const [r, g, b] = style.color ?? [255, 0, 0];
|
|
941
941
|
const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));
|
|
942
|
+
const selected = style.indicates === "selected";
|
|
942
943
|
const out = new Uint8ClampedArray(mask.data.length * 4);
|
|
943
944
|
for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {
|
|
944
945
|
out[p] = r;
|
|
945
946
|
out[p + 1] = g;
|
|
946
947
|
out[p + 2] = b;
|
|
947
|
-
out[p + 3] = Math.round((255 - mask.data[i]) * k);
|
|
948
|
+
out[p + 3] = Math.round((selected ? mask.data[i] : 255 - mask.data[i]) * k);
|
|
948
949
|
}
|
|
949
950
|
return out;
|
|
950
951
|
}
|
|
@@ -1362,15 +1363,31 @@ var ImageEditorRenderer = class {
|
|
|
1362
1363
|
}
|
|
1363
1364
|
ctx.restore();
|
|
1364
1365
|
}
|
|
1365
|
-
const display = options.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1366
|
-
|
|
1366
|
+
const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1367
|
+
const shownChannels = (state.channels ?? []).filter((c) => c.visible);
|
|
1368
|
+
if (shownChannels.length) {
|
|
1367
1369
|
ctx.save();
|
|
1368
1370
|
try {
|
|
1369
1371
|
applyImageProjectionToContext(ctx, projection);
|
|
1370
1372
|
ctx.beginPath();
|
|
1371
1373
|
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1372
1374
|
ctx.clip();
|
|
1373
|
-
|
|
1375
|
+
for (const c of shownChannels) this.drawMaskOverlay(ctx, c.mask, { color: hexRgb(c.color), opacity: c.opacity, indicates: c.indicates });
|
|
1376
|
+
} catch {
|
|
1377
|
+
}
|
|
1378
|
+
ctx.restore();
|
|
1379
|
+
}
|
|
1380
|
+
const { width: iw, height: ih } = state.imageDimensions;
|
|
1381
|
+
const quickMaskSel = display === "quickMask" && state.quickMaskEditing && !state.selection?.bounds ? this.emptyMask(Math.round(iw), Math.round(ih)) : null;
|
|
1382
|
+
if (state.selection?.bounds && display !== "none" || quickMaskSel || options.draftSelectionPath?.length) {
|
|
1383
|
+
ctx.save();
|
|
1384
|
+
try {
|
|
1385
|
+
applyImageProjectionToContext(ctx, projection);
|
|
1386
|
+
ctx.beginPath();
|
|
1387
|
+
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1388
|
+
ctx.clip();
|
|
1389
|
+
if (quickMaskSel) this.drawQuickMask(ctx, quickMaskSel);
|
|
1390
|
+
else if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
|
|
1374
1391
|
else if (state.selection?.bounds && display === "ants") this.drawAnts(ctx, state.selection, projection.scale / u, options.antsOffset ?? 0);
|
|
1375
1392
|
if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
|
|
1376
1393
|
} catch {
|
|
@@ -1776,6 +1793,31 @@ var ImageEditorRenderer = class {
|
|
|
1776
1793
|
}
|
|
1777
1794
|
quickMaskCache = /* @__PURE__ */ new WeakMap();
|
|
1778
1795
|
outlineCache = /* @__PURE__ */ new WeakMap();
|
|
1796
|
+
emptyMaskCache = null;
|
|
1797
|
+
/** An empty selection of the image's size (Quick Mask mode with nothing selected). */
|
|
1798
|
+
emptyMask(width, height) {
|
|
1799
|
+
const m = this.emptyMaskCache;
|
|
1800
|
+
if (m && m.width === width && m.height === height) return m;
|
|
1801
|
+
this.emptyMaskCache = createSelectionMask(Math.max(1, width), Math.max(1, height));
|
|
1802
|
+
return this.emptyMaskCache;
|
|
1803
|
+
}
|
|
1804
|
+
overlayCache = /* @__PURE__ */ new WeakMap();
|
|
1805
|
+
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
1806
|
+
drawMaskOverlay(ctx, mask, style) {
|
|
1807
|
+
const key = `${style.color?.join(",")}|${style.opacity}|${style.indicates}`;
|
|
1808
|
+
let cached = this.overlayCache.get(mask);
|
|
1809
|
+
if (!cached || cached.key !== key) {
|
|
1810
|
+
const canvas = this.createCanvas(mask.width, mask.height);
|
|
1811
|
+
const mctx = canvas.getContext("2d");
|
|
1812
|
+
if (!mctx || typeof mctx.createImageData !== "function") return;
|
|
1813
|
+
const img = mctx.createImageData(mask.width, mask.height);
|
|
1814
|
+
img.data.set(quickMaskPixels(mask, style));
|
|
1815
|
+
mctx.putImageData(img, 0, 0);
|
|
1816
|
+
cached = { key, canvas };
|
|
1817
|
+
this.overlayCache.set(mask, cached);
|
|
1818
|
+
}
|
|
1819
|
+
ctx.drawImage(cached.canvas, 0, 0, mask.width, mask.height);
|
|
1820
|
+
}
|
|
1779
1821
|
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
1780
1822
|
drawQuickMask(ctx, selection) {
|
|
1781
1823
|
let canvas = this.quickMaskCache.get(selection);
|
|
@@ -2320,6 +2362,12 @@ function removeAnnotationLayers(layers) {
|
|
|
2320
2362
|
}
|
|
2321
2363
|
return out;
|
|
2322
2364
|
}
|
|
2365
|
+
function hexRgb(hex) {
|
|
2366
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex);
|
|
2367
|
+
if (!m) return [255, 0, 0];
|
|
2368
|
+
const v = parseInt(m[1], 16);
|
|
2369
|
+
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
2370
|
+
}
|
|
2323
2371
|
|
|
2324
2372
|
// src/image-editor/layers/shape-type-ui.ts
|
|
2325
2373
|
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
@@ -3065,7 +3113,7 @@ var TONE_RANGE_PRESETS = {
|
|
|
3065
3113
|
function toneRange(pixels, range, falloff = 20) {
|
|
3066
3114
|
const n = checkPixels(pixels);
|
|
3067
3115
|
const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
|
|
3068
|
-
const
|
|
3116
|
+
const ramp2 = Math.max(1e-3, falloff);
|
|
3069
3117
|
const out = new Uint8Array(n);
|
|
3070
3118
|
const d = pixels.data;
|
|
3071
3119
|
for (let i = 0; i < n; i++) {
|
|
@@ -3073,7 +3121,7 @@ function toneRange(pixels, range, falloff = 20) {
|
|
|
3073
3121
|
if (d[p + 3] <= 0) continue;
|
|
3074
3122
|
const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
|
|
3075
3123
|
const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
|
|
3076
|
-
out[i] = coverageToByte(1 - outside /
|
|
3124
|
+
out[i] = coverageToByte(1 - outside / ramp2);
|
|
3077
3125
|
}
|
|
3078
3126
|
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3079
3127
|
}
|
|
@@ -3102,6 +3150,111 @@ function hueRange(pixels, center) {
|
|
|
3102
3150
|
}
|
|
3103
3151
|
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3104
3152
|
}
|
|
3153
|
+
var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
|
|
3154
|
+
var COLOR_RANGE_PRESETS = [
|
|
3155
|
+
...Object.keys(HUE_RANGE_PRESETS),
|
|
3156
|
+
"highlights",
|
|
3157
|
+
"midtones",
|
|
3158
|
+
"shadows",
|
|
3159
|
+
"skinTones"
|
|
3160
|
+
];
|
|
3161
|
+
function isColorRangePreset(v) {
|
|
3162
|
+
return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
|
|
3163
|
+
}
|
|
3164
|
+
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;
|
|
3165
|
+
function skinToneRange(pixels) {
|
|
3166
|
+
const n = checkPixels(pixels);
|
|
3167
|
+
const out = new Uint8Array(n);
|
|
3168
|
+
const d = pixels.data;
|
|
3169
|
+
for (let i = 0; i < n; i++) {
|
|
3170
|
+
const p = i * 4;
|
|
3171
|
+
if (d[p + 3] <= 0) continue;
|
|
3172
|
+
const r = d[p] / 255;
|
|
3173
|
+
const g = d[p + 1] / 255;
|
|
3174
|
+
const b = d[p + 2] / 255;
|
|
3175
|
+
const mx = Math.max(r, g, b);
|
|
3176
|
+
const mn = Math.min(r, g, b);
|
|
3177
|
+
const chroma = mx - mn;
|
|
3178
|
+
if (chroma <= 0 || mx <= 0) continue;
|
|
3179
|
+
let hue;
|
|
3180
|
+
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
3181
|
+
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
3182
|
+
else hue = (r - g) / chroma + 4;
|
|
3183
|
+
hue *= 60;
|
|
3184
|
+
if (hue > 180) hue -= 360;
|
|
3185
|
+
const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
|
|
3186
|
+
out[i] = coverageToByte(k);
|
|
3187
|
+
}
|
|
3188
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3189
|
+
}
|
|
3190
|
+
function colorRangePreset(pixels, preset, options = {}) {
|
|
3191
|
+
if (preset === "skinTones") return skinToneRange(pixels);
|
|
3192
|
+
if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
|
|
3193
|
+
if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
|
|
3194
|
+
throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
|
|
3195
|
+
}
|
|
3196
|
+
function selectedColorBins(pixels, sel, tolerance) {
|
|
3197
|
+
const S = 64;
|
|
3198
|
+
let bins = new Uint8Array(S * S * S);
|
|
3199
|
+
const d = pixels.data;
|
|
3200
|
+
const n = pixels.width * pixels.height;
|
|
3201
|
+
for (let i = 0; i < n; i++) {
|
|
3202
|
+
if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
|
|
3203
|
+
bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
|
|
3204
|
+
}
|
|
3205
|
+
const r = Math.round(Math.max(0, tolerance) / 4);
|
|
3206
|
+
if (r === 0) return bins;
|
|
3207
|
+
for (const stride of [1, S, S * S]) {
|
|
3208
|
+
const out = new Uint8Array(bins.length);
|
|
3209
|
+
for (let i = 0; i < bins.length; i++) {
|
|
3210
|
+
if (!bins[i]) continue;
|
|
3211
|
+
const c = Math.floor(i / stride) % S;
|
|
3212
|
+
const lo = Math.max(0, c - r);
|
|
3213
|
+
const hi = Math.min(S - 1, c + r);
|
|
3214
|
+
const base = i - c * stride;
|
|
3215
|
+
for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
|
|
3216
|
+
}
|
|
3217
|
+
bins = out;
|
|
3218
|
+
}
|
|
3219
|
+
return bins;
|
|
3220
|
+
}
|
|
3221
|
+
function matcher(pixels, sel, options) {
|
|
3222
|
+
if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
|
|
3223
|
+
const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
|
|
3224
|
+
const d = pixels.data;
|
|
3225
|
+
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;
|
|
3226
|
+
}
|
|
3227
|
+
function growSelection(pixels, sel, options = {}) {
|
|
3228
|
+
checkPixels(pixels);
|
|
3229
|
+
const match = matcher(pixels, sel, options);
|
|
3230
|
+
const { width: w, height: h } = pixels;
|
|
3231
|
+
const out = new Uint8Array(sel.data);
|
|
3232
|
+
const stack = [];
|
|
3233
|
+
for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
|
|
3234
|
+
while (stack.length) {
|
|
3235
|
+
const i = stack.pop();
|
|
3236
|
+
const x = i % w;
|
|
3237
|
+
const y = (i - x) / w;
|
|
3238
|
+
const visit = (j) => {
|
|
3239
|
+
if (out[j] < 255 && match(j)) {
|
|
3240
|
+
out[j] = 255;
|
|
3241
|
+
stack.push(j);
|
|
3242
|
+
}
|
|
3243
|
+
};
|
|
3244
|
+
if (x > 0) visit(i - 1);
|
|
3245
|
+
if (x < w - 1) visit(i + 1);
|
|
3246
|
+
if (y > 0) visit(i - w);
|
|
3247
|
+
if (y < h - 1) visit(i + w);
|
|
3248
|
+
}
|
|
3249
|
+
return createSelectionMask(w, h, out);
|
|
3250
|
+
}
|
|
3251
|
+
function selectSimilar(pixels, sel, options = {}) {
|
|
3252
|
+
const n = checkPixels(pixels);
|
|
3253
|
+
const match = matcher(pixels, sel, options);
|
|
3254
|
+
const out = new Uint8Array(sel.data);
|
|
3255
|
+
for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
|
|
3256
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3257
|
+
}
|
|
3105
3258
|
|
|
3106
3259
|
// src/image-editor/selection/segmentation.ts
|
|
3107
3260
|
var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
|
|
@@ -3364,6 +3517,67 @@ function refineSelection(pixels, mask, params = {}) {
|
|
|
3364
3517
|
return createSelectionMask(mask.width, mask.height, out);
|
|
3365
3518
|
}
|
|
3366
3519
|
|
|
3520
|
+
// src/image-editor/selection/stroke.ts
|
|
3521
|
+
function selectionRing(sel, width, location) {
|
|
3522
|
+
const half = location === "center" ? width / 2 : width;
|
|
3523
|
+
const outer = location === "inside" ? sel : modifySelection(sel, "expand", half);
|
|
3524
|
+
const inner = location === "outside" ? sel : modifySelection(sel, "contract", half);
|
|
3525
|
+
const out = new Uint8Array(sel.data.length);
|
|
3526
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.max(0, outer.data[i] - inner.data[i]);
|
|
3527
|
+
return createSelectionMask(sel.width, sel.height, out);
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// src/image-editor/selection/channels.ts
|
|
3531
|
+
var DEFAULT_CHANNEL_COLOR = "#ff0000";
|
|
3532
|
+
function describeChannel(channel) {
|
|
3533
|
+
const { mask, ...rest } = channel;
|
|
3534
|
+
let sum = 0;
|
|
3535
|
+
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
3536
|
+
const percent = Math.round(sum / 255 / Math.max(1, mask.data.length) * 1e4) / 100;
|
|
3537
|
+
return { ...rest, bounds: mask.bounds, percent };
|
|
3538
|
+
}
|
|
3539
|
+
function serializeChannels(channels) {
|
|
3540
|
+
return channels.map((c) => ({ ...c, mask: serializeSelection(c.mask) }));
|
|
3541
|
+
}
|
|
3542
|
+
function deserializeChannels(value, size, problems = []) {
|
|
3543
|
+
if (!Array.isArray(value)) return [];
|
|
3544
|
+
const out = [];
|
|
3545
|
+
for (const raw of value) {
|
|
3546
|
+
const v = raw;
|
|
3547
|
+
try {
|
|
3548
|
+
const mask = deserializeSelection(v.mask);
|
|
3549
|
+
if (mask.width !== size.width || mask.height !== size.height) throw new Error("the channel is a different size from the canvas");
|
|
3550
|
+
out.push({
|
|
3551
|
+
id: typeof v.id === "string" && v.id ? v.id : `ch_${out.length + 1}`,
|
|
3552
|
+
name: typeof v.name === "string" && v.name.trim() ? v.name : `Alpha ${out.length + 1}`,
|
|
3553
|
+
mask,
|
|
3554
|
+
color: typeof v.color === "string" && /^#[0-9a-f]{6}$/i.test(v.color) ? v.color : DEFAULT_CHANNEL_COLOR,
|
|
3555
|
+
opacity: typeof v.opacity === "number" && v.opacity >= 0 && v.opacity <= 1 ? v.opacity : 0.5,
|
|
3556
|
+
indicates: v.indicates === "selected" ? "selected" : "masked",
|
|
3557
|
+
visible: v.visible === true
|
|
3558
|
+
});
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
problems.push(`Alpha channel "${String(v?.name ?? "?")}" could not be read: ${err instanceof Error ? err.message : String(err)}.`);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
return out;
|
|
3564
|
+
}
|
|
3565
|
+
function channelThumbnail(channel, maxSide = 64) {
|
|
3566
|
+
const { width: w, height: h, data } = channel.mask;
|
|
3567
|
+
const k = Math.min(1, maxSide / Math.max(w, h));
|
|
3568
|
+
const tw = Math.max(1, Math.round(w * k));
|
|
3569
|
+
const th = Math.max(1, Math.round(h * k));
|
|
3570
|
+
const out = new Uint8ClampedArray(tw * th * 4);
|
|
3571
|
+
for (let y = 0; y < th; y++) {
|
|
3572
|
+
const sy = Math.min(h - 1, Math.floor((y + 0.5) / k));
|
|
3573
|
+
for (let x = 0; x < tw; x++) {
|
|
3574
|
+
const v = data[sy * w + Math.min(w - 1, Math.floor((x + 0.5) / k))];
|
|
3575
|
+
out.set([v, v, v, 255], (y * tw + x) * 4);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return { width: tw, height: th, data: out };
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3367
3581
|
// src/image-editor/effect-layers.ts
|
|
3368
3582
|
function hasLayersBelow(layers, id) {
|
|
3369
3583
|
const all = flattenLayers(layers);
|
|
@@ -4274,6 +4488,8 @@ var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
|
|
|
4274
4488
|
var RETOUCH_TOOLS = ["clone", "heal", "spot-heal", "smudge", "dodge", "burn", "sponge", "blur", "sharpen", "history"];
|
|
4275
4489
|
var isRetouchTool = (tool) => RETOUCH_TOOLS.includes(tool);
|
|
4276
4490
|
var STROKE_TOOLS = ["brush", "eraser", "mixer", ...RETOUCH_TOOLS];
|
|
4491
|
+
var QUICK_MASK_TARGET = "quick-mask";
|
|
4492
|
+
var PASTE_MODES = ["new-layer", "in-place", "into", "outside"];
|
|
4277
4493
|
function paintCoverageInto(base, coverage, apply, rects, depth = null) {
|
|
4278
4494
|
const tiles = new Map(base.tiles);
|
|
4279
4495
|
const done = /* @__PURE__ */ new Set();
|
|
@@ -4783,6 +4999,10 @@ var ImageEditorController = class {
|
|
|
4783
4999
|
} catch {
|
|
4784
5000
|
}
|
|
4785
5001
|
}
|
|
5002
|
+
if (options.initialState?.channels) {
|
|
5003
|
+
const channels = deserializeChannels(options.initialState.channels, this.naturalSize, this.lastLoadProblems);
|
|
5004
|
+
if (channels.length) this.state.channels = channels;
|
|
5005
|
+
}
|
|
4786
5006
|
this.initialSnapshot = this.captureHistoryEntry();
|
|
4787
5007
|
if (typeof options.image === "string") {
|
|
4788
5008
|
this.loadImage(options.image);
|
|
@@ -4974,8 +5194,11 @@ var ImageEditorController = class {
|
|
|
4974
5194
|
}
|
|
4975
5195
|
const filter = normalizeSmartFilter({ kind: "effect", effect: d.id, provider: this.effects.require().id, name: d.name, values, enabled: options.enabled !== false, blend: options.blend, opacity: options.opacity });
|
|
4976
5196
|
const res = addSmartFilter(layers, id, filter, this.canvasSize, options.index);
|
|
4977
|
-
this.
|
|
4978
|
-
|
|
5197
|
+
const masked = this.asOneStep(() => {
|
|
5198
|
+
this.commitLayers(res.layers);
|
|
5199
|
+
return this.maskFiltersWithSelection(id, options.useSelection);
|
|
5200
|
+
});
|
|
5201
|
+
return { layerId: id, filter: res.filter, converted, masked };
|
|
4979
5202
|
}
|
|
4980
5203
|
/**
|
|
4981
5204
|
* Changes an effect smart filter: `values` merged over its own (`null` resets a control to the
|
|
@@ -5160,6 +5383,7 @@ var ImageEditorController = class {
|
|
|
5160
5383
|
transform: { ...s.transform },
|
|
5161
5384
|
outputOpacity: s.outputOpacity,
|
|
5162
5385
|
selection: s.selection ?? null,
|
|
5386
|
+
...s.channels?.length ? { channels: s.channels } : {},
|
|
5163
5387
|
source: {
|
|
5164
5388
|
sourceUrl: s.sourceUrl,
|
|
5165
5389
|
element: this.imageElement,
|
|
@@ -5195,6 +5419,8 @@ var ImageEditorController = class {
|
|
|
5195
5419
|
if (entry.colorMode) s.colorMode = entry.colorMode;
|
|
5196
5420
|
else delete s.colorMode;
|
|
5197
5421
|
s.selection = entry.selection;
|
|
5422
|
+
if (entry.channels?.length) s.channels = entry.channels;
|
|
5423
|
+
else delete s.channels;
|
|
5198
5424
|
if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
|
|
5199
5425
|
this.imageElement = entry.source.element;
|
|
5200
5426
|
s.sourceUrl = entry.source.sourceUrl;
|
|
@@ -5204,8 +5430,38 @@ var ImageEditorController = class {
|
|
|
5204
5430
|
s.selectedAnnotationId = null;
|
|
5205
5431
|
}
|
|
5206
5432
|
}
|
|
5433
|
+
/** Set while `asOneStep` runs: the step is already recorded, so nested commands add none. */
|
|
5434
|
+
historyGrouped = false;
|
|
5435
|
+
/**
|
|
5436
|
+
* Runs `fn` as one undo step: history is recorded once, before it, and the commands it calls
|
|
5437
|
+
* record none. If `fn` throws, the step is dropped and the state goes back to before it, so a
|
|
5438
|
+
* failure never leaves half a change.
|
|
5439
|
+
*/
|
|
5440
|
+
asOneStep(fn) {
|
|
5441
|
+
if (this.historyGrouped || this.isApplyingHistory) return fn();
|
|
5442
|
+
this.recordHistory();
|
|
5443
|
+
const entry = this.undoStack[this.undoStack.length - 1];
|
|
5444
|
+
this.historyGrouped = true;
|
|
5445
|
+
try {
|
|
5446
|
+
return fn();
|
|
5447
|
+
} catch (err) {
|
|
5448
|
+
if (this.undoStack[this.undoStack.length - 1] === entry) {
|
|
5449
|
+
this.undoStack.pop();
|
|
5450
|
+
this.isApplyingHistory = true;
|
|
5451
|
+
try {
|
|
5452
|
+
this.restoreHistoryEntry(entry);
|
|
5453
|
+
} finally {
|
|
5454
|
+
this.isApplyingHistory = false;
|
|
5455
|
+
}
|
|
5456
|
+
this.notify();
|
|
5457
|
+
}
|
|
5458
|
+
throw err;
|
|
5459
|
+
} finally {
|
|
5460
|
+
this.historyGrouped = false;
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5207
5463
|
recordHistory() {
|
|
5208
|
-
if (this.isApplyingHistory) return;
|
|
5464
|
+
if (this.isApplyingHistory || this.historyGrouped) return;
|
|
5209
5465
|
this.docRevision++;
|
|
5210
5466
|
this.undoStack.push(this.captureHistoryEntry());
|
|
5211
5467
|
if (this.undoStack.length > this.maxHistoryDepth) {
|
|
@@ -5851,16 +6107,23 @@ var ImageEditorController = class {
|
|
|
5851
6107
|
return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };
|
|
5852
6108
|
}
|
|
5853
6109
|
/**
|
|
5854
|
-
* Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black)
|
|
5855
|
-
* `selection` (the selection's coverage)
|
|
6110
|
+
* Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black),
|
|
6111
|
+
* `selection` (the selection's coverage), `hide-selection` (its inverse) or `transparency`
|
|
6112
|
+
* (the layer's own alpha, so the pixels can then be made opaque and edited). Replaces an existing mask. Makes the mask the paint
|
|
5856
6113
|
* target when the layer is active.
|
|
5857
6114
|
*/
|
|
5858
6115
|
addLayerMask(id, from = "reveal-all", props = {}) {
|
|
5859
6116
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
5860
6117
|
let mask;
|
|
5861
|
-
if (from === "selection") {
|
|
5862
|
-
|
|
5863
|
-
if (
|
|
6118
|
+
if (from === "selection" || from === "hide-selection" || from === "transparency") {
|
|
6119
|
+
let sel;
|
|
6120
|
+
if (from === "transparency") {
|
|
6121
|
+
sel = this.layerTransparency(id);
|
|
6122
|
+
} else {
|
|
6123
|
+
const current = this.state.selection;
|
|
6124
|
+
if (!current?.bounds) throw new LayerOperationError("Nothing is selected; select first, or use reveal-all / hide-all / transparency.");
|
|
6125
|
+
sel = from === "hide-selection" ? invertSelection(current) : current;
|
|
6126
|
+
}
|
|
5864
6127
|
mask = this.maskFromSelection(sel, props);
|
|
5865
6128
|
if (layer.type === "pixel" && (layer.offset.x || layer.offset.y)) {
|
|
5866
6129
|
mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };
|
|
@@ -6314,10 +6577,22 @@ var ImageEditorController = class {
|
|
|
6314
6577
|
return res.layer.id;
|
|
6315
6578
|
}
|
|
6316
6579
|
/** Adds a smart filter (`develop` or `blur`, with `blend` and `opacity`) at `index` (default the top), as one undo step. */
|
|
6317
|
-
addSmartFilter(id, filter, index) {
|
|
6580
|
+
addSmartFilter(id, filter, index, options = {}) {
|
|
6318
6581
|
const res = addSmartFilter(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);
|
|
6319
|
-
this.
|
|
6320
|
-
|
|
6582
|
+
return this.asOneStep(() => {
|
|
6583
|
+
this.commitLayers(res.layers);
|
|
6584
|
+
this.maskFiltersWithSelection(id, options.useSelection);
|
|
6585
|
+
return res.filter;
|
|
6586
|
+
});
|
|
6587
|
+
}
|
|
6588
|
+
/**
|
|
6589
|
+
* After adding a smart filter: with `useSelection` and a selection, sets the filter mask from
|
|
6590
|
+
* it in the same undo step (Photoshop limits a filter to the selection). Returns whether it did.
|
|
6591
|
+
*/
|
|
6592
|
+
maskFiltersWithSelection(id, useSelection) {
|
|
6593
|
+
if (!useSelection || !this.state.selection?.bounds) return false;
|
|
6594
|
+
this.setSmartFilterMask(id, "selection", {}, false);
|
|
6595
|
+
return true;
|
|
6321
6596
|
}
|
|
6322
6597
|
/** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */
|
|
6323
6598
|
updateSmartFilter(id, filterId, changes, commitToHistory = true) {
|
|
@@ -6857,6 +7132,12 @@ var ImageEditorController = class {
|
|
|
6857
7132
|
}
|
|
6858
7133
|
/** Resolves and validates what a stroke, fill or clear paints into. */
|
|
6859
7134
|
paintTarget(layerId, channel, action) {
|
|
7135
|
+
if (channel === "quick-mask" || channel === void 0 && layerId === void 0 && this.state.quickMaskEditing) {
|
|
7136
|
+
if (!this.state.quickMaskEditing) {
|
|
7137
|
+
throw new LayerOperationError(`Quick Mask is off; turn it on (set_quick_mask) to ${action} on the selection, or target a layer.`, `Turn on Quick Mask (Q) to ${action} on the selection.`);
|
|
7138
|
+
}
|
|
7139
|
+
return { id: QUICK_MASK_TARGET, layer: null, channel: "quick-mask", offset: { x: 0, y: 0 }, surface: this.quickMaskSurface(), lockTransparency: false };
|
|
7140
|
+
}
|
|
6860
7141
|
const id = layerId ?? this.state.activeLayerId;
|
|
6861
7142
|
if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`, `Pick a layer to ${action} on in the Layers panel.`);
|
|
6862
7143
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
@@ -6925,10 +7206,11 @@ var ImageEditorController = class {
|
|
|
6925
7206
|
opacity: brush.opacity,
|
|
6926
7207
|
blend: target.channel !== "pixels" ? "normal" : brush.blend,
|
|
6927
7208
|
lockTransparency: target.lockTransparency,
|
|
6928
|
-
|
|
7209
|
+
// In Quick Mask mode the selection is what is painted, so it does not limit the stroke.
|
|
7210
|
+
selection: target.channel !== "quick-mask" && this.state.selection?.bounds ? this.state.selection : null,
|
|
6929
7211
|
offset: target.offset
|
|
6930
7212
|
};
|
|
6931
|
-
const depth = target.channel !== "filter-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
7213
|
+
const depth = target.channel !== "filter-mask" && target.channel !== "quick-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
6932
7214
|
if (tool === "mixer") return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply, depth);
|
|
6933
7215
|
if (isRetouchTool(tool)) return new PaintStrokeSession(this, target, this.retouchStroke(tool, init, brush, target, apply), apply, depth);
|
|
6934
7216
|
const gradient = tool === "brush" && strokePaint?.type === "gradient" ? resolvePaintGradient(strokePaint, { paint: toHex(color), secondary: toHex(background) }) : null;
|
|
@@ -7093,12 +7375,19 @@ var ImageEditorController = class {
|
|
|
7093
7375
|
}
|
|
7094
7376
|
/** @internal Shows a stroke's live surface (no history). */
|
|
7095
7377
|
_strokeUpdate(layerId, channel, surface, rects) {
|
|
7378
|
+
if (channel === "quick-mask") {
|
|
7379
|
+
this.state.selection = this.selectionFromSurface(surface, this.state.selection ?? null, rects);
|
|
7380
|
+
this.notify();
|
|
7381
|
+
return;
|
|
7382
|
+
}
|
|
7096
7383
|
this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
|
|
7097
7384
|
this.notify(channel === "filter-mask" ? void 0 : rects);
|
|
7098
7385
|
}
|
|
7099
7386
|
/** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */
|
|
7100
7387
|
_strokeCommit(before, layerId, channel, surface) {
|
|
7101
|
-
if (
|
|
7388
|
+
if (channel === "quick-mask") {
|
|
7389
|
+
this.state.selection = surface ? this.selectionFromSurface(surface) : before.selection;
|
|
7390
|
+
} else if (surface) {
|
|
7102
7391
|
const prev = before.layers;
|
|
7103
7392
|
this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
|
|
7104
7393
|
if (this.bitDepth !== 8) this.state.layers = keepDeepLayers(prev, this.state.layers);
|
|
@@ -7145,13 +7434,177 @@ var ImageEditorController = class {
|
|
|
7145
7434
|
clearPixels(options = {}) {
|
|
7146
7435
|
return this.fillOrClear(options, "erase");
|
|
7147
7436
|
}
|
|
7148
|
-
|
|
7149
|
-
|
|
7437
|
+
/**
|
|
7438
|
+
* Edit › Stroke: paints a band `width` px wide along the selection's edge, `inside` it, on the
|
|
7439
|
+
* `center` of the edge or `outside` it (default), with `color` (default the foreground),
|
|
7440
|
+
* `opacity` and `blend`, on a layer's pixels or mask. One undo step. Returns the changed bounds.
|
|
7441
|
+
*/
|
|
7442
|
+
strokeSelection(options) {
|
|
7443
|
+
const sel = this.state.selection;
|
|
7444
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then stroke the selection.");
|
|
7445
|
+
const width = options.width;
|
|
7446
|
+
if (!(width > 0 && width <= 250)) throw new LayerOperationError("width must be greater than 0 and at most 250 px.");
|
|
7447
|
+
const location = options.location ?? "outside";
|
|
7448
|
+
if (location !== "inside" && location !== "center" && location !== "outside") {
|
|
7449
|
+
throw new LayerOperationError(`Unknown location "${String(location)}"; use inside, center or outside.`);
|
|
7450
|
+
}
|
|
7451
|
+
const ring = selectionRing(sel, width, location);
|
|
7452
|
+
if (!ring.bounds) return { layerId: options.layerId ?? this.state.activeLayerId ?? "", channel: "pixels", bounds: null, dabs: 0 };
|
|
7453
|
+
return this.fillOrClear(options, "paint", ring);
|
|
7454
|
+
}
|
|
7455
|
+
/**
|
|
7456
|
+
* Image › Crop to the selection's bounds (one undo step). Returns the crop box (natural
|
|
7457
|
+
* pixels). Throws without a selection.
|
|
7458
|
+
*/
|
|
7459
|
+
cropToSelection() {
|
|
7460
|
+
const sel = this.state.selection;
|
|
7461
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select the area to keep, then crop to it.");
|
|
7462
|
+
const crop = { ...sel.bounds };
|
|
7463
|
+
this.setCrop(crop);
|
|
7464
|
+
return crop;
|
|
7465
|
+
}
|
|
7466
|
+
/**
|
|
7467
|
+
* Edit › Content-Aware Fill: fills the selection with texture synthesised from around it
|
|
7468
|
+
* (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
|
|
7469
|
+
* coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
|
|
7470
|
+
* above it. Samples within `sampleRadius` px of the selection (default its larger side, at
|
|
7471
|
+
* least 32). Deterministic for a `seed`. One undo step. 8-bit documents only for now.
|
|
7472
|
+
*/
|
|
7473
|
+
contentAwareFill(options = {}) {
|
|
7474
|
+
const sel = this.state.selection;
|
|
7475
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
|
|
7476
|
+
if (this.bitDepth !== 8) throw new LayerOperationError("Content-Aware Fill works on 8-bit documents for now; set_bit_depth 8 first, or use spot_heal.");
|
|
7477
|
+
const output = options.output ?? "current";
|
|
7478
|
+
if (output !== "current" && output !== "new-layer") throw new LayerOperationError(`Unknown output "${String(output)}"; use current or new-layer.`);
|
|
7479
|
+
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
7480
|
+
const { width, height } = this.naturalSize;
|
|
7481
|
+
const b = sel.bounds;
|
|
7482
|
+
const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
|
|
7483
|
+
const x0 = Math.max(0, b.x - margin);
|
|
7484
|
+
const y0 = Math.max(0, b.y - margin);
|
|
7485
|
+
const win = { x: x0, y: y0, width: Math.min(width, b.x + b.width + margin) - x0, height: Math.min(height, b.y + b.height + margin) - y0 };
|
|
7486
|
+
if (win.width * win.height > 16e6) throw new LayerOperationError("The area to fill and sample is too large (over 16 MP); fill a smaller selection or lower sampleRadius.");
|
|
7487
|
+
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
7488
|
+
const before = readRegion(target.surface, layerRect);
|
|
7489
|
+
const img = new Float32Array(before.length);
|
|
7490
|
+
for (let i = 0; i < before.length; i++) img[i] = before[i];
|
|
7491
|
+
const coverage = new Float32Array(win.width * win.height);
|
|
7492
|
+
const hole = new Uint8Array(win.width * win.height);
|
|
7493
|
+
for (let y = 0; y < win.height; y++) {
|
|
7494
|
+
for (let x = 0; x < win.width; x++) {
|
|
7495
|
+
const c = sel.data[(win.y + y) * sel.width + win.x + x] / 255;
|
|
7496
|
+
coverage[y * win.width + x] = c;
|
|
7497
|
+
hole[y * win.width + x] = c > 0 ? 1 : 0;
|
|
7498
|
+
}
|
|
7499
|
+
}
|
|
7500
|
+
const filled = contentAwareFill(win.width, win.height, 4, img, hole, { seed: options.seed ?? 0 });
|
|
7501
|
+
const out = new Uint8ClampedArray(before.length);
|
|
7502
|
+
for (let i = 0; i < coverage.length; i++) {
|
|
7503
|
+
const k = coverage[i];
|
|
7504
|
+
for (let c = 0; c < 4; c++) {
|
|
7505
|
+
const p = i * 4 + c;
|
|
7506
|
+
out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
|
|
7507
|
+
}
|
|
7508
|
+
}
|
|
7509
|
+
if (output === "new-layer") {
|
|
7510
|
+
const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
|
|
7511
|
+
const data = new Uint8ClampedArray(b.width * b.height * 4);
|
|
7512
|
+
for (let y = 0; y < b.height; y++) data.set(out.subarray(((crop.y + y) * win.width + crop.x) * 4, ((crop.y + y) * win.width + crop.x + b.width) * 4), y * b.width * 4);
|
|
7513
|
+
const id = this.addPixelLayer({ name: "Content-Aware Fill", pixels: { width: b.width, height: b.height, data }, x: b.x, y: b.y, aboveId: target.id });
|
|
7514
|
+
return { layerId: id, bounds: { ...b }, output };
|
|
7515
|
+
}
|
|
7516
|
+
const surface = writeRegion(target.surface, layerRect, out);
|
|
7517
|
+
this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
7518
|
+
return { layerId: target.id, bounds: { ...b }, output };
|
|
7519
|
+
}
|
|
7520
|
+
/** The pixel clipboard (Copy / Cut), or null. */
|
|
7521
|
+
clipboard = null;
|
|
7522
|
+
/** What Copy or Cut last put on the editor's clipboard, or null. */
|
|
7523
|
+
getClipboard() {
|
|
7524
|
+
return this.clipboard;
|
|
7525
|
+
}
|
|
7526
|
+
/** Puts pixels on the editor's clipboard (e.g. an image pasted from the system clipboard). */
|
|
7527
|
+
setClipboard(image) {
|
|
7528
|
+
if (image && (image.data.length !== image.width * image.height * 4 || image.width < 1 || image.height < 1)) {
|
|
7529
|
+
throw new LayerOperationError("Clipboard pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
|
|
7530
|
+
}
|
|
7531
|
+
this.clipboard = image;
|
|
7532
|
+
}
|
|
7533
|
+
/**
|
|
7534
|
+
* Edit › Copy (or Copy Merged with `merged`): the selected pixels of a pixel layer (default the
|
|
7535
|
+
* active one) or of the whole visible image, cropped to the selection's bounds, with the
|
|
7536
|
+
* selection's soft edge in their alpha. Without a selection, the whole canvas. Not an undo step.
|
|
7537
|
+
*/
|
|
7538
|
+
copyPixels(options = {}) {
|
|
7539
|
+
const { width, height } = this.naturalSize;
|
|
7540
|
+
const sel = this.state.selection?.bounds ? this.state.selection : null;
|
|
7541
|
+
const rect = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
7542
|
+
let data;
|
|
7543
|
+
if (options.merged) {
|
|
7544
|
+
data = new Uint8ClampedArray(this.compositePixels(rect).data);
|
|
7545
|
+
} else {
|
|
7546
|
+
const id = options.layerId ?? this.state.activeLayerId;
|
|
7547
|
+
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
|
|
7548
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
7549
|
+
if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is not a pixel layer; copy merged, or rasterize it first.`);
|
|
7550
|
+
data = readRegion(this.surfaceOf(layer), { x: rect.x - layer.offset.x, y: rect.y - layer.offset.y, width: rect.width, height: rect.height });
|
|
7551
|
+
}
|
|
7552
|
+
if (sel) {
|
|
7553
|
+
for (let y = 0; y < rect.height; y++) {
|
|
7554
|
+
for (let x = 0; x < rect.width; x++) {
|
|
7555
|
+
const k = sel.data[(rect.y + y) * sel.width + rect.x + x];
|
|
7556
|
+
const p = (y * rect.width + x) * 4 + 3;
|
|
7557
|
+
data[p] = Math.round(data[p] * k / 255);
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
this.clipboard = { width: rect.width, height: rect.height, x: rect.x, y: rect.y, data };
|
|
7562
|
+
return this.clipboard;
|
|
7563
|
+
}
|
|
7564
|
+
/** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
|
|
7565
|
+
cutPixels(options = {}) {
|
|
7566
|
+
return this.asOneStep(() => {
|
|
7567
|
+
const clip = this.copyPixels({ layerId: options.layerId });
|
|
7568
|
+
this.clearPixels({ layerId: options.layerId, channel: "pixels" });
|
|
7569
|
+
return clip;
|
|
7570
|
+
});
|
|
7571
|
+
}
|
|
7572
|
+
/**
|
|
7573
|
+
* Edit › Paste as a new pixel layer above the active one (one undo step): `new-layer` centred
|
|
7574
|
+
* on the selection (or the canvas), `in-place` where it was copied, `into` centred in the
|
|
7575
|
+
* selection with the selection as its mask (Paste Into), or `outside` with the inverse
|
|
7576
|
+
* (Paste Outside). Returns the new layer id and where it went.
|
|
7577
|
+
*/
|
|
7578
|
+
pastePixels(mode = "new-layer", image = this.clipboard) {
|
|
7579
|
+
if (!image) throw new LayerOperationError("The clipboard is empty; copy_pixels first.");
|
|
7580
|
+
if (!PASTE_MODES.includes(mode)) throw new LayerOperationError(`Unknown paste mode "${String(mode)}"; use ${PASTE_MODES.join(", ")}.`);
|
|
7581
|
+
const sel = this.state.selection?.bounds ? this.state.selection : null;
|
|
7582
|
+
if ((mode === "into" || mode === "outside") && !sel) throw new LayerOperationError(`Paste ${mode === "into" ? "Into" : "Outside"} needs a selection; select where it goes first.`);
|
|
7583
|
+
const { width, height } = this.naturalSize;
|
|
7584
|
+
let x;
|
|
7585
|
+
let y;
|
|
7586
|
+
if (mode === "in-place" && image.x !== void 0 && image.y !== void 0) {
|
|
7587
|
+
x = image.x;
|
|
7588
|
+
y = image.y;
|
|
7589
|
+
} else {
|
|
7590
|
+
const frame = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
7591
|
+
x = Math.round(frame.x + (frame.width - image.width) / 2);
|
|
7592
|
+
y = Math.round(frame.y + (frame.height - image.height) / 2);
|
|
7593
|
+
}
|
|
7594
|
+
return this.asOneStep(() => {
|
|
7595
|
+
const id = this.addPixelLayer({ name: "Pasted", pixels: { width: image.width, height: image.height, data: new Uint8ClampedArray(image.data) }, x, y });
|
|
7596
|
+
if (mode === "into" || mode === "outside") this.addLayerMask(id, mode === "into" ? "selection" : "hide-selection");
|
|
7597
|
+
return { layerId: id, bounds: { x, y, width: image.width, height: image.height }, masked: mode === "into" || mode === "outside" };
|
|
7598
|
+
});
|
|
7599
|
+
}
|
|
7600
|
+
fillOrClear(options, mode, coverageMask) {
|
|
7601
|
+
const target = this.paintTarget(options.layerId, options.channel, mode === "paint" ? coverageMask ? "stroke" : "fill" : "clear");
|
|
7150
7602
|
const { width, height } = this.naturalSize;
|
|
7151
7603
|
const opacity = options.opacity ?? 1;
|
|
7152
7604
|
if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
|
|
7153
7605
|
const blend = parseBlend(options.blend) ?? "normal";
|
|
7154
|
-
const
|
|
7606
|
+
const quickMask = target.channel === "quick-mask";
|
|
7607
|
+
const sel = coverageMask ?? (!quickMask && options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null);
|
|
7155
7608
|
const region = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
7156
7609
|
const coverage = new CoverageMap();
|
|
7157
7610
|
const ones = new Float32Array(region.width * region.height);
|
|
@@ -7177,8 +7630,14 @@ var ImageEditorController = class {
|
|
|
7177
7630
|
selection: null,
|
|
7178
7631
|
offset: target.offset
|
|
7179
7632
|
};
|
|
7180
|
-
const depth = target.channel !== "filter-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
7633
|
+
const depth = target.channel !== "filter-mask" && target.channel !== "quick-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
7181
7634
|
const surface = paintCoverageInto(target.surface, coverage, apply, [region], depth);
|
|
7635
|
+
if (quickMask) {
|
|
7636
|
+
this.recordHistory();
|
|
7637
|
+
this.state.selection = this.selectionFromSurface(surface);
|
|
7638
|
+
this.notify();
|
|
7639
|
+
return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };
|
|
7640
|
+
}
|
|
7182
7641
|
const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));
|
|
7183
7642
|
this.commitLayers(next);
|
|
7184
7643
|
return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };
|
|
@@ -7325,7 +7784,7 @@ var ImageEditorController = class {
|
|
|
7325
7784
|
enabled: spec.enabled,
|
|
7326
7785
|
opacity: spec.opacity,
|
|
7327
7786
|
blend: spec.blend,
|
|
7328
|
-
mask: spec.mask ?? void 0
|
|
7787
|
+
mask: spec.mask ?? (spec.useSelection && this.state.selection?.bounds ? layerMaskFromSelection(this.state.selection) : void 0)
|
|
7329
7788
|
});
|
|
7330
7789
|
while (ids.has(layer.id)) layer.id = newLayerId("adj");
|
|
7331
7790
|
const { width, height } = this.naturalSize;
|
|
@@ -7535,6 +7994,7 @@ var ImageEditorController = class {
|
|
|
7535
7994
|
const prev = normalizeSelection(this.state.selection ?? null);
|
|
7536
7995
|
if (prev === next || prev && next && sameSelection(prev, next)) return false;
|
|
7537
7996
|
this.recordHistory();
|
|
7997
|
+
if (!next && prev?.bounds) this.lastSelection = prev;
|
|
7538
7998
|
this.state.selection = next;
|
|
7539
7999
|
this.notify();
|
|
7540
8000
|
return true;
|
|
@@ -7548,6 +8008,74 @@ var ImageEditorController = class {
|
|
|
7548
8008
|
deselect() {
|
|
7549
8009
|
return this.setSelection(null);
|
|
7550
8010
|
}
|
|
8011
|
+
/** The selection a Deselect cleared, for `reselect` (not saved, not part of history). */
|
|
8012
|
+
lastSelection = null;
|
|
8013
|
+
/** Whether `reselect` has a selection to bring back. */
|
|
8014
|
+
canReselect() {
|
|
8015
|
+
const { width, height } = this.naturalSize;
|
|
8016
|
+
return Boolean(this.lastSelection && this.lastSelection.width === width && this.lastSelection.height === height);
|
|
8017
|
+
}
|
|
8018
|
+
/** Select › Reselect: brings back the last selection a Deselect cleared, as one undo step. */
|
|
8019
|
+
reselect() {
|
|
8020
|
+
if (!this.canReselect()) return false;
|
|
8021
|
+
return this.setSelection(this.lastSelection);
|
|
8022
|
+
}
|
|
8023
|
+
/**
|
|
8024
|
+
* Single Row / Single Column Marquee: a 1 px line across the whole image at `position`
|
|
8025
|
+
* (natural pixels; y for a row, x for a column).
|
|
8026
|
+
*/
|
|
8027
|
+
selectLine(orientation, position, options = {}) {
|
|
8028
|
+
const { width, height } = this.naturalSize;
|
|
8029
|
+
const max = orientation === "row" ? height : width;
|
|
8030
|
+
const at = Math.floor(position);
|
|
8031
|
+
if (!Number.isFinite(position) || at < 0 || at >= max) {
|
|
8032
|
+
throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
|
|
8033
|
+
}
|
|
8034
|
+
const rect = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
|
|
8035
|
+
return this.shapeCommand(rectSelection(width, height, rect, { antiAlias: false }), options);
|
|
8036
|
+
}
|
|
8037
|
+
/** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
|
|
8038
|
+
async growSelection(options = {}) {
|
|
8039
|
+
const sel = this.state.selection;
|
|
8040
|
+
if (!sel?.bounds) return false;
|
|
8041
|
+
const pixels = await this.requirePixels(options);
|
|
8042
|
+
return this.setSelection(growSelection(pixels, sel, options));
|
|
8043
|
+
}
|
|
8044
|
+
/** Select › Similar: adds every pixel within `tolerance` of a selected colour (one undo step). */
|
|
8045
|
+
async selectSimilar(options = {}) {
|
|
8046
|
+
const sel = this.state.selection;
|
|
8047
|
+
if (!sel?.bounds) return false;
|
|
8048
|
+
const pixels = await this.requirePixels(options);
|
|
8049
|
+
return this.setSelection(selectSimilar(pixels, sel, options));
|
|
8050
|
+
}
|
|
8051
|
+
/**
|
|
8052
|
+
* Load Selection from a layer's transparency (Ctrl/⌘-click its thumbnail): its alpha in
|
|
8053
|
+
* document space, combined by `mode`. Works on pixel, shape, type and smart object layers.
|
|
8054
|
+
*/
|
|
8055
|
+
selectLayerTransparency(id, options = {}) {
|
|
8056
|
+
const mask = this.layerTransparency(id);
|
|
8057
|
+
return this.shapeCommand(mask, options);
|
|
8058
|
+
}
|
|
8059
|
+
layerTransparency(id) {
|
|
8060
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
8061
|
+
if (!hasSurface(layer)) {
|
|
8062
|
+
throw new LayerOperationError(`Layer "${layer.name || id}" has no pixels of its own; only pixel, shape, type and smart object layers do.`);
|
|
8063
|
+
}
|
|
8064
|
+
const { width, height } = this.naturalSize;
|
|
8065
|
+
const out = new Uint8Array(width * height);
|
|
8066
|
+
const pixels = this.getLayerPixels(id);
|
|
8067
|
+
if (!pixels) return createSelectionMask(width, height, out);
|
|
8068
|
+
for (let y = 0; y < pixels.height; y++) {
|
|
8069
|
+
const dy = y + pixels.y;
|
|
8070
|
+
if (dy < 0 || dy >= height) continue;
|
|
8071
|
+
for (let x = 0; x < pixels.width; x++) {
|
|
8072
|
+
const dx = x + pixels.x;
|
|
8073
|
+
if (dx < 0 || dx >= width) continue;
|
|
8074
|
+
out[dy * width + dx] = pixels.data[(y * pixels.width + x) * 4 + 3];
|
|
8075
|
+
}
|
|
8076
|
+
}
|
|
8077
|
+
return createSelectionMask(width, height, out);
|
|
8078
|
+
}
|
|
7551
8079
|
/** Select › Inverse (with nothing selected, selects everything). */
|
|
7552
8080
|
invertSelection() {
|
|
7553
8081
|
const { width, height } = this.naturalSize;
|
|
@@ -7555,7 +8083,8 @@ var ImageEditorController = class {
|
|
|
7555
8083
|
return this.setSelection(invertSelection(current));
|
|
7556
8084
|
}
|
|
7557
8085
|
shapeCommand(mask, options) {
|
|
7558
|
-
const
|
|
8086
|
+
const feathered = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
|
|
8087
|
+
const shaped = options.invert ? invertSelection(feathered) : feathered;
|
|
7559
8088
|
return this.setSelection(shaped, options.mode ?? "replace");
|
|
7560
8089
|
}
|
|
7561
8090
|
/** Rectangular marquee in natural image pixels. */
|
|
@@ -7657,6 +8186,15 @@ var ImageEditorController = class {
|
|
|
7657
8186
|
this.shapeCommand(colorRange(pixels, samples, options), options);
|
|
7658
8187
|
return this.getSelection();
|
|
7659
8188
|
}
|
|
8189
|
+
/**
|
|
8190
|
+
* Select › Color Range with a preset: `reds` … `magentas`, `highlights` / `midtones` /
|
|
8191
|
+
* `shadows` (with `falloff` levels, default 20) or `skinTones`. One undo step.
|
|
8192
|
+
*/
|
|
8193
|
+
async selectColorRangePreset(preset, options = {}) {
|
|
8194
|
+
const pixels = await this.requirePixels(options);
|
|
8195
|
+
this.shapeCommand(colorRangePreset(pixels, preset, options), options);
|
|
8196
|
+
return this.getSelection();
|
|
8197
|
+
}
|
|
7660
8198
|
/** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
|
|
7661
8199
|
modifySelection(op, radius) {
|
|
7662
8200
|
const sel = this.state.selection;
|
|
@@ -7733,10 +8271,175 @@ var ImageEditorController = class {
|
|
|
7733
8271
|
}
|
|
7734
8272
|
/** Quick Mask view (tints the unselected area). View state: no undo step. */
|
|
7735
8273
|
setQuickMask(on) {
|
|
7736
|
-
|
|
7737
|
-
|
|
8274
|
+
this.setSelectionDisplay(on ? "quickMask" : "ants");
|
|
8275
|
+
}
|
|
8276
|
+
/** How the canvas shows the selection (`state.selectionDisplay`). */
|
|
8277
|
+
getSelectionDisplay() {
|
|
8278
|
+
return this.state.selectionDisplay ?? (this.state.quickMask ? "quickMask" : "ants");
|
|
8279
|
+
}
|
|
8280
|
+
/**
|
|
8281
|
+
* Shows the selection as marching ants, as a Quick Mask tint over the unselected area, or hides
|
|
8282
|
+
* it (the selection still applies). View state: no undo step, not saved.
|
|
8283
|
+
*/
|
|
8284
|
+
setSelectionDisplay(display) {
|
|
8285
|
+
if (display !== "ants" && display !== "quickMask" && display !== "none") {
|
|
8286
|
+
throw new Error(`Unknown selection display "${String(display)}". Use ants, quickMask or none.`);
|
|
8287
|
+
}
|
|
8288
|
+
if (this.getSelectionDisplay() === display) return;
|
|
8289
|
+
if (display !== "quickMask") delete this.state.quickMaskEditing;
|
|
8290
|
+
this.state.selectionDisplay = display;
|
|
8291
|
+
this.state.quickMask = display === "quickMask";
|
|
8292
|
+
this.notify();
|
|
8293
|
+
}
|
|
8294
|
+
// --- Alpha channels ---
|
|
8295
|
+
/** The alpha channels (saved selections), in panel order. */
|
|
8296
|
+
getChannels() {
|
|
8297
|
+
return this.state.channels ?? [];
|
|
8298
|
+
}
|
|
8299
|
+
getChannel(id) {
|
|
8300
|
+
return this.state.channels?.find((c) => c.id === id) ?? null;
|
|
8301
|
+
}
|
|
8302
|
+
requireChannel(id) {
|
|
8303
|
+
const channel = this.getChannel(id);
|
|
8304
|
+
if (!channel) {
|
|
8305
|
+
const ids = this.getChannels().map((c) => `${c.id} (${c.name})`);
|
|
8306
|
+
throw new LayerOperationError(`There is no alpha channel "${id}"; the channels are ${ids.join(", ") || "none (save a selection first)"}.`);
|
|
8307
|
+
}
|
|
8308
|
+
return channel;
|
|
8309
|
+
}
|
|
8310
|
+
setChannels(next, commitToHistory = true) {
|
|
8311
|
+
if (commitToHistory) this.recordHistory();
|
|
8312
|
+
if (next.length) this.state.channels = next;
|
|
8313
|
+
else delete this.state.channels;
|
|
8314
|
+
this.notify();
|
|
8315
|
+
}
|
|
8316
|
+
newChannelId() {
|
|
8317
|
+
const ids = new Set(this.getChannels().map((c) => c.id));
|
|
8318
|
+
let n = this.getChannels().length + 1;
|
|
8319
|
+
while (ids.has(`ch_${n}`)) n++;
|
|
8320
|
+
return `ch_${n}`;
|
|
8321
|
+
}
|
|
8322
|
+
/**
|
|
8323
|
+
* Select › Save Selection: a new alpha channel (`name`, default "Alpha n") holding the
|
|
8324
|
+
* selection, or with `channelId` the selection combined into that channel by `mode` (replace,
|
|
8325
|
+
* add, subtract, intersect). One undo step. Throws without a selection.
|
|
8326
|
+
*/
|
|
8327
|
+
saveSelection(options = {}) {
|
|
8328
|
+
const sel = this.state.selection;
|
|
8329
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then save the selection to a channel.");
|
|
8330
|
+
const { width, height } = this.naturalSize;
|
|
8331
|
+
const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
|
|
8332
|
+
const channels = [...this.getChannels()];
|
|
8333
|
+
if (options.channelId !== void 0) {
|
|
8334
|
+
const existing = this.requireChannel(options.channelId);
|
|
8335
|
+
const mask = combineSelections(existing.mask.bounds ? existing.mask : null, sized, options.mode ?? "replace") ?? createSelectionMask(width, height);
|
|
8336
|
+
const next = { ...existing, mask, ...options.name?.trim() ? { name: options.name.trim() } : {} };
|
|
8337
|
+
this.setChannels(channels.map((c) => c.id === existing.id ? next : c));
|
|
8338
|
+
return next;
|
|
8339
|
+
}
|
|
8340
|
+
const channel = {
|
|
8341
|
+
id: this.newChannelId(),
|
|
8342
|
+
name: options.name?.trim() || `Alpha ${channels.length + 1}`,
|
|
8343
|
+
mask: sized,
|
|
8344
|
+
color: DEFAULT_CHANNEL_COLOR,
|
|
8345
|
+
opacity: 0.5,
|
|
8346
|
+
indicates: "masked",
|
|
8347
|
+
visible: false
|
|
8348
|
+
};
|
|
8349
|
+
this.setChannels([...channels, channel]);
|
|
8350
|
+
return channel;
|
|
8351
|
+
}
|
|
8352
|
+
/**
|
|
8353
|
+
* Select › Load Selection from an alpha channel (inverted with `invert`), combined with the
|
|
8354
|
+
* selection by `mode`. One undo step.
|
|
8355
|
+
*/
|
|
8356
|
+
loadSelectionFromChannel(id, options = {}) {
|
|
8357
|
+
const channel = this.requireChannel(id);
|
|
8358
|
+
const mask = options.invert ? invertSelection(channel.mask) : channel.mask;
|
|
8359
|
+
return this.setSelection(mask.bounds ? mask : null, options.mode ?? "replace");
|
|
8360
|
+
}
|
|
8361
|
+
/** Changes a channel's name, overlay colour, opacity, what it indicates or visibility (one undo step). */
|
|
8362
|
+
setChannelProperties(id, props) {
|
|
8363
|
+
const channel = this.requireChannel(id);
|
|
8364
|
+
if (props.name !== void 0 && !props.name.trim()) throw new LayerOperationError("A channel name cannot be empty.");
|
|
8365
|
+
if (props.color !== void 0 && !/^#[0-9a-f]{6}$/i.test(props.color)) throw new LayerOperationError("color must be #rrggbb.");
|
|
8366
|
+
if (props.opacity !== void 0 && !(props.opacity >= 0 && props.opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
|
|
8367
|
+
if (props.indicates !== void 0 && props.indicates !== "masked" && props.indicates !== "selected") throw new LayerOperationError("indicates must be masked or selected.");
|
|
8368
|
+
const next = {
|
|
8369
|
+
...channel,
|
|
8370
|
+
...props.name !== void 0 ? { name: props.name.trim() } : {},
|
|
8371
|
+
...props.color !== void 0 ? { color: props.color.toLowerCase() } : {},
|
|
8372
|
+
...props.opacity !== void 0 ? { opacity: props.opacity } : {},
|
|
8373
|
+
...props.indicates !== void 0 ? { indicates: props.indicates } : {},
|
|
8374
|
+
...props.visible !== void 0 ? { visible: props.visible } : {}
|
|
8375
|
+
};
|
|
8376
|
+
this.setChannels(this.getChannels().map((c) => c.id === id ? next : c));
|
|
8377
|
+
return next;
|
|
8378
|
+
}
|
|
8379
|
+
/** Deletes an alpha channel (one undo step). */
|
|
8380
|
+
deleteChannel(id) {
|
|
8381
|
+
this.requireChannel(id);
|
|
8382
|
+
this.setChannels(this.getChannels().filter((c) => c.id !== id));
|
|
8383
|
+
}
|
|
8384
|
+
/** Duplicates an alpha channel just after it (one undo step). Returns the copy. */
|
|
8385
|
+
duplicateChannel(id, name) {
|
|
8386
|
+
const channel = this.requireChannel(id);
|
|
8387
|
+
const copy = { ...channel, id: this.newChannelId(), name: name?.trim() || `${channel.name} copy` };
|
|
8388
|
+
const channels = [...this.getChannels()];
|
|
8389
|
+
channels.splice(channels.findIndex((c) => c.id === id) + 1, 0, copy);
|
|
8390
|
+
this.setChannels(channels);
|
|
8391
|
+
return copy;
|
|
8392
|
+
}
|
|
8393
|
+
/** Channel summaries (no pixels) for panels and agents. */
|
|
8394
|
+
describeChannels() {
|
|
8395
|
+
return this.getChannels().map(describeChannel);
|
|
8396
|
+
}
|
|
8397
|
+
// --- Quick Mask mode ---
|
|
8398
|
+
/** Whether Quick Mask mode is on (painting edits the selection). */
|
|
8399
|
+
get quickMaskEditing() {
|
|
8400
|
+
return Boolean(this.state.quickMaskEditing);
|
|
8401
|
+
}
|
|
8402
|
+
/**
|
|
8403
|
+
* Quick Mask mode (Q): on, the selection shows as a tint and painting, fills and strokes that
|
|
8404
|
+
* name no layer edit the selection itself (white selects, black deselects, grey partly);
|
|
8405
|
+
* off, it shows as marching ants again. View state: no undo step (the edits are).
|
|
8406
|
+
*/
|
|
8407
|
+
setQuickMaskMode(on) {
|
|
8408
|
+
if (this.quickMaskEditing === on) return;
|
|
8409
|
+
if (on) this.state.quickMaskEditing = true;
|
|
8410
|
+
else delete this.state.quickMaskEditing;
|
|
8411
|
+
this.setSelectionDisplay(on ? "quickMask" : "ants");
|
|
7738
8412
|
this.notify();
|
|
7739
8413
|
}
|
|
8414
|
+
/** The selection as a 1-channel surface to paint in Quick Mask mode. */
|
|
8415
|
+
quickMaskSurface() {
|
|
8416
|
+
const { width, height } = this.naturalSize;
|
|
8417
|
+
const sel = this.state.selection;
|
|
8418
|
+
if (!sel?.bounds) return emptySurface(1, 0);
|
|
8419
|
+
const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
|
|
8420
|
+
return surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);
|
|
8421
|
+
}
|
|
8422
|
+
/** The selection a painted Quick Mask surface stands for (`rects` changed since `base`). */
|
|
8423
|
+
selectionFromSurface(surface, base = null, rects) {
|
|
8424
|
+
const { width, height } = this.naturalSize;
|
|
8425
|
+
let data;
|
|
8426
|
+
if (base && rects && base.width === width && base.height === height) {
|
|
8427
|
+
data = new Uint8Array(base.data);
|
|
8428
|
+
for (const r of rects) {
|
|
8429
|
+
const x0 = Math.max(0, Math.floor(r.x));
|
|
8430
|
+
const y0 = Math.max(0, Math.floor(r.y));
|
|
8431
|
+
const x1 = Math.min(width, Math.ceil(r.x + r.width));
|
|
8432
|
+
const y1 = Math.min(height, Math.ceil(r.y + r.height));
|
|
8433
|
+
if (x1 <= x0 || y1 <= y0) continue;
|
|
8434
|
+
const part = readRegion(surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
8435
|
+
for (let y = y0; y < y1; y++) data.set(part.subarray((y - y0) * (x1 - x0), (y - y0 + 1) * (x1 - x0)), y * width + x0);
|
|
8436
|
+
}
|
|
8437
|
+
} else {
|
|
8438
|
+
const all = readRegion(surface, { x: 0, y: 0, width, height });
|
|
8439
|
+
data = new Uint8Array(all.buffer, all.byteOffset, all.length);
|
|
8440
|
+
}
|
|
8441
|
+
return normalizeSelection(createSelectionMask(width, height, data));
|
|
8442
|
+
}
|
|
7740
8443
|
// --- Layer masks ---
|
|
7741
8444
|
maskFromSelection(sel, props = {}) {
|
|
7742
8445
|
const { width, height } = this.naturalSize;
|
|
@@ -7778,8 +8481,11 @@ var ImageEditorController = class {
|
|
|
7778
8481
|
if (!layer || layer.type !== "adjustment" || !layer.mask) return false;
|
|
7779
8482
|
return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);
|
|
7780
8483
|
}
|
|
7781
|
-
/**
|
|
7782
|
-
|
|
8484
|
+
/**
|
|
8485
|
+
* Load Selection from a layer mask (its pixels; density and feather are not applied), inverted
|
|
8486
|
+
* with `invert`. False without a painted mask (a depth mask is computed, not painted).
|
|
8487
|
+
*/
|
|
8488
|
+
selectionFromLayerMask(layerId, mode = "replace", invert2 = false) {
|
|
7783
8489
|
const layer = this.getLayer(layerId);
|
|
7784
8490
|
if (!layer?.mask || layer.mask.source) return false;
|
|
7785
8491
|
const { width, height } = this.naturalSize;
|
|
@@ -7787,7 +8493,7 @@ var ImageEditorController = class {
|
|
|
7787
8493
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
7788
8494
|
const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });
|
|
7789
8495
|
const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));
|
|
7790
|
-
return this.setSelection(sel, mode);
|
|
8496
|
+
return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
|
|
7791
8497
|
}
|
|
7792
8498
|
// --- Histogram ---
|
|
7793
8499
|
/**
|
|
@@ -8847,6 +9553,17 @@ var ImageEditorController = class {
|
|
|
8847
9553
|
s.outputOpacity = 1;
|
|
8848
9554
|
delete s.legacyOutputBlur;
|
|
8849
9555
|
s.selection = null;
|
|
9556
|
+
const psdChannels = (result.alphaChannels ?? []).map((c, k) => ({
|
|
9557
|
+
id: `ch_${k + 1}`,
|
|
9558
|
+
name: c.name,
|
|
9559
|
+
mask: createSelectionMask(result.width, result.height, c.data),
|
|
9560
|
+
color: c.color,
|
|
9561
|
+
opacity: c.opacity,
|
|
9562
|
+
indicates: c.indicates,
|
|
9563
|
+
visible: false
|
|
9564
|
+
}));
|
|
9565
|
+
if (psdChannels.length) s.channels = psdChannels;
|
|
9566
|
+
else delete s.channels;
|
|
8850
9567
|
s.selectedAnnotationId = null;
|
|
8851
9568
|
s.activeLayerId = defaultActiveLayerId(s.layers);
|
|
8852
9569
|
s.activeTarget = "pixels";
|
|
@@ -9037,7 +9754,14 @@ var ImageEditorController = class {
|
|
|
9037
9754
|
if (color.colorMode && this.bitDepth === 32) warnings.push(`${color.colorMode.toUpperCase()} files are written at 16 bits per channel at most; the 32-bit samples were converted (colour above 1 clipped).`);
|
|
9038
9755
|
const depth = color.colorMode && this.bitDepth === 32 ? 16 : this.bitDepth;
|
|
9039
9756
|
const mergedDeep = depth === 8 ? void 0 : workingToSamples(compositeDeep(prepared, canvas, sources, depth), depth);
|
|
9040
|
-
const
|
|
9757
|
+
const alphaChannels = this.getChannels().map((c) => ({
|
|
9758
|
+
name: c.name,
|
|
9759
|
+
data: c.mask.width === width && c.mask.height === height ? c.mask.data : resizeSelection(c.mask, width, height).data,
|
|
9760
|
+
color: c.color,
|
|
9761
|
+
opacity: c.opacity,
|
|
9762
|
+
indicates: c.indicates
|
|
9763
|
+
}));
|
|
9764
|
+
const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, ...color });
|
|
9041
9765
|
warnings.push(...result.warnings);
|
|
9042
9766
|
return { bytes: result.bytes, warnings, psb: result.psb, width, height };
|
|
9043
9767
|
}
|
|
@@ -9110,6 +9834,7 @@ var ImageEditorController = class {
|
|
|
9110
9834
|
try {
|
|
9111
9835
|
out = this.serializeState();
|
|
9112
9836
|
if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
|
|
9837
|
+
if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
|
|
9113
9838
|
} finally {
|
|
9114
9839
|
if (root) {
|
|
9115
9840
|
this.state = current;
|
|
@@ -9150,6 +9875,11 @@ var ImageEditorController = class {
|
|
|
9150
9875
|
}
|
|
9151
9876
|
this.state.selection = selection;
|
|
9152
9877
|
this.dropMismatchedSelection();
|
|
9878
|
+
const problems = [];
|
|
9879
|
+
const channels = deserializeChannels(data.channels, this.naturalSize, problems);
|
|
9880
|
+
if (channels.length) this.state.channels = channels;
|
|
9881
|
+
else delete this.state.channels;
|
|
9882
|
+
if (problems.length) this.lastLoadProblems = [...this.lastLoadProblems, ...problems];
|
|
9153
9883
|
this.notify();
|
|
9154
9884
|
}
|
|
9155
9885
|
/** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */
|
|
@@ -10121,9 +10851,9 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
10121
10851
|
// src/image-editor/playback.ts
|
|
10122
10852
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
10123
10853
|
var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
|
|
10124
|
-
var SELECT = /^(select_region|select_subject|select_by_color|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy)$/;
|
|
10854
|
+
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|set_quick_mask)$/;
|
|
10125
10855
|
var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
|
|
10126
|
-
var CROP = /^(set_crop|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
10856
|
+
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
10127
10857
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
10128
10858
|
function imagePlaybackPanel(tool) {
|
|
10129
10859
|
if (ADJUST.test(tool)) return "adjust";
|
|
@@ -10238,8 +10968,24 @@ var SELECTION_TOOLS = [
|
|
|
10238
10968
|
{ id: "wand", label: "Magic Wand" },
|
|
10239
10969
|
{ id: "rect", label: "Rectangle" },
|
|
10240
10970
|
{ id: "ellipse", label: "Ellipse" },
|
|
10241
|
-
{ id: "
|
|
10971
|
+
{ id: "row", label: "Single Row" },
|
|
10972
|
+
{ id: "column", label: "Single Column" },
|
|
10973
|
+
{ id: "lasso", label: "Lasso" },
|
|
10974
|
+
{ id: "polygon", label: "Polygonal Lasso" },
|
|
10975
|
+
{ id: "range", label: "Color Range" }
|
|
10242
10976
|
];
|
|
10977
|
+
var COLOR_RANGE_PRESET_LABELS = {
|
|
10978
|
+
reds: "Reds",
|
|
10979
|
+
yellows: "Yellows",
|
|
10980
|
+
greens: "Greens",
|
|
10981
|
+
cyans: "Cyans",
|
|
10982
|
+
blues: "Blues",
|
|
10983
|
+
magentas: "Magentas",
|
|
10984
|
+
highlights: "Highlights",
|
|
10985
|
+
midtones: "Midtones",
|
|
10986
|
+
shadows: "Shadows",
|
|
10987
|
+
skinTones: "Skin Tones"
|
|
10988
|
+
};
|
|
10243
10989
|
var SELECTION_MODE_LABELS = {
|
|
10244
10990
|
replace: "New",
|
|
10245
10991
|
add: "Add",
|
|
@@ -10253,7 +10999,11 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
|
|
|
10253
10999
|
contiguous: true,
|
|
10254
11000
|
antiAlias: true,
|
|
10255
11001
|
sampleMerged: false,
|
|
10256
|
-
distance: "rgb"
|
|
11002
|
+
distance: "rgb",
|
|
11003
|
+
feather: 0,
|
|
11004
|
+
fuzziness: 40,
|
|
11005
|
+
localized: 0,
|
|
11006
|
+
rangeInvert: false
|
|
10257
11007
|
};
|
|
10258
11008
|
function selectionModeFromModifiers(modifiers, fallback) {
|
|
10259
11009
|
if (modifiers.shiftKey && modifiers.altKey) return "intersect";
|
|
@@ -10284,6 +11034,41 @@ var modeSchema = {
|
|
|
10284
11034
|
description: "How the result combines with the current selection (default replace)."
|
|
10285
11035
|
};
|
|
10286
11036
|
var num2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
11037
|
+
var featherSchema = { type: "number", minimum: 0, maximum: 1e3, description: "Feather the new shape by this many pixels before combining (default 0)." };
|
|
11038
|
+
var antiAliasSchema = { type: "boolean", description: "Soft one-pixel edge (default true)." };
|
|
11039
|
+
var sampleMergedSchema = { type: "boolean", description: "Sample the adjusted image (adjustment layers and filters applied) rather than the pixels as painted (default false)." };
|
|
11040
|
+
var sampleSchema = {
|
|
11041
|
+
type: "string",
|
|
11042
|
+
enum: ["composite", "active-layer"],
|
|
11043
|
+
description: "composite (default): all layers; active-layer: only the active pixel layer (see set_active_layer)."
|
|
11044
|
+
};
|
|
11045
|
+
var pointSchema = {
|
|
11046
|
+
type: "object",
|
|
11047
|
+
properties: { x: { type: "number", description: "x in natural image pixels." }, y: { type: "number", description: "y in natural image pixels." } },
|
|
11048
|
+
required: ["x", "y"],
|
|
11049
|
+
additionalProperties: false
|
|
11050
|
+
};
|
|
11051
|
+
var rectProperties = {
|
|
11052
|
+
x: { type: "number", description: "Left edge in natural image pixels." },
|
|
11053
|
+
y: { type: "number", description: "Top edge in natural image pixels." },
|
|
11054
|
+
width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
|
|
11055
|
+
height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
|
|
11056
|
+
};
|
|
11057
|
+
function shapeOptions(args) {
|
|
11058
|
+
return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
|
|
11059
|
+
}
|
|
11060
|
+
function sampleOptions(args) {
|
|
11061
|
+
if (args.sample !== void 0 && args.sample !== "composite" && args.sample !== "active-layer") {
|
|
11062
|
+
throw new Error(`Unknown sample "${String(args.sample)}". Use composite or active-layer.`);
|
|
11063
|
+
}
|
|
11064
|
+
return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
|
|
11065
|
+
}
|
|
11066
|
+
function rectOf(args) {
|
|
11067
|
+
const rect = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
|
|
11068
|
+
if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) throw new Error("Pass numeric x, y, width and height in natural image pixels.");
|
|
11069
|
+
if (rect.width <= 0 || rect.height <= 0) throw new Error("width and height must be greater than 0.");
|
|
11070
|
+
return rect;
|
|
11071
|
+
}
|
|
10287
11072
|
function summary(controller) {
|
|
10288
11073
|
const sel = controller.getSelection();
|
|
10289
11074
|
const { width, height } = controller.getState().imageDimensions;
|
|
@@ -10316,7 +11101,7 @@ function createImageSelectionTools(controller) {
|
|
|
10316
11101
|
const selectRegion = {
|
|
10317
11102
|
name: "select_region",
|
|
10318
11103
|
title: "Select a region",
|
|
10319
|
-
description: 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), a polygon (the lasso: points along an outline, closed automatically), everything ("all"), nothing ("none") or the inverse of the current selection ("invert"). To cut out a subject, trace its contour with polygon (then refine_selection for soft edges such as fur or foliage) rather than selecting its bounding box. One undoable step.',
|
|
11104
|
+
description: 'Deprecated: use select_rect, select_ellipse, select_polygon, select_all, deselect or invert_selection (this alias goes in the next minor release). Selects a rectangle or ellipse (x, y, width, height in natural image pixels), a polygon (the lasso: points along an outline, closed automatically), everything ("all"), nothing ("none") or the inverse of the current selection ("invert"). To cut out a subject, trace its contour with polygon (then refine_selection for soft edges such as fur or foliage) rather than selecting its bounding box. One undoable step.',
|
|
10320
11105
|
inputSchema: {
|
|
10321
11106
|
type: "object",
|
|
10322
11107
|
properties: {
|
|
@@ -10382,7 +11167,7 @@ function createImageSelectionTools(controller) {
|
|
|
10382
11167
|
const selectByColor = {
|
|
10383
11168
|
name: "select_by_color",
|
|
10384
11169
|
title: "Select by colour",
|
|
10385
|
-
description: 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with "color" ("#rrggbb") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',
|
|
11170
|
+
description: 'Deprecated: use magic_wand or select_color_range (this alias goes in the next minor release). Magic wand at (x, y) with tolerance (0-255) and contiguous, or with "color" ("#rrggbb") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',
|
|
10386
11171
|
inputSchema: {
|
|
10387
11172
|
type: "object",
|
|
10388
11173
|
properties: {
|
|
@@ -10422,7 +11207,7 @@ function createImageSelectionTools(controller) {
|
|
|
10422
11207
|
const modify = {
|
|
10423
11208
|
name: "modify_selection",
|
|
10424
11209
|
title: "Modify the selection",
|
|
10425
|
-
description: "Select \u203A Modify:
|
|
11210
|
+
description: "Select \u203A Modify: expand, contract, feather, border or smooth the selection by radius pixels (grow and shrink are older names for expand and contract). Expand and contract move the edge by distance; to add pixels of similar colour use grow_selection or select_similar. One undoable step.",
|
|
10426
11211
|
inputSchema: {
|
|
10427
11212
|
type: "object",
|
|
10428
11213
|
properties: {
|
|
@@ -10512,27 +11297,633 @@ function createImageSelectionTools(controller) {
|
|
|
10512
11297
|
return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };
|
|
10513
11298
|
}
|
|
10514
11299
|
};
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
11300
|
+
const selectAll = {
|
|
11301
|
+
name: "select_all",
|
|
11302
|
+
title: "Select all",
|
|
11303
|
+
description: "Select \u203A All: selects the whole image. One undoable step. Returns the selection.",
|
|
11304
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11305
|
+
annotations: writeAnnotations("Select all", true),
|
|
11306
|
+
handler: () => {
|
|
11307
|
+
controller.selectAll();
|
|
11308
|
+
return summary(controller);
|
|
11309
|
+
}
|
|
11310
|
+
};
|
|
11311
|
+
const deselect = {
|
|
11312
|
+
name: "deselect",
|
|
11313
|
+
title: "Deselect",
|
|
11314
|
+
description: "Select \u203A Deselect: clears the selection, so later edits apply to the whole layer. One undoable step. Returns the (empty) selection.",
|
|
11315
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11316
|
+
annotations: writeAnnotations("Deselect", true),
|
|
11317
|
+
handler: () => {
|
|
11318
|
+
controller.deselect();
|
|
11319
|
+
return summary(controller);
|
|
11320
|
+
}
|
|
11321
|
+
};
|
|
11322
|
+
const invert2 = {
|
|
11323
|
+
name: "invert_selection",
|
|
11324
|
+
title: "Invert the selection",
|
|
11325
|
+
description: "Select \u203A Inverse: selects what was not selected (soft edges stay soft). With nothing selected, selects everything. One undoable step. Returns the selection.",
|
|
11326
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11327
|
+
annotations: writeAnnotations("Invert the selection"),
|
|
11328
|
+
handler: () => {
|
|
11329
|
+
controller.invertSelection();
|
|
11330
|
+
return summary(controller);
|
|
11331
|
+
}
|
|
11332
|
+
};
|
|
11333
|
+
const selectRect = {
|
|
11334
|
+
name: "select_rect",
|
|
11335
|
+
title: "Select a rectangle",
|
|
11336
|
+
description: "Rectangular Marquee: selects x, y, width, height (natural image pixels), combined with the selection by mode. Use it for geometric areas (a frame, a screen, a strip along an edge); for a subject, trace its outline with select_polygon or use select_subject. One undoable step. Returns the selection.",
|
|
11337
|
+
inputSchema: {
|
|
11338
|
+
type: "object",
|
|
11339
|
+
properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
|
|
11340
|
+
required: ["x", "y", "width", "height"],
|
|
11341
|
+
additionalProperties: false
|
|
11342
|
+
},
|
|
11343
|
+
annotations: writeAnnotations("Select a rectangle", true),
|
|
11344
|
+
handler: (args) => {
|
|
11345
|
+
controller.selectRect(rectOf(args), shapeOptions(args));
|
|
11346
|
+
return summary(controller);
|
|
11347
|
+
}
|
|
11348
|
+
};
|
|
11349
|
+
const selectEllipse = {
|
|
11350
|
+
name: "select_ellipse",
|
|
11351
|
+
title: "Select an ellipse",
|
|
11352
|
+
description: "Elliptical Marquee: selects the ellipse inscribed in x, y, width, height (natural image pixels), combined by mode. Use feather for a vignette. One undoable step. Returns the selection.",
|
|
11353
|
+
inputSchema: {
|
|
11354
|
+
type: "object",
|
|
11355
|
+
properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
|
|
11356
|
+
required: ["x", "y", "width", "height"],
|
|
11357
|
+
additionalProperties: false
|
|
11358
|
+
},
|
|
11359
|
+
annotations: writeAnnotations("Select an ellipse", true),
|
|
11360
|
+
handler: (args) => {
|
|
11361
|
+
controller.selectEllipse(rectOf(args), shapeOptions(args));
|
|
11362
|
+
return summary(controller);
|
|
11363
|
+
}
|
|
11364
|
+
};
|
|
11365
|
+
const selectPolygonTool = {
|
|
11366
|
+
name: "select_polygon",
|
|
11367
|
+
title: "Select a polygon (lasso)",
|
|
11368
|
+
description: "Lasso: selects the polygon through points (natural image pixels, in order along the outline, closed automatically), combined by mode. To cut out a subject, follow its contour closely (more points give a tighter edge), then refine_selection for hair or foliage. One undoable step. Returns the selection.",
|
|
11369
|
+
inputSchema: {
|
|
11370
|
+
type: "object",
|
|
11371
|
+
properties: {
|
|
11372
|
+
points: { type: "array", minItems: 3, items: pointSchema, description: "At least 3 vertices along the outline, in order, in natural image pixels." },
|
|
11373
|
+
mode: modeSchema,
|
|
11374
|
+
feather: featherSchema,
|
|
11375
|
+
antiAlias: antiAliasSchema
|
|
11376
|
+
},
|
|
11377
|
+
required: ["points"],
|
|
11378
|
+
additionalProperties: false
|
|
11379
|
+
},
|
|
11380
|
+
annotations: writeAnnotations("Select a polygon (lasso)", true),
|
|
11381
|
+
handler: (args) => {
|
|
11382
|
+
const list = Array.isArray(args.points) ? args.points.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : [];
|
|
11383
|
+
if (list.length < 3 || !list.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) {
|
|
11384
|
+
throw new Error("points needs at least 3 { x, y } vertices in natural image pixels.");
|
|
11385
|
+
}
|
|
11386
|
+
controller.selectPolygon(list, shapeOptions(args));
|
|
11387
|
+
return summary(controller);
|
|
11388
|
+
}
|
|
11389
|
+
};
|
|
11390
|
+
const magicWandTool = {
|
|
11391
|
+
name: "magic_wand",
|
|
11392
|
+
title: "Magic wand",
|
|
11393
|
+
description: "Magic Wand: selects pixels similar in colour to the one at (x, y). tolerance 0..255 (default 32) is the largest channel difference (rgb) or \u0394E scaled so 255 \u2248 \u0394E 100 (perceptual, closer to how colours look); contiguous (default true) keeps only the region connected to the seed. Best on flat backdrops and solid colours; check percent with get_selection before using it. One undoable step. Returns the selection.",
|
|
11394
|
+
inputSchema: {
|
|
11395
|
+
type: "object",
|
|
11396
|
+
properties: {
|
|
11397
|
+
x: { type: "number", description: "Seed x in natural image pixels." },
|
|
11398
|
+
y: { type: "number", description: "Seed y in natural image pixels." },
|
|
11399
|
+
tolerance: { type: "number", minimum: 0, maximum: 255, description: "Colour tolerance 0..255 (default 32)." },
|
|
11400
|
+
contiguous: { type: "boolean", description: "Only the region connected to the seed (default true); false selects every similar pixel." },
|
|
11401
|
+
distance: { type: "string", enum: ["rgb", "perceptual"], description: "How colour difference is measured: rgb (default, as Photoshop) or perceptual (CIE \u0394E)." },
|
|
11402
|
+
antiAlias: antiAliasSchema,
|
|
11403
|
+
sampleMerged: sampleMergedSchema,
|
|
11404
|
+
sample: sampleSchema,
|
|
11405
|
+
mode: modeSchema,
|
|
11406
|
+
feather: featherSchema
|
|
11407
|
+
},
|
|
11408
|
+
required: ["x", "y"],
|
|
11409
|
+
additionalProperties: false
|
|
11410
|
+
},
|
|
11411
|
+
annotations: writeAnnotations("Magic wand", true),
|
|
11412
|
+
handler: async (args) => {
|
|
11413
|
+
const x = num2(args.x, NaN);
|
|
11414
|
+
const y = num2(args.y, NaN);
|
|
11415
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("Pass the seed point x and y in natural image pixels.");
|
|
11416
|
+
const { width, height } = controller.getState().imageDimensions;
|
|
11417
|
+
if (x < 0 || y < 0 || x >= width || y >= height) throw new Error(`(${x}, ${y}) is outside the image (0..${width - 1}, 0..${height - 1}).`);
|
|
11418
|
+
if (args.distance !== void 0 && args.distance !== "rgb" && args.distance !== "perceptual") {
|
|
11419
|
+
throw new Error(`Unknown distance "${String(args.distance)}". Use rgb or perceptual.`);
|
|
11420
|
+
}
|
|
11421
|
+
await controller.magicWand(
|
|
11422
|
+
{ x, y },
|
|
11423
|
+
{
|
|
11424
|
+
...shapeOptions(args),
|
|
11425
|
+
...sampleOptions(args),
|
|
11426
|
+
tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))),
|
|
11427
|
+
contiguous: args.contiguous !== false,
|
|
11428
|
+
distance: args.distance ?? "rgb"
|
|
11429
|
+
}
|
|
11430
|
+
);
|
|
11431
|
+
return summary(controller);
|
|
11432
|
+
}
|
|
11433
|
+
};
|
|
11434
|
+
const colorRangeTool = {
|
|
11435
|
+
name: "select_color_range",
|
|
11436
|
+
title: "Select a colour range",
|
|
11437
|
+
description: "Select \u203A Color Range: selects every pixel matching samples (colours, each optionally at the x, y where it was picked) or a preset (reds \u2026 magentas, highlights, midtones, shadows, skinTones), softly: coverage falls off with colour distance over fuzziness levels. localized limits each positioned sample to a radius. invert selects everything else. Pass samples or preset, not both. One undoable step. Returns the selection.",
|
|
11438
|
+
inputSchema: {
|
|
11439
|
+
type: "object",
|
|
11440
|
+
properties: {
|
|
11441
|
+
samples: {
|
|
11442
|
+
type: "array",
|
|
11443
|
+
minItems: 1,
|
|
11444
|
+
maxItems: 32,
|
|
11445
|
+
items: {
|
|
11446
|
+
type: "object",
|
|
11447
|
+
properties: {
|
|
11448
|
+
color: { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "The sampled colour, #rrggbb." },
|
|
11449
|
+
x: { type: "number", description: "Where it was picked (natural image pixels), for localized." },
|
|
11450
|
+
y: { type: "number", description: "Where it was picked (natural image pixels), for localized." }
|
|
11451
|
+
},
|
|
11452
|
+
required: ["color"],
|
|
11453
|
+
additionalProperties: false
|
|
11454
|
+
},
|
|
11455
|
+
description: "Colours to select (the + eyedropper adds one). Read colours from capture_view or a seed pixel."
|
|
11456
|
+
},
|
|
11457
|
+
preset: { type: "string", enum: [...COLOR_RANGE_PRESETS], description: "A preset range instead of samples." },
|
|
11458
|
+
fuzziness: {
|
|
11459
|
+
type: "number",
|
|
11460
|
+
minimum: 0,
|
|
11461
|
+
maximum: 200,
|
|
11462
|
+
description: "Colour samples: levels over which coverage fades to 0 (default 40). Tone presets: falloff in levels around the range (default 20). Ignored by colour and skin presets."
|
|
11463
|
+
},
|
|
11464
|
+
localized: { type: "number", exclusiveMinimum: 0, description: "Localized Color Clusters: positioned samples fade to 0 at this many pixels from where they were picked (default off)." },
|
|
11465
|
+
invert: { type: "boolean", description: "Select everything except the range (default false)." },
|
|
11466
|
+
sampleMerged: sampleMergedSchema,
|
|
11467
|
+
sample: sampleSchema,
|
|
11468
|
+
mode: modeSchema,
|
|
11469
|
+
feather: featherSchema
|
|
11470
|
+
},
|
|
11471
|
+
additionalProperties: false
|
|
11472
|
+
},
|
|
11473
|
+
annotations: writeAnnotations("Select a colour range", true),
|
|
11474
|
+
handler: async (args) => {
|
|
11475
|
+
const hasSamples = Array.isArray(args.samples) && args.samples.length > 0;
|
|
11476
|
+
if (hasSamples === (args.preset !== void 0)) throw new Error("Pass samples (colours to match) or preset, not both.");
|
|
11477
|
+
const common = { ...sampleOptions(args), mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), invert: args.invert === true };
|
|
11478
|
+
if (!hasSamples) {
|
|
11479
|
+
if (!isColorRangePreset(args.preset)) throw new Error(`Unknown preset "${String(args.preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
|
|
11480
|
+
await controller.selectColorRangePreset(args.preset, { ...common, falloff: num2(args.fuzziness, 20) });
|
|
11481
|
+
} else {
|
|
11482
|
+
const samples = args.samples.map((s, i) => {
|
|
11483
|
+
if (typeof s?.color !== "string" || !/^#?[0-9a-f]{6}$/i.test(s.color.trim())) throw new Error(`samples[${i}].color must be #rrggbb.`);
|
|
11484
|
+
const at = typeof s.x === "number" && typeof s.y === "number" ? { x: s.x, y: s.y } : void 0;
|
|
11485
|
+
return at ? { color: s.color, at } : { color: s.color };
|
|
11486
|
+
});
|
|
11487
|
+
const localized = typeof args.localized === "number" && args.localized > 0 ? args.localized : void 0;
|
|
11488
|
+
await controller.selectColorRange(samples, { ...common, fuzziness: num2(args.fuzziness, 40), localized });
|
|
11489
|
+
}
|
|
11490
|
+
return summary(controller);
|
|
11491
|
+
}
|
|
11492
|
+
};
|
|
11493
|
+
const loadFromImage = {
|
|
11494
|
+
name: "load_selection_from_image",
|
|
11495
|
+
title: "Load a selection from a mask image",
|
|
11496
|
+
description: "Loads a selection from a mask image (an AI segmentation, a saved matte): white = selected by luminance, or one channel. Any size; it is resampled to the image. threshold (0..255) makes it hard-edged; leave it unset to keep soft edges. Follow with refine_selection for a coarse mask. One undoable step. Returns the selection.",
|
|
11497
|
+
inputSchema: {
|
|
11498
|
+
type: "object",
|
|
11499
|
+
properties: {
|
|
11500
|
+
image: { type: "string", minLength: 1, description: "The mask as a data URL (data:image/png;base64,\u2026) or an image URL." },
|
|
11501
|
+
channel: { type: "string", enum: ["luminance", "alpha", "red", "green", "blue"], description: "What to read as coverage (default luminance; alpha for a cutout PNG)." },
|
|
11502
|
+
threshold: { type: "number", minimum: 0, maximum: 255, description: "Values at or above this are selected, the rest not (default: keep soft values)." },
|
|
11503
|
+
invert: { type: "boolean", description: "Black = selected instead (default false)." },
|
|
11504
|
+
mode: modeSchema
|
|
11505
|
+
},
|
|
11506
|
+
required: ["image"],
|
|
11507
|
+
additionalProperties: false
|
|
11508
|
+
},
|
|
11509
|
+
annotations: { ...writeAnnotations("Load a selection from a mask image", true), openWorldHint: true },
|
|
11510
|
+
handler: async (args) => {
|
|
11511
|
+
if (typeof args.image !== "string" || !args.image.trim()) throw new Error("image must be a data URL or image URL.");
|
|
11512
|
+
const channel = args.channel ?? "luminance";
|
|
11513
|
+
if (!["luminance", "alpha", "red", "green", "blue"].includes(channel)) throw new Error(`Unknown channel "${String(channel)}". Use luminance, alpha, red, green or blue.`);
|
|
11514
|
+
await controller.loadSelectionFromImage(args.image, {
|
|
11515
|
+
channel,
|
|
11516
|
+
invert: args.invert === true,
|
|
11517
|
+
threshold: typeof args.threshold === "number" ? Math.min(255, Math.max(0, args.threshold)) : void 0,
|
|
11518
|
+
mode: parseSelectionMode(args.mode)
|
|
11519
|
+
});
|
|
11520
|
+
return summary(controller);
|
|
11521
|
+
}
|
|
11522
|
+
};
|
|
11523
|
+
const loadFromMask = {
|
|
11524
|
+
name: "load_selection_from_mask",
|
|
11525
|
+
title: "Load a selection from a layer mask",
|
|
11526
|
+
description: "Load Selection from a layer's mask (Ctrl/\u2318-click its mask thumbnail): the mask's pixels become the selection, combined by mode (density and feather are not applied). Use it to reuse a mask on another layer or to edit it as a selection. Fails when the layer has no painted mask. One undoable step. Returns the selection.",
|
|
11527
|
+
inputSchema: {
|
|
11528
|
+
type: "object",
|
|
11529
|
+
properties: {
|
|
11530
|
+
layerId: { type: "string", minLength: 1, description: "The layer whose mask to load (see list_layers)." },
|
|
11531
|
+
invert: { type: "boolean", description: "Select what the mask hides instead (default false)." },
|
|
11532
|
+
mode: modeSchema
|
|
11533
|
+
},
|
|
11534
|
+
required: ["layerId"],
|
|
11535
|
+
additionalProperties: false
|
|
11536
|
+
},
|
|
11537
|
+
annotations: writeAnnotations("Load a selection from a layer mask", true),
|
|
11538
|
+
handler: (args) => {
|
|
11539
|
+
const id = String(args.layerId ?? "");
|
|
11540
|
+
const layer = controller.getLayer(id);
|
|
11541
|
+
if (!layer) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
11542
|
+
if (!layer.mask) throw new Error(`Layer "${id}" has no mask. Add one with add_layer_mask, or pick a layer with a mask.`);
|
|
11543
|
+
if (layer.mask.source) throw new Error(`Layer "${id}" has a computed (depth) mask, not a painted one, so it cannot be loaded as a selection.`);
|
|
11544
|
+
controller.selectionFromLayerMask(id, parseSelectionMode(args.mode), args.invert === true);
|
|
11545
|
+
return summary(controller);
|
|
11546
|
+
}
|
|
11547
|
+
};
|
|
11548
|
+
const display = {
|
|
11549
|
+
name: "set_selection_display",
|
|
11550
|
+
title: "Show or hide the selection",
|
|
11551
|
+
description: "How the canvas shows the selection: ants (marching ants), quickMask (a red tint over what is not selected, to judge soft edges and feather) or none (hidden; it still limits edits). View state only: no undo step, not saved. Returns the display and the selection.",
|
|
11552
|
+
inputSchema: {
|
|
11553
|
+
type: "object",
|
|
11554
|
+
properties: { display: { type: "string", enum: ["ants", "quickMask", "none"], description: "ants, quickMask or none." } },
|
|
11555
|
+
required: ["display"],
|
|
11556
|
+
additionalProperties: false
|
|
11557
|
+
},
|
|
11558
|
+
annotations: writeAnnotations("Show or hide the selection", true),
|
|
11559
|
+
handler: (args) => {
|
|
11560
|
+
controller.setSelectionDisplay(args.display);
|
|
11561
|
+
return { display: controller.getSelectionDisplay(), selection: summary(controller) };
|
|
11562
|
+
}
|
|
11563
|
+
};
|
|
11564
|
+
const sampleTolerance = {
|
|
11565
|
+
tolerance: { type: "number", minimum: 0, maximum: 255, description: "Largest per-channel difference from a selected colour, 0..255 (default 32, as magic_wand)." },
|
|
11566
|
+
sampleMerged: sampleMergedSchema,
|
|
11567
|
+
sample: sampleSchema
|
|
11568
|
+
};
|
|
11569
|
+
const growTool = {
|
|
11570
|
+
name: "grow_selection",
|
|
11571
|
+
title: "Grow the selection by colour",
|
|
11572
|
+
description: "Select \u203A Grow: adds the pixels connected to the selection whose colour is within tolerance of a selected colour (to the nearest 4 levels). Use it after a magic_wand click that missed shades of the same area. Unlike modify_selection expand, it follows colour, not distance. Fails when nothing is selected. One undoable step. Returns the selection.",
|
|
11573
|
+
inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
|
|
11574
|
+
annotations: writeAnnotations("Grow the selection by colour"),
|
|
11575
|
+
handler: async (args) => {
|
|
11576
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select part of the area first (magic_wand, select_rect).");
|
|
11577
|
+
await controller.growSelection({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
|
|
11578
|
+
return summary(controller);
|
|
11579
|
+
}
|
|
11580
|
+
};
|
|
11581
|
+
const similarTool = {
|
|
11582
|
+
name: "select_similar",
|
|
11583
|
+
title: "Select similar colours",
|
|
11584
|
+
description: "Select \u203A Similar: adds every pixel in the image whose colour is within tolerance of a selected colour, connected or not. Fails when nothing is selected. One undoable step. Returns the selection.",
|
|
11585
|
+
inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
|
|
11586
|
+
annotations: writeAnnotations("Select similar colours"),
|
|
11587
|
+
handler: async (args) => {
|
|
11588
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select a sample of the colour first.");
|
|
11589
|
+
await controller.selectSimilar({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
|
|
11590
|
+
return summary(controller);
|
|
11591
|
+
}
|
|
11592
|
+
};
|
|
11593
|
+
const lineTool = {
|
|
11594
|
+
name: "select_line",
|
|
11595
|
+
title: "Select a single row or column",
|
|
11596
|
+
description: "Single Row / Single Column Marquee: selects a 1 px line across the whole image, a row at y = position or a column at x = position (natural image pixels). Use it to fix a dead row or column of pixels, or to stretch an edge line. One undoable step. Returns the selection.",
|
|
11597
|
+
inputSchema: {
|
|
11598
|
+
type: "object",
|
|
11599
|
+
properties: {
|
|
11600
|
+
orientation: { type: "string", enum: ["row", "column"], description: "row (horizontal, at y) or column (vertical, at x)." },
|
|
11601
|
+
position: { type: "number", minimum: 0, description: "y of the row or x of the column, in natural image pixels." },
|
|
11602
|
+
mode: modeSchema
|
|
11603
|
+
},
|
|
11604
|
+
required: ["orientation", "position"],
|
|
11605
|
+
additionalProperties: false
|
|
11606
|
+
},
|
|
11607
|
+
annotations: writeAnnotations("Select a single row or column", true),
|
|
11608
|
+
handler: (args) => {
|
|
11609
|
+
if (args.orientation !== "row" && args.orientation !== "column") throw new Error("orientation must be row or column.");
|
|
11610
|
+
controller.selectLine(args.orientation, num2(args.position, NaN), { mode: parseSelectionMode(args.mode) });
|
|
11611
|
+
return summary(controller);
|
|
11612
|
+
}
|
|
11613
|
+
};
|
|
11614
|
+
const reselectTool = {
|
|
11615
|
+
name: "reselect",
|
|
11616
|
+
title: "Reselect",
|
|
11617
|
+
description: "Select \u203A Reselect: brings back the last selection that deselect (or select_region none) cleared. Fails when there is none (for example after reopening the document). One undoable step. Returns the selection.",
|
|
11618
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11619
|
+
annotations: writeAnnotations("Reselect", true),
|
|
11620
|
+
handler: () => {
|
|
11621
|
+
if (!controller.canReselect()) throw new Error("There is no cleared selection to bring back.");
|
|
11622
|
+
controller.reselect();
|
|
11623
|
+
return summary(controller);
|
|
11624
|
+
}
|
|
11625
|
+
};
|
|
11626
|
+
const transparencyTool = {
|
|
11627
|
+
name: "select_layer_transparency",
|
|
11628
|
+
title: "Select a layer's pixels",
|
|
11629
|
+
description: "Load Selection from a layer's transparency (Ctrl/\u2318-click its thumbnail): selects where the layer has pixels, soft where it is partly transparent, combined by mode. Use it to select a cutout, a shape or text exactly. Pixel, shape, type and smart object layers. One undoable step. Returns the selection.",
|
|
11630
|
+
inputSchema: {
|
|
11631
|
+
type: "object",
|
|
11632
|
+
properties: {
|
|
11633
|
+
layerId: { type: "string", minLength: 1, description: "The layer whose pixels to select (see list_layers)." },
|
|
11634
|
+
mode: modeSchema,
|
|
11635
|
+
feather: featherSchema
|
|
11636
|
+
},
|
|
11637
|
+
required: ["layerId"],
|
|
11638
|
+
additionalProperties: false
|
|
11639
|
+
},
|
|
11640
|
+
annotations: writeAnnotations("Select a layer's pixels", true),
|
|
11641
|
+
handler: (args) => {
|
|
11642
|
+
const id = String(args.layerId ?? "");
|
|
11643
|
+
if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
11644
|
+
controller.selectLayerTransparency(id, { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0) });
|
|
11645
|
+
return summary(controller);
|
|
11646
|
+
}
|
|
11647
|
+
};
|
|
11648
|
+
const colorSchema = { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "Colour #rrggbb (default the foreground colour)." };
|
|
11649
|
+
const layerIdSchema = { type: "string", minLength: 1, description: "The layer (see list_layers; default the active layer)." };
|
|
11650
|
+
const requireLayerId = (v) => {
|
|
11651
|
+
if (v === void 0) return void 0;
|
|
11652
|
+
const id = String(v);
|
|
11653
|
+
if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
11654
|
+
return id;
|
|
11655
|
+
};
|
|
11656
|
+
const strokeTool = {
|
|
11657
|
+
name: "stroke_selection",
|
|
11658
|
+
title: "Stroke the selection",
|
|
11659
|
+
description: "Edit \u203A Stroke: paints a band width px wide along the selection edge, outside it (default), inside it or centred on it, with color, opacity and blend, on a layer's pixels or mask (channel). Use it for outlines, frames and borders that follow a shape. Fails when nothing is selected. One undo step. Returns the layer, channel and changed bounds.",
|
|
11660
|
+
inputSchema: {
|
|
11661
|
+
type: "object",
|
|
11662
|
+
properties: {
|
|
11663
|
+
width: { type: "number", exclusiveMinimum: 0, maximum: 250, description: "Band width in pixels." },
|
|
11664
|
+
location: { type: "string", enum: ["outside", "inside", "center"], description: "Where the band lies against the edge (default outside)." },
|
|
11665
|
+
color: colorSchema,
|
|
11666
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
|
|
11667
|
+
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
|
|
11668
|
+
layerId: layerIdSchema,
|
|
11669
|
+
channel: { type: "string", enum: ["pixels", "mask"], description: "Paint the pixels (default) or the layer mask." }
|
|
11670
|
+
},
|
|
11671
|
+
required: ["width"],
|
|
11672
|
+
additionalProperties: false
|
|
11673
|
+
},
|
|
11674
|
+
annotations: writeAnnotations("Stroke the selection"),
|
|
11675
|
+
handler: (args) => {
|
|
11676
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select the shape to outline first.");
|
|
11677
|
+
return controller.strokeSelection({
|
|
11678
|
+
width: num2(args.width, NaN),
|
|
11679
|
+
location: args.location,
|
|
11680
|
+
color: args.color,
|
|
11681
|
+
opacity: args.opacity,
|
|
11682
|
+
blend: args.blend,
|
|
11683
|
+
layerId: requireLayerId(args.layerId),
|
|
11684
|
+
channel: args.channel
|
|
11685
|
+
});
|
|
11686
|
+
}
|
|
11687
|
+
};
|
|
11688
|
+
const cropTool = {
|
|
11689
|
+
name: "crop_to_selection",
|
|
11690
|
+
title: "Crop to the selection",
|
|
11691
|
+
description: "Image \u203A Crop to the selection: sets the crop box to the selection's bounds (non-destructive; reset_crop undoes it). Fails when nothing is selected. One undo step. Returns the crop box in natural image pixels.",
|
|
11692
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11693
|
+
annotations: writeAnnotations("Crop to the selection", true),
|
|
11694
|
+
handler: () => {
|
|
11695
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select the area to keep first.");
|
|
11696
|
+
return { crop: controller.cropToSelection() };
|
|
11697
|
+
}
|
|
11698
|
+
};
|
|
11699
|
+
const fillTool = {
|
|
11700
|
+
name: "content_aware_fill",
|
|
11701
|
+
title: "Content-Aware Fill",
|
|
11702
|
+
description: "Edit \u203A Content-Aware Fill: removes what is selected by filling it with texture and edges synthesised from around it (on a pixel layer, default the active one). Select the object with a small margin (expand 2\u20134 px) first. output current paints into the layer; new-layer puts the fill on its own layer above it, so it can be masked or removed (recommended). sampleRadius limits where it samples from. Deterministic for a seed. 8-bit documents. One undo step. Returns the layer id and filled bounds.",
|
|
11703
|
+
inputSchema: {
|
|
11704
|
+
type: "object",
|
|
11705
|
+
properties: {
|
|
11706
|
+
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
|
|
11707
|
+
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
11708
|
+
seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
|
|
11709
|
+
layerId: layerIdSchema
|
|
11710
|
+
},
|
|
11711
|
+
additionalProperties: false
|
|
11712
|
+
},
|
|
11713
|
+
annotations: writeAnnotations("Content-Aware Fill"),
|
|
11714
|
+
handler: (args) => {
|
|
11715
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
|
|
11716
|
+
return controller.contentAwareFill({
|
|
11717
|
+
output: args.output,
|
|
11718
|
+
sampleRadius: args.sampleRadius,
|
|
11719
|
+
seed: args.seed,
|
|
11720
|
+
layerId: requireLayerId(args.layerId)
|
|
11721
|
+
});
|
|
11722
|
+
}
|
|
11723
|
+
};
|
|
11724
|
+
const copyTool = {
|
|
11725
|
+
name: "copy_pixels",
|
|
11726
|
+
title: "Copy pixels",
|
|
11727
|
+
description: "Edit \u203A Copy: puts the selected pixels of a pixel layer (default the active one), or with merged everything visible (Copy Merged), on the editor clipboard, cropped to the selection with its soft edge as alpha. Without a selection, the whole canvas. The document does not change. Returns the copied size and where it came from; then paste_pixels.",
|
|
11728
|
+
inputSchema: {
|
|
11729
|
+
type: "object",
|
|
11730
|
+
properties: { layerId: layerIdSchema, merged: { type: "boolean", description: "Copy the visible image of all layers (default false)." } },
|
|
11731
|
+
additionalProperties: false
|
|
11732
|
+
},
|
|
11733
|
+
annotations: { title: "Copy pixels", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
11734
|
+
handler: (args) => {
|
|
11735
|
+
const { width, height, x, y } = controller.copyPixels({ layerId: requireLayerId(args.layerId), merged: args.merged === true });
|
|
11736
|
+
return { width, height, x, y };
|
|
11737
|
+
}
|
|
11738
|
+
};
|
|
11739
|
+
const cutTool = {
|
|
11740
|
+
name: "cut_pixels",
|
|
11741
|
+
title: "Cut pixels",
|
|
11742
|
+
description: "Edit \u203A Cut: copies the selected pixels of a pixel layer (default the active one) to the editor clipboard and clears them from the layer (transparent, or the background colour on a transparency-locked layer). One undo step. Returns the copied size and position; then paste_pixels.",
|
|
11743
|
+
inputSchema: { type: "object", properties: { layerId: layerIdSchema }, additionalProperties: false },
|
|
11744
|
+
annotations: { title: "Cut pixels", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11745
|
+
handler: (args) => {
|
|
11746
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select what to cut first (cutting everything would empty the layer).");
|
|
11747
|
+
const { width, height, x, y } = controller.cutPixels({ layerId: requireLayerId(args.layerId) });
|
|
11748
|
+
return { width, height, x, y };
|
|
11749
|
+
}
|
|
11750
|
+
};
|
|
11751
|
+
const pasteTool = {
|
|
11752
|
+
name: "paste_pixels",
|
|
11753
|
+
title: "Paste pixels",
|
|
11754
|
+
description: "Edit \u203A Paste the editor clipboard (from copy_pixels or cut_pixels) as a new pixel layer above the active one: new-layer (default, centred on the selection or the canvas), in-place (where it was copied), into (centred in the selection, masked by it: Paste Into) or outside (masked by its inverse). One undo step. Returns the new layerId, its bounds and whether it is masked.",
|
|
11755
|
+
inputSchema: {
|
|
11756
|
+
type: "object",
|
|
11757
|
+
properties: { mode: { type: "string", enum: ["new-layer", "in-place", "into", "outside"], description: "Where and how to paste (default new-layer)." } },
|
|
11758
|
+
additionalProperties: false
|
|
11759
|
+
},
|
|
11760
|
+
annotations: writeAnnotations("Paste pixels"),
|
|
11761
|
+
handler: (args) => controller.pastePixels(args.mode ?? "new-layer")
|
|
11762
|
+
};
|
|
11763
|
+
const channelIdSchema = { type: "string", minLength: 1, description: "The alpha channel (see list_channels)." };
|
|
11764
|
+
const channelResult = (id) => controller.describeChannels().find((c) => c.id === id);
|
|
11765
|
+
const listChannels = {
|
|
11766
|
+
name: "list_channels",
|
|
11767
|
+
title: "List alpha channels",
|
|
11768
|
+
description: "Lists the alpha channels (saved selections kept with the document): id, name, bounds, percent of the image covered, overlay color, opacity, what the tint indicates and whether it is visible. Also whether Quick Mask mode is on.",
|
|
11769
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11770
|
+
annotations: { title: "List alpha channels", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
11771
|
+
handler: () => ({ channels: controller.describeChannels(), quickMask: controller.quickMaskEditing })
|
|
11772
|
+
};
|
|
11773
|
+
const saveSelectionTool = {
|
|
11774
|
+
name: "save_selection",
|
|
11775
|
+
title: "Save the selection to a channel",
|
|
11776
|
+
description: 'Select \u203A Save Selection: stores the selection in a new alpha channel (name, default "Alpha n"), or with channelId combines it into that channel by mode (replace, add, subtract, intersect). Save any selection that took work, so it can be loaded again with load_selection_from_channel. Fails when nothing is selected. One undo step. Returns the channel.',
|
|
11777
|
+
inputSchema: {
|
|
11778
|
+
type: "object",
|
|
11779
|
+
properties: { channelId: channelIdSchema, name: { type: "string", minLength: 1, description: "Name of a new channel, or a new name for channelId." }, mode: modeSchema },
|
|
11780
|
+
additionalProperties: false
|
|
11781
|
+
},
|
|
11782
|
+
annotations: writeAnnotations("Save the selection to a channel"),
|
|
11783
|
+
handler: (args) => {
|
|
11784
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select first, then save it.");
|
|
11785
|
+
const c = controller.saveSelection({ channelId: args.channelId, name: args.name, mode: parseSelectionMode(args.mode) });
|
|
11786
|
+
return { channel: channelResult(c.id) };
|
|
11787
|
+
}
|
|
11788
|
+
};
|
|
11789
|
+
const loadChannelTool = {
|
|
11790
|
+
name: "load_selection_from_channel",
|
|
11791
|
+
title: "Load a selection from a channel",
|
|
11792
|
+
description: "Select \u203A Load Selection from an alpha channel (invert for its inverse), combined with the current selection by mode. One undo step. Returns the selection.",
|
|
11793
|
+
inputSchema: {
|
|
11794
|
+
type: "object",
|
|
11795
|
+
properties: { channelId: channelIdSchema, invert: { type: "boolean", description: "Load the inverse (default false)." }, mode: modeSchema },
|
|
11796
|
+
required: ["channelId"],
|
|
11797
|
+
additionalProperties: false
|
|
11798
|
+
},
|
|
11799
|
+
annotations: writeAnnotations("Load a selection from a channel", true),
|
|
11800
|
+
handler: (args) => {
|
|
11801
|
+
controller.loadSelectionFromChannel(String(args.channelId), { mode: parseSelectionMode(args.mode), invert: args.invert === true });
|
|
11802
|
+
return summary(controller);
|
|
11803
|
+
}
|
|
11804
|
+
};
|
|
11805
|
+
const channelPropsTool = {
|
|
11806
|
+
name: "set_channel_properties",
|
|
11807
|
+
title: "Change an alpha channel",
|
|
11808
|
+
description: "Renames an alpha channel or changes its overlay: color (#rrggbb), opacity (0..1), indicates (masked: tint the unselected area, or selected) and visible (show the tint on the canvas). Pixels do not change. One undo step. Returns the channel.",
|
|
11809
|
+
inputSchema: {
|
|
11810
|
+
type: "object",
|
|
11811
|
+
properties: {
|
|
11812
|
+
channelId: channelIdSchema,
|
|
11813
|
+
name: { type: "string", minLength: 1, description: "New name." },
|
|
11814
|
+
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Overlay colour #rrggbb." },
|
|
11815
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Overlay opacity 0..1." },
|
|
11816
|
+
indicates: { type: "string", enum: ["masked", "selected"], description: "Which area the tint covers." },
|
|
11817
|
+
visible: { type: "boolean", description: "Show the channel as a tint on the canvas." }
|
|
11818
|
+
},
|
|
11819
|
+
required: ["channelId"],
|
|
11820
|
+
additionalProperties: false
|
|
11821
|
+
},
|
|
11822
|
+
annotations: writeAnnotations("Change an alpha channel", true),
|
|
11823
|
+
handler: (args) => {
|
|
11824
|
+
const { channelId, ...props } = args;
|
|
11825
|
+
const c = controller.setChannelProperties(String(channelId), props);
|
|
11826
|
+
return { channel: channelResult(c.id) };
|
|
11827
|
+
}
|
|
11828
|
+
};
|
|
11829
|
+
const duplicateChannelTool = {
|
|
11830
|
+
name: "duplicate_channel",
|
|
11831
|
+
title: "Duplicate an alpha channel",
|
|
11832
|
+
description: 'Copies an alpha channel (name, default "<name> copy") just after it, to keep a version before changing it. One undo step. Returns the copy.',
|
|
11833
|
+
inputSchema: {
|
|
11834
|
+
type: "object",
|
|
11835
|
+
properties: { channelId: channelIdSchema, name: { type: "string", minLength: 1, description: "Name of the copy." } },
|
|
11836
|
+
required: ["channelId"],
|
|
11837
|
+
additionalProperties: false
|
|
11838
|
+
},
|
|
11839
|
+
annotations: writeAnnotations("Duplicate an alpha channel"),
|
|
11840
|
+
handler: (args) => ({ channel: channelResult(controller.duplicateChannel(String(args.channelId), args.name).id) })
|
|
11841
|
+
};
|
|
11842
|
+
const deleteChannelTool = {
|
|
11843
|
+
name: "delete_channel",
|
|
11844
|
+
title: "Delete an alpha channel",
|
|
11845
|
+
description: "Deletes an alpha channel (undo_image brings it back). Returns the remaining channels.",
|
|
11846
|
+
inputSchema: { type: "object", properties: { channelId: channelIdSchema }, required: ["channelId"], additionalProperties: false },
|
|
11847
|
+
annotations: { title: "Delete an alpha channel", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11848
|
+
handler: (args) => {
|
|
11849
|
+
controller.deleteChannel(String(args.channelId));
|
|
11850
|
+
return { channels: controller.describeChannels() };
|
|
11851
|
+
}
|
|
11852
|
+
};
|
|
11853
|
+
const quickMaskTool = {
|
|
11854
|
+
name: "set_quick_mask",
|
|
11855
|
+
title: "Quick Mask mode",
|
|
11856
|
+
description: "Turns Quick Mask mode on or off (Q). On, the selection shows as a red tint and paint_stroke, fill_pixels, clear_pixels, stroke_selection and the retouch tools called without layerId edit the selection itself: white selects, black deselects, grey selects partly, and blur_stroke softens its edge. Each edit is one undo step. Turn it off before painting layers again. Returns the mode and the selection.",
|
|
11857
|
+
inputSchema: {
|
|
11858
|
+
type: "object",
|
|
11859
|
+
properties: { enabled: { type: "boolean", description: "true to enter Quick Mask mode, false to leave it." } },
|
|
11860
|
+
required: ["enabled"],
|
|
11861
|
+
additionalProperties: false
|
|
11862
|
+
},
|
|
11863
|
+
annotations: writeAnnotations("Quick Mask mode", true),
|
|
11864
|
+
handler: (args) => {
|
|
11865
|
+
if (typeof args.enabled !== "boolean") throw new Error("enabled must be true or false.");
|
|
11866
|
+
controller.setQuickMaskMode(args.enabled);
|
|
11867
|
+
return { quickMask: controller.quickMaskEditing, selection: summary(controller) };
|
|
11868
|
+
}
|
|
11869
|
+
};
|
|
11870
|
+
return [
|
|
11871
|
+
getSelection,
|
|
11872
|
+
selectAll,
|
|
11873
|
+
deselect,
|
|
11874
|
+
invert2,
|
|
11875
|
+
selectRect,
|
|
11876
|
+
selectEllipse,
|
|
11877
|
+
selectPolygonTool,
|
|
11878
|
+
lineTool,
|
|
11879
|
+
magicWandTool,
|
|
11880
|
+
colorRangeTool,
|
|
11881
|
+
loadFromImage,
|
|
11882
|
+
loadFromMask,
|
|
11883
|
+
transparencyTool,
|
|
11884
|
+
growTool,
|
|
11885
|
+
similarTool,
|
|
11886
|
+
reselectTool,
|
|
11887
|
+
display,
|
|
11888
|
+
selectRegion,
|
|
11889
|
+
selectByColor,
|
|
11890
|
+
modify,
|
|
11891
|
+
refine,
|
|
11892
|
+
applyMask,
|
|
11893
|
+
strokeTool,
|
|
11894
|
+
cropTool,
|
|
11895
|
+
fillTool,
|
|
11896
|
+
copyTool,
|
|
11897
|
+
cutTool,
|
|
11898
|
+
pasteTool,
|
|
11899
|
+
listChannels,
|
|
11900
|
+
saveSelectionTool,
|
|
11901
|
+
loadChannelTool,
|
|
11902
|
+
channelPropsTool,
|
|
11903
|
+
duplicateChannelTool,
|
|
11904
|
+
deleteChannelTool,
|
|
11905
|
+
quickMaskTool
|
|
11906
|
+
];
|
|
11907
|
+
}
|
|
11908
|
+
function createImageSubjectSelectionTool(controller) {
|
|
11909
|
+
return {
|
|
11910
|
+
name: "select_subject",
|
|
11911
|
+
title: "Select a subject",
|
|
11912
|
+
description: 'Select \u203A Subject: an AI segmentation model finds the objects matching target (a description such as "the dog", "the house", "the sky") in the flattened image and selects their outlines, combined with the selection by mode. Use it to cut a subject out by its contour (then layer_via_copy for a layer of just that subject); describe one object precisely to get only that one. One undoable step. Returns the selection and the objects found (label, bounds); none found leaves the selection unchanged.',
|
|
11913
|
+
inputSchema: {
|
|
11914
|
+
type: "object",
|
|
11915
|
+
properties: {
|
|
11916
|
+
target: { type: "string", minLength: 1, description: "What to select, in words." },
|
|
11917
|
+
maxSegments: {
|
|
11918
|
+
type: "integer",
|
|
11919
|
+
minimum: 1,
|
|
11920
|
+
maximum: 32,
|
|
11921
|
+
description: "Select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
|
|
11922
|
+
},
|
|
11923
|
+
mode: modeSchema,
|
|
11924
|
+
feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
|
|
11925
|
+
},
|
|
11926
|
+
required: ["target"],
|
|
10536
11927
|
additionalProperties: false
|
|
10537
11928
|
},
|
|
10538
11929
|
annotations: { ...writeAnnotations("Select a subject"), openWorldHint: true },
|
|
@@ -11272,12 +12663,12 @@ function createImageLayerTools(controller) {
|
|
|
11272
12663
|
const addMask = {
|
|
11273
12664
|
name: "add_layer_mask",
|
|
11274
12665
|
title: "Add a layer mask",
|
|
11275
|
-
description: "Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal)
|
|
12666
|
+
description: "Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal), selection (Reveal Selection: the selection's coverage), hide-selection (its inverse) or transparency (the layer's own alpha, for pixel, shape, type and smart object layers). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.",
|
|
11276
12667
|
inputSchema: {
|
|
11277
12668
|
type: "object",
|
|
11278
12669
|
properties: {
|
|
11279
12670
|
layerId: LAYER_ID2,
|
|
11280
|
-
from: { type: "string", enum: ["reveal-all", "hide-all", "selection"], description: "What the mask starts as (default reveal-all)." },
|
|
12671
|
+
from: { type: "string", enum: ["reveal-all", "hide-all", "selection", "hide-selection", "transparency"], description: "What the mask starts as (default reveal-all)." },
|
|
11281
12672
|
density: { type: "number", minimum: 0, maximum: 1, description: "Mask density 0..1 (default 1)." },
|
|
11282
12673
|
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px (default 0)." }
|
|
11283
12674
|
},
|
|
@@ -11932,6 +13323,9 @@ function handlePaintShortcut(controller, e) {
|
|
|
11932
13323
|
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
11933
13324
|
const key = e.key;
|
|
11934
13325
|
switch (key.toLowerCase()) {
|
|
13326
|
+
case "q":
|
|
13327
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13328
|
+
return true;
|
|
11935
13329
|
case "b":
|
|
11936
13330
|
controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
|
|
11937
13331
|
return true;
|
|
@@ -12071,8 +13465,8 @@ var TARGET = {
|
|
|
12071
13465
|
layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
|
|
12072
13466
|
channel: {
|
|
12073
13467
|
type: "string",
|
|
12074
|
-
enum: ["pixels", "mask", "filter-mask"],
|
|
12075
|
-
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals."
|
|
13468
|
+
enum: ["pixels", "mask", "filter-mask", "quick-mask"],
|
|
13469
|
+
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals. In Quick Mask mode (set_quick_mask), quick-mask (the default when no layerId is given) paints the selection: white selects, black deselects."
|
|
12076
13470
|
}
|
|
12077
13471
|
};
|
|
12078
13472
|
var BRUSH = {
|
|
@@ -12460,8 +13854,8 @@ var TARGET2 = {
|
|
|
12460
13854
|
layerId: { type: "string", description: "Layer to retouch (default the active layer; see list_layers)." },
|
|
12461
13855
|
channel: {
|
|
12462
13856
|
type: "string",
|
|
12463
|
-
enum: ["pixels", "mask", "filter-mask"],
|
|
12464
|
-
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves."
|
|
13857
|
+
enum: ["pixels", "mask", "filter-mask", "quick-mask"],
|
|
13858
|
+
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves. In Quick Mask mode (set_quick_mask), quick-mask (the default when no layerId is given) paints the selection: white selects, black deselects."
|
|
12465
13859
|
}
|
|
12466
13860
|
};
|
|
12467
13861
|
var SAMPLE = {
|
|
@@ -13331,13 +14725,14 @@ function createImageSmartObjectTools(controller) {
|
|
|
13331
14725
|
const addFilter = {
|
|
13332
14726
|
name: "add_smart_filter",
|
|
13333
14727
|
title: "Add a smart filter",
|
|
13334
|
-
description: "Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id)
|
|
14728
|
+
description: "Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. With a selection (and useSelection, default true), the filter mask is set from it in the same step. One undo step. Returns the filter (with its id), the stack and masked.",
|
|
13335
14729
|
inputSchema: {
|
|
13336
14730
|
type: "object",
|
|
13337
14731
|
properties: {
|
|
13338
14732
|
layerId: LAYER_ID4,
|
|
13339
14733
|
kind: { type: "string", enum: ["develop", "blur"], description: "Filter kind." },
|
|
13340
14734
|
index: { type: "integer", minimum: 0, description: "Position in the stack, 0 = bottom, applied first (default the top)." },
|
|
14735
|
+
useSelection: { type: "boolean", description: "With a selection, set the filter mask from it so the filter only changes the selected area (default true)." },
|
|
13341
14736
|
...FILTER_SETTINGS
|
|
13342
14737
|
},
|
|
13343
14738
|
required: ["layerId", "kind"],
|
|
@@ -13346,8 +14741,9 @@ function createImageSmartObjectTools(controller) {
|
|
|
13346
14741
|
annotations: { title: "Add a smart filter", ...WRITE7 },
|
|
13347
14742
|
handler: (args) => {
|
|
13348
14743
|
const id = smartId(args.layerId);
|
|
13349
|
-
const
|
|
13350
|
-
|
|
14744
|
+
const masked = args.useSelection !== false && controller.hasSelection;
|
|
14745
|
+
const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index, { useSelection: args.useSelection !== false });
|
|
14746
|
+
return { layerId: id, filter, smartFilters: info(id).smartFilters, masked };
|
|
13351
14747
|
}
|
|
13352
14748
|
};
|
|
13353
14749
|
const updateFilter = {
|
|
@@ -14160,7 +15556,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14160
15556
|
const addLayer = {
|
|
14161
15557
|
name: "add_adjustment_layer",
|
|
14162
15558
|
title: "Add an adjustment layer",
|
|
14163
|
-
description: "Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: " + ADJUSTMENT_KINDS.map((k) => k.kind).join(", ") + ". Omitted params are neutral.",
|
|
15559
|
+
description: "Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it; with a selection it is masked by it (useSelection, default true; masked in the result says so). Kinds: " + ADJUSTMENT_KINDS.map((k) => k.kind).join(", ") + ". Omitted params are neutral.",
|
|
14164
15560
|
inputSchema: {
|
|
14165
15561
|
type: "object",
|
|
14166
15562
|
properties: {
|
|
@@ -14170,7 +15566,8 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14170
15566
|
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
|
|
14171
15567
|
name: { type: "string", description: "Layer name shown in the layers panel." },
|
|
14172
15568
|
aboveId: { type: "string", description: "Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations." },
|
|
14173
|
-
parentId: { type: "string", description: "Put the layer at the top of this group (from list_layers)." }
|
|
15569
|
+
parentId: { type: "string", description: "Put the layer at the top of this group (from list_layers)." },
|
|
15570
|
+
useSelection: { type: "boolean", description: "With a selection, mask the new layer with it so the adjustment only affects the selected area (default true; false for a global layer)." }
|
|
14174
15571
|
},
|
|
14175
15572
|
required: ["kind"],
|
|
14176
15573
|
additionalProperties: false
|
|
@@ -14182,7 +15579,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14182
15579
|
idempotentHint: false,
|
|
14183
15580
|
openWorldHint: false
|
|
14184
15581
|
},
|
|
14185
|
-
handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {
|
|
15582
|
+
handler: ({ kind, params, opacity, blend, name, aboveId, parentId, useSelection }) => {
|
|
14186
15583
|
if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind "${kind}".`);
|
|
14187
15584
|
if (blend !== void 0 && !isBlendMode(blend)) throw new Error(`Unknown blend mode "${blend}".`);
|
|
14188
15585
|
if (aboveId !== void 0 && parentId !== void 0) throw new Error("Give aboveId or parentId, not both.");
|
|
@@ -14190,9 +15587,9 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14190
15587
|
if (ref !== void 0 && !controller.getLayer(ref)) throw new Error(`Unknown layer "${ref}". Call list_layers for ids.`);
|
|
14191
15588
|
}
|
|
14192
15589
|
if (parentId !== void 0 && controller.getLayer(parentId)?.type !== "group") throw new Error(`Layer "${parentId}" is not a group.`);
|
|
14193
|
-
const id = controller.addAdjustmentLayer({ kind, params, opacity, blend, name, aboveId, parentId });
|
|
15590
|
+
const id = controller.addAdjustmentLayer({ kind, params, opacity, blend, name, aboveId, parentId, useSelection: useSelection !== false });
|
|
14194
15591
|
const layer = controller.getAdjustmentLayer(id);
|
|
14195
|
-
return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };
|
|
15592
|
+
return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id), masked: Boolean(layer.mask) };
|
|
14196
15593
|
}
|
|
14197
15594
|
};
|
|
14198
15595
|
const renderPreview = {
|
|
@@ -14326,6 +15723,14 @@ function removeCurvePoint(points, index) {
|
|
|
14326
15723
|
var round22 = (v) => Math.round(v * 100) / 100;
|
|
14327
15724
|
|
|
14328
15725
|
// src/image-editor/selection/gesture.ts
|
|
15726
|
+
function snapTo45(from, point) {
|
|
15727
|
+
const dx = point.x - from.x;
|
|
15728
|
+
const dy = point.y - from.y;
|
|
15729
|
+
const len = Math.hypot(dx, dy);
|
|
15730
|
+
if (len === 0) return point;
|
|
15731
|
+
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
15732
|
+
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
15733
|
+
}
|
|
14329
15734
|
var SelectionGesture = class {
|
|
14330
15735
|
constructor(controller, getOptions) {
|
|
14331
15736
|
this.controller = controller;
|
|
@@ -14337,13 +15742,20 @@ var SelectionGesture = class {
|
|
|
14337
15742
|
points = [];
|
|
14338
15743
|
current = null;
|
|
14339
15744
|
modifiers = {};
|
|
15745
|
+
/** Polygonal lasso corners while one is being drawn. */
|
|
15746
|
+
polygon = null;
|
|
15747
|
+
pressedAt = null;
|
|
15748
|
+
freehand = false;
|
|
15749
|
+
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
15750
|
+
closeRadius = 8;
|
|
14340
15751
|
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
14341
15752
|
error = null;
|
|
14342
15753
|
get active() {
|
|
14343
|
-
return this.start !== null;
|
|
15754
|
+
return this.start !== null || this.polygon !== null;
|
|
14344
15755
|
}
|
|
14345
15756
|
/** The outline of the shape being drawn, or `null`. */
|
|
14346
15757
|
get draftPath() {
|
|
15758
|
+
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
14347
15759
|
if (!this.start || !this.current) return null;
|
|
14348
15760
|
const { tool } = this.getOptions();
|
|
14349
15761
|
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
@@ -14366,18 +15778,71 @@ var SelectionGesture = class {
|
|
|
14366
15778
|
async begin(point, modifiers = {}) {
|
|
14367
15779
|
const options = this.getOptions();
|
|
14368
15780
|
this.error = null;
|
|
15781
|
+
if (this.polygon && options.tool !== "polygon") this.cancel();
|
|
15782
|
+
if (options.tool === "polygon") {
|
|
15783
|
+
this.pressedAt = point;
|
|
15784
|
+
this.freehand = false;
|
|
15785
|
+
if (!this.polygon) {
|
|
15786
|
+
this.modifiers = modifiers;
|
|
15787
|
+
this.polygon = [point];
|
|
15788
|
+
this.current = point;
|
|
15789
|
+
return;
|
|
15790
|
+
}
|
|
15791
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
15792
|
+
const p = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15793
|
+
const first = this.polygon[0];
|
|
15794
|
+
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
15795
|
+
this.close();
|
|
15796
|
+
return;
|
|
15797
|
+
}
|
|
15798
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
15799
|
+
this.current = p;
|
|
15800
|
+
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
15801
|
+
return;
|
|
15802
|
+
}
|
|
14369
15803
|
this.modifiers = modifiers;
|
|
14370
|
-
if (options.tool === "
|
|
15804
|
+
if (options.tool === "row" || options.tool === "column") {
|
|
14371
15805
|
this.start = null;
|
|
14372
15806
|
try {
|
|
15807
|
+
this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
|
|
15808
|
+
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
15809
|
+
feather: options.feather ?? 0
|
|
15810
|
+
});
|
|
15811
|
+
} catch (err) {
|
|
15812
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
15813
|
+
}
|
|
15814
|
+
return;
|
|
15815
|
+
}
|
|
15816
|
+
if (options.tool === "wand" || options.tool === "range") {
|
|
15817
|
+
this.start = null;
|
|
15818
|
+
const common = {
|
|
15819
|
+
sampleMerged: options.sampleMerged,
|
|
15820
|
+
sample: options.sample,
|
|
15821
|
+
feather: options.feather ?? 0,
|
|
15822
|
+
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
15823
|
+
};
|
|
15824
|
+
try {
|
|
15825
|
+
if (options.tool === "range") {
|
|
15826
|
+
const pixels = await this.controller.getSamplePixels(common);
|
|
15827
|
+
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
15828
|
+
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
|
|
15829
|
+
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
|
|
15830
|
+
const p = (y * pixels.width + x) * 4;
|
|
15831
|
+
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
15832
|
+
await this.controller.selectColorRange([{ color, at: point }], {
|
|
15833
|
+
...common,
|
|
15834
|
+
fuzziness: options.fuzziness ?? 40,
|
|
15835
|
+
localized: options.localized ? options.localized : void 0,
|
|
15836
|
+
invert: options.rangeInvert ?? false
|
|
15837
|
+
});
|
|
15838
|
+
return;
|
|
15839
|
+
}
|
|
14373
15840
|
await this.controller.magicWand(point, {
|
|
15841
|
+
...common,
|
|
14374
15842
|
tolerance: options.tolerance,
|
|
14375
15843
|
contiguous: options.contiguous,
|
|
14376
15844
|
antiAlias: options.antiAlias,
|
|
14377
|
-
distance: options.distance
|
|
14378
|
-
sampleMerged: options.sampleMerged,
|
|
14379
|
-
sample: options.sample,
|
|
14380
|
-
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
15845
|
+
distance: options.distance
|
|
14381
15846
|
});
|
|
14382
15847
|
} catch (err) {
|
|
14383
15848
|
this.error = err instanceof Error ? err.message : String(err);
|
|
@@ -14389,7 +15854,18 @@ var SelectionGesture = class {
|
|
|
14389
15854
|
this.points = [point];
|
|
14390
15855
|
}
|
|
14391
15856
|
/** Pointer move; returns true when the draft changed (re-render). */
|
|
14392
|
-
move(point) {
|
|
15857
|
+
move(point, modifiers = {}) {
|
|
15858
|
+
if (this.polygon) {
|
|
15859
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
15860
|
+
if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
|
|
15861
|
+
this.freehand = true;
|
|
15862
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
|
|
15863
|
+
this.current = null;
|
|
15864
|
+
return true;
|
|
15865
|
+
}
|
|
15866
|
+
this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15867
|
+
return true;
|
|
15868
|
+
}
|
|
14393
15869
|
if (!this.start) return false;
|
|
14394
15870
|
this.current = point;
|
|
14395
15871
|
if (this.getOptions().tool === "lasso") {
|
|
@@ -14401,6 +15877,12 @@ var SelectionGesture = class {
|
|
|
14401
15877
|
}
|
|
14402
15878
|
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
14403
15879
|
end(point, modifiers) {
|
|
15880
|
+
if (this.polygon) {
|
|
15881
|
+
if (point && this.freehand) this.move(point);
|
|
15882
|
+
this.pressedAt = null;
|
|
15883
|
+
this.freehand = false;
|
|
15884
|
+
return false;
|
|
15885
|
+
}
|
|
14404
15886
|
if (!this.start) return false;
|
|
14405
15887
|
if (point) this.move(point);
|
|
14406
15888
|
const options = this.getOptions();
|
|
@@ -14416,17 +15898,70 @@ var SelectionGesture = class {
|
|
|
14416
15898
|
this.current = null;
|
|
14417
15899
|
this.points = [];
|
|
14418
15900
|
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
14419
|
-
const opts = { mode, antiAlias: options.antiAlias };
|
|
15901
|
+
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
14420
15902
|
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
14421
15903
|
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
14422
15904
|
}
|
|
14423
|
-
/**
|
|
15905
|
+
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
15906
|
+
close() {
|
|
15907
|
+
const polygon = this.polygon;
|
|
15908
|
+
if (!polygon) return false;
|
|
15909
|
+
const options = this.getOptions();
|
|
15910
|
+
this.cancel();
|
|
15911
|
+
if (polygon.length < 3) return false;
|
|
15912
|
+
return this.controller.selectPolygon(polygon, {
|
|
15913
|
+
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
15914
|
+
antiAlias: options.antiAlias,
|
|
15915
|
+
feather: options.feather ?? 0
|
|
15916
|
+
});
|
|
15917
|
+
}
|
|
15918
|
+
/**
|
|
15919
|
+
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
15920
|
+
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
15921
|
+
*/
|
|
15922
|
+
key(key) {
|
|
15923
|
+
if (!this.polygon) return false;
|
|
15924
|
+
if (key === "Enter") this.close();
|
|
15925
|
+
else if (key === "Escape") this.cancel();
|
|
15926
|
+
else if (key === "Backspace" || key === "Delete") {
|
|
15927
|
+
this.polygon.pop();
|
|
15928
|
+
if (!this.polygon.length) this.cancel();
|
|
15929
|
+
} else return false;
|
|
15930
|
+
return true;
|
|
15931
|
+
}
|
|
15932
|
+
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
14424
15933
|
cancel() {
|
|
14425
15934
|
this.start = null;
|
|
14426
15935
|
this.current = null;
|
|
14427
15936
|
this.points = [];
|
|
15937
|
+
this.polygon = null;
|
|
15938
|
+
this.pressedAt = null;
|
|
15939
|
+
this.freehand = false;
|
|
14428
15940
|
}
|
|
14429
15941
|
};
|
|
15942
|
+
function handleSelectionShortcut(controller, e, onError) {
|
|
15943
|
+
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
15944
|
+
const key = e.key.toLowerCase();
|
|
15945
|
+
const run = (fn) => {
|
|
15946
|
+
try {
|
|
15947
|
+
fn();
|
|
15948
|
+
} catch (err) {
|
|
15949
|
+
onError?.(err instanceof Error ? err.message : String(err));
|
|
15950
|
+
}
|
|
15951
|
+
return true;
|
|
15952
|
+
};
|
|
15953
|
+
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
15954
|
+
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
15955
|
+
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
15956
|
+
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
15957
|
+
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
15958
|
+
return false;
|
|
15959
|
+
}
|
|
15960
|
+
function handleQuickMaskKey(controller, e) {
|
|
15961
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
15962
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
15963
|
+
return true;
|
|
15964
|
+
}
|
|
14430
15965
|
|
|
14431
15966
|
// src/image-editor/mosaic-filter.ts
|
|
14432
15967
|
function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
|
|
@@ -14566,6 +16101,21 @@ function applyRegionBlur(imageData, regions, radius = 10) {
|
|
|
14566
16101
|
}
|
|
14567
16102
|
|
|
14568
16103
|
// src/image-editor/layers/panel.ts
|
|
16104
|
+
function maskThumbnailClick(controller, layer, e) {
|
|
16105
|
+
if (!layer.mask) return;
|
|
16106
|
+
if (e.ctrlKey || e.metaKey) {
|
|
16107
|
+
controller.selectionFromLayerMask(layer.id, selectionModeFromModifiers(e, "replace"));
|
|
16108
|
+
} else if (e.shiftKey) {
|
|
16109
|
+
controller.setLayerMaskProps(layer.id, { enabled: layer.mask.enabled === false });
|
|
16110
|
+
} else {
|
|
16111
|
+
controller.setActiveLayer(layer.id, "mask");
|
|
16112
|
+
}
|
|
16113
|
+
}
|
|
16114
|
+
function layerThumbnailClick(controller, layer, e) {
|
|
16115
|
+
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
16116
|
+
controller.selectLayerTransparency(layer.id, { mode: selectionModeFromModifiers(e, "replace") });
|
|
16117
|
+
return true;
|
|
16118
|
+
}
|
|
14569
16119
|
function layerPanelRows(layers, activeId) {
|
|
14570
16120
|
const rows = [];
|
|
14571
16121
|
const walk = (list, depth, parentId) => {
|
|
@@ -14637,6 +16187,7 @@ function layerMenu(controller) {
|
|
|
14637
16187
|
const below = loc ? siblings[loc.index - 1] : void 0;
|
|
14638
16188
|
const hasSel = controller.hasSelection;
|
|
14639
16189
|
const isPixel = active?.type === "pixel";
|
|
16190
|
+
const hasOwnPixels = active?.type === "pixel" || active?.type === "shape" || active?.type === "type" || active?.type === "smart";
|
|
14640
16191
|
return [
|
|
14641
16192
|
{ action: "new-layer", label: "New Layer", enabled: true },
|
|
14642
16193
|
{ action: "new-group", label: "New Group", enabled: true },
|
|
@@ -14651,6 +16202,9 @@ function layerMenu(controller) {
|
|
|
14651
16202
|
{ action: "via-cut", label: "Layer via Cut", enabled: isPixel && hasSel },
|
|
14652
16203
|
{ action: "add-mask", label: "Add Layer Mask", enabled: Boolean(active) },
|
|
14653
16204
|
{ action: "add-mask-selection", label: "Add Mask from Selection", enabled: Boolean(active) && hasSel },
|
|
16205
|
+
{ action: "add-mask-hide-selection", label: "Add Mask Hiding Selection", enabled: Boolean(active) && hasSel },
|
|
16206
|
+
{ action: "add-mask-transparency", label: "Add Mask from Transparency", enabled: hasOwnPixels },
|
|
16207
|
+
{ action: "select-transparency", label: "Select Layer Pixels", enabled: hasOwnPixels },
|
|
14654
16208
|
{ action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
|
|
14655
16209
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
14656
16210
|
{ action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
|
|
@@ -14702,6 +16256,15 @@ async function runLayerMenuAction(controller, action) {
|
|
|
14702
16256
|
case "add-mask":
|
|
14703
16257
|
if (id) controller.addLayerMask(id, "reveal-all");
|
|
14704
16258
|
break;
|
|
16259
|
+
case "add-mask-hide-selection":
|
|
16260
|
+
if (id) controller.addLayerMask(id, "hide-selection");
|
|
16261
|
+
break;
|
|
16262
|
+
case "add-mask-transparency":
|
|
16263
|
+
if (id) controller.addLayerMask(id, "transparency");
|
|
16264
|
+
break;
|
|
16265
|
+
case "select-transparency":
|
|
16266
|
+
if (id) controller.selectLayerTransparency(id);
|
|
16267
|
+
break;
|
|
14705
16268
|
case "add-mask-selection":
|
|
14706
16269
|
if (id) controller.addLayerMask(id, "selection");
|
|
14707
16270
|
break;
|
|
@@ -14886,6 +16449,7 @@ async function addLayerOfKind(controller, id, init) {
|
|
|
14886
16449
|
if (isAdjustmentKind(kind)) {
|
|
14887
16450
|
return controller.addAdjustmentLayer({
|
|
14888
16451
|
kind,
|
|
16452
|
+
useSelection: true,
|
|
14889
16453
|
...init
|
|
14890
16454
|
});
|
|
14891
16455
|
}
|
|
@@ -15834,7 +17398,7 @@ function createImageEffectsToolProvider(controller, options = {}) {
|
|
|
15834
17398
|
skill: "pixerate-image-editor",
|
|
15835
17399
|
target: { noun: "layer", id: "A layer id from list_layers." },
|
|
15836
17400
|
applyUses: ["filter"],
|
|
15837
|
-
applyDetails: "The effect becomes a smart filter: it runs on the layer's pixels after the smart filters below it, with its own blend and opacity, under the smart filter mask, and can be changed or removed later. A pixel, shape or type layer (or a group) is converted to a smart object first, in the same undo step (one layer keeps its id; the result says converted). Generators and overlays go on their own layer through add_effect_layer.",
|
|
17401
|
+
applyDetails: "The effect becomes a smart filter: it runs on the layer's pixels after the smart filters below it, with its own blend and opacity, under the smart filter mask, and can be changed or removed later. A pixel, shape or type layer (or a group) is converted to a smart object first, in the same undo step (one layer keeps its id; the result says converted). With a selection, the filter mask is set from it in the same step, so the filter only changes the selected area (the result says masked); deselect first to filter the whole layer. Generators and overlays go on their own layer through add_effect_layer.",
|
|
15838
17402
|
provider: () => controller.effectsProvider,
|
|
15839
17403
|
readOnlyReason,
|
|
15840
17404
|
list: (target) => {
|
|
@@ -15842,8 +17406,8 @@ function createImageEffectsToolProvider(controller, options = {}) {
|
|
|
15842
17406
|
return layers.map((l) => ({ target: l.id, ...l.name ? { name: l.name } : {}, effects: imageLayerAppliedEffects(l) })).filter((t) => target || t.effects.length);
|
|
15843
17407
|
},
|
|
15844
17408
|
apply: async (target, effect, args) => {
|
|
15845
|
-
const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity });
|
|
15846
|
-
return result(r.layerId, r.filter.id, { converted: r.converted });
|
|
17409
|
+
const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity, useSelection: true });
|
|
17410
|
+
return result(r.layerId, r.filter.id, { converted: r.converted, masked: r.masked });
|
|
15847
17411
|
},
|
|
15848
17412
|
update: async (target, instance, changes) => {
|
|
15849
17413
|
const layer = layerOf(target);
|
|
@@ -15974,6 +17538,8 @@ export {
|
|
|
15974
17538
|
CANVAS_SIZE_PRESETS,
|
|
15975
17539
|
CLONE_TRANSFORM_CONTROLS,
|
|
15976
17540
|
COLOR_MODE_OPTIONS,
|
|
17541
|
+
COLOR_RANGE_PRESETS,
|
|
17542
|
+
COLOR_RANGE_PRESET_LABELS,
|
|
15977
17543
|
CoverageMap,
|
|
15978
17544
|
D50,
|
|
15979
17545
|
D65,
|
|
@@ -15982,6 +17548,7 @@ export {
|
|
|
15982
17548
|
DEFAULT_BLENDER,
|
|
15983
17549
|
DEFAULT_BRUSH,
|
|
15984
17550
|
DEFAULT_CANVAS_VIEWPORT,
|
|
17551
|
+
DEFAULT_CHANNEL_COLOR,
|
|
15985
17552
|
DEFAULT_DEPTH_MASK,
|
|
15986
17553
|
DEFAULT_FLOW,
|
|
15987
17554
|
DEFAULT_GLOBAL_LIGHT,
|
|
@@ -16030,6 +17597,7 @@ export {
|
|
|
16030
17597
|
HIDE,
|
|
16031
17598
|
HISTOGRAM_BINS,
|
|
16032
17599
|
HUE_RANGE_LABELS,
|
|
17600
|
+
HUE_RANGE_PRESETS,
|
|
16033
17601
|
IDENTITY_CLONE_TRANSFORM,
|
|
16034
17602
|
IDENTITY_MATRIX3,
|
|
16035
17603
|
IMAGE_ADJUSTMENT_CONTROLS,
|
|
@@ -16093,6 +17661,7 @@ export {
|
|
|
16093
17661
|
PARAGRAPH_STYLE_FIELDS,
|
|
16094
17662
|
PARAM,
|
|
16095
17663
|
PARAM_FLOATS,
|
|
17664
|
+
PASTE_MODES,
|
|
16096
17665
|
PATTERN_BLOCK_KEYS,
|
|
16097
17666
|
PIGMENT_BANDS,
|
|
16098
17667
|
PIGMENT_CHANNELS,
|
|
@@ -16124,6 +17693,7 @@ export {
|
|
|
16124
17693
|
PatternPlacement,
|
|
16125
17694
|
PatternTile,
|
|
16126
17695
|
PsdError,
|
|
17696
|
+
QUICK_MASK_TARGET,
|
|
16127
17697
|
REFINE_EDGE_CONTROLS,
|
|
16128
17698
|
RETOUCH_CHOICES,
|
|
16129
17699
|
RETOUCH_OPTION_CONTROLS,
|
|
@@ -16281,6 +17851,7 @@ export {
|
|
|
16281
17851
|
canvasToImagePoint,
|
|
16282
17852
|
caretAt,
|
|
16283
17853
|
changedTiles,
|
|
17854
|
+
channelThumbnail,
|
|
16284
17855
|
checkEffectAnimation,
|
|
16285
17856
|
checkEffectTime,
|
|
16286
17857
|
checkImageEffectPlace,
|
|
@@ -16296,6 +17867,7 @@ export {
|
|
|
16296
17867
|
colorModePlanes16,
|
|
16297
17868
|
colorPanelView,
|
|
16298
17869
|
colorRange,
|
|
17870
|
+
colorRangePreset,
|
|
16299
17871
|
combineSelections,
|
|
16300
17872
|
compileAdjustmentLayer,
|
|
16301
17873
|
componentPath,
|
|
@@ -16378,10 +17950,12 @@ export {
|
|
|
16378
17950
|
depthMaskAlpha,
|
|
16379
17951
|
depthOf,
|
|
16380
17952
|
depthRasterMask,
|
|
17953
|
+
describeChannel,
|
|
16381
17954
|
describeLayer,
|
|
16382
17955
|
describeLayerEffects,
|
|
16383
17956
|
describeSelection,
|
|
16384
17957
|
describeTransform,
|
|
17958
|
+
deserializeChannels,
|
|
16385
17959
|
deserializeColorMode,
|
|
16386
17960
|
deserializeColorProfile,
|
|
16387
17961
|
deserializeEffects,
|
|
@@ -16468,9 +18042,12 @@ export {
|
|
|
16468
18042
|
grainHeights,
|
|
16469
18043
|
greyMorphology,
|
|
16470
18044
|
groupLayers,
|
|
18045
|
+
growSelection,
|
|
16471
18046
|
guidePlanes,
|
|
16472
18047
|
guidedFilterColor,
|
|
16473
18048
|
handlePaintShortcut,
|
|
18049
|
+
handleQuickMaskKey,
|
|
18050
|
+
handleSelectionShortcut,
|
|
16474
18051
|
hasActiveEffects,
|
|
16475
18052
|
hasLayersBelow,
|
|
16476
18053
|
hasResampledLegacyMask,
|
|
@@ -16515,6 +18092,7 @@ export {
|
|
|
16515
18092
|
isBitDepth,
|
|
16516
18093
|
isBlendMode,
|
|
16517
18094
|
isBrushstrokeBrush,
|
|
18095
|
+
isColorRangePreset,
|
|
16518
18096
|
isContentsDecoded,
|
|
16519
18097
|
isDefaultDepthMask,
|
|
16520
18098
|
isGradientPaint,
|
|
@@ -16561,6 +18139,7 @@ export {
|
|
|
16561
18139
|
layerName,
|
|
16562
18140
|
layerPanelRows,
|
|
16563
18141
|
layerPanels,
|
|
18142
|
+
layerThumbnailClick,
|
|
16564
18143
|
layerTree,
|
|
16565
18144
|
layerTypeLabel,
|
|
16566
18145
|
layerWarning,
|
|
@@ -16589,6 +18168,7 @@ export {
|
|
|
16589
18168
|
mapSubpaths,
|
|
16590
18169
|
maskBounds,
|
|
16591
18170
|
maskSummary,
|
|
18171
|
+
maskThumbnailClick,
|
|
16592
18172
|
matrixFromCorners,
|
|
16593
18173
|
matrixFromParams,
|
|
16594
18174
|
measureCanvasViewport,
|
|
@@ -16784,14 +18364,17 @@ export {
|
|
|
16784
18364
|
segmentToSelection,
|
|
16785
18365
|
segmentationOutlines,
|
|
16786
18366
|
segmentsToSelection,
|
|
18367
|
+
selectSimilar,
|
|
16787
18368
|
selectionArea,
|
|
16788
18369
|
selectionFromCoverage,
|
|
16789
18370
|
selectionFromPixels,
|
|
16790
18371
|
selectionModeFromModifiers,
|
|
16791
18372
|
selectionOutline,
|
|
18373
|
+
selectionRing,
|
|
16792
18374
|
selectionToCoverage,
|
|
16793
18375
|
selectionToPixels,
|
|
16794
18376
|
selectionValueAt,
|
|
18377
|
+
serializeChannels,
|
|
16795
18378
|
serializeColorMode,
|
|
16796
18379
|
serializeColorProfile,
|
|
16797
18380
|
serializeContents,
|
|
@@ -16821,11 +18404,13 @@ export {
|
|
|
16821
18404
|
shapeOutline,
|
|
16822
18405
|
shapeRenderClip,
|
|
16823
18406
|
shiftCoverage,
|
|
18407
|
+
skinToneRange,
|
|
16824
18408
|
smartFilterRows,
|
|
16825
18409
|
smartObjectLayers,
|
|
16826
18410
|
smoothCoverage,
|
|
16827
18411
|
smoothPoints,
|
|
16828
18412
|
smudgeEffect,
|
|
18413
|
+
snapTo45,
|
|
16829
18414
|
solidPaint,
|
|
16830
18415
|
solidSurface,
|
|
16831
18416
|
solveMembrane,
|