@pixerate/editor 0.211.0 → 0.212.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@
5
5
  * applies it as one undo step through `contentAwareFillAsync` (the same fill the
6
6
  * `content_aware_fill` tool makes, with a sampling area).
7
7
  */
8
- import type { ImageEditorController } from './controller.cjs';
8
+ import type { ContentAwareFillOutput, ImageEditorController } from './controller.cjs';
9
9
  import type { CropBox, ImagePoint } from './types.cjs';
10
10
  import { type SelectionMask } from './selection/mask.cjs';
11
11
  export type ContentAwareFillSampling = 'auto' | 'rectangular' | 'custom';
@@ -32,7 +32,7 @@ export interface ContentAwareFillSettings {
32
32
  brushMode: 'add' | 'subtract';
33
33
  /** The fill's random seed; another gives another fill to choose from. */
34
34
  seed: number;
35
- output: 'current' | 'new-layer';
35
+ output: ContentAwareFillOutput;
36
36
  /** Update the preview as the area changes (otherwise with Preview). */
37
37
  livePreview: boolean;
38
38
  }
@@ -61,11 +61,23 @@ export declare class ContentAwareFillSession {
61
61
  get window(): CropBox | null;
62
62
  subscribe(listener: () => void): () => void;
63
63
  private emit;
64
+ /** Images up to this many pixels show (and can be sampled) whole in the workspace. */
65
+ static readonly WHOLE_IMAGE_PIXELS = 4000000;
66
+ /** Room around the selection the window holds, in px each way. */
67
+ private margin;
64
68
  /**
65
- * Reads the layer around the selection and makes the first preview. The window holds the
66
- * selection and room around it to sample from (its larger side, at least 64 px, each way).
69
+ * Reads the layer and makes the first preview. The window is the whole image up to
70
+ * {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
71
+ * its larger side, at least 256 px, each way), and {@link expandView} widens it.
67
72
  */
68
73
  open(): Promise<boolean>;
74
+ /** Whether the window shows the whole image (else {@link expandView} can widen it). */
75
+ get showsWholeImage(): boolean;
76
+ /**
77
+ * Doubles the room around the selection (up to the whole image), keeping what was painted; a
78
+ * preset fills the new room as it would. Returns false when the whole image already shows.
79
+ */
80
+ expandView(): boolean;
69
81
  /** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
70
82
  private applySampling;
71
83
  /** Changes settings; a sampling preset replaces the area, and the preview follows when live. */
@@ -104,6 +116,6 @@ export declare class ContentAwareFillSession {
104
116
  }): Promise<{
105
117
  layerId: string;
106
118
  bounds: CropBox;
107
- output: 'current' | 'new-layer';
119
+ output: ContentAwareFillOutput;
108
120
  }>;
109
121
  }
@@ -5,7 +5,7 @@
5
5
  * applies it as one undo step through `contentAwareFillAsync` (the same fill the
6
6
  * `content_aware_fill` tool makes, with a sampling area).
7
7
  */
8
- import type { ImageEditorController } from './controller.js';
8
+ import type { ContentAwareFillOutput, ImageEditorController } from './controller.js';
9
9
  import type { CropBox, ImagePoint } from './types.js';
10
10
  import { type SelectionMask } from './selection/mask.js';
11
11
  export type ContentAwareFillSampling = 'auto' | 'rectangular' | 'custom';
@@ -32,7 +32,7 @@ export interface ContentAwareFillSettings {
32
32
  brushMode: 'add' | 'subtract';
33
33
  /** The fill's random seed; another gives another fill to choose from. */
34
34
  seed: number;
35
- output: 'current' | 'new-layer';
35
+ output: ContentAwareFillOutput;
36
36
  /** Update the preview as the area changes (otherwise with Preview). */
37
37
  livePreview: boolean;
38
38
  }
@@ -61,11 +61,23 @@ export declare class ContentAwareFillSession {
61
61
  get window(): CropBox | null;
62
62
  subscribe(listener: () => void): () => void;
63
63
  private emit;
64
+ /** Images up to this many pixels show (and can be sampled) whole in the workspace. */
65
+ static readonly WHOLE_IMAGE_PIXELS = 4000000;
66
+ /** Room around the selection the window holds, in px each way. */
67
+ private margin;
64
68
  /**
65
- * Reads the layer around the selection and makes the first preview. The window holds the
66
- * selection and room around it to sample from (its larger side, at least 64 px, each way).
69
+ * Reads the layer and makes the first preview. The window is the whole image up to
70
+ * {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
71
+ * its larger side, at least 256 px, each way), and {@link expandView} widens it.
67
72
  */
68
73
  open(): Promise<boolean>;
74
+ /** Whether the window shows the whole image (else {@link expandView} can widen it). */
75
+ get showsWholeImage(): boolean;
76
+ /**
77
+ * Doubles the room around the selection (up to the whole image), keeping what was painted; a
78
+ * preset fills the new room as it would. Returns false when the whole image already shows.
79
+ */
80
+ expandView(): boolean;
69
81
  /** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
70
82
  private applySampling;
71
83
  /** Changes settings; a sampling preset replaces the area, and the preview follows when live. */
@@ -104,6 +116,6 @@ export declare class ContentAwareFillSession {
104
116
  }): Promise<{
105
117
  layerId: string;
106
118
  bounds: CropBox;
107
- output: 'current' | 'new-layer';
119
+ output: ContentAwareFillOutput;
108
120
  }>;
109
121
  }
@@ -479,12 +479,17 @@ export interface StrokeSelectionOptions extends Omit<FillOptions, 'useSelection'
479
479
  /** Where the band lies against the selection edge (default `outside`). */
480
480
  location?: 'inside' | 'center' | 'outside';
481
481
  }
482
+ /** Where Content-Aware Fill puts its result. */
483
+ export type ContentAwareFillOutput = 'current' | 'new-layer' | 'duplicate-layer';
482
484
  /** Edit › Content-Aware Fill (see `contentAwareFill`). */
483
485
  export interface ContentAwareFillOptions {
484
486
  /** The pixel layer to sample and fill (default the active layer). */
485
487
  layerId?: string;
486
- /** `current` (default) paints into the layer; `new-layer` puts the fill on a new layer above it. */
487
- output?: 'current' | 'new-layer';
488
+ /**
489
+ * `current` (default) paints into the layer; `new-layer` puts the fill on a new layer above it;
490
+ * `duplicate-layer` paints it into a copy of the layer, above it.
491
+ */
492
+ output?: ContentAwareFillOutput;
488
493
  /** How far around the selection to sample, in pixels (default its larger side, at least 32). */
489
494
  sampleRadius?: number;
490
495
  /** Random seed: the same seed gives the same fill (default 0). */
@@ -1528,7 +1533,7 @@ export declare class ImageEditorController {
1528
1533
  contentAwareFill(options?: ContentAwareFillOptions): {
1529
1534
  layerId: string;
1530
1535
  bounds: CropBox;
1531
- output: 'current' | 'new-layer';
1536
+ output: ContentAwareFillOutput;
1532
1537
  };
1533
1538
  /**
1534
1539
  * `contentAwareFill` with the synthesis in the selection worker when there is one, with
@@ -1540,7 +1545,7 @@ export declare class ImageEditorController {
1540
1545
  }): Promise<{
1541
1546
  layerId: string;
1542
1547
  bounds: CropBox;
1543
- output: 'current' | 'new-layer';
1548
+ output: ContentAwareFillOutput;
1544
1549
  }>;
1545
1550
  /**
1546
1551
  * The fill without changing anything: the window, its pixels before and after (RGBA, the fill
@@ -479,12 +479,17 @@ export interface StrokeSelectionOptions extends Omit<FillOptions, 'useSelection'
479
479
  /** Where the band lies against the selection edge (default `outside`). */
480
480
  location?: 'inside' | 'center' | 'outside';
481
481
  }
482
+ /** Where Content-Aware Fill puts its result. */
483
+ export type ContentAwareFillOutput = 'current' | 'new-layer' | 'duplicate-layer';
482
484
  /** Edit › Content-Aware Fill (see `contentAwareFill`). */
483
485
  export interface ContentAwareFillOptions {
484
486
  /** The pixel layer to sample and fill (default the active layer). */
485
487
  layerId?: string;
486
- /** `current` (default) paints into the layer; `new-layer` puts the fill on a new layer above it. */
487
- output?: 'current' | 'new-layer';
488
+ /**
489
+ * `current` (default) paints into the layer; `new-layer` puts the fill on a new layer above it;
490
+ * `duplicate-layer` paints it into a copy of the layer, above it.
491
+ */
492
+ output?: ContentAwareFillOutput;
488
493
  /** How far around the selection to sample, in pixels (default its larger side, at least 32). */
489
494
  sampleRadius?: number;
490
495
  /** Random seed: the same seed gives the same fill (default 0). */
@@ -1528,7 +1533,7 @@ export declare class ImageEditorController {
1528
1533
  contentAwareFill(options?: ContentAwareFillOptions): {
1529
1534
  layerId: string;
1530
1535
  bounds: CropBox;
1531
- output: 'current' | 'new-layer';
1536
+ output: ContentAwareFillOutput;
1532
1537
  };
1533
1538
  /**
1534
1539
  * `contentAwareFill` with the synthesis in the selection worker when there is one, with
@@ -1540,7 +1545,7 @@ export declare class ImageEditorController {
1540
1545
  }): Promise<{
1541
1546
  layerId: string;
1542
1547
  bounds: CropBox;
1543
- output: 'current' | 'new-layer';
1548
+ output: ContentAwareFillOutput;
1544
1549
  }>;
1545
1550
  /**
1546
1551
  * The fill without changing anything: the window, its pixels before and after (RGBA, the fill
@@ -8139,7 +8139,9 @@ var ImageEditorController = (_class12 = class {
8139
8139
  if (!_optionalChain([sel, 'optionalAccess', _322 => _322.bounds])) throw new (0, _chunkH65IBXFIcjs.LayerOperationError)("Nothing is selected; select what to remove, then fill it.");
8140
8140
  if (this.bitDepth !== 8) throw new (0, _chunkH65IBXFIcjs.LayerOperationError)("Content-Aware Fill works on 8-bit documents for now; set_bit_depth 8 first, or use spot_heal.");
8141
8141
  const output = _nullishCoalesce(options.output, () => ( "current"));
8142
- if (output !== "current" && output !== "new-layer") throw new (0, _chunkH65IBXFIcjs.LayerOperationError)(`Unknown output "${String(output)}"; use current or new-layer.`);
8142
+ if (output !== "current" && output !== "new-layer" && output !== "duplicate-layer") {
8143
+ throw new (0, _chunkH65IBXFIcjs.LayerOperationError)(`Unknown output "${String(output)}"; use current, new-layer or duplicate-layer.`);
8144
+ }
8143
8145
  const target = this.paintTarget(options.layerId, "pixels", "fill");
8144
8146
  const { width, height } = this.naturalSize;
8145
8147
  const b = sel.bounds;
@@ -8216,6 +8218,13 @@ var ImageEditorController = (_class12 = class {
8216
8218
  }
8217
8219
  const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
8218
8220
  const surface = _chunk474IQG74cjs.writeRegion.call(void 0, target.surface, layerRect, out);
8221
+ if (output === "duplicate-layer") {
8222
+ return this.asOneStep(() => {
8223
+ const copy = this.duplicateLayer(target.id);
8224
+ this.commitLayers(_chunkUOGKSVRQcjs.mapLayer.call(void 0, this.state.layers, copy, (l) => this.withSurface(l, "pixels", surface)));
8225
+ return { layerId: copy, bounds: { ...b }, output };
8226
+ });
8227
+ }
8219
8228
  this.commitLayers(_chunkUOGKSVRQcjs.mapLayer.call(void 0, this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
8220
8229
  return { layerId: target.id, bounds: { ...b }, output };
8221
8230
  }
@@ -13075,7 +13084,7 @@ function createImageSelectionTools(controller) {
13075
13084
  inputSchema: {
13076
13085
  type: "object",
13077
13086
  properties: {
13078
- output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
13087
+ output: { type: "string", enum: ["new-layer", "current", "duplicate-layer"], description: "new-layer (the fill alone on a layer above), current (into the layer) or duplicate-layer (into a copy of the layer, above it); default current." },
13079
13088
  sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
13080
13089
  seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
13081
13090
  sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
@@ -20188,8 +20197,8 @@ var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
20188
20197
  output: "new-layer",
20189
20198
  livePreview: true
20190
20199
  };
20191
- var ContentAwareFillSession = (_class17 = class {
20192
- constructor(controller) {;_class17.prototype.__init149.call(this);_class17.prototype.__init150.call(this);_class17.prototype.__init151.call(this);_class17.prototype.__init152.call(this);_class17.prototype.__init153.call(this);_class17.prototype.__init154.call(this);_class17.prototype.__init155.call(this);_class17.prototype.__init156.call(this);_class17.prototype.__init157.call(this);_class17.prototype.__init158.call(this);_class17.prototype.__init159.call(this);_class17.prototype.__init160.call(this);_class17.prototype.__init161.call(this);_class17.prototype.__init162.call(this);
20200
+ var ContentAwareFillSession = (_class17 = class _ContentAwareFillSession {
20201
+ constructor(controller) {;_class17.prototype.__init149.call(this);_class17.prototype.__init150.call(this);_class17.prototype.__init151.call(this);_class17.prototype.__init152.call(this);_class17.prototype.__init153.call(this);_class17.prototype.__init154.call(this);_class17.prototype.__init155.call(this);_class17.prototype.__init156.call(this);_class17.prototype.__init157.call(this);_class17.prototype.__init158.call(this);_class17.prototype.__init159.call(this);_class17.prototype.__init160.call(this);_class17.prototype.__init161.call(this);_class17.prototype.__init162.call(this);_class17.prototype.__init163.call(this);
20193
20202
  this.controller = controller;
20194
20203
  }
20195
20204
 
@@ -20224,16 +20233,21 @@ var ContentAwareFillSession = (_class17 = class {
20224
20233
  this.revision++;
20225
20234
  for (const l of this.listeners) l();
20226
20235
  }
20236
+ /** Images up to this many pixels show (and can be sampled) whole in the workspace. */
20237
+ static __initStatic() {this.WHOLE_IMAGE_PIXELS = 4e6}
20238
+ /** Room around the selection the window holds, in px each way. */
20239
+ __init162() {this.margin = 0}
20227
20240
  /**
20228
- * Reads the layer around the selection and makes the first preview. The window holds the
20229
- * selection and room around it to sample from (its larger side, at least 64 px, each way).
20241
+ * Reads the layer and makes the first preview. The window is the whole image up to
20242
+ * {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
20243
+ * its larger side, at least 256 px, each way), and {@link expandView} widens it.
20230
20244
  */
20231
20245
  async open() {
20232
20246
  try {
20233
- const sel = this.controller.getSelection();
20234
- const b = _optionalChain([sel, 'optionalAccess', _682 => _682.bounds]);
20235
- const margin = b ? Math.max(64, Math.max(b.width, b.height)) : 64;
20236
- this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
20247
+ const { width, height } = this.controller.getState().imageDimensions;
20248
+ const b = _optionalChain([this, 'access', _682 => _682.controller, 'access', _683 => _683.getSelection, 'call', _684 => _684(), 'optionalAccess', _685 => _685.bounds]);
20249
+ this.margin = width * height <= _ContentAwareFillSession.WHOLE_IMAGE_PIXELS ? Math.max(width, height) : Math.max(256, 2 * Math.max(_nullishCoalesce(_optionalChain([b, 'optionalAccess', _686 => _686.width]), () => ( 0)), _nullishCoalesce(_optionalChain([b, 'optionalAccess', _687 => _687.height]), () => ( 0))));
20250
+ this.plan = this.controller.prepareContentAwareFill({ sampleRadius: this.margin });
20237
20251
  } catch (err) {
20238
20252
  this.error = err instanceof Error ? err.message : String(err);
20239
20253
  this.emit();
@@ -20247,6 +20261,42 @@ var ContentAwareFillSession = (_class17 = class {
20247
20261
  await this.refresh();
20248
20262
  return true;
20249
20263
  }
20264
+ /** Whether the window shows the whole image (else {@link expandView} can widen it). */
20265
+ get showsWholeImage() {
20266
+ const { width, height } = this.controller.getState().imageDimensions;
20267
+ const w = this.win;
20268
+ return !w || w.x === 0 && w.y === 0 && w.width >= width && w.height >= height;
20269
+ }
20270
+ /**
20271
+ * Doubles the room around the selection (up to the whole image), keeping what was painted; a
20272
+ * preset fills the new room as it would. Returns false when the whole image already shows.
20273
+ */
20274
+ expandView() {
20275
+ if (!this.win || this.showsWholeImage) return false;
20276
+ const { width, height } = this.controller.getState().imageDimensions;
20277
+ const old = { win: this.win, area: this.area };
20278
+ this.margin = Math.min(Math.max(width, height), this.margin * 2);
20279
+ const plan = this.controller.prepareContentAwareFill({ layerId: this.plan.layerId, sampleRadius: this.margin });
20280
+ this.plan = plan;
20281
+ this.win = plan.window;
20282
+ this.before = plan.before;
20283
+ this.hole = plan.hole;
20284
+ const mode = this.settingsValue.sampling;
20285
+ this.area = null;
20286
+ this.applySampling(mode === "custom" ? "auto" : mode);
20287
+ if (mode === "custom") {
20288
+ const area = new Uint8Array(plan.window.width * plan.window.height);
20289
+ for (let y = 0; y < old.win.height; y++) {
20290
+ const src = y * old.win.width;
20291
+ const dst = (old.win.y - plan.window.y + y) * plan.window.width + (old.win.x - plan.window.x);
20292
+ area.set(old.area.subarray(src, src + old.win.width), dst);
20293
+ }
20294
+ this.area = area;
20295
+ }
20296
+ this.emit();
20297
+ if (this.settingsValue.livePreview) void this.refresh();
20298
+ return true;
20299
+ }
20250
20300
  /** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
20251
20301
  applySampling(mode) {
20252
20302
  const win = this.win;
@@ -20328,7 +20378,7 @@ var ContentAwareFillSession = (_class17 = class {
20328
20378
  this.emit();
20329
20379
  return true;
20330
20380
  }
20331
- __init162() {this.strokeAdds = true}
20381
+ __init163() {this.strokeAdds = true}
20332
20382
  move(point) {
20333
20383
  if (!this.lastPoint) return;
20334
20384
  this.paint(this.lastPoint, point, this.strokeAdds);
@@ -20354,7 +20404,7 @@ var ContentAwareFillSession = (_class17 = class {
20354
20404
  this.dirty = false;
20355
20405
  try {
20356
20406
  const area = this.samplingMask();
20357
- if (!_optionalChain([area, 'optionalAccess', _683 => _683.bounds])) throw new Error("The sampling area is empty; paint where the fill may copy from.");
20407
+ if (!_optionalChain([area, 'optionalAccess', _688 => _688.bounds])) throw new Error("The sampling area is empty; paint where the fill may copy from.");
20358
20408
  const { plan, after } = await this.controller.previewContentAwareFill({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed });
20359
20409
  const win = this.win;
20360
20410
  const out = Uint8ClampedArray.from(this.before);
@@ -20433,10 +20483,10 @@ var ContentAwareFillSession = (_class17 = class {
20433
20483
  /** OK: fills with this sampling area, seed and output as one undo step. */
20434
20484
  async commit(options = {}) {
20435
20485
  const area = this.samplingMask();
20436
- if (!this.plan || !_optionalChain([area, 'optionalAccess', _684 => _684.bounds])) throw new Error(_nullishCoalesce(this.error, () => ( "The sampling area is empty; paint where the fill may copy from.")));
20486
+ if (!this.plan || !_optionalChain([area, 'optionalAccess', _689 => _689.bounds])) throw new Error(_nullishCoalesce(this.error, () => ( "The sampling area is empty; paint where the fill may copy from.")));
20437
20487
  return this.controller.contentAwareFillAsync({ layerId: this.plan.layerId, samplingArea: area, seed: this.settingsValue.seed, output: this.settingsValue.output, signal: options.signal });
20438
20488
  }
20439
- }, _class17);
20489
+ }, _class17.__initStatic(), _class17);
20440
20490
 
20441
20491
 
20442
20492
 
@@ -8139,7 +8139,9 @@ var ImageEditorController = class {
8139
8139
  if (!sel?.bounds) throw new LayerOperationError("Nothing is selected; select what to remove, then fill it.");
8140
8140
  if (this.bitDepth !== 8) throw new LayerOperationError("Content-Aware Fill works on 8-bit documents for now; set_bit_depth 8 first, or use spot_heal.");
8141
8141
  const output = options.output ?? "current";
8142
- if (output !== "current" && output !== "new-layer") throw new LayerOperationError(`Unknown output "${String(output)}"; use current or new-layer.`);
8142
+ if (output !== "current" && output !== "new-layer" && output !== "duplicate-layer") {
8143
+ throw new LayerOperationError(`Unknown output "${String(output)}"; use current, new-layer or duplicate-layer.`);
8144
+ }
8143
8145
  const target = this.paintTarget(options.layerId, "pixels", "fill");
8144
8146
  const { width, height } = this.naturalSize;
8145
8147
  const b = sel.bounds;
@@ -8216,6 +8218,13 @@ var ImageEditorController = class {
8216
8218
  }
8217
8219
  const layerRect = { x: win.x - target.offset.x, y: win.y - target.offset.y, width: win.width, height: win.height };
8218
8220
  const surface = writeRegion(target.surface, layerRect, out);
8221
+ if (output === "duplicate-layer") {
8222
+ return this.asOneStep(() => {
8223
+ const copy = this.duplicateLayer(target.id);
8224
+ this.commitLayers(mapLayer(this.state.layers, copy, (l) => this.withSurface(l, "pixels", surface)));
8225
+ return { layerId: copy, bounds: { ...b }, output };
8226
+ });
8227
+ }
8219
8228
  this.commitLayers(mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, "pixels", surface)));
8220
8229
  return { layerId: target.id, bounds: { ...b }, output };
8221
8230
  }
@@ -13075,7 +13084,7 @@ function createImageSelectionTools(controller) {
13075
13084
  inputSchema: {
13076
13085
  type: "object",
13077
13086
  properties: {
13078
- output: { type: "string", enum: ["new-layer", "current"], description: "new-layer or current (default current)." },
13087
+ output: { type: "string", enum: ["new-layer", "current", "duplicate-layer"], description: "new-layer (the fill alone on a layer above), current (into the layer) or duplicate-layer (into a copy of the layer, above it); default current." },
13079
13088
  sampleRadius: { type: "number", minimum: 32, maximum: 4e3, description: "Sample within this many pixels of the selection (default its larger side)." },
13080
13089
  seed: { type: "integer", minimum: 0, description: "Random seed; another seed gives another fill to choose from (default 0)." },
13081
13090
  sampleRects: { type: "array", minItems: 1, items: rectSchemaStrict, description: "Sample only from these rectangles (natural image pixels), instead of the ring around the selection." },
@@ -20188,7 +20197,7 @@ var DEFAULT_CONTENT_AWARE_FILL_SETTINGS = {
20188
20197
  output: "new-layer",
20189
20198
  livePreview: true
20190
20199
  };
20191
- var ContentAwareFillSession = class {
20200
+ var ContentAwareFillSession = class _ContentAwareFillSession {
20192
20201
  constructor(controller) {
20193
20202
  this.controller = controller;
20194
20203
  }
@@ -20224,16 +20233,21 @@ var ContentAwareFillSession = class {
20224
20233
  this.revision++;
20225
20234
  for (const l of this.listeners) l();
20226
20235
  }
20236
+ /** Images up to this many pixels show (and can be sampled) whole in the workspace. */
20237
+ static WHOLE_IMAGE_PIXELS = 4e6;
20238
+ /** Room around the selection the window holds, in px each way. */
20239
+ margin = 0;
20227
20240
  /**
20228
- * Reads the layer around the selection and makes the first preview. The window holds the
20229
- * selection and room around it to sample from (its larger side, at least 64 px, each way).
20241
+ * Reads the layer and makes the first preview. The window is the whole image up to
20242
+ * {@link WHOLE_IMAGE_PIXELS}; on a larger one it holds the selection and room around it (twice
20243
+ * its larger side, at least 256 px, each way), and {@link expandView} widens it.
20230
20244
  */
20231
20245
  async open() {
20232
20246
  try {
20233
- const sel = this.controller.getSelection();
20234
- const b = sel?.bounds;
20235
- const margin = b ? Math.max(64, Math.max(b.width, b.height)) : 64;
20236
- this.plan = this.controller.prepareContentAwareFill({ sampleRadius: margin });
20247
+ const { width, height } = this.controller.getState().imageDimensions;
20248
+ const b = this.controller.getSelection()?.bounds;
20249
+ this.margin = width * height <= _ContentAwareFillSession.WHOLE_IMAGE_PIXELS ? Math.max(width, height) : Math.max(256, 2 * Math.max(b?.width ?? 0, b?.height ?? 0));
20250
+ this.plan = this.controller.prepareContentAwareFill({ sampleRadius: this.margin });
20237
20251
  } catch (err) {
20238
20252
  this.error = err instanceof Error ? err.message : String(err);
20239
20253
  this.emit();
@@ -20247,6 +20261,42 @@ var ContentAwareFillSession = class {
20247
20261
  await this.refresh();
20248
20262
  return true;
20249
20263
  }
20264
+ /** Whether the window shows the whole image (else {@link expandView} can widen it). */
20265
+ get showsWholeImage() {
20266
+ const { width, height } = this.controller.getState().imageDimensions;
20267
+ const w = this.win;
20268
+ return !w || w.x === 0 && w.y === 0 && w.width >= width && w.height >= height;
20269
+ }
20270
+ /**
20271
+ * Doubles the room around the selection (up to the whole image), keeping what was painted; a
20272
+ * preset fills the new room as it would. Returns false when the whole image already shows.
20273
+ */
20274
+ expandView() {
20275
+ if (!this.win || this.showsWholeImage) return false;
20276
+ const { width, height } = this.controller.getState().imageDimensions;
20277
+ const old = { win: this.win, area: this.area };
20278
+ this.margin = Math.min(Math.max(width, height), this.margin * 2);
20279
+ const plan = this.controller.prepareContentAwareFill({ layerId: this.plan.layerId, sampleRadius: this.margin });
20280
+ this.plan = plan;
20281
+ this.win = plan.window;
20282
+ this.before = plan.before;
20283
+ this.hole = plan.hole;
20284
+ const mode = this.settingsValue.sampling;
20285
+ this.area = null;
20286
+ this.applySampling(mode === "custom" ? "auto" : mode);
20287
+ if (mode === "custom") {
20288
+ const area = new Uint8Array(plan.window.width * plan.window.height);
20289
+ for (let y = 0; y < old.win.height; y++) {
20290
+ const src = y * old.win.width;
20291
+ const dst = (old.win.y - plan.window.y + y) * plan.window.width + (old.win.x - plan.window.x);
20292
+ area.set(old.area.subarray(src, src + old.win.width), dst);
20293
+ }
20294
+ this.area = area;
20295
+ }
20296
+ this.emit();
20297
+ if (this.settingsValue.livePreview) void this.refresh();
20298
+ return true;
20299
+ }
20250
20300
  /** Sets the area to a preset: Auto (a band around the selection), Rectangular (the whole window). */
20251
20301
  applySampling(mode) {
20252
20302
  const win = this.win;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.211.0",
3
+ "version": "0.212.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -384,7 +384,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
384
384
  | `apply_selection_as_mask` | Mask a layer with the current selection: pass `layerId` to mask an existing layer, or `kind` (plus optional `params`) to create a new masked layer in one step. `density` 0..1 and `feather` px soften the mask non-destructively. Returns `layerId`, kind, mask info and the selection summary. |
385
385
  | `stroke_selection` | Edit › Stroke: a band `width` px (≤ 250) `outside` (default), `inside` or `center` on the selection edge, with `color`, `opacity`, `blend`, on a layer's pixels or mask (`channel`). One undo step. Returns the changed `bounds`. |
386
386
  | `crop_to_selection` | Set the crop box to the selection's bounds (non-destructive; `reset_crop` undoes it). Returns the `crop`. |
387
- | `content_aware_fill` | Remove what is selected by synthesising texture from around it on a pixel layer; `output` `new-layer` (keeps the fill separate, recommended) or `current`; `sampleRadius`, `seed` (another seed, another fill); the sampling area with `sampleRects`, `samplingChannelId` and `excludeRects` (never copy from these). Select with a 2–4 px margin. 8-bit only. Returns `layerId` and `bounds`. |
387
+ | `content_aware_fill` | Remove what is selected by synthesising texture from around it on a pixel layer; `output` `new-layer` (keeps the fill separate, recommended), `current` or `duplicate-layer` (a filled copy); `sampleRadius`, `seed` (another seed, another fill); the sampling area with `sampleRects`, `samplingChannelId` and `excludeRects` (never copy from these). Select with a 2–4 px margin. 8-bit only. Returns `layerId` and `bounds`. |
388
388
  | `copy_pixels` | Copy the selected pixels of a layer (or with `merged` the visible image) to the editor clipboard, cropped to the selection with its soft edge. No undo step. Returns `width`, `height`, `x`, `y`. |
389
389
  | `cut_pixels` | Copy, then clear the selected pixels (one undo step). Returns the size and position. |
390
390
  | `paste_pixels` | Paste the clipboard as a new layer: `mode` `new-layer` (centred on the selection or canvas), `in-place`, `into` (masked by the selection) or `outside` (masked by its inverse). One undo step. Returns `layerId`, `bounds`, `masked`. |