@pixerate/editor 0.188.0 → 0.190.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-IGMNJM2H.js → chunk-7ZWLDVFH.js} +979 -753
- package/dist/{chunk-JYV4R62M.cjs → chunk-T4U4O6DS.cjs} +1013 -787
- package/dist/image-editor/controller.d.cts +68 -1
- package/dist/image-editor/controller.d.ts +68 -1
- package/dist/image-editor/free-transform.d.cts +3 -6
- package/dist/image-editor/free-transform.d.ts +3 -6
- package/dist/image-editor/index.cjs +1867 -1413
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +869 -415
- package/dist/image-editor/layer-tools.d.cts +1 -1
- package/dist/image-editor/layer-tools.d.ts +1 -1
- package/dist/image-editor/layers/compositor.d.cts +3 -0
- package/dist/image-editor/layers/compositor.d.ts +3 -0
- package/dist/image-editor/layers/model.d.cts +23 -0
- package/dist/image-editor/layers/model.d.ts +23 -0
- package/dist/image-editor/layers/paint-ui.d.cts +2 -1
- package/dist/image-editor/layers/paint-ui.d.ts +2 -1
- package/dist/image-editor/layers/panel.d.cts +14 -3
- package/dist/image-editor/layers/panel.d.ts +14 -3
- package/dist/image-editor/layers/serialize.d.cts +11 -0
- package/dist/image-editor/layers/serialize.d.ts +11 -0
- package/dist/image-editor/layers/shape.d.cts +5 -0
- package/dist/image-editor/layers/shape.d.ts +5 -0
- package/dist/image-editor/layers/vector-mask.d.cts +51 -0
- package/dist/image-editor/layers/vector-mask.d.ts +51 -0
- package/dist/image-editor/psd/import.d.cts +0 -7
- package/dist/image-editor/psd/import.d.ts +0 -7
- package/dist/image-editor/renderer.d.cts +6 -0
- package/dist/image-editor/renderer.d.ts +6 -0
- package/dist/image-editor/selection/gesture.d.cts +10 -0
- package/dist/image-editor/selection/gesture.d.ts +10 -0
- package/dist/image-editor/selection/layer-mask.d.cts +6 -0
- package/dist/image-editor/selection/layer-mask.d.ts +6 -0
- package/dist/image-editor/types.d.cts +8 -0
- package/dist/image-editor/types.d.ts +8 -0
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +7 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +10 -3
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
combineSelections,
|
|
100
100
|
compileAdjustmentLayer,
|
|
101
101
|
componentPath,
|
|
102
|
+
componentsCoverage,
|
|
102
103
|
compositeDeep,
|
|
103
104
|
computeSelectionBounds,
|
|
104
105
|
contentsBox,
|
|
@@ -137,6 +138,7 @@ import {
|
|
|
137
138
|
distanceTransform,
|
|
138
139
|
documentContents,
|
|
139
140
|
duplicateLayer,
|
|
141
|
+
effectiveLayerMask,
|
|
140
142
|
encodeDeepSurface,
|
|
141
143
|
encodeRle,
|
|
142
144
|
encodeSelectionRle,
|
|
@@ -148,6 +150,7 @@ import {
|
|
|
148
150
|
filterMaskCoverage,
|
|
149
151
|
flattenPath,
|
|
150
152
|
flipCorners,
|
|
153
|
+
followLayerMove,
|
|
151
154
|
fullSelection,
|
|
152
155
|
geometryHomography,
|
|
153
156
|
getAnnotationBounds,
|
|
@@ -177,6 +180,7 @@ import {
|
|
|
177
180
|
invertMatrix3,
|
|
178
181
|
invertSelection,
|
|
179
182
|
isActiveFilter,
|
|
183
|
+
isActiveVectorMask,
|
|
180
184
|
isAdjustmentKind,
|
|
181
185
|
isContentsDecoded,
|
|
182
186
|
isDefaultDepthMask,
|
|
@@ -196,6 +200,7 @@ import {
|
|
|
196
200
|
layerMaskCoverage,
|
|
197
201
|
layerMaskFromSelection,
|
|
198
202
|
layerMaskKey,
|
|
203
|
+
layerMaskSelection,
|
|
199
204
|
layerName,
|
|
200
205
|
layerTree,
|
|
201
206
|
layerTypeLabel,
|
|
@@ -236,6 +241,7 @@ import {
|
|
|
236
241
|
offsetLayer,
|
|
237
242
|
offsetShape,
|
|
238
243
|
offsetSmartObject,
|
|
244
|
+
offsetVectorMask,
|
|
239
245
|
parseAdjustmentBlock,
|
|
240
246
|
parseBlend,
|
|
241
247
|
parseLfx2,
|
|
@@ -289,6 +295,7 @@ import {
|
|
|
289
295
|
rerenderText,
|
|
290
296
|
resizeSelection,
|
|
291
297
|
resolveExternalAssets,
|
|
298
|
+
resolveVectorMasks,
|
|
292
299
|
rgbToGray,
|
|
293
300
|
rgbToHsl,
|
|
294
301
|
runLayerFilters,
|
|
@@ -340,6 +347,7 @@ import {
|
|
|
340
347
|
transformMap,
|
|
341
348
|
transformShapeComponents,
|
|
342
349
|
transformTypeLayer,
|
|
350
|
+
transformVectorMask,
|
|
343
351
|
transformedBox,
|
|
344
352
|
transformedPoint,
|
|
345
353
|
translateLayer,
|
|
@@ -358,6 +366,9 @@ import {
|
|
|
358
366
|
updateTypeLayer,
|
|
359
367
|
userErrorMessage,
|
|
360
368
|
uvToXy,
|
|
369
|
+
vectorMaskBounds,
|
|
370
|
+
vectorMaskCoverage,
|
|
371
|
+
vectorMaskThumbnail,
|
|
361
372
|
versionedDescriptorBytes,
|
|
362
373
|
vibranceLinear,
|
|
363
374
|
vibrancePixel,
|
|
@@ -372,7 +383,7 @@ import {
|
|
|
372
383
|
writePsd,
|
|
373
384
|
xyToTempTint,
|
|
374
385
|
xyToUv
|
|
375
|
-
} from "../chunk-
|
|
386
|
+
} from "../chunk-7ZWLDVFH.js";
|
|
376
387
|
import {
|
|
377
388
|
parseCssColor
|
|
378
389
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -1365,6 +1376,24 @@ var ImageEditorRenderer = class {
|
|
|
1365
1376
|
}
|
|
1366
1377
|
ctx.restore();
|
|
1367
1378
|
}
|
|
1379
|
+
const maskView = state.maskView;
|
|
1380
|
+
if (maskView) {
|
|
1381
|
+
const layer = flattenLayers(state.layers ?? []).find((l) => l.id === maskView.layerId);
|
|
1382
|
+
const mask = layer ? this.maskViewSelection(layer, Math.round(state.imageDimensions.width), Math.round(state.imageDimensions.height)) : null;
|
|
1383
|
+
if (mask) {
|
|
1384
|
+
ctx.save();
|
|
1385
|
+
try {
|
|
1386
|
+
applyImageProjectionToContext(ctx, projection);
|
|
1387
|
+
ctx.beginPath();
|
|
1388
|
+
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1389
|
+
ctx.clip();
|
|
1390
|
+
if (maskView.mode === "gray") this.drawMaskGray(ctx, mask);
|
|
1391
|
+
else this.drawMaskOverlay(ctx, mask, { color: [255, 0, 0], opacity: 0.5, indicates: "masked" });
|
|
1392
|
+
} catch {
|
|
1393
|
+
}
|
|
1394
|
+
ctx.restore();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1368
1397
|
const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1369
1398
|
const shownChannels = (state.channels ?? []).filter((c) => c.visible);
|
|
1370
1399
|
if (shownChannels.length) {
|
|
@@ -1804,6 +1833,32 @@ var ImageEditorRenderer = class {
|
|
|
1804
1833
|
return this.emptyMaskCache;
|
|
1805
1834
|
}
|
|
1806
1835
|
overlayCache = /* @__PURE__ */ new WeakMap();
|
|
1836
|
+
maskViewCache = /* @__PURE__ */ new WeakMap();
|
|
1837
|
+
grayCache = /* @__PURE__ */ new WeakMap();
|
|
1838
|
+
/** A layer's mask as canvas-sized coverage for its mask view, kept while the mask is unchanged. */
|
|
1839
|
+
maskViewSelection(layer, width, height) {
|
|
1840
|
+
if (!layer.mask || layer.mask.source || width < 1 || height < 1) return null;
|
|
1841
|
+
const key = `${width}x${height}:${layer.type === "pixel" ? `${layer.offset.x},${layer.offset.y}` : ""}`;
|
|
1842
|
+
const cached = this.maskViewCache.get(layer.mask.surface);
|
|
1843
|
+
if (cached?.key === key) return cached.mask;
|
|
1844
|
+
const mask = layerMaskSelection(layer, width, height);
|
|
1845
|
+
if (mask) this.maskViewCache.set(layer.mask.surface, { key, mask });
|
|
1846
|
+
return mask;
|
|
1847
|
+
}
|
|
1848
|
+
/** A mask alone in grey (white = revealed), opaque, drawn in natural image space. */
|
|
1849
|
+
drawMaskGray(ctx, mask) {
|
|
1850
|
+
let canvas = this.grayCache.get(mask);
|
|
1851
|
+
if (!canvas) {
|
|
1852
|
+
canvas = this.createCanvas(mask.width, mask.height);
|
|
1853
|
+
const mctx = canvas.getContext("2d");
|
|
1854
|
+
if (!mctx || typeof mctx.createImageData !== "function") return;
|
|
1855
|
+
const img = mctx.createImageData(mask.width, mask.height);
|
|
1856
|
+
img.data.set(selectionToPixels(mask, "gray").data);
|
|
1857
|
+
mctx.putImageData(img, 0, 0);
|
|
1858
|
+
this.grayCache.set(mask, canvas);
|
|
1859
|
+
}
|
|
1860
|
+
ctx.drawImage(canvas, 0, 0, mask.width, mask.height);
|
|
1861
|
+
}
|
|
1807
1862
|
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
1808
1863
|
drawMaskOverlay(ctx, mask, style) {
|
|
1809
1864
|
const key = `${style.color?.join(",")}|${style.opacity}|${style.indicates}`;
|
|
@@ -4119,6 +4174,24 @@ var FreeTransformSession = class {
|
|
|
4119
4174
|
}
|
|
4120
4175
|
/** Resamples every part for the current geometry and shows it (or returns it, for commit). */
|
|
4121
4176
|
resample(final, preview = {}) {
|
|
4177
|
+
const out = this.resampleContent(final, preview);
|
|
4178
|
+
return { ...out, layers: this.moveVectorMask(out.layers) };
|
|
4179
|
+
}
|
|
4180
|
+
/** A linked vector mask follows the layer's content through the same document-space map. */
|
|
4181
|
+
moveVectorMask(layers) {
|
|
4182
|
+
const g = this.geometryValue;
|
|
4183
|
+
if (!this.layerId || this.copyId || !this.moves.includes("pixels") || isIdentityTransform(g)) return layers;
|
|
4184
|
+
const vm = findLayer(layers, this.layerId)?.layer.vectorMask;
|
|
4185
|
+
if (!vm || vm.linked === false) return layers;
|
|
4186
|
+
const warp = hasWarp(g);
|
|
4187
|
+
const f = warp ? transformMap(g) : null;
|
|
4188
|
+
const h = warp ? null : geometryHomography(g);
|
|
4189
|
+
const affine = !warp && h && Math.abs(h[6]) < 1e-12 && Math.abs(h[7]) < 1e-12 && Math.abs(h[8] - 1) < 1e-12 ? [h[0], h[3], h[1], h[4], h[2], h[5]] : void 0;
|
|
4190
|
+
const map = (p) => (f ? f(p) : h ? applyHomography(h, p) : p) ?? p;
|
|
4191
|
+
const moved = transformVectorMask(vm, map, affine);
|
|
4192
|
+
return mapLayer(layers, this.layerId, (l) => ({ ...l, vectorMask: moved }));
|
|
4193
|
+
}
|
|
4194
|
+
resampleContent(final, preview = {}) {
|
|
4122
4195
|
if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };
|
|
4123
4196
|
if (this.typeTarget) return { layers: this.resampleType(), selection: this.startSelection };
|
|
4124
4197
|
const g = this.geometryValue;
|
|
@@ -6122,6 +6195,7 @@ var ImageEditorController = class {
|
|
|
6122
6195
|
let moved = { ...transformTypeLayer(layer, [1, 0, 0, 1, mx, my]), surface: translateSurface(layer.surface, mx, my) };
|
|
6123
6196
|
const mask = layer.mask;
|
|
6124
6197
|
if (mask && mask.linked !== false && !mask.source && (mx || my)) moved = { ...moved, mask: { ...mask, surface: translateSurface(mask.surface, mx, my) } };
|
|
6198
|
+
moved = followLayerMove(moved, mx, my);
|
|
6125
6199
|
this.commitLayers(mapLayer(this.state.layers, id, () => moved), void 0, commitToHistory);
|
|
6126
6200
|
const t = moved.transform ?? [1, 0, 0, 1, 0, 0];
|
|
6127
6201
|
return { offset: { x: Math.round(t[4]), y: Math.round(t[5]) }, bounds: this.getLayerBounds(id) };
|
|
@@ -6173,6 +6247,139 @@ var ImageEditorController = class {
|
|
|
6173
6247
|
if (this.state.activeLayerId === id) this.state.activeTarget = "pixels";
|
|
6174
6248
|
});
|
|
6175
6249
|
}
|
|
6250
|
+
// --- Vector masks ---
|
|
6251
|
+
/** The layer for a vector mask command: not a shape layer (its path is its vector mask), not a depth-masked one. */
|
|
6252
|
+
vectorMaskLayer(id) {
|
|
6253
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
6254
|
+
const name = layer.name || id;
|
|
6255
|
+
if (layer.type === "shape") throw new LayerOperationError(`Layer "${name}" is a shape layer, whose path already is its vector mask; change it with set_shape_component, or rasterize the layer first.`);
|
|
6256
|
+
if (layer.mask?.source) throw new LayerOperationError(`Layer "${name}" has a depth mask, which cannot be combined with a vector mask; remove the depth mask first.`);
|
|
6257
|
+
return layer;
|
|
6258
|
+
}
|
|
6259
|
+
requireVectorMask(id) {
|
|
6260
|
+
const layer = this.vectorMaskLayer(id);
|
|
6261
|
+
if (!layer.vectorMask) throw new LayerOperationError(`Layer "${layer.name || id}" has no vector mask; add one with add_vector_mask.`);
|
|
6262
|
+
return { layer, vm: layer.vectorMask };
|
|
6263
|
+
}
|
|
6264
|
+
/** Components from a path spec: `components`, or one `shape` / `subpaths`. */
|
|
6265
|
+
vectorMaskComponents(path) {
|
|
6266
|
+
if (path.components?.length && (path.shape !== void 0 || path.subpaths !== void 0)) throw new LayerOperationError("Give components, or shape / subpaths, not both.");
|
|
6267
|
+
if (path.fillRule !== void 0 && path.fillRule !== "nonzero" && path.fillRule !== "evenodd") throw new LayerOperationError("fillRule must be nonzero or evenodd.");
|
|
6268
|
+
const specs = path.components?.length ? path.components : [path];
|
|
6269
|
+
return specs.map((c, i) => this.componentFrom(c, path.components?.length ? `components[${i}].` : ""));
|
|
6270
|
+
}
|
|
6271
|
+
/**
|
|
6272
|
+
* Layer › Vector Mask: `reveal-all` (an empty path showing everything), `hide-all` (an empty path
|
|
6273
|
+
* hiding everything) or `path` (reveals inside the path given). Replaces an existing vector mask;
|
|
6274
|
+
* the raster mask is kept and both apply. One undo step. Returns the vector mask.
|
|
6275
|
+
*/
|
|
6276
|
+
addVectorMask(id, from = "reveal-all", path = {}) {
|
|
6277
|
+
this.vectorMaskLayer(id);
|
|
6278
|
+
let vm;
|
|
6279
|
+
if (from === "path") {
|
|
6280
|
+
vm = { components: this.vectorMaskComponents(path) };
|
|
6281
|
+
if (path.fillRule === "evenodd") vm.fillRule = "evenodd";
|
|
6282
|
+
} else if (from === "reveal-all" || from === "hide-all") {
|
|
6283
|
+
if (path.components?.length || path.shape !== void 0 || path.subpaths !== void 0) throw new LayerOperationError(`A ${from} vector mask starts empty; use from path to give a path.`);
|
|
6284
|
+
vm = { components: [], ...from === "reveal-all" ? { inverted: true } : {} };
|
|
6285
|
+
} else {
|
|
6286
|
+
throw new LayerOperationError(`Unknown vector mask source "${String(from)}". Use reveal-all, hide-all or path.`);
|
|
6287
|
+
}
|
|
6288
|
+
this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: vm })));
|
|
6289
|
+
return vm;
|
|
6290
|
+
}
|
|
6291
|
+
/** Replaces a vector mask's path (its properties are kept). One undo step. Returns the vector mask. */
|
|
6292
|
+
setVectorMaskPath(id, path) {
|
|
6293
|
+
const { vm } = this.requireVectorMask(id);
|
|
6294
|
+
const next = { ...vm, components: this.vectorMaskComponents(path) };
|
|
6295
|
+
if (path.fillRule === "evenodd") next.fillRule = "evenodd";
|
|
6296
|
+
else if (path.fillRule === "nonzero") delete next.fillRule;
|
|
6297
|
+
this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: next })));
|
|
6298
|
+
return next;
|
|
6299
|
+
}
|
|
6300
|
+
/** Changes a vector mask's enabled, linked, inverted, density or feather (the path is kept). One undo step. */
|
|
6301
|
+
setVectorMaskProps(id, props) {
|
|
6302
|
+
const { vm } = this.requireVectorMask(id);
|
|
6303
|
+
if (props.density !== void 0 && !(props.density >= 0 && props.density <= 1)) throw new LayerOperationError("density must be from 0 to 1.");
|
|
6304
|
+
if (props.feather !== void 0 && !(props.feather >= 0 && props.feather <= 1e3)) throw new LayerOperationError("feather must be from 0 to 1000 px.");
|
|
6305
|
+
const next = { ...vm };
|
|
6306
|
+
for (const k of ["enabled", "linked", "inverted"]) {
|
|
6307
|
+
if (props[k] === void 0) continue;
|
|
6308
|
+
if (typeof props[k] !== "boolean") throw new LayerOperationError(`${k} must be true or false.`);
|
|
6309
|
+
if (k === "inverted" ? props[k] : !props[k]) next[k] = k === "inverted";
|
|
6310
|
+
else delete next[k];
|
|
6311
|
+
}
|
|
6312
|
+
if (props.density !== void 0) {
|
|
6313
|
+
if (props.density === 1) delete next.density;
|
|
6314
|
+
else next.density = props.density;
|
|
6315
|
+
}
|
|
6316
|
+
if (props.feather !== void 0) {
|
|
6317
|
+
if (props.feather === 0) delete next.feather;
|
|
6318
|
+
else next.feather = props.feather;
|
|
6319
|
+
}
|
|
6320
|
+
this.commitLayers(mapLayer(this.state.layers, id, (l) => ({ ...l, vectorMask: next })));
|
|
6321
|
+
return next;
|
|
6322
|
+
}
|
|
6323
|
+
/** Deletes a layer's vector mask (one undo step). */
|
|
6324
|
+
removeVectorMask(id) {
|
|
6325
|
+
this.requireVectorMask(id);
|
|
6326
|
+
this.commitLayers(
|
|
6327
|
+
mapLayer(this.state.layers, id, (l) => {
|
|
6328
|
+
const next = { ...l };
|
|
6329
|
+
delete next.vectorMask;
|
|
6330
|
+
return next;
|
|
6331
|
+
})
|
|
6332
|
+
);
|
|
6333
|
+
}
|
|
6334
|
+
/**
|
|
6335
|
+
* Layer › Rasterize › Vector Mask: multiplies the path (with its density and feather) into the
|
|
6336
|
+
* layer's raster mask, adding one when there is none, and removes the vector mask. The raster
|
|
6337
|
+
* mask keeps its own properties. One undo step. Returns the raster mask.
|
|
6338
|
+
*/
|
|
6339
|
+
rasterizeVectorMask(id) {
|
|
6340
|
+
const { layer } = this.requireVectorMask(id);
|
|
6341
|
+
const { width, height } = this.canvasSize;
|
|
6342
|
+
const raster = layer.mask;
|
|
6343
|
+
const plain3 = { ...layer, vectorMask: { ...layer.vectorMask, enabled: true }, mask: raster ? { surface: raster.surface, defaultValue: raster.defaultValue } : void 0 };
|
|
6344
|
+
const baked = effectiveLayerMask(plain3, width, height);
|
|
6345
|
+
const mask = raster ? { ...raster, surface: baked.surface, defaultValue: baked.defaultValue } : baked;
|
|
6346
|
+
this.commitLayers(
|
|
6347
|
+
mapLayer(this.state.layers, id, (l) => {
|
|
6348
|
+
const next = { ...l, mask };
|
|
6349
|
+
delete next.vectorMask;
|
|
6350
|
+
return next;
|
|
6351
|
+
})
|
|
6352
|
+
);
|
|
6353
|
+
return mask;
|
|
6354
|
+
}
|
|
6355
|
+
/** Load Selection from a vector mask's path (density and feather are not applied), inverted with `invert`. */
|
|
6356
|
+
selectionFromVectorMask(id, mode = "replace", invert2 = false) {
|
|
6357
|
+
const { vm } = this.requireVectorMask(id);
|
|
6358
|
+
const { width, height } = this.naturalSize;
|
|
6359
|
+
const cov = componentsCoverage(vm.components, { x: 0, y: 0, width, height }, vm.fillRule ?? "nonzero", vm.inverted === true !== invert2);
|
|
6360
|
+
const bytes = new Uint8Array(cov.length);
|
|
6361
|
+
for (let i = 0; i < cov.length; i++) bytes[i] = Math.round(Math.min(1, Math.max(0, cov[i])) * 255);
|
|
6362
|
+
return this.setSelection(createSelectionMask(width, height, bytes), mode);
|
|
6363
|
+
}
|
|
6364
|
+
/** A layer's vector mask with its paths (rounded), or `null` without one. */
|
|
6365
|
+
getVectorMask(id) {
|
|
6366
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
6367
|
+
const vm = layer.vectorMask;
|
|
6368
|
+
if (!vm) return null;
|
|
6369
|
+
const b = vectorMaskBounds(vm);
|
|
6370
|
+
const r = (n) => Math.round(n * 1e3) / 1e3;
|
|
6371
|
+
return {
|
|
6372
|
+
layerId: id,
|
|
6373
|
+
enabled: vm.enabled !== false,
|
|
6374
|
+
linked: vm.linked !== false,
|
|
6375
|
+
inverted: vm.inverted === true,
|
|
6376
|
+
fillRule: vm.fillRule ?? "nonzero",
|
|
6377
|
+
density: vm.density ?? 1,
|
|
6378
|
+
feather: vm.feather ?? 0,
|
|
6379
|
+
bounds: b ? { x: r(b.x), y: r(b.y), width: r(b.width), height: r(b.height) } : null,
|
|
6380
|
+
components: vm.components.map((c, index) => ({ index, op: c.op, live: c.live ? JSON.parse(JSON.stringify(c.live)) : null, subpaths: roundSubpaths(c.subpaths) }))
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6176
6383
|
/** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
|
|
6177
6384
|
setLayerMaskProps(id, props, commitToHistory = true) {
|
|
6178
6385
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
@@ -8351,6 +8558,41 @@ var ImageEditorController = class {
|
|
|
8351
8558
|
this.state.quickMask = display === "quickMask";
|
|
8352
8559
|
this.notify();
|
|
8353
8560
|
}
|
|
8561
|
+
// --- Mask views ---
|
|
8562
|
+
/** The layer mask shown on the canvas, or `null` (see `setMaskView`). */
|
|
8563
|
+
getMaskView() {
|
|
8564
|
+
const view = this.state.maskView;
|
|
8565
|
+
if (!view) return null;
|
|
8566
|
+
const layer = this.getLayer(view.layerId);
|
|
8567
|
+
return layer?.mask && !layer.mask.source ? view : null;
|
|
8568
|
+
}
|
|
8569
|
+
/**
|
|
8570
|
+
* Shows a layer's mask on the canvas: `gray` alone in grey (Alt-click the mask thumbnail),
|
|
8571
|
+
* `overlay` as a red tint over what it hides (the \ key), `off` back to the image. The layer
|
|
8572
|
+
* defaults to the active one. View state: no undo step, not saved. Throws when the layer has
|
|
8573
|
+
* no painted mask.
|
|
8574
|
+
*/
|
|
8575
|
+
setMaskView(mode, layerId) {
|
|
8576
|
+
if (mode !== "off" && mode !== "gray" && mode !== "overlay") throw new Error(`Unknown mask view "${String(mode)}". Use gray, overlay or off.`);
|
|
8577
|
+
if (mode === "off") {
|
|
8578
|
+
if (this.state.maskView) {
|
|
8579
|
+
this.state.maskView = null;
|
|
8580
|
+
this.notify();
|
|
8581
|
+
}
|
|
8582
|
+
return null;
|
|
8583
|
+
}
|
|
8584
|
+
const id = layerId ?? this.state.activeLayerId;
|
|
8585
|
+
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
|
|
8586
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
8587
|
+
if (!layer.mask) throw new LayerOperationError(`Layer "${layer.name || id}" has no mask to show; add one with add_layer_mask.`);
|
|
8588
|
+
if (layer.mask.source) throw new LayerOperationError(`Layer "${layer.name || id}" has a depth mask, which is computed, not painted, so it has no mask view.`);
|
|
8589
|
+
const cur = this.state.maskView;
|
|
8590
|
+
if (cur?.layerId !== id || cur.mode !== mode) {
|
|
8591
|
+
this.state.maskView = { layerId: id, mode };
|
|
8592
|
+
this.notify();
|
|
8593
|
+
}
|
|
8594
|
+
return this.state.maskView;
|
|
8595
|
+
}
|
|
8354
8596
|
// --- Alpha channels ---
|
|
8355
8597
|
/** The alpha channels (saved selections), in panel order. */
|
|
8356
8598
|
getChannels() {
|
|
@@ -11578,11 +11820,12 @@ function createImageSelectionTools(controller) {
|
|
|
11578
11820
|
const loadFromMask = {
|
|
11579
11821
|
name: "load_selection_from_mask",
|
|
11580
11822
|
title: "Load a selection from a layer mask",
|
|
11581
|
-
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
|
|
11823
|
+
description: "Load Selection from a layer's mask (Ctrl/\u2318-click its mask thumbnail): the raster mask's pixels, or with source vector the vector mask's path, 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 such mask. One undoable step. Returns the selection.",
|
|
11582
11824
|
inputSchema: {
|
|
11583
11825
|
type: "object",
|
|
11584
11826
|
properties: {
|
|
11585
11827
|
layerId: { type: "string", minLength: 1, description: "The layer whose mask to load (see list_layers)." },
|
|
11828
|
+
source: { type: "string", enum: ["pixels", "vector"], description: "pixels: the raster (layer) mask (default); vector: the vector mask's path." },
|
|
11586
11829
|
invert: { type: "boolean", description: "Select what the mask hides instead (default false)." },
|
|
11587
11830
|
mode: modeSchema
|
|
11588
11831
|
},
|
|
@@ -11594,6 +11837,11 @@ function createImageSelectionTools(controller) {
|
|
|
11594
11837
|
const id = String(args.layerId ?? "");
|
|
11595
11838
|
const layer = controller.getLayer(id);
|
|
11596
11839
|
if (!layer) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
11840
|
+
if (args.source === "vector") {
|
|
11841
|
+
if (!layer.vectorMask) throw new Error(`Layer "${id}" has no vector mask. Add one with add_vector_mask, or load its pixels mask (source pixels).`);
|
|
11842
|
+
controller.selectionFromVectorMask(id, parseSelectionMode(args.mode), args.invert === true);
|
|
11843
|
+
return summary(controller);
|
|
11844
|
+
}
|
|
11597
11845
|
if (!layer.mask) throw new Error(`Layer "${id}" has no mask. Add one with add_layer_mask, or pick a layer with a mask.`);
|
|
11598
11846
|
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.`);
|
|
11599
11847
|
controller.selectionFromLayerMask(id, parseSelectionMode(args.mode), args.invert === true);
|
|
@@ -12087,6 +12335,22 @@ function selectionShade(selection, doc) {
|
|
|
12087
12335
|
context.putImageData(pixels, 0, 0);
|
|
12088
12336
|
return canvas;
|
|
12089
12337
|
}
|
|
12338
|
+
function maskViewCanvas(controller, doc) {
|
|
12339
|
+
const view = controller.getMaskView();
|
|
12340
|
+
const layer = view ? controller.getLayer(view.layerId) : null;
|
|
12341
|
+
const { width, height } = controller.getState().imageDimensions;
|
|
12342
|
+
const mask = layer ? layerMaskSelection(layer, Math.round(width), Math.round(height)) : null;
|
|
12343
|
+
if (!view || !mask) return null;
|
|
12344
|
+
const canvas = doc.createElement("canvas");
|
|
12345
|
+
canvas.width = mask.width;
|
|
12346
|
+
canvas.height = mask.height;
|
|
12347
|
+
const context = canvas.getContext("2d");
|
|
12348
|
+
if (!context) return null;
|
|
12349
|
+
const pixels = context.createImageData(mask.width, mask.height);
|
|
12350
|
+
pixels.data.set(view.mode === "gray" ? selectionToPixels(mask, "gray").data : quickMaskPixels(mask));
|
|
12351
|
+
context.putImageData(pixels, 0, 0);
|
|
12352
|
+
return canvas;
|
|
12353
|
+
}
|
|
12090
12354
|
async function renderImageRegion(controller, request) {
|
|
12091
12355
|
const state = controller.getState();
|
|
12092
12356
|
const k = Math.min(1, request.scale);
|
|
@@ -12109,6 +12373,8 @@ async function renderImageRegion(controller, request) {
|
|
|
12109
12373
|
context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);
|
|
12110
12374
|
if (request.includeOverlays) {
|
|
12111
12375
|
const s = request.scale;
|
|
12376
|
+
const maskView = maskViewCanvas(controller, document);
|
|
12377
|
+
if (maskView) context.drawImage(maskView, rect.x, rect.y, rect.width, rect.height, 0, 0, out.width, out.height);
|
|
12112
12378
|
const selection = state.selection;
|
|
12113
12379
|
if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {
|
|
12114
12380
|
const shade = selectionShade(selection, document);
|
|
@@ -12136,7 +12402,7 @@ function createImageCaptureSurface(controller) {
|
|
|
12136
12402
|
units: "image pixels",
|
|
12137
12403
|
content: whole,
|
|
12138
12404
|
selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),
|
|
12139
|
-
overlays: "a shade over unselected pixels
|
|
12405
|
+
overlays: "a shade over unselected pixels, the crop box outline and a layer mask view (set_mask_view)",
|
|
12140
12406
|
maxScale: 8,
|
|
12141
12407
|
render: (request) => renderImageRegion(controller, request)
|
|
12142
12408
|
};
|
|
@@ -12442,6 +12708,7 @@ function describeLayer(layer, activeId, boundsOf) {
|
|
|
12442
12708
|
clipped: Boolean(layer.clipped),
|
|
12443
12709
|
locks: layer.locks ? { ...layer.locks } : {},
|
|
12444
12710
|
mask: maskSummary(layer.mask),
|
|
12711
|
+
vectorMask: layer.vectorMask ? { enabled: layer.vectorMask.enabled !== false, linked: layer.vectorMask.linked !== false, inverted: layer.vectorMask.inverted === true, components: layer.vectorMask.components.length, bounds: vectorMaskBounds(layer.vectorMask) } : null,
|
|
12445
12712
|
effects: layer.effects ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) } : null,
|
|
12446
12713
|
active: layer.id === activeId
|
|
12447
12714
|
};
|
|
@@ -12824,6 +13091,27 @@ function createImageLayerTools(controller) {
|
|
|
12824
13091
|
return { layer: describe(id) };
|
|
12825
13092
|
}
|
|
12826
13093
|
};
|
|
13094
|
+
const maskView = {
|
|
13095
|
+
name: "set_mask_view",
|
|
13096
|
+
title: "Show a layer mask",
|
|
13097
|
+
description: "Shows a layer's mask on the canvas to check it: gray (the mask alone, white = revealed, as Alt-clicking its thumbnail does), overlay (a red tint over what it hides, as the backslash key does) or off (the image again). It changes only the view: no undo step, nothing saved, and render_preview and exports still show the image. Look with capture_view includeOverlays true, then turn it off. Fails when the layer has no painted mask. Returns the view.",
|
|
13098
|
+
inputSchema: {
|
|
13099
|
+
type: "object",
|
|
13100
|
+
properties: {
|
|
13101
|
+
view: { type: "string", enum: ["gray", "overlay", "off"], description: "gray, overlay or off." },
|
|
13102
|
+
layerId: { ...LAYER_ID2, description: "The layer whose mask to show (default the active layer; see list_layers). Ignored for off." }
|
|
13103
|
+
},
|
|
13104
|
+
required: ["view"],
|
|
13105
|
+
additionalProperties: false
|
|
13106
|
+
},
|
|
13107
|
+
annotations: { title: "Show a layer mask", ...WRITE_IDEMPOTENT2 },
|
|
13108
|
+
handler: ({ view, layerId }) => {
|
|
13109
|
+
if (view !== "gray" && view !== "overlay" && view !== "off") throw new Error(`Unknown view "${String(view)}". Use gray, overlay or off.`);
|
|
13110
|
+
const id = view === "off" || layerId === void 0 ? void 0 : exists(layerId);
|
|
13111
|
+
const shown = controller.setMaskView(view, id);
|
|
13112
|
+
return { view: shown?.mode ?? "off", layerId: shown?.layerId ?? null };
|
|
13113
|
+
}
|
|
13114
|
+
};
|
|
12827
13115
|
const maskProps = {
|
|
12828
13116
|
name: "set_layer_mask_properties",
|
|
12829
13117
|
title: "Set layer mask properties",
|
|
@@ -12964,6 +13252,7 @@ function createImageLayerTools(controller) {
|
|
|
12964
13252
|
addMask,
|
|
12965
13253
|
applyMask,
|
|
12966
13254
|
removeMask,
|
|
13255
|
+
maskView,
|
|
12967
13256
|
maskProps,
|
|
12968
13257
|
viaCopy,
|
|
12969
13258
|
rasterize,
|
|
@@ -13304,171 +13593,422 @@ function createImageEffectTools(controller) {
|
|
|
13304
13593
|
return [list, add2, update, remove, setEnabled, light];
|
|
13305
13594
|
}
|
|
13306
13595
|
|
|
13307
|
-
// src/image-editor/
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
|
|
13314
|
-
{
|
|
13315
|
-
];
|
|
13316
|
-
var MIXER_OPTION_CONTROLS = [
|
|
13317
|
-
{ id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13318
|
-
{ id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13319
|
-
{ id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
|
|
13320
|
-
];
|
|
13321
|
-
var MIXER_MIXING_OPTIONS = [
|
|
13322
|
-
{ id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
|
|
13323
|
-
{ id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
|
|
13324
|
-
];
|
|
13325
|
-
var PAINT_TOOLS = [
|
|
13326
|
-
{ id: "move", label: "Move", shortcut: "V" },
|
|
13327
|
-
{ id: "brush", label: "Brush", shortcut: "B" },
|
|
13328
|
-
{ id: "mixer", label: "Mixer", shortcut: "Shift+B" },
|
|
13329
|
-
{ id: "eraser", label: "Eraser", shortcut: "E" },
|
|
13330
|
-
{ id: "clone", label: "Clone", shortcut: "S" },
|
|
13331
|
-
{ id: "heal", label: "Heal", shortcut: "Shift+J" },
|
|
13332
|
-
{ id: "spot-heal", label: "Spot Heal", shortcut: "J" },
|
|
13333
|
-
{ id: "smudge", label: "Smudge", shortcut: "" },
|
|
13334
|
-
{ id: "dodge", label: "Dodge", shortcut: "O" },
|
|
13335
|
-
{ id: "burn", label: "Burn", shortcut: "Shift+O" },
|
|
13336
|
-
{ id: "sponge", label: "Sponge", shortcut: "Shift+O" },
|
|
13337
|
-
{ id: "blur", label: "Blur", shortcut: "" },
|
|
13338
|
-
{ id: "sharpen", label: "Sharpen", shortcut: "" },
|
|
13339
|
-
{ id: "history", label: "History", shortcut: "Y" }
|
|
13340
|
-
];
|
|
13341
|
-
var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
|
|
13342
|
-
var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge" || tool === "blur" || tool === "sharpen" || tool === "history";
|
|
13343
|
-
var RETOUCH_OPTION_CONTROLS = [
|
|
13344
|
-
{ id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
|
|
13345
|
-
{ id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["smudge", "blur", "sharpen"] },
|
|
13346
|
-
{ id: "historySteps", label: "Steps back", section: "layer", min: 0, max: 50, step: 1, default: 0, decimals: 0, track: plain2, unit: "", tools: ["history"] }
|
|
13347
|
-
];
|
|
13348
|
-
var CLONE_TRANSFORM_CONTROLS = [
|
|
13349
|
-
{ id: "width", label: "Source W", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13350
|
-
{ id: "height", label: "Source H", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13351
|
-
{ id: "angle", label: "Source angle", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: plain2, unit: "\xB0" }
|
|
13352
|
-
];
|
|
13353
|
-
function cloneTransformView(controller) {
|
|
13354
|
-
const t = controller.getState().paint.cloneTransform;
|
|
13355
|
-
return {
|
|
13356
|
-
width: Math.round(Math.abs(t.scaleX) * 100),
|
|
13357
|
-
height: Math.round(Math.abs(t.scaleY) * 100),
|
|
13358
|
-
angle: t.rotate,
|
|
13359
|
-
flipH: t.scaleX < 0,
|
|
13360
|
-
flipV: t.scaleY < 0,
|
|
13361
|
-
identity: t.scaleX === 1 && t.scaleY === 1 && t.rotate === 0
|
|
13362
|
-
};
|
|
13363
|
-
}
|
|
13364
|
-
function setCloneTransformValue(controller, id, value) {
|
|
13365
|
-
const t = controller.getState().paint.cloneTransform;
|
|
13366
|
-
if (id === "width") controller.setCloneTransform({ scaleX: Math.sign(t.scaleX) * (value ?? 100) / 100 });
|
|
13367
|
-
else if (id === "height") controller.setCloneTransform({ scaleY: Math.sign(t.scaleY) * (value ?? 100) / 100 });
|
|
13368
|
-
else if (id === "angle") controller.setCloneTransform({ rotate: value ?? 0 });
|
|
13369
|
-
else if (id === "flipH") controller.setCloneTransform({ scaleX: -t.scaleX });
|
|
13370
|
-
else controller.setCloneTransform({ scaleY: -t.scaleY });
|
|
13596
|
+
// src/image-editor/selection/gesture.ts
|
|
13597
|
+
function snapTo45(from, point) {
|
|
13598
|
+
const dx = point.x - from.x;
|
|
13599
|
+
const dy = point.y - from.y;
|
|
13600
|
+
const len = Math.hypot(dx, dy);
|
|
13601
|
+
if (len === 0) return point;
|
|
13602
|
+
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
13603
|
+
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
13371
13604
|
}
|
|
13372
|
-
var
|
|
13373
|
-
{
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
tools: ["clone", "heal", "spot-heal"],
|
|
13377
|
-
options: [
|
|
13378
|
-
{ id: "current", label: "Current", title: "Sample the layer being retouched" },
|
|
13379
|
-
{ id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
|
|
13380
|
-
{ id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
|
|
13381
|
-
]
|
|
13382
|
-
},
|
|
13383
|
-
{
|
|
13384
|
-
id: "spotType",
|
|
13385
|
-
label: "Type",
|
|
13386
|
-
tools: ["spot-heal"],
|
|
13387
|
-
options: [
|
|
13388
|
-
{ id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
|
|
13389
|
-
{ id: "content-aware", label: "Content-Aware", title: "Synthesise texture and edges from patches around the spot" },
|
|
13390
|
-
{ id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
|
|
13391
|
-
]
|
|
13392
|
-
},
|
|
13393
|
-
{
|
|
13394
|
-
id: "range",
|
|
13395
|
-
label: "Range",
|
|
13396
|
-
tools: ["dodge", "burn"],
|
|
13397
|
-
options: [
|
|
13398
|
-
{ id: "shadows", label: "Shadows", title: "Change the dark tones most" },
|
|
13399
|
-
{ id: "midtones", label: "Midtones", title: "Change the middle tones most" },
|
|
13400
|
-
{ id: "highlights", label: "Highlights", title: "Change the light tones most" }
|
|
13401
|
-
]
|
|
13402
|
-
},
|
|
13403
|
-
{
|
|
13404
|
-
id: "spongeMode",
|
|
13405
|
-
label: "Mode",
|
|
13406
|
-
tools: ["sponge"],
|
|
13407
|
-
options: [
|
|
13408
|
-
{ id: "desaturate", label: "Desaturate", title: "Take colour out" },
|
|
13409
|
-
{ id: "saturate", label: "Saturate", title: "Add colour" }
|
|
13410
|
-
]
|
|
13605
|
+
var SelectionGesture = class {
|
|
13606
|
+
constructor(controller, getOptions) {
|
|
13607
|
+
this.controller = controller;
|
|
13608
|
+
this.getOptions = getOptions;
|
|
13411
13609
|
}
|
|
13412
|
-
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
|
|
13428
|
-
|
|
13429
|
-
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
|
|
13433
|
-
|
|
13434
|
-
|
|
13435
|
-
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
|
|
13439
|
-
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13610
|
+
controller;
|
|
13611
|
+
getOptions;
|
|
13612
|
+
start = null;
|
|
13613
|
+
points = [];
|
|
13614
|
+
current = null;
|
|
13615
|
+
modifiers = {};
|
|
13616
|
+
/** Polygonal lasso corners while one is being drawn. */
|
|
13617
|
+
polygon = null;
|
|
13618
|
+
pressedAt = null;
|
|
13619
|
+
freehand = false;
|
|
13620
|
+
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
13621
|
+
closeRadius = 8;
|
|
13622
|
+
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
13623
|
+
error = null;
|
|
13624
|
+
get active() {
|
|
13625
|
+
return this.start !== null || this.polygon !== null;
|
|
13626
|
+
}
|
|
13627
|
+
/** The outline of the shape being drawn, or `null`. */
|
|
13628
|
+
get draftPath() {
|
|
13629
|
+
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
13630
|
+
if (!this.start || !this.current) return null;
|
|
13631
|
+
const { tool } = this.getOptions();
|
|
13632
|
+
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
13633
|
+
const rect = normalizeRect({
|
|
13634
|
+
x: this.start.x,
|
|
13635
|
+
y: this.start.y,
|
|
13636
|
+
width: this.current.x - this.start.x,
|
|
13637
|
+
height: this.current.y - this.start.y
|
|
13638
|
+
});
|
|
13639
|
+
if (rect.width < 1 && rect.height < 1) return null;
|
|
13640
|
+
if (tool === "ellipse") return ellipsePoints(rect);
|
|
13641
|
+
return [
|
|
13642
|
+
{ x: rect.x, y: rect.y },
|
|
13643
|
+
{ x: rect.x + rect.width, y: rect.y },
|
|
13644
|
+
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
13645
|
+
{ x: rect.x, y: rect.y + rect.height }
|
|
13646
|
+
];
|
|
13647
|
+
}
|
|
13648
|
+
/** Pointer down. Resolves once a wand click has been applied. */
|
|
13649
|
+
async begin(point, modifiers = {}) {
|
|
13650
|
+
const options = this.getOptions();
|
|
13651
|
+
this.error = null;
|
|
13652
|
+
if (this.polygon && options.tool !== "polygon") this.cancel();
|
|
13653
|
+
if (options.tool === "polygon") {
|
|
13654
|
+
this.pressedAt = point;
|
|
13655
|
+
this.freehand = false;
|
|
13656
|
+
if (!this.polygon) {
|
|
13657
|
+
this.modifiers = modifiers;
|
|
13658
|
+
this.polygon = [point];
|
|
13659
|
+
this.current = point;
|
|
13660
|
+
return;
|
|
13661
|
+
}
|
|
13662
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
13663
|
+
const p = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
13664
|
+
const first = this.polygon[0];
|
|
13665
|
+
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
13666
|
+
this.close();
|
|
13667
|
+
return;
|
|
13668
|
+
}
|
|
13669
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
13670
|
+
this.current = p;
|
|
13671
|
+
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
13672
|
+
return;
|
|
13673
|
+
}
|
|
13674
|
+
this.modifiers = modifiers;
|
|
13675
|
+
if (options.tool === "row" || options.tool === "column") {
|
|
13676
|
+
this.start = null;
|
|
13677
|
+
try {
|
|
13678
|
+
this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
|
|
13679
|
+
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
13680
|
+
feather: options.feather ?? 0
|
|
13681
|
+
});
|
|
13682
|
+
} catch (err) {
|
|
13683
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
13684
|
+
}
|
|
13685
|
+
return;
|
|
13686
|
+
}
|
|
13687
|
+
if (options.tool === "wand" || options.tool === "range") {
|
|
13688
|
+
this.start = null;
|
|
13689
|
+
const common = {
|
|
13690
|
+
sampleMerged: options.sampleMerged,
|
|
13691
|
+
sample: options.sample,
|
|
13692
|
+
feather: options.feather ?? 0,
|
|
13693
|
+
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
13694
|
+
};
|
|
13695
|
+
try {
|
|
13696
|
+
if (options.tool === "range") {
|
|
13697
|
+
const pixels = await this.controller.getSamplePixels(common);
|
|
13698
|
+
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
13699
|
+
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
|
|
13700
|
+
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
|
|
13701
|
+
const p = (y * pixels.width + x) * 4;
|
|
13702
|
+
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
13703
|
+
await this.controller.selectColorRange([{ color, at: point }], {
|
|
13704
|
+
...common,
|
|
13705
|
+
fuzziness: options.fuzziness ?? 40,
|
|
13706
|
+
localized: options.localized ? options.localized : void 0,
|
|
13707
|
+
invert: options.rangeInvert ?? false
|
|
13708
|
+
});
|
|
13709
|
+
return;
|
|
13710
|
+
}
|
|
13711
|
+
await this.controller.magicWand(point, {
|
|
13712
|
+
...common,
|
|
13713
|
+
tolerance: options.tolerance,
|
|
13714
|
+
contiguous: options.contiguous,
|
|
13715
|
+
antiAlias: options.antiAlias,
|
|
13716
|
+
distance: options.distance
|
|
13717
|
+
});
|
|
13718
|
+
} catch (err) {
|
|
13719
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
13720
|
+
}
|
|
13721
|
+
return;
|
|
13722
|
+
}
|
|
13723
|
+
this.start = point;
|
|
13724
|
+
this.current = point;
|
|
13725
|
+
this.points = [point];
|
|
13726
|
+
}
|
|
13727
|
+
/** Pointer move; returns true when the draft changed (re-render). */
|
|
13728
|
+
move(point, modifiers = {}) {
|
|
13729
|
+
if (this.polygon) {
|
|
13730
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
13731
|
+
if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
|
|
13732
|
+
this.freehand = true;
|
|
13733
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
|
|
13734
|
+
this.current = null;
|
|
13735
|
+
return true;
|
|
13736
|
+
}
|
|
13737
|
+
this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
13738
|
+
return true;
|
|
13739
|
+
}
|
|
13740
|
+
if (!this.start) return false;
|
|
13741
|
+
this.current = point;
|
|
13742
|
+
if (this.getOptions().tool === "lasso") {
|
|
13743
|
+
const last = this.points[this.points.length - 1];
|
|
13744
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
13745
|
+
this.points.push(point);
|
|
13746
|
+
}
|
|
13747
|
+
return true;
|
|
13748
|
+
}
|
|
13749
|
+
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
13750
|
+
end(point, modifiers) {
|
|
13751
|
+
if (this.polygon) {
|
|
13752
|
+
if (point && this.freehand) this.move(point);
|
|
13753
|
+
this.pressedAt = null;
|
|
13754
|
+
this.freehand = false;
|
|
13755
|
+
return false;
|
|
13756
|
+
}
|
|
13757
|
+
if (!this.start) return false;
|
|
13758
|
+
if (point) this.move(point);
|
|
13759
|
+
const options = this.getOptions();
|
|
13760
|
+
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
13761
|
+
const path = this.draftPath;
|
|
13762
|
+
const rect = normalizeRect({
|
|
13763
|
+
x: this.start.x,
|
|
13764
|
+
y: this.start.y,
|
|
13765
|
+
width: this.current.x - this.start.x,
|
|
13766
|
+
height: this.current.y - this.start.y
|
|
13767
|
+
});
|
|
13768
|
+
this.start = null;
|
|
13769
|
+
this.current = null;
|
|
13770
|
+
this.points = [];
|
|
13771
|
+
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
13772
|
+
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
13773
|
+
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
13774
|
+
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
13775
|
+
}
|
|
13776
|
+
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
13777
|
+
close() {
|
|
13778
|
+
const polygon = this.polygon;
|
|
13779
|
+
if (!polygon) return false;
|
|
13780
|
+
const options = this.getOptions();
|
|
13781
|
+
this.cancel();
|
|
13782
|
+
if (polygon.length < 3) return false;
|
|
13783
|
+
return this.controller.selectPolygon(polygon, {
|
|
13784
|
+
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
13785
|
+
antiAlias: options.antiAlias,
|
|
13786
|
+
feather: options.feather ?? 0
|
|
13787
|
+
});
|
|
13788
|
+
}
|
|
13789
|
+
/**
|
|
13790
|
+
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
13791
|
+
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
13792
|
+
*/
|
|
13793
|
+
key(key) {
|
|
13794
|
+
if (!this.polygon) return false;
|
|
13795
|
+
if (key === "Enter") this.close();
|
|
13796
|
+
else if (key === "Escape") this.cancel();
|
|
13797
|
+
else if (key === "Backspace" || key === "Delete") {
|
|
13798
|
+
this.polygon.pop();
|
|
13799
|
+
if (!this.polygon.length) this.cancel();
|
|
13800
|
+
} else return false;
|
|
13801
|
+
return true;
|
|
13802
|
+
}
|
|
13803
|
+
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
13804
|
+
cancel() {
|
|
13805
|
+
this.start = null;
|
|
13806
|
+
this.current = null;
|
|
13807
|
+
this.points = [];
|
|
13808
|
+
this.polygon = null;
|
|
13809
|
+
this.pressedAt = null;
|
|
13810
|
+
this.freehand = false;
|
|
13811
|
+
}
|
|
13812
|
+
};
|
|
13813
|
+
function handleSelectionShortcut(controller, e, onError) {
|
|
13814
|
+
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
13815
|
+
const key = e.key.toLowerCase();
|
|
13816
|
+
const run = (fn) => {
|
|
13817
|
+
try {
|
|
13818
|
+
fn();
|
|
13819
|
+
} catch (err) {
|
|
13820
|
+
onError?.(err instanceof Error ? err.message : String(err));
|
|
13821
|
+
}
|
|
13822
|
+
return true;
|
|
13823
|
+
};
|
|
13824
|
+
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
13825
|
+
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
13826
|
+
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
13827
|
+
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
13828
|
+
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
13829
|
+
return false;
|
|
13830
|
+
}
|
|
13831
|
+
function handleMaskViewKey(controller, e) {
|
|
13832
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key !== "\\") return false;
|
|
13833
|
+
const id = controller.getState().activeLayerId;
|
|
13834
|
+
const layer = id ? controller.getLayer(id) : null;
|
|
13835
|
+
if (!layer?.mask || layer.mask.source) return false;
|
|
13836
|
+
const view = controller.getMaskView();
|
|
13837
|
+
controller.setMaskView(view?.layerId === layer.id && view.mode === "overlay" ? "off" : "overlay", layer.id);
|
|
13838
|
+
return true;
|
|
13839
|
+
}
|
|
13840
|
+
function handleQuickMaskKey(controller, e) {
|
|
13841
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
13842
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13843
|
+
return true;
|
|
13844
|
+
}
|
|
13845
|
+
|
|
13846
|
+
// src/image-editor/layers/paint-ui.ts
|
|
13847
|
+
var plain2 = { kind: "plain" };
|
|
13848
|
+
var BRUSH_OPTION_CONTROLS = [
|
|
13849
|
+
{ id: "size", label: "Size", section: "layer", min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain2, unit: "px" },
|
|
13850
|
+
{ id: "hardness", label: "Hardness", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13851
|
+
{ id: "opacity", label: "Opacity", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13852
|
+
{ id: "flow", label: "Flow", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13853
|
+
{ id: "smoothing", label: "Smoothing", section: "layer", min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
|
|
13854
|
+
];
|
|
13855
|
+
var MIXER_OPTION_CONTROLS = [
|
|
13856
|
+
{ id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13857
|
+
{ id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13858
|
+
{ id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
|
|
13859
|
+
];
|
|
13860
|
+
var MIXER_MIXING_OPTIONS = [
|
|
13861
|
+
{ id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
|
|
13862
|
+
{ id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
|
|
13863
|
+
];
|
|
13864
|
+
var PAINT_TOOLS = [
|
|
13865
|
+
{ id: "move", label: "Move", shortcut: "V" },
|
|
13866
|
+
{ id: "brush", label: "Brush", shortcut: "B" },
|
|
13867
|
+
{ id: "mixer", label: "Mixer", shortcut: "Shift+B" },
|
|
13868
|
+
{ id: "eraser", label: "Eraser", shortcut: "E" },
|
|
13869
|
+
{ id: "clone", label: "Clone", shortcut: "S" },
|
|
13870
|
+
{ id: "heal", label: "Heal", shortcut: "Shift+J" },
|
|
13871
|
+
{ id: "spot-heal", label: "Spot Heal", shortcut: "J" },
|
|
13872
|
+
{ id: "smudge", label: "Smudge", shortcut: "" },
|
|
13873
|
+
{ id: "dodge", label: "Dodge", shortcut: "O" },
|
|
13874
|
+
{ id: "burn", label: "Burn", shortcut: "Shift+O" },
|
|
13875
|
+
{ id: "sponge", label: "Sponge", shortcut: "Shift+O" },
|
|
13876
|
+
{ id: "blur", label: "Blur", shortcut: "" },
|
|
13877
|
+
{ id: "sharpen", label: "Sharpen", shortcut: "" },
|
|
13878
|
+
{ id: "history", label: "History", shortcut: "Y" }
|
|
13879
|
+
];
|
|
13880
|
+
var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
|
|
13881
|
+
var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge" || tool === "blur" || tool === "sharpen" || tool === "history";
|
|
13882
|
+
var RETOUCH_OPTION_CONTROLS = [
|
|
13883
|
+
{ id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
|
|
13884
|
+
{ id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["smudge", "blur", "sharpen"] },
|
|
13885
|
+
{ id: "historySteps", label: "Steps back", section: "layer", min: 0, max: 50, step: 1, default: 0, decimals: 0, track: plain2, unit: "", tools: ["history"] }
|
|
13886
|
+
];
|
|
13887
|
+
var CLONE_TRANSFORM_CONTROLS = [
|
|
13888
|
+
{ id: "width", label: "Source W", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13889
|
+
{ id: "height", label: "Source H", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13890
|
+
{ id: "angle", label: "Source angle", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: plain2, unit: "\xB0" }
|
|
13891
|
+
];
|
|
13892
|
+
function cloneTransformView(controller) {
|
|
13893
|
+
const t = controller.getState().paint.cloneTransform;
|
|
13894
|
+
return {
|
|
13895
|
+
width: Math.round(Math.abs(t.scaleX) * 100),
|
|
13896
|
+
height: Math.round(Math.abs(t.scaleY) * 100),
|
|
13897
|
+
angle: t.rotate,
|
|
13898
|
+
flipH: t.scaleX < 0,
|
|
13899
|
+
flipV: t.scaleY < 0,
|
|
13900
|
+
identity: t.scaleX === 1 && t.scaleY === 1 && t.rotate === 0
|
|
13901
|
+
};
|
|
13902
|
+
}
|
|
13903
|
+
function setCloneTransformValue(controller, id, value) {
|
|
13904
|
+
const t = controller.getState().paint.cloneTransform;
|
|
13905
|
+
if (id === "width") controller.setCloneTransform({ scaleX: Math.sign(t.scaleX) * (value ?? 100) / 100 });
|
|
13906
|
+
else if (id === "height") controller.setCloneTransform({ scaleY: Math.sign(t.scaleY) * (value ?? 100) / 100 });
|
|
13907
|
+
else if (id === "angle") controller.setCloneTransform({ rotate: value ?? 0 });
|
|
13908
|
+
else if (id === "flipH") controller.setCloneTransform({ scaleX: -t.scaleX });
|
|
13909
|
+
else controller.setCloneTransform({ scaleY: -t.scaleY });
|
|
13910
|
+
}
|
|
13911
|
+
var RETOUCH_CHOICES = [
|
|
13912
|
+
{
|
|
13913
|
+
id: "sample",
|
|
13914
|
+
label: "Sample",
|
|
13915
|
+
tools: ["clone", "heal", "spot-heal"],
|
|
13916
|
+
options: [
|
|
13917
|
+
{ id: "current", label: "Current", title: "Sample the layer being retouched" },
|
|
13918
|
+
{ id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
|
|
13919
|
+
{ id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
|
|
13920
|
+
]
|
|
13921
|
+
},
|
|
13922
|
+
{
|
|
13923
|
+
id: "spotType",
|
|
13924
|
+
label: "Type",
|
|
13925
|
+
tools: ["spot-heal"],
|
|
13926
|
+
options: [
|
|
13927
|
+
{ id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
|
|
13928
|
+
{ id: "content-aware", label: "Content-Aware", title: "Synthesise texture and edges from patches around the spot" },
|
|
13929
|
+
{ id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
|
|
13930
|
+
]
|
|
13931
|
+
},
|
|
13932
|
+
{
|
|
13933
|
+
id: "range",
|
|
13934
|
+
label: "Range",
|
|
13935
|
+
tools: ["dodge", "burn"],
|
|
13936
|
+
options: [
|
|
13937
|
+
{ id: "shadows", label: "Shadows", title: "Change the dark tones most" },
|
|
13938
|
+
{ id: "midtones", label: "Midtones", title: "Change the middle tones most" },
|
|
13939
|
+
{ id: "highlights", label: "Highlights", title: "Change the light tones most" }
|
|
13940
|
+
]
|
|
13941
|
+
},
|
|
13942
|
+
{
|
|
13943
|
+
id: "spongeMode",
|
|
13944
|
+
label: "Mode",
|
|
13945
|
+
tools: ["sponge"],
|
|
13946
|
+
options: [
|
|
13947
|
+
{ id: "desaturate", label: "Desaturate", title: "Take colour out" },
|
|
13948
|
+
{ id: "saturate", label: "Saturate", title: "Add colour" }
|
|
13949
|
+
]
|
|
13950
|
+
}
|
|
13951
|
+
];
|
|
13952
|
+
var RETOUCH_TOGGLES = [
|
|
13953
|
+
{ id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
|
|
13954
|
+
{ id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
|
|
13955
|
+
{ id: "vibrance", label: "Vibrance", tools: ["sponge"] }
|
|
13956
|
+
];
|
|
13957
|
+
function setRetouchChoice(controller, id, value) {
|
|
13958
|
+
controller.setPaintOptions({ retouch: { [id]: value } });
|
|
13959
|
+
}
|
|
13960
|
+
function cloneSourceLabel(controller) {
|
|
13961
|
+
const cs = controller.getState().paint.cloneSource;
|
|
13962
|
+
return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
|
|
13963
|
+
}
|
|
13964
|
+
function rgbUnitToHex(c) {
|
|
13965
|
+
return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
13966
|
+
}
|
|
13967
|
+
function sizeStep(size) {
|
|
13968
|
+
return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
|
|
13969
|
+
}
|
|
13970
|
+
var isTyping = (t) => {
|
|
13971
|
+
const el = t;
|
|
13972
|
+
return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
|
|
13973
|
+
};
|
|
13974
|
+
function handlePaintShortcut(controller, e) {
|
|
13975
|
+
if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
|
|
13976
|
+
const brush = controller.getState().paint.brush;
|
|
13977
|
+
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
13978
|
+
const key = e.key;
|
|
13979
|
+
if (handleMaskViewKey(controller, e)) return true;
|
|
13980
|
+
switch (key.toLowerCase()) {
|
|
13981
|
+
case "q":
|
|
13982
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13983
|
+
return true;
|
|
13984
|
+
case "b":
|
|
13985
|
+
controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
|
|
13986
|
+
return true;
|
|
13987
|
+
case "e":
|
|
13988
|
+
controller.setTool("eraser");
|
|
13989
|
+
return true;
|
|
13990
|
+
case "v":
|
|
13991
|
+
controller.setTool("move");
|
|
13992
|
+
return true;
|
|
13993
|
+
case "s":
|
|
13994
|
+
controller.setTool("clone");
|
|
13995
|
+
return true;
|
|
13996
|
+
case "j":
|
|
13997
|
+
controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
|
|
13998
|
+
return true;
|
|
13999
|
+
case "o": {
|
|
14000
|
+
const cur = controller.getState().activeTool;
|
|
14001
|
+
const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
|
|
14002
|
+
controller.setTool(next);
|
|
14003
|
+
return true;
|
|
14004
|
+
}
|
|
14005
|
+
case "y":
|
|
14006
|
+
controller.setTool("history");
|
|
14007
|
+
return true;
|
|
14008
|
+
case "d":
|
|
14009
|
+
controller.resetPaintColors();
|
|
14010
|
+
return true;
|
|
14011
|
+
case "x":
|
|
13472
14012
|
controller.swapPaintColors();
|
|
13473
14013
|
return true;
|
|
13474
14014
|
}
|
|
@@ -15347,15 +15887,130 @@ function createImageShapeTools(controller) {
|
|
|
15347
15887
|
annotations: { title: "Delete a shape component", ...DESTRUCTIVE6 },
|
|
15348
15888
|
handler: ({ layerId, componentIndex }) => controller.removeShapeComponent(shapeId(layerId), componentIndex)
|
|
15349
15889
|
};
|
|
15350
|
-
const merge = {
|
|
15351
|
-
name: "merge_shape_components",
|
|
15352
|
-
title: "Merge shape components",
|
|
15353
|
-
description: "Merge Shape Components: the area a shape\u2019s components combine to (after their path operations) becomes one plain component; live shapes become paths. The look is unchanged. One undo step. Returns the shape.",
|
|
15354
|
-
inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, required: ["layerId"], additionalProperties: false },
|
|
15355
|
-
annotations: { title: "Merge shape components", ...DESTRUCTIVE6 },
|
|
15356
|
-
handler: ({ layerId }) => controller.mergeShapeComponents(shapeId(layerId))
|
|
15890
|
+
const merge = {
|
|
15891
|
+
name: "merge_shape_components",
|
|
15892
|
+
title: "Merge shape components",
|
|
15893
|
+
description: "Merge Shape Components: the area a shape\u2019s components combine to (after their path operations) becomes one plain component; live shapes become paths. The look is unchanged. One undo step. Returns the shape.",
|
|
15894
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, required: ["layerId"], additionalProperties: false },
|
|
15895
|
+
annotations: { title: "Merge shape components", ...DESTRUCTIVE6 },
|
|
15896
|
+
handler: ({ layerId }) => controller.mergeShapeComponents(shapeId(layerId))
|
|
15897
|
+
};
|
|
15898
|
+
const layerArg = (v) => {
|
|
15899
|
+
const id = String(v ?? "");
|
|
15900
|
+
if (!controller.getLayer(id)) throw new Error(`No layer "${id}". Call list_layers for ids.`);
|
|
15901
|
+
return id;
|
|
15902
|
+
};
|
|
15903
|
+
const pathSpec = (args) => {
|
|
15904
|
+
const spec = { ...geometry(args) };
|
|
15905
|
+
if (args.components !== void 0) spec.components = args.components.map(geometry);
|
|
15906
|
+
if (args.fillRule !== void 0) spec.fillRule = args.fillRule;
|
|
15907
|
+
return spec;
|
|
15908
|
+
};
|
|
15909
|
+
const PATH_PROPS = {
|
|
15910
|
+
...GEOMETRY2,
|
|
15911
|
+
components: {
|
|
15912
|
+
type: "array",
|
|
15913
|
+
minItems: 1,
|
|
15914
|
+
description: "Several components instead of shape / subpaths, bottom first, each with its path operation (combine, subtract, intersect, exclude).",
|
|
15915
|
+
items: { type: "object", description: "One component.", properties: { ...GEOMETRY2, op: OP }, additionalProperties: false }
|
|
15916
|
+
},
|
|
15917
|
+
fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." }
|
|
15918
|
+
};
|
|
15919
|
+
const LAYER = { type: "string", minLength: 1, description: "The layer (see list_layers). Not a shape layer: its path already is its vector mask." };
|
|
15920
|
+
const getVectorMask = {
|
|
15921
|
+
name: "get_vector_mask",
|
|
15922
|
+
title: "Read a vector mask",
|
|
15923
|
+
description: "Reads a layer's vector mask: enabled, linked, inverted, fill rule, density, feather, the bounds of its path and its components (op, live shape, subpaths in document pixels). Use it before set_vector_mask_path to edit the path. Returns null when the layer has none.",
|
|
15924
|
+
inputSchema: { type: "object", properties: { layerId: LAYER }, required: ["layerId"], additionalProperties: false },
|
|
15925
|
+
annotations: { title: "Read a vector mask", ...READ10 },
|
|
15926
|
+
handler: ({ layerId }) => ({ vectorMask: controller.getVectorMask(layerArg(layerId)) })
|
|
15927
|
+
};
|
|
15928
|
+
const addVectorMask = {
|
|
15929
|
+
name: "add_vector_mask",
|
|
15930
|
+
title: "Add a vector mask",
|
|
15931
|
+
description: "Layer \u203A Vector Mask: a path that reveals the layer inside it and stays sharp at any size, on top of any raster mask (both apply). from path takes a live shape, free subpaths or components like add_shape_layer; reveal-all and hide-all start with an empty path. Use it for hard-edged geometric or traced cutouts that must stay editable; use add_layer_mask for soft or painted ones. A linked vector mask moves and transforms with the layer. Replaces an existing vector mask. One undo step. Returns the vector mask.",
|
|
15932
|
+
inputSchema: {
|
|
15933
|
+
type: "object",
|
|
15934
|
+
$defs: DEFS2,
|
|
15935
|
+
properties: {
|
|
15936
|
+
layerId: LAYER,
|
|
15937
|
+
from: { type: "string", enum: ["path", "reveal-all", "hide-all"], description: "path (default when a shape, subpaths or components are given), reveal-all or hide-all (an empty path)." },
|
|
15938
|
+
...PATH_PROPS
|
|
15939
|
+
},
|
|
15940
|
+
required: ["layerId"],
|
|
15941
|
+
additionalProperties: false
|
|
15942
|
+
},
|
|
15943
|
+
annotations: { title: "Add a vector mask", ...WRITE8 },
|
|
15944
|
+
handler: (args) => {
|
|
15945
|
+
const id = layerArg(args.layerId);
|
|
15946
|
+
const spec = pathSpec(args);
|
|
15947
|
+
const hasPath = spec.components?.length || spec.shape !== void 0 || spec.subpaths !== void 0;
|
|
15948
|
+
const from = args.from ?? (hasPath ? "path" : "reveal-all");
|
|
15949
|
+
controller.addVectorMask(id, from, spec);
|
|
15950
|
+
return { vectorMask: controller.getVectorMask(id) };
|
|
15951
|
+
}
|
|
15952
|
+
};
|
|
15953
|
+
const setVectorMaskPath = {
|
|
15954
|
+
name: "set_vector_mask_path",
|
|
15955
|
+
title: "Replace a vector mask path",
|
|
15956
|
+
description: "Replaces the path of a layer's vector mask (shape, subpaths or components, document pixels); enabled, linked, inverted, density and feather are kept. Read the current path with get_vector_mask first to change part of it. One undo step. Returns the vector mask.",
|
|
15957
|
+
inputSchema: { type: "object", $defs: DEFS2, properties: { layerId: LAYER, ...PATH_PROPS }, required: ["layerId"], additionalProperties: false },
|
|
15958
|
+
annotations: { title: "Replace a vector mask path", ...WRITE_IDEMPOTENT5 },
|
|
15959
|
+
handler: (args) => {
|
|
15960
|
+
const id = layerArg(args.layerId);
|
|
15961
|
+
controller.setVectorMaskPath(id, pathSpec(args));
|
|
15962
|
+
return { vectorMask: controller.getVectorMask(id) };
|
|
15963
|
+
}
|
|
15964
|
+
};
|
|
15965
|
+
const setVectorMaskProps = {
|
|
15966
|
+
name: "set_vector_mask_properties",
|
|
15967
|
+
title: "Set vector mask properties",
|
|
15968
|
+
description: "Changes a layer's vector mask without touching its path: enabled (false ignores it), linked (false: it stays put when the layer moves or transforms), inverted (reveal outside the path), density 0..1 and feather in px (soften the edge without losing the path). One undo step. Returns the vector mask.",
|
|
15969
|
+
inputSchema: {
|
|
15970
|
+
type: "object",
|
|
15971
|
+
properties: {
|
|
15972
|
+
layerId: LAYER,
|
|
15973
|
+
enabled: { type: "boolean", description: "false ignores the vector mask but keeps it." },
|
|
15974
|
+
linked: { type: "boolean", description: "false keeps the path in place when the layer moves or transforms." },
|
|
15975
|
+
inverted: { type: "boolean", description: "true reveals outside the path instead." },
|
|
15976
|
+
density: { type: "number", minimum: 0, maximum: 1, description: "Density 0..1 (default 1): at 0.5 the hidden area still shows half." },
|
|
15977
|
+
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px (default 0)." }
|
|
15978
|
+
},
|
|
15979
|
+
required: ["layerId"],
|
|
15980
|
+
additionalProperties: false
|
|
15981
|
+
},
|
|
15982
|
+
annotations: { title: "Set vector mask properties", ...WRITE_IDEMPOTENT5 },
|
|
15983
|
+
handler: ({ layerId, ...props }) => {
|
|
15984
|
+
const id = layerArg(layerId);
|
|
15985
|
+
controller.setVectorMaskProps(id, props);
|
|
15986
|
+
return { vectorMask: controller.getVectorMask(id) };
|
|
15987
|
+
}
|
|
15988
|
+
};
|
|
15989
|
+
const rasterizeVectorMask = {
|
|
15990
|
+
name: "rasterize_vector_mask",
|
|
15991
|
+
title: "Rasterize a vector mask",
|
|
15992
|
+
description: "Layer \u203A Rasterize \u203A Vector Mask: multiplies the path (with its density and feather) into the layer's raster mask, adding one if needed, and removes the vector mask, so the mask can be painted. The path can no longer be edited (undo_image brings it back). One undo step. Returns the layer's raster mask summary.",
|
|
15993
|
+
inputSchema: { type: "object", properties: { layerId: LAYER }, required: ["layerId"], additionalProperties: false },
|
|
15994
|
+
annotations: { title: "Rasterize a vector mask", ...DESTRUCTIVE6 },
|
|
15995
|
+
handler: ({ layerId }) => {
|
|
15996
|
+
const id = layerArg(layerId);
|
|
15997
|
+
const mask = controller.rasterizeVectorMask(id);
|
|
15998
|
+
return { layerId: id, mask: { defaultValue: mask.defaultValue, density: mask.density ?? 1, feather: mask.feather ?? 0, enabled: mask.enabled !== false } };
|
|
15999
|
+
}
|
|
16000
|
+
};
|
|
16001
|
+
const removeVectorMask = {
|
|
16002
|
+
name: "remove_vector_mask",
|
|
16003
|
+
title: "Delete a vector mask",
|
|
16004
|
+
description: "Deletes a layer's vector mask without applying it (the raster mask, if any, stays). Disable it instead with set_vector_mask_properties enabled false to keep it. One undo step. Returns the layer id.",
|
|
16005
|
+
inputSchema: { type: "object", properties: { layerId: LAYER }, required: ["layerId"], additionalProperties: false },
|
|
16006
|
+
annotations: { title: "Delete a vector mask", ...DESTRUCTIVE6 },
|
|
16007
|
+
handler: ({ layerId }) => {
|
|
16008
|
+
const id = layerArg(layerId);
|
|
16009
|
+
controller.removeVectorMask(id);
|
|
16010
|
+
return { layerId: id, vectorMask: null };
|
|
16011
|
+
}
|
|
15357
16012
|
};
|
|
15358
|
-
return [get, add2, update, setComponent, addComponent, removeComponent, merge];
|
|
16013
|
+
return [get, add2, update, setComponent, addComponent, removeComponent, merge, getVectorMask, addVectorMask, setVectorMaskPath, setVectorMaskProps, rasterizeVectorMask, removeVectorMask];
|
|
15359
16014
|
}
|
|
15360
16015
|
|
|
15361
16016
|
// src/image-editor/type-tools.ts
|
|
@@ -15837,247 +16492,6 @@ function removeCurvePoint(points, index) {
|
|
|
15837
16492
|
}
|
|
15838
16493
|
var round22 = (v) => Math.round(v * 100) / 100;
|
|
15839
16494
|
|
|
15840
|
-
// src/image-editor/selection/gesture.ts
|
|
15841
|
-
function snapTo45(from, point) {
|
|
15842
|
-
const dx = point.x - from.x;
|
|
15843
|
-
const dy = point.y - from.y;
|
|
15844
|
-
const len = Math.hypot(dx, dy);
|
|
15845
|
-
if (len === 0) return point;
|
|
15846
|
-
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
15847
|
-
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
15848
|
-
}
|
|
15849
|
-
var SelectionGesture = class {
|
|
15850
|
-
constructor(controller, getOptions) {
|
|
15851
|
-
this.controller = controller;
|
|
15852
|
-
this.getOptions = getOptions;
|
|
15853
|
-
}
|
|
15854
|
-
controller;
|
|
15855
|
-
getOptions;
|
|
15856
|
-
start = null;
|
|
15857
|
-
points = [];
|
|
15858
|
-
current = null;
|
|
15859
|
-
modifiers = {};
|
|
15860
|
-
/** Polygonal lasso corners while one is being drawn. */
|
|
15861
|
-
polygon = null;
|
|
15862
|
-
pressedAt = null;
|
|
15863
|
-
freehand = false;
|
|
15864
|
-
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
15865
|
-
closeRadius = 8;
|
|
15866
|
-
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
15867
|
-
error = null;
|
|
15868
|
-
get active() {
|
|
15869
|
-
return this.start !== null || this.polygon !== null;
|
|
15870
|
-
}
|
|
15871
|
-
/** The outline of the shape being drawn, or `null`. */
|
|
15872
|
-
get draftPath() {
|
|
15873
|
-
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
15874
|
-
if (!this.start || !this.current) return null;
|
|
15875
|
-
const { tool } = this.getOptions();
|
|
15876
|
-
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
15877
|
-
const rect = normalizeRect({
|
|
15878
|
-
x: this.start.x,
|
|
15879
|
-
y: this.start.y,
|
|
15880
|
-
width: this.current.x - this.start.x,
|
|
15881
|
-
height: this.current.y - this.start.y
|
|
15882
|
-
});
|
|
15883
|
-
if (rect.width < 1 && rect.height < 1) return null;
|
|
15884
|
-
if (tool === "ellipse") return ellipsePoints(rect);
|
|
15885
|
-
return [
|
|
15886
|
-
{ x: rect.x, y: rect.y },
|
|
15887
|
-
{ x: rect.x + rect.width, y: rect.y },
|
|
15888
|
-
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
15889
|
-
{ x: rect.x, y: rect.y + rect.height }
|
|
15890
|
-
];
|
|
15891
|
-
}
|
|
15892
|
-
/** Pointer down. Resolves once a wand click has been applied. */
|
|
15893
|
-
async begin(point, modifiers = {}) {
|
|
15894
|
-
const options = this.getOptions();
|
|
15895
|
-
this.error = null;
|
|
15896
|
-
if (this.polygon && options.tool !== "polygon") this.cancel();
|
|
15897
|
-
if (options.tool === "polygon") {
|
|
15898
|
-
this.pressedAt = point;
|
|
15899
|
-
this.freehand = false;
|
|
15900
|
-
if (!this.polygon) {
|
|
15901
|
-
this.modifiers = modifiers;
|
|
15902
|
-
this.polygon = [point];
|
|
15903
|
-
this.current = point;
|
|
15904
|
-
return;
|
|
15905
|
-
}
|
|
15906
|
-
const last = this.polygon[this.polygon.length - 1];
|
|
15907
|
-
const p = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15908
|
-
const first = this.polygon[0];
|
|
15909
|
-
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
15910
|
-
this.close();
|
|
15911
|
-
return;
|
|
15912
|
-
}
|
|
15913
|
-
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
15914
|
-
this.current = p;
|
|
15915
|
-
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
15916
|
-
return;
|
|
15917
|
-
}
|
|
15918
|
-
this.modifiers = modifiers;
|
|
15919
|
-
if (options.tool === "row" || options.tool === "column") {
|
|
15920
|
-
this.start = null;
|
|
15921
|
-
try {
|
|
15922
|
-
this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
|
|
15923
|
-
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
15924
|
-
feather: options.feather ?? 0
|
|
15925
|
-
});
|
|
15926
|
-
} catch (err) {
|
|
15927
|
-
this.error = err instanceof Error ? err.message : String(err);
|
|
15928
|
-
}
|
|
15929
|
-
return;
|
|
15930
|
-
}
|
|
15931
|
-
if (options.tool === "wand" || options.tool === "range") {
|
|
15932
|
-
this.start = null;
|
|
15933
|
-
const common = {
|
|
15934
|
-
sampleMerged: options.sampleMerged,
|
|
15935
|
-
sample: options.sample,
|
|
15936
|
-
feather: options.feather ?? 0,
|
|
15937
|
-
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
15938
|
-
};
|
|
15939
|
-
try {
|
|
15940
|
-
if (options.tool === "range") {
|
|
15941
|
-
const pixels = await this.controller.getSamplePixels(common);
|
|
15942
|
-
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
15943
|
-
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
|
|
15944
|
-
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
|
|
15945
|
-
const p = (y * pixels.width + x) * 4;
|
|
15946
|
-
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
15947
|
-
await this.controller.selectColorRange([{ color, at: point }], {
|
|
15948
|
-
...common,
|
|
15949
|
-
fuzziness: options.fuzziness ?? 40,
|
|
15950
|
-
localized: options.localized ? options.localized : void 0,
|
|
15951
|
-
invert: options.rangeInvert ?? false
|
|
15952
|
-
});
|
|
15953
|
-
return;
|
|
15954
|
-
}
|
|
15955
|
-
await this.controller.magicWand(point, {
|
|
15956
|
-
...common,
|
|
15957
|
-
tolerance: options.tolerance,
|
|
15958
|
-
contiguous: options.contiguous,
|
|
15959
|
-
antiAlias: options.antiAlias,
|
|
15960
|
-
distance: options.distance
|
|
15961
|
-
});
|
|
15962
|
-
} catch (err) {
|
|
15963
|
-
this.error = err instanceof Error ? err.message : String(err);
|
|
15964
|
-
}
|
|
15965
|
-
return;
|
|
15966
|
-
}
|
|
15967
|
-
this.start = point;
|
|
15968
|
-
this.current = point;
|
|
15969
|
-
this.points = [point];
|
|
15970
|
-
}
|
|
15971
|
-
/** Pointer move; returns true when the draft changed (re-render). */
|
|
15972
|
-
move(point, modifiers = {}) {
|
|
15973
|
-
if (this.polygon) {
|
|
15974
|
-
const last = this.polygon[this.polygon.length - 1];
|
|
15975
|
-
if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
|
|
15976
|
-
this.freehand = true;
|
|
15977
|
-
if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
|
|
15978
|
-
this.current = null;
|
|
15979
|
-
return true;
|
|
15980
|
-
}
|
|
15981
|
-
this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15982
|
-
return true;
|
|
15983
|
-
}
|
|
15984
|
-
if (!this.start) return false;
|
|
15985
|
-
this.current = point;
|
|
15986
|
-
if (this.getOptions().tool === "lasso") {
|
|
15987
|
-
const last = this.points[this.points.length - 1];
|
|
15988
|
-
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
15989
|
-
this.points.push(point);
|
|
15990
|
-
}
|
|
15991
|
-
return true;
|
|
15992
|
-
}
|
|
15993
|
-
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
15994
|
-
end(point, modifiers) {
|
|
15995
|
-
if (this.polygon) {
|
|
15996
|
-
if (point && this.freehand) this.move(point);
|
|
15997
|
-
this.pressedAt = null;
|
|
15998
|
-
this.freehand = false;
|
|
15999
|
-
return false;
|
|
16000
|
-
}
|
|
16001
|
-
if (!this.start) return false;
|
|
16002
|
-
if (point) this.move(point);
|
|
16003
|
-
const options = this.getOptions();
|
|
16004
|
-
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
16005
|
-
const path = this.draftPath;
|
|
16006
|
-
const rect = normalizeRect({
|
|
16007
|
-
x: this.start.x,
|
|
16008
|
-
y: this.start.y,
|
|
16009
|
-
width: this.current.x - this.start.x,
|
|
16010
|
-
height: this.current.y - this.start.y
|
|
16011
|
-
});
|
|
16012
|
-
this.start = null;
|
|
16013
|
-
this.current = null;
|
|
16014
|
-
this.points = [];
|
|
16015
|
-
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
16016
|
-
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
16017
|
-
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
16018
|
-
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
16019
|
-
}
|
|
16020
|
-
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
16021
|
-
close() {
|
|
16022
|
-
const polygon = this.polygon;
|
|
16023
|
-
if (!polygon) return false;
|
|
16024
|
-
const options = this.getOptions();
|
|
16025
|
-
this.cancel();
|
|
16026
|
-
if (polygon.length < 3) return false;
|
|
16027
|
-
return this.controller.selectPolygon(polygon, {
|
|
16028
|
-
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
16029
|
-
antiAlias: options.antiAlias,
|
|
16030
|
-
feather: options.feather ?? 0
|
|
16031
|
-
});
|
|
16032
|
-
}
|
|
16033
|
-
/**
|
|
16034
|
-
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
16035
|
-
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
16036
|
-
*/
|
|
16037
|
-
key(key) {
|
|
16038
|
-
if (!this.polygon) return false;
|
|
16039
|
-
if (key === "Enter") this.close();
|
|
16040
|
-
else if (key === "Escape") this.cancel();
|
|
16041
|
-
else if (key === "Backspace" || key === "Delete") {
|
|
16042
|
-
this.polygon.pop();
|
|
16043
|
-
if (!this.polygon.length) this.cancel();
|
|
16044
|
-
} else return false;
|
|
16045
|
-
return true;
|
|
16046
|
-
}
|
|
16047
|
-
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
16048
|
-
cancel() {
|
|
16049
|
-
this.start = null;
|
|
16050
|
-
this.current = null;
|
|
16051
|
-
this.points = [];
|
|
16052
|
-
this.polygon = null;
|
|
16053
|
-
this.pressedAt = null;
|
|
16054
|
-
this.freehand = false;
|
|
16055
|
-
}
|
|
16056
|
-
};
|
|
16057
|
-
function handleSelectionShortcut(controller, e, onError) {
|
|
16058
|
-
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
16059
|
-
const key = e.key.toLowerCase();
|
|
16060
|
-
const run = (fn) => {
|
|
16061
|
-
try {
|
|
16062
|
-
fn();
|
|
16063
|
-
} catch (err) {
|
|
16064
|
-
onError?.(err instanceof Error ? err.message : String(err));
|
|
16065
|
-
}
|
|
16066
|
-
return true;
|
|
16067
|
-
};
|
|
16068
|
-
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
16069
|
-
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
16070
|
-
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
16071
|
-
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
16072
|
-
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
16073
|
-
return false;
|
|
16074
|
-
}
|
|
16075
|
-
function handleQuickMaskKey(controller, e) {
|
|
16076
|
-
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
16077
|
-
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
16078
|
-
return true;
|
|
16079
|
-
}
|
|
16080
|
-
|
|
16081
16495
|
// src/image-editor/selection/mask-files.ts
|
|
16082
16496
|
var MASK_FILE_ACCEPT = "image/png,image/jpeg,image/webp,image/gif,image/bmp";
|
|
16083
16497
|
function blobDataUrl(file) {
|
|
@@ -16253,12 +16667,21 @@ function maskThumbnailClick(controller, layer, e) {
|
|
|
16253
16667
|
if (!layer.mask) return;
|
|
16254
16668
|
if (e.ctrlKey || e.metaKey) {
|
|
16255
16669
|
controller.selectionFromLayerMask(layer.id, selectionModeFromModifiers(e, "replace"));
|
|
16670
|
+
} else if (e.altKey && !layer.mask.source) {
|
|
16671
|
+
const mode = e.shiftKey ? "overlay" : "gray";
|
|
16672
|
+
const view = controller.getMaskView();
|
|
16673
|
+
controller.setMaskView(view?.layerId === layer.id && view.mode === mode ? "off" : mode, layer.id);
|
|
16256
16674
|
} else if (e.shiftKey) {
|
|
16257
16675
|
controller.setLayerMaskProps(layer.id, { enabled: layer.mask.enabled === false });
|
|
16258
16676
|
} else {
|
|
16259
16677
|
controller.setActiveLayer(layer.id, "mask");
|
|
16260
16678
|
}
|
|
16261
16679
|
}
|
|
16680
|
+
function vectorMaskThumbnailClick(controller, layer, e) {
|
|
16681
|
+
if (!layer.vectorMask) return;
|
|
16682
|
+
if (e.ctrlKey || e.metaKey) controller.selectionFromVectorMask(layer.id, selectionModeFromModifiers(e, "replace"));
|
|
16683
|
+
else if (e.shiftKey) controller.setVectorMaskProps(layer.id, { enabled: layer.vectorMask.enabled === false });
|
|
16684
|
+
}
|
|
16262
16685
|
function layerThumbnailClick(controller, layer, e) {
|
|
16263
16686
|
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
16264
16687
|
controller.selectLayerTransparency(layer.id, { mode: selectionModeFromModifiers(e, "replace") });
|
|
@@ -16357,6 +16780,11 @@ function layerMenu(controller) {
|
|
|
16357
16780
|
{ action: "delete-mask", label: "Delete Layer Mask", enabled: Boolean(active?.mask) },
|
|
16358
16781
|
{ action: "toggle-mask", label: active?.mask?.enabled === false ? "Enable Layer Mask" : "Disable Layer Mask", enabled: Boolean(active?.mask) },
|
|
16359
16782
|
{ action: "toggle-mask-link", label: active?.mask?.linked === false ? "Link Layer Mask" : "Unlink Layer Mask", enabled: Boolean(active?.mask && !active.mask.source) },
|
|
16783
|
+
{ action: "add-vector-mask", label: "Add Vector Mask (Reveal All)", enabled: Boolean(active && active.type !== "shape" && !active.mask?.source) },
|
|
16784
|
+
{ action: "add-vector-mask-hide", label: "Add Vector Mask (Hide All)", enabled: Boolean(active && active.type !== "shape" && !active.mask?.source) },
|
|
16785
|
+
{ action: "delete-vector-mask", label: "Delete Vector Mask", enabled: Boolean(active?.vectorMask) },
|
|
16786
|
+
{ action: "toggle-vector-mask", label: active?.vectorMask?.enabled === false ? "Enable Vector Mask" : "Disable Vector Mask", enabled: Boolean(active?.vectorMask) },
|
|
16787
|
+
{ action: "rasterize-vector-mask", label: "Rasterize Vector Mask", enabled: Boolean(active?.vectorMask) },
|
|
16360
16788
|
{ action: "rasterize", label: "Rasterize Layer", enabled: active?.type === "annotation" || active?.type === "smart" || active?.type === "shape" || active?.type === "type" || isPixel && Boolean(active?.type === "pixel" && active.filters?.length) },
|
|
16361
16789
|
{ action: "convert-smart", label: "Convert to Smart Object", enabled: Boolean(active && active.type !== "annotation") },
|
|
16362
16790
|
{ action: "edit-contents", label: "Edit Contents", enabled: active?.type === "smart" },
|
|
@@ -16428,6 +16856,19 @@ async function runLayerMenuAction(controller, action) {
|
|
|
16428
16856
|
case "toggle-mask-link":
|
|
16429
16857
|
if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });
|
|
16430
16858
|
break;
|
|
16859
|
+
case "add-vector-mask":
|
|
16860
|
+
case "add-vector-mask-hide":
|
|
16861
|
+
if (id) controller.addVectorMask(id, action === "add-vector-mask" ? "reveal-all" : "hide-all");
|
|
16862
|
+
break;
|
|
16863
|
+
case "delete-vector-mask":
|
|
16864
|
+
if (id) controller.removeVectorMask(id);
|
|
16865
|
+
break;
|
|
16866
|
+
case "toggle-vector-mask":
|
|
16867
|
+
if (id && active?.vectorMask) controller.setVectorMaskProps(id, { enabled: active.vectorMask.enabled === false });
|
|
16868
|
+
break;
|
|
16869
|
+
case "rasterize-vector-mask":
|
|
16870
|
+
if (id) controller.rasterizeVectorMask(id);
|
|
16871
|
+
break;
|
|
16431
16872
|
case "rasterize":
|
|
16432
16873
|
if (id) await controller.rasterizeLayer(id);
|
|
16433
16874
|
break;
|
|
@@ -18020,6 +18461,7 @@ export {
|
|
|
18020
18461
|
combineSelections,
|
|
18021
18462
|
compileAdjustmentLayer,
|
|
18022
18463
|
componentPath,
|
|
18464
|
+
componentsCoverage,
|
|
18023
18465
|
compositeDeep,
|
|
18024
18466
|
compositeSurfaceOver,
|
|
18025
18467
|
computeHistogram,
|
|
@@ -18139,6 +18581,7 @@ export {
|
|
|
18139
18581
|
effectLayerTiming,
|
|
18140
18582
|
effectPixelsToSurface,
|
|
18141
18583
|
effectReach,
|
|
18584
|
+
effectiveLayerMask,
|
|
18142
18585
|
effectiveProof,
|
|
18143
18586
|
effectsFingerprint,
|
|
18144
18587
|
effectsMargin,
|
|
@@ -18175,6 +18618,7 @@ export {
|
|
|
18175
18618
|
flattenPath,
|
|
18176
18619
|
flipCorners,
|
|
18177
18620
|
flowField,
|
|
18621
|
+
followLayerMove,
|
|
18178
18622
|
fontOf,
|
|
18179
18623
|
formatControlValue,
|
|
18180
18624
|
fullSelection,
|
|
@@ -18195,6 +18639,7 @@ export {
|
|
|
18195
18639
|
growSelection,
|
|
18196
18640
|
guidePlanes,
|
|
18197
18641
|
guidedFilterColor,
|
|
18642
|
+
handleMaskViewKey,
|
|
18198
18643
|
handlePaintShortcut,
|
|
18199
18644
|
handleQuickMaskKey,
|
|
18200
18645
|
handleSelectionShortcut,
|
|
@@ -18238,6 +18683,7 @@ export {
|
|
|
18238
18683
|
invertMatrix3,
|
|
18239
18684
|
invertSelection,
|
|
18240
18685
|
isActiveFilter,
|
|
18686
|
+
isActiveVectorMask,
|
|
18241
18687
|
isAdjustmentKind,
|
|
18242
18688
|
isBitDepth,
|
|
18243
18689
|
isBlendMode,
|
|
@@ -18285,6 +18731,7 @@ export {
|
|
|
18285
18731
|
layerMaskCoverage,
|
|
18286
18732
|
layerMaskFromSelection,
|
|
18287
18733
|
layerMaskKey,
|
|
18734
|
+
layerMaskSelection,
|
|
18288
18735
|
layerMenu,
|
|
18289
18736
|
layerName,
|
|
18290
18737
|
layerPanelRows,
|
|
@@ -18376,6 +18823,7 @@ export {
|
|
|
18376
18823
|
offsetLayer,
|
|
18377
18824
|
offsetShape,
|
|
18378
18825
|
offsetSmartObject,
|
|
18826
|
+
offsetVectorMask,
|
|
18379
18827
|
openMaskFile,
|
|
18380
18828
|
openPsdFile,
|
|
18381
18829
|
packRgba,
|
|
@@ -18496,6 +18944,7 @@ export {
|
|
|
18496
18944
|
resolveMosaicRegionBounds,
|
|
18497
18945
|
resolvePaintGradient,
|
|
18498
18946
|
resolveProfile,
|
|
18947
|
+
resolveVectorMasks,
|
|
18499
18948
|
restyleRange,
|
|
18500
18949
|
retouchFilter,
|
|
18501
18950
|
reversePaintGradient,
|
|
@@ -18625,6 +19074,7 @@ export {
|
|
|
18625
19074
|
transformShapeComponents,
|
|
18626
19075
|
transformSurface,
|
|
18627
19076
|
transformTypeLayer,
|
|
19077
|
+
transformVectorMask,
|
|
18628
19078
|
transformedBounds,
|
|
18629
19079
|
transformedBox,
|
|
18630
19080
|
transformedPoint,
|
|
@@ -18651,6 +19101,10 @@ export {
|
|
|
18651
19101
|
validateEffectGradient,
|
|
18652
19102
|
validateLayerEffect,
|
|
18653
19103
|
validateLayerEffects,
|
|
19104
|
+
vectorMaskBounds,
|
|
19105
|
+
vectorMaskCoverage,
|
|
19106
|
+
vectorMaskThumbnail,
|
|
19107
|
+
vectorMaskThumbnailClick,
|
|
18654
19108
|
versionedDescriptorBytes,
|
|
18655
19109
|
vibranceLinear,
|
|
18656
19110
|
vibrancePixel,
|