@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.
@@ -67,3 +67,4 @@ export * from './provider-effect-tools.cjs';
67
67
  export * from './playback.cjs';
68
68
  export * from './tool-groups.cjs';
69
69
  export * from './editorDocument.cjs';
70
+ export * from './content-aware-fill-session.cjs';
@@ -67,3 +67,4 @@ export * from './provider-effect-tools.js';
67
67
  export * from './playback.js';
68
68
  export * from './tool-groups.js';
69
69
  export * from './editorDocument.js';
70
+ export * from './content-aware-fill-session.js';
@@ -52,7 +52,7 @@ import {
52
52
  workImage,
53
53
  workLuma,
54
54
  workPlanes
55
- } from "../chunk-DWYQPL2A.js";
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-USQCKRZG.js";
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. 8-bit documents only for now.
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
- const margin = Math.max(32, Math.round(options.sampleRadius ?? Math.max(b.width, b.height)));
7918
- const x0 = Math.max(0, b.x - margin);
7919
- const y0 = Math.max(0, b.y - margin);
7920
- const 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 };
7921
- 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 lower sampleRadius.");
7922
- const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
7923
- const before = readRegion(target.surface, layerRect);
7924
- const img = new Float32Array(before.length);
7925
- for (let i = 0; i < before.length; i++) img[i] = before[i];
7926
- const coverage = new Float32Array(win.width * win.height);
7927
- const hole = new Uint8Array(win.width * win.height);
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 = sel.data[(win.y + y) * sel.width + win.x + x] / 255;
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
- const filled = contentAwareFill(win.width, win.height, 4, img, hole, { seed: options.seed ?? 0 });
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
- return controller.contentAwareFill({
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 _chunkX4LUGTQXcjs = require('../chunk-X4LUGTQX.cjs');
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
- _chunkX4LUGTQXcjs.startSelectionWorker.call(void 0, self);
11
+ _chunkL52VMQZScjs.startSelectionWorker.call(void 0, self);
11
12
  }
12
13
 
13
14
 
14
- exports.startSelectionWorker = _chunkX4LUGTQXcjs.startSelectionWorker;
15
+ exports.startSelectionWorker = _chunkL52VMQZScjs.startSelectionWorker;
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  startSelectionWorker
3
- } from "../chunk-DWYQPL2A.js";
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
@@ -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;