@pixerate/editor 0.183.0 → 0.185.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-UHMSMUHK.cjs → chunk-C6TRAAG4.cjs} +3 -1
- package/dist/{chunk-Y46OMERN.js → chunk-GYYSPMVU.js} +3 -1
- package/dist/image-editor/controller.d.cts +152 -8
- package/dist/image-editor/controller.d.ts +152 -8
- package/dist/image-editor/index.cjs +1986 -832
- package/dist/image-editor/index.js +1209 -55
- package/dist/image-editor/layers/panel.d.cts +23 -1
- package/dist/image-editor/layers/panel.d.ts +23 -1
- package/dist/image-editor/selection/gesture.d.cts +40 -2
- package/dist/image-editor/selection/gesture.d.ts +40 -2
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/modify.d.cts +3 -2
- package/dist/image-editor/selection/modify.d.ts +3 -2
- 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 +8 -2
- package/dist/image-editor/tools.d.ts +8 -2
- package/dist/image-editor/types.d.cts +2 -0
- package/dist/image-editor/types.d.ts +2 -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 +12 -21
- package/skills/pixerate-image-editor/references/free-transform.md +22 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +23 -2
|
@@ -370,7 +370,7 @@ import {
|
|
|
370
370
|
writePsd,
|
|
371
371
|
xyToTempTint,
|
|
372
372
|
xyToUv
|
|
373
|
-
} from "../chunk-
|
|
373
|
+
} from "../chunk-GYYSPMVU.js";
|
|
374
374
|
import {
|
|
375
375
|
parseCssColor
|
|
376
376
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -1362,7 +1362,7 @@ var ImageEditorRenderer = class {
|
|
|
1362
1362
|
}
|
|
1363
1363
|
ctx.restore();
|
|
1364
1364
|
}
|
|
1365
|
-
const display = options.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1365
|
+
const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1366
1366
|
if (state.selection?.bounds && display !== "none" || options.draftSelectionPath?.length) {
|
|
1367
1367
|
ctx.save();
|
|
1368
1368
|
try {
|
|
@@ -3065,7 +3065,7 @@ var TONE_RANGE_PRESETS = {
|
|
|
3065
3065
|
function toneRange(pixels, range, falloff = 20) {
|
|
3066
3066
|
const n = checkPixels(pixels);
|
|
3067
3067
|
const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
|
|
3068
|
-
const
|
|
3068
|
+
const ramp2 = Math.max(1e-3, falloff);
|
|
3069
3069
|
const out = new Uint8Array(n);
|
|
3070
3070
|
const d = pixels.data;
|
|
3071
3071
|
for (let i = 0; i < n; i++) {
|
|
@@ -3073,7 +3073,7 @@ function toneRange(pixels, range, falloff = 20) {
|
|
|
3073
3073
|
if (d[p + 3] <= 0) continue;
|
|
3074
3074
|
const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
|
|
3075
3075
|
const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
|
|
3076
|
-
out[i] = coverageToByte(1 - outside /
|
|
3076
|
+
out[i] = coverageToByte(1 - outside / ramp2);
|
|
3077
3077
|
}
|
|
3078
3078
|
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3079
3079
|
}
|
|
@@ -3102,6 +3102,111 @@ function hueRange(pixels, center) {
|
|
|
3102
3102
|
}
|
|
3103
3103
|
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3104
3104
|
}
|
|
3105
|
+
var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
|
|
3106
|
+
var COLOR_RANGE_PRESETS = [
|
|
3107
|
+
...Object.keys(HUE_RANGE_PRESETS),
|
|
3108
|
+
"highlights",
|
|
3109
|
+
"midtones",
|
|
3110
|
+
"shadows",
|
|
3111
|
+
"skinTones"
|
|
3112
|
+
];
|
|
3113
|
+
function isColorRangePreset(v) {
|
|
3114
|
+
return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
|
|
3115
|
+
}
|
|
3116
|
+
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;
|
|
3117
|
+
function skinToneRange(pixels) {
|
|
3118
|
+
const n = checkPixels(pixels);
|
|
3119
|
+
const out = new Uint8Array(n);
|
|
3120
|
+
const d = pixels.data;
|
|
3121
|
+
for (let i = 0; i < n; i++) {
|
|
3122
|
+
const p = i * 4;
|
|
3123
|
+
if (d[p + 3] <= 0) continue;
|
|
3124
|
+
const r = d[p] / 255;
|
|
3125
|
+
const g = d[p + 1] / 255;
|
|
3126
|
+
const b = d[p + 2] / 255;
|
|
3127
|
+
const mx = Math.max(r, g, b);
|
|
3128
|
+
const mn = Math.min(r, g, b);
|
|
3129
|
+
const chroma = mx - mn;
|
|
3130
|
+
if (chroma <= 0 || mx <= 0) continue;
|
|
3131
|
+
let hue;
|
|
3132
|
+
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
3133
|
+
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
3134
|
+
else hue = (r - g) / chroma + 4;
|
|
3135
|
+
hue *= 60;
|
|
3136
|
+
if (hue > 180) hue -= 360;
|
|
3137
|
+
const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
|
|
3138
|
+
out[i] = coverageToByte(k);
|
|
3139
|
+
}
|
|
3140
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3141
|
+
}
|
|
3142
|
+
function colorRangePreset(pixels, preset, options = {}) {
|
|
3143
|
+
if (preset === "skinTones") return skinToneRange(pixels);
|
|
3144
|
+
if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
|
|
3145
|
+
if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
|
|
3146
|
+
throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
|
|
3147
|
+
}
|
|
3148
|
+
function selectedColorBins(pixels, sel, tolerance) {
|
|
3149
|
+
const S = 64;
|
|
3150
|
+
let bins = new Uint8Array(S * S * S);
|
|
3151
|
+
const d = pixels.data;
|
|
3152
|
+
const n = pixels.width * pixels.height;
|
|
3153
|
+
for (let i = 0; i < n; i++) {
|
|
3154
|
+
if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
|
|
3155
|
+
bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
|
|
3156
|
+
}
|
|
3157
|
+
const r = Math.round(Math.max(0, tolerance) / 4);
|
|
3158
|
+
if (r === 0) return bins;
|
|
3159
|
+
for (const stride of [1, S, S * S]) {
|
|
3160
|
+
const out = new Uint8Array(bins.length);
|
|
3161
|
+
for (let i = 0; i < bins.length; i++) {
|
|
3162
|
+
if (!bins[i]) continue;
|
|
3163
|
+
const c = Math.floor(i / stride) % S;
|
|
3164
|
+
const lo = Math.max(0, c - r);
|
|
3165
|
+
const hi = Math.min(S - 1, c + r);
|
|
3166
|
+
const base = i - c * stride;
|
|
3167
|
+
for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
|
|
3168
|
+
}
|
|
3169
|
+
bins = out;
|
|
3170
|
+
}
|
|
3171
|
+
return bins;
|
|
3172
|
+
}
|
|
3173
|
+
function matcher(pixels, sel, options) {
|
|
3174
|
+
if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
|
|
3175
|
+
const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
|
|
3176
|
+
const d = pixels.data;
|
|
3177
|
+
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;
|
|
3178
|
+
}
|
|
3179
|
+
function growSelection(pixels, sel, options = {}) {
|
|
3180
|
+
checkPixels(pixels);
|
|
3181
|
+
const match = matcher(pixels, sel, options);
|
|
3182
|
+
const { width: w, height: h } = pixels;
|
|
3183
|
+
const out = new Uint8Array(sel.data);
|
|
3184
|
+
const stack = [];
|
|
3185
|
+
for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
|
|
3186
|
+
while (stack.length) {
|
|
3187
|
+
const i = stack.pop();
|
|
3188
|
+
const x = i % w;
|
|
3189
|
+
const y = (i - x) / w;
|
|
3190
|
+
const visit = (j) => {
|
|
3191
|
+
if (out[j] < 255 && match(j)) {
|
|
3192
|
+
out[j] = 255;
|
|
3193
|
+
stack.push(j);
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
if (x > 0) visit(i - 1);
|
|
3197
|
+
if (x < w - 1) visit(i + 1);
|
|
3198
|
+
if (y > 0) visit(i - w);
|
|
3199
|
+
if (y < h - 1) visit(i + w);
|
|
3200
|
+
}
|
|
3201
|
+
return createSelectionMask(w, h, out);
|
|
3202
|
+
}
|
|
3203
|
+
function selectSimilar(pixels, sel, options = {}) {
|
|
3204
|
+
const n = checkPixels(pixels);
|
|
3205
|
+
const match = matcher(pixels, sel, options);
|
|
3206
|
+
const out = new Uint8Array(sel.data);
|
|
3207
|
+
for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
|
|
3208
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3209
|
+
}
|
|
3105
3210
|
|
|
3106
3211
|
// src/image-editor/selection/segmentation.ts
|
|
3107
3212
|
var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
|
|
@@ -3364,6 +3469,16 @@ function refineSelection(pixels, mask, params = {}) {
|
|
|
3364
3469
|
return createSelectionMask(mask.width, mask.height, out);
|
|
3365
3470
|
}
|
|
3366
3471
|
|
|
3472
|
+
// src/image-editor/selection/stroke.ts
|
|
3473
|
+
function selectionRing(sel, width, location) {
|
|
3474
|
+
const half = location === "center" ? width / 2 : width;
|
|
3475
|
+
const outer = location === "inside" ? sel : modifySelection(sel, "expand", half);
|
|
3476
|
+
const inner = location === "outside" ? sel : modifySelection(sel, "contract", half);
|
|
3477
|
+
const out = new Uint8Array(sel.data.length);
|
|
3478
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.max(0, outer.data[i] - inner.data[i]);
|
|
3479
|
+
return createSelectionMask(sel.width, sel.height, out);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3367
3482
|
// src/image-editor/effect-layers.ts
|
|
3368
3483
|
function hasLayersBelow(layers, id) {
|
|
3369
3484
|
const all = flattenLayers(layers);
|
|
@@ -4274,6 +4389,7 @@ var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
|
|
|
4274
4389
|
var RETOUCH_TOOLS = ["clone", "heal", "spot-heal", "smudge", "dodge", "burn", "sponge", "blur", "sharpen", "history"];
|
|
4275
4390
|
var isRetouchTool = (tool) => RETOUCH_TOOLS.includes(tool);
|
|
4276
4391
|
var STROKE_TOOLS = ["brush", "eraser", "mixer", ...RETOUCH_TOOLS];
|
|
4392
|
+
var PASTE_MODES = ["new-layer", "in-place", "into", "outside"];
|
|
4277
4393
|
function paintCoverageInto(base, coverage, apply, rects, depth = null) {
|
|
4278
4394
|
const tiles = new Map(base.tiles);
|
|
4279
4395
|
const done = /* @__PURE__ */ new Set();
|
|
@@ -4974,8 +5090,11 @@ var ImageEditorController = class {
|
|
|
4974
5090
|
}
|
|
4975
5091
|
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
5092
|
const res = addSmartFilter(layers, id, filter, this.canvasSize, options.index);
|
|
4977
|
-
this.
|
|
4978
|
-
|
|
5093
|
+
const masked = this.asOneStep(() => {
|
|
5094
|
+
this.commitLayers(res.layers);
|
|
5095
|
+
return this.maskFiltersWithSelection(id, options.useSelection);
|
|
5096
|
+
});
|
|
5097
|
+
return { layerId: id, filter: res.filter, converted, masked };
|
|
4979
5098
|
}
|
|
4980
5099
|
/**
|
|
4981
5100
|
* Changes an effect smart filter: `values` merged over its own (`null` resets a control to the
|
|
@@ -5204,8 +5323,38 @@ var ImageEditorController = class {
|
|
|
5204
5323
|
s.selectedAnnotationId = null;
|
|
5205
5324
|
}
|
|
5206
5325
|
}
|
|
5326
|
+
/** Set while `asOneStep` runs: the step is already recorded, so nested commands add none. */
|
|
5327
|
+
historyGrouped = false;
|
|
5328
|
+
/**
|
|
5329
|
+
* Runs `fn` as one undo step: history is recorded once, before it, and the commands it calls
|
|
5330
|
+
* record none. If `fn` throws, the step is dropped and the state goes back to before it, so a
|
|
5331
|
+
* failure never leaves half a change.
|
|
5332
|
+
*/
|
|
5333
|
+
asOneStep(fn) {
|
|
5334
|
+
if (this.historyGrouped || this.isApplyingHistory) return fn();
|
|
5335
|
+
this.recordHistory();
|
|
5336
|
+
const entry = this.undoStack[this.undoStack.length - 1];
|
|
5337
|
+
this.historyGrouped = true;
|
|
5338
|
+
try {
|
|
5339
|
+
return fn();
|
|
5340
|
+
} catch (err) {
|
|
5341
|
+
if (this.undoStack[this.undoStack.length - 1] === entry) {
|
|
5342
|
+
this.undoStack.pop();
|
|
5343
|
+
this.isApplyingHistory = true;
|
|
5344
|
+
try {
|
|
5345
|
+
this.restoreHistoryEntry(entry);
|
|
5346
|
+
} finally {
|
|
5347
|
+
this.isApplyingHistory = false;
|
|
5348
|
+
}
|
|
5349
|
+
this.notify();
|
|
5350
|
+
}
|
|
5351
|
+
throw err;
|
|
5352
|
+
} finally {
|
|
5353
|
+
this.historyGrouped = false;
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5207
5356
|
recordHistory() {
|
|
5208
|
-
if (this.isApplyingHistory) return;
|
|
5357
|
+
if (this.isApplyingHistory || this.historyGrouped) return;
|
|
5209
5358
|
this.docRevision++;
|
|
5210
5359
|
this.undoStack.push(this.captureHistoryEntry());
|
|
5211
5360
|
if (this.undoStack.length > this.maxHistoryDepth) {
|
|
@@ -5851,16 +6000,23 @@ var ImageEditorController = class {
|
|
|
5851
6000
|
return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };
|
|
5852
6001
|
}
|
|
5853
6002
|
/**
|
|
5854
|
-
* Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black)
|
|
5855
|
-
* `selection` (the selection's coverage)
|
|
6003
|
+
* Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black),
|
|
6004
|
+
* `selection` (the selection's coverage), `hide-selection` (its inverse) or `transparency`
|
|
6005
|
+
* (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
6006
|
* target when the layer is active.
|
|
5857
6007
|
*/
|
|
5858
6008
|
addLayerMask(id, from = "reveal-all", props = {}) {
|
|
5859
6009
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
5860
6010
|
let mask;
|
|
5861
|
-
if (from === "selection") {
|
|
5862
|
-
|
|
5863
|
-
if (
|
|
6011
|
+
if (from === "selection" || from === "hide-selection" || from === "transparency") {
|
|
6012
|
+
let sel;
|
|
6013
|
+
if (from === "transparency") {
|
|
6014
|
+
sel = this.layerTransparency(id);
|
|
6015
|
+
} else {
|
|
6016
|
+
const current = this.state.selection;
|
|
6017
|
+
if (!current?.bounds) throw new LayerOperationError("Nothing is selected; select first, or use reveal-all / hide-all / transparency.");
|
|
6018
|
+
sel = from === "hide-selection" ? invertSelection(current) : current;
|
|
6019
|
+
}
|
|
5864
6020
|
mask = this.maskFromSelection(sel, props);
|
|
5865
6021
|
if (layer.type === "pixel" && (layer.offset.x || layer.offset.y)) {
|
|
5866
6022
|
mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };
|
|
@@ -6314,10 +6470,22 @@ var ImageEditorController = class {
|
|
|
6314
6470
|
return res.layer.id;
|
|
6315
6471
|
}
|
|
6316
6472
|
/** 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) {
|
|
6473
|
+
addSmartFilter(id, filter, index, options = {}) {
|
|
6318
6474
|
const res = addSmartFilter(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);
|
|
6319
|
-
this.
|
|
6320
|
-
|
|
6475
|
+
return this.asOneStep(() => {
|
|
6476
|
+
this.commitLayers(res.layers);
|
|
6477
|
+
this.maskFiltersWithSelection(id, options.useSelection);
|
|
6478
|
+
return res.filter;
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
/**
|
|
6482
|
+
* After adding a smart filter: with `useSelection` and a selection, sets the filter mask from
|
|
6483
|
+
* it in the same undo step (Photoshop limits a filter to the selection). Returns whether it did.
|
|
6484
|
+
*/
|
|
6485
|
+
maskFiltersWithSelection(id, useSelection) {
|
|
6486
|
+
if (!useSelection || !this.state.selection?.bounds) return false;
|
|
6487
|
+
this.setSmartFilterMask(id, "selection", {}, false);
|
|
6488
|
+
return true;
|
|
6321
6489
|
}
|
|
6322
6490
|
/** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */
|
|
6323
6491
|
updateSmartFilter(id, filterId, changes, commitToHistory = true) {
|
|
@@ -7145,13 +7313,176 @@ var ImageEditorController = class {
|
|
|
7145
7313
|
clearPixels(options = {}) {
|
|
7146
7314
|
return this.fillOrClear(options, "erase");
|
|
7147
7315
|
}
|
|
7148
|
-
|
|
7149
|
-
|
|
7316
|
+
/**
|
|
7317
|
+
* Edit › Stroke: paints a band `width` px wide along the selection's edge, `inside` it, on the
|
|
7318
|
+
* `center` of the edge or `outside` it (default), with `color` (default the foreground),
|
|
7319
|
+
* `opacity` and `blend`, on a layer's pixels or mask. One undo step. Returns the changed bounds.
|
|
7320
|
+
*/
|
|
7321
|
+
strokeSelection(options) {
|
|
7322
|
+
const sel = this.state.selection;
|
|
7323
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then stroke the selection.");
|
|
7324
|
+
const width = options.width;
|
|
7325
|
+
if (!(width > 0 && width <= 250)) throw new LayerOperationError("width must be greater than 0 and at most 250 px.");
|
|
7326
|
+
const location = options.location ?? "outside";
|
|
7327
|
+
if (location !== "inside" && location !== "center" && location !== "outside") {
|
|
7328
|
+
throw new LayerOperationError(`Unknown location "${String(location)}"; use inside, center or outside.`);
|
|
7329
|
+
}
|
|
7330
|
+
const ring = selectionRing(sel, width, location);
|
|
7331
|
+
if (!ring.bounds) return { layerId: options.layerId ?? this.state.activeLayerId ?? "", channel: "pixels", bounds: null, dabs: 0 };
|
|
7332
|
+
return this.fillOrClear(options, "paint", ring);
|
|
7333
|
+
}
|
|
7334
|
+
/**
|
|
7335
|
+
* Image › Crop to the selection's bounds (one undo step). Returns the crop box (natural
|
|
7336
|
+
* pixels). Throws without a selection.
|
|
7337
|
+
*/
|
|
7338
|
+
cropToSelection() {
|
|
7339
|
+
const sel = this.state.selection;
|
|
7340
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select the area to keep, then crop to it.");
|
|
7341
|
+
const crop = { ...sel.bounds };
|
|
7342
|
+
this.setCrop(crop);
|
|
7343
|
+
return crop;
|
|
7344
|
+
}
|
|
7345
|
+
/**
|
|
7346
|
+
* Edit › Content-Aware Fill: fills the selection with texture synthesised from around it
|
|
7347
|
+
* (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
|
|
7348
|
+
* coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
|
|
7349
|
+
* above it. Samples within `sampleRadius` px of the selection (default its larger side, at
|
|
7350
|
+
* least 32). Deterministic for a `seed`. One undo step. 8-bit documents only for now.
|
|
7351
|
+
*/
|
|
7352
|
+
contentAwareFill(options = {}) {
|
|
7353
|
+
const sel = this.state.selection;
|
|
7354
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
|
|
7355
|
+
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.");
|
|
7356
|
+
const output = options.output ?? "current";
|
|
7357
|
+
if (output !== "current" && output !== "new-layer") throw new LayerOperationError(`Unknown output "${String(output)}"; use current or new-layer.`);
|
|
7358
|
+
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
7359
|
+
const { width, height } = this.naturalSize;
|
|
7360
|
+
const b = sel.bounds;
|
|
7361
|
+
const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
|
|
7362
|
+
const x0 = Math.max(0, b.x - margin);
|
|
7363
|
+
const y0 = Math.max(0, b.y - margin);
|
|
7364
|
+
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 };
|
|
7365
|
+
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.");
|
|
7366
|
+
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
7367
|
+
const before = readRegion(target.surface, layerRect);
|
|
7368
|
+
const img = new Float32Array(before.length);
|
|
7369
|
+
for (let i = 0; i < before.length; i++) img[i] = before[i];
|
|
7370
|
+
const coverage = new Float32Array(win.width * win.height);
|
|
7371
|
+
const hole = new Uint8Array(win.width * win.height);
|
|
7372
|
+
for (let y = 0; y < win.height; y++) {
|
|
7373
|
+
for (let x = 0; x < win.width; x++) {
|
|
7374
|
+
const c = sel.data[(win.y + y) * sel.width + win.x + x] / 255;
|
|
7375
|
+
coverage[y * win.width + x] = c;
|
|
7376
|
+
hole[y * win.width + x] = c > 0 ? 1 : 0;
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
7379
|
+
const filled = contentAwareFill(win.width, win.height, 4, img, hole, { seed: options.seed ?? 0 });
|
|
7380
|
+
const out = new Uint8ClampedArray(before.length);
|
|
7381
|
+
for (let i = 0; i < coverage.length; i++) {
|
|
7382
|
+
const k = coverage[i];
|
|
7383
|
+
for (let c = 0; c < 4; c++) {
|
|
7384
|
+
const p = i * 4 + c;
|
|
7385
|
+
out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
if (output === "new-layer") {
|
|
7389
|
+
const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
|
|
7390
|
+
const data = new Uint8ClampedArray(b.width * b.height * 4);
|
|
7391
|
+
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);
|
|
7392
|
+
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 });
|
|
7393
|
+
return { layerId: id, bounds: { ...b }, output };
|
|
7394
|
+
}
|
|
7395
|
+
const surface = writeRegion(target.surface, layerRect, out);
|
|
7396
|
+
this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
7397
|
+
return { layerId: target.id, bounds: { ...b }, output };
|
|
7398
|
+
}
|
|
7399
|
+
/** The pixel clipboard (Copy / Cut), or null. */
|
|
7400
|
+
clipboard = null;
|
|
7401
|
+
/** What Copy or Cut last put on the editor's clipboard, or null. */
|
|
7402
|
+
getClipboard() {
|
|
7403
|
+
return this.clipboard;
|
|
7404
|
+
}
|
|
7405
|
+
/** Puts pixels on the editor's clipboard (e.g. an image pasted from the system clipboard). */
|
|
7406
|
+
setClipboard(image) {
|
|
7407
|
+
if (image && (image.data.length !== image.width * image.height * 4 || image.width < 1 || image.height < 1)) {
|
|
7408
|
+
throw new LayerOperationError("Clipboard pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
|
|
7409
|
+
}
|
|
7410
|
+
this.clipboard = image;
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* Edit › Copy (or Copy Merged with `merged`): the selected pixels of a pixel layer (default the
|
|
7414
|
+
* active one) or of the whole visible image, cropped to the selection's bounds, with the
|
|
7415
|
+
* selection's soft edge in their alpha. Without a selection, the whole canvas. Not an undo step.
|
|
7416
|
+
*/
|
|
7417
|
+
copyPixels(options = {}) {
|
|
7418
|
+
const { width, height } = this.naturalSize;
|
|
7419
|
+
const sel = this.state.selection?.bounds ? this.state.selection : null;
|
|
7420
|
+
const rect = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
7421
|
+
let data;
|
|
7422
|
+
if (options.merged) {
|
|
7423
|
+
data = new Uint8ClampedArray(this.compositePixels(rect).data);
|
|
7424
|
+
} else {
|
|
7425
|
+
const id = options.layerId ?? this.state.activeLayerId;
|
|
7426
|
+
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId, or merged: true to copy the visible image.");
|
|
7427
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
7428
|
+
if (layer.type !== "pixel") throw new LayerOperationError(`Layer "${layer.name || id}" is not a pixel layer; copy merged, or rasterize it first.`);
|
|
7429
|
+
data = readRegion(this.surfaceOf(layer), { x: rect.x - layer.offset.x, y: rect.y - layer.offset.y, width: rect.width, height: rect.height });
|
|
7430
|
+
}
|
|
7431
|
+
if (sel) {
|
|
7432
|
+
for (let y = 0; y < rect.height; y++) {
|
|
7433
|
+
for (let x = 0; x < rect.width; x++) {
|
|
7434
|
+
const k = sel.data[(rect.y + y) * sel.width + rect.x + x];
|
|
7435
|
+
const p = (y * rect.width + x) * 4 + 3;
|
|
7436
|
+
data[p] = Math.round(data[p] * k / 255);
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
this.clipboard = { width: rect.width, height: rect.height, x: rect.x, y: rect.y, data };
|
|
7441
|
+
return this.clipboard;
|
|
7442
|
+
}
|
|
7443
|
+
/** Edit › Cut: Copy, then clear the selected pixels, as one undo step. */
|
|
7444
|
+
cutPixels(options = {}) {
|
|
7445
|
+
return this.asOneStep(() => {
|
|
7446
|
+
const clip = this.copyPixels({ layerId: options.layerId });
|
|
7447
|
+
this.clearPixels({ layerId: options.layerId, channel: "pixels" });
|
|
7448
|
+
return clip;
|
|
7449
|
+
});
|
|
7450
|
+
}
|
|
7451
|
+
/**
|
|
7452
|
+
* Edit › Paste as a new pixel layer above the active one (one undo step): `new-layer` centred
|
|
7453
|
+
* on the selection (or the canvas), `in-place` where it was copied, `into` centred in the
|
|
7454
|
+
* selection with the selection as its mask (Paste Into), or `outside` with the inverse
|
|
7455
|
+
* (Paste Outside). Returns the new layer id and where it went.
|
|
7456
|
+
*/
|
|
7457
|
+
pastePixels(mode = "new-layer", image = this.clipboard) {
|
|
7458
|
+
if (!image) throw new LayerOperationError("The clipboard is empty; copy_pixels first.");
|
|
7459
|
+
if (!PASTE_MODES.includes(mode)) throw new LayerOperationError(`Unknown paste mode "${String(mode)}"; use ${PASTE_MODES.join(", ")}.`);
|
|
7460
|
+
const sel = this.state.selection?.bounds ? this.state.selection : null;
|
|
7461
|
+
if ((mode === "into" || mode === "outside") && !sel) throw new LayerOperationError(`Paste ${mode === "into" ? "Into" : "Outside"} needs a selection; select where it goes first.`);
|
|
7462
|
+
const { width, height } = this.naturalSize;
|
|
7463
|
+
let x;
|
|
7464
|
+
let y;
|
|
7465
|
+
if (mode === "in-place" && image.x !== void 0 && image.y !== void 0) {
|
|
7466
|
+
x = image.x;
|
|
7467
|
+
y = image.y;
|
|
7468
|
+
} else {
|
|
7469
|
+
const frame = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
7470
|
+
x = Math.round(frame.x + (frame.width - image.width) / 2);
|
|
7471
|
+
y = Math.round(frame.y + (frame.height - image.height) / 2);
|
|
7472
|
+
}
|
|
7473
|
+
return this.asOneStep(() => {
|
|
7474
|
+
const id = this.addPixelLayer({ name: "Pasted", pixels: { width: image.width, height: image.height, data: new Uint8ClampedArray(image.data) }, x, y });
|
|
7475
|
+
if (mode === "into" || mode === "outside") this.addLayerMask(id, mode === "into" ? "selection" : "hide-selection");
|
|
7476
|
+
return { layerId: id, bounds: { x, y, width: image.width, height: image.height }, masked: mode === "into" || mode === "outside" };
|
|
7477
|
+
});
|
|
7478
|
+
}
|
|
7479
|
+
fillOrClear(options, mode, coverageMask) {
|
|
7480
|
+
const target = this.paintTarget(options.layerId, options.channel, mode === "paint" ? coverageMask ? "stroke" : "fill" : "clear");
|
|
7150
7481
|
const { width, height } = this.naturalSize;
|
|
7151
7482
|
const opacity = options.opacity ?? 1;
|
|
7152
7483
|
if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
|
|
7153
7484
|
const blend = parseBlend(options.blend) ?? "normal";
|
|
7154
|
-
const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;
|
|
7485
|
+
const sel = coverageMask ?? (options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null);
|
|
7155
7486
|
const region = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
7156
7487
|
const coverage = new CoverageMap();
|
|
7157
7488
|
const ones = new Float32Array(region.width * region.height);
|
|
@@ -7325,7 +7656,7 @@ var ImageEditorController = class {
|
|
|
7325
7656
|
enabled: spec.enabled,
|
|
7326
7657
|
opacity: spec.opacity,
|
|
7327
7658
|
blend: spec.blend,
|
|
7328
|
-
mask: spec.mask ?? void 0
|
|
7659
|
+
mask: spec.mask ?? (spec.useSelection && this.state.selection?.bounds ? layerMaskFromSelection(this.state.selection) : void 0)
|
|
7329
7660
|
});
|
|
7330
7661
|
while (ids.has(layer.id)) layer.id = newLayerId("adj");
|
|
7331
7662
|
const { width, height } = this.naturalSize;
|
|
@@ -7535,6 +7866,7 @@ var ImageEditorController = class {
|
|
|
7535
7866
|
const prev = normalizeSelection(this.state.selection ?? null);
|
|
7536
7867
|
if (prev === next || prev && next && sameSelection(prev, next)) return false;
|
|
7537
7868
|
this.recordHistory();
|
|
7869
|
+
if (!next && prev?.bounds) this.lastSelection = prev;
|
|
7538
7870
|
this.state.selection = next;
|
|
7539
7871
|
this.notify();
|
|
7540
7872
|
return true;
|
|
@@ -7548,6 +7880,74 @@ var ImageEditorController = class {
|
|
|
7548
7880
|
deselect() {
|
|
7549
7881
|
return this.setSelection(null);
|
|
7550
7882
|
}
|
|
7883
|
+
/** The selection a Deselect cleared, for `reselect` (not saved, not part of history). */
|
|
7884
|
+
lastSelection = null;
|
|
7885
|
+
/** Whether `reselect` has a selection to bring back. */
|
|
7886
|
+
canReselect() {
|
|
7887
|
+
const { width, height } = this.naturalSize;
|
|
7888
|
+
return Boolean(this.lastSelection && this.lastSelection.width === width && this.lastSelection.height === height);
|
|
7889
|
+
}
|
|
7890
|
+
/** Select › Reselect: brings back the last selection a Deselect cleared, as one undo step. */
|
|
7891
|
+
reselect() {
|
|
7892
|
+
if (!this.canReselect()) return false;
|
|
7893
|
+
return this.setSelection(this.lastSelection);
|
|
7894
|
+
}
|
|
7895
|
+
/**
|
|
7896
|
+
* Single Row / Single Column Marquee: a 1 px line across the whole image at `position`
|
|
7897
|
+
* (natural pixels; y for a row, x for a column).
|
|
7898
|
+
*/
|
|
7899
|
+
selectLine(orientation, position, options = {}) {
|
|
7900
|
+
const { width, height } = this.naturalSize;
|
|
7901
|
+
const max = orientation === "row" ? height : width;
|
|
7902
|
+
const at = Math.floor(position);
|
|
7903
|
+
if (!Number.isFinite(position) || at < 0 || at >= max) {
|
|
7904
|
+
throw new RangeError(`${orientation === "row" ? "y" : "x"} ${position} is outside the image (0..${max - 1}).`);
|
|
7905
|
+
}
|
|
7906
|
+
const rect = orientation === "row" ? { x: 0, y: at, width, height: 1 } : { x: at, y: 0, width: 1, height };
|
|
7907
|
+
return this.shapeCommand(rectSelection(width, height, rect, { antiAlias: false }), options);
|
|
7908
|
+
}
|
|
7909
|
+
/** Select › Grow: adds connected pixels within `tolerance` of a selected colour (one undo step). */
|
|
7910
|
+
async growSelection(options = {}) {
|
|
7911
|
+
const sel = this.state.selection;
|
|
7912
|
+
if (!sel?.bounds) return false;
|
|
7913
|
+
const pixels = await this.requirePixels(options);
|
|
7914
|
+
return this.setSelection(growSelection(pixels, sel, options));
|
|
7915
|
+
}
|
|
7916
|
+
/** Select › Similar: adds every pixel within `tolerance` of a selected colour (one undo step). */
|
|
7917
|
+
async selectSimilar(options = {}) {
|
|
7918
|
+
const sel = this.state.selection;
|
|
7919
|
+
if (!sel?.bounds) return false;
|
|
7920
|
+
const pixels = await this.requirePixels(options);
|
|
7921
|
+
return this.setSelection(selectSimilar(pixels, sel, options));
|
|
7922
|
+
}
|
|
7923
|
+
/**
|
|
7924
|
+
* Load Selection from a layer's transparency (Ctrl/⌘-click its thumbnail): its alpha in
|
|
7925
|
+
* document space, combined by `mode`. Works on pixel, shape, type and smart object layers.
|
|
7926
|
+
*/
|
|
7927
|
+
selectLayerTransparency(id, options = {}) {
|
|
7928
|
+
const mask = this.layerTransparency(id);
|
|
7929
|
+
return this.shapeCommand(mask, options);
|
|
7930
|
+
}
|
|
7931
|
+
layerTransparency(id) {
|
|
7932
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
7933
|
+
if (!hasSurface(layer)) {
|
|
7934
|
+
throw new LayerOperationError(`Layer "${layer.name || id}" has no pixels of its own; only pixel, shape, type and smart object layers do.`);
|
|
7935
|
+
}
|
|
7936
|
+
const { width, height } = this.naturalSize;
|
|
7937
|
+
const out = new Uint8Array(width * height);
|
|
7938
|
+
const pixels = this.getLayerPixels(id);
|
|
7939
|
+
if (!pixels) return createSelectionMask(width, height, out);
|
|
7940
|
+
for (let y = 0; y < pixels.height; y++) {
|
|
7941
|
+
const dy = y + pixels.y;
|
|
7942
|
+
if (dy < 0 || dy >= height) continue;
|
|
7943
|
+
for (let x = 0; x < pixels.width; x++) {
|
|
7944
|
+
const dx = x + pixels.x;
|
|
7945
|
+
if (dx < 0 || dx >= width) continue;
|
|
7946
|
+
out[dy * width + dx] = pixels.data[(y * pixels.width + x) * 4 + 3];
|
|
7947
|
+
}
|
|
7948
|
+
}
|
|
7949
|
+
return createSelectionMask(width, height, out);
|
|
7950
|
+
}
|
|
7551
7951
|
/** Select › Inverse (with nothing selected, selects everything). */
|
|
7552
7952
|
invertSelection() {
|
|
7553
7953
|
const { width, height } = this.naturalSize;
|
|
@@ -7555,7 +7955,8 @@ var ImageEditorController = class {
|
|
|
7555
7955
|
return this.setSelection(invertSelection(current));
|
|
7556
7956
|
}
|
|
7557
7957
|
shapeCommand(mask, options) {
|
|
7558
|
-
const
|
|
7958
|
+
const feathered = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
|
|
7959
|
+
const shaped = options.invert ? invertSelection(feathered) : feathered;
|
|
7559
7960
|
return this.setSelection(shaped, options.mode ?? "replace");
|
|
7560
7961
|
}
|
|
7561
7962
|
/** Rectangular marquee in natural image pixels. */
|
|
@@ -7657,6 +8058,15 @@ var ImageEditorController = class {
|
|
|
7657
8058
|
this.shapeCommand(colorRange(pixels, samples, options), options);
|
|
7658
8059
|
return this.getSelection();
|
|
7659
8060
|
}
|
|
8061
|
+
/**
|
|
8062
|
+
* Select › Color Range with a preset: `reds` … `magentas`, `highlights` / `midtones` /
|
|
8063
|
+
* `shadows` (with `falloff` levels, default 20) or `skinTones`. One undo step.
|
|
8064
|
+
*/
|
|
8065
|
+
async selectColorRangePreset(preset, options = {}) {
|
|
8066
|
+
const pixels = await this.requirePixels(options);
|
|
8067
|
+
this.shapeCommand(colorRangePreset(pixels, preset, options), options);
|
|
8068
|
+
return this.getSelection();
|
|
8069
|
+
}
|
|
7660
8070
|
/** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
|
|
7661
8071
|
modifySelection(op, radius) {
|
|
7662
8072
|
const sel = this.state.selection;
|
|
@@ -7733,8 +8143,23 @@ var ImageEditorController = class {
|
|
|
7733
8143
|
}
|
|
7734
8144
|
/** Quick Mask view (tints the unselected area). View state: no undo step. */
|
|
7735
8145
|
setQuickMask(on) {
|
|
7736
|
-
|
|
7737
|
-
|
|
8146
|
+
this.setSelectionDisplay(on ? "quickMask" : "ants");
|
|
8147
|
+
}
|
|
8148
|
+
/** How the canvas shows the selection (`state.selectionDisplay`). */
|
|
8149
|
+
getSelectionDisplay() {
|
|
8150
|
+
return this.state.selectionDisplay ?? (this.state.quickMask ? "quickMask" : "ants");
|
|
8151
|
+
}
|
|
8152
|
+
/**
|
|
8153
|
+
* Shows the selection as marching ants, as a Quick Mask tint over the unselected area, or hides
|
|
8154
|
+
* it (the selection still applies). View state: no undo step, not saved.
|
|
8155
|
+
*/
|
|
8156
|
+
setSelectionDisplay(display) {
|
|
8157
|
+
if (display !== "ants" && display !== "quickMask" && display !== "none") {
|
|
8158
|
+
throw new Error(`Unknown selection display "${String(display)}". Use ants, quickMask or none.`);
|
|
8159
|
+
}
|
|
8160
|
+
if (this.getSelectionDisplay() === display) return;
|
|
8161
|
+
this.state.selectionDisplay = display;
|
|
8162
|
+
this.state.quickMask = display === "quickMask";
|
|
7738
8163
|
this.notify();
|
|
7739
8164
|
}
|
|
7740
8165
|
// --- Layer masks ---
|
|
@@ -7778,8 +8203,11 @@ var ImageEditorController = class {
|
|
|
7778
8203
|
if (!layer || layer.type !== "adjustment" || !layer.mask) return false;
|
|
7779
8204
|
return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);
|
|
7780
8205
|
}
|
|
7781
|
-
/**
|
|
7782
|
-
|
|
8206
|
+
/**
|
|
8207
|
+
* Load Selection from a layer mask (its pixels; density and feather are not applied), inverted
|
|
8208
|
+
* with `invert`. False without a painted mask (a depth mask is computed, not painted).
|
|
8209
|
+
*/
|
|
8210
|
+
selectionFromLayerMask(layerId, mode = "replace", invert2 = false) {
|
|
7783
8211
|
const layer = this.getLayer(layerId);
|
|
7784
8212
|
if (!layer?.mask || layer.mask.source) return false;
|
|
7785
8213
|
const { width, height } = this.naturalSize;
|
|
@@ -7787,7 +8215,7 @@ var ImageEditorController = class {
|
|
|
7787
8215
|
const oy = layer.type === "pixel" ? layer.offset.y : 0;
|
|
7788
8216
|
const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });
|
|
7789
8217
|
const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));
|
|
7790
|
-
return this.setSelection(sel, mode);
|
|
8218
|
+
return this.setSelection(invert2 ? invertSelection(sel) : sel, mode);
|
|
7791
8219
|
}
|
|
7792
8220
|
// --- Histogram ---
|
|
7793
8221
|
/**
|
|
@@ -10121,9 +10549,9 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
10121
10549
|
// src/image-editor/playback.ts
|
|
10122
10550
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
10123
10551
|
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)$/;
|
|
10552
|
+
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)$/;
|
|
10125
10553
|
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)$/;
|
|
10554
|
+
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
10127
10555
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
10128
10556
|
function imagePlaybackPanel(tool) {
|
|
10129
10557
|
if (ADJUST.test(tool)) return "adjust";
|
|
@@ -10238,8 +10666,24 @@ var SELECTION_TOOLS = [
|
|
|
10238
10666
|
{ id: "wand", label: "Magic Wand" },
|
|
10239
10667
|
{ id: "rect", label: "Rectangle" },
|
|
10240
10668
|
{ id: "ellipse", label: "Ellipse" },
|
|
10241
|
-
{ id: "
|
|
10669
|
+
{ id: "row", label: "Single Row" },
|
|
10670
|
+
{ id: "column", label: "Single Column" },
|
|
10671
|
+
{ id: "lasso", label: "Lasso" },
|
|
10672
|
+
{ id: "polygon", label: "Polygonal Lasso" },
|
|
10673
|
+
{ id: "range", label: "Color Range" }
|
|
10242
10674
|
];
|
|
10675
|
+
var COLOR_RANGE_PRESET_LABELS = {
|
|
10676
|
+
reds: "Reds",
|
|
10677
|
+
yellows: "Yellows",
|
|
10678
|
+
greens: "Greens",
|
|
10679
|
+
cyans: "Cyans",
|
|
10680
|
+
blues: "Blues",
|
|
10681
|
+
magentas: "Magentas",
|
|
10682
|
+
highlights: "Highlights",
|
|
10683
|
+
midtones: "Midtones",
|
|
10684
|
+
shadows: "Shadows",
|
|
10685
|
+
skinTones: "Skin Tones"
|
|
10686
|
+
};
|
|
10243
10687
|
var SELECTION_MODE_LABELS = {
|
|
10244
10688
|
replace: "New",
|
|
10245
10689
|
add: "Add",
|
|
@@ -10253,7 +10697,11 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
|
|
|
10253
10697
|
contiguous: true,
|
|
10254
10698
|
antiAlias: true,
|
|
10255
10699
|
sampleMerged: false,
|
|
10256
|
-
distance: "rgb"
|
|
10700
|
+
distance: "rgb",
|
|
10701
|
+
feather: 0,
|
|
10702
|
+
fuzziness: 40,
|
|
10703
|
+
localized: 0,
|
|
10704
|
+
rangeInvert: false
|
|
10257
10705
|
};
|
|
10258
10706
|
function selectionModeFromModifiers(modifiers, fallback) {
|
|
10259
10707
|
if (modifiers.shiftKey && modifiers.altKey) return "intersect";
|
|
@@ -10284,6 +10732,41 @@ var modeSchema = {
|
|
|
10284
10732
|
description: "How the result combines with the current selection (default replace)."
|
|
10285
10733
|
};
|
|
10286
10734
|
var num2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
10735
|
+
var featherSchema = { type: "number", minimum: 0, maximum: 1e3, description: "Feather the new shape by this many pixels before combining (default 0)." };
|
|
10736
|
+
var antiAliasSchema = { type: "boolean", description: "Soft one-pixel edge (default true)." };
|
|
10737
|
+
var sampleMergedSchema = { type: "boolean", description: "Sample the adjusted image (adjustment layers and filters applied) rather than the pixels as painted (default false)." };
|
|
10738
|
+
var sampleSchema = {
|
|
10739
|
+
type: "string",
|
|
10740
|
+
enum: ["composite", "active-layer"],
|
|
10741
|
+
description: "composite (default): all layers; active-layer: only the active pixel layer (see set_active_layer)."
|
|
10742
|
+
};
|
|
10743
|
+
var pointSchema = {
|
|
10744
|
+
type: "object",
|
|
10745
|
+
properties: { x: { type: "number", description: "x in natural image pixels." }, y: { type: "number", description: "y in natural image pixels." } },
|
|
10746
|
+
required: ["x", "y"],
|
|
10747
|
+
additionalProperties: false
|
|
10748
|
+
};
|
|
10749
|
+
var rectProperties = {
|
|
10750
|
+
x: { type: "number", description: "Left edge in natural image pixels." },
|
|
10751
|
+
y: { type: "number", description: "Top edge in natural image pixels." },
|
|
10752
|
+
width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
|
|
10753
|
+
height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
|
|
10754
|
+
};
|
|
10755
|
+
function shapeOptions(args) {
|
|
10756
|
+
return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
|
|
10757
|
+
}
|
|
10758
|
+
function sampleOptions(args) {
|
|
10759
|
+
if (args.sample !== void 0 && args.sample !== "composite" && args.sample !== "active-layer") {
|
|
10760
|
+
throw new Error(`Unknown sample "${String(args.sample)}". Use composite or active-layer.`);
|
|
10761
|
+
}
|
|
10762
|
+
return { sampleMerged: args.sampleMerged === true, sample: args.sample ?? "composite" };
|
|
10763
|
+
}
|
|
10764
|
+
function rectOf(args) {
|
|
10765
|
+
const rect = { x: num2(args.x, NaN), y: num2(args.y, NaN), width: num2(args.width, NaN), height: num2(args.height, NaN) };
|
|
10766
|
+
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.");
|
|
10767
|
+
if (rect.width <= 0 || rect.height <= 0) throw new Error("width and height must be greater than 0.");
|
|
10768
|
+
return rect;
|
|
10769
|
+
}
|
|
10287
10770
|
function summary(controller) {
|
|
10288
10771
|
const sel = controller.getSelection();
|
|
10289
10772
|
const { width, height } = controller.getState().imageDimensions;
|
|
@@ -10316,7 +10799,7 @@ function createImageSelectionTools(controller) {
|
|
|
10316
10799
|
const selectRegion = {
|
|
10317
10800
|
name: "select_region",
|
|
10318
10801
|
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.',
|
|
10802
|
+
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
10803
|
inputSchema: {
|
|
10321
10804
|
type: "object",
|
|
10322
10805
|
properties: {
|
|
@@ -10382,7 +10865,7 @@ function createImageSelectionTools(controller) {
|
|
|
10382
10865
|
const selectByColor = {
|
|
10383
10866
|
name: "select_by_color",
|
|
10384
10867
|
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.',
|
|
10868
|
+
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
10869
|
inputSchema: {
|
|
10387
10870
|
type: "object",
|
|
10388
10871
|
properties: {
|
|
@@ -10422,7 +10905,7 @@ function createImageSelectionTools(controller) {
|
|
|
10422
10905
|
const modify = {
|
|
10423
10906
|
name: "modify_selection",
|
|
10424
10907
|
title: "Modify the selection",
|
|
10425
|
-
description: "Select \u203A Modify:
|
|
10908
|
+
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
10909
|
inputSchema: {
|
|
10427
10910
|
type: "object",
|
|
10428
10911
|
properties: {
|
|
@@ -10512,7 +10995,499 @@ function createImageSelectionTools(controller) {
|
|
|
10512
10995
|
return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };
|
|
10513
10996
|
}
|
|
10514
10997
|
};
|
|
10515
|
-
|
|
10998
|
+
const selectAll = {
|
|
10999
|
+
name: "select_all",
|
|
11000
|
+
title: "Select all",
|
|
11001
|
+
description: "Select \u203A All: selects the whole image. One undoable step. Returns the selection.",
|
|
11002
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11003
|
+
annotations: writeAnnotations("Select all", true),
|
|
11004
|
+
handler: () => {
|
|
11005
|
+
controller.selectAll();
|
|
11006
|
+
return summary(controller);
|
|
11007
|
+
}
|
|
11008
|
+
};
|
|
11009
|
+
const deselect = {
|
|
11010
|
+
name: "deselect",
|
|
11011
|
+
title: "Deselect",
|
|
11012
|
+
description: "Select \u203A Deselect: clears the selection, so later edits apply to the whole layer. One undoable step. Returns the (empty) selection.",
|
|
11013
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11014
|
+
annotations: writeAnnotations("Deselect", true),
|
|
11015
|
+
handler: () => {
|
|
11016
|
+
controller.deselect();
|
|
11017
|
+
return summary(controller);
|
|
11018
|
+
}
|
|
11019
|
+
};
|
|
11020
|
+
const invert2 = {
|
|
11021
|
+
name: "invert_selection",
|
|
11022
|
+
title: "Invert the selection",
|
|
11023
|
+
description: "Select \u203A Inverse: selects what was not selected (soft edges stay soft). With nothing selected, selects everything. One undoable step. Returns the selection.",
|
|
11024
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11025
|
+
annotations: writeAnnotations("Invert the selection"),
|
|
11026
|
+
handler: () => {
|
|
11027
|
+
controller.invertSelection();
|
|
11028
|
+
return summary(controller);
|
|
11029
|
+
}
|
|
11030
|
+
};
|
|
11031
|
+
const selectRect = {
|
|
11032
|
+
name: "select_rect",
|
|
11033
|
+
title: "Select a rectangle",
|
|
11034
|
+
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.",
|
|
11035
|
+
inputSchema: {
|
|
11036
|
+
type: "object",
|
|
11037
|
+
properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
|
|
11038
|
+
required: ["x", "y", "width", "height"],
|
|
11039
|
+
additionalProperties: false
|
|
11040
|
+
},
|
|
11041
|
+
annotations: writeAnnotations("Select a rectangle", true),
|
|
11042
|
+
handler: (args) => {
|
|
11043
|
+
controller.selectRect(rectOf(args), shapeOptions(args));
|
|
11044
|
+
return summary(controller);
|
|
11045
|
+
}
|
|
11046
|
+
};
|
|
11047
|
+
const selectEllipse = {
|
|
11048
|
+
name: "select_ellipse",
|
|
11049
|
+
title: "Select an ellipse",
|
|
11050
|
+
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.",
|
|
11051
|
+
inputSchema: {
|
|
11052
|
+
type: "object",
|
|
11053
|
+
properties: { ...rectProperties, mode: modeSchema, feather: featherSchema, antiAlias: antiAliasSchema },
|
|
11054
|
+
required: ["x", "y", "width", "height"],
|
|
11055
|
+
additionalProperties: false
|
|
11056
|
+
},
|
|
11057
|
+
annotations: writeAnnotations("Select an ellipse", true),
|
|
11058
|
+
handler: (args) => {
|
|
11059
|
+
controller.selectEllipse(rectOf(args), shapeOptions(args));
|
|
11060
|
+
return summary(controller);
|
|
11061
|
+
}
|
|
11062
|
+
};
|
|
11063
|
+
const selectPolygonTool = {
|
|
11064
|
+
name: "select_polygon",
|
|
11065
|
+
title: "Select a polygon (lasso)",
|
|
11066
|
+
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.",
|
|
11067
|
+
inputSchema: {
|
|
11068
|
+
type: "object",
|
|
11069
|
+
properties: {
|
|
11070
|
+
points: { type: "array", minItems: 3, items: pointSchema, description: "At least 3 vertices along the outline, in order, in natural image pixels." },
|
|
11071
|
+
mode: modeSchema,
|
|
11072
|
+
feather: featherSchema,
|
|
11073
|
+
antiAlias: antiAliasSchema
|
|
11074
|
+
},
|
|
11075
|
+
required: ["points"],
|
|
11076
|
+
additionalProperties: false
|
|
11077
|
+
},
|
|
11078
|
+
annotations: writeAnnotations("Select a polygon (lasso)", true),
|
|
11079
|
+
handler: (args) => {
|
|
11080
|
+
const list = Array.isArray(args.points) ? args.points.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : [];
|
|
11081
|
+
if (list.length < 3 || !list.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) {
|
|
11082
|
+
throw new Error("points needs at least 3 { x, y } vertices in natural image pixels.");
|
|
11083
|
+
}
|
|
11084
|
+
controller.selectPolygon(list, shapeOptions(args));
|
|
11085
|
+
return summary(controller);
|
|
11086
|
+
}
|
|
11087
|
+
};
|
|
11088
|
+
const magicWandTool = {
|
|
11089
|
+
name: "magic_wand",
|
|
11090
|
+
title: "Magic wand",
|
|
11091
|
+
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.",
|
|
11092
|
+
inputSchema: {
|
|
11093
|
+
type: "object",
|
|
11094
|
+
properties: {
|
|
11095
|
+
x: { type: "number", description: "Seed x in natural image pixels." },
|
|
11096
|
+
y: { type: "number", description: "Seed y in natural image pixels." },
|
|
11097
|
+
tolerance: { type: "number", minimum: 0, maximum: 255, description: "Colour tolerance 0..255 (default 32)." },
|
|
11098
|
+
contiguous: { type: "boolean", description: "Only the region connected to the seed (default true); false selects every similar pixel." },
|
|
11099
|
+
distance: { type: "string", enum: ["rgb", "perceptual"], description: "How colour difference is measured: rgb (default, as Photoshop) or perceptual (CIE \u0394E)." },
|
|
11100
|
+
antiAlias: antiAliasSchema,
|
|
11101
|
+
sampleMerged: sampleMergedSchema,
|
|
11102
|
+
sample: sampleSchema,
|
|
11103
|
+
mode: modeSchema,
|
|
11104
|
+
feather: featherSchema
|
|
11105
|
+
},
|
|
11106
|
+
required: ["x", "y"],
|
|
11107
|
+
additionalProperties: false
|
|
11108
|
+
},
|
|
11109
|
+
annotations: writeAnnotations("Magic wand", true),
|
|
11110
|
+
handler: async (args) => {
|
|
11111
|
+
const x = num2(args.x, NaN);
|
|
11112
|
+
const y = num2(args.y, NaN);
|
|
11113
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("Pass the seed point x and y in natural image pixels.");
|
|
11114
|
+
const { width, height } = controller.getState().imageDimensions;
|
|
11115
|
+
if (x < 0 || y < 0 || x >= width || y >= height) throw new Error(`(${x}, ${y}) is outside the image (0..${width - 1}, 0..${height - 1}).`);
|
|
11116
|
+
if (args.distance !== void 0 && args.distance !== "rgb" && args.distance !== "perceptual") {
|
|
11117
|
+
throw new Error(`Unknown distance "${String(args.distance)}". Use rgb or perceptual.`);
|
|
11118
|
+
}
|
|
11119
|
+
await controller.magicWand(
|
|
11120
|
+
{ x, y },
|
|
11121
|
+
{
|
|
11122
|
+
...shapeOptions(args),
|
|
11123
|
+
...sampleOptions(args),
|
|
11124
|
+
tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))),
|
|
11125
|
+
contiguous: args.contiguous !== false,
|
|
11126
|
+
distance: args.distance ?? "rgb"
|
|
11127
|
+
}
|
|
11128
|
+
);
|
|
11129
|
+
return summary(controller);
|
|
11130
|
+
}
|
|
11131
|
+
};
|
|
11132
|
+
const colorRangeTool = {
|
|
11133
|
+
name: "select_color_range",
|
|
11134
|
+
title: "Select a colour range",
|
|
11135
|
+
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.",
|
|
11136
|
+
inputSchema: {
|
|
11137
|
+
type: "object",
|
|
11138
|
+
properties: {
|
|
11139
|
+
samples: {
|
|
11140
|
+
type: "array",
|
|
11141
|
+
minItems: 1,
|
|
11142
|
+
maxItems: 32,
|
|
11143
|
+
items: {
|
|
11144
|
+
type: "object",
|
|
11145
|
+
properties: {
|
|
11146
|
+
color: { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "The sampled colour, #rrggbb." },
|
|
11147
|
+
x: { type: "number", description: "Where it was picked (natural image pixels), for localized." },
|
|
11148
|
+
y: { type: "number", description: "Where it was picked (natural image pixels), for localized." }
|
|
11149
|
+
},
|
|
11150
|
+
required: ["color"],
|
|
11151
|
+
additionalProperties: false
|
|
11152
|
+
},
|
|
11153
|
+
description: "Colours to select (the + eyedropper adds one). Read colours from capture_view or a seed pixel."
|
|
11154
|
+
},
|
|
11155
|
+
preset: { type: "string", enum: [...COLOR_RANGE_PRESETS], description: "A preset range instead of samples." },
|
|
11156
|
+
fuzziness: {
|
|
11157
|
+
type: "number",
|
|
11158
|
+
minimum: 0,
|
|
11159
|
+
maximum: 200,
|
|
11160
|
+
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."
|
|
11161
|
+
},
|
|
11162
|
+
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)." },
|
|
11163
|
+
invert: { type: "boolean", description: "Select everything except the range (default false)." },
|
|
11164
|
+
sampleMerged: sampleMergedSchema,
|
|
11165
|
+
sample: sampleSchema,
|
|
11166
|
+
mode: modeSchema,
|
|
11167
|
+
feather: featherSchema
|
|
11168
|
+
},
|
|
11169
|
+
additionalProperties: false
|
|
11170
|
+
},
|
|
11171
|
+
annotations: writeAnnotations("Select a colour range", true),
|
|
11172
|
+
handler: async (args) => {
|
|
11173
|
+
const hasSamples = Array.isArray(args.samples) && args.samples.length > 0;
|
|
11174
|
+
if (hasSamples === (args.preset !== void 0)) throw new Error("Pass samples (colours to match) or preset, not both.");
|
|
11175
|
+
const common = { ...sampleOptions(args), mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), invert: args.invert === true };
|
|
11176
|
+
if (!hasSamples) {
|
|
11177
|
+
if (!isColorRangePreset(args.preset)) throw new Error(`Unknown preset "${String(args.preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
|
|
11178
|
+
await controller.selectColorRangePreset(args.preset, { ...common, falloff: num2(args.fuzziness, 20) });
|
|
11179
|
+
} else {
|
|
11180
|
+
const samples = args.samples.map((s, i) => {
|
|
11181
|
+
if (typeof s?.color !== "string" || !/^#?[0-9a-f]{6}$/i.test(s.color.trim())) throw new Error(`samples[${i}].color must be #rrggbb.`);
|
|
11182
|
+
const at = typeof s.x === "number" && typeof s.y === "number" ? { x: s.x, y: s.y } : void 0;
|
|
11183
|
+
return at ? { color: s.color, at } : { color: s.color };
|
|
11184
|
+
});
|
|
11185
|
+
const localized = typeof args.localized === "number" && args.localized > 0 ? args.localized : void 0;
|
|
11186
|
+
await controller.selectColorRange(samples, { ...common, fuzziness: num2(args.fuzziness, 40), localized });
|
|
11187
|
+
}
|
|
11188
|
+
return summary(controller);
|
|
11189
|
+
}
|
|
11190
|
+
};
|
|
11191
|
+
const loadFromImage = {
|
|
11192
|
+
name: "load_selection_from_image",
|
|
11193
|
+
title: "Load a selection from a mask image",
|
|
11194
|
+
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.",
|
|
11195
|
+
inputSchema: {
|
|
11196
|
+
type: "object",
|
|
11197
|
+
properties: {
|
|
11198
|
+
image: { type: "string", minLength: 1, description: "The mask as a data URL (data:image/png;base64,\u2026) or an image URL." },
|
|
11199
|
+
channel: { type: "string", enum: ["luminance", "alpha", "red", "green", "blue"], description: "What to read as coverage (default luminance; alpha for a cutout PNG)." },
|
|
11200
|
+
threshold: { type: "number", minimum: 0, maximum: 255, description: "Values at or above this are selected, the rest not (default: keep soft values)." },
|
|
11201
|
+
invert: { type: "boolean", description: "Black = selected instead (default false)." },
|
|
11202
|
+
mode: modeSchema
|
|
11203
|
+
},
|
|
11204
|
+
required: ["image"],
|
|
11205
|
+
additionalProperties: false
|
|
11206
|
+
},
|
|
11207
|
+
annotations: { ...writeAnnotations("Load a selection from a mask image", true), openWorldHint: true },
|
|
11208
|
+
handler: async (args) => {
|
|
11209
|
+
if (typeof args.image !== "string" || !args.image.trim()) throw new Error("image must be a data URL or image URL.");
|
|
11210
|
+
const channel = args.channel ?? "luminance";
|
|
11211
|
+
if (!["luminance", "alpha", "red", "green", "blue"].includes(channel)) throw new Error(`Unknown channel "${String(channel)}". Use luminance, alpha, red, green or blue.`);
|
|
11212
|
+
await controller.loadSelectionFromImage(args.image, {
|
|
11213
|
+
channel,
|
|
11214
|
+
invert: args.invert === true,
|
|
11215
|
+
threshold: typeof args.threshold === "number" ? Math.min(255, Math.max(0, args.threshold)) : void 0,
|
|
11216
|
+
mode: parseSelectionMode(args.mode)
|
|
11217
|
+
});
|
|
11218
|
+
return summary(controller);
|
|
11219
|
+
}
|
|
11220
|
+
};
|
|
11221
|
+
const loadFromMask = {
|
|
11222
|
+
name: "load_selection_from_mask",
|
|
11223
|
+
title: "Load a selection from a layer mask",
|
|
11224
|
+
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.",
|
|
11225
|
+
inputSchema: {
|
|
11226
|
+
type: "object",
|
|
11227
|
+
properties: {
|
|
11228
|
+
layerId: { type: "string", minLength: 1, description: "The layer whose mask to load (see list_layers)." },
|
|
11229
|
+
invert: { type: "boolean", description: "Select what the mask hides instead (default false)." },
|
|
11230
|
+
mode: modeSchema
|
|
11231
|
+
},
|
|
11232
|
+
required: ["layerId"],
|
|
11233
|
+
additionalProperties: false
|
|
11234
|
+
},
|
|
11235
|
+
annotations: writeAnnotations("Load a selection from a layer mask", true),
|
|
11236
|
+
handler: (args) => {
|
|
11237
|
+
const id = String(args.layerId ?? "");
|
|
11238
|
+
const layer = controller.getLayer(id);
|
|
11239
|
+
if (!layer) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
11240
|
+
if (!layer.mask) throw new Error(`Layer "${id}" has no mask. Add one with add_layer_mask, or pick a layer with a mask.`);
|
|
11241
|
+
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.`);
|
|
11242
|
+
controller.selectionFromLayerMask(id, parseSelectionMode(args.mode), args.invert === true);
|
|
11243
|
+
return summary(controller);
|
|
11244
|
+
}
|
|
11245
|
+
};
|
|
11246
|
+
const display = {
|
|
11247
|
+
name: "set_selection_display",
|
|
11248
|
+
title: "Show or hide the selection",
|
|
11249
|
+
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.",
|
|
11250
|
+
inputSchema: {
|
|
11251
|
+
type: "object",
|
|
11252
|
+
properties: { display: { type: "string", enum: ["ants", "quickMask", "none"], description: "ants, quickMask or none." } },
|
|
11253
|
+
required: ["display"],
|
|
11254
|
+
additionalProperties: false
|
|
11255
|
+
},
|
|
11256
|
+
annotations: writeAnnotations("Show or hide the selection", true),
|
|
11257
|
+
handler: (args) => {
|
|
11258
|
+
controller.setSelectionDisplay(args.display);
|
|
11259
|
+
return { display: controller.getSelectionDisplay(), selection: summary(controller) };
|
|
11260
|
+
}
|
|
11261
|
+
};
|
|
11262
|
+
const sampleTolerance = {
|
|
11263
|
+
tolerance: { type: "number", minimum: 0, maximum: 255, description: "Largest per-channel difference from a selected colour, 0..255 (default 32, as magic_wand)." },
|
|
11264
|
+
sampleMerged: sampleMergedSchema,
|
|
11265
|
+
sample: sampleSchema
|
|
11266
|
+
};
|
|
11267
|
+
const growTool = {
|
|
11268
|
+
name: "grow_selection",
|
|
11269
|
+
title: "Grow the selection by colour",
|
|
11270
|
+
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.",
|
|
11271
|
+
inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
|
|
11272
|
+
annotations: writeAnnotations("Grow the selection by colour"),
|
|
11273
|
+
handler: async (args) => {
|
|
11274
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select part of the area first (magic_wand, select_rect).");
|
|
11275
|
+
await controller.growSelection({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
|
|
11276
|
+
return summary(controller);
|
|
11277
|
+
}
|
|
11278
|
+
};
|
|
11279
|
+
const similarTool = {
|
|
11280
|
+
name: "select_similar",
|
|
11281
|
+
title: "Select similar colours",
|
|
11282
|
+
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.",
|
|
11283
|
+
inputSchema: { type: "object", properties: sampleTolerance, additionalProperties: false },
|
|
11284
|
+
annotations: writeAnnotations("Select similar colours"),
|
|
11285
|
+
handler: async (args) => {
|
|
11286
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select a sample of the colour first.");
|
|
11287
|
+
await controller.selectSimilar({ ...sampleOptions(args), tolerance: Math.min(255, Math.max(0, num2(args.tolerance, 32))) });
|
|
11288
|
+
return summary(controller);
|
|
11289
|
+
}
|
|
11290
|
+
};
|
|
11291
|
+
const lineTool = {
|
|
11292
|
+
name: "select_line",
|
|
11293
|
+
title: "Select a single row or column",
|
|
11294
|
+
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.",
|
|
11295
|
+
inputSchema: {
|
|
11296
|
+
type: "object",
|
|
11297
|
+
properties: {
|
|
11298
|
+
orientation: { type: "string", enum: ["row", "column"], description: "row (horizontal, at y) or column (vertical, at x)." },
|
|
11299
|
+
position: { type: "number", minimum: 0, description: "y of the row or x of the column, in natural image pixels." },
|
|
11300
|
+
mode: modeSchema
|
|
11301
|
+
},
|
|
11302
|
+
required: ["orientation", "position"],
|
|
11303
|
+
additionalProperties: false
|
|
11304
|
+
},
|
|
11305
|
+
annotations: writeAnnotations("Select a single row or column", true),
|
|
11306
|
+
handler: (args) => {
|
|
11307
|
+
if (args.orientation !== "row" && args.orientation !== "column") throw new Error("orientation must be row or column.");
|
|
11308
|
+
controller.selectLine(args.orientation, num2(args.position, NaN), { mode: parseSelectionMode(args.mode) });
|
|
11309
|
+
return summary(controller);
|
|
11310
|
+
}
|
|
11311
|
+
};
|
|
11312
|
+
const reselectTool = {
|
|
11313
|
+
name: "reselect",
|
|
11314
|
+
title: "Reselect",
|
|
11315
|
+
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.",
|
|
11316
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11317
|
+
annotations: writeAnnotations("Reselect", true),
|
|
11318
|
+
handler: () => {
|
|
11319
|
+
if (!controller.canReselect()) throw new Error("There is no cleared selection to bring back.");
|
|
11320
|
+
controller.reselect();
|
|
11321
|
+
return summary(controller);
|
|
11322
|
+
}
|
|
11323
|
+
};
|
|
11324
|
+
const transparencyTool = {
|
|
11325
|
+
name: "select_layer_transparency",
|
|
11326
|
+
title: "Select a layer's pixels",
|
|
11327
|
+
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.",
|
|
11328
|
+
inputSchema: {
|
|
11329
|
+
type: "object",
|
|
11330
|
+
properties: {
|
|
11331
|
+
layerId: { type: "string", minLength: 1, description: "The layer whose pixels to select (see list_layers)." },
|
|
11332
|
+
mode: modeSchema,
|
|
11333
|
+
feather: featherSchema
|
|
11334
|
+
},
|
|
11335
|
+
required: ["layerId"],
|
|
11336
|
+
additionalProperties: false
|
|
11337
|
+
},
|
|
11338
|
+
annotations: writeAnnotations("Select a layer's pixels", true),
|
|
11339
|
+
handler: (args) => {
|
|
11340
|
+
const id = String(args.layerId ?? "");
|
|
11341
|
+
if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
11342
|
+
controller.selectLayerTransparency(id, { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0) });
|
|
11343
|
+
return summary(controller);
|
|
11344
|
+
}
|
|
11345
|
+
};
|
|
11346
|
+
const colorSchema = { type: "string", pattern: "^#?[0-9a-fA-F]{6}$", description: "Colour #rrggbb (default the foreground colour)." };
|
|
11347
|
+
const layerIdSchema = { type: "string", minLength: 1, description: "The layer (see list_layers; default the active layer)." };
|
|
11348
|
+
const requireLayerId = (v) => {
|
|
11349
|
+
if (v === void 0) return void 0;
|
|
11350
|
+
const id = String(v);
|
|
11351
|
+
if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
11352
|
+
return id;
|
|
11353
|
+
};
|
|
11354
|
+
const strokeTool = {
|
|
11355
|
+
name: "stroke_selection",
|
|
11356
|
+
title: "Stroke the selection",
|
|
11357
|
+
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.",
|
|
11358
|
+
inputSchema: {
|
|
11359
|
+
type: "object",
|
|
11360
|
+
properties: {
|
|
11361
|
+
width: { type: "number", exclusiveMinimum: 0, maximum: 250, description: "Band width in pixels." },
|
|
11362
|
+
location: { type: "string", enum: ["outside", "inside", "center"], description: "Where the band lies against the edge (default outside)." },
|
|
11363
|
+
color: colorSchema,
|
|
11364
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
|
|
11365
|
+
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
|
|
11366
|
+
layerId: layerIdSchema,
|
|
11367
|
+
channel: { type: "string", enum: ["pixels", "mask"], description: "Paint the pixels (default) or the layer mask." }
|
|
11368
|
+
},
|
|
11369
|
+
required: ["width"],
|
|
11370
|
+
additionalProperties: false
|
|
11371
|
+
},
|
|
11372
|
+
annotations: writeAnnotations("Stroke the selection"),
|
|
11373
|
+
handler: (args) => {
|
|
11374
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select the shape to outline first.");
|
|
11375
|
+
return controller.strokeSelection({
|
|
11376
|
+
width: num2(args.width, NaN),
|
|
11377
|
+
location: args.location,
|
|
11378
|
+
color: args.color,
|
|
11379
|
+
opacity: args.opacity,
|
|
11380
|
+
blend: args.blend,
|
|
11381
|
+
layerId: requireLayerId(args.layerId),
|
|
11382
|
+
channel: args.channel
|
|
11383
|
+
});
|
|
11384
|
+
}
|
|
11385
|
+
};
|
|
11386
|
+
const cropTool = {
|
|
11387
|
+
name: "crop_to_selection",
|
|
11388
|
+
title: "Crop to the selection",
|
|
11389
|
+
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.",
|
|
11390
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11391
|
+
annotations: writeAnnotations("Crop to the selection", true),
|
|
11392
|
+
handler: () => {
|
|
11393
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select the area to keep first.");
|
|
11394
|
+
return { crop: controller.cropToSelection() };
|
|
11395
|
+
}
|
|
11396
|
+
};
|
|
11397
|
+
const fillTool = {
|
|
11398
|
+
name: "content_aware_fill",
|
|
11399
|
+
title: "Content-Aware Fill",
|
|
11400
|
+
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.",
|
|
11401
|
+
inputSchema: {
|
|
11402
|
+
type: "object",
|
|
11403
|
+
properties: {
|
|
11404
|
+
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
|
|
11405
|
+
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
11406
|
+
seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
|
|
11407
|
+
layerId: layerIdSchema
|
|
11408
|
+
},
|
|
11409
|
+
additionalProperties: false
|
|
11410
|
+
},
|
|
11411
|
+
annotations: writeAnnotations("Content-Aware Fill"),
|
|
11412
|
+
handler: (args) => {
|
|
11413
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
|
|
11414
|
+
return controller.contentAwareFill({
|
|
11415
|
+
output: args.output,
|
|
11416
|
+
sampleRadius: args.sampleRadius,
|
|
11417
|
+
seed: args.seed,
|
|
11418
|
+
layerId: requireLayerId(args.layerId)
|
|
11419
|
+
});
|
|
11420
|
+
}
|
|
11421
|
+
};
|
|
11422
|
+
const copyTool = {
|
|
11423
|
+
name: "copy_pixels",
|
|
11424
|
+
title: "Copy pixels",
|
|
11425
|
+
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.",
|
|
11426
|
+
inputSchema: {
|
|
11427
|
+
type: "object",
|
|
11428
|
+
properties: { layerId: layerIdSchema, merged: { type: "boolean", description: "Copy the visible image of all layers (default false)." } },
|
|
11429
|
+
additionalProperties: false
|
|
11430
|
+
},
|
|
11431
|
+
annotations: { title: "Copy pixels", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
11432
|
+
handler: (args) => {
|
|
11433
|
+
const { width, height, x, y } = controller.copyPixels({ layerId: requireLayerId(args.layerId), merged: args.merged === true });
|
|
11434
|
+
return { width, height, x, y };
|
|
11435
|
+
}
|
|
11436
|
+
};
|
|
11437
|
+
const cutTool = {
|
|
11438
|
+
name: "cut_pixels",
|
|
11439
|
+
title: "Cut pixels",
|
|
11440
|
+
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.",
|
|
11441
|
+
inputSchema: { type: "object", properties: { layerId: layerIdSchema }, additionalProperties: false },
|
|
11442
|
+
annotations: { title: "Cut pixels", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11443
|
+
handler: (args) => {
|
|
11444
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select what to cut first (cutting everything would empty the layer).");
|
|
11445
|
+
const { width, height, x, y } = controller.cutPixels({ layerId: requireLayerId(args.layerId) });
|
|
11446
|
+
return { width, height, x, y };
|
|
11447
|
+
}
|
|
11448
|
+
};
|
|
11449
|
+
const pasteTool = {
|
|
11450
|
+
name: "paste_pixels",
|
|
11451
|
+
title: "Paste pixels",
|
|
11452
|
+
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.",
|
|
11453
|
+
inputSchema: {
|
|
11454
|
+
type: "object",
|
|
11455
|
+
properties: { mode: { type: "string", enum: ["new-layer", "in-place", "into", "outside"], description: "Where and how to paste (default new-layer)." } },
|
|
11456
|
+
additionalProperties: false
|
|
11457
|
+
},
|
|
11458
|
+
annotations: writeAnnotations("Paste pixels"),
|
|
11459
|
+
handler: (args) => controller.pastePixels(args.mode ?? "new-layer")
|
|
11460
|
+
};
|
|
11461
|
+
return [
|
|
11462
|
+
getSelection,
|
|
11463
|
+
selectAll,
|
|
11464
|
+
deselect,
|
|
11465
|
+
invert2,
|
|
11466
|
+
selectRect,
|
|
11467
|
+
selectEllipse,
|
|
11468
|
+
selectPolygonTool,
|
|
11469
|
+
lineTool,
|
|
11470
|
+
magicWandTool,
|
|
11471
|
+
colorRangeTool,
|
|
11472
|
+
loadFromImage,
|
|
11473
|
+
loadFromMask,
|
|
11474
|
+
transparencyTool,
|
|
11475
|
+
growTool,
|
|
11476
|
+
similarTool,
|
|
11477
|
+
reselectTool,
|
|
11478
|
+
display,
|
|
11479
|
+
selectRegion,
|
|
11480
|
+
selectByColor,
|
|
11481
|
+
modify,
|
|
11482
|
+
refine,
|
|
11483
|
+
applyMask,
|
|
11484
|
+
strokeTool,
|
|
11485
|
+
cropTool,
|
|
11486
|
+
fillTool,
|
|
11487
|
+
copyTool,
|
|
11488
|
+
cutTool,
|
|
11489
|
+
pasteTool
|
|
11490
|
+
];
|
|
10516
11491
|
}
|
|
10517
11492
|
function createImageSubjectSelectionTool(controller) {
|
|
10518
11493
|
return {
|
|
@@ -11272,12 +12247,12 @@ function createImageLayerTools(controller) {
|
|
|
11272
12247
|
const addMask = {
|
|
11273
12248
|
name: "add_layer_mask",
|
|
11274
12249
|
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)
|
|
12250
|
+
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
12251
|
inputSchema: {
|
|
11277
12252
|
type: "object",
|
|
11278
12253
|
properties: {
|
|
11279
12254
|
layerId: LAYER_ID2,
|
|
11280
|
-
from: { type: "string", enum: ["reveal-all", "hide-all", "selection"], description: "What the mask starts as (default reveal-all)." },
|
|
12255
|
+
from: { type: "string", enum: ["reveal-all", "hide-all", "selection", "hide-selection", "transparency"], description: "What the mask starts as (default reveal-all)." },
|
|
11281
12256
|
density: { type: "number", minimum: 0, maximum: 1, description: "Mask density 0..1 (default 1)." },
|
|
11282
12257
|
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px (default 0)." }
|
|
11283
12258
|
},
|
|
@@ -13331,13 +14306,14 @@ function createImageSmartObjectTools(controller) {
|
|
|
13331
14306
|
const addFilter = {
|
|
13332
14307
|
name: "add_smart_filter",
|
|
13333
14308
|
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)
|
|
14309
|
+
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
14310
|
inputSchema: {
|
|
13336
14311
|
type: "object",
|
|
13337
14312
|
properties: {
|
|
13338
14313
|
layerId: LAYER_ID4,
|
|
13339
14314
|
kind: { type: "string", enum: ["develop", "blur"], description: "Filter kind." },
|
|
13340
14315
|
index: { type: "integer", minimum: 0, description: "Position in the stack, 0 = bottom, applied first (default the top)." },
|
|
14316
|
+
useSelection: { type: "boolean", description: "With a selection, set the filter mask from it so the filter only changes the selected area (default true)." },
|
|
13341
14317
|
...FILTER_SETTINGS
|
|
13342
14318
|
},
|
|
13343
14319
|
required: ["layerId", "kind"],
|
|
@@ -13346,8 +14322,9 @@ function createImageSmartObjectTools(controller) {
|
|
|
13346
14322
|
annotations: { title: "Add a smart filter", ...WRITE7 },
|
|
13347
14323
|
handler: (args) => {
|
|
13348
14324
|
const id = smartId(args.layerId);
|
|
13349
|
-
const
|
|
13350
|
-
|
|
14325
|
+
const masked = args.useSelection !== false && controller.hasSelection;
|
|
14326
|
+
const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index, { useSelection: args.useSelection !== false });
|
|
14327
|
+
return { layerId: id, filter, smartFilters: info(id).smartFilters, masked };
|
|
13351
14328
|
}
|
|
13352
14329
|
};
|
|
13353
14330
|
const updateFilter = {
|
|
@@ -14160,7 +15137,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14160
15137
|
const addLayer = {
|
|
14161
15138
|
name: "add_adjustment_layer",
|
|
14162
15139
|
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.",
|
|
15140
|
+
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
15141
|
inputSchema: {
|
|
14165
15142
|
type: "object",
|
|
14166
15143
|
properties: {
|
|
@@ -14170,7 +15147,8 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14170
15147
|
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Blend mode (default normal)." },
|
|
14171
15148
|
name: { type: "string", description: "Layer name shown in the layers panel." },
|
|
14172
15149
|
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)." }
|
|
15150
|
+
parentId: { type: "string", description: "Put the layer at the top of this group (from list_layers)." },
|
|
15151
|
+
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
15152
|
},
|
|
14175
15153
|
required: ["kind"],
|
|
14176
15154
|
additionalProperties: false
|
|
@@ -14182,7 +15160,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14182
15160
|
idempotentHint: false,
|
|
14183
15161
|
openWorldHint: false
|
|
14184
15162
|
},
|
|
14185
|
-
handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {
|
|
15163
|
+
handler: ({ kind, params, opacity, blend, name, aboveId, parentId, useSelection }) => {
|
|
14186
15164
|
if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind "${kind}".`);
|
|
14187
15165
|
if (blend !== void 0 && !isBlendMode(blend)) throw new Error(`Unknown blend mode "${blend}".`);
|
|
14188
15166
|
if (aboveId !== void 0 && parentId !== void 0) throw new Error("Give aboveId or parentId, not both.");
|
|
@@ -14190,9 +15168,9 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
14190
15168
|
if (ref !== void 0 && !controller.getLayer(ref)) throw new Error(`Unknown layer "${ref}". Call list_layers for ids.`);
|
|
14191
15169
|
}
|
|
14192
15170
|
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 });
|
|
15171
|
+
const id = controller.addAdjustmentLayer({ kind, params, opacity, blend, name, aboveId, parentId, useSelection: useSelection !== false });
|
|
14194
15172
|
const layer = controller.getAdjustmentLayer(id);
|
|
14195
|
-
return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };
|
|
15173
|
+
return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id), masked: Boolean(layer.mask) };
|
|
14196
15174
|
}
|
|
14197
15175
|
};
|
|
14198
15176
|
const renderPreview = {
|
|
@@ -14326,6 +15304,14 @@ function removeCurvePoint(points, index) {
|
|
|
14326
15304
|
var round22 = (v) => Math.round(v * 100) / 100;
|
|
14327
15305
|
|
|
14328
15306
|
// src/image-editor/selection/gesture.ts
|
|
15307
|
+
function snapTo45(from, point) {
|
|
15308
|
+
const dx = point.x - from.x;
|
|
15309
|
+
const dy = point.y - from.y;
|
|
15310
|
+
const len = Math.hypot(dx, dy);
|
|
15311
|
+
if (len === 0) return point;
|
|
15312
|
+
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
15313
|
+
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
15314
|
+
}
|
|
14329
15315
|
var SelectionGesture = class {
|
|
14330
15316
|
constructor(controller, getOptions) {
|
|
14331
15317
|
this.controller = controller;
|
|
@@ -14337,13 +15323,20 @@ var SelectionGesture = class {
|
|
|
14337
15323
|
points = [];
|
|
14338
15324
|
current = null;
|
|
14339
15325
|
modifiers = {};
|
|
15326
|
+
/** Polygonal lasso corners while one is being drawn. */
|
|
15327
|
+
polygon = null;
|
|
15328
|
+
pressedAt = null;
|
|
15329
|
+
freehand = false;
|
|
15330
|
+
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
15331
|
+
closeRadius = 8;
|
|
14340
15332
|
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
14341
15333
|
error = null;
|
|
14342
15334
|
get active() {
|
|
14343
|
-
return this.start !== null;
|
|
15335
|
+
return this.start !== null || this.polygon !== null;
|
|
14344
15336
|
}
|
|
14345
15337
|
/** The outline of the shape being drawn, or `null`. */
|
|
14346
15338
|
get draftPath() {
|
|
15339
|
+
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
14347
15340
|
if (!this.start || !this.current) return null;
|
|
14348
15341
|
const { tool } = this.getOptions();
|
|
14349
15342
|
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
@@ -14366,18 +15359,71 @@ var SelectionGesture = class {
|
|
|
14366
15359
|
async begin(point, modifiers = {}) {
|
|
14367
15360
|
const options = this.getOptions();
|
|
14368
15361
|
this.error = null;
|
|
15362
|
+
if (this.polygon && options.tool !== "polygon") this.cancel();
|
|
15363
|
+
if (options.tool === "polygon") {
|
|
15364
|
+
this.pressedAt = point;
|
|
15365
|
+
this.freehand = false;
|
|
15366
|
+
if (!this.polygon) {
|
|
15367
|
+
this.modifiers = modifiers;
|
|
15368
|
+
this.polygon = [point];
|
|
15369
|
+
this.current = point;
|
|
15370
|
+
return;
|
|
15371
|
+
}
|
|
15372
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
15373
|
+
const p = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15374
|
+
const first = this.polygon[0];
|
|
15375
|
+
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
15376
|
+
this.close();
|
|
15377
|
+
return;
|
|
15378
|
+
}
|
|
15379
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
15380
|
+
this.current = p;
|
|
15381
|
+
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
15382
|
+
return;
|
|
15383
|
+
}
|
|
14369
15384
|
this.modifiers = modifiers;
|
|
14370
|
-
if (options.tool === "
|
|
15385
|
+
if (options.tool === "row" || options.tool === "column") {
|
|
15386
|
+
this.start = null;
|
|
15387
|
+
try {
|
|
15388
|
+
this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
|
|
15389
|
+
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
15390
|
+
feather: options.feather ?? 0
|
|
15391
|
+
});
|
|
15392
|
+
} catch (err) {
|
|
15393
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
15394
|
+
}
|
|
15395
|
+
return;
|
|
15396
|
+
}
|
|
15397
|
+
if (options.tool === "wand" || options.tool === "range") {
|
|
14371
15398
|
this.start = null;
|
|
15399
|
+
const common = {
|
|
15400
|
+
sampleMerged: options.sampleMerged,
|
|
15401
|
+
sample: options.sample,
|
|
15402
|
+
feather: options.feather ?? 0,
|
|
15403
|
+
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
15404
|
+
};
|
|
14372
15405
|
try {
|
|
15406
|
+
if (options.tool === "range") {
|
|
15407
|
+
const pixels = await this.controller.getSamplePixels(common);
|
|
15408
|
+
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
15409
|
+
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
|
|
15410
|
+
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
|
|
15411
|
+
const p = (y * pixels.width + x) * 4;
|
|
15412
|
+
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
15413
|
+
await this.controller.selectColorRange([{ color, at: point }], {
|
|
15414
|
+
...common,
|
|
15415
|
+
fuzziness: options.fuzziness ?? 40,
|
|
15416
|
+
localized: options.localized ? options.localized : void 0,
|
|
15417
|
+
invert: options.rangeInvert ?? false
|
|
15418
|
+
});
|
|
15419
|
+
return;
|
|
15420
|
+
}
|
|
14373
15421
|
await this.controller.magicWand(point, {
|
|
15422
|
+
...common,
|
|
14374
15423
|
tolerance: options.tolerance,
|
|
14375
15424
|
contiguous: options.contiguous,
|
|
14376
15425
|
antiAlias: options.antiAlias,
|
|
14377
|
-
distance: options.distance
|
|
14378
|
-
sampleMerged: options.sampleMerged,
|
|
14379
|
-
sample: options.sample,
|
|
14380
|
-
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
15426
|
+
distance: options.distance
|
|
14381
15427
|
});
|
|
14382
15428
|
} catch (err) {
|
|
14383
15429
|
this.error = err instanceof Error ? err.message : String(err);
|
|
@@ -14389,7 +15435,18 @@ var SelectionGesture = class {
|
|
|
14389
15435
|
this.points = [point];
|
|
14390
15436
|
}
|
|
14391
15437
|
/** Pointer move; returns true when the draft changed (re-render). */
|
|
14392
|
-
move(point) {
|
|
15438
|
+
move(point, modifiers = {}) {
|
|
15439
|
+
if (this.polygon) {
|
|
15440
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
15441
|
+
if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
|
|
15442
|
+
this.freehand = true;
|
|
15443
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
|
|
15444
|
+
this.current = null;
|
|
15445
|
+
return true;
|
|
15446
|
+
}
|
|
15447
|
+
this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15448
|
+
return true;
|
|
15449
|
+
}
|
|
14393
15450
|
if (!this.start) return false;
|
|
14394
15451
|
this.current = point;
|
|
14395
15452
|
if (this.getOptions().tool === "lasso") {
|
|
@@ -14401,6 +15458,12 @@ var SelectionGesture = class {
|
|
|
14401
15458
|
}
|
|
14402
15459
|
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
14403
15460
|
end(point, modifiers) {
|
|
15461
|
+
if (this.polygon) {
|
|
15462
|
+
if (point && this.freehand) this.move(point);
|
|
15463
|
+
this.pressedAt = null;
|
|
15464
|
+
this.freehand = false;
|
|
15465
|
+
return false;
|
|
15466
|
+
}
|
|
14404
15467
|
if (!this.start) return false;
|
|
14405
15468
|
if (point) this.move(point);
|
|
14406
15469
|
const options = this.getOptions();
|
|
@@ -14416,17 +15479,65 @@ var SelectionGesture = class {
|
|
|
14416
15479
|
this.current = null;
|
|
14417
15480
|
this.points = [];
|
|
14418
15481
|
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
14419
|
-
const opts = { mode, antiAlias: options.antiAlias };
|
|
15482
|
+
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
14420
15483
|
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
14421
15484
|
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
14422
15485
|
}
|
|
14423
|
-
/**
|
|
15486
|
+
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
15487
|
+
close() {
|
|
15488
|
+
const polygon = this.polygon;
|
|
15489
|
+
if (!polygon) return false;
|
|
15490
|
+
const options = this.getOptions();
|
|
15491
|
+
this.cancel();
|
|
15492
|
+
if (polygon.length < 3) return false;
|
|
15493
|
+
return this.controller.selectPolygon(polygon, {
|
|
15494
|
+
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
15495
|
+
antiAlias: options.antiAlias,
|
|
15496
|
+
feather: options.feather ?? 0
|
|
15497
|
+
});
|
|
15498
|
+
}
|
|
15499
|
+
/**
|
|
15500
|
+
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
15501
|
+
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
15502
|
+
*/
|
|
15503
|
+
key(key) {
|
|
15504
|
+
if (!this.polygon) return false;
|
|
15505
|
+
if (key === "Enter") this.close();
|
|
15506
|
+
else if (key === "Escape") this.cancel();
|
|
15507
|
+
else if (key === "Backspace" || key === "Delete") {
|
|
15508
|
+
this.polygon.pop();
|
|
15509
|
+
if (!this.polygon.length) this.cancel();
|
|
15510
|
+
} else return false;
|
|
15511
|
+
return true;
|
|
15512
|
+
}
|
|
15513
|
+
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
14424
15514
|
cancel() {
|
|
14425
15515
|
this.start = null;
|
|
14426
15516
|
this.current = null;
|
|
14427
15517
|
this.points = [];
|
|
15518
|
+
this.polygon = null;
|
|
15519
|
+
this.pressedAt = null;
|
|
15520
|
+
this.freehand = false;
|
|
14428
15521
|
}
|
|
14429
15522
|
};
|
|
15523
|
+
function handleSelectionShortcut(controller, e, onError) {
|
|
15524
|
+
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
15525
|
+
const key = e.key.toLowerCase();
|
|
15526
|
+
const run = (fn) => {
|
|
15527
|
+
try {
|
|
15528
|
+
fn();
|
|
15529
|
+
} catch (err) {
|
|
15530
|
+
onError?.(err instanceof Error ? err.message : String(err));
|
|
15531
|
+
}
|
|
15532
|
+
return true;
|
|
15533
|
+
};
|
|
15534
|
+
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
15535
|
+
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
15536
|
+
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
15537
|
+
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
15538
|
+
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
15539
|
+
return false;
|
|
15540
|
+
}
|
|
14430
15541
|
|
|
14431
15542
|
// src/image-editor/mosaic-filter.ts
|
|
14432
15543
|
function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
|
|
@@ -14566,6 +15677,21 @@ function applyRegionBlur(imageData, regions, radius = 10) {
|
|
|
14566
15677
|
}
|
|
14567
15678
|
|
|
14568
15679
|
// src/image-editor/layers/panel.ts
|
|
15680
|
+
function maskThumbnailClick(controller, layer, e) {
|
|
15681
|
+
if (!layer.mask) return;
|
|
15682
|
+
if (e.ctrlKey || e.metaKey) {
|
|
15683
|
+
controller.selectionFromLayerMask(layer.id, selectionModeFromModifiers(e, "replace"));
|
|
15684
|
+
} else if (e.shiftKey) {
|
|
15685
|
+
controller.setLayerMaskProps(layer.id, { enabled: layer.mask.enabled === false });
|
|
15686
|
+
} else {
|
|
15687
|
+
controller.setActiveLayer(layer.id, "mask");
|
|
15688
|
+
}
|
|
15689
|
+
}
|
|
15690
|
+
function layerThumbnailClick(controller, layer, e) {
|
|
15691
|
+
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
15692
|
+
controller.selectLayerTransparency(layer.id, { mode: selectionModeFromModifiers(e, "replace") });
|
|
15693
|
+
return true;
|
|
15694
|
+
}
|
|
14569
15695
|
function layerPanelRows(layers, activeId) {
|
|
14570
15696
|
const rows = [];
|
|
14571
15697
|
const walk = (list, depth, parentId) => {
|
|
@@ -14637,6 +15763,7 @@ function layerMenu(controller) {
|
|
|
14637
15763
|
const below = loc ? siblings[loc.index - 1] : void 0;
|
|
14638
15764
|
const hasSel = controller.hasSelection;
|
|
14639
15765
|
const isPixel = active?.type === "pixel";
|
|
15766
|
+
const hasOwnPixels = active?.type === "pixel" || active?.type === "shape" || active?.type === "type" || active?.type === "smart";
|
|
14640
15767
|
return [
|
|
14641
15768
|
{ action: "new-layer", label: "New Layer", enabled: true },
|
|
14642
15769
|
{ action: "new-group", label: "New Group", enabled: true },
|
|
@@ -14651,6 +15778,9 @@ function layerMenu(controller) {
|
|
|
14651
15778
|
{ action: "via-cut", label: "Layer via Cut", enabled: isPixel && hasSel },
|
|
14652
15779
|
{ action: "add-mask", label: "Add Layer Mask", enabled: Boolean(active) },
|
|
14653
15780
|
{ action: "add-mask-selection", label: "Add Mask from Selection", enabled: Boolean(active) && hasSel },
|
|
15781
|
+
{ action: "add-mask-hide-selection", label: "Add Mask Hiding Selection", enabled: Boolean(active) && hasSel },
|
|
15782
|
+
{ action: "add-mask-transparency", label: "Add Mask from Transparency", enabled: hasOwnPixels },
|
|
15783
|
+
{ action: "select-transparency", label: "Select Layer Pixels", enabled: hasOwnPixels },
|
|
14654
15784
|
{ action: "apply-mask", label: "Apply Layer Mask", enabled: isPixel && Boolean(active?.mask) },
|
|
14655
15785
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
14656
15786
|
{ action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
|
|
@@ -14702,6 +15832,15 @@ async function runLayerMenuAction(controller, action) {
|
|
|
14702
15832
|
case "add-mask":
|
|
14703
15833
|
if (id) controller.addLayerMask(id, "reveal-all");
|
|
14704
15834
|
break;
|
|
15835
|
+
case "add-mask-hide-selection":
|
|
15836
|
+
if (id) controller.addLayerMask(id, "hide-selection");
|
|
15837
|
+
break;
|
|
15838
|
+
case "add-mask-transparency":
|
|
15839
|
+
if (id) controller.addLayerMask(id, "transparency");
|
|
15840
|
+
break;
|
|
15841
|
+
case "select-transparency":
|
|
15842
|
+
if (id) controller.selectLayerTransparency(id);
|
|
15843
|
+
break;
|
|
14705
15844
|
case "add-mask-selection":
|
|
14706
15845
|
if (id) controller.addLayerMask(id, "selection");
|
|
14707
15846
|
break;
|
|
@@ -14886,6 +16025,7 @@ async function addLayerOfKind(controller, id, init) {
|
|
|
14886
16025
|
if (isAdjustmentKind(kind)) {
|
|
14887
16026
|
return controller.addAdjustmentLayer({
|
|
14888
16027
|
kind,
|
|
16028
|
+
useSelection: true,
|
|
14889
16029
|
...init
|
|
14890
16030
|
});
|
|
14891
16031
|
}
|
|
@@ -15834,7 +16974,7 @@ function createImageEffectsToolProvider(controller, options = {}) {
|
|
|
15834
16974
|
skill: "pixerate-image-editor",
|
|
15835
16975
|
target: { noun: "layer", id: "A layer id from list_layers." },
|
|
15836
16976
|
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.",
|
|
16977
|
+
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
16978
|
provider: () => controller.effectsProvider,
|
|
15839
16979
|
readOnlyReason,
|
|
15840
16980
|
list: (target) => {
|
|
@@ -15842,8 +16982,8 @@ function createImageEffectsToolProvider(controller, options = {}) {
|
|
|
15842
16982
|
return layers.map((l) => ({ target: l.id, ...l.name ? { name: l.name } : {}, effects: imageLayerAppliedEffects(l) })).filter((t) => target || t.effects.length);
|
|
15843
16983
|
},
|
|
15844
16984
|
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 });
|
|
16985
|
+
const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity, useSelection: true });
|
|
16986
|
+
return result(r.layerId, r.filter.id, { converted: r.converted, masked: r.masked });
|
|
15847
16987
|
},
|
|
15848
16988
|
update: async (target, instance, changes) => {
|
|
15849
16989
|
const layer = layerOf(target);
|
|
@@ -15974,6 +17114,8 @@ export {
|
|
|
15974
17114
|
CANVAS_SIZE_PRESETS,
|
|
15975
17115
|
CLONE_TRANSFORM_CONTROLS,
|
|
15976
17116
|
COLOR_MODE_OPTIONS,
|
|
17117
|
+
COLOR_RANGE_PRESETS,
|
|
17118
|
+
COLOR_RANGE_PRESET_LABELS,
|
|
15977
17119
|
CoverageMap,
|
|
15978
17120
|
D50,
|
|
15979
17121
|
D65,
|
|
@@ -16030,6 +17172,7 @@ export {
|
|
|
16030
17172
|
HIDE,
|
|
16031
17173
|
HISTOGRAM_BINS,
|
|
16032
17174
|
HUE_RANGE_LABELS,
|
|
17175
|
+
HUE_RANGE_PRESETS,
|
|
16033
17176
|
IDENTITY_CLONE_TRANSFORM,
|
|
16034
17177
|
IDENTITY_MATRIX3,
|
|
16035
17178
|
IMAGE_ADJUSTMENT_CONTROLS,
|
|
@@ -16093,6 +17236,7 @@ export {
|
|
|
16093
17236
|
PARAGRAPH_STYLE_FIELDS,
|
|
16094
17237
|
PARAM,
|
|
16095
17238
|
PARAM_FLOATS,
|
|
17239
|
+
PASTE_MODES,
|
|
16096
17240
|
PATTERN_BLOCK_KEYS,
|
|
16097
17241
|
PIGMENT_BANDS,
|
|
16098
17242
|
PIGMENT_CHANNELS,
|
|
@@ -16296,6 +17440,7 @@ export {
|
|
|
16296
17440
|
colorModePlanes16,
|
|
16297
17441
|
colorPanelView,
|
|
16298
17442
|
colorRange,
|
|
17443
|
+
colorRangePreset,
|
|
16299
17444
|
combineSelections,
|
|
16300
17445
|
compileAdjustmentLayer,
|
|
16301
17446
|
componentPath,
|
|
@@ -16468,9 +17613,11 @@ export {
|
|
|
16468
17613
|
grainHeights,
|
|
16469
17614
|
greyMorphology,
|
|
16470
17615
|
groupLayers,
|
|
17616
|
+
growSelection,
|
|
16471
17617
|
guidePlanes,
|
|
16472
17618
|
guidedFilterColor,
|
|
16473
17619
|
handlePaintShortcut,
|
|
17620
|
+
handleSelectionShortcut,
|
|
16474
17621
|
hasActiveEffects,
|
|
16475
17622
|
hasLayersBelow,
|
|
16476
17623
|
hasResampledLegacyMask,
|
|
@@ -16515,6 +17662,7 @@ export {
|
|
|
16515
17662
|
isBitDepth,
|
|
16516
17663
|
isBlendMode,
|
|
16517
17664
|
isBrushstrokeBrush,
|
|
17665
|
+
isColorRangePreset,
|
|
16518
17666
|
isContentsDecoded,
|
|
16519
17667
|
isDefaultDepthMask,
|
|
16520
17668
|
isGradientPaint,
|
|
@@ -16561,6 +17709,7 @@ export {
|
|
|
16561
17709
|
layerName,
|
|
16562
17710
|
layerPanelRows,
|
|
16563
17711
|
layerPanels,
|
|
17712
|
+
layerThumbnailClick,
|
|
16564
17713
|
layerTree,
|
|
16565
17714
|
layerTypeLabel,
|
|
16566
17715
|
layerWarning,
|
|
@@ -16589,6 +17738,7 @@ export {
|
|
|
16589
17738
|
mapSubpaths,
|
|
16590
17739
|
maskBounds,
|
|
16591
17740
|
maskSummary,
|
|
17741
|
+
maskThumbnailClick,
|
|
16592
17742
|
matrixFromCorners,
|
|
16593
17743
|
matrixFromParams,
|
|
16594
17744
|
measureCanvasViewport,
|
|
@@ -16784,11 +17934,13 @@ export {
|
|
|
16784
17934
|
segmentToSelection,
|
|
16785
17935
|
segmentationOutlines,
|
|
16786
17936
|
segmentsToSelection,
|
|
17937
|
+
selectSimilar,
|
|
16787
17938
|
selectionArea,
|
|
16788
17939
|
selectionFromCoverage,
|
|
16789
17940
|
selectionFromPixels,
|
|
16790
17941
|
selectionModeFromModifiers,
|
|
16791
17942
|
selectionOutline,
|
|
17943
|
+
selectionRing,
|
|
16792
17944
|
selectionToCoverage,
|
|
16793
17945
|
selectionToPixels,
|
|
16794
17946
|
selectionValueAt,
|
|
@@ -16821,11 +17973,13 @@ export {
|
|
|
16821
17973
|
shapeOutline,
|
|
16822
17974
|
shapeRenderClip,
|
|
16823
17975
|
shiftCoverage,
|
|
17976
|
+
skinToneRange,
|
|
16824
17977
|
smartFilterRows,
|
|
16825
17978
|
smartObjectLayers,
|
|
16826
17979
|
smoothCoverage,
|
|
16827
17980
|
smoothPoints,
|
|
16828
17981
|
smudgeEffect,
|
|
17982
|
+
snapTo45,
|
|
16829
17983
|
solidPaint,
|
|
16830
17984
|
solidSurface,
|
|
16831
17985
|
solveMembrane,
|