@pixerate/editor 0.208.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 +85 -3
- package/dist/image-editor/controller.d.ts +85 -3
- package/dist/image-editor/index.cjs +847 -396
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +492 -41
- package/dist/image-editor/selection/tool-options.d.cts +3 -1
- package/dist/image-editor/selection/tool-options.d.ts +3 -1
- 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 +14 -0
- package/dist/image-editor/types.d.ts +14 -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/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +21 -23
- 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,
|
|
@@ -5103,6 +5105,7 @@ var ImageEditorController = class {
|
|
|
5103
5105
|
/** The segmentation provider behind `selectSubject` (see `setSegmentationProvider`). */
|
|
5104
5106
|
segmentation = null;
|
|
5105
5107
|
selectionWorker = null;
|
|
5108
|
+
documentOpener = null;
|
|
5106
5109
|
/** The effects provider slot (see `setEffectsProvider`). */
|
|
5107
5110
|
effects = new EffectsSlot();
|
|
5108
5111
|
/** Renders provider effects: effect smart filter results and effect layers. */
|
|
@@ -5114,6 +5117,7 @@ var ImageEditorController = class {
|
|
|
5114
5117
|
if (options.effects) this.effects.set(options.effects);
|
|
5115
5118
|
if (options.segmentation) this.segmentation = options.segmentation;
|
|
5116
5119
|
if (options.selectionWorker) this.selectionWorker = new SelectionWorkerClient(options.selectionWorker);
|
|
5120
|
+
if (options.openDocument) this.documentOpener = options.openDocument;
|
|
5117
5121
|
this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
|
|
5118
5122
|
if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
|
|
5119
5123
|
this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
|
|
@@ -5311,6 +5315,70 @@ var ImageEditorController = class {
|
|
|
5311
5315
|
this.selectionWorker?.terminate();
|
|
5312
5316
|
this.selectionWorker = factory ? new SelectionWorkerClient(factory) : null;
|
|
5313
5317
|
}
|
|
5318
|
+
/** Sets (or with `null` removes) how new documents are opened (see `DocumentOpener`). Not an edit. */
|
|
5319
|
+
setDocumentOpener(opener) {
|
|
5320
|
+
if (this.documentOpener === opener) return;
|
|
5321
|
+
this.documentOpener = opener;
|
|
5322
|
+
this.notify();
|
|
5323
|
+
}
|
|
5324
|
+
/** Whether the host can open new documents (the New Document outputs are available). */
|
|
5325
|
+
get canOpenDocuments() {
|
|
5326
|
+
return this.documentOpener !== null;
|
|
5327
|
+
}
|
|
5328
|
+
/**
|
|
5329
|
+
* A new one-layer document from the selection (or `options.selection`) on a pixel layer
|
|
5330
|
+
* (`layerId`, default the active one), the size of this image: the selected pixels on
|
|
5331
|
+
* transparency, or with `withMask` the whole layer masked by the selection. This document is
|
|
5332
|
+
* not changed. `decontaminate` (0..100) cleans the soft edge's colours as the new-layer
|
|
5333
|
+
* outputs do.
|
|
5334
|
+
*/
|
|
5335
|
+
documentFromSelection(options = {}) {
|
|
5336
|
+
const id = options.layerId ?? this.state.activeLayerId;
|
|
5337
|
+
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
|
|
5338
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
5339
|
+
if (layer.type !== "pixel") throw new LayerOperationError(`A new document needs a pixel layer; "${id}" is a ${layer.type} layer.`);
|
|
5340
|
+
const sel = options.selection ?? this.state.selection;
|
|
5341
|
+
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what goes in the new document first.");
|
|
5342
|
+
const { width, height } = this.naturalSize;
|
|
5343
|
+
const surface = this.surfaceOf(layer);
|
|
5344
|
+
let copy;
|
|
5345
|
+
if (options.withMask) {
|
|
5346
|
+
let mask = this.maskFromSelection(sel);
|
|
5347
|
+
if (layer.offset.x || layer.offset.y) mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };
|
|
5348
|
+
copy = { ...layer, id: newLayerId(), surface, mask };
|
|
5349
|
+
} else {
|
|
5350
|
+
const sb = sel.bounds;
|
|
5351
|
+
const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };
|
|
5352
|
+
const px2 = readRegion(surface, src);
|
|
5353
|
+
const data = sel.data;
|
|
5354
|
+
for (let y = 0; y < sb.height; y++) {
|
|
5355
|
+
for (let x = 0; x < sb.width; x++) px2[(y * sb.width + x) * 4 + 3] = Math.round(px2[(y * sb.width + x) * 4 + 3] * (data[(sb.y + y) * width + sb.x + x] / 255));
|
|
5356
|
+
}
|
|
5357
|
+
copy = { ...layer, id: newLayerId(), surface: surfaceFromPixels({ width: src.width, height: src.height, data: px2 }, 4, src.x, src.y) };
|
|
5358
|
+
delete copy.mask;
|
|
5359
|
+
}
|
|
5360
|
+
delete copy.source;
|
|
5361
|
+
copy.visible = true;
|
|
5362
|
+
const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
|
|
5363
|
+
if (amount > 0) copy = { ...copy, surface: this.decontaminatedSurface(copy, layer, sel, amount, Math.max(4, options.radius ?? 0)) };
|
|
5364
|
+
const { layers, assets, smartContents } = serializeLayers([copy]);
|
|
5365
|
+
const out = {
|
|
5366
|
+
version: LAYERED_SCHEMA_VERSION,
|
|
5367
|
+
width,
|
|
5368
|
+
height,
|
|
5369
|
+
layers,
|
|
5370
|
+
assets,
|
|
5371
|
+
activeLayerId: copy.id,
|
|
5372
|
+
crop: null,
|
|
5373
|
+
transform: { ...DEFAULT_TRANSFORM },
|
|
5374
|
+
opacity: 1
|
|
5375
|
+
};
|
|
5376
|
+
if (smartContents) out.smartContents = smartContents;
|
|
5377
|
+
if (this.state.bitDepth === 16 || this.state.bitDepth === 32) out.bitDepth = this.state.bitDepth;
|
|
5378
|
+
if (this.state.colorProfile) out.colorProfile = serializeColorProfile(this.state.colorProfile);
|
|
5379
|
+
if (this.state.colorMode) out.colorMode = serializeColorMode(this.state.colorMode);
|
|
5380
|
+
return out;
|
|
5381
|
+
}
|
|
5314
5382
|
/** Whether the long selections run in a worker. */
|
|
5315
5383
|
get hasSelectionWorker() {
|
|
5316
5384
|
return this.selectionWorker !== null;
|
|
@@ -7837,9 +7905,46 @@ var ImageEditorController = class {
|
|
|
7837
7905
|
* (PatchMatch, as the Spot Healing Brush's content-aware mode), blended by the selection's
|
|
7838
7906
|
* coverage. `output` `current` paints into the layer; `new-layer` puts the fill on a new layer
|
|
7839
7907
|
* above it. Samples within `sampleRadius` px of the selection (default its larger side, at
|
|
7840
|
-
* 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.
|
|
7841
7910
|
*/
|
|
7842
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 = {}) {
|
|
7843
7948
|
const sel = this.state.selection;
|
|
7844
7949
|
if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
|
|
7845
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.");
|
|
@@ -7848,33 +7953,70 @@ var ImageEditorController = class {
|
|
|
7848
7953
|
const target = this.paintTarget(options.layerId, "pixels", "fill");
|
|
7849
7954
|
const { width, height } = this.naturalSize;
|
|
7850
7955
|
const b = sel.bounds;
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
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;
|
|
7862
7980
|
for (let y = 0; y < win.height; y++) {
|
|
7863
7981
|
for (let x = 0; x < win.width; x++) {
|
|
7864
|
-
const c =
|
|
7982
|
+
const c = selData[(win.y + y) * sel.width + win.x + x];
|
|
7865
7983
|
coverage[y * win.width + x] = c;
|
|
7866
7984
|
hole[y * win.width + x] = c > 0 ? 1 : 0;
|
|
7867
7985
|
}
|
|
7868
7986
|
}
|
|
7869
|
-
|
|
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;
|
|
7870
8005
|
const out = new Uint8ClampedArray(before.length);
|
|
7871
8006
|
for (let i = 0; i < coverage.length; i++) {
|
|
7872
|
-
const k = coverage[i];
|
|
8007
|
+
const k = coverage[i] / 255;
|
|
7873
8008
|
for (let c = 0; c < 4; c++) {
|
|
7874
8009
|
const p = i * 4 + c;
|
|
7875
8010
|
out[p] = output === "new-layer" ? c === 3 ? Math.round(filled[p] * k) : filled[p] : before[p] + (filled[p] - before[p]) * k;
|
|
7876
8011
|
}
|
|
7877
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);
|
|
7878
8020
|
if (output === "new-layer") {
|
|
7879
8021
|
const crop = { x: b.x - win.x, y: b.y - win.y, width: b.width, height: b.height };
|
|
7880
8022
|
const data = new Uint8ClampedArray(b.width * b.height * 4);
|
|
@@ -7882,6 +8024,7 @@ var ImageEditorController = class {
|
|
|
7882
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 });
|
|
7883
8025
|
return { layerId: id, bounds: { ...b }, output };
|
|
7884
8026
|
}
|
|
8027
|
+
const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
|
|
7885
8028
|
const surface = writeRegion(target.surface, layerRect, out);
|
|
7886
8029
|
this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
|
|
7887
8030
|
return { layerId: target.id, bounds: { ...b }, output };
|
|
@@ -8752,8 +8895,12 @@ var ImageEditorController = class {
|
|
|
8752
8895
|
throw new Error("Object aware refinement needs a segmentation model (setSegmentationProvider); use refineMode color.");
|
|
8753
8896
|
}
|
|
8754
8897
|
const amount = Math.min(100, Math.max(0, options.decontaminate ?? 0)) / 100;
|
|
8755
|
-
const
|
|
8756
|
-
|
|
8898
|
+
const toDocument = output === "new-document" || output === "new-document-with-mask";
|
|
8899
|
+
const toLayer = output === "new-layer" || output === "new-layer-with-mask" || toDocument;
|
|
8900
|
+
if (amount > 0 && !toLayer) throw new Error("Decontaminate colors writes the cleaned colours to a new layer or document: use output new-layer, new-layer-with-mask, new-document or new-document-with-mask.");
|
|
8901
|
+
if (toDocument && !this.documentOpener) {
|
|
8902
|
+
throw new Error("This editor cannot open new documents (no document opener is set); use output new-layer or new-layer-with-mask instead.");
|
|
8903
|
+
}
|
|
8757
8904
|
let layerId;
|
|
8758
8905
|
if (output !== "selection") {
|
|
8759
8906
|
layerId = options.layerId ?? this.state.activeLayerId ?? void 0;
|
|
@@ -8778,6 +8925,12 @@ var ImageEditorController = class {
|
|
|
8778
8925
|
}
|
|
8779
8926
|
const refined = refineSelection(pixels, sel, params, edited.refineBrush);
|
|
8780
8927
|
if (!refined.bounds) throw new Error("Refining left nothing selected; use a smaller shiftEdge or contrast.");
|
|
8928
|
+
if (toDocument) {
|
|
8929
|
+
const document2 = this.documentFromSelection({ layerId, withMask: output === "new-document-with-mask", selection: refined, decontaminate: amount * 100, radius: params.radius });
|
|
8930
|
+
const name = `${requireLayer(this.state.layers, layerId).layer.name || "Layer"} ${output === "new-document" ? "cutout" : "masked"}`;
|
|
8931
|
+
const documentId = await this.documentOpener(document2, { name, from: "select-and-mask" });
|
|
8932
|
+
return { output, layerId, refineMode, ...documentId ? { documentId } : {} };
|
|
8933
|
+
}
|
|
8781
8934
|
return this.asOneStep(() => {
|
|
8782
8935
|
this.setSelection(refined);
|
|
8783
8936
|
if (output === "selection") return { output, refineMode };
|
|
@@ -8808,6 +8961,16 @@ var ImageEditorController = class {
|
|
|
8808
8961
|
const layer = requireLayer(this.state.layers, id).layer;
|
|
8809
8962
|
const source = requireLayer(this.state.layers, sourceId).layer;
|
|
8810
8963
|
if (layer.type !== "pixel" || source.type !== "pixel" || !coverage.bounds) return;
|
|
8964
|
+
const surface = this.decontaminatedSurface(layer, source, coverage, amount, radius);
|
|
8965
|
+
const next = mapLayer(this.state.layers, id, (l) => {
|
|
8966
|
+
const updated = { ...l, surface };
|
|
8967
|
+
delete updated.source;
|
|
8968
|
+
return updated;
|
|
8969
|
+
});
|
|
8970
|
+
this.commitLayers(next);
|
|
8971
|
+
}
|
|
8972
|
+
/** `layer`'s surface with Decontaminate Colors applied along `coverage`'s soft edge, colours read from `source`. */
|
|
8973
|
+
decontaminatedSurface(layer, source, coverage, amount, radius) {
|
|
8811
8974
|
const { width, height } = this.naturalSize;
|
|
8812
8975
|
const pad = Math.ceil(radius * 3);
|
|
8813
8976
|
const b = coverage.bounds;
|
|
@@ -8823,12 +8986,7 @@ var ImageEditorController = class {
|
|
|
8823
8986
|
for (let y = 0; y < box.height; y++) cov.set(all.subarray((box.y + y) * width + box.x, (box.y + y) * width + box.x + box.width), y * box.width);
|
|
8824
8987
|
const cleaned = decontaminateColors(original, cov, box.width, box.height, amount, radius);
|
|
8825
8988
|
for (let i = 3; i < cleaned.length; i += 4) cleaned[i] = rgba[i];
|
|
8826
|
-
|
|
8827
|
-
const updated = { ...l, surface: writeRegion(surface, src, cleaned) };
|
|
8828
|
-
delete updated.source;
|
|
8829
|
-
return updated;
|
|
8830
|
-
});
|
|
8831
|
-
this.commitLayers(next);
|
|
8989
|
+
return writeRegion(surface, src, cleaned);
|
|
8832
8990
|
}
|
|
8833
8991
|
/**
|
|
8834
8992
|
* Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or
|
|
@@ -11805,7 +11963,9 @@ var REFINE_OUTPUTS = [
|
|
|
11805
11963
|
{ id: "selection", label: "Selection" },
|
|
11806
11964
|
{ id: "layer-mask", label: "Layer Mask" },
|
|
11807
11965
|
{ id: "new-layer", label: "New Layer" },
|
|
11808
|
-
{ id: "new-layer-with-mask", label: "New Layer with Layer Mask" }
|
|
11966
|
+
{ id: "new-layer-with-mask", label: "New Layer with Layer Mask" },
|
|
11967
|
+
{ id: "new-document", label: "New Document", needsDocuments: true },
|
|
11968
|
+
{ id: "new-document-with-mask", label: "New Document with Layer Mask", needsDocuments: true }
|
|
11809
11969
|
];
|
|
11810
11970
|
function describeSelection(mask) {
|
|
11811
11971
|
if (!mask || !mask.bounds) return "No selection";
|
|
@@ -11841,6 +12001,13 @@ var rectProperties = {
|
|
|
11841
12001
|
width: { type: "number", exclusiveMinimum: 0, description: "Width in natural image pixels." },
|
|
11842
12002
|
height: { type: "number", exclusiveMinimum: 0, description: "Height in natural image pixels." }
|
|
11843
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
|
+
};
|
|
11844
12011
|
function shapeOptions(args) {
|
|
11845
12012
|
return { mode: parseSelectionMode(args.mode), feather: num2(args.feather, 0), antiAlias: args.antiAlias !== false };
|
|
11846
12013
|
}
|
|
@@ -12027,11 +12194,11 @@ function createImageSelectionTools(controller) {
|
|
|
12027
12194
|
shiftEdge: { type: "number", minimum: -100, maximum: 100, description: "Move the edge out (+) or in (-), in percent." },
|
|
12028
12195
|
output: {
|
|
12029
12196
|
type: "string",
|
|
12030
|
-
enum: ["selection", "layer-mask", "new-layer", "new-layer-with-mask"],
|
|
12031
|
-
description: "Where the result goes (default selection). layer-mask keeps every pixel and is easiest to revise; new-layer-with-mask too, on a copy."
|
|
12197
|
+
enum: ["selection", "layer-mask", "new-layer", "new-layer-with-mask", "new-document", "new-document-with-mask"],
|
|
12198
|
+
description: "Where the result goes (default selection). layer-mask keeps every pixel and is easiest to revise; new-layer-with-mask too, on a copy. new-document and new-document-with-mask hand a new one-layer document to the host (only when the host can open documents; this one is unchanged) and return its documentId."
|
|
12032
12199
|
},
|
|
12033
12200
|
layerId: { type: "string", description: "The layer for layer outputs (default the active layer; see list_layers). New-layer outputs need a pixel layer." },
|
|
12034
|
-
decontaminate: { type: "number", minimum: 0, maximum: 100, description: "Decontaminate Colors amount 0-100 (default 0), only with
|
|
12201
|
+
decontaminate: { type: "number", minimum: 0, maximum: 100, description: "Decontaminate Colors amount 0-100 (default 0), only with the new-layer and new-document outputs." },
|
|
12035
12202
|
edits: {
|
|
12036
12203
|
type: "array",
|
|
12037
12204
|
description: "Strokes applied before refining, in order.",
|
|
@@ -12694,25 +12861,33 @@ function createImageSelectionTools(controller) {
|
|
|
12694
12861
|
const fillTool = {
|
|
12695
12862
|
name: "content_aware_fill",
|
|
12696
12863
|
title: "Content-Aware Fill",
|
|
12697
|
-
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.",
|
|
12698
12865
|
inputSchema: {
|
|
12699
12866
|
type: "object",
|
|
12700
12867
|
properties: {
|
|
12701
12868
|
output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
|
|
12702
12869
|
sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
|
|
12703
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)." },
|
|
12704
12874
|
layerId: layerIdSchema
|
|
12705
12875
|
},
|
|
12706
12876
|
additionalProperties: false
|
|
12707
12877
|
},
|
|
12708
12878
|
annotations: writeAnnotations("Content-Aware Fill"),
|
|
12709
|
-
handler: (args) => {
|
|
12879
|
+
handler: async (args, context) => {
|
|
12710
12880
|
if (!controller.getSelection()) throw new Error("Nothing is selected; select what to remove first.");
|
|
12711
|
-
|
|
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({
|
|
12712
12883
|
output: args.output,
|
|
12713
12884
|
sampleRadius: args.sampleRadius,
|
|
12714
12885
|
seed: args.seed,
|
|
12715
|
-
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
|
|
12716
12891
|
});
|
|
12717
12892
|
}
|
|
12718
12893
|
};
|
|
@@ -17877,7 +18052,7 @@ var SelectAndMaskSession = class {
|
|
|
17877
18052
|
remembered = s.remember ? { ...s, params: { ...s.params } } : null;
|
|
17878
18053
|
return this.controller.refineSelectionTo(s.params, s.output, {
|
|
17879
18054
|
edits: [...this.editsList],
|
|
17880
|
-
decontaminate: s.output === "
|
|
18055
|
+
decontaminate: s.output === "selection" || s.output === "layer-mask" ? 0 : s.decontaminate,
|
|
17881
18056
|
refineMode: s.refineMode
|
|
17882
18057
|
});
|
|
17883
18058
|
}
|
|
@@ -19539,6 +19714,278 @@ function createImageEditorDocument(controller, options = {}) {
|
|
|
19539
19714
|
}
|
|
19540
19715
|
});
|
|
19541
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
|
+
};
|
|
19542
19989
|
export {
|
|
19543
19990
|
ADJUSTMENT_KEYS,
|
|
19544
19991
|
ADJUSTMENT_KINDS,
|
|
@@ -19564,7 +20011,10 @@ export {
|
|
|
19564
20011
|
COLOR_MODE_OPTIONS,
|
|
19565
20012
|
COLOR_RANGE_PRESETS,
|
|
19566
20013
|
COLOR_RANGE_PRESET_LABELS,
|
|
20014
|
+
CONTENT_AWARE_FILL_SAMPLING,
|
|
20015
|
+
CONTENT_AWARE_FILL_TOOLS,
|
|
19567
20016
|
ColorGmm,
|
|
20017
|
+
ContentAwareFillSession,
|
|
19568
20018
|
CoverageMap,
|
|
19569
20019
|
D50,
|
|
19570
20020
|
D65,
|
|
@@ -19574,6 +20024,7 @@ export {
|
|
|
19574
20024
|
DEFAULT_BRUSH,
|
|
19575
20025
|
DEFAULT_CANVAS_VIEWPORT,
|
|
19576
20026
|
DEFAULT_CHANNEL_COLOR,
|
|
20027
|
+
DEFAULT_CONTENT_AWARE_FILL_SETTINGS,
|
|
19577
20028
|
DEFAULT_DEPTH_MASK,
|
|
19578
20029
|
DEFAULT_FLOW,
|
|
19579
20030
|
DEFAULT_GLOBAL_LIGHT,
|