@pixerate/editor 0.209.0 → 0.210.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-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-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 +60 -1
- package/dist/image-editor/controller.d.ts +60 -1
- package/dist/image-editor/index.cjs +620 -252
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +396 -28
- 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/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/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,
|
|
@@ -523,33 +523,35 @@ import {
|
|
|
523
523
|
TransformedPixelSource,
|
|
524
524
|
applyRetouchToTile,
|
|
525
525
|
applyToneToDeepTile,
|
|
526
|
-
bestOffset,
|
|
527
526
|
boxBlurPixels,
|
|
528
527
|
cloneSamplePoint,
|
|
529
|
-
contentAwareFill,
|
|
530
|
-
dilateMask,
|
|
531
528
|
dodgeBurn,
|
|
532
529
|
dodgeBurnCurve,
|
|
533
530
|
healPixels,
|
|
534
531
|
isIdentityCloneTransform,
|
|
535
532
|
luma,
|
|
536
|
-
maskBounds,
|
|
537
|
-
membraneFill,
|
|
538
533
|
normalizeRetouch,
|
|
539
534
|
perDabStrength,
|
|
540
535
|
readSourceRegion,
|
|
541
536
|
retouchFilter,
|
|
542
537
|
rgbaViewOfGrey,
|
|
543
538
|
saturationOf,
|
|
544
|
-
seamlessClone,
|
|
545
539
|
smudgeEffect,
|
|
546
|
-
solveMembrane,
|
|
547
540
|
sponge,
|
|
548
541
|
spongeEffect,
|
|
549
542
|
spotHealPixels,
|
|
550
543
|
spotHealWindow,
|
|
551
544
|
toneOf
|
|
552
|
-
} from "../chunk-
|
|
545
|
+
} from "../chunk-ZGJGSCUW.js";
|
|
546
|
+
import {
|
|
547
|
+
bestOffset,
|
|
548
|
+
contentAwareFill,
|
|
549
|
+
dilateMask,
|
|
550
|
+
maskBounds,
|
|
551
|
+
membraneFill,
|
|
552
|
+
seamlessClone,
|
|
553
|
+
solveMembrane
|
|
554
|
+
} from "../chunk-XI2NV5U7.js";
|
|
553
555
|
import {
|
|
554
556
|
BODY_SCATTER,
|
|
555
557
|
DEFAULT_FLOW,
|
|
@@ -7903,9 +7905,46 @@ var ImageEditorController = class {
|
|
|
7903
7905
|
* (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
|
|
7904
7906
|
* coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
|
|
7905
7907
|
* above it. Samples within `sampleRadius` px of the selection (default its larger side, at
|
|
7906
|
-
* least 32). Deterministic for a `seed`. One undo step.
|
|
7908
|
+
* least 32), or from the given sampling area. Deterministic for a `seed`. One undo step.
|
|
7909
|
+
* 8-bit documents only for now. Runs here; see `contentAwareFillAsync` for the worker.
|
|
7907
7910
|
*/
|
|
7908
7911
|
contentAwareFill(options = {}) {
|
|
7912
|
+
const plan = this.prepareContentAwareFill(options);
|
|
7913
|
+
const filled = contentAwareFill(plan.window.width, plan.window.height, 4, plan.before, plan.hole, { seed: options.seed ?? 0, exclude: plan.exclude });
|
|
7914
|
+
return this.applyContentAwareFill(plan, Uint8ClampedArray.from(filled), options.output ?? "current");
|
|
7915
|
+
}
|
|
7916
|
+
/**
|
|
7917
|
+
* `contentAwareFill` with the synthesis in the selection worker when there is one, with
|
|
7918
|
+
* `signal` and `onProgress`. The layer is checked again before the result lands.
|
|
7919
|
+
*/
|
|
7920
|
+
async contentAwareFillAsync(options = {}) {
|
|
7921
|
+
const plan = this.prepareContentAwareFill(options);
|
|
7922
|
+
const filled = await this.synthesiseFill(plan, options.seed ?? 0, options);
|
|
7923
|
+
return this.applyContentAwareFill(plan, filled, options.output ?? "current");
|
|
7924
|
+
}
|
|
7925
|
+
/**
|
|
7926
|
+
* The fill without changing anything: the window, its pixels before and after (RGBA, the fill
|
|
7927
|
+
* blended by the selection's coverage) and the sampling area used, for a preview.
|
|
7928
|
+
*/
|
|
7929
|
+
async previewContentAwareFill(options = {}) {
|
|
7930
|
+
const plan = this.prepareContentAwareFill(options);
|
|
7931
|
+
const filled = await this.synthesiseFill(plan, options.seed ?? 0, options);
|
|
7932
|
+
return { plan, after: this.blendFill(plan, filled, "current") };
|
|
7933
|
+
}
|
|
7934
|
+
async synthesiseFill(plan, seed, options) {
|
|
7935
|
+
const { width, height } = plan.window;
|
|
7936
|
+
const { pixels } = await this.runSelectionJob(
|
|
7937
|
+
{ op: "fill", hole: plan.hole, exclude: plan.exclude, seed },
|
|
7938
|
+
{ width, height, data: plan.before },
|
|
7939
|
+
options
|
|
7940
|
+
);
|
|
7941
|
+
return pixels;
|
|
7942
|
+
}
|
|
7943
|
+
/**
|
|
7944
|
+
* Checks and gathers what a Content-Aware Fill needs: the selection, the 8-bit pixel layer, the
|
|
7945
|
+
* window around the selection (and the sampling area), and which pixels it may copy from.
|
|
7946
|
+
*/
|
|
7947
|
+
prepareContentAwareFill(options = {}) {
|
|
7909
7948
|
const sel = this.state.selection;
|
|
7910
7949
|
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
|
|
7911
7950
|
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 +7953,70 @@ var ImageEditorController = class {
|
|
|
7914
7953
|
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
7915
7954
|
const { width, height } = this.naturalSize;
|
|
7916
7955
|
const b = sel.bounds;
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7956
|
+
let area = options.samplingArea ?? null;
|
|
7957
|
+
if (!area && options.samplingChannelId) area = this.requireChannel(options.samplingChannelId).mask;
|
|
7958
|
+
if (!area && options.sampleRects?.length) {
|
|
7959
|
+
for (const r of options.sampleRects) area = combineSelections(area, rectSelection(width, height, r, { antiAlias: false }), "add");
|
|
7960
|
+
}
|
|
7961
|
+
if (area && !area.bounds) throw new LayerOperationError("The sampling area is empty; paint or give an area to sample from.");
|
|
7962
|
+
let win;
|
|
7963
|
+
if (area?.bounds) {
|
|
7964
|
+
const a = area.bounds;
|
|
7965
|
+
const x0 = Math.max(0, Math.min(b.x - 8, a.x));
|
|
7966
|
+
const y0 = Math.max(0, Math.min(b.y - 8, a.y));
|
|
7967
|
+
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 };
|
|
7968
|
+
} else {
|
|
7969
|
+
const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
|
|
7970
|
+
const x0 = Math.max(0, b.x - margin);
|
|
7971
|
+
const y0 = Math.max(0, b.y - margin);
|
|
7972
|
+
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 };
|
|
7973
|
+
}
|
|
7974
|
+
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.");
|
|
7975
|
+
const before = readRegion(target.surface, { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height });
|
|
7976
|
+
const n = win.width * win.height;
|
|
7977
|
+
const coverage = new Uint8Array(n);
|
|
7978
|
+
const hole = new Uint8Array(n);
|
|
7979
|
+
const selData = sel.data;
|
|
7928
7980
|
for (let y = 0; y < win.height; y++) {
|
|
7929
7981
|
for (let x = 0; x < win.width; x++) {
|
|
7930
|
-
const c =
|
|
7982
|
+
const c = selData[(win.y + y) * sel.width + win.x + x];
|
|
7931
7983
|
coverage[y * win.width + x] = c;
|
|
7932
7984
|
hole[y * win.width + x] = c > 0 ? 1 : 0;
|
|
7933
7985
|
}
|
|
7934
7986
|
}
|
|
7935
|
-
|
|
7987
|
+
let exclude = null;
|
|
7988
|
+
if (area || options.excludeRects?.length) {
|
|
7989
|
+
exclude = new Uint8Array(n);
|
|
7990
|
+
if (area) {
|
|
7991
|
+
const ad = area.data;
|
|
7992
|
+
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;
|
|
7993
|
+
}
|
|
7994
|
+
for (const r of options.excludeRects ?? []) {
|
|
7995
|
+
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++) {
|
|
7996
|
+
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;
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
return { layerId: target.id, window: win, bounds: { ...b }, before, hole, coverage, exclude };
|
|
8001
|
+
}
|
|
8002
|
+
/** The window's pixels with `filled` blended in by the coverage (`current`), or the fill alone, its alpha by the coverage (`new-layer`). */
|
|
8003
|
+
blendFill(plan, filled, output) {
|
|
8004
|
+
const { before, coverage } = plan;
|
|
7936
8005
|
const out = new Uint8ClampedArray(before.length);
|
|
7937
8006
|
for (let i = 0; i < coverage.length; i++) {
|
|
7938
|
-
const k = coverage[i];
|
|
8007
|
+
const k = coverage[i] / 255;
|
|
7939
8008
|
for (let c = 0; c < 4; c++) {
|
|
7940
8009
|
const p = i * 4 + c;
|
|
7941
8010
|
out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
|
|
7942
8011
|
}
|
|
7943
8012
|
}
|
|
8013
|
+
return out;
|
|
8014
|
+
}
|
|
8015
|
+
/** Puts a computed fill into the document as one undo step. */
|
|
8016
|
+
applyContentAwareFill(plan, filled, output) {
|
|
8017
|
+
const target = this.paintTarget(plan.layerId, "pixels", "fill");
|
|
8018
|
+
const { window: win, bounds: b } = plan;
|
|
8019
|
+
const out = this.blendFill(plan, filled, output);
|
|
7944
8020
|
if (output === "new-layer") {
|
|
7945
8021
|
const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
|
|
7946
8022
|
const data = new Uint8ClampedArray(b.width * b.height * 4);
|
|
@@ -7948,6 +8024,7 @@ var ImageEditorController = class {
|
|
|
7948
8024
|
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
8025
|
return { layerId: id, bounds: { ...b }, output };
|
|
7950
8026
|
}
|
|
8027
|
+
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
7951
8028
|
const surface = writeRegion(target.surface, layerRect, out);
|
|
7952
8029
|
this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
7953
8030
|
return { layerId: target.id, bounds: { ...b }, output };
|
|
@@ -11924,6 +12001,13 @@ var rectProperties = {
|
|
|
11924
12001
|
width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
|
|
11925
12002
|
height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
|
|
11926
12003
|
};
|
|
12004
|
+
var rectSchemaStrict = {
|
|
12005
|
+
type: "object",
|
|
12006
|
+
properties: rectProperties,
|
|
12007
|
+
required: ["x", "y", "width", "height"],
|
|
12008
|
+
additionalProperties: false,
|
|
12009
|
+
description: "A rectangle in natural image pixels."
|
|
12010
|
+
};
|
|
11927
12011
|
function shapeOptions(args) {
|
|
11928
12012
|
return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
|
|
11929
12013
|
}
|
|
@@ -12777,25 +12861,33 @@ function createImageSelectionTools(controller) {
|
|
|
12777
12861
|
const fillTool = {
|
|
12778
12862
|
name: "content_aware_fill",
|
|
12779
12863
|
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.",
|
|
12864
|
+
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
12865
|
inputSchema: {
|
|
12782
12866
|
type: "object",
|
|
12783
12867
|
properties: {
|
|
12784
12868
|
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
|
|
12785
12869
|
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
12786
12870
|
seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
|
|
12871
|
+
sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
|
|
12872
|
+
excludeRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Never sample from these rectangles (natural image pixels)." },
|
|
12873
|
+
samplingChannelId: { type: "string", description: "Sample only where this alpha channel is selected (see list_channels)." },
|
|
12787
12874
|
layerId: layerIdSchema
|
|
12788
12875
|
},
|
|
12789
12876
|
additionalProperties: false
|
|
12790
12877
|
},
|
|
12791
12878
|
annotations: writeAnnotations("Content-Aware Fill"),
|
|
12792
|
-
handler: (args) => {
|
|
12879
|
+
handler: async (args, context) => {
|
|
12793
12880
|
if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
|
|
12794
|
-
|
|
12881
|
+
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;
|
|
12882
|
+
return controller.contentAwareFillAsync({
|
|
12795
12883
|
output: args.output,
|
|
12796
12884
|
sampleRadius: args.sampleRadius,
|
|
12797
12885
|
seed: args.seed,
|
|
12798
|
-
layerId: requireLayerId(args.layerId)
|
|
12886
|
+
layerId: requireLayerId(args.layerId),
|
|
12887
|
+
sampleRects: rects(args.sampleRects),
|
|
12888
|
+
excludeRects: rects(args.excludeRects),
|
|
12889
|
+
samplingChannelId: typeof args.samplingChannelId === "string" ? args.samplingChannelId : void 0,
|
|
12890
|
+
signal: context?.signal
|
|
12799
12891
|
});
|
|
12800
12892
|
}
|
|
12801
12893
|
};
|
|
@@ -19622,6 +19714,278 @@ function createImageEditorDocument(controller, options = {}) {
|
|
|
19622
19714
|
}
|
|
19623
19715
|
});
|
|
19624
19716
|
}
|
|
19717
|
+
|
|
19718
|
+
// src/image-editor/content-aware-fill-session.ts
|
|
19719
|
+
var CONTENT_AWARE_FILL_TOOLS = [
|
|
19720
|
+
{ id: "sampling-brush", label: "Sampling Brush", key: "b" },
|
|
19721
|
+
{ id: "hand", label: "Hand", key: "h" },
|
|
19722
|
+
{ id: "zoom", label: "Zoom", key: "z" }
|
|
19723
|
+
];
|
|
19724
|
+
var CONTENT_AWARE_FILL_SAMPLING = [
|
|
19725
|
+
{ id: "auto", label: "Auto" },
|
|
19726
|
+
{ id: "rectangular", label: "Rectangular" },
|
|
19727
|
+
{ id: "custom", label: "Custom" }
|
|
19728
|
+
];
|
|
19729
|
+
var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
|
|
19730
|
+
sampling: "auto",
|
|
19731
|
+
showSampling: true,
|
|
19732
|
+
opacity: 0.5,
|
|
19733
|
+
tool: "sampling-brush",
|
|
19734
|
+
brushSize: 40,
|
|
19735
|
+
brushMode: "add",
|
|
19736
|
+
seed: 0,
|
|
19737
|
+
output: "new-layer",
|
|
19738
|
+
livePreview: true
|
|
19739
|
+
};
|
|
19740
|
+
var ContentAwareFillSession = class {
|
|
19741
|
+
constructor(controller) {
|
|
19742
|
+
this.controller = controller;
|
|
19743
|
+
}
|
|
19744
|
+
controller;
|
|
19745
|
+
settingsValue = { ...DEFAULT_CONTENT_AWARE_FILL_SETTINGS };
|
|
19746
|
+
plan = null;
|
|
19747
|
+
/** The source view's window (document pixels): the selection with room around it to sample. */
|
|
19748
|
+
win = null;
|
|
19749
|
+
before = null;
|
|
19750
|
+
hole = null;
|
|
19751
|
+
/** 1 where the fill may copy from, per window pixel. */
|
|
19752
|
+
area = null;
|
|
19753
|
+
after = null;
|
|
19754
|
+
lastPoint = null;
|
|
19755
|
+
listeners = /* @__PURE__ */ new Set();
|
|
19756
|
+
dirty = false;
|
|
19757
|
+
/** Why the workspace can't fill (nothing selected, not a pixel layer), for the UI. */
|
|
19758
|
+
error = null;
|
|
19759
|
+
computing = false;
|
|
19760
|
+
revision = 0;
|
|
19761
|
+
get settings() {
|
|
19762
|
+
return this.settingsValue;
|
|
19763
|
+
}
|
|
19764
|
+
/** The source view's window in document pixels (`null` until opened). */
|
|
19765
|
+
get window() {
|
|
19766
|
+
return this.win;
|
|
19767
|
+
}
|
|
19768
|
+
subscribe(listener) {
|
|
19769
|
+
this.listeners.add(listener);
|
|
19770
|
+
return () => this.listeners.delete(listener);
|
|
19771
|
+
}
|
|
19772
|
+
emit() {
|
|
19773
|
+
this.revision++;
|
|
19774
|
+
for (const l of this.listeners) l();
|
|
19775
|
+
}
|
|
19776
|
+
/**
|
|
19777
|
+
* Reads the layer around the selection and makes the first preview. The window holds the
|
|
19778
|
+
* selection and room around it to sample from (its larger side, at least 64 px, each way).
|
|
19779
|
+
*/
|
|
19780
|
+
async open() {
|
|
19781
|
+
try {
|
|
19782
|
+
const sel = this.controller.getSelection();
|
|
19783
|
+
const b = sel?.bounds;
|
|
19784
|
+
const margin = b ? Math.max(64, Math.max(b.width, b.height)) : 64;
|
|
19785
|
+
this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
|
|
19786
|
+
} catch (err) {
|
|
19787
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
19788
|
+
this.emit();
|
|
19789
|
+
return false;
|
|
19790
|
+
}
|
|
19791
|
+
this.win = this.plan.window;
|
|
19792
|
+
this.before = this.plan.before;
|
|
19793
|
+
this.hole = this.plan.hole;
|
|
19794
|
+
this.applySampling("auto");
|
|
19795
|
+
this.emit();
|
|
19796
|
+
await this.refresh();
|
|
19797
|
+
return true;
|
|
19798
|
+
}
|
|
19799
|
+
/** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
|
|
19800
|
+
applySampling(mode) {
|
|
19801
|
+
const win = this.win;
|
|
19802
|
+
const hole = this.hole;
|
|
19803
|
+
const n = win.width * win.height;
|
|
19804
|
+
const area = this.area && mode === "custom" ? this.area : new Uint8Array(n);
|
|
19805
|
+
if (mode === "rectangular") {
|
|
19806
|
+
for (let i = 0; i < n; i++) area[i] = hole[i] ? 0 : 1;
|
|
19807
|
+
} else if (mode === "auto") {
|
|
19808
|
+
const b = this.plan.bounds;
|
|
19809
|
+
const reach = Math.max(24, Math.max(b.width, b.height) / 2);
|
|
19810
|
+
const d = distanceTransform(hole, win.width, win.height);
|
|
19811
|
+
for (let i = 0; i < n; i++) area[i] = !hole[i] && d[i] <= reach ? 1 : 0;
|
|
19812
|
+
}
|
|
19813
|
+
this.area = area;
|
|
19814
|
+
}
|
|
19815
|
+
/** Changes settings; a sampling preset replaces the area, and the preview follows when live. */
|
|
19816
|
+
update(changes) {
|
|
19817
|
+
const prev = this.settingsValue;
|
|
19818
|
+
this.settingsValue = { ...prev, ...changes };
|
|
19819
|
+
if (!this.win) {
|
|
19820
|
+
this.emit();
|
|
19821
|
+
return;
|
|
19822
|
+
}
|
|
19823
|
+
if (changes.sampling && changes.sampling !== prev.sampling && changes.sampling !== "custom") this.applySampling(changes.sampling);
|
|
19824
|
+
this.emit();
|
|
19825
|
+
const fillChanged = changes.sampling !== void 0 || changes.seed !== void 0;
|
|
19826
|
+
if (fillChanged && this.settingsValue.livePreview) void this.refresh();
|
|
19827
|
+
}
|
|
19828
|
+
/** The sampling area as a selection of the whole image (what OK samples from). */
|
|
19829
|
+
samplingMask() {
|
|
19830
|
+
if (!this.win || !this.area) return null;
|
|
19831
|
+
const { width, height } = this.controller.getState().imageDimensions;
|
|
19832
|
+
const bytes = new Uint8Array(this.area.length);
|
|
19833
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = this.area[i] ? 255 : 0;
|
|
19834
|
+
return selectionFromRegion(width, height, this.win, bytes);
|
|
19835
|
+
}
|
|
19836
|
+
/** Whether (x, y) in document pixels is in the sampling area. */
|
|
19837
|
+
samplesAt(x, y) {
|
|
19838
|
+
const win = this.win;
|
|
19839
|
+
if (!win || !this.area) return false;
|
|
19840
|
+
const xi = Math.floor(x - win.x);
|
|
19841
|
+
const yi = Math.floor(y - win.y);
|
|
19842
|
+
if (xi < 0 || yi < 0 || xi >= win.width || yi >= win.height) return false;
|
|
19843
|
+
return this.area[yi * win.width + xi] === 1;
|
|
19844
|
+
}
|
|
19845
|
+
/** Paints the sampling area with the brush along a stroke segment (document pixels). */
|
|
19846
|
+
paint(from, to, add2) {
|
|
19847
|
+
const win = this.win;
|
|
19848
|
+
const area = this.area;
|
|
19849
|
+
const hole = this.hole;
|
|
19850
|
+
const r = Math.max(0.5, this.settingsValue.brushSize / 2);
|
|
19851
|
+
const x0 = Math.max(0, Math.floor(Math.min(from.x, to.x) - r - win.x));
|
|
19852
|
+
const y0 = Math.max(0, Math.floor(Math.min(from.y, to.y) - r - win.y));
|
|
19853
|
+
const x1 = Math.min(win.width - 1, Math.ceil(Math.max(from.x, to.x) + r - win.x));
|
|
19854
|
+
const y1 = Math.min(win.height - 1, Math.ceil(Math.max(from.y, to.y) + r - win.y));
|
|
19855
|
+
const dx = to.x - from.x;
|
|
19856
|
+
const dy = to.y - from.y;
|
|
19857
|
+
const len2 = dx * dx + dy * dy;
|
|
19858
|
+
for (let y = y0; y <= y1; y++) {
|
|
19859
|
+
for (let x = x0; x <= x1; x++) {
|
|
19860
|
+
const px2 = win.x + x + 0.5 - from.x;
|
|
19861
|
+
const py = win.y + y + 0.5 - from.y;
|
|
19862
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, (px2 * dx + py * dy) / len2)) : 0;
|
|
19863
|
+
if ((px2 - t * dx) ** 2 + (py - t * dy) ** 2 > r * r) continue;
|
|
19864
|
+
const i = y * win.width + x;
|
|
19865
|
+
area[i] = add2 && !hole[i] ? 1 : 0;
|
|
19866
|
+
}
|
|
19867
|
+
}
|
|
19868
|
+
}
|
|
19869
|
+
/** Pointer down with the Sampling Brush (Alt flips add and subtract). Returns false for other tools. */
|
|
19870
|
+
begin(point, modifiers = {}) {
|
|
19871
|
+
if (this.settingsValue.tool !== "sampling-brush" || !this.win) return false;
|
|
19872
|
+
const add2 = this.settingsValue.brushMode === "add" !== Boolean(modifiers.altKey);
|
|
19873
|
+
this.settingsValue = { ...this.settingsValue, sampling: "custom" };
|
|
19874
|
+
this.lastPoint = point;
|
|
19875
|
+
this.strokeAdds = add2;
|
|
19876
|
+
this.paint(point, point, add2);
|
|
19877
|
+
this.emit();
|
|
19878
|
+
return true;
|
|
19879
|
+
}
|
|
19880
|
+
strokeAdds = true;
|
|
19881
|
+
move(point) {
|
|
19882
|
+
if (!this.lastPoint) return;
|
|
19883
|
+
this.paint(this.lastPoint, point, this.strokeAdds);
|
|
19884
|
+
this.lastPoint = point;
|
|
19885
|
+
this.emit();
|
|
19886
|
+
}
|
|
19887
|
+
/** Pointer up: the preview follows the new area when live. */
|
|
19888
|
+
end() {
|
|
19889
|
+
if (!this.lastPoint) return;
|
|
19890
|
+
this.lastPoint = null;
|
|
19891
|
+
if (this.settingsValue.livePreview) void this.refresh();
|
|
19892
|
+
}
|
|
19893
|
+
/** Recomputes the preview: one at a time, the newest area following when one is running. */
|
|
19894
|
+
async refresh() {
|
|
19895
|
+
if (!this.win) return;
|
|
19896
|
+
if (this.computing) {
|
|
19897
|
+
this.dirty = true;
|
|
19898
|
+
return;
|
|
19899
|
+
}
|
|
19900
|
+
this.computing = true;
|
|
19901
|
+
this.emit();
|
|
19902
|
+
do {
|
|
19903
|
+
this.dirty = false;
|
|
19904
|
+
try {
|
|
19905
|
+
const area = this.samplingMask();
|
|
19906
|
+
if (!area?.bounds) throw new Error("The sampling area is empty; paint where the fill may copy from.");
|
|
19907
|
+
const { plan, after } = await this.controller.previewContentAwareFill({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed });
|
|
19908
|
+
const win = this.win;
|
|
19909
|
+
const out = Uint8ClampedArray.from(this.before);
|
|
19910
|
+
const pw = plan.window;
|
|
19911
|
+
for (let y = 0; y < pw.height; y++) {
|
|
19912
|
+
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);
|
|
19913
|
+
}
|
|
19914
|
+
this.after = out;
|
|
19915
|
+
this.error = null;
|
|
19916
|
+
} catch (err) {
|
|
19917
|
+
this.after = null;
|
|
19918
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
19919
|
+
}
|
|
19920
|
+
this.emit();
|
|
19921
|
+
} while (this.dirty);
|
|
19922
|
+
this.computing = false;
|
|
19923
|
+
this.emit();
|
|
19924
|
+
}
|
|
19925
|
+
/** Resolves once no preview is being computed. */
|
|
19926
|
+
async idle() {
|
|
19927
|
+
while (this.computing) await new Promise((r) => setTimeout(r, 5));
|
|
19928
|
+
}
|
|
19929
|
+
/** The source view: the layer's pixels, the selection tinted red, the sampling area green. RGBA at the window's size. */
|
|
19930
|
+
renderSource() {
|
|
19931
|
+
if (!this.before || !this.area || !this.hole) return new Uint8ClampedArray(0);
|
|
19932
|
+
const out = Uint8ClampedArray.from(this.before);
|
|
19933
|
+
const k = this.settingsValue.showSampling ? Math.min(1, Math.max(0, this.settingsValue.opacity)) : 0;
|
|
19934
|
+
for (let i = 0; i < this.area.length; i++) {
|
|
19935
|
+
const p = i * 4;
|
|
19936
|
+
if (this.hole[i]) {
|
|
19937
|
+
out[p] = out[p] + (220 - out[p]) * 0.5;
|
|
19938
|
+
out[p + 1] *= 0.5;
|
|
19939
|
+
out[p + 2] *= 0.5;
|
|
19940
|
+
} else if (this.area[i] && k) {
|
|
19941
|
+
out[p] *= 1 - k;
|
|
19942
|
+
out[p + 1] = out[p + 1] + (200 - out[p + 1]) * k;
|
|
19943
|
+
out[p + 2] *= 1 - k;
|
|
19944
|
+
}
|
|
19945
|
+
out[p + 3] = 255;
|
|
19946
|
+
}
|
|
19947
|
+
return out;
|
|
19948
|
+
}
|
|
19949
|
+
/** The preview: the window with the fill in place (the source until the first preview). */
|
|
19950
|
+
renderPreview() {
|
|
19951
|
+
const src = this.after ?? this.before;
|
|
19952
|
+
if (!src) return new Uint8ClampedArray(0);
|
|
19953
|
+
const out = Uint8ClampedArray.from(src);
|
|
19954
|
+
for (let i = 3; i < out.length; i += 4) out[i] = 255;
|
|
19955
|
+
return out;
|
|
19956
|
+
}
|
|
19957
|
+
/** Clears painting and goes back to Auto. */
|
|
19958
|
+
reset() {
|
|
19959
|
+
if (!this.win) return;
|
|
19960
|
+
this.settingsValue = { ...DEFAULT_CONTENT_AWARE_FILL_SETTINGS, tool: this.settingsValue.tool };
|
|
19961
|
+
this.applySampling("auto");
|
|
19962
|
+
this.emit();
|
|
19963
|
+
if (this.settingsValue.livePreview) void this.refresh();
|
|
19964
|
+
}
|
|
19965
|
+
/**
|
|
19966
|
+
* Keys: B, H, Z for the tools, [ and ] for the brush size. Returns true when the key was used.
|
|
19967
|
+
*/
|
|
19968
|
+
handleKey(key) {
|
|
19969
|
+
const k = key.toLowerCase();
|
|
19970
|
+
const tool = CONTENT_AWARE_FILL_TOOLS.find((t) => t.key === k);
|
|
19971
|
+
if (tool) {
|
|
19972
|
+
this.update({ tool: tool.id });
|
|
19973
|
+
return true;
|
|
19974
|
+
}
|
|
19975
|
+
if (k === "[" || k === "]") {
|
|
19976
|
+
const size = this.settingsValue.brushSize;
|
|
19977
|
+
this.update({ brushSize: Math.max(1, Math.round(k === "]" ? size * 1.25 : size / 1.25)) });
|
|
19978
|
+
return true;
|
|
19979
|
+
}
|
|
19980
|
+
return false;
|
|
19981
|
+
}
|
|
19982
|
+
/** OK: fills with this sampling area, seed and output as one undo step. */
|
|
19983
|
+
async commit(options = {}) {
|
|
19984
|
+
const area = this.samplingMask();
|
|
19985
|
+
if (!this.plan || !area?.bounds) throw new Error(this.error ?? "The sampling area is empty; paint where the fill may copy from.");
|
|
19986
|
+
return this.controller.contentAwareFillAsync({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed, output: this.settingsValue.output, signal: options.signal });
|
|
19987
|
+
}
|
|
19988
|
+
};
|
|
19625
19989
|
export {
|
|
19626
19990
|
ADJUSTMENT_KEYS,
|
|
19627
19991
|
ADJUSTMENT_KINDS,
|
|
@@ -19647,7 +20011,10 @@ export {
|
|
|
19647
20011
|
COLOR_MODE_OPTIONS,
|
|
19648
20012
|
COLOR_RANGE_PRESETS,
|
|
19649
20013
|
COLOR_RANGE_PRESET_LABELS,
|
|
20014
|
+
CONTENT_AWARE_FILL_SAMPLING,
|
|
20015
|
+
CONTENT_AWARE_FILL_TOOLS,
|
|
19650
20016
|
ColorGmm,
|
|
20017
|
+
ContentAwareFillSession,
|
|
19651
20018
|
CoverageMap,
|
|
19652
20019
|
D50,
|
|
19653
20020
|
D65,
|
|
@@ -19657,6 +20024,7 @@ export {
|
|
|
19657
20024
|
DEFAULT_BRUSH,
|
|
19658
20025
|
DEFAULT_CANVAS_VIEWPORT,
|
|
19659
20026
|
DEFAULT_CHANNEL_COLOR,
|
|
20027
|
+
DEFAULT_CONTENT_AWARE_FILL_SETTINGS,
|
|
19660
20028
|
DEFAULT_DEPTH_MASK,
|
|
19661
20029
|
DEFAULT_FLOW,
|
|
19662
20030
|
DEFAULT_GLOBAL_LIGHT,
|
|
@@ -52,6 +52,13 @@ export type SelectionJob = {
|
|
|
52
52
|
base: SerializedMask | null;
|
|
53
53
|
edits: RefineEdit[];
|
|
54
54
|
params: RefineEdgeParams;
|
|
55
|
+
}
|
|
56
|
+
/** Content-Aware Fill's synthesis on a window (the pixels): `hole` to fill, `exclude` not to copy from. */
|
|
57
|
+
| {
|
|
58
|
+
op: 'fill';
|
|
59
|
+
hole: Uint8Array;
|
|
60
|
+
exclude: Uint8Array | null;
|
|
61
|
+
seed: number;
|
|
55
62
|
};
|
|
56
63
|
/** What a job found: a selection of the whole image, and the job's notes. */
|
|
57
64
|
export interface SelectionJobResult {
|
|
@@ -60,6 +67,8 @@ export interface SelectionJobResult {
|
|
|
60
67
|
reason?: string;
|
|
61
68
|
/** Focus Area: the noise level it measured. */
|
|
62
69
|
noiseLevel?: number;
|
|
70
|
+
/** Content-Aware Fill: the window's pixels with the hole filled (RGBA). */
|
|
71
|
+
pixels?: Uint8ClampedArray;
|
|
63
72
|
}
|
|
64
73
|
/** Runs `job` on `pixels` here and now. */
|
|
65
74
|
export declare function runSelectionJob(job: SelectionJob, pixels: PixelBuffer, onProgress?: SelectionProgress): SelectionJobResult;
|
|
@@ -86,6 +95,7 @@ export type SelectionWorkerResponse = {
|
|
|
86
95
|
mask: SerializedMask;
|
|
87
96
|
reason?: string;
|
|
88
97
|
noiseLevel?: number;
|
|
98
|
+
pixels?: Uint8ClampedArray;
|
|
89
99
|
} | {
|
|
90
100
|
type: 'error';
|
|
91
101
|
id: number;
|
|
@@ -52,6 +52,13 @@ export type SelectionJob = {
|
|
|
52
52
|
base: SerializedMask | null;
|
|
53
53
|
edits: RefineEdit[];
|
|
54
54
|
params: RefineEdgeParams;
|
|
55
|
+
}
|
|
56
|
+
/** Content-Aware Fill's synthesis on a window (the pixels): `hole` to fill, `exclude` not to copy from. */
|
|
57
|
+
| {
|
|
58
|
+
op: 'fill';
|
|
59
|
+
hole: Uint8Array;
|
|
60
|
+
exclude: Uint8Array | null;
|
|
61
|
+
seed: number;
|
|
55
62
|
};
|
|
56
63
|
/** What a job found: a selection of the whole image, and the job's notes. */
|
|
57
64
|
export interface SelectionJobResult {
|
|
@@ -60,6 +67,8 @@ export interface SelectionJobResult {
|
|
|
60
67
|
reason?: string;
|
|
61
68
|
/** Focus Area: the noise level it measured. */
|
|
62
69
|
noiseLevel?: number;
|
|
70
|
+
/** Content-Aware Fill: the window's pixels with the hole filled (RGBA). */
|
|
71
|
+
pixels?: Uint8ClampedArray;
|
|
63
72
|
}
|
|
64
73
|
/** Runs `job` on `pixels` here and now. */
|
|
65
74
|
export declare function runSelectionJob(job: SelectionJob, pixels: PixelBuffer, onProgress?: SelectionProgress): SelectionJobResult;
|
|
@@ -86,6 +95,7 @@ export type SelectionWorkerResponse = {
|
|
|
86
95
|
mask: SerializedMask;
|
|
87
96
|
reason?: string;
|
|
88
97
|
noiseLevel?: number;
|
|
98
|
+
pixels?: Uint8ClampedArray;
|
|
89
99
|
} | {
|
|
90
100
|
type: 'error';
|
|
91
101
|
id: number;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkL52VMQZScjs = require('../chunk-L52VMQZS.cjs');
|
|
4
4
|
require('../chunk-QYLWG22N.cjs');
|
|
5
5
|
require('../chunk-K6PXS6DO.cjs');
|
|
6
|
+
require('../chunk-MQDIP4CF.cjs');
|
|
6
7
|
require('../chunk-474IQG74.cjs');
|
|
7
8
|
|
|
8
9
|
// src/image-editor/selection-worker.ts
|
|
9
10
|
if (typeof self !== "undefined" && typeof window === "undefined" && typeof self.postMessage === "function") {
|
|
10
|
-
|
|
11
|
+
_chunkL52VMQZScjs.startSelectionWorker.call(void 0, self);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
exports.startSelectionWorker =
|
|
15
|
+
exports.startSelectionWorker = _chunkL52VMQZScjs.startSelectionWorker;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
startSelectionWorker
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-LQ5SALHK.js";
|
|
4
4
|
import "../chunk-L3G23N5Y.js";
|
|
5
5
|
import "../chunk-ZIF4UJJ6.js";
|
|
6
|
+
import "../chunk-XI2NV5U7.js";
|
|
6
7
|
import "../chunk-HVVR5PZW.js";
|
|
7
8
|
|
|
8
9
|
// src/image-editor/selection-worker.ts
|
package/dist/paint/heal.d.cts
CHANGED
|
@@ -29,9 +29,11 @@ export declare function bestOffset(w: number, h: number, ch: number, img: ArrayL
|
|
|
29
29
|
* continue across it: PatchMatch nearest-neighbour fields over 7 × 7 patches with patch voting
|
|
30
30
|
* (Wexler et al. / Barnes et al.), run coarse to fine from a smooth fill. Deterministic for the
|
|
31
31
|
* same input. Returns the filled image; pixels outside `hole` are unchanged. Falls back to a
|
|
32
|
-
* smooth fill when there is no whole patch to copy from.
|
|
32
|
+
* smooth fill when there is no whole patch to copy from. `exclude` marks pixels no source patch
|
|
33
|
+
* may include (Content-Aware Fill's sampling area is everything else).
|
|
33
34
|
*/
|
|
34
35
|
export declare function contentAwareFill(w: number, h: number, ch: number, img: ArrayLike<number>, hole: ArrayLike<boolean | number>, options?: {
|
|
35
36
|
iterations?: number;
|
|
36
37
|
seed?: number;
|
|
38
|
+
exclude?: ArrayLike<boolean | number> | null;
|
|
37
39
|
}): Float32Array;
|