@pixerate/editor 0.210.0 → 0.212.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-QE7D7TZR.cjs → chunk-H65IBXFI.cjs} +74 -25
- package/dist/{chunk-KFIOEOCQ.js → chunk-W3TOZTL7.js} +55 -6
- package/dist/image-editor/content-aware-fill-session.d.cts +17 -5
- package/dist/image-editor/content-aware-fill-session.d.ts +17 -5
- package/dist/image-editor/controller.d.cts +60 -4
- package/dist/image-editor/controller.d.ts +60 -4
- package/dist/image-editor/index.cjs +1522 -1013
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +532 -23
- package/dist/image-editor/layers/serialize.d.cts +5 -8
- package/dist/image-editor/layers/serialize.d.ts +5 -8
- package/dist/image-editor/paths.d.cts +41 -0
- package/dist/image-editor/paths.d.ts +41 -0
- package/dist/image-editor/psd/export.d.cts +8 -1
- package/dist/image-editor/psd/export.d.ts +8 -1
- package/dist/image-editor/psd/import.d.cts +7 -0
- package/dist/image-editor/psd/import.d.ts +7 -0
- package/dist/image-editor/psd/paths.d.cts +11 -0
- package/dist/image-editor/psd/paths.d.ts +11 -0
- package/dist/image-editor/selection/gesture.d.cts +10 -2
- package/dist/image-editor/selection/gesture.d.ts +10 -2
- package/dist/image-editor/selection/select-and-mask.d.cts +5 -0
- package/dist/image-editor/selection/select-and-mask.d.ts +5 -0
- package/dist/image-editor/types.d.cts +12 -0
- package/dist/image-editor/types.d.ts +12 -0
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
|
@@ -301,6 +301,7 @@ import {
|
|
|
301
301
|
rasterizeCoverage,
|
|
302
302
|
rasterizeSmartObject,
|
|
303
303
|
readPsd,
|
|
304
|
+
readShapeComponents,
|
|
304
305
|
referencePoint,
|
|
305
306
|
removeAlpha,
|
|
306
307
|
removeLayer,
|
|
@@ -401,7 +402,7 @@ import {
|
|
|
401
402
|
writePsd,
|
|
402
403
|
xyToTempTint,
|
|
403
404
|
xyToUv
|
|
404
|
-
} from "../chunk-
|
|
405
|
+
} from "../chunk-W3TOZTL7.js";
|
|
405
406
|
import {
|
|
406
407
|
parseCssColor
|
|
407
408
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -1477,8 +1478,9 @@ var ImageEditorRenderer = class {
|
|
|
1477
1478
|
ctx.restore();
|
|
1478
1479
|
}
|
|
1479
1480
|
const { width: iw, height: ih } = state.imageDimensions;
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1481
|
+
const shownSel = state.selectionPreview !== void 0 ? state.selectionPreview : state.selection;
|
|
1482
|
+
const quickMaskSel = display === "quickMask" && state.quickMaskEditing && !shownSel?.bounds ? this.emptyMask(Math.round(iw), Math.round(ih)) : null;
|
|
1483
|
+
if (shownSel?.bounds && display !== "none" || quickMaskSel || options.draftSelectionPath?.length) {
|
|
1482
1484
|
ctx.save();
|
|
1483
1485
|
try {
|
|
1484
1486
|
applyImageProjectionToContext(ctx, projection);
|
|
@@ -1486,8 +1488,8 @@ var ImageEditorRenderer = class {
|
|
|
1486
1488
|
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1487
1489
|
ctx.clip();
|
|
1488
1490
|
if (quickMaskSel) this.drawQuickMask(ctx, quickMaskSel);
|
|
1489
|
-
else if (
|
|
1490
|
-
else if (
|
|
1491
|
+
else if (shownSel?.bounds && display === "quickMask") this.drawQuickMask(ctx, shownSel);
|
|
1492
|
+
else if (shownSel?.bounds && display === "ants") this.drawAnts(ctx, shownSel, projection.scale / u, options.antsOffset ?? 0);
|
|
1491
1493
|
if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
|
|
1492
1494
|
} catch {
|
|
1493
1495
|
}
|
|
@@ -2861,6 +2863,58 @@ function magneticAnchorSpacing(frequency = 57) {
|
|
|
2861
2863
|
return 8 + (100 - f);
|
|
2862
2864
|
}
|
|
2863
2865
|
|
|
2866
|
+
// src/image-editor/paths.ts
|
|
2867
|
+
var WORK_PATH_NAME = "Work Path";
|
|
2868
|
+
var counter = 0;
|
|
2869
|
+
function newPathId() {
|
|
2870
|
+
counter = (counter + 1) % 1e6;
|
|
2871
|
+
return `path-${Date.now().toString(36)}-${counter.toString(36)}`;
|
|
2872
|
+
}
|
|
2873
|
+
function pathSvgData(path) {
|
|
2874
|
+
return subpathsToSvgPath(path.components.flatMap((c) => c.subpaths));
|
|
2875
|
+
}
|
|
2876
|
+
function pathBounds(path) {
|
|
2877
|
+
return vectorMaskBounds({ components: path.components });
|
|
2878
|
+
}
|
|
2879
|
+
function pathAnchorCount(path) {
|
|
2880
|
+
return path.components.reduce((n, c) => n + c.subpaths.reduce((m, sp) => m + sp.anchors.length, 0), 0);
|
|
2881
|
+
}
|
|
2882
|
+
function serializePaths(paths) {
|
|
2883
|
+
return paths.map((p) => ({
|
|
2884
|
+
id: p.id,
|
|
2885
|
+
name: p.name,
|
|
2886
|
+
components: JSON.parse(JSON.stringify(p.components)),
|
|
2887
|
+
...p.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
|
|
2888
|
+
...p.work ? { work: true } : {}
|
|
2889
|
+
}));
|
|
2890
|
+
}
|
|
2891
|
+
function readPaths(stored, problems = []) {
|
|
2892
|
+
if (!Array.isArray(stored)) return [];
|
|
2893
|
+
const out = [];
|
|
2894
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2895
|
+
let work = false;
|
|
2896
|
+
for (const [i, raw] of stored.entries()) {
|
|
2897
|
+
const r = raw;
|
|
2898
|
+
const name = typeof r?.name === "string" && r.name.trim() ? r.name.trim() : `Path ${i + 1}`;
|
|
2899
|
+
try {
|
|
2900
|
+
const components = readShapeComponents(r?.components);
|
|
2901
|
+
let id = typeof r?.id === "string" && r.id ? r.id : newPathId();
|
|
2902
|
+
if (ids.has(id)) id = newPathId();
|
|
2903
|
+
ids.add(id);
|
|
2904
|
+
const path = { id, name, components };
|
|
2905
|
+
if (r?.fillRule === "evenodd") path.fillRule = "evenodd";
|
|
2906
|
+
if (r?.work === true && !work) {
|
|
2907
|
+
path.work = true;
|
|
2908
|
+
work = true;
|
|
2909
|
+
}
|
|
2910
|
+
out.push(path);
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
problems.push(`Path "${name}" could not be read (${err instanceof Error ? err.message : String(err)}) and was left out.`);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return out;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2864
2918
|
// src/image-editor/layers/shape-type-ui.ts
|
|
2865
2919
|
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
2866
2920
|
kind: "rect",
|
|
@@ -5643,6 +5697,7 @@ var ImageEditorController = class {
|
|
|
5643
5697
|
outputOpacity: s.outputOpacity,
|
|
5644
5698
|
selection: s.selection ?? null,
|
|
5645
5699
|
...s.channels?.length ? { channels: s.channels } : {},
|
|
5700
|
+
...s.paths?.length ? { paths: s.paths } : {},
|
|
5646
5701
|
source: {
|
|
5647
5702
|
sourceUrl: s.sourceUrl,
|
|
5648
5703
|
element: this.imageElement,
|
|
@@ -5680,6 +5735,9 @@ var ImageEditorController = class {
|
|
|
5680
5735
|
s.selection = entry.selection;
|
|
5681
5736
|
if (entry.channels?.length) s.channels = entry.channels;
|
|
5682
5737
|
else delete s.channels;
|
|
5738
|
+
if (entry.paths?.length) s.paths = entry.paths;
|
|
5739
|
+
else delete s.paths;
|
|
5740
|
+
if (s.activePathId && !s.paths?.some((p) => p.id === s.activePathId)) s.activePathId = null;
|
|
5683
5741
|
if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
|
|
5684
5742
|
this.imageElement = entry.source.element;
|
|
5685
5743
|
s.sourceUrl = entry.source.sourceUrl;
|
|
@@ -6491,6 +6549,138 @@ var ImageEditorController = class {
|
|
|
6491
6549
|
const mask = pathToSelection(components, width, height, { ...options, fillRule: path.fillRule ?? options.fillRule });
|
|
6492
6550
|
return this.setSelection(mask, options.mode ?? "replace");
|
|
6493
6551
|
}
|
|
6552
|
+
// --- Document paths (the Paths panel) ---
|
|
6553
|
+
/** The document's paths: saved paths and the Work Path, in panel order. */
|
|
6554
|
+
getPaths() {
|
|
6555
|
+
return this.state.paths ?? [];
|
|
6556
|
+
}
|
|
6557
|
+
getPath(id) {
|
|
6558
|
+
return this.state.paths?.find((p) => p.id === id) ?? null;
|
|
6559
|
+
}
|
|
6560
|
+
requirePath(id) {
|
|
6561
|
+
const path = this.getPath(id);
|
|
6562
|
+
if (!path) {
|
|
6563
|
+
const ids = this.getPaths().map((p) => `${p.id} (${p.name})`);
|
|
6564
|
+
throw new LayerOperationError(`There is no path "${id}"; the paths are ${ids.join(", ") || "none (make a work path from a selection, or add one)"}.`);
|
|
6565
|
+
}
|
|
6566
|
+
return path;
|
|
6567
|
+
}
|
|
6568
|
+
/** Selects a path in the Paths panel (or none). View state: no undo step. */
|
|
6569
|
+
setActivePath(id) {
|
|
6570
|
+
if (id !== null) this.requirePath(id);
|
|
6571
|
+
if ((this.state.activePathId ?? null) === id) return;
|
|
6572
|
+
this.state.activePathId = id;
|
|
6573
|
+
this.notify();
|
|
6574
|
+
}
|
|
6575
|
+
commitPaths(next, active) {
|
|
6576
|
+
this.recordHistory();
|
|
6577
|
+
if (next.length) this.state.paths = next;
|
|
6578
|
+
else delete this.state.paths;
|
|
6579
|
+
if (active !== void 0) this.state.activePathId = active;
|
|
6580
|
+
else if (this.state.activePathId && !next.some((p) => p.id === this.state.activePathId)) this.state.activePathId = null;
|
|
6581
|
+
this.notify();
|
|
6582
|
+
}
|
|
6583
|
+
/**
|
|
6584
|
+
* Paths › Make Work Path: the selection's outline, traced, becomes the Work Path (replacing the
|
|
6585
|
+
* last one, unless it was saved). One undo step. Returns the path.
|
|
6586
|
+
*/
|
|
6587
|
+
makeWorkPath(options = {}) {
|
|
6588
|
+
const subpaths = this.traceSelection(options);
|
|
6589
|
+
const paths = this.getPaths();
|
|
6590
|
+
const old = paths.find((p) => p.work);
|
|
6591
|
+
const work = { id: old?.id ?? newPathId(), name: WORK_PATH_NAME, components: [{ op: "combine", subpaths }], fillRule: "evenodd", work: true };
|
|
6592
|
+
this.commitPaths(old ? paths.map((p) => p === old ? work : p) : [...paths, work], work.id);
|
|
6593
|
+
return work;
|
|
6594
|
+
}
|
|
6595
|
+
/** A new saved path from geometry (`shape`, `subpaths` or `components`). One undo step. */
|
|
6596
|
+
addPath(input) {
|
|
6597
|
+
const paths = this.getPaths();
|
|
6598
|
+
const path = { id: newPathId(), name: input.name?.trim() || `Path ${paths.filter((p) => !p.work).length + 1}`, components: this.vectorMaskComponents(input) };
|
|
6599
|
+
if (input.fillRule === "evenodd") path.fillRule = "evenodd";
|
|
6600
|
+
this.commitPaths([...paths.filter((p) => !p.work), path, ...paths.filter((p) => p.work)], path.id);
|
|
6601
|
+
return path;
|
|
6602
|
+
}
|
|
6603
|
+
/** Replaces a path's geometry (its name is kept). One undo step. */
|
|
6604
|
+
setPathGeometry(id, input) {
|
|
6605
|
+
const old = this.requirePath(id);
|
|
6606
|
+
const path = { ...old, components: this.vectorMaskComponents(input) };
|
|
6607
|
+
if (input.fillRule === "evenodd") path.fillRule = "evenodd";
|
|
6608
|
+
else if (input.fillRule === "nonzero") delete path.fillRule;
|
|
6609
|
+
this.commitPaths(this.getPaths().map((p) => p.id === id ? path : p));
|
|
6610
|
+
return path;
|
|
6611
|
+
}
|
|
6612
|
+
/** Renames a path; renaming the Work Path saves it (Photoshop's Save Path). One undo step. */
|
|
6613
|
+
renamePath(id, name) {
|
|
6614
|
+
const old = this.requirePath(id);
|
|
6615
|
+
const trimmed = name.trim();
|
|
6616
|
+
if (!trimmed) throw new LayerOperationError("A path needs a name.");
|
|
6617
|
+
const path = { ...old, name: trimmed };
|
|
6618
|
+
delete path.work;
|
|
6619
|
+
this.commitPaths(this.getPaths().map((p) => p.id === id ? path : p));
|
|
6620
|
+
return path;
|
|
6621
|
+
}
|
|
6622
|
+
/** Duplicates a path as a saved path. One undo step. */
|
|
6623
|
+
duplicatePath(id, name) {
|
|
6624
|
+
const old = this.requirePath(id);
|
|
6625
|
+
const copy = { ...old, id: newPathId(), name: name?.trim() || `${old.name} copy`, components: JSON.parse(JSON.stringify(old.components)) };
|
|
6626
|
+
delete copy.work;
|
|
6627
|
+
const paths = this.getPaths();
|
|
6628
|
+
this.commitPaths([...paths.filter((p) => !p.work), copy, ...paths.filter((p) => p.work)], copy.id);
|
|
6629
|
+
return copy;
|
|
6630
|
+
}
|
|
6631
|
+
/** Deletes a path. One undo step. */
|
|
6632
|
+
deletePath(id) {
|
|
6633
|
+
this.requirePath(id);
|
|
6634
|
+
this.commitPaths(this.getPaths().filter((p) => p.id !== id));
|
|
6635
|
+
}
|
|
6636
|
+
/** The area a path fills over the image, as a selection mask. */
|
|
6637
|
+
pathArea(path, options = {}) {
|
|
6638
|
+
const { width, height } = this.naturalSize;
|
|
6639
|
+
return pathToSelection(path.components, width, height, { ...options, fillRule: path.fillRule ?? options.fillRule });
|
|
6640
|
+
}
|
|
6641
|
+
/** Paths › Make Selection: loads a path as the selection, combined by `mode`. One undo step. */
|
|
6642
|
+
loadPathAsSelection(id, options = {}) {
|
|
6643
|
+
if (options.feather !== void 0 && !(options.feather >= 0 && options.feather <= 1e3)) throw new LayerOperationError("feather must be from 0 to 1000 px.");
|
|
6644
|
+
return this.setSelection(this.pathArea(this.requirePath(id), options), options.mode ?? "replace");
|
|
6645
|
+
}
|
|
6646
|
+
/**
|
|
6647
|
+
* Paths › Fill Path: fills the path's area on a pixel layer (default the active one) with a
|
|
6648
|
+
* colour (default the foreground). The selection is not changed. One undo step.
|
|
6649
|
+
*/
|
|
6650
|
+
fillPath(id, options = {}) {
|
|
6651
|
+
const area = this.pathArea(this.requirePath(id));
|
|
6652
|
+
if (!area.bounds) throw new LayerOperationError("The path encloses no area to fill.");
|
|
6653
|
+
return this.withSelection(area, () => this.fillPixels({ ...options, useSelection: true }));
|
|
6654
|
+
}
|
|
6655
|
+
/**
|
|
6656
|
+
* Paths › Stroke Path: strokes along the path's outline on a pixel layer (default the active
|
|
6657
|
+
* one), `width` px wide and centred on it. The selection is not changed. One undo step.
|
|
6658
|
+
*/
|
|
6659
|
+
strokePath(id, options) {
|
|
6660
|
+
const area = this.pathArea(this.requirePath(id), { antiAlias: true });
|
|
6661
|
+
if (!area.bounds) throw new LayerOperationError("The path encloses no area; stroke a closed path.");
|
|
6662
|
+
return this.withSelection(area, () => this.strokeSelection({ ...options, location: "center" }));
|
|
6663
|
+
}
|
|
6664
|
+
/** Paths › Add Vector Mask: masks a layer (default the active one) with a document path. One undo step. */
|
|
6665
|
+
addVectorMaskFromPath(pathId, layerId) {
|
|
6666
|
+
const path = this.requirePath(pathId);
|
|
6667
|
+
const id = layerId ?? this.state.activeLayerId;
|
|
6668
|
+
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
|
|
6669
|
+
return this.addVectorMask(id, "path", { components: path.components.map((c) => ({ op: c.op, subpaths: c.subpaths })), ...path.fillRule ? { fillRule: path.fillRule } : {} });
|
|
6670
|
+
}
|
|
6671
|
+
/** Runs `fn` with `mask` as the selection, then puts the selection back, all as one undo step. */
|
|
6672
|
+
withSelection(mask, fn) {
|
|
6673
|
+
return this.asOneStep(() => {
|
|
6674
|
+
const before = this.state.selection ?? null;
|
|
6675
|
+
this.state.selection = mask;
|
|
6676
|
+
try {
|
|
6677
|
+
return fn();
|
|
6678
|
+
} finally {
|
|
6679
|
+
this.state.selection = before;
|
|
6680
|
+
this.notify();
|
|
6681
|
+
}
|
|
6682
|
+
});
|
|
6683
|
+
}
|
|
6494
6684
|
/** Replaces a vector mask's path (its properties are kept). One undo step. Returns the vector mask. */
|
|
6495
6685
|
setVectorMaskPath(id, path) {
|
|
6496
6686
|
const { vm } = this.requireVectorMask(id);
|
|
@@ -7949,7 +8139,9 @@ var ImageEditorController = class {
|
|
|
7949
8139
|
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
|
|
7950
8140
|
if (this.bitDepth !== 8) throw new LayerOperationError("Content-Aware Fill works on 8-bit documents for now; set_bit_depth 8 first, or use spot_heal.");
|
|
7951
8141
|
const output = options.output ?? "current";
|
|
7952
|
-
if (output !== "current" && output !== "new-layer"
|
|
8142
|
+
if (output !== "current" && output !== "new-layer" && output !== "duplicate-layer") {
|
|
8143
|
+
throw new LayerOperationError(`Unknown output "${String(output)}"; use current, new-layer or duplicate-layer.`);
|
|
8144
|
+
}
|
|
7953
8145
|
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
7954
8146
|
const { width, height } = this.naturalSize;
|
|
7955
8147
|
const b = sel.bounds;
|
|
@@ -8026,6 +8218,13 @@ var ImageEditorController = class {
|
|
|
8026
8218
|
}
|
|
8027
8219
|
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
8028
8220
|
const surface = writeRegion(target.surface, layerRect, out);
|
|
8221
|
+
if (output === "duplicate-layer") {
|
|
8222
|
+
return this.asOneStep(() => {
|
|
8223
|
+
const copy = this.duplicateLayer(target.id);
|
|
8224
|
+
this.commitLayers(mapLayer(this.state.layers, copy, (l) => this.withSurface(l, "pixels", surface)));
|
|
8225
|
+
return { layerId: copy, bounds: { ...b }, output };
|
|
8226
|
+
});
|
|
8227
|
+
}
|
|
8029
8228
|
this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
8030
8229
|
return { layerId: target.id, bounds: { ...b }, output };
|
|
8031
8230
|
}
|
|
@@ -8511,6 +8710,17 @@ var ImageEditorController = class {
|
|
|
8511
8710
|
this.notify();
|
|
8512
8711
|
return true;
|
|
8513
8712
|
}
|
|
8713
|
+
/**
|
|
8714
|
+
* Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
|
|
8715
|
+
* moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo
|
|
8716
|
+
* step, not saved.
|
|
8717
|
+
*/
|
|
8718
|
+
setSelectionPreview(mask) {
|
|
8719
|
+
if (this.state.selectionPreview === mask) return;
|
|
8720
|
+
if (mask === void 0) delete this.state.selectionPreview;
|
|
8721
|
+
else this.state.selectionPreview = mask;
|
|
8722
|
+
this.notify();
|
|
8723
|
+
}
|
|
8514
8724
|
/** Select › All. */
|
|
8515
8725
|
selectAll() {
|
|
8516
8726
|
const { width, height } = this.naturalSize;
|
|
@@ -10482,6 +10692,10 @@ var ImageEditorController = class {
|
|
|
10482
10692
|
}));
|
|
10483
10693
|
if (psdChannels.length) s.channels = psdChannels;
|
|
10484
10694
|
else delete s.channels;
|
|
10695
|
+
const psdPaths = (result.paths ?? []).map((p) => ({ id: newPathId(), name: p.name, components: p.components, ...p.work ? { work: true } : {} }));
|
|
10696
|
+
if (psdPaths.length) s.paths = psdPaths;
|
|
10697
|
+
else delete s.paths;
|
|
10698
|
+
s.activePathId = null;
|
|
10485
10699
|
s.selectedAnnotationId = null;
|
|
10486
10700
|
s.activeLayerId = defaultActiveLayerId(s.layers);
|
|
10487
10701
|
s.activeTarget = "pixels";
|
|
@@ -10679,7 +10893,7 @@ var ImageEditorController = class {
|
|
|
10679
10893
|
opacity: c.opacity,
|
|
10680
10894
|
indicates: c.indicates
|
|
10681
10895
|
}));
|
|
10682
|
-
const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, ...color });
|
|
10896
|
+
const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, paths: this.getPaths(), ...color });
|
|
10683
10897
|
warnings.push(...result.warnings);
|
|
10684
10898
|
return { bytes: result.bytes, warnings, psb: result.psb, width, height };
|
|
10685
10899
|
}
|
|
@@ -10753,6 +10967,7 @@ var ImageEditorController = class {
|
|
|
10753
10967
|
out = this.serializeState();
|
|
10754
10968
|
if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
|
|
10755
10969
|
if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
|
|
10970
|
+
if (this.state.paths?.length) out.paths = serializePaths(this.state.paths);
|
|
10756
10971
|
} finally {
|
|
10757
10972
|
if (root) {
|
|
10758
10973
|
this.state = current;
|
|
@@ -10797,6 +11012,10 @@ var ImageEditorController = class {
|
|
|
10797
11012
|
const channels = deserializeChannels(data.channels, this.naturalSize, problems);
|
|
10798
11013
|
if (channels.length) this.state.channels = channels;
|
|
10799
11014
|
else delete this.state.channels;
|
|
11015
|
+
const paths = readPaths(data.paths, problems);
|
|
11016
|
+
if (paths.length) this.state.paths = paths;
|
|
11017
|
+
else delete this.state.paths;
|
|
11018
|
+
this.state.activePathId = null;
|
|
10800
11019
|
if (problems.length) this.lastLoadProblems = [...this.lastLoadProblems, ...problems];
|
|
10801
11020
|
this.notify();
|
|
10802
11021
|
}
|
|
@@ -11773,7 +11992,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
11773
11992
|
// src/image-editor/playback.ts
|
|
11774
11993
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
11775
11994
|
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)$/;
|
|
11776
|
-
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_focus_area|select_sky|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|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
|
|
11995
|
+
var SELECT = /^(make_work_path|add_path|set_path_geometry|rename_path|duplicate_path|delete_path|fill_path|stroke_path|select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_focus_area|select_sky|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|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
|
|
11777
11996
|
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|set_text_orientation|convert_text_to_shape)$/;
|
|
11778
11997
|
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
11779
11998
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -12865,7 +13084,7 @@ function createImageSelectionTools(controller) {
|
|
|
12865
13084
|
inputSchema: {
|
|
12866
13085
|
type: "object",
|
|
12867
13086
|
properties: {
|
|
12868
|
-
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer
|
|
13087
|
+
output: { type: "string", enum: ["new-layer", "current", "duplicate-layer"], description: "new-layer (the fill alone on a layer above), current (into the layer) or duplicate-layer (into a copy of the layer, above it); default current." },
|
|
12869
13088
|
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
12870
13089
|
seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
|
|
12871
13090
|
sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
|
|
@@ -14497,6 +14716,9 @@ var SelectionGesture = class {
|
|
|
14497
14716
|
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
14498
14717
|
error = null;
|
|
14499
14718
|
pending = Promise.resolve();
|
|
14719
|
+
/** Quick Selection while dragging: the selection before the stroke, how the stroke combines, the pixels. */
|
|
14720
|
+
quick = null;
|
|
14721
|
+
livePreviews = Promise.resolve();
|
|
14500
14722
|
get active() {
|
|
14501
14723
|
return this.start !== null || this.polygon !== null || this.magnetic !== null;
|
|
14502
14724
|
}
|
|
@@ -14504,6 +14726,46 @@ var SelectionGesture = class {
|
|
|
14504
14726
|
get settled() {
|
|
14505
14727
|
return this.pending;
|
|
14506
14728
|
}
|
|
14729
|
+
/** Resolves once no live Quick Selection preview is being computed. */
|
|
14730
|
+
get previewed() {
|
|
14731
|
+
return this.livePreviews;
|
|
14732
|
+
}
|
|
14733
|
+
/** Grows the Quick Selection preview to the stroke so far: one at a time, the newest points following. */
|
|
14734
|
+
growPreview() {
|
|
14735
|
+
const q = this.quick;
|
|
14736
|
+
if (!q) return;
|
|
14737
|
+
if (q.running) {
|
|
14738
|
+
q.dirty = true;
|
|
14739
|
+
return;
|
|
14740
|
+
}
|
|
14741
|
+
q.running = true;
|
|
14742
|
+
this.livePreviews = (async () => {
|
|
14743
|
+
do {
|
|
14744
|
+
q.dirty = false;
|
|
14745
|
+
const stroke = this.points.slice();
|
|
14746
|
+
const options = this.getOptions();
|
|
14747
|
+
try {
|
|
14748
|
+
const pixels = await q.pixels;
|
|
14749
|
+
if (!pixels || this.quick !== q) break;
|
|
14750
|
+
const { mask } = await this.controller.runSelectionJob(
|
|
14751
|
+
{
|
|
14752
|
+
op: "quick",
|
|
14753
|
+
stroke,
|
|
14754
|
+
size: options.quickSize ?? 30,
|
|
14755
|
+
grow: q.mode === "add",
|
|
14756
|
+
enhanceEdge: options.quickEnhance ?? false,
|
|
14757
|
+
selection: q.mode === "add" && q.base?.bounds ? serializeMask(q.base) : null
|
|
14758
|
+
},
|
|
14759
|
+
pixels
|
|
14760
|
+
);
|
|
14761
|
+
if (this.quick === q) this.controller.setSelectionPreview(combineSelections(q.base, mask, q.mode));
|
|
14762
|
+
} catch {
|
|
14763
|
+
break;
|
|
14764
|
+
}
|
|
14765
|
+
} while (q.dirty && this.quick === q);
|
|
14766
|
+
q.running = false;
|
|
14767
|
+
})();
|
|
14768
|
+
}
|
|
14507
14769
|
/** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
|
|
14508
14770
|
get magneticAnchors() {
|
|
14509
14771
|
const m = this.magnetic;
|
|
@@ -14515,7 +14777,8 @@ var SelectionGesture = class {
|
|
|
14515
14777
|
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
14516
14778
|
if (!this.start || !this.current) return null;
|
|
14517
14779
|
const { tool } = this.getOptions();
|
|
14518
|
-
if (tool === "
|
|
14780
|
+
if (tool === "quick") return null;
|
|
14781
|
+
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
14519
14782
|
const rect = normalizeRect({
|
|
14520
14783
|
x: this.start.x,
|
|
14521
14784
|
y: this.start.y,
|
|
@@ -14628,6 +14891,18 @@ var SelectionGesture = class {
|
|
|
14628
14891
|
this.start = point;
|
|
14629
14892
|
this.current = point;
|
|
14630
14893
|
this.points = [point];
|
|
14894
|
+
if (options.tool === "quick") {
|
|
14895
|
+
const picked = selectionModeFromModifiers(modifiers, options.mode);
|
|
14896
|
+
const base = this.controller.getSelection();
|
|
14897
|
+
this.quick = {
|
|
14898
|
+
base,
|
|
14899
|
+
mode: picked === "replace" && base ? "add" : picked,
|
|
14900
|
+
pixels: this.controller.getSamplePixels({ sample: options.quickSampleAll === false ? "active-layer" : "composite" }).catch(() => null),
|
|
14901
|
+
running: false,
|
|
14902
|
+
dirty: false
|
|
14903
|
+
};
|
|
14904
|
+
this.growPreview();
|
|
14905
|
+
}
|
|
14631
14906
|
}
|
|
14632
14907
|
/** Pointer move; returns true when the draft changed (re-render). */
|
|
14633
14908
|
move(point, modifiers = {}) {
|
|
@@ -14654,6 +14929,7 @@ var SelectionGesture = class {
|
|
|
14654
14929
|
const last = this.points[this.points.length - 1];
|
|
14655
14930
|
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
14656
14931
|
this.points.push(point);
|
|
14932
|
+
if (drawn === "quick") this.growPreview();
|
|
14657
14933
|
}
|
|
14658
14934
|
return true;
|
|
14659
14935
|
}
|
|
@@ -14689,7 +14965,8 @@ var SelectionGesture = class {
|
|
|
14689
14965
|
this.start = null;
|
|
14690
14966
|
this.current = null;
|
|
14691
14967
|
this.points = [];
|
|
14692
|
-
const strokeMode = mode === "replace" && this.controller.getSelection() ? "add" : mode;
|
|
14968
|
+
const strokeMode = this.quick?.mode ?? (mode === "replace" && this.controller.getSelection() ? "add" : mode);
|
|
14969
|
+
this.quick = null;
|
|
14693
14970
|
this.pending = this.controller.quickSelect([stroke], {
|
|
14694
14971
|
size: options.quickSize ?? 30,
|
|
14695
14972
|
mode: strokeMode,
|
|
@@ -14700,7 +14977,7 @@ var SelectionGesture = class {
|
|
|
14700
14977
|
(err) => {
|
|
14701
14978
|
this.error = err instanceof Error ? err.message : String(err);
|
|
14702
14979
|
}
|
|
14703
|
-
);
|
|
14980
|
+
).finally(() => this.controller.setSelectionPreview(void 0));
|
|
14704
14981
|
return false;
|
|
14705
14982
|
}
|
|
14706
14983
|
const path = this.draftPath;
|
|
@@ -14829,6 +15106,10 @@ var SelectionGesture = class {
|
|
|
14829
15106
|
}
|
|
14830
15107
|
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
14831
15108
|
cancel() {
|
|
15109
|
+
if (this.quick) {
|
|
15110
|
+
this.quick = null;
|
|
15111
|
+
this.controller.setSelectionPreview(void 0);
|
|
15112
|
+
}
|
|
14832
15113
|
this.start = null;
|
|
14833
15114
|
this.current = null;
|
|
14834
15115
|
this.points = [];
|
|
@@ -16945,6 +17226,19 @@ function createImageShapeTools(controller) {
|
|
|
16945
17226
|
fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." }
|
|
16946
17227
|
};
|
|
16947
17228
|
const LAYER = { type: "string", minLength: 1, description: "The layer (see list_layers). Not a shape layer: its path already is its vector mask." };
|
|
17229
|
+
const PATH_ID = { type: "string", minLength: 1, description: "A document path id from list_paths." };
|
|
17230
|
+
const fromPathId = (args) => {
|
|
17231
|
+
if (args.pathId === void 0) return null;
|
|
17232
|
+
const own = Object.keys(PATH_PROPS).filter((k) => args[k] !== void 0);
|
|
17233
|
+
if (own.length) throw new Error(`Give pathId or ${own.join(" / ")}, not both.`);
|
|
17234
|
+
const path = controller.getPath(String(args.pathId));
|
|
17235
|
+
if (!path) throw new Error(`No path "${String(args.pathId)}". Call list_paths for ids.`);
|
|
17236
|
+
return { components: path.components.map((c) => ({ op: c.op, subpaths: c.subpaths })), ...path.fillRule ? { fillRule: path.fillRule } : {} };
|
|
17237
|
+
};
|
|
17238
|
+
const pathSummary = (id) => {
|
|
17239
|
+
const p = controller.getPath(id);
|
|
17240
|
+
return { id: p.id, name: p.name, work: Boolean(p.work), fillRule: p.fillRule ?? "nonzero", anchors: pathAnchorCount(p), bounds: pathBounds(p) };
|
|
17241
|
+
};
|
|
16948
17242
|
const TRACE_PROPS = {
|
|
16949
17243
|
tolerance: { type: "number", minimum: 0.1, maximum: 10, description: "How far (px) the path may stray from the selection edge (default 0.5; lower is tighter, more anchors)." },
|
|
16950
17244
|
cornerRoundness: { type: "number", minimum: 0, maximum: 1, description: "0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5)." },
|
|
@@ -16976,6 +17270,7 @@ function createImageShapeTools(controller) {
|
|
|
16976
17270
|
type: "object",
|
|
16977
17271
|
$defs: DEFS2,
|
|
16978
17272
|
properties: {
|
|
17273
|
+
pathId: { ...PATH_ID, description: "A document path from list_paths, instead of shape / subpaths / components." },
|
|
16979
17274
|
...PATH_PROPS,
|
|
16980
17275
|
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather radius in px (default 0)." },
|
|
16981
17276
|
antiAlias: { type: "boolean", description: "Soft edges (default true); false makes every pixel fully in or out." },
|
|
@@ -16986,7 +17281,7 @@ function createImageShapeTools(controller) {
|
|
|
16986
17281
|
annotations: { title: "Make a selection from a path", ...WRITE8 },
|
|
16987
17282
|
handler: (args) => {
|
|
16988
17283
|
const mode = args.mode;
|
|
16989
|
-
controller.pathToSelection(pathSpec(args), { feather: args.feather, antiAlias: args.antiAlias, mode });
|
|
17284
|
+
controller.pathToSelection(fromPathId(args) ?? pathSpec(args), { feather: args.feather, antiAlias: args.antiAlias, mode });
|
|
16990
17285
|
const sel = controller.getSelection();
|
|
16991
17286
|
return { empty: !sel, bounds: sel?.bounds ?? null, area: Math.round(sel ? selectionArea(sel) : 0) };
|
|
16992
17287
|
}
|
|
@@ -17008,7 +17303,8 @@ function createImageShapeTools(controller) {
|
|
|
17008
17303
|
$defs: DEFS2,
|
|
17009
17304
|
properties: {
|
|
17010
17305
|
layerId: LAYER,
|
|
17011
|
-
from: { type: "string", enum: ["path", "selection", "reveal-all", "hide-all"], description: "path (default when a shape, subpaths or
|
|
17306
|
+
from: { type: "string", enum: ["path", "selection", "reveal-all", "hide-all"], description: "path (default when a shape, subpaths, components or pathId are given), selection (trace the current selection), reveal-all or hide-all (an empty path)." },
|
|
17307
|
+
pathId: { ...PATH_ID, description: "A document path from list_paths to use as the mask (Paths \u203A Add Vector Mask), instead of shape / subpaths / components." },
|
|
17012
17308
|
...PATH_PROPS,
|
|
17013
17309
|
...TRACE_PROPS
|
|
17014
17310
|
},
|
|
@@ -17018,7 +17314,7 @@ function createImageShapeTools(controller) {
|
|
|
17018
17314
|
annotations: { title: "Add a vector mask", ...WRITE8 },
|
|
17019
17315
|
handler: (args) => {
|
|
17020
17316
|
const id = layerArg(args.layerId);
|
|
17021
|
-
const spec = pathSpec(args);
|
|
17317
|
+
const spec = fromPathId(args) ?? pathSpec(args);
|
|
17022
17318
|
const hasPath = spec.components?.length || spec.shape !== void 0 || spec.subpaths !== void 0;
|
|
17023
17319
|
const from = args.from ?? (hasPath ? "path" : "reveal-all");
|
|
17024
17320
|
if (from !== "selection" && TRACE_KEYS.some((k) => args[k] !== void 0)) throw new Error("tolerance, cornerRoundness and minArea apply only to from selection.");
|
|
@@ -17086,7 +17382,139 @@ function createImageShapeTools(controller) {
|
|
|
17086
17382
|
return { layerId: id, vectorMask: null };
|
|
17087
17383
|
}
|
|
17088
17384
|
};
|
|
17089
|
-
|
|
17385
|
+
const listPaths = {
|
|
17386
|
+
name: "list_paths",
|
|
17387
|
+
title: "List document paths",
|
|
17388
|
+
description: "Paths panel: the document's saved paths and its Work Path (paths kept with the document, on no layer), each with id, name, work (true for the Work Path, which the next make_work_path replaces), fill rule, anchor count and bounds, and which is active. Use the ids with path_to_selection, add_vector_mask, fill_path, stroke_path and the other path tools.",
|
|
17389
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
17390
|
+
annotations: { title: "List document paths", ...READ10 },
|
|
17391
|
+
handler: () => ({ paths: controller.getPaths().map((p) => pathSummary(p.id)), activePathId: controller.getState().activePathId ?? null })
|
|
17392
|
+
};
|
|
17393
|
+
const getPath2 = {
|
|
17394
|
+
name: "get_path",
|
|
17395
|
+
title: "Read a document path",
|
|
17396
|
+
description: "Reads a document path: its summary, svg (compact SVG path data) and components (op, subpaths of anchors with handles, in document pixels), ready to edit and pass back to set_path_geometry.",
|
|
17397
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID }, required: ["pathId"], additionalProperties: false },
|
|
17398
|
+
annotations: { title: "Read a document path", ...READ10 },
|
|
17399
|
+
handler: ({ pathId }) => {
|
|
17400
|
+
const p = controller.getPath(String(pathId));
|
|
17401
|
+
if (!p) throw new Error(`No path "${String(pathId)}". Call list_paths for ids.`);
|
|
17402
|
+
return { path: { ...pathSummary(p.id), svg: pathSvgData(p), components: p.components } };
|
|
17403
|
+
}
|
|
17404
|
+
};
|
|
17405
|
+
const makeWorkPath = {
|
|
17406
|
+
name: "make_work_path",
|
|
17407
|
+
title: "Make a work path from the selection",
|
|
17408
|
+
description: "Paths \u203A Make Work Path: traces the selection's outline (within tolerance px) into the Work Path, replacing the last Work Path unless it was saved (rename_path saves it). Use it to keep a selection as an editable, resolution-independent outline. One undo step. Returns the path.",
|
|
17409
|
+
inputSchema: { type: "object", properties: { ...TRACE_PROPS }, additionalProperties: false },
|
|
17410
|
+
annotations: { title: "Make a work path from the selection", ...WRITE8 },
|
|
17411
|
+
handler: (args) => ({ path: pathSummary(controller.makeWorkPath(traceOptions(args)).id) })
|
|
17412
|
+
};
|
|
17413
|
+
const addPath = {
|
|
17414
|
+
name: "add_path",
|
|
17415
|
+
title: "Add a document path",
|
|
17416
|
+
description: "Adds a saved document path from a live shape, free subpaths or SVG path data, or components (document pixels), as add_shape_layer takes them. One undo step. Returns the path.",
|
|
17417
|
+
inputSchema: {
|
|
17418
|
+
type: "object",
|
|
17419
|
+
$defs: DEFS2,
|
|
17420
|
+
properties: { name: { type: "string", description: 'The path name (default "Path n").' }, ...PATH_PROPS },
|
|
17421
|
+
additionalProperties: false
|
|
17422
|
+
},
|
|
17423
|
+
annotations: { title: "Add a document path", ...WRITE8 },
|
|
17424
|
+
handler: (args) => {
|
|
17425
|
+
const spec = pathSpec(args);
|
|
17426
|
+
if (!spec.components?.length && spec.shape === void 0 && spec.subpaths === void 0) throw new Error("Give the path as shape, subpaths or components.");
|
|
17427
|
+
return { path: pathSummary(controller.addPath({ ...spec, name: args.name }).id) };
|
|
17428
|
+
}
|
|
17429
|
+
};
|
|
17430
|
+
const setPathGeometry = {
|
|
17431
|
+
name: "set_path_geometry",
|
|
17432
|
+
title: "Replace a document path",
|
|
17433
|
+
description: "Replaces a document path's geometry (shape, subpaths or components, document pixels); its name is kept. Read it with get_path first to change part of it. One undo step. Returns the path.",
|
|
17434
|
+
inputSchema: { type: "object", $defs: DEFS2, properties: { pathId: PATH_ID, ...PATH_PROPS }, required: ["pathId"], additionalProperties: false },
|
|
17435
|
+
annotations: { title: "Replace a document path", ...WRITE_IDEMPOTENT5 },
|
|
17436
|
+
handler: (args) => ({ path: pathSummary(controller.setPathGeometry(String(args.pathId), pathSpec(args)).id) })
|
|
17437
|
+
};
|
|
17438
|
+
const renamePath = {
|
|
17439
|
+
name: "rename_path",
|
|
17440
|
+
title: "Rename or save a path",
|
|
17441
|
+
description: "Renames a document path. Renaming the Work Path saves it (Paths \u203A Save Path), so make_work_path no longer replaces it. One undo step. Returns the path.",
|
|
17442
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID, name: { type: "string", minLength: 1, description: "The new name." } }, required: ["pathId", "name"], additionalProperties: false },
|
|
17443
|
+
annotations: { title: "Rename or save a path", ...WRITE_IDEMPOTENT5 },
|
|
17444
|
+
handler: ({ pathId, name }) => ({ path: pathSummary(controller.renamePath(String(pathId), String(name)).id) })
|
|
17445
|
+
};
|
|
17446
|
+
const duplicatePath = {
|
|
17447
|
+
name: "duplicate_path",
|
|
17448
|
+
title: "Duplicate a path",
|
|
17449
|
+
description: 'Duplicates a document path as a saved path (default name "<name> copy"). One undo step. Returns the new path.',
|
|
17450
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID, name: { type: "string", description: "The copy's name." } }, required: ["pathId"], additionalProperties: false },
|
|
17451
|
+
annotations: { title: "Duplicate a path", ...WRITE8 },
|
|
17452
|
+
handler: ({ pathId, name }) => ({ path: pathSummary(controller.duplicatePath(String(pathId), name).id) })
|
|
17453
|
+
};
|
|
17454
|
+
const deletePath = {
|
|
17455
|
+
name: "delete_path",
|
|
17456
|
+
title: "Delete a path",
|
|
17457
|
+
description: "Deletes a document path (undo_image brings it back). Returns the remaining paths.",
|
|
17458
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID }, required: ["pathId"], additionalProperties: false },
|
|
17459
|
+
annotations: { title: "Delete a path", ...DESTRUCTIVE6 },
|
|
17460
|
+
handler: ({ pathId }) => {
|
|
17461
|
+
controller.deletePath(String(pathId));
|
|
17462
|
+
return { paths: controller.getPaths().map((p) => pathSummary(p.id)) };
|
|
17463
|
+
}
|
|
17464
|
+
};
|
|
17465
|
+
const PAINT_PROPS = {
|
|
17466
|
+
layerId: { type: "string", description: "The pixel layer to paint on (default the active layer)." },
|
|
17467
|
+
color: { type: "string", description: "A CSS colour such as #ff0000 (default the foreground colour)." },
|
|
17468
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." }
|
|
17469
|
+
};
|
|
17470
|
+
const fillPath = {
|
|
17471
|
+
name: "fill_path",
|
|
17472
|
+
title: "Fill a path",
|
|
17473
|
+
description: "Paths \u203A Fill Path: fills the area a document path encloses on a pixel layer with a colour. The selection is not changed. One undo step. Returns what was painted.",
|
|
17474
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID, ...PAINT_PROPS }, required: ["pathId"], additionalProperties: false },
|
|
17475
|
+
annotations: { title: "Fill a path", ...WRITE8 },
|
|
17476
|
+
handler: (args) => controller.fillPath(String(args.pathId), { layerId: args.layerId, color: args.color, opacity: args.opacity })
|
|
17477
|
+
};
|
|
17478
|
+
const strokePath = {
|
|
17479
|
+
name: "stroke_path",
|
|
17480
|
+
title: "Stroke a path",
|
|
17481
|
+
description: "Paths \u203A Stroke Path: strokes along a document path's outline on a pixel layer, width px wide and centred on it. The selection is not changed. One undo step. Returns what was painted.",
|
|
17482
|
+
inputSchema: {
|
|
17483
|
+
type: "object",
|
|
17484
|
+
properties: { pathId: PATH_ID, width: { type: "number", exclusiveMinimum: 0, maximum: 250, description: "Stroke width in px." }, ...PAINT_PROPS },
|
|
17485
|
+
required: ["pathId", "width"],
|
|
17486
|
+
additionalProperties: false
|
|
17487
|
+
},
|
|
17488
|
+
annotations: { title: "Stroke a path", ...WRITE8 },
|
|
17489
|
+
handler: (args) => controller.strokePath(String(args.pathId), { width: args.width, layerId: args.layerId, color: args.color, opacity: args.opacity })
|
|
17490
|
+
};
|
|
17491
|
+
return [
|
|
17492
|
+
get,
|
|
17493
|
+
add2,
|
|
17494
|
+
update,
|
|
17495
|
+
setComponent,
|
|
17496
|
+
addComponent,
|
|
17497
|
+
removeComponent,
|
|
17498
|
+
merge,
|
|
17499
|
+
getVectorMask,
|
|
17500
|
+
addVectorMask,
|
|
17501
|
+
setVectorMaskPath,
|
|
17502
|
+
setVectorMaskProps,
|
|
17503
|
+
rasterizeVectorMask,
|
|
17504
|
+
removeVectorMask,
|
|
17505
|
+
selectionToPath,
|
|
17506
|
+
pathToSelection2,
|
|
17507
|
+
listPaths,
|
|
17508
|
+
getPath2,
|
|
17509
|
+
makeWorkPath,
|
|
17510
|
+
addPath,
|
|
17511
|
+
setPathGeometry,
|
|
17512
|
+
renamePath,
|
|
17513
|
+
duplicatePath,
|
|
17514
|
+
deletePath,
|
|
17515
|
+
fillPath,
|
|
17516
|
+
strokePath
|
|
17517
|
+
];
|
|
17090
17518
|
}
|
|
17091
17519
|
|
|
17092
17520
|
// src/image-editor/type-tools.ts
|
|
@@ -17733,6 +18161,9 @@ var SelectAndMaskSession = class {
|
|
|
17733
18161
|
dragStart = null;
|
|
17734
18162
|
pixels = null;
|
|
17735
18163
|
previewPixels = null;
|
|
18164
|
+
/** The layers below the active one, composited (On Layers shows them where nothing is selected). */
|
|
18165
|
+
below = null;
|
|
18166
|
+
belowPreview = null;
|
|
17736
18167
|
base;
|
|
17737
18168
|
previewMask = null;
|
|
17738
18169
|
baseMask = null;
|
|
@@ -17764,10 +18195,24 @@ var SelectAndMaskSession = class {
|
|
|
17764
18195
|
this.emit();
|
|
17765
18196
|
return false;
|
|
17766
18197
|
}
|
|
18198
|
+
this.below = this.layersBelow();
|
|
17767
18199
|
this.rescale();
|
|
17768
18200
|
this.recompute();
|
|
17769
18201
|
return true;
|
|
17770
18202
|
}
|
|
18203
|
+
/** The visible layers under the active one, composited at full size, or null when there are none. */
|
|
18204
|
+
layersBelow() {
|
|
18205
|
+
const active = this.controller.getState().activeLayerId;
|
|
18206
|
+
const flat = flattenLayers(this.controller.getLayers()).filter((l) => l.type !== "group");
|
|
18207
|
+
const at = flat.findIndex((l) => l.id === active);
|
|
18208
|
+
const ids = (at > 0 ? flat.slice(0, at) : []).filter((l) => l.visible).map((l) => l.id);
|
|
18209
|
+
if (!ids.length) return null;
|
|
18210
|
+
try {
|
|
18211
|
+
return this.controller.compositePixels(void 0, { layerIds: ids });
|
|
18212
|
+
} catch {
|
|
18213
|
+
return null;
|
|
18214
|
+
}
|
|
18215
|
+
}
|
|
17771
18216
|
subscribe(listener) {
|
|
17772
18217
|
this.listeners.add(listener);
|
|
17773
18218
|
return () => this.listeners.delete(listener);
|
|
@@ -17800,6 +18245,7 @@ var SelectAndMaskSession = class {
|
|
|
17800
18245
|
const w = Math.max(1, Math.round(px2.width * s));
|
|
17801
18246
|
const h = Math.max(1, Math.round(px2.height * s));
|
|
17802
18247
|
this.previewPixels = scalePixels(px2, w, h);
|
|
18248
|
+
this.belowPreview = this.below ? scalePixels(this.below, w, h) : null;
|
|
17803
18249
|
this.baseMask = this.base?.bounds ? s === 1 ? this.base : resizeSelection(this.base, w, h) : null;
|
|
17804
18250
|
}
|
|
17805
18251
|
/**
|
|
@@ -17923,6 +18369,20 @@ var SelectAndMaskSession = class {
|
|
|
17923
18369
|
}
|
|
17924
18370
|
break;
|
|
17925
18371
|
}
|
|
18372
|
+
case "on-layers": {
|
|
18373
|
+
const under = this.belowPreview?.data;
|
|
18374
|
+
let ur = checker;
|
|
18375
|
+
let ug = checker;
|
|
18376
|
+
let ub = checker;
|
|
18377
|
+
if (under) {
|
|
18378
|
+
const ua = under[p + 3] / 255;
|
|
18379
|
+
ur = under[p] * ua + checker * (1 - ua);
|
|
18380
|
+
ug = under[p + 1] * ua + checker * (1 - ua);
|
|
18381
|
+
ub = under[p + 2] * ua + checker * (1 - ua);
|
|
18382
|
+
}
|
|
18383
|
+
o = [r + (ur - r) * hide, g + (ug - g) * hide, b + (ub - b) * hide];
|
|
18384
|
+
break;
|
|
18385
|
+
}
|
|
17926
18386
|
default:
|
|
17927
18387
|
o = [r + (checker - r) * hide, g + (checker - g) * hide, b + (checker - b) * hide];
|
|
17928
18388
|
}
|
|
@@ -19737,7 +20197,7 @@ var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
|
|
|
19737
20197
|
output: "new-layer",
|
|
19738
20198
|
livePreview: true
|
|
19739
20199
|
};
|
|
19740
|
-
var ContentAwareFillSession = class {
|
|
20200
|
+
var ContentAwareFillSession = class _ContentAwareFillSession {
|
|
19741
20201
|
constructor(controller) {
|
|
19742
20202
|
this.controller = controller;
|
|
19743
20203
|
}
|
|
@@ -19773,16 +20233,21 @@ var ContentAwareFillSession = class {
|
|
|
19773
20233
|
this.revision++;
|
|
19774
20234
|
for (const l of this.listeners) l();
|
|
19775
20235
|
}
|
|
20236
|
+
/** Images up to this many pixels show (and can be sampled) whole in the workspace. */
|
|
20237
|
+
static WHOLE_IMAGE_PIXELS = 4e6;
|
|
20238
|
+
/** Room around the selection the window holds, in px each way. */
|
|
20239
|
+
margin = 0;
|
|
19776
20240
|
/**
|
|
19777
|
-
* Reads the layer
|
|
19778
|
-
*
|
|
20241
|
+
* Reads the layer and makes the first preview. The window is the whole image up to
|
|
20242
|
+
* {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
|
|
20243
|
+
* its larger side, at least 256 px, each way), and {@link expandView} widens it.
|
|
19779
20244
|
*/
|
|
19780
20245
|
async open() {
|
|
19781
20246
|
try {
|
|
19782
|
-
const
|
|
19783
|
-
const b =
|
|
19784
|
-
|
|
19785
|
-
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
|
|
20247
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20248
|
+
const b = this.controller.getSelection()?.bounds;
|
|
20249
|
+
this.margin = width * height <= _ContentAwareFillSession.WHOLE_IMAGE_PIXELS ? Math.max(width, height) : Math.max(256, 2 * Math.max(b?.width ?? 0, b?.height ?? 0));
|
|
20250
|
+
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: this.margin });
|
|
19786
20251
|
} catch (err) {
|
|
19787
20252
|
this.error = err instanceof Error ? err.message : String(err);
|
|
19788
20253
|
this.emit();
|
|
@@ -19796,6 +20261,42 @@ var ContentAwareFillSession = class {
|
|
|
19796
20261
|
await this.refresh();
|
|
19797
20262
|
return true;
|
|
19798
20263
|
}
|
|
20264
|
+
/** Whether the window shows the whole image (else {@link expandView} can widen it). */
|
|
20265
|
+
get showsWholeImage() {
|
|
20266
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20267
|
+
const w = this.win;
|
|
20268
|
+
return !w || w.x === 0 && w.y === 0 && w.width >= width && w.height >= height;
|
|
20269
|
+
}
|
|
20270
|
+
/**
|
|
20271
|
+
* Doubles the room around the selection (up to the whole image), keeping what was painted; a
|
|
20272
|
+
* preset fills the new room as it would. Returns false when the whole image already shows.
|
|
20273
|
+
*/
|
|
20274
|
+
expandView() {
|
|
20275
|
+
if (!this.win || this.showsWholeImage) return false;
|
|
20276
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20277
|
+
const old = { win: this.win, area: this.area };
|
|
20278
|
+
this.margin = Math.min(Math.max(width, height), this.margin * 2);
|
|
20279
|
+
const plan = this.controller.prepareContentAwareFill({ layerId: this.plan.layerId, sampleRadius: this.margin });
|
|
20280
|
+
this.plan = plan;
|
|
20281
|
+
this.win = plan.window;
|
|
20282
|
+
this.before = plan.before;
|
|
20283
|
+
this.hole = plan.hole;
|
|
20284
|
+
const mode = this.settingsValue.sampling;
|
|
20285
|
+
this.area = null;
|
|
20286
|
+
this.applySampling(mode === "custom" ? "auto" : mode);
|
|
20287
|
+
if (mode === "custom") {
|
|
20288
|
+
const area = new Uint8Array(plan.window.width * plan.window.height);
|
|
20289
|
+
for (let y = 0; y < old.win.height; y++) {
|
|
20290
|
+
const src = y * old.win.width;
|
|
20291
|
+
const dst = (old.win.y - plan.window.y + y) * plan.window.width + (old.win.x - plan.window.x);
|
|
20292
|
+
area.set(old.area.subarray(src, src + old.win.width), dst);
|
|
20293
|
+
}
|
|
20294
|
+
this.area = area;
|
|
20295
|
+
}
|
|
20296
|
+
this.emit();
|
|
20297
|
+
if (this.settingsValue.livePreview) void this.refresh();
|
|
20298
|
+
return true;
|
|
20299
|
+
}
|
|
19799
20300
|
/** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
|
|
19800
20301
|
applySampling(mode) {
|
|
19801
20302
|
const win = this.win;
|
|
@@ -20250,6 +20751,7 @@ export {
|
|
|
20250
20751
|
WGSL_SHADE,
|
|
20251
20752
|
WGSL_STEP,
|
|
20252
20753
|
WORKING_SPACE_OPTIONS,
|
|
20754
|
+
WORK_PATH_NAME,
|
|
20253
20755
|
WORK_SIZE,
|
|
20254
20756
|
WetGpuEngine,
|
|
20255
20757
|
WetMedium,
|
|
@@ -20715,6 +21217,7 @@ export {
|
|
|
20715
21217
|
newEffectId,
|
|
20716
21218
|
newLayerId,
|
|
20717
21219
|
newLayerOptions,
|
|
21220
|
+
newPathId,
|
|
20718
21221
|
normalizeAdjustmentLayer,
|
|
20719
21222
|
normalizeAdjustmentLayers,
|
|
20720
21223
|
normalizeAngle,
|
|
@@ -20771,8 +21274,11 @@ export {
|
|
|
20771
21274
|
parseVersionedDescriptor,
|
|
20772
21275
|
patchParagraphStyle,
|
|
20773
21276
|
patchTextStyle,
|
|
21277
|
+
pathAnchorCount,
|
|
21278
|
+
pathBounds,
|
|
20774
21279
|
pathStrokeCoverage,
|
|
20775
21280
|
pathStrokeDabs,
|
|
21281
|
+
pathSvgData,
|
|
20776
21282
|
pathToSelection,
|
|
20777
21283
|
perDabStrength,
|
|
20778
21284
|
photoFilterMatrix,
|
|
@@ -20825,9 +21331,11 @@ export {
|
|
|
20825
21331
|
readAdjustmentControls,
|
|
20826
21332
|
readDeepRegion,
|
|
20827
21333
|
readPaintSource,
|
|
21334
|
+
readPaths,
|
|
20828
21335
|
readPixel,
|
|
20829
21336
|
readPsd,
|
|
20830
21337
|
readRegion,
|
|
21338
|
+
readShapeComponents,
|
|
20831
21339
|
readSourceRegion,
|
|
20832
21340
|
rectSelection,
|
|
20833
21341
|
rectToQuad,
|
|
@@ -20916,6 +21424,7 @@ export {
|
|
|
20916
21424
|
serializeLayers,
|
|
20917
21425
|
serializeLayersExternal,
|
|
20918
21426
|
serializeMask,
|
|
21427
|
+
serializePaths,
|
|
20919
21428
|
serializePatterns,
|
|
20920
21429
|
serializeSelection,
|
|
20921
21430
|
setCloneTransformValue,
|