@pixerate/editor 0.185.0 → 0.186.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-GYYSPMVU.js → chunk-FW7N64XK.js} +295 -188
- package/dist/{chunk-C6TRAAG4.cjs → chunk-X2KQQZBT.cjs} +355 -248
- package/dist/image-editor/controller.d.cts +56 -0
- package/dist/image-editor/controller.d.ts +56 -0
- package/dist/image-editor/index.cjs +1461 -1030
- package/dist/image-editor/index.js +447 -16
- package/dist/image-editor/layers/model.d.cts +5 -1
- package/dist/image-editor/layers/model.d.ts +5 -1
- package/dist/image-editor/layers/paint-ui.d.cts +2 -1
- package/dist/image-editor/layers/paint-ui.d.ts +2 -1
- package/dist/image-editor/layers/serialize.d.cts +3 -0
- package/dist/image-editor/layers/serialize.d.ts +3 -0
- package/dist/image-editor/psd/channels.d.cts +24 -0
- package/dist/image-editor/psd/channels.d.ts +24 -0
- package/dist/image-editor/psd/export.d.cts +3 -0
- package/dist/image-editor/psd/export.d.ts +3 -0
- package/dist/image-editor/psd/import.d.cts +10 -0
- package/dist/image-editor/psd/import.d.ts +10 -0
- package/dist/image-editor/renderer.d.cts +6 -0
- package/dist/image-editor/renderer.d.ts +6 -0
- package/dist/image-editor/selection/channels.d.cts +52 -0
- package/dist/image-editor/selection/channels.d.ts +52 -0
- package/dist/image-editor/selection/gesture.d.cts +7 -0
- package/dist/image-editor/selection/gesture.d.ts +7 -0
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/overlay.d.cts +2 -0
- package/dist/image-editor/selection/overlay.d.ts +2 -0
- package/dist/image-editor/tools.d.cts +3 -1
- package/dist/image-editor/tools.d.ts +3 -1
- package/dist/image-editor/types.d.cts +10 -0
- package/dist/image-editor/types.d.ts +10 -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 +13 -1
|
@@ -370,7 +370,7 @@ import {
|
|
|
370
370
|
writePsd,
|
|
371
371
|
xyToTempTint,
|
|
372
372
|
xyToUv
|
|
373
|
-
} from "../chunk-
|
|
373
|
+
} from "../chunk-FW7N64XK.js";
|
|
374
374
|
import {
|
|
375
375
|
parseCssColor
|
|
376
376
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -939,12 +939,13 @@ function selectionOutline(mask, threshold = 128) {
|
|
|
939
939
|
function quickMaskPixels(mask, style = {}) {
|
|
940
940
|
const [r, g, b] = style.color ?? [255, 0, 0];
|
|
941
941
|
const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));
|
|
942
|
+
const selected = style.indicates === "selected";
|
|
942
943
|
const out = new Uint8ClampedArray(mask.data.length * 4);
|
|
943
944
|
for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {
|
|
944
945
|
out[p] = r;
|
|
945
946
|
out[p + 1] = g;
|
|
946
947
|
out[p + 2] = b;
|
|
947
|
-
out[p + 3] = Math.round((255 - mask.data[i]) * k);
|
|
948
|
+
out[p + 3] = Math.round((selected ? mask.data[i] : 255 - mask.data[i]) * k);
|
|
948
949
|
}
|
|
949
950
|
return out;
|
|
950
951
|
}
|
|
@@ -1363,14 +1364,30 @@ var ImageEditorRenderer = class {
|
|
|
1363
1364
|
ctx.restore();
|
|
1364
1365
|
}
|
|
1365
1366
|
const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1366
|
-
|
|
1367
|
+
const shownChannels = (state.channels ?? []).filter((c) => c.visible);
|
|
1368
|
+
if (shownChannels.length) {
|
|
1367
1369
|
ctx.save();
|
|
1368
1370
|
try {
|
|
1369
1371
|
applyImageProjectionToContext(ctx, projection);
|
|
1370
1372
|
ctx.beginPath();
|
|
1371
1373
|
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1372
1374
|
ctx.clip();
|
|
1373
|
-
|
|
1375
|
+
for (const c of shownChannels) this.drawMaskOverlay(ctx, c.mask, { color: hexRgb(c.color), opacity: c.opacity, indicates: c.indicates });
|
|
1376
|
+
} catch {
|
|
1377
|
+
}
|
|
1378
|
+
ctx.restore();
|
|
1379
|
+
}
|
|
1380
|
+
const { width: iw, height: ih } = state.imageDimensions;
|
|
1381
|
+
const quickMaskSel = display === "quickMask" && state.quickMaskEditing && !state.selection?.bounds ? this.emptyMask(Math.round(iw), Math.round(ih)) : null;
|
|
1382
|
+
if (state.selection?.bounds && display !== "none" || quickMaskSel || options.draftSelectionPath?.length) {
|
|
1383
|
+
ctx.save();
|
|
1384
|
+
try {
|
|
1385
|
+
applyImageProjectionToContext(ctx, projection);
|
|
1386
|
+
ctx.beginPath();
|
|
1387
|
+
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1388
|
+
ctx.clip();
|
|
1389
|
+
if (quickMaskSel) this.drawQuickMask(ctx, quickMaskSel);
|
|
1390
|
+
else if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
|
|
1374
1391
|
else if (state.selection?.bounds && display === "ants") this.drawAnts(ctx, state.selection, projection.scale / u, options.antsOffset ?? 0);
|
|
1375
1392
|
if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
|
|
1376
1393
|
} catch {
|
|
@@ -1776,6 +1793,31 @@ var ImageEditorRenderer = class {
|
|
|
1776
1793
|
}
|
|
1777
1794
|
quickMaskCache = /* @__PURE__ */ new WeakMap();
|
|
1778
1795
|
outlineCache = /* @__PURE__ */ new WeakMap();
|
|
1796
|
+
emptyMaskCache = null;
|
|
1797
|
+
/** An empty selection of the image's size (Quick Mask mode with nothing selected). */
|
|
1798
|
+
emptyMask(width, height) {
|
|
1799
|
+
const m = this.emptyMaskCache;
|
|
1800
|
+
if (m && m.width === width && m.height === height) return m;
|
|
1801
|
+
this.emptyMaskCache = createSelectionMask(Math.max(1, width), Math.max(1, height));
|
|
1802
|
+
return this.emptyMaskCache;
|
|
1803
|
+
}
|
|
1804
|
+
overlayCache = /* @__PURE__ */ new WeakMap();
|
|
1805
|
+
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
1806
|
+
drawMaskOverlay(ctx, mask, style) {
|
|
1807
|
+
const key = `${style.color?.join(",")}|${style.opacity}|${style.indicates}`;
|
|
1808
|
+
let cached = this.overlayCache.get(mask);
|
|
1809
|
+
if (!cached || cached.key !== key) {
|
|
1810
|
+
const canvas = this.createCanvas(mask.width, mask.height);
|
|
1811
|
+
const mctx = canvas.getContext("2d");
|
|
1812
|
+
if (!mctx || typeof mctx.createImageData !== "function") return;
|
|
1813
|
+
const img = mctx.createImageData(mask.width, mask.height);
|
|
1814
|
+
img.data.set(quickMaskPixels(mask, style));
|
|
1815
|
+
mctx.putImageData(img, 0, 0);
|
|
1816
|
+
cached = { key, canvas };
|
|
1817
|
+
this.overlayCache.set(mask, cached);
|
|
1818
|
+
}
|
|
1819
|
+
ctx.drawImage(cached.canvas, 0, 0, mask.width, mask.height);
|
|
1820
|
+
}
|
|
1779
1821
|
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
1780
1822
|
drawQuickMask(ctx, selection) {
|
|
1781
1823
|
let canvas = this.quickMaskCache.get(selection);
|
|
@@ -2320,6 +2362,12 @@ function removeAnnotationLayers(layers) {
|
|
|
2320
2362
|
}
|
|
2321
2363
|
return out;
|
|
2322
2364
|
}
|
|
2365
|
+
function hexRgb(hex) {
|
|
2366
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex);
|
|
2367
|
+
if (!m) return [255, 0, 0];
|
|
2368
|
+
const v = parseInt(m[1], 16);
|
|
2369
|
+
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
2370
|
+
}
|
|
2323
2371
|
|
|
2324
2372
|
// src/image-editor/layers/shape-type-ui.ts
|
|
2325
2373
|
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
@@ -3479,6 +3527,57 @@ function selectionRing(sel, width, location) {
|
|
|
3479
3527
|
return createSelectionMask(sel.width, sel.height, out);
|
|
3480
3528
|
}
|
|
3481
3529
|
|
|
3530
|
+
// src/image-editor/selection/channels.ts
|
|
3531
|
+
var DEFAULT_CHANNEL_COLOR = "#ff0000";
|
|
3532
|
+
function describeChannel(channel) {
|
|
3533
|
+
const { mask, ...rest } = channel;
|
|
3534
|
+
let sum = 0;
|
|
3535
|
+
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
3536
|
+
const percent = Math.round(sum / 255 / Math.max(1, mask.data.length) * 1e4) / 100;
|
|
3537
|
+
return { ...rest, bounds: mask.bounds, percent };
|
|
3538
|
+
}
|
|
3539
|
+
function serializeChannels(channels) {
|
|
3540
|
+
return channels.map((c) => ({ ...c, mask: serializeSelection(c.mask) }));
|
|
3541
|
+
}
|
|
3542
|
+
function deserializeChannels(value, size, problems = []) {
|
|
3543
|
+
if (!Array.isArray(value)) return [];
|
|
3544
|
+
const out = [];
|
|
3545
|
+
for (const raw of value) {
|
|
3546
|
+
const v = raw;
|
|
3547
|
+
try {
|
|
3548
|
+
const mask = deserializeSelection(v.mask);
|
|
3549
|
+
if (mask.width !== size.width || mask.height !== size.height) throw new Error("the channel is a different size from the canvas");
|
|
3550
|
+
out.push({
|
|
3551
|
+
id: typeof v.id === "string" && v.id ? v.id : `ch_${out.length + 1}`,
|
|
3552
|
+
name: typeof v.name === "string" && v.name.trim() ? v.name : `Alpha ${out.length + 1}`,
|
|
3553
|
+
mask,
|
|
3554
|
+
color: typeof v.color === "string" && /^#[0-9a-f]{6}$/i.test(v.color) ? v.color : DEFAULT_CHANNEL_COLOR,
|
|
3555
|
+
opacity: typeof v.opacity === "number" && v.opacity >= 0 && v.opacity <= 1 ? v.opacity : 0.5,
|
|
3556
|
+
indicates: v.indicates === "selected" ? "selected" : "masked",
|
|
3557
|
+
visible: v.visible === true
|
|
3558
|
+
});
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
problems.push(`Alpha channel "${String(v?.name ?? "?")}" could not be read: ${err instanceof Error ? err.message : String(err)}.`);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
return out;
|
|
3564
|
+
}
|
|
3565
|
+
function channelThumbnail(channel, maxSide = 64) {
|
|
3566
|
+
const { width: w, height: h, data } = channel.mask;
|
|
3567
|
+
const k = Math.min(1, maxSide / Math.max(w, h));
|
|
3568
|
+
const tw = Math.max(1, Math.round(w * k));
|
|
3569
|
+
const th = Math.max(1, Math.round(h * k));
|
|
3570
|
+
const out = new Uint8ClampedArray(tw * th * 4);
|
|
3571
|
+
for (let y = 0; y < th; y++) {
|
|
3572
|
+
const sy = Math.min(h - 1, Math.floor((y + 0.5) / k));
|
|
3573
|
+
for (let x = 0; x < tw; x++) {
|
|
3574
|
+
const v = data[sy * w + Math.min(w - 1, Math.floor((x + 0.5) / k))];
|
|
3575
|
+
out.set([v, v, v, 255], (y * tw + x) * 4);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return { width: tw, height: th, data: out };
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3482
3581
|
// src/image-editor/effect-layers.ts
|
|
3483
3582
|
function hasLayersBelow(layers, id) {
|
|
3484
3583
|
const all = flattenLayers(layers);
|
|
@@ -4389,6 +4488,7 @@ var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
|
|
|
4389
4488
|
var RETOUCH_TOOLS = ["clone", "heal", "spot-heal", "smudge", "dodge", "burn", "sponge", "blur", "sharpen", "history"];
|
|
4390
4489
|
var isRetouchTool = (tool) => RETOUCH_TOOLS.includes(tool);
|
|
4391
4490
|
var STROKE_TOOLS = ["brush", "eraser", "mixer", ...RETOUCH_TOOLS];
|
|
4491
|
+
var QUICK_MASK_TARGET = "quick-mask";
|
|
4392
4492
|
var PASTE_MODES = ["new-layer", "in-place", "into", "outside"];
|
|
4393
4493
|
function paintCoverageInto(base, coverage, apply, rects, depth = null) {
|
|
4394
4494
|
const tiles = new Map(base.tiles);
|
|
@@ -4899,6 +4999,10 @@ var ImageEditorController = class {
|
|
|
4899
4999
|
} catch {
|
|
4900
5000
|
}
|
|
4901
5001
|
}
|
|
5002
|
+
if (options.initialState?.channels) {
|
|
5003
|
+
const channels = deserializeChannels(options.initialState.channels, this.naturalSize, this.lastLoadProblems);
|
|
5004
|
+
if (channels.length) this.state.channels = channels;
|
|
5005
|
+
}
|
|
4902
5006
|
this.initialSnapshot = this.captureHistoryEntry();
|
|
4903
5007
|
if (typeof options.image === "string") {
|
|
4904
5008
|
this.loadImage(options.image);
|
|
@@ -5279,6 +5383,7 @@ var ImageEditorController = class {
|
|
|
5279
5383
|
transform: { ...s.transform },
|
|
5280
5384
|
outputOpacity: s.outputOpacity,
|
|
5281
5385
|
selection: s.selection ?? null,
|
|
5386
|
+
...s.channels?.length ? { channels: s.channels } : {},
|
|
5282
5387
|
source: {
|
|
5283
5388
|
sourceUrl: s.sourceUrl,
|
|
5284
5389
|
element: this.imageElement,
|
|
@@ -5314,6 +5419,8 @@ var ImageEditorController = class {
|
|
|
5314
5419
|
if (entry.colorMode) s.colorMode = entry.colorMode;
|
|
5315
5420
|
else delete s.colorMode;
|
|
5316
5421
|
s.selection = entry.selection;
|
|
5422
|
+
if (entry.channels?.length) s.channels = entry.channels;
|
|
5423
|
+
else delete s.channels;
|
|
5317
5424
|
if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
|
|
5318
5425
|
this.imageElement = entry.source.element;
|
|
5319
5426
|
s.sourceUrl = entry.source.sourceUrl;
|
|
@@ -7025,6 +7132,12 @@ var ImageEditorController = class {
|
|
|
7025
7132
|
}
|
|
7026
7133
|
/** Resolves and validates what a stroke, fill or clear paints into. */
|
|
7027
7134
|
paintTarget(layerId, channel, action) {
|
|
7135
|
+
if (channel === "quick-mask" || channel === void 0 && layerId === void 0 && this.state.quickMaskEditing) {
|
|
7136
|
+
if (!this.state.quickMaskEditing) {
|
|
7137
|
+
throw new LayerOperationError(`Quick Mask is off; turn it on (set_quick_mask) to ${action} on the selection, or target a layer.`, `Turn on Quick Mask (Q) to ${action} on the selection.`);
|
|
7138
|
+
}
|
|
7139
|
+
return { id: QUICK_MASK_TARGET, layer: null, channel: "quick-mask", offset: { x: 0, y: 0 }, surface: this.quickMaskSurface(), lockTransparency: false };
|
|
7140
|
+
}
|
|
7028
7141
|
const id = layerId ?? this.state.activeLayerId;
|
|
7029
7142
|
if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`, `Pick a layer to ${action} on in the Layers panel.`);
|
|
7030
7143
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
@@ -7093,10 +7206,11 @@ var ImageEditorController = class {
|
|
|
7093
7206
|
opacity: brush.opacity,
|
|
7094
7207
|
blend: target.channel !== "pixels" ? "normal" : brush.blend,
|
|
7095
7208
|
lockTransparency: target.lockTransparency,
|
|
7096
|
-
|
|
7209
|
+
// In Quick Mask mode the selection is what is painted, so it does not limit the stroke.
|
|
7210
|
+
selection: target.channel !== "quick-mask" && this.state.selection?.bounds ? this.state.selection : null,
|
|
7097
7211
|
offset: target.offset
|
|
7098
7212
|
};
|
|
7099
|
-
const depth = target.channel !== "filter-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
7213
|
+
const depth = target.channel !== "filter-mask" && target.channel !== "quick-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
7100
7214
|
if (tool === "mixer") return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply, depth);
|
|
7101
7215
|
if (isRetouchTool(tool)) return new PaintStrokeSession(this, target, this.retouchStroke(tool, init, brush, target, apply), apply, depth);
|
|
7102
7216
|
const gradient = tool === "brush" && strokePaint?.type === "gradient" ? resolvePaintGradient(strokePaint, { paint: toHex(color), secondary: toHex(background) }) : null;
|
|
@@ -7261,12 +7375,19 @@ var ImageEditorController = class {
|
|
|
7261
7375
|
}
|
|
7262
7376
|
/** @internal Shows a stroke's live surface (no history). */
|
|
7263
7377
|
_strokeUpdate(layerId, channel, surface, rects) {
|
|
7378
|
+
if (channel === "quick-mask") {
|
|
7379
|
+
this.state.selection = this.selectionFromSurface(surface, this.state.selection ?? null, rects);
|
|
7380
|
+
this.notify();
|
|
7381
|
+
return;
|
|
7382
|
+
}
|
|
7264
7383
|
this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
|
|
7265
7384
|
this.notify(channel === "filter-mask" ? void 0 : rects);
|
|
7266
7385
|
}
|
|
7267
7386
|
/** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */
|
|
7268
7387
|
_strokeCommit(before, layerId, channel, surface) {
|
|
7269
|
-
if (
|
|
7388
|
+
if (channel === "quick-mask") {
|
|
7389
|
+
this.state.selection = surface ? this.selectionFromSurface(surface) : before.selection;
|
|
7390
|
+
} else if (surface) {
|
|
7270
7391
|
const prev = before.layers;
|
|
7271
7392
|
this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));
|
|
7272
7393
|
if (this.bitDepth !== 8) this.state.layers = keepDeepLayers(prev, this.state.layers);
|
|
@@ -7482,7 +7603,8 @@ var ImageEditorController = class {
|
|
|
7482
7603
|
const opacity = options.opacity ?? 1;
|
|
7483
7604
|
if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
|
|
7484
7605
|
const blend = parseBlend(options.blend) ?? "normal";
|
|
7485
|
-
const
|
|
7606
|
+
const quickMask = target.channel === "quick-mask";
|
|
7607
|
+
const sel = coverageMask ?? (!quickMask && options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null);
|
|
7486
7608
|
const region = sel?.bounds ?? { x: 0, y: 0, width, height };
|
|
7487
7609
|
const coverage = new CoverageMap();
|
|
7488
7610
|
const ones = new Float32Array(region.width * region.height);
|
|
@@ -7508,8 +7630,14 @@ var ImageEditorController = class {
|
|
|
7508
7630
|
selection: null,
|
|
7509
7631
|
offset: target.offset
|
|
7510
7632
|
};
|
|
7511
|
-
const depth = target.channel !== "filter-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
7633
|
+
const depth = target.channel !== "filter-mask" && target.channel !== "quick-mask" && this.bitDepth !== 8 ? this.bitDepth : null;
|
|
7512
7634
|
const surface = paintCoverageInto(target.surface, coverage, apply, [region], depth);
|
|
7635
|
+
if (quickMask) {
|
|
7636
|
+
this.recordHistory();
|
|
7637
|
+
this.state.selection = this.selectionFromSurface(surface);
|
|
7638
|
+
this.notify();
|
|
7639
|
+
return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };
|
|
7640
|
+
}
|
|
7513
7641
|
const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));
|
|
7514
7642
|
this.commitLayers(next);
|
|
7515
7643
|
return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };
|
|
@@ -8158,10 +8286,160 @@ var ImageEditorController = class {
|
|
|
8158
8286
|
throw new Error(`Unknown selection display "${String(display)}". Use ants, quickMask or none.`);
|
|
8159
8287
|
}
|
|
8160
8288
|
if (this.getSelectionDisplay() === display) return;
|
|
8289
|
+
if (display !== "quickMask") delete this.state.quickMaskEditing;
|
|
8161
8290
|
this.state.selectionDisplay = display;
|
|
8162
8291
|
this.state.quickMask = display === "quickMask";
|
|
8163
8292
|
this.notify();
|
|
8164
8293
|
}
|
|
8294
|
+
// --- Alpha channels ---
|
|
8295
|
+
/** The alpha channels (saved selections), in panel order. */
|
|
8296
|
+
getChannels() {
|
|
8297
|
+
return this.state.channels ?? [];
|
|
8298
|
+
}
|
|
8299
|
+
getChannel(id) {
|
|
8300
|
+
return this.state.channels?.find((c) => c.id === id) ?? null;
|
|
8301
|
+
}
|
|
8302
|
+
requireChannel(id) {
|
|
8303
|
+
const channel = this.getChannel(id);
|
|
8304
|
+
if (!channel) {
|
|
8305
|
+
const ids = this.getChannels().map((c) => `${c.id} (${c.name})`);
|
|
8306
|
+
throw new LayerOperationError(`There is no alpha channel "${id}"; the channels are ${ids.join(", ") || "none (save a selection first)"}.`);
|
|
8307
|
+
}
|
|
8308
|
+
return channel;
|
|
8309
|
+
}
|
|
8310
|
+
setChannels(next, commitToHistory = true) {
|
|
8311
|
+
if (commitToHistory) this.recordHistory();
|
|
8312
|
+
if (next.length) this.state.channels = next;
|
|
8313
|
+
else delete this.state.channels;
|
|
8314
|
+
this.notify();
|
|
8315
|
+
}
|
|
8316
|
+
newChannelId() {
|
|
8317
|
+
const ids = new Set(this.getChannels().map((c) => c.id));
|
|
8318
|
+
let n = this.getChannels().length + 1;
|
|
8319
|
+
while (ids.has(`ch_${n}`)) n++;
|
|
8320
|
+
return `ch_${n}`;
|
|
8321
|
+
}
|
|
8322
|
+
/**
|
|
8323
|
+
* Select › Save Selection: a new alpha channel (`name`, default "Alpha n") holding the
|
|
8324
|
+
* selection, or with `channelId` the selection combined into that channel by `mode` (replace,
|
|
8325
|
+
* add, subtract, intersect). One undo step. Throws without a selection.
|
|
8326
|
+
*/
|
|
8327
|
+
saveSelection(options = {}) {
|
|
8328
|
+
const sel = this.state.selection;
|
|
8329
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select first, then save the selection to a channel.");
|
|
8330
|
+
const { width, height } = this.naturalSize;
|
|
8331
|
+
const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
|
|
8332
|
+
const channels = [...this.getChannels()];
|
|
8333
|
+
if (options.channelId !== void 0) {
|
|
8334
|
+
const existing = this.requireChannel(options.channelId);
|
|
8335
|
+
const mask = combineSelections(existing.mask.bounds ? existing.mask : null, sized, options.mode ?? "replace") ?? createSelectionMask(width, height);
|
|
8336
|
+
const next = { ...existing, mask, ...options.name?.trim() ? { name: options.name.trim() } : {} };
|
|
8337
|
+
this.setChannels(channels.map((c) => c.id === existing.id ? next : c));
|
|
8338
|
+
return next;
|
|
8339
|
+
}
|
|
8340
|
+
const channel = {
|
|
8341
|
+
id: this.newChannelId(),
|
|
8342
|
+
name: options.name?.trim() || `Alpha ${channels.length + 1}`,
|
|
8343
|
+
mask: sized,
|
|
8344
|
+
color: DEFAULT_CHANNEL_COLOR,
|
|
8345
|
+
opacity: 0.5,
|
|
8346
|
+
indicates: "masked",
|
|
8347
|
+
visible: false
|
|
8348
|
+
};
|
|
8349
|
+
this.setChannels([...channels, channel]);
|
|
8350
|
+
return channel;
|
|
8351
|
+
}
|
|
8352
|
+
/**
|
|
8353
|
+
* Select › Load Selection from an alpha channel (inverted with `invert`), combined with the
|
|
8354
|
+
* selection by `mode`. One undo step.
|
|
8355
|
+
*/
|
|
8356
|
+
loadSelectionFromChannel(id, options = {}) {
|
|
8357
|
+
const channel = this.requireChannel(id);
|
|
8358
|
+
const mask = options.invert ? invertSelection(channel.mask) : channel.mask;
|
|
8359
|
+
return this.setSelection(mask.bounds ? mask : null, options.mode ?? "replace");
|
|
8360
|
+
}
|
|
8361
|
+
/** Changes a channel's name, overlay colour, opacity, what it indicates or visibility (one undo step). */
|
|
8362
|
+
setChannelProperties(id, props) {
|
|
8363
|
+
const channel = this.requireChannel(id);
|
|
8364
|
+
if (props.name !== void 0 && !props.name.trim()) throw new LayerOperationError("A channel name cannot be empty.");
|
|
8365
|
+
if (props.color !== void 0 && !/^#[0-9a-f]{6}$/i.test(props.color)) throw new LayerOperationError("color must be #rrggbb.");
|
|
8366
|
+
if (props.opacity !== void 0 && !(props.opacity >= 0 && props.opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
|
|
8367
|
+
if (props.indicates !== void 0 && props.indicates !== "masked" && props.indicates !== "selected") throw new LayerOperationError("indicates must be masked or selected.");
|
|
8368
|
+
const next = {
|
|
8369
|
+
...channel,
|
|
8370
|
+
...props.name !== void 0 ? { name: props.name.trim() } : {},
|
|
8371
|
+
...props.color !== void 0 ? { color: props.color.toLowerCase() } : {},
|
|
8372
|
+
...props.opacity !== void 0 ? { opacity: props.opacity } : {},
|
|
8373
|
+
...props.indicates !== void 0 ? { indicates: props.indicates } : {},
|
|
8374
|
+
...props.visible !== void 0 ? { visible: props.visible } : {}
|
|
8375
|
+
};
|
|
8376
|
+
this.setChannels(this.getChannels().map((c) => c.id === id ? next : c));
|
|
8377
|
+
return next;
|
|
8378
|
+
}
|
|
8379
|
+
/** Deletes an alpha channel (one undo step). */
|
|
8380
|
+
deleteChannel(id) {
|
|
8381
|
+
this.requireChannel(id);
|
|
8382
|
+
this.setChannels(this.getChannels().filter((c) => c.id !== id));
|
|
8383
|
+
}
|
|
8384
|
+
/** Duplicates an alpha channel just after it (one undo step). Returns the copy. */
|
|
8385
|
+
duplicateChannel(id, name) {
|
|
8386
|
+
const channel = this.requireChannel(id);
|
|
8387
|
+
const copy = { ...channel, id: this.newChannelId(), name: name?.trim() || `${channel.name} copy` };
|
|
8388
|
+
const channels = [...this.getChannels()];
|
|
8389
|
+
channels.splice(channels.findIndex((c) => c.id === id) + 1, 0, copy);
|
|
8390
|
+
this.setChannels(channels);
|
|
8391
|
+
return copy;
|
|
8392
|
+
}
|
|
8393
|
+
/** Channel summaries (no pixels) for panels and agents. */
|
|
8394
|
+
describeChannels() {
|
|
8395
|
+
return this.getChannels().map(describeChannel);
|
|
8396
|
+
}
|
|
8397
|
+
// --- Quick Mask mode ---
|
|
8398
|
+
/** Whether Quick Mask mode is on (painting edits the selection). */
|
|
8399
|
+
get quickMaskEditing() {
|
|
8400
|
+
return Boolean(this.state.quickMaskEditing);
|
|
8401
|
+
}
|
|
8402
|
+
/**
|
|
8403
|
+
* Quick Mask mode (Q): on, the selection shows as a tint and painting, fills and strokes that
|
|
8404
|
+
* name no layer edit the selection itself (white selects, black deselects, grey partly);
|
|
8405
|
+
* off, it shows as marching ants again. View state: no undo step (the edits are).
|
|
8406
|
+
*/
|
|
8407
|
+
setQuickMaskMode(on) {
|
|
8408
|
+
if (this.quickMaskEditing === on) return;
|
|
8409
|
+
if (on) this.state.quickMaskEditing = true;
|
|
8410
|
+
else delete this.state.quickMaskEditing;
|
|
8411
|
+
this.setSelectionDisplay(on ? "quickMask" : "ants");
|
|
8412
|
+
this.notify();
|
|
8413
|
+
}
|
|
8414
|
+
/** The selection as a 1-channel surface to paint in Quick Mask mode. */
|
|
8415
|
+
quickMaskSurface() {
|
|
8416
|
+
const { width, height } = this.naturalSize;
|
|
8417
|
+
const sel = this.state.selection;
|
|
8418
|
+
if (!sel?.bounds) return emptySurface(1, 0);
|
|
8419
|
+
const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
|
|
8420
|
+
return surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);
|
|
8421
|
+
}
|
|
8422
|
+
/** The selection a painted Quick Mask surface stands for (`rects` changed since `base`). */
|
|
8423
|
+
selectionFromSurface(surface, base = null, rects) {
|
|
8424
|
+
const { width, height } = this.naturalSize;
|
|
8425
|
+
let data;
|
|
8426
|
+
if (base && rects && base.width === width && base.height === height) {
|
|
8427
|
+
data = new Uint8Array(base.data);
|
|
8428
|
+
for (const r of rects) {
|
|
8429
|
+
const x0 = Math.max(0, Math.floor(r.x));
|
|
8430
|
+
const y0 = Math.max(0, Math.floor(r.y));
|
|
8431
|
+
const x1 = Math.min(width, Math.ceil(r.x + r.width));
|
|
8432
|
+
const y1 = Math.min(height, Math.ceil(r.y + r.height));
|
|
8433
|
+
if (x1 <= x0 || y1 <= y0) continue;
|
|
8434
|
+
const part = readRegion(surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
8435
|
+
for (let y = y0; y < y1; y++) data.set(part.subarray((y - y0) * (x1 - x0), (y - y0 + 1) * (x1 - x0)), y * width + x0);
|
|
8436
|
+
}
|
|
8437
|
+
} else {
|
|
8438
|
+
const all = readRegion(surface, { x: 0, y: 0, width, height });
|
|
8439
|
+
data = new Uint8Array(all.buffer, all.byteOffset, all.length);
|
|
8440
|
+
}
|
|
8441
|
+
return normalizeSelection(createSelectionMask(width, height, data));
|
|
8442
|
+
}
|
|
8165
8443
|
// --- Layer masks ---
|
|
8166
8444
|
maskFromSelection(sel, props = {}) {
|
|
8167
8445
|
const { width, height } = this.naturalSize;
|
|
@@ -9275,6 +9553,17 @@ var ImageEditorController = class {
|
|
|
9275
9553
|
s.outputOpacity = 1;
|
|
9276
9554
|
delete s.legacyOutputBlur;
|
|
9277
9555
|
s.selection = null;
|
|
9556
|
+
const psdChannels = (result.alphaChannels ?? []).map((c, k) => ({
|
|
9557
|
+
id: `ch_${k + 1}`,
|
|
9558
|
+
name: c.name,
|
|
9559
|
+
mask: createSelectionMask(result.width, result.height, c.data),
|
|
9560
|
+
color: c.color,
|
|
9561
|
+
opacity: c.opacity,
|
|
9562
|
+
indicates: c.indicates,
|
|
9563
|
+
visible: false
|
|
9564
|
+
}));
|
|
9565
|
+
if (psdChannels.length) s.channels = psdChannels;
|
|
9566
|
+
else delete s.channels;
|
|
9278
9567
|
s.selectedAnnotationId = null;
|
|
9279
9568
|
s.activeLayerId = defaultActiveLayerId(s.layers);
|
|
9280
9569
|
s.activeTarget = "pixels";
|
|
@@ -9465,7 +9754,14 @@ var ImageEditorController = class {
|
|
|
9465
9754
|
if (color.colorMode && this.bitDepth === 32) warnings.push(`${color.colorMode.toUpperCase()} files are written at 16 bits per channel at most; the 32-bit samples were converted (colour above 1 clipped).`);
|
|
9466
9755
|
const depth = color.colorMode && this.bitDepth === 32 ? 16 : this.bitDepth;
|
|
9467
9756
|
const mergedDeep = depth === 8 ? void 0 : workingToSamples(compositeDeep(prepared, canvas, sources, depth), depth);
|
|
9468
|
-
const
|
|
9757
|
+
const alphaChannels = this.getChannels().map((c) => ({
|
|
9758
|
+
name: c.name,
|
|
9759
|
+
data: c.mask.width === width && c.mask.height === height ? c.mask.data : resizeSelection(c.mask, width, height).data,
|
|
9760
|
+
color: c.color,
|
|
9761
|
+
opacity: c.opacity,
|
|
9762
|
+
indicates: c.indicates
|
|
9763
|
+
}));
|
|
9764
|
+
const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, ...color });
|
|
9469
9765
|
warnings.push(...result.warnings);
|
|
9470
9766
|
return { bytes: result.bytes, warnings, psb: result.psb, width, height };
|
|
9471
9767
|
}
|
|
@@ -9538,6 +9834,7 @@ var ImageEditorController = class {
|
|
|
9538
9834
|
try {
|
|
9539
9835
|
out = this.serializeState();
|
|
9540
9836
|
if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
|
|
9837
|
+
if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
|
|
9541
9838
|
} finally {
|
|
9542
9839
|
if (root) {
|
|
9543
9840
|
this.state = current;
|
|
@@ -9578,6 +9875,11 @@ var ImageEditorController = class {
|
|
|
9578
9875
|
}
|
|
9579
9876
|
this.state.selection = selection;
|
|
9580
9877
|
this.dropMismatchedSelection();
|
|
9878
|
+
const problems = [];
|
|
9879
|
+
const channels = deserializeChannels(data.channels, this.naturalSize, problems);
|
|
9880
|
+
if (channels.length) this.state.channels = channels;
|
|
9881
|
+
else delete this.state.channels;
|
|
9882
|
+
if (problems.length) this.lastLoadProblems = [...this.lastLoadProblems, ...problems];
|
|
9581
9883
|
this.notify();
|
|
9582
9884
|
}
|
|
9583
9885
|
/** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */
|
|
@@ -10549,7 +10851,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
10549
10851
|
// src/image-editor/playback.ts
|
|
10550
10852
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
10551
10853
|
var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
|
|
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)$/;
|
|
10854
|
+
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|set_quick_mask)$/;
|
|
10553
10855
|
var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
|
|
10554
10856
|
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
10555
10857
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -11458,6 +11760,113 @@ function createImageSelectionTools(controller) {
|
|
|
11458
11760
|
annotations: writeAnnotations("Paste pixels"),
|
|
11459
11761
|
handler: (args) => controller.pastePixels(args.mode ?? "new-layer")
|
|
11460
11762
|
};
|
|
11763
|
+
const channelIdSchema = { type: "string", minLength: 1, description: "The alpha channel (see list_channels)." };
|
|
11764
|
+
const channelResult = (id) => controller.describeChannels().find((c) => c.id === id);
|
|
11765
|
+
const listChannels = {
|
|
11766
|
+
name: "list_channels",
|
|
11767
|
+
title: "List alpha channels",
|
|
11768
|
+
description: "Lists the alpha channels (saved selections kept with the document): id, name, bounds, percent of the image covered, overlay color, opacity, what the tint indicates and whether it is visible. Also whether Quick Mask mode is on.",
|
|
11769
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
11770
|
+
annotations: { title: "List alpha channels", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
11771
|
+
handler: () => ({ channels: controller.describeChannels(), quickMask: controller.quickMaskEditing })
|
|
11772
|
+
};
|
|
11773
|
+
const saveSelectionTool = {
|
|
11774
|
+
name: "save_selection",
|
|
11775
|
+
title: "Save the selection to a channel",
|
|
11776
|
+
description: 'Select \u203A Save Selection: stores the selection in a new alpha channel (name, default "Alpha n"), or with channelId combines it into that channel by mode (replace, add, subtract, intersect). Save any selection that took work, so it can be loaded again with load_selection_from_channel. Fails when nothing is selected. One undo step. Returns the channel.',
|
|
11777
|
+
inputSchema: {
|
|
11778
|
+
type: "object",
|
|
11779
|
+
properties: { channelId: channelIdSchema, name: { type: "string", minLength: 1, description: "Name of a new channel, or a new name for channelId." }, mode: modeSchema },
|
|
11780
|
+
additionalProperties: false
|
|
11781
|
+
},
|
|
11782
|
+
annotations: writeAnnotations("Save the selection to a channel"),
|
|
11783
|
+
handler: (args) => {
|
|
11784
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected; select first, then save it.");
|
|
11785
|
+
const c = controller.saveSelection({ channelId: args.channelId, name: args.name, mode: parseSelectionMode(args.mode) });
|
|
11786
|
+
return { channel: channelResult(c.id) };
|
|
11787
|
+
}
|
|
11788
|
+
};
|
|
11789
|
+
const loadChannelTool = {
|
|
11790
|
+
name: "load_selection_from_channel",
|
|
11791
|
+
title: "Load a selection from a channel",
|
|
11792
|
+
description: "Select \u203A Load Selection from an alpha channel (invert for its inverse), combined with the current selection by mode. One undo step. Returns the selection.",
|
|
11793
|
+
inputSchema: {
|
|
11794
|
+
type: "object",
|
|
11795
|
+
properties: { channelId: channelIdSchema, invert: { type: "boolean", description: "Load the inverse (default false)." }, mode: modeSchema },
|
|
11796
|
+
required: ["channelId"],
|
|
11797
|
+
additionalProperties: false
|
|
11798
|
+
},
|
|
11799
|
+
annotations: writeAnnotations("Load a selection from a channel", true),
|
|
11800
|
+
handler: (args) => {
|
|
11801
|
+
controller.loadSelectionFromChannel(String(args.channelId), { mode: parseSelectionMode(args.mode), invert: args.invert === true });
|
|
11802
|
+
return summary(controller);
|
|
11803
|
+
}
|
|
11804
|
+
};
|
|
11805
|
+
const channelPropsTool = {
|
|
11806
|
+
name: "set_channel_properties",
|
|
11807
|
+
title: "Change an alpha channel",
|
|
11808
|
+
description: "Renames an alpha channel or changes its overlay: color (#rrggbb), opacity (0..1), indicates (masked: tint the unselected area, or selected) and visible (show the tint on the canvas). Pixels do not change. One undo step. Returns the channel.",
|
|
11809
|
+
inputSchema: {
|
|
11810
|
+
type: "object",
|
|
11811
|
+
properties: {
|
|
11812
|
+
channelId: channelIdSchema,
|
|
11813
|
+
name: { type: "string", minLength: 1, description: "New name." },
|
|
11814
|
+
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Overlay colour #rrggbb." },
|
|
11815
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Overlay opacity 0..1." },
|
|
11816
|
+
indicates: { type: "string", enum: ["masked", "selected"], description: "Which area the tint covers." },
|
|
11817
|
+
visible: { type: "boolean", description: "Show the channel as a tint on the canvas." }
|
|
11818
|
+
},
|
|
11819
|
+
required: ["channelId"],
|
|
11820
|
+
additionalProperties: false
|
|
11821
|
+
},
|
|
11822
|
+
annotations: writeAnnotations("Change an alpha channel", true),
|
|
11823
|
+
handler: (args) => {
|
|
11824
|
+
const { channelId, ...props } = args;
|
|
11825
|
+
const c = controller.setChannelProperties(String(channelId), props);
|
|
11826
|
+
return { channel: channelResult(c.id) };
|
|
11827
|
+
}
|
|
11828
|
+
};
|
|
11829
|
+
const duplicateChannelTool = {
|
|
11830
|
+
name: "duplicate_channel",
|
|
11831
|
+
title: "Duplicate an alpha channel",
|
|
11832
|
+
description: 'Copies an alpha channel (name, default "<name> copy") just after it, to keep a version before changing it. One undo step. Returns the copy.',
|
|
11833
|
+
inputSchema: {
|
|
11834
|
+
type: "object",
|
|
11835
|
+
properties: { channelId: channelIdSchema, name: { type: "string", minLength: 1, description: "Name of the copy." } },
|
|
11836
|
+
required: ["channelId"],
|
|
11837
|
+
additionalProperties: false
|
|
11838
|
+
},
|
|
11839
|
+
annotations: writeAnnotations("Duplicate an alpha channel"),
|
|
11840
|
+
handler: (args) => ({ channel: channelResult(controller.duplicateChannel(String(args.channelId), args.name).id) })
|
|
11841
|
+
};
|
|
11842
|
+
const deleteChannelTool = {
|
|
11843
|
+
name: "delete_channel",
|
|
11844
|
+
title: "Delete an alpha channel",
|
|
11845
|
+
description: "Deletes an alpha channel (undo_image brings it back). Returns the remaining channels.",
|
|
11846
|
+
inputSchema: { type: "object", properties: { channelId: channelIdSchema }, required: ["channelId"], additionalProperties: false },
|
|
11847
|
+
annotations: { title: "Delete an alpha channel", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11848
|
+
handler: (args) => {
|
|
11849
|
+
controller.deleteChannel(String(args.channelId));
|
|
11850
|
+
return { channels: controller.describeChannels() };
|
|
11851
|
+
}
|
|
11852
|
+
};
|
|
11853
|
+
const quickMaskTool = {
|
|
11854
|
+
name: "set_quick_mask",
|
|
11855
|
+
title: "Quick Mask mode",
|
|
11856
|
+
description: "Turns Quick Mask mode on or off (Q). On, the selection shows as a red tint and paint_stroke, fill_pixels, clear_pixels, stroke_selection and the retouch tools called without layerId edit the selection itself: white selects, black deselects, grey selects partly, and blur_stroke softens its edge. Each edit is one undo step. Turn it off before painting layers again. Returns the mode and the selection.",
|
|
11857
|
+
inputSchema: {
|
|
11858
|
+
type: "object",
|
|
11859
|
+
properties: { enabled: { type: "boolean", description: "true to enter Quick Mask mode, false to leave it." } },
|
|
11860
|
+
required: ["enabled"],
|
|
11861
|
+
additionalProperties: false
|
|
11862
|
+
},
|
|
11863
|
+
annotations: writeAnnotations("Quick Mask mode", true),
|
|
11864
|
+
handler: (args) => {
|
|
11865
|
+
if (typeof args.enabled !== "boolean") throw new Error("enabled must be true or false.");
|
|
11866
|
+
controller.setQuickMaskMode(args.enabled);
|
|
11867
|
+
return { quickMask: controller.quickMaskEditing, selection: summary(controller) };
|
|
11868
|
+
}
|
|
11869
|
+
};
|
|
11461
11870
|
return [
|
|
11462
11871
|
getSelection,
|
|
11463
11872
|
selectAll,
|
|
@@ -11486,7 +11895,14 @@ function createImageSelectionTools(controller) {
|
|
|
11486
11895
|
fillTool,
|
|
11487
11896
|
copyTool,
|
|
11488
11897
|
cutTool,
|
|
11489
|
-
pasteTool
|
|
11898
|
+
pasteTool,
|
|
11899
|
+
listChannels,
|
|
11900
|
+
saveSelectionTool,
|
|
11901
|
+
loadChannelTool,
|
|
11902
|
+
channelPropsTool,
|
|
11903
|
+
duplicateChannelTool,
|
|
11904
|
+
deleteChannelTool,
|
|
11905
|
+
quickMaskTool
|
|
11490
11906
|
];
|
|
11491
11907
|
}
|
|
11492
11908
|
function createImageSubjectSelectionTool(controller) {
|
|
@@ -12907,6 +13323,9 @@ function handlePaintShortcut(controller, e) {
|
|
|
12907
13323
|
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
12908
13324
|
const key = e.key;
|
|
12909
13325
|
switch (key.toLowerCase()) {
|
|
13326
|
+
case "q":
|
|
13327
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13328
|
+
return true;
|
|
12910
13329
|
case "b":
|
|
12911
13330
|
controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
|
|
12912
13331
|
return true;
|
|
@@ -13046,8 +13465,8 @@ var TARGET = {
|
|
|
13046
13465
|
layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
|
|
13047
13466
|
channel: {
|
|
13048
13467
|
type: "string",
|
|
13049
|
-
enum: ["pixels", "mask", "filter-mask"],
|
|
13050
|
-
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals."
|
|
13468
|
+
enum: ["pixels", "mask", "filter-mask", "quick-mask"],
|
|
13469
|
+
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals. In Quick Mask mode (set_quick_mask), quick-mask (the default when no layerId is given) paints the selection: white selects, black deselects."
|
|
13051
13470
|
}
|
|
13052
13471
|
};
|
|
13053
13472
|
var BRUSH = {
|
|
@@ -13435,8 +13854,8 @@ var TARGET2 = {
|
|
|
13435
13854
|
layerId: { type: "string", description: "Layer to retouch (default the active layer; see list_layers)." },
|
|
13436
13855
|
channel: {
|
|
13437
13856
|
type: "string",
|
|
13438
|
-
enum: ["pixels", "mask", "filter-mask"],
|
|
13439
|
-
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves."
|
|
13857
|
+
enum: ["pixels", "mask", "filter-mask", "quick-mask"],
|
|
13858
|
+
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves. In Quick Mask mode (set_quick_mask), quick-mask (the default when no layerId is given) paints the selection: white selects, black deselects."
|
|
13440
13859
|
}
|
|
13441
13860
|
};
|
|
13442
13861
|
var SAMPLE = {
|
|
@@ -15538,6 +15957,11 @@ function handleSelectionShortcut(controller, e, onError) {
|
|
|
15538
15957
|
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
15539
15958
|
return false;
|
|
15540
15959
|
}
|
|
15960
|
+
function handleQuickMaskKey(controller, e) {
|
|
15961
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
15962
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
15963
|
+
return true;
|
|
15964
|
+
}
|
|
15541
15965
|
|
|
15542
15966
|
// src/image-editor/mosaic-filter.ts
|
|
15543
15967
|
function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
|
|
@@ -17124,6 +17548,7 @@ export {
|
|
|
17124
17548
|
DEFAULT_BLENDER,
|
|
17125
17549
|
DEFAULT_BRUSH,
|
|
17126
17550
|
DEFAULT_CANVAS_VIEWPORT,
|
|
17551
|
+
DEFAULT_CHANNEL_COLOR,
|
|
17127
17552
|
DEFAULT_DEPTH_MASK,
|
|
17128
17553
|
DEFAULT_FLOW,
|
|
17129
17554
|
DEFAULT_GLOBAL_LIGHT,
|
|
@@ -17268,6 +17693,7 @@ export {
|
|
|
17268
17693
|
PatternPlacement,
|
|
17269
17694
|
PatternTile,
|
|
17270
17695
|
PsdError,
|
|
17696
|
+
QUICK_MASK_TARGET,
|
|
17271
17697
|
REFINE_EDGE_CONTROLS,
|
|
17272
17698
|
RETOUCH_CHOICES,
|
|
17273
17699
|
RETOUCH_OPTION_CONTROLS,
|
|
@@ -17425,6 +17851,7 @@ export {
|
|
|
17425
17851
|
canvasToImagePoint,
|
|
17426
17852
|
caretAt,
|
|
17427
17853
|
changedTiles,
|
|
17854
|
+
channelThumbnail,
|
|
17428
17855
|
checkEffectAnimation,
|
|
17429
17856
|
checkEffectTime,
|
|
17430
17857
|
checkImageEffectPlace,
|
|
@@ -17523,10 +17950,12 @@ export {
|
|
|
17523
17950
|
depthMaskAlpha,
|
|
17524
17951
|
depthOf,
|
|
17525
17952
|
depthRasterMask,
|
|
17953
|
+
describeChannel,
|
|
17526
17954
|
describeLayer,
|
|
17527
17955
|
describeLayerEffects,
|
|
17528
17956
|
describeSelection,
|
|
17529
17957
|
describeTransform,
|
|
17958
|
+
deserializeChannels,
|
|
17530
17959
|
deserializeColorMode,
|
|
17531
17960
|
deserializeColorProfile,
|
|
17532
17961
|
deserializeEffects,
|
|
@@ -17617,6 +18046,7 @@ export {
|
|
|
17617
18046
|
guidePlanes,
|
|
17618
18047
|
guidedFilterColor,
|
|
17619
18048
|
handlePaintShortcut,
|
|
18049
|
+
handleQuickMaskKey,
|
|
17620
18050
|
handleSelectionShortcut,
|
|
17621
18051
|
hasActiveEffects,
|
|
17622
18052
|
hasLayersBelow,
|
|
@@ -17944,6 +18374,7 @@ export {
|
|
|
17944
18374
|
selectionToCoverage,
|
|
17945
18375
|
selectionToPixels,
|
|
17946
18376
|
selectionValueAt,
|
|
18377
|
+
serializeChannels,
|
|
17947
18378
|
serializeColorMode,
|
|
17948
18379
|
serializeColorProfile,
|
|
17949
18380
|
serializeContents,
|