@pixerate/editor 0.216.0 → 0.218.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.
@@ -32,6 +32,7 @@ import {
32
32
  SRGB_TO_LINEAR,
33
33
  WET_FLOW_KINDS,
34
34
  WET_PARAM_KEYS,
35
+ WET_TILE,
35
36
  WetMedium,
36
37
  WetWorld,
37
38
  createPigmentTools,
@@ -39,13 +40,14 @@ import {
39
40
  findWebGpu,
40
41
  flowField,
41
42
  linearToSrgb8,
43
+ moveWetSnapshot,
42
44
  normalizeFlow,
43
45
  normalizeTilt,
44
46
  normalizeWetParams,
45
47
  pigmentOf,
46
48
  pointerPressure,
47
49
  wetSupport
48
- } from "../chunk-WJ2RBZMM.js";
50
+ } from "../chunk-HLBTRSQJ.js";
49
51
  import "../chunk-HVQVHUTV.js";
50
52
  import {
51
53
  writeZip
@@ -2162,15 +2164,50 @@ var PaintWetLayers = class {
2162
2164
  if (this.world.laneIds().length) throw new Error("The wet world is the wrong size; dry the paint before resizing the canvas.");
2163
2165
  this.dropWorld();
2164
2166
  }
2165
- if (!this.world) {
2166
- this.world = new WetWorld(width, height, wetPaperInput(this.host.paper()), { kernel: this.options.kernel, threads: this.options.threads });
2167
- this.world.setTilt(this.tilt);
2168
- this.world.gpuMinTiles = this.gpuMinTiles;
2169
- if (this.gpu) this.world.attachGpu(this.gpu);
2170
- else this.startGpu();
2171
- }
2167
+ if (!this.world) this.world = this.makeWorld(width, height, this.host.paper());
2172
2168
  return this.world;
2173
2169
  }
2170
+ makeWorld(width, height, paper) {
2171
+ const world = new WetWorld(width, height, wetPaperInput(paper), { kernel: this.options.kernel, threads: this.options.threads });
2172
+ world.setTilt(this.tilt);
2173
+ world.gpuMinTiles = this.gpuMinTiles;
2174
+ if (this.gpu) world.attachGpu(this.gpu);
2175
+ else this.startGpu();
2176
+ return world;
2177
+ }
2178
+ /**
2179
+ * Whether `extend` can keep the wet paint when the canvas grows by `left` / `top` on those
2180
+ * sides: the shift has to be whole wet tiles (256 px), and no wet stroke may be live.
2181
+ */
2182
+ canExtend(left, top) {
2183
+ return left % WET_TILE === 0 && top % WET_TILE === 0 && !this.driver;
2184
+ }
2185
+ /**
2186
+ * Keeps the wet paint while the canvas grows to `width`×`height` by `left` / `top` on those
2187
+ * sides (`canExtend` must hold): the world is made again at the new size with every wet layer
2188
+ * moved by (left, top), still wet, its base and flow map with it; `paper` is the new
2189
+ * document's paper (its grain moved with the paint). Wet settings' flow centres move too. The
2190
+ * caller shows the layers again (`syncDisplays`) once the document is the new size.
2191
+ */
2192
+ extend(width, height, left, top, paper) {
2193
+ if (!this.canExtend(left, top)) throw new Error("Wet paint only moves by whole 256 px tiles, between strokes.");
2194
+ for (const [id, s] of this.settings) if (s.flow.center) this.settings.set(id, { ...s, flow: { ...s.flow, center: { x: s.flow.center.x + left, y: s.flow.center.y + top } } });
2195
+ const old = this.world;
2196
+ const snap = old?.laneIds().length ? old.snapshot() : null;
2197
+ this.dropWorld();
2198
+ this.lastTick = null;
2199
+ this.stepsOwed = 0;
2200
+ if (!old || !snap) return;
2201
+ const moved = moveWetSnapshot(snap, old, { width, height }, left, top, (l) => {
2202
+ const flow = this.settings.get(l.id)?.flow;
2203
+ return {
2204
+ base: translateSurface(l.base, left, top),
2205
+ flow: l.flow && flow && flow.kind !== "none" ? flowField(flow, width, height) : l.flow
2206
+ };
2207
+ });
2208
+ this.world = this.makeWorld(width, height, paper);
2209
+ this.world.restore(moved);
2210
+ }
2174
2211
  /** Whether any layer (or `layerId`) has wet paint. */
2175
2212
  isWet(layerId) {
2176
2213
  const w = this.world;
@@ -3709,9 +3746,11 @@ var PaintEditorController = class _PaintEditorController {
3709
3746
  * move by (left, top) in document pixels, `document.origin` moves by (−left, −top), and the view
3710
3747
  * pans so the picture stays still on screen.
3711
3748
  *
3712
- * Wet paint dries first (the wet simulation is canvas-sized), as with `setCanvas`; one undo step
3713
- * (undo brings back the old size, origin and guides, and the wet paint). Returns the new canvas,
3714
- * where it sits in the previous document's pixels, and the origin.
3749
+ * Wet paint stays wet and moves with the picture when `left` and `top` are whole 256 px tiles
3750
+ * (so a host growing a canvas while someone paints should grow it in tile steps); otherwise it
3751
+ * dries first (the wet simulation is canvas-sized), as with `setCanvas`. One undo step (undo
3752
+ * brings back the old size, origin and guides, and the wet paint). Returns the new canvas, where
3753
+ * it sits in the previous document's pixels, and the origin.
3715
3754
  */
3716
3755
  extendCanvas(by) {
3717
3756
  this.requireIdle("extending the canvas");
@@ -3731,14 +3770,29 @@ var PaintEditorController = class _PaintEditorController {
3731
3770
  if (width > MAX_CANVAS_SIDE || height > MAX_CANVAS_SIDE) {
3732
3771
  throw new PaintEditorError(`The canvas would be ${width}\xD7${height}; the limit is ${MAX_CANVAS_SIDE} px per side. Extend by less (it is ${was.width}\xD7${was.height} now).`);
3733
3772
  }
3773
+ let keptWet = [];
3734
3774
  if (this.wet.world) {
3735
3775
  this.requireWetIdle("extending the canvas");
3736
- this.dryLayersFirst(this.wet.wetLayerIds());
3737
- const settings = [...this.wet.settings];
3738
- this.wet.reset();
3739
- for (const [id, s] of settings) this.wet.settings.set(id, s.flow.center ? { ...s, flow: { ...s.flow, center: { x: s.flow.center.x + left, y: s.flow.center.y + top } } } : s);
3776
+ if (this.wet.canExtend(left, top)) {
3777
+ keptWet = this.wet.wetLayerIds();
3778
+ if (keptWet.length) this.beginWetOp();
3779
+ } else {
3780
+ this.dryLayersFirst(this.wet.wetLayerIds());
3781
+ }
3782
+ }
3783
+ let next = { ...shiftDocument(this.withWetEvents(this.doc), left, top), canvas: { ...was, width, height } };
3784
+ for (const id of keptWet) {
3785
+ const l = findLayer(next.layers, id)?.layer;
3786
+ if (isPixelLayer(l)) next = { ...next, layers: mapLayer(next.layers, id, () => ({ ...l, surface: translateSurface(l.surface, l.offset.x, l.offset.y), offset: { x: 0, y: 0 } })) };
3787
+ }
3788
+ if (this.wet.world) {
3789
+ if (keptWet.length) this.wet.extend(width, height, left, top, next.paper);
3790
+ else {
3791
+ const settings = [...this.wet.settings];
3792
+ this.wet.reset();
3793
+ for (const [id, s] of settings) this.wet.settings.set(id, s.flow.center ? { ...s, flow: { ...s.flow, center: { x: s.flow.center.x + left, y: s.flow.center.y + top } } } : s);
3794
+ }
3740
3795
  }
3741
- const next = { ...shiftDocument(this.withWetEvents(this.doc), left, top), canvas: { ...was, width, height } };
3742
3796
  const m = viewMatrix(this.view, was, { width: 0, height: 0 });
3743
3797
  const dx = (right - left) / 2;
3744
3798
  const dy = (bottom - top) / 2;
@@ -3746,6 +3800,7 @@ var PaintEditorController = class _PaintEditorController {
3746
3800
  const guidesBefore = this.guides;
3747
3801
  if (left || top) this.restoreGuides(shiftGuides(this.guides, left, top));
3748
3802
  this.commit("Extend canvas", next);
3803
+ if (keptWet.length) this.wet.syncDisplays();
3749
3804
  if (this.guides !== guidesBefore) this.undoStack[this.undoStack.length - 1].guides = { before: guidesBefore, after: this.guides };
3750
3805
  const origin = { ...next.origin ?? { x: 0, y: 0 } };
3751
3806
  return { canvas: { ...next.canvas }, rect: { x: 0 - left, y: 0 - top, width, height }, origin };
@@ -7433,7 +7488,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
7433
7488
  add({
7434
7489
  name: "extend_canvas",
7435
7490
  title: "Extend the canvas",
7436
- description: "Grows the canvas by adding pixels on any side, keeping everything painted. Adding on the left or top moves the whole picture (layers, paper grain, guides, stencils, recorded strokes) right or down by that much in document pixels, so coordinates you read before change by (+left, +top). Wet paint dries first. One undo step. Returns the canvas, rect (the new canvas in the previous coordinates: x -left, y -top) and origin.",
7491
+ description: "Grows the canvas by adding pixels on any side, keeping everything painted. Adding on the left or top moves the whole picture (layers, paper grain, guides, stencils, recorded strokes) right or down by that much in document pixels, so coordinates you read before change by (+left, +top). Wet paint stays wet and moves with it when left and top are multiples of 256; otherwise it dries first. One undo step. Returns the canvas, rect (the new canvas in the previous coordinates: x -left, y -top) and origin.",
7437
7492
  inputSchema: {
7438
7493
  type: "object",
7439
7494
  properties: { left: EXTEND_SIDE("left"), top: EXTEND_SIDE("top"), right: EXTEND_SIDE("right"), bottom: EXTEND_SIDE("bottom") },
@@ -214,6 +214,20 @@ export declare class PaintWetLayers {
214
214
  engineInfo(): WetEngineInfo;
215
215
  /** The world (made on first use, at the canvas size). */
216
216
  ensureWorld(): WetWorld;
217
+ private makeWorld;
218
+ /**
219
+ * Whether `extend` can keep the wet paint when the canvas grows by `left` / `top` on those
220
+ * sides: the shift has to be whole wet tiles (256 px), and no wet stroke may be live.
221
+ */
222
+ canExtend(left: number, top: number): boolean;
223
+ /**
224
+ * Keeps the wet paint while the canvas grows to `width`×`height` by `left` / `top` on those
225
+ * sides (`canExtend` must hold): the world is made again at the new size with every wet layer
226
+ * moved by (left, top), still wet, its base and flow map with it; `paper` is the new
227
+ * document's paper (its grain moved with the paint). Wet settings' flow centres move too. The
228
+ * caller shows the layers again (`syncDisplays`) once the document is the new size.
229
+ */
230
+ extend(width: number, height: number, left: number, top: number, paper: PaperSettings): void;
217
231
  /** Whether any layer (or `layerId`) has wet paint. */
218
232
  isWet(layerId?: string): boolean;
219
233
  /** Layers holding a wet layer (wet or drying). */
@@ -214,6 +214,20 @@ export declare class PaintWetLayers {
214
214
  engineInfo(): WetEngineInfo;
215
215
  /** The world (made on first use, at the canvas size). */
216
216
  ensureWorld(): WetWorld;
217
+ private makeWorld;
218
+ /**
219
+ * Whether `extend` can keep the wet paint when the canvas grows by `left` / `top` on those
220
+ * sides: the shift has to be whole wet tiles (256 px), and no wet stroke may be live.
221
+ */
222
+ canExtend(left: number, top: number): boolean;
223
+ /**
224
+ * Keeps the wet paint while the canvas grows to `width`×`height` by `left` / `top` on those
225
+ * sides (`canExtend` must hold): the world is made again at the new size with every wet layer
226
+ * moved by (left, top), still wet, its base and flow map with it; `paper` is the new
227
+ * document's paper (its grain moved with the paint). Wet settings' flow centres move too. The
228
+ * caller shows the layers again (`syncDisplays`) once the document is the new size.
229
+ */
230
+ extend(width: number, height: number, left: number, top: number, paper: PaperSettings): void;
217
231
  /** Whether any layer (or `layerId`) has wet paint. */
218
232
  isWet(layerId?: string): boolean;
219
233
  /** Layers holding a wet layer (wet or drying). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.216.0",
3
+ "version": "0.218.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -120,8 +120,7 @@ Keep outlines as paths (`make_work_path`, `add_path`) and edit their anchors: se
120
120
 
121
121
  ### Colour management
122
122
 
123
- The working space, Assign / Convert to Profile with intents, soft proofing with a gamut warning
124
- and colour conversions for print briefs: read [references/color-management.md](references/color-management.md).
123
+ Working space, profiles, soft proofing and print conversions: read [references/color-management.md](references/color-management.md).
125
124
  ### Retouching
126
125
 
127
126
  Clone Stamp, Healing Brush, Spot Healing, Smudge, Dodge, Burn, Sponge, Blur, Sharpen and the
@@ -209,6 +208,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
209
208
  | `list_controls` | Get every base control (id, label, section, min, max, step, default, `requiresProcess`), each layer kind's settable controls (plus `opacity`), the blend mode ids and the `layers.<layerId>.<controlId>` path format. Call once per session before writing. |
210
209
  | `get_selection` | Read the active selection: `empty`, `bounds` (natural pixels), `area` in pixels, `percent` of the image and a description. Check it after every select or modify step and before masking: a wand that covered 90% of the image is the wrong selection. |
211
210
  | `inspect_mask_edge` | Measure an edge without looking: the selection (default) or a layer's `layer-mask` / `vector-mask` (`layerId`). Returns `haloScore`, `edgeAlignment`, `jaggedness`, `softFraction`, `holes`, `islands`, `worst` regions for `capture_view` and `issues` with fixes. Inspect, fix, inspect again (see `pixerate-image-selection-masking`). |
211
+ | `compare_selections` | What changed between two selections: `a` (default the one before the last undo step) and `b` (default current), each `selection`, `history` (`steps`), `channel` or a layer's `layer-mask` / `vector-mask`. Returns `added` / `removed` (area, largest pieces with bounds), `iou` and `identical`; `show` tints the change on the canvas. Check every modify or refine step. |
212
212
  | `list_channels` | Read the alpha channels (saved selections): id, name, bounds, percent, overlay and visibility, and whether Quick Mask mode is on. |
213
213
  | `select_subject` | Select › Subject. With a host segmentation model, `target` describes what to select ("the dog", "the sky") and the model's outlines are selected (`maxSegments` 1 for one). Without one, the subject that stands out most is found classically; `target` can't choose it (`method: "saliency"`, `note`). Combined by `mode`. Returns the selection, `found` (label, bounds) and `method`; nothing found leaves the selection unchanged. |
214
214
  | `list_layers` | Read the layer stack bottom to top as a tree: id, type (`pixel`, `adjustment`, `group`, `shape`, `annotation`, `smart`; smart objects add their `contents`, `smartFilters` and `filterMask`; shapes a `shape` summary of component ops and kinds and paint types, and `annotation` when migrated from one), name, document `bounds` of pixel layers, visibility, opacity, `fillOpacity`, blend, `clipped`, locks, a `mask` summary, an `effects` summary (master switch, scale, effect ids and kinds), pixel-layer `filters` (the Background's `develop` filter is the base adjustments), adjustment `kind` and `params`, group `children`, and which layer is `active`. Call it to understand a document with more than the Background before changing it; adjustment layer ids work with every adjustment-layer tool. |
@@ -65,8 +65,8 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
65
65
  can open the same edits in the workspace. With a host model, `refineMode: "object"`
66
66
  refines from the model's outline.
67
67
  - `shiftEdge` −10..−30 % tightens a refined edge that picked up background.
68
- **Check:** `percent` changed by roughly what you intended (a few percent at most for a
69
- cleanup).
68
+ **Check:** `compare_selections` after the step: `added` / `removed` are what you meant and
69
+ `iou` stays near 1 for a cleanup (it tints the change on the canvas).
70
70
  5. **Output.** Choose by goal:
71
71
  - A local adjustment: `add_adjustment_layer` while the selection is active (it is masked
72
72
  by it), or `apply_selection_as_mask` with `kind`. Filters (`apply_effect`,
@@ -239,7 +239,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
239
239
  - `modify_selection`: expand, contract, feather, border, smooth
240
240
  - `refine_selection`: snap the edge to the image
241
241
  - `transform_selection`: move or scale the outline only
242
- - `inspect_mask_edge`: measured halo, alignment, jaggedness, softness, holes and islands of an edge
242
+ - `inspect_mask_edge`, `compare_selections`: measure an edge (halo, alignment, jaggedness, holes, islands) and what a step changed (IoU)
243
243
  - `set_edge_decontamination`, `set_edge_despill`, `set_edge_ambient`, `get_edge_color`: live edge colour on a masked cutout
244
244
  - `capture_view`, `render_preview`: look at the edge at pixel level; check the result
245
245
  - `apply_selection_as_mask`: a masked adjustment layer in one step
@@ -283,7 +283,7 @@ view), so work with the tools you are given.
283
283
  | `get_canvas_info` | Read the canvas size and dpi, its `origin` (moved by `extend_canvas`), paper (preset, colour, depth), active layer, layer count, active brush id, colours, view, hand, number of recorded strokes, undo / redo labels and the blend modes. Call first. |
284
284
  | `new_canvas` | Start over with a blank canvas: one empty layer over the paper. `width`, `height` (1..16384 px), `dpi`, `paper` preset id; missing ones keep the current values. Undo brings the old painting back. Returns the size, paper and layer id. |
285
285
  | `set_canvas` | Resize the canvas (pixels stay put, anchored top-left) or change its `dpi`. Returns the canvas. |
286
- | `extend_canvas` | Add pixels on any side (`left`, `top`, `right`, `bottom`, default 0) keeping everything painted, the paper grain and the guides in place relative to each other. `left` / `top` move all document coordinates by that much. Wet paint dries first; one undo step. Returns `canvas`, `rect` (the new canvas in the previous coordinates) and `origin` (also in `get_canvas_info`). |
286
+ | `extend_canvas` | Add pixels on any side (`left`, `top`, `right`, `bottom`, default 0) keeping everything painted, the paper grain and the guides in place relative to each other. `left` / `top` move all document coordinates by that much. Wet paint stays wet and moves with it when `left` and `top` are multiples of 256; otherwise it dries first. One undo step. Returns `canvas`, `rect` (the new canvas in the previous coordinates) and `origin` (also in `get_canvas_info`). |
287
287
  | `list_papers` | Read the paper library: id, name, family, grain pattern, colour, depth and the wet-media properties (absorption, permeability, roughness, stored for later wet media). Call before `set_paper`. |
288
288
  | `get_paper` | Read every setting of the current paper (pattern, seed, scale, tiling, depth, shading, dry and wet tuning). |
289
289
  | `set_paper` | Change the paper: `preset`, then any of `color`, `depth` (0..1), `scale` (0.25..8), `seed` (another sheet), `pattern`, `tiling`, `shading`, `dry` / `wet` tuning (`contrast` 0..3, `invert`). Affects new strokes' grain and how the paper shows; strokes already painted keep their pixels. Returns the paper. |