@pixerate/editor 0.209.0 → 0.211.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-X4LUGTQX.cjs → chunk-L52VMQZS.cjs} +21 -10
- package/dist/{chunk-DWYQPL2A.js → chunk-LQ5SALHK.js} +13 -2
- package/dist/chunk-MQDIP4CF.cjs +462 -0
- package/dist/{chunk-KFIOEOCQ.js → chunk-W3TOZTL7.js} +55 -6
- package/dist/chunk-XI2NV5U7.js +462 -0
- package/dist/{chunk-3EHPLDFQ.cjs → chunk-YF3YZZ6G.cjs} +16 -453
- package/dist/{chunk-USQCKRZG.js → chunk-ZGJGSCUW.js} +8 -445
- package/dist/image-editor/content-aware-fill-session.d.cts +109 -0
- package/dist/image-editor/content-aware-fill-session.d.ts +109 -0
- package/dist/image-editor/controller.d.cts +111 -1
- package/dist/image-editor/controller.d.ts +111 -1
- package/dist/image-editor/index.cjs +1879 -1052
- package/dist/image-editor/index.d.cts +2 -0
- package/dist/image-editor/index.d.ts +2 -0
- package/dist/image-editor/index.js +869 -42
- 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/selection/worker.d.cts +10 -0
- package/dist/image-editor/selection/worker.d.ts +10 -0
- package/dist/image-editor/selection-worker.cjs +4 -3
- package/dist/image-editor/selection-worker.js +2 -1
- package/dist/image-editor/types.d.cts +12 -0
- package/dist/image-editor/types.d.ts +12 -0
- package/dist/paint/heal.d.cts +3 -1
- package/dist/paint/heal.d.ts +3 -1
- package/dist/paint/index.cjs +4 -2
- package/dist/paint/index.js +10 -8
- 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 +20 -22
- package/skills/pixerate-image-editor/references/layer-effects.md +24 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +2 -1
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
workImage,
|
|
53
53
|
workLuma,
|
|
54
54
|
workPlanes
|
|
55
|
-
} from "../chunk-
|
|
55
|
+
} from "../chunk-LQ5SALHK.js";
|
|
56
56
|
import {
|
|
57
57
|
ADJUSTMENT_KEYS,
|
|
58
58
|
ADJUSTMENT_KINDS,
|
|
@@ -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";
|
|
@@ -523,33 +524,35 @@ import {
|
|
|
523
524
|
TransformedPixelSource,
|
|
524
525
|
applyRetouchToTile,
|
|
525
526
|
applyToneToDeepTile,
|
|
526
|
-
bestOffset,
|
|
527
527
|
boxBlurPixels,
|
|
528
528
|
cloneSamplePoint,
|
|
529
|
-
contentAwareFill,
|
|
530
|
-
dilateMask,
|
|
531
529
|
dodgeBurn,
|
|
532
530
|
dodgeBurnCurve,
|
|
533
531
|
healPixels,
|
|
534
532
|
isIdentityCloneTransform,
|
|
535
533
|
luma,
|
|
536
|
-
maskBounds,
|
|
537
|
-
membraneFill,
|
|
538
534
|
normalizeRetouch,
|
|
539
535
|
perDabStrength,
|
|
540
536
|
readSourceRegion,
|
|
541
537
|
retouchFilter,
|
|
542
538
|
rgbaViewOfGrey,
|
|
543
539
|
saturationOf,
|
|
544
|
-
seamlessClone,
|
|
545
540
|
smudgeEffect,
|
|
546
|
-
solveMembrane,
|
|
547
541
|
sponge,
|
|
548
542
|
spongeEffect,
|
|
549
543
|
spotHealPixels,
|
|
550
544
|
spotHealWindow,
|
|
551
545
|
toneOf
|
|
552
|
-
} from "../chunk-
|
|
546
|
+
} from "../chunk-ZGJGSCUW.js";
|
|
547
|
+
import {
|
|
548
|
+
bestOffset,
|
|
549
|
+
contentAwareFill,
|
|
550
|
+
dilateMask,
|
|
551
|
+
maskBounds,
|
|
552
|
+
membraneFill,
|
|
553
|
+
seamlessClone,
|
|
554
|
+
solveMembrane
|
|
555
|
+
} from "../chunk-XI2NV5U7.js";
|
|
553
556
|
import {
|
|
554
557
|
BODY_SCATTER,
|
|
555
558
|
DEFAULT_FLOW,
|
|
@@ -1475,8 +1478,9 @@ var ImageEditorRenderer = class {
|
|
|
1475
1478
|
ctx.restore();
|
|
1476
1479
|
}
|
|
1477
1480
|
const { width: iw, height: ih } = state.imageDimensions;
|
|
1478
|
-
const
|
|
1479
|
-
|
|
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) {
|
|
1480
1484
|
ctx.save();
|
|
1481
1485
|
try {
|
|
1482
1486
|
applyImageProjectionToContext(ctx, projection);
|
|
@@ -1484,8 +1488,8 @@ var ImageEditorRenderer = class {
|
|
|
1484
1488
|
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1485
1489
|
ctx.clip();
|
|
1486
1490
|
if (quickMaskSel) this.drawQuickMask(ctx, quickMaskSel);
|
|
1487
|
-
else if (
|
|
1488
|
-
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);
|
|
1489
1493
|
if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
|
|
1490
1494
|
} catch {
|
|
1491
1495
|
}
|
|
@@ -2859,6 +2863,58 @@ function magneticAnchorSpacing(frequency = 57) {
|
|
|
2859
2863
|
return 8 + (100 - f);
|
|
2860
2864
|
}
|
|
2861
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
|
+
|
|
2862
2918
|
// src/image-editor/layers/shape-type-ui.ts
|
|
2863
2919
|
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
2864
2920
|
kind: "rect",
|
|
@@ -5641,6 +5697,7 @@ var ImageEditorController = class {
|
|
|
5641
5697
|
outputOpacity: s.outputOpacity,
|
|
5642
5698
|
selection: s.selection ?? null,
|
|
5643
5699
|
...s.channels?.length ? { channels: s.channels } : {},
|
|
5700
|
+
...s.paths?.length ? { paths: s.paths } : {},
|
|
5644
5701
|
source: {
|
|
5645
5702
|
sourceUrl: s.sourceUrl,
|
|
5646
5703
|
element: this.imageElement,
|
|
@@ -5678,6 +5735,9 @@ var ImageEditorController = class {
|
|
|
5678
5735
|
s.selection = entry.selection;
|
|
5679
5736
|
if (entry.channels?.length) s.channels = entry.channels;
|
|
5680
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;
|
|
5681
5741
|
if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
|
|
5682
5742
|
this.imageElement = entry.source.element;
|
|
5683
5743
|
s.sourceUrl = entry.source.sourceUrl;
|
|
@@ -6489,6 +6549,138 @@ var ImageEditorController = class {
|
|
|
6489
6549
|
const mask = pathToSelection(components, width, height, { ...options, fillRule: path.fillRule ?? options.fillRule });
|
|
6490
6550
|
return this.setSelection(mask, options.mode ?? "replace");
|
|
6491
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
|
+
}
|
|
6492
6684
|
/** Replaces a vector mask's path (its properties are kept). One undo step. Returns the vector mask. */
|
|
6493
6685
|
setVectorMaskPath(id, path) {
|
|
6494
6686
|
const { vm } = this.requireVectorMask(id);
|
|
@@ -7903,9 +8095,46 @@ var ImageEditorController = class {
|
|
|
7903
8095
|
* (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
|
|
7904
8096
|
* coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
|
|
7905
8097
|
* above it. Samples within `sampleRadius` px of the selection (default its larger side, at
|
|
7906
|
-
* least 32). Deterministic for a `seed`. One undo step.
|
|
8098
|
+
* least 32), or from the given sampling area. Deterministic for a `seed`. One undo step.
|
|
8099
|
+
* 8-bit documents only for now. Runs here; see `contentAwareFillAsync` for the worker.
|
|
7907
8100
|
*/
|
|
7908
8101
|
contentAwareFill(options = {}) {
|
|
8102
|
+
const plan = this.prepareContentAwareFill(options);
|
|
8103
|
+
const filled = contentAwareFill(plan.window.width, plan.window.height, 4, plan.before, plan.hole, { seed: options.seed ?? 0, exclude: plan.exclude });
|
|
8104
|
+
return this.applyContentAwareFill(plan, Uint8ClampedArray.from(filled), options.output ?? "current");
|
|
8105
|
+
}
|
|
8106
|
+
/**
|
|
8107
|
+
* `contentAwareFill` with the synthesis in the selection worker when there is one, with
|
|
8108
|
+
* `signal` and `onProgress`. The layer is checked again before the result lands.
|
|
8109
|
+
*/
|
|
8110
|
+
async contentAwareFillAsync(options = {}) {
|
|
8111
|
+
const plan = this.prepareContentAwareFill(options);
|
|
8112
|
+
const filled = await this.synthesiseFill(plan, options.seed ?? 0, options);
|
|
8113
|
+
return this.applyContentAwareFill(plan, filled, options.output ?? "current");
|
|
8114
|
+
}
|
|
8115
|
+
/**
|
|
8116
|
+
* The fill without changing anything: the window, its pixels before and after (RGBA, the fill
|
|
8117
|
+
* blended by the selection's coverage) and the sampling area used, for a preview.
|
|
8118
|
+
*/
|
|
8119
|
+
async previewContentAwareFill(options = {}) {
|
|
8120
|
+
const plan = this.prepareContentAwareFill(options);
|
|
8121
|
+
const filled = await this.synthesiseFill(plan, options.seed ?? 0, options);
|
|
8122
|
+
return { plan, after: this.blendFill(plan, filled, "current") };
|
|
8123
|
+
}
|
|
8124
|
+
async synthesiseFill(plan, seed, options) {
|
|
8125
|
+
const { width, height } = plan.window;
|
|
8126
|
+
const { pixels } = await this.runSelectionJob(
|
|
8127
|
+
{ op: "fill", hole: plan.hole, exclude: plan.exclude, seed },
|
|
8128
|
+
{ width, height, data: plan.before },
|
|
8129
|
+
options
|
|
8130
|
+
);
|
|
8131
|
+
return pixels;
|
|
8132
|
+
}
|
|
8133
|
+
/**
|
|
8134
|
+
* Checks and gathers what a Content-Aware Fill needs: the selection, the 8-bit pixel layer, the
|
|
8135
|
+
* window around the selection (and the sampling area), and which pixels it may copy from.
|
|
8136
|
+
*/
|
|
8137
|
+
prepareContentAwareFill(options = {}) {
|
|
7909
8138
|
const sel = this.state.selection;
|
|
7910
8139
|
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
|
|
7911
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.");
|
|
@@ -7914,33 +8143,70 @@ var ImageEditorController = class {
|
|
|
7914
8143
|
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
7915
8144
|
const { width, height } = this.naturalSize;
|
|
7916
8145
|
const b = sel.bounds;
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
8146
|
+
let area = options.samplingArea ?? null;
|
|
8147
|
+
if (!area && options.samplingChannelId) area = this.requireChannel(options.samplingChannelId).mask;
|
|
8148
|
+
if (!area && options.sampleRects?.length) {
|
|
8149
|
+
for (const r of options.sampleRects) area = combineSelections(area, rectSelection(width, height, r, { antiAlias: false }), "add");
|
|
8150
|
+
}
|
|
8151
|
+
if (area && !area.bounds) throw new LayerOperationError("The sampling area is empty; paint or give an area to sample from.");
|
|
8152
|
+
let win;
|
|
8153
|
+
if (area?.bounds) {
|
|
8154
|
+
const a = area.bounds;
|
|
8155
|
+
const x0 = Math.max(0, Math.min(b.x - 8, a.x));
|
|
8156
|
+
const y0 = Math.max(0, Math.min(b.y - 8, a.y));
|
|
8157
|
+
win = { x: x0, y: y0, width: Math.min(width, Math.max(b.x + b.width + 8, a.x + a.width)) - x0, height: Math.min(height, Math.max(b.y + b.height + 8, a.y + a.height)) - y0 };
|
|
8158
|
+
} else {
|
|
8159
|
+
const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
|
|
8160
|
+
const x0 = Math.max(0, b.x - margin);
|
|
8161
|
+
const y0 = Math.max(0, b.y - margin);
|
|
8162
|
+
win = { x: x0, y: y0, width: Math.min(width, b.x + b.width + margin) - x0, height: Math.min(height, b.y + b.height + margin) - y0 };
|
|
8163
|
+
}
|
|
8164
|
+
if (win.width * win.height > 16e6) throw new LayerOperationError("The area to fill and sample is too large (over 16 MP); fill a smaller selection or sample from a smaller area.");
|
|
8165
|
+
const before = readRegion(target.surface, { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height });
|
|
8166
|
+
const n = win.width * win.height;
|
|
8167
|
+
const coverage = new Uint8Array(n);
|
|
8168
|
+
const hole = new Uint8Array(n);
|
|
8169
|
+
const selData = sel.data;
|
|
7928
8170
|
for (let y = 0; y < win.height; y++) {
|
|
7929
8171
|
for (let x = 0; x < win.width; x++) {
|
|
7930
|
-
const c =
|
|
8172
|
+
const c = selData[(win.y + y) * sel.width + win.x + x];
|
|
7931
8173
|
coverage[y * win.width + x] = c;
|
|
7932
8174
|
hole[y * win.width + x] = c > 0 ? 1 : 0;
|
|
7933
8175
|
}
|
|
7934
8176
|
}
|
|
7935
|
-
|
|
8177
|
+
let exclude = null;
|
|
8178
|
+
if (area || options.excludeRects?.length) {
|
|
8179
|
+
exclude = new Uint8Array(n);
|
|
8180
|
+
if (area) {
|
|
8181
|
+
const ad = area.data;
|
|
8182
|
+
for (let y = 0; y < win.height; y++) for (let x = 0; x < win.width; x++) if (ad[(win.y + y) * width + win.x + x] < 128) exclude[y * win.width + x] = 1;
|
|
8183
|
+
}
|
|
8184
|
+
for (const r of options.excludeRects ?? []) {
|
|
8185
|
+
for (let y = Math.max(0, Math.floor(r.y) - win.y); y < Math.min(win.height, Math.ceil(r.y + r.height) - win.y); y++) {
|
|
8186
|
+
for (let x = Math.max(0, Math.floor(r.x) - win.x); x < Math.min(win.width, Math.ceil(r.x + r.width) - win.x); x++) exclude[y * win.width + x] = 1;
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
8189
|
+
}
|
|
8190
|
+
return { layerId: target.id, window: win, bounds: { ...b }, before, hole, coverage, exclude };
|
|
8191
|
+
}
|
|
8192
|
+
/** The window's pixels with `filled` blended in by the coverage (`current`), or the fill alone, its alpha by the coverage (`new-layer`). */
|
|
8193
|
+
blendFill(plan, filled, output) {
|
|
8194
|
+
const { before, coverage } = plan;
|
|
7936
8195
|
const out = new Uint8ClampedArray(before.length);
|
|
7937
8196
|
for (let i = 0; i < coverage.length; i++) {
|
|
7938
|
-
const k = coverage[i];
|
|
8197
|
+
const k = coverage[i] / 255;
|
|
7939
8198
|
for (let c = 0; c < 4; c++) {
|
|
7940
8199
|
const p = i * 4 + c;
|
|
7941
8200
|
out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
|
|
7942
8201
|
}
|
|
7943
8202
|
}
|
|
8203
|
+
return out;
|
|
8204
|
+
}
|
|
8205
|
+
/** Puts a computed fill into the document as one undo step. */
|
|
8206
|
+
applyContentAwareFill(plan, filled, output) {
|
|
8207
|
+
const target = this.paintTarget(plan.layerId, "pixels", "fill");
|
|
8208
|
+
const { window: win, bounds: b } = plan;
|
|
8209
|
+
const out = this.blendFill(plan, filled, output);
|
|
7944
8210
|
if (output === "new-layer") {
|
|
7945
8211
|
const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
|
|
7946
8212
|
const data = new Uint8ClampedArray(b.width * b.height * 4);
|
|
@@ -7948,6 +8214,7 @@ var ImageEditorController = class {
|
|
|
7948
8214
|
const id = this.addPixelLayer({ name: "Content-Aware Fill", pixels: { width: b.width, height: b.height, data }, x: b.x, y: b.y, aboveId: target.id });
|
|
7949
8215
|
return { layerId: id, bounds: { ...b }, output };
|
|
7950
8216
|
}
|
|
8217
|
+
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
7951
8218
|
const surface = writeRegion(target.surface, layerRect, out);
|
|
7952
8219
|
this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
7953
8220
|
return { layerId: target.id, bounds: { ...b }, output };
|
|
@@ -8434,6 +8701,17 @@ var ImageEditorController = class {
|
|
|
8434
8701
|
this.notify();
|
|
8435
8702
|
return true;
|
|
8436
8703
|
}
|
|
8704
|
+
/**
|
|
8705
|
+
* Shows `mask` in place of the selection while a gesture runs (Quick Selection as the brush
|
|
8706
|
+
* moves; `null` shows nothing selected), or with `undefined` stops. View state only: no undo
|
|
8707
|
+
* step, not saved.
|
|
8708
|
+
*/
|
|
8709
|
+
setSelectionPreview(mask) {
|
|
8710
|
+
if (this.state.selectionPreview === mask) return;
|
|
8711
|
+
if (mask === void 0) delete this.state.selectionPreview;
|
|
8712
|
+
else this.state.selectionPreview = mask;
|
|
8713
|
+
this.notify();
|
|
8714
|
+
}
|
|
8437
8715
|
/** Select › All. */
|
|
8438
8716
|
selectAll() {
|
|
8439
8717
|
const { width, height } = this.naturalSize;
|
|
@@ -10405,6 +10683,10 @@ var ImageEditorController = class {
|
|
|
10405
10683
|
}));
|
|
10406
10684
|
if (psdChannels.length) s.channels = psdChannels;
|
|
10407
10685
|
else delete s.channels;
|
|
10686
|
+
const psdPaths = (result.paths ?? []).map((p) => ({ id: newPathId(), name: p.name, components: p.components, ...p.work ? { work: true } : {} }));
|
|
10687
|
+
if (psdPaths.length) s.paths = psdPaths;
|
|
10688
|
+
else delete s.paths;
|
|
10689
|
+
s.activePathId = null;
|
|
10408
10690
|
s.selectedAnnotationId = null;
|
|
10409
10691
|
s.activeLayerId = defaultActiveLayerId(s.layers);
|
|
10410
10692
|
s.activeTarget = "pixels";
|
|
@@ -10602,7 +10884,7 @@ var ImageEditorController = class {
|
|
|
10602
10884
|
opacity: c.opacity,
|
|
10603
10885
|
indicates: c.indicates
|
|
10604
10886
|
}));
|
|
10605
|
-
const result = exportPsd({ width, height, layers: prepared, merged, mergedDeep, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight, depth, alphaChannels, ...color });
|
|
10887
|
+
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 });
|
|
10606
10888
|
warnings.push(...result.warnings);
|
|
10607
10889
|
return { bytes: result.bytes, warnings, psb: result.psb, width, height };
|
|
10608
10890
|
}
|
|
@@ -10676,6 +10958,7 @@ var ImageEditorController = class {
|
|
|
10676
10958
|
out = this.serializeState();
|
|
10677
10959
|
if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
|
|
10678
10960
|
if (this.state.channels?.length) out.channels = serializeChannels(this.state.channels);
|
|
10961
|
+
if (this.state.paths?.length) out.paths = serializePaths(this.state.paths);
|
|
10679
10962
|
} finally {
|
|
10680
10963
|
if (root) {
|
|
10681
10964
|
this.state = current;
|
|
@@ -10720,6 +11003,10 @@ var ImageEditorController = class {
|
|
|
10720
11003
|
const channels = deserializeChannels(data.channels, this.naturalSize, problems);
|
|
10721
11004
|
if (channels.length) this.state.channels = channels;
|
|
10722
11005
|
else delete this.state.channels;
|
|
11006
|
+
const paths = readPaths(data.paths, problems);
|
|
11007
|
+
if (paths.length) this.state.paths = paths;
|
|
11008
|
+
else delete this.state.paths;
|
|
11009
|
+
this.state.activePathId = null;
|
|
10723
11010
|
if (problems.length) this.lastLoadProblems = [...this.lastLoadProblems, ...problems];
|
|
10724
11011
|
this.notify();
|
|
10725
11012
|
}
|
|
@@ -11696,7 +11983,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
11696
11983
|
// src/image-editor/playback.ts
|
|
11697
11984
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
11698
11985
|
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)$/;
|
|
11699
|
-
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)$/;
|
|
11986
|
+
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)$/;
|
|
11700
11987
|
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)$/;
|
|
11701
11988
|
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
11702
11989
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -11924,6 +12211,13 @@ var rectProperties = {
|
|
|
11924
12211
|
width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
|
|
11925
12212
|
height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
|
|
11926
12213
|
};
|
|
12214
|
+
var rectSchemaStrict = {
|
|
12215
|
+
type: "object",
|
|
12216
|
+
properties: rectProperties,
|
|
12217
|
+
required: ["x", "y", "width", "height"],
|
|
12218
|
+
additionalProperties: false,
|
|
12219
|
+
description: "A rectangle in natural image pixels."
|
|
12220
|
+
};
|
|
11927
12221
|
function shapeOptions(args) {
|
|
11928
12222
|
return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
|
|
11929
12223
|
}
|
|
@@ -12777,25 +13071,33 @@ function createImageSelectionTools(controller) {
|
|
|
12777
13071
|
const fillTool = {
|
|
12778
13072
|
name: "content_aware_fill",
|
|
12779
13073
|
title: "Content-Aware Fill",
|
|
12780
|
-
description: "Edit \u203A Content-Aware Fill: removes what is selected by filling it with texture and edges synthesised from around it (on a pixel layer, default the active one). Select the object with a small margin (expand 2\u20134 px) first. output current paints into the layer; new-layer puts the fill on its own layer above it, so it can be masked or removed (recommended). sampleRadius limits where it samples from. Deterministic for a seed. 8-bit documents. One undo step. Returns the layer id and filled bounds.",
|
|
13074
|
+
description: "Edit \u203A Content-Aware Fill: removes what is selected by filling it with texture and edges synthesised from around it (on a pixel layer, default the active one). Select the object with a small margin (expand 2\u20134 px) first. output current paints into the layer; new-layer puts the fill on its own layer above it, so it can be masked or removed (recommended). sampleRadius limits where it samples from; sampleRects (only these areas), samplingChannelId (a saved alpha channel) and excludeRects (never these, e.g. another object that would be copied in) set the sampling area as the Content-Aware Fill workspace does. Deterministic for a seed. 8-bit documents. One undo step. Returns the layer id and filled bounds.",
|
|
12781
13075
|
inputSchema: {
|
|
12782
13076
|
type: "object",
|
|
12783
13077
|
properties: {
|
|
12784
13078
|
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
|
|
12785
13079
|
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
12786
13080
|
seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
|
|
13081
|
+
sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
|
|
13082
|
+
excludeRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Never sample from these rectangles (natural image pixels)." },
|
|
13083
|
+
samplingChannelId: { type: "string", description: "Sample only where this alpha channel is selected (see list_channels)." },
|
|
12787
13084
|
layerId: layerIdSchema
|
|
12788
13085
|
},
|
|
12789
13086
|
additionalProperties: false
|
|
12790
13087
|
},
|
|
12791
13088
|
annotations: writeAnnotations("Content-Aware Fill"),
|
|
12792
|
-
handler: (args) => {
|
|
13089
|
+
handler: async (args, context) => {
|
|
12793
13090
|
if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
|
|
12794
|
-
|
|
13091
|
+
const rects = (v) => Array.isArray(v) ? v.map((r) => ({ x: num2(r?.x, NaN), y: num2(r?.y, NaN), width: num2(r?.width, NaN), height: num2(r?.height, NaN) })) : void 0;
|
|
13092
|
+
return controller.contentAwareFillAsync({
|
|
12795
13093
|
output: args.output,
|
|
12796
13094
|
sampleRadius: args.sampleRadius,
|
|
12797
13095
|
seed: args.seed,
|
|
12798
|
-
layerId: requireLayerId(args.layerId)
|
|
13096
|
+
layerId: requireLayerId(args.layerId),
|
|
13097
|
+
sampleRects: rects(args.sampleRects),
|
|
13098
|
+
excludeRects: rects(args.excludeRects),
|
|
13099
|
+
samplingChannelId: typeof args.samplingChannelId === "string" ? args.samplingChannelId : void 0,
|
|
13100
|
+
signal: context?.signal
|
|
12799
13101
|
});
|
|
12800
13102
|
}
|
|
12801
13103
|
};
|
|
@@ -14405,6 +14707,9 @@ var SelectionGesture = class {
|
|
|
14405
14707
|
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
14406
14708
|
error = null;
|
|
14407
14709
|
pending = Promise.resolve();
|
|
14710
|
+
/** Quick Selection while dragging: the selection before the stroke, how the stroke combines, the pixels. */
|
|
14711
|
+
quick = null;
|
|
14712
|
+
livePreviews = Promise.resolve();
|
|
14408
14713
|
get active() {
|
|
14409
14714
|
return this.start !== null || this.polygon !== null || this.magnetic !== null;
|
|
14410
14715
|
}
|
|
@@ -14412,6 +14717,46 @@ var SelectionGesture = class {
|
|
|
14412
14717
|
get settled() {
|
|
14413
14718
|
return this.pending;
|
|
14414
14719
|
}
|
|
14720
|
+
/** Resolves once no live Quick Selection preview is being computed. */
|
|
14721
|
+
get previewed() {
|
|
14722
|
+
return this.livePreviews;
|
|
14723
|
+
}
|
|
14724
|
+
/** Grows the Quick Selection preview to the stroke so far: one at a time, the newest points following. */
|
|
14725
|
+
growPreview() {
|
|
14726
|
+
const q = this.quick;
|
|
14727
|
+
if (!q) return;
|
|
14728
|
+
if (q.running) {
|
|
14729
|
+
q.dirty = true;
|
|
14730
|
+
return;
|
|
14731
|
+
}
|
|
14732
|
+
q.running = true;
|
|
14733
|
+
this.livePreviews = (async () => {
|
|
14734
|
+
do {
|
|
14735
|
+
q.dirty = false;
|
|
14736
|
+
const stroke = this.points.slice();
|
|
14737
|
+
const options = this.getOptions();
|
|
14738
|
+
try {
|
|
14739
|
+
const pixels = await q.pixels;
|
|
14740
|
+
if (!pixels || this.quick !== q) break;
|
|
14741
|
+
const { mask } = await this.controller.runSelectionJob(
|
|
14742
|
+
{
|
|
14743
|
+
op: "quick",
|
|
14744
|
+
stroke,
|
|
14745
|
+
size: options.quickSize ?? 30,
|
|
14746
|
+
grow: q.mode === "add",
|
|
14747
|
+
enhanceEdge: options.quickEnhance ?? false,
|
|
14748
|
+
selection: q.mode === "add" && q.base?.bounds ? serializeMask(q.base) : null
|
|
14749
|
+
},
|
|
14750
|
+
pixels
|
|
14751
|
+
);
|
|
14752
|
+
if (this.quick === q) this.controller.setSelectionPreview(combineSelections(q.base, mask, q.mode));
|
|
14753
|
+
} catch {
|
|
14754
|
+
break;
|
|
14755
|
+
}
|
|
14756
|
+
} while (q.dirty && this.quick === q);
|
|
14757
|
+
q.running = false;
|
|
14758
|
+
})();
|
|
14759
|
+
}
|
|
14415
14760
|
/** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
|
|
14416
14761
|
get magneticAnchors() {
|
|
14417
14762
|
const m = this.magnetic;
|
|
@@ -14423,7 +14768,8 @@ var SelectionGesture = class {
|
|
|
14423
14768
|
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
14424
14769
|
if (!this.start || !this.current) return null;
|
|
14425
14770
|
const { tool } = this.getOptions();
|
|
14426
|
-
if (tool === "
|
|
14771
|
+
if (tool === "quick") return null;
|
|
14772
|
+
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
14427
14773
|
const rect = normalizeRect({
|
|
14428
14774
|
x: this.start.x,
|
|
14429
14775
|
y: this.start.y,
|
|
@@ -14536,6 +14882,18 @@ var SelectionGesture = class {
|
|
|
14536
14882
|
this.start = point;
|
|
14537
14883
|
this.current = point;
|
|
14538
14884
|
this.points = [point];
|
|
14885
|
+
if (options.tool === "quick") {
|
|
14886
|
+
const picked = selectionModeFromModifiers(modifiers, options.mode);
|
|
14887
|
+
const base = this.controller.getSelection();
|
|
14888
|
+
this.quick = {
|
|
14889
|
+
base,
|
|
14890
|
+
mode: picked === "replace" && base ? "add" : picked,
|
|
14891
|
+
pixels: this.controller.getSamplePixels({ sample: options.quickSampleAll === false ? "active-layer" : "composite" }).catch(() => null),
|
|
14892
|
+
running: false,
|
|
14893
|
+
dirty: false
|
|
14894
|
+
};
|
|
14895
|
+
this.growPreview();
|
|
14896
|
+
}
|
|
14539
14897
|
}
|
|
14540
14898
|
/** Pointer move; returns true when the draft changed (re-render). */
|
|
14541
14899
|
move(point, modifiers = {}) {
|
|
@@ -14562,6 +14920,7 @@ var SelectionGesture = class {
|
|
|
14562
14920
|
const last = this.points[this.points.length - 1];
|
|
14563
14921
|
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
14564
14922
|
this.points.push(point);
|
|
14923
|
+
if (drawn === "quick") this.growPreview();
|
|
14565
14924
|
}
|
|
14566
14925
|
return true;
|
|
14567
14926
|
}
|
|
@@ -14597,7 +14956,8 @@ var SelectionGesture = class {
|
|
|
14597
14956
|
this.start = null;
|
|
14598
14957
|
this.current = null;
|
|
14599
14958
|
this.points = [];
|
|
14600
|
-
const strokeMode = mode === "replace" && this.controller.getSelection() ? "add" : mode;
|
|
14959
|
+
const strokeMode = this.quick?.mode ?? (mode === "replace" && this.controller.getSelection() ? "add" : mode);
|
|
14960
|
+
this.quick = null;
|
|
14601
14961
|
this.pending = this.controller.quickSelect([stroke], {
|
|
14602
14962
|
size: options.quickSize ?? 30,
|
|
14603
14963
|
mode: strokeMode,
|
|
@@ -14608,7 +14968,7 @@ var SelectionGesture = class {
|
|
|
14608
14968
|
(err) => {
|
|
14609
14969
|
this.error = err instanceof Error ? err.message : String(err);
|
|
14610
14970
|
}
|
|
14611
|
-
);
|
|
14971
|
+
).finally(() => this.controller.setSelectionPreview(void 0));
|
|
14612
14972
|
return false;
|
|
14613
14973
|
}
|
|
14614
14974
|
const path = this.draftPath;
|
|
@@ -14737,6 +15097,10 @@ var SelectionGesture = class {
|
|
|
14737
15097
|
}
|
|
14738
15098
|
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
14739
15099
|
cancel() {
|
|
15100
|
+
if (this.quick) {
|
|
15101
|
+
this.quick = null;
|
|
15102
|
+
this.controller.setSelectionPreview(void 0);
|
|
15103
|
+
}
|
|
14740
15104
|
this.start = null;
|
|
14741
15105
|
this.current = null;
|
|
14742
15106
|
this.points = [];
|
|
@@ -16853,6 +17217,19 @@ function createImageShapeTools(controller) {
|
|
|
16853
17217
|
fillRule: { type: "string", enum: ["nonzero", "evenodd"], description: "Winding rule within a component (default nonzero)." }
|
|
16854
17218
|
};
|
|
16855
17219
|
const LAYER = { type: "string", minLength: 1, description: "The layer (see list_layers). Not a shape layer: its path already is its vector mask." };
|
|
17220
|
+
const PATH_ID = { type: "string", minLength: 1, description: "A document path id from list_paths." };
|
|
17221
|
+
const fromPathId = (args) => {
|
|
17222
|
+
if (args.pathId === void 0) return null;
|
|
17223
|
+
const own = Object.keys(PATH_PROPS).filter((k) => args[k] !== void 0);
|
|
17224
|
+
if (own.length) throw new Error(`Give pathId or ${own.join(" / ")}, not both.`);
|
|
17225
|
+
const path = controller.getPath(String(args.pathId));
|
|
17226
|
+
if (!path) throw new Error(`No path "${String(args.pathId)}". Call list_paths for ids.`);
|
|
17227
|
+
return { components: path.components.map((c) => ({ op: c.op, subpaths: c.subpaths })), ...path.fillRule ? { fillRule: path.fillRule } : {} };
|
|
17228
|
+
};
|
|
17229
|
+
const pathSummary = (id) => {
|
|
17230
|
+
const p = controller.getPath(id);
|
|
17231
|
+
return { id: p.id, name: p.name, work: Boolean(p.work), fillRule: p.fillRule ?? "nonzero", anchors: pathAnchorCount(p), bounds: pathBounds(p) };
|
|
17232
|
+
};
|
|
16856
17233
|
const TRACE_PROPS = {
|
|
16857
17234
|
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)." },
|
|
16858
17235
|
cornerRoundness: { type: "number", minimum: 0, maximum: 1, description: "0: straight sides and sharp corners; 1: as smooth as the outline allows (default 0.5)." },
|
|
@@ -16884,6 +17261,7 @@ function createImageShapeTools(controller) {
|
|
|
16884
17261
|
type: "object",
|
|
16885
17262
|
$defs: DEFS2,
|
|
16886
17263
|
properties: {
|
|
17264
|
+
pathId: { ...PATH_ID, description: "A document path from list_paths, instead of shape / subpaths / components." },
|
|
16887
17265
|
...PATH_PROPS,
|
|
16888
17266
|
feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather radius in px (default 0)." },
|
|
16889
17267
|
antiAlias: { type: "boolean", description: "Soft edges (default true); false makes every pixel fully in or out." },
|
|
@@ -16894,7 +17272,7 @@ function createImageShapeTools(controller) {
|
|
|
16894
17272
|
annotations: { title: "Make a selection from a path", ...WRITE8 },
|
|
16895
17273
|
handler: (args) => {
|
|
16896
17274
|
const mode = args.mode;
|
|
16897
|
-
controller.pathToSelection(pathSpec(args), { feather: args.feather, antiAlias: args.antiAlias, mode });
|
|
17275
|
+
controller.pathToSelection(fromPathId(args) ?? pathSpec(args), { feather: args.feather, antiAlias: args.antiAlias, mode });
|
|
16898
17276
|
const sel = controller.getSelection();
|
|
16899
17277
|
return { empty: !sel, bounds: sel?.bounds ?? null, area: Math.round(sel ? selectionArea(sel) : 0) };
|
|
16900
17278
|
}
|
|
@@ -16916,7 +17294,8 @@ function createImageShapeTools(controller) {
|
|
|
16916
17294
|
$defs: DEFS2,
|
|
16917
17295
|
properties: {
|
|
16918
17296
|
layerId: LAYER,
|
|
16919
|
-
from: { type: "string", enum: ["path", "selection", "reveal-all", "hide-all"], description: "path (default when a shape, subpaths or
|
|
17297
|
+
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)." },
|
|
17298
|
+
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." },
|
|
16920
17299
|
...PATH_PROPS,
|
|
16921
17300
|
...TRACE_PROPS
|
|
16922
17301
|
},
|
|
@@ -16926,7 +17305,7 @@ function createImageShapeTools(controller) {
|
|
|
16926
17305
|
annotations: { title: "Add a vector mask", ...WRITE8 },
|
|
16927
17306
|
handler: (args) => {
|
|
16928
17307
|
const id = layerArg(args.layerId);
|
|
16929
|
-
const spec = pathSpec(args);
|
|
17308
|
+
const spec = fromPathId(args) ?? pathSpec(args);
|
|
16930
17309
|
const hasPath = spec.components?.length || spec.shape !== void 0 || spec.subpaths !== void 0;
|
|
16931
17310
|
const from = args.from ?? (hasPath ? "path" : "reveal-all");
|
|
16932
17311
|
if (from !== "selection" && TRACE_KEYS.some((k) => args[k] !== void 0)) throw new Error("tolerance, cornerRoundness and minArea apply only to from selection.");
|
|
@@ -16994,7 +17373,139 @@ function createImageShapeTools(controller) {
|
|
|
16994
17373
|
return { layerId: id, vectorMask: null };
|
|
16995
17374
|
}
|
|
16996
17375
|
};
|
|
16997
|
-
|
|
17376
|
+
const listPaths = {
|
|
17377
|
+
name: "list_paths",
|
|
17378
|
+
title: "List document paths",
|
|
17379
|
+
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.",
|
|
17380
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
17381
|
+
annotations: { title: "List document paths", ...READ10 },
|
|
17382
|
+
handler: () => ({ paths: controller.getPaths().map((p) => pathSummary(p.id)), activePathId: controller.getState().activePathId ?? null })
|
|
17383
|
+
};
|
|
17384
|
+
const getPath2 = {
|
|
17385
|
+
name: "get_path",
|
|
17386
|
+
title: "Read a document path",
|
|
17387
|
+
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.",
|
|
17388
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID }, required: ["pathId"], additionalProperties: false },
|
|
17389
|
+
annotations: { title: "Read a document path", ...READ10 },
|
|
17390
|
+
handler: ({ pathId }) => {
|
|
17391
|
+
const p = controller.getPath(String(pathId));
|
|
17392
|
+
if (!p) throw new Error(`No path "${String(pathId)}". Call list_paths for ids.`);
|
|
17393
|
+
return { path: { ...pathSummary(p.id), svg: pathSvgData(p), components: p.components } };
|
|
17394
|
+
}
|
|
17395
|
+
};
|
|
17396
|
+
const makeWorkPath = {
|
|
17397
|
+
name: "make_work_path",
|
|
17398
|
+
title: "Make a work path from the selection",
|
|
17399
|
+
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.",
|
|
17400
|
+
inputSchema: { type: "object", properties: { ...TRACE_PROPS }, additionalProperties: false },
|
|
17401
|
+
annotations: { title: "Make a work path from the selection", ...WRITE8 },
|
|
17402
|
+
handler: (args) => ({ path: pathSummary(controller.makeWorkPath(traceOptions(args)).id) })
|
|
17403
|
+
};
|
|
17404
|
+
const addPath = {
|
|
17405
|
+
name: "add_path",
|
|
17406
|
+
title: "Add a document path",
|
|
17407
|
+
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.",
|
|
17408
|
+
inputSchema: {
|
|
17409
|
+
type: "object",
|
|
17410
|
+
$defs: DEFS2,
|
|
17411
|
+
properties: { name: { type: "string", description: 'The path name (default "Path n").' }, ...PATH_PROPS },
|
|
17412
|
+
additionalProperties: false
|
|
17413
|
+
},
|
|
17414
|
+
annotations: { title: "Add a document path", ...WRITE8 },
|
|
17415
|
+
handler: (args) => {
|
|
17416
|
+
const spec = pathSpec(args);
|
|
17417
|
+
if (!spec.components?.length && spec.shape === void 0 && spec.subpaths === void 0) throw new Error("Give the path as shape, subpaths or components.");
|
|
17418
|
+
return { path: pathSummary(controller.addPath({ ...spec, name: args.name }).id) };
|
|
17419
|
+
}
|
|
17420
|
+
};
|
|
17421
|
+
const setPathGeometry = {
|
|
17422
|
+
name: "set_path_geometry",
|
|
17423
|
+
title: "Replace a document path",
|
|
17424
|
+
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.",
|
|
17425
|
+
inputSchema: { type: "object", $defs: DEFS2, properties: { pathId: PATH_ID, ...PATH_PROPS }, required: ["pathId"], additionalProperties: false },
|
|
17426
|
+
annotations: { title: "Replace a document path", ...WRITE_IDEMPOTENT5 },
|
|
17427
|
+
handler: (args) => ({ path: pathSummary(controller.setPathGeometry(String(args.pathId), pathSpec(args)).id) })
|
|
17428
|
+
};
|
|
17429
|
+
const renamePath = {
|
|
17430
|
+
name: "rename_path",
|
|
17431
|
+
title: "Rename or save a path",
|
|
17432
|
+
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.",
|
|
17433
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID, name: { type: "string", minLength: 1, description: "The new name." } }, required: ["pathId", "name"], additionalProperties: false },
|
|
17434
|
+
annotations: { title: "Rename or save a path", ...WRITE_IDEMPOTENT5 },
|
|
17435
|
+
handler: ({ pathId, name }) => ({ path: pathSummary(controller.renamePath(String(pathId), String(name)).id) })
|
|
17436
|
+
};
|
|
17437
|
+
const duplicatePath = {
|
|
17438
|
+
name: "duplicate_path",
|
|
17439
|
+
title: "Duplicate a path",
|
|
17440
|
+
description: 'Duplicates a document path as a saved path (default name "<name> copy"). One undo step. Returns the new path.',
|
|
17441
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID, name: { type: "string", description: "The copy's name." } }, required: ["pathId"], additionalProperties: false },
|
|
17442
|
+
annotations: { title: "Duplicate a path", ...WRITE8 },
|
|
17443
|
+
handler: ({ pathId, name }) => ({ path: pathSummary(controller.duplicatePath(String(pathId), name).id) })
|
|
17444
|
+
};
|
|
17445
|
+
const deletePath = {
|
|
17446
|
+
name: "delete_path",
|
|
17447
|
+
title: "Delete a path",
|
|
17448
|
+
description: "Deletes a document path (undo_image brings it back). Returns the remaining paths.",
|
|
17449
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID }, required: ["pathId"], additionalProperties: false },
|
|
17450
|
+
annotations: { title: "Delete a path", ...DESTRUCTIVE6 },
|
|
17451
|
+
handler: ({ pathId }) => {
|
|
17452
|
+
controller.deletePath(String(pathId));
|
|
17453
|
+
return { paths: controller.getPaths().map((p) => pathSummary(p.id)) };
|
|
17454
|
+
}
|
|
17455
|
+
};
|
|
17456
|
+
const PAINT_PROPS = {
|
|
17457
|
+
layerId: { type: "string", description: "The pixel layer to paint on (default the active layer)." },
|
|
17458
|
+
color: { type: "string", description: "A CSS colour such as #ff0000 (default the foreground colour)." },
|
|
17459
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." }
|
|
17460
|
+
};
|
|
17461
|
+
const fillPath = {
|
|
17462
|
+
name: "fill_path",
|
|
17463
|
+
title: "Fill a path",
|
|
17464
|
+
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.",
|
|
17465
|
+
inputSchema: { type: "object", properties: { pathId: PATH_ID, ...PAINT_PROPS }, required: ["pathId"], additionalProperties: false },
|
|
17466
|
+
annotations: { title: "Fill a path", ...WRITE8 },
|
|
17467
|
+
handler: (args) => controller.fillPath(String(args.pathId), { layerId: args.layerId, color: args.color, opacity: args.opacity })
|
|
17468
|
+
};
|
|
17469
|
+
const strokePath = {
|
|
17470
|
+
name: "stroke_path",
|
|
17471
|
+
title: "Stroke a path",
|
|
17472
|
+
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.",
|
|
17473
|
+
inputSchema: {
|
|
17474
|
+
type: "object",
|
|
17475
|
+
properties: { pathId: PATH_ID, width: { type: "number", exclusiveMinimum: 0, maximum: 250, description: "Stroke width in px." }, ...PAINT_PROPS },
|
|
17476
|
+
required: ["pathId", "width"],
|
|
17477
|
+
additionalProperties: false
|
|
17478
|
+
},
|
|
17479
|
+
annotations: { title: "Stroke a path", ...WRITE8 },
|
|
17480
|
+
handler: (args) => controller.strokePath(String(args.pathId), { width: args.width, layerId: args.layerId, color: args.color, opacity: args.opacity })
|
|
17481
|
+
};
|
|
17482
|
+
return [
|
|
17483
|
+
get,
|
|
17484
|
+
add2,
|
|
17485
|
+
update,
|
|
17486
|
+
setComponent,
|
|
17487
|
+
addComponent,
|
|
17488
|
+
removeComponent,
|
|
17489
|
+
merge,
|
|
17490
|
+
getVectorMask,
|
|
17491
|
+
addVectorMask,
|
|
17492
|
+
setVectorMaskPath,
|
|
17493
|
+
setVectorMaskProps,
|
|
17494
|
+
rasterizeVectorMask,
|
|
17495
|
+
removeVectorMask,
|
|
17496
|
+
selectionToPath,
|
|
17497
|
+
pathToSelection2,
|
|
17498
|
+
listPaths,
|
|
17499
|
+
getPath2,
|
|
17500
|
+
makeWorkPath,
|
|
17501
|
+
addPath,
|
|
17502
|
+
setPathGeometry,
|
|
17503
|
+
renamePath,
|
|
17504
|
+
duplicatePath,
|
|
17505
|
+
deletePath,
|
|
17506
|
+
fillPath,
|
|
17507
|
+
strokePath
|
|
17508
|
+
];
|
|
16998
17509
|
}
|
|
16999
17510
|
|
|
17000
17511
|
// src/image-editor/type-tools.ts
|
|
@@ -17641,6 +18152,9 @@ var SelectAndMaskSession = class {
|
|
|
17641
18152
|
dragStart = null;
|
|
17642
18153
|
pixels = null;
|
|
17643
18154
|
previewPixels = null;
|
|
18155
|
+
/** The layers below the active one, composited (On Layers shows them where nothing is selected). */
|
|
18156
|
+
below = null;
|
|
18157
|
+
belowPreview = null;
|
|
17644
18158
|
base;
|
|
17645
18159
|
previewMask = null;
|
|
17646
18160
|
baseMask = null;
|
|
@@ -17672,10 +18186,24 @@ var SelectAndMaskSession = class {
|
|
|
17672
18186
|
this.emit();
|
|
17673
18187
|
return false;
|
|
17674
18188
|
}
|
|
18189
|
+
this.below = this.layersBelow();
|
|
17675
18190
|
this.rescale();
|
|
17676
18191
|
this.recompute();
|
|
17677
18192
|
return true;
|
|
17678
18193
|
}
|
|
18194
|
+
/** The visible layers under the active one, composited at full size, or null when there are none. */
|
|
18195
|
+
layersBelow() {
|
|
18196
|
+
const active = this.controller.getState().activeLayerId;
|
|
18197
|
+
const flat = flattenLayers(this.controller.getLayers()).filter((l) => l.type !== "group");
|
|
18198
|
+
const at = flat.findIndex((l) => l.id === active);
|
|
18199
|
+
const ids = (at > 0 ? flat.slice(0, at) : []).filter((l) => l.visible).map((l) => l.id);
|
|
18200
|
+
if (!ids.length) return null;
|
|
18201
|
+
try {
|
|
18202
|
+
return this.controller.compositePixels(void 0, { layerIds: ids });
|
|
18203
|
+
} catch {
|
|
18204
|
+
return null;
|
|
18205
|
+
}
|
|
18206
|
+
}
|
|
17679
18207
|
subscribe(listener) {
|
|
17680
18208
|
this.listeners.add(listener);
|
|
17681
18209
|
return () => this.listeners.delete(listener);
|
|
@@ -17708,6 +18236,7 @@ var SelectAndMaskSession = class {
|
|
|
17708
18236
|
const w = Math.max(1, Math.round(px2.width * s));
|
|
17709
18237
|
const h = Math.max(1, Math.round(px2.height * s));
|
|
17710
18238
|
this.previewPixels = scalePixels(px2, w, h);
|
|
18239
|
+
this.belowPreview = this.below ? scalePixels(this.below, w, h) : null;
|
|
17711
18240
|
this.baseMask = this.base?.bounds ? s === 1 ? this.base : resizeSelection(this.base, w, h) : null;
|
|
17712
18241
|
}
|
|
17713
18242
|
/**
|
|
@@ -17831,6 +18360,20 @@ var SelectAndMaskSession = class {
|
|
|
17831
18360
|
}
|
|
17832
18361
|
break;
|
|
17833
18362
|
}
|
|
18363
|
+
case "on-layers": {
|
|
18364
|
+
const under = this.belowPreview?.data;
|
|
18365
|
+
let ur = checker;
|
|
18366
|
+
let ug = checker;
|
|
18367
|
+
let ub = checker;
|
|
18368
|
+
if (under) {
|
|
18369
|
+
const ua = under[p + 3] / 255;
|
|
18370
|
+
ur = under[p] * ua + checker * (1 - ua);
|
|
18371
|
+
ug = under[p + 1] * ua + checker * (1 - ua);
|
|
18372
|
+
ub = under[p + 2] * ua + checker * (1 - ua);
|
|
18373
|
+
}
|
|
18374
|
+
o = [r + (ur - r) * hide, g + (ug - g) * hide, b + (ub - b) * hide];
|
|
18375
|
+
break;
|
|
18376
|
+
}
|
|
17834
18377
|
default:
|
|
17835
18378
|
o = [r + (checker - r) * hide, g + (checker - g) * hide, b + (checker - b) * hide];
|
|
17836
18379
|
}
|
|
@@ -19622,6 +20165,278 @@ function createImageEditorDocument(controller, options = {}) {
|
|
|
19622
20165
|
}
|
|
19623
20166
|
});
|
|
19624
20167
|
}
|
|
20168
|
+
|
|
20169
|
+
// src/image-editor/content-aware-fill-session.ts
|
|
20170
|
+
var CONTENT_AWARE_FILL_TOOLS = [
|
|
20171
|
+
{ id: "sampling-brush", label: "Sampling Brush", key: "b" },
|
|
20172
|
+
{ id: "hand", label: "Hand", key: "h" },
|
|
20173
|
+
{ id: "zoom", label: "Zoom", key: "z" }
|
|
20174
|
+
];
|
|
20175
|
+
var CONTENT_AWARE_FILL_SAMPLING = [
|
|
20176
|
+
{ id: "auto", label: "Auto" },
|
|
20177
|
+
{ id: "rectangular", label: "Rectangular" },
|
|
20178
|
+
{ id: "custom", label: "Custom" }
|
|
20179
|
+
];
|
|
20180
|
+
var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
|
|
20181
|
+
sampling: "auto",
|
|
20182
|
+
showSampling: true,
|
|
20183
|
+
opacity: 0.5,
|
|
20184
|
+
tool: "sampling-brush",
|
|
20185
|
+
brushSize: 40,
|
|
20186
|
+
brushMode: "add",
|
|
20187
|
+
seed: 0,
|
|
20188
|
+
output: "new-layer",
|
|
20189
|
+
livePreview: true
|
|
20190
|
+
};
|
|
20191
|
+
var ContentAwareFillSession = class {
|
|
20192
|
+
constructor(controller) {
|
|
20193
|
+
this.controller = controller;
|
|
20194
|
+
}
|
|
20195
|
+
controller;
|
|
20196
|
+
settingsValue = { ...DEFAULT_CONTENT_AWARE_FILL_SETTINGS };
|
|
20197
|
+
plan = null;
|
|
20198
|
+
/** The source view's window (document pixels): the selection with room around it to sample. */
|
|
20199
|
+
win = null;
|
|
20200
|
+
before = null;
|
|
20201
|
+
hole = null;
|
|
20202
|
+
/** 1 where the fill may copy from, per window pixel. */
|
|
20203
|
+
area = null;
|
|
20204
|
+
after = null;
|
|
20205
|
+
lastPoint = null;
|
|
20206
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20207
|
+
dirty = false;
|
|
20208
|
+
/** Why the workspace can't fill (nothing selected, not a pixel layer), for the UI. */
|
|
20209
|
+
error = null;
|
|
20210
|
+
computing = false;
|
|
20211
|
+
revision = 0;
|
|
20212
|
+
get settings() {
|
|
20213
|
+
return this.settingsValue;
|
|
20214
|
+
}
|
|
20215
|
+
/** The source view's window in document pixels (`null` until opened). */
|
|
20216
|
+
get window() {
|
|
20217
|
+
return this.win;
|
|
20218
|
+
}
|
|
20219
|
+
subscribe(listener) {
|
|
20220
|
+
this.listeners.add(listener);
|
|
20221
|
+
return () => this.listeners.delete(listener);
|
|
20222
|
+
}
|
|
20223
|
+
emit() {
|
|
20224
|
+
this.revision++;
|
|
20225
|
+
for (const l of this.listeners) l();
|
|
20226
|
+
}
|
|
20227
|
+
/**
|
|
20228
|
+
* Reads the layer around the selection and makes the first preview. The window holds the
|
|
20229
|
+
* selection and room around it to sample from (its larger side, at least 64 px, each way).
|
|
20230
|
+
*/
|
|
20231
|
+
async open() {
|
|
20232
|
+
try {
|
|
20233
|
+
const sel = this.controller.getSelection();
|
|
20234
|
+
const b = sel?.bounds;
|
|
20235
|
+
const margin = b ? Math.max(64, Math.max(b.width, b.height)) : 64;
|
|
20236
|
+
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
|
|
20237
|
+
} catch (err) {
|
|
20238
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
20239
|
+
this.emit();
|
|
20240
|
+
return false;
|
|
20241
|
+
}
|
|
20242
|
+
this.win = this.plan.window;
|
|
20243
|
+
this.before = this.plan.before;
|
|
20244
|
+
this.hole = this.plan.hole;
|
|
20245
|
+
this.applySampling("auto");
|
|
20246
|
+
this.emit();
|
|
20247
|
+
await this.refresh();
|
|
20248
|
+
return true;
|
|
20249
|
+
}
|
|
20250
|
+
/** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
|
|
20251
|
+
applySampling(mode) {
|
|
20252
|
+
const win = this.win;
|
|
20253
|
+
const hole = this.hole;
|
|
20254
|
+
const n = win.width * win.height;
|
|
20255
|
+
const area = this.area && mode === "custom" ? this.area : new Uint8Array(n);
|
|
20256
|
+
if (mode === "rectangular") {
|
|
20257
|
+
for (let i = 0; i < n; i++) area[i] = hole[i] ? 0 : 1;
|
|
20258
|
+
} else if (mode === "auto") {
|
|
20259
|
+
const b = this.plan.bounds;
|
|
20260
|
+
const reach = Math.max(24, Math.max(b.width, b.height) / 2);
|
|
20261
|
+
const d = distanceTransform(hole, win.width, win.height);
|
|
20262
|
+
for (let i = 0; i < n; i++) area[i] = !hole[i] && d[i] <= reach ? 1 : 0;
|
|
20263
|
+
}
|
|
20264
|
+
this.area = area;
|
|
20265
|
+
}
|
|
20266
|
+
/** Changes settings; a sampling preset replaces the area, and the preview follows when live. */
|
|
20267
|
+
update(changes) {
|
|
20268
|
+
const prev = this.settingsValue;
|
|
20269
|
+
this.settingsValue = { ...prev, ...changes };
|
|
20270
|
+
if (!this.win) {
|
|
20271
|
+
this.emit();
|
|
20272
|
+
return;
|
|
20273
|
+
}
|
|
20274
|
+
if (changes.sampling && changes.sampling !== prev.sampling && changes.sampling !== "custom") this.applySampling(changes.sampling);
|
|
20275
|
+
this.emit();
|
|
20276
|
+
const fillChanged = changes.sampling !== void 0 || changes.seed !== void 0;
|
|
20277
|
+
if (fillChanged && this.settingsValue.livePreview) void this.refresh();
|
|
20278
|
+
}
|
|
20279
|
+
/** The sampling area as a selection of the whole image (what OK samples from). */
|
|
20280
|
+
samplingMask() {
|
|
20281
|
+
if (!this.win || !this.area) return null;
|
|
20282
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
20283
|
+
const bytes = new Uint8Array(this.area.length);
|
|
20284
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = this.area[i] ? 255 : 0;
|
|
20285
|
+
return selectionFromRegion(width, height, this.win, bytes);
|
|
20286
|
+
}
|
|
20287
|
+
/** Whether (x, y) in document pixels is in the sampling area. */
|
|
20288
|
+
samplesAt(x, y) {
|
|
20289
|
+
const win = this.win;
|
|
20290
|
+
if (!win || !this.area) return false;
|
|
20291
|
+
const xi = Math.floor(x - win.x);
|
|
20292
|
+
const yi = Math.floor(y - win.y);
|
|
20293
|
+
if (xi < 0 || yi < 0 || xi >= win.width || yi >= win.height) return false;
|
|
20294
|
+
return this.area[yi * win.width + xi] === 1;
|
|
20295
|
+
}
|
|
20296
|
+
/** Paints the sampling area with the brush along a stroke segment (document pixels). */
|
|
20297
|
+
paint(from, to, add2) {
|
|
20298
|
+
const win = this.win;
|
|
20299
|
+
const area = this.area;
|
|
20300
|
+
const hole = this.hole;
|
|
20301
|
+
const r = Math.max(0.5, this.settingsValue.brushSize / 2);
|
|
20302
|
+
const x0 = Math.max(0, Math.floor(Math.min(from.x, to.x) - r - win.x));
|
|
20303
|
+
const y0 = Math.max(0, Math.floor(Math.min(from.y, to.y) - r - win.y));
|
|
20304
|
+
const x1 = Math.min(win.width - 1, Math.ceil(Math.max(from.x, to.x) + r - win.x));
|
|
20305
|
+
const y1 = Math.min(win.height - 1, Math.ceil(Math.max(from.y, to.y) + r - win.y));
|
|
20306
|
+
const dx = to.x - from.x;
|
|
20307
|
+
const dy = to.y - from.y;
|
|
20308
|
+
const len2 = dx * dx + dy * dy;
|
|
20309
|
+
for (let y = y0; y <= y1; y++) {
|
|
20310
|
+
for (let x = x0; x <= x1; x++) {
|
|
20311
|
+
const px2 = win.x + x + 0.5 - from.x;
|
|
20312
|
+
const py = win.y + y + 0.5 - from.y;
|
|
20313
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, (px2 * dx + py * dy) / len2)) : 0;
|
|
20314
|
+
if ((px2 - t * dx) ** 2 + (py - t * dy) ** 2 > r * r) continue;
|
|
20315
|
+
const i = y * win.width + x;
|
|
20316
|
+
area[i] = add2 && !hole[i] ? 1 : 0;
|
|
20317
|
+
}
|
|
20318
|
+
}
|
|
20319
|
+
}
|
|
20320
|
+
/** Pointer down with the Sampling Brush (Alt flips add and subtract). Returns false for other tools. */
|
|
20321
|
+
begin(point, modifiers = {}) {
|
|
20322
|
+
if (this.settingsValue.tool !== "sampling-brush" || !this.win) return false;
|
|
20323
|
+
const add2 = this.settingsValue.brushMode === "add" !== Boolean(modifiers.altKey);
|
|
20324
|
+
this.settingsValue = { ...this.settingsValue, sampling: "custom" };
|
|
20325
|
+
this.lastPoint = point;
|
|
20326
|
+
this.strokeAdds = add2;
|
|
20327
|
+
this.paint(point, point, add2);
|
|
20328
|
+
this.emit();
|
|
20329
|
+
return true;
|
|
20330
|
+
}
|
|
20331
|
+
strokeAdds = true;
|
|
20332
|
+
move(point) {
|
|
20333
|
+
if (!this.lastPoint) return;
|
|
20334
|
+
this.paint(this.lastPoint, point, this.strokeAdds);
|
|
20335
|
+
this.lastPoint = point;
|
|
20336
|
+
this.emit();
|
|
20337
|
+
}
|
|
20338
|
+
/** Pointer up: the preview follows the new area when live. */
|
|
20339
|
+
end() {
|
|
20340
|
+
if (!this.lastPoint) return;
|
|
20341
|
+
this.lastPoint = null;
|
|
20342
|
+
if (this.settingsValue.livePreview) void this.refresh();
|
|
20343
|
+
}
|
|
20344
|
+
/** Recomputes the preview: one at a time, the newest area following when one is running. */
|
|
20345
|
+
async refresh() {
|
|
20346
|
+
if (!this.win) return;
|
|
20347
|
+
if (this.computing) {
|
|
20348
|
+
this.dirty = true;
|
|
20349
|
+
return;
|
|
20350
|
+
}
|
|
20351
|
+
this.computing = true;
|
|
20352
|
+
this.emit();
|
|
20353
|
+
do {
|
|
20354
|
+
this.dirty = false;
|
|
20355
|
+
try {
|
|
20356
|
+
const area = this.samplingMask();
|
|
20357
|
+
if (!area?.bounds) throw new Error("The sampling area is empty; paint where the fill may copy from.");
|
|
20358
|
+
const { plan, after } = await this.controller.previewContentAwareFill({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed });
|
|
20359
|
+
const win = this.win;
|
|
20360
|
+
const out = Uint8ClampedArray.from(this.before);
|
|
20361
|
+
const pw = plan.window;
|
|
20362
|
+
for (let y = 0; y < pw.height; y++) {
|
|
20363
|
+
out.set(after.subarray(y * pw.width * 4, (y + 1) * pw.width * 4), ((pw.y - win.y + y) * win.width + (pw.x - win.x)) * 4);
|
|
20364
|
+
}
|
|
20365
|
+
this.after = out;
|
|
20366
|
+
this.error = null;
|
|
20367
|
+
} catch (err) {
|
|
20368
|
+
this.after = null;
|
|
20369
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
20370
|
+
}
|
|
20371
|
+
this.emit();
|
|
20372
|
+
} while (this.dirty);
|
|
20373
|
+
this.computing = false;
|
|
20374
|
+
this.emit();
|
|
20375
|
+
}
|
|
20376
|
+
/** Resolves once no preview is being computed. */
|
|
20377
|
+
async idle() {
|
|
20378
|
+
while (this.computing) await new Promise((r) => setTimeout(r, 5));
|
|
20379
|
+
}
|
|
20380
|
+
/** The source view: the layer's pixels, the selection tinted red, the sampling area green. RGBA at the window's size. */
|
|
20381
|
+
renderSource() {
|
|
20382
|
+
if (!this.before || !this.area || !this.hole) return new Uint8ClampedArray(0);
|
|
20383
|
+
const out = Uint8ClampedArray.from(this.before);
|
|
20384
|
+
const k = this.settingsValue.showSampling ? Math.min(1, Math.max(0, this.settingsValue.opacity)) : 0;
|
|
20385
|
+
for (let i = 0; i < this.area.length; i++) {
|
|
20386
|
+
const p = i * 4;
|
|
20387
|
+
if (this.hole[i]) {
|
|
20388
|
+
out[p] = out[p] + (220 - out[p]) * 0.5;
|
|
20389
|
+
out[p + 1] *= 0.5;
|
|
20390
|
+
out[p + 2] *= 0.5;
|
|
20391
|
+
} else if (this.area[i] && k) {
|
|
20392
|
+
out[p] *= 1 - k;
|
|
20393
|
+
out[p + 1] = out[p + 1] + (200 - out[p + 1]) * k;
|
|
20394
|
+
out[p + 2] *= 1 - k;
|
|
20395
|
+
}
|
|
20396
|
+
out[p + 3] = 255;
|
|
20397
|
+
}
|
|
20398
|
+
return out;
|
|
20399
|
+
}
|
|
20400
|
+
/** The preview: the window with the fill in place (the source until the first preview). */
|
|
20401
|
+
renderPreview() {
|
|
20402
|
+
const src = this.after ?? this.before;
|
|
20403
|
+
if (!src) return new Uint8ClampedArray(0);
|
|
20404
|
+
const out = Uint8ClampedArray.from(src);
|
|
20405
|
+
for (let i = 3; i < out.length; i += 4) out[i] = 255;
|
|
20406
|
+
return out;
|
|
20407
|
+
}
|
|
20408
|
+
/** Clears painting and goes back to Auto. */
|
|
20409
|
+
reset() {
|
|
20410
|
+
if (!this.win) return;
|
|
20411
|
+
this.settingsValue = { ...DEFAULT_CONTENT_AWARE_FILL_SETTINGS, tool: this.settingsValue.tool };
|
|
20412
|
+
this.applySampling("auto");
|
|
20413
|
+
this.emit();
|
|
20414
|
+
if (this.settingsValue.livePreview) void this.refresh();
|
|
20415
|
+
}
|
|
20416
|
+
/**
|
|
20417
|
+
* Keys: B, H, Z for the tools, [ and ] for the brush size. Returns true when the key was used.
|
|
20418
|
+
*/
|
|
20419
|
+
handleKey(key) {
|
|
20420
|
+
const k = key.toLowerCase();
|
|
20421
|
+
const tool = CONTENT_AWARE_FILL_TOOLS.find((t) => t.key === k);
|
|
20422
|
+
if (tool) {
|
|
20423
|
+
this.update({ tool: tool.id });
|
|
20424
|
+
return true;
|
|
20425
|
+
}
|
|
20426
|
+
if (k === "[" || k === "]") {
|
|
20427
|
+
const size = this.settingsValue.brushSize;
|
|
20428
|
+
this.update({ brushSize: Math.max(1, Math.round(k === "]" ? size * 1.25 : size / 1.25)) });
|
|
20429
|
+
return true;
|
|
20430
|
+
}
|
|
20431
|
+
return false;
|
|
20432
|
+
}
|
|
20433
|
+
/** OK: fills with this sampling area, seed and output as one undo step. */
|
|
20434
|
+
async commit(options = {}) {
|
|
20435
|
+
const area = this.samplingMask();
|
|
20436
|
+
if (!this.plan || !area?.bounds) throw new Error(this.error ?? "The sampling area is empty; paint where the fill may copy from.");
|
|
20437
|
+
return this.controller.contentAwareFillAsync({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed, output: this.settingsValue.output, signal: options.signal });
|
|
20438
|
+
}
|
|
20439
|
+
};
|
|
19625
20440
|
export {
|
|
19626
20441
|
ADJUSTMENT_KEYS,
|
|
19627
20442
|
ADJUSTMENT_KINDS,
|
|
@@ -19647,7 +20462,10 @@ export {
|
|
|
19647
20462
|
COLOR_MODE_OPTIONS,
|
|
19648
20463
|
COLOR_RANGE_PRESETS,
|
|
19649
20464
|
COLOR_RANGE_PRESET_LABELS,
|
|
20465
|
+
CONTENT_AWARE_FILL_SAMPLING,
|
|
20466
|
+
CONTENT_AWARE_FILL_TOOLS,
|
|
19650
20467
|
ColorGmm,
|
|
20468
|
+
ContentAwareFillSession,
|
|
19651
20469
|
CoverageMap,
|
|
19652
20470
|
D50,
|
|
19653
20471
|
D65,
|
|
@@ -19657,6 +20475,7 @@ export {
|
|
|
19657
20475
|
DEFAULT_BRUSH,
|
|
19658
20476
|
DEFAULT_CANVAS_VIEWPORT,
|
|
19659
20477
|
DEFAULT_CHANNEL_COLOR,
|
|
20478
|
+
DEFAULT_CONTENT_AWARE_FILL_SETTINGS,
|
|
19660
20479
|
DEFAULT_DEPTH_MASK,
|
|
19661
20480
|
DEFAULT_FLOW,
|
|
19662
20481
|
DEFAULT_GLOBAL_LIGHT,
|
|
@@ -19882,6 +20701,7 @@ export {
|
|
|
19882
20701
|
WGSL_SHADE,
|
|
19883
20702
|
WGSL_STEP,
|
|
19884
20703
|
WORKING_SPACE_OPTIONS,
|
|
20704
|
+
WORK_PATH_NAME,
|
|
19885
20705
|
WORK_SIZE,
|
|
19886
20706
|
WetGpuEngine,
|
|
19887
20707
|
WetMedium,
|
|
@@ -20347,6 +21167,7 @@ export {
|
|
|
20347
21167
|
newEffectId,
|
|
20348
21168
|
newLayerId,
|
|
20349
21169
|
newLayerOptions,
|
|
21170
|
+
newPathId,
|
|
20350
21171
|
normalizeAdjustmentLayer,
|
|
20351
21172
|
normalizeAdjustmentLayers,
|
|
20352
21173
|
normalizeAngle,
|
|
@@ -20403,8 +21224,11 @@ export {
|
|
|
20403
21224
|
parseVersionedDescriptor,
|
|
20404
21225
|
patchParagraphStyle,
|
|
20405
21226
|
patchTextStyle,
|
|
21227
|
+
pathAnchorCount,
|
|
21228
|
+
pathBounds,
|
|
20406
21229
|
pathStrokeCoverage,
|
|
20407
21230
|
pathStrokeDabs,
|
|
21231
|
+
pathSvgData,
|
|
20408
21232
|
pathToSelection,
|
|
20409
21233
|
perDabStrength,
|
|
20410
21234
|
photoFilterMatrix,
|
|
@@ -20457,9 +21281,11 @@ export {
|
|
|
20457
21281
|
readAdjustmentControls,
|
|
20458
21282
|
readDeepRegion,
|
|
20459
21283
|
readPaintSource,
|
|
21284
|
+
readPaths,
|
|
20460
21285
|
readPixel,
|
|
20461
21286
|
readPsd,
|
|
20462
21287
|
readRegion,
|
|
21288
|
+
readShapeComponents,
|
|
20463
21289
|
readSourceRegion,
|
|
20464
21290
|
rectSelection,
|
|
20465
21291
|
rectToQuad,
|
|
@@ -20548,6 +21374,7 @@ export {
|
|
|
20548
21374
|
serializeLayers,
|
|
20549
21375
|
serializeLayersExternal,
|
|
20550
21376
|
serializeMask,
|
|
21377
|
+
serializePaths,
|
|
20551
21378
|
serializePatterns,
|
|
20552
21379
|
serializeSelection,
|
|
20553
21380
|
setCloneTransformValue,
|