@pixerate/editor 0.220.0 → 0.221.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.
@@ -242,6 +242,8 @@ export interface SerializedLayeredImageEditorState {
242
242
  /** Blur migrated from a v1 / v2 document with adjustment layers (runs after the layers). */
243
243
  legacyOutputBlur?: number;
244
244
  selection?: SerializedSelection;
245
+ /** The selection's recipe (live selections): the steps that made it, as JSON. */
246
+ selectionRecipe?: unknown[];
245
247
  /** Alpha channels (saved selections), run-length encoded. */
246
248
  channels?: SerializedAlphaChannel[];
247
249
  /** Document paths (the Paths panel). */
@@ -242,6 +242,8 @@ export interface SerializedLayeredImageEditorState {
242
242
  /** Blur migrated from a v1 / v2 document with adjustment layers (runs after the layers). */
243
243
  legacyOutputBlur?: number;
244
244
  selection?: SerializedSelection;
245
+ /** The selection's recipe (live selections): the steps that made it, as JSON. */
246
+ selectionRecipe?: unknown[];
245
247
  /** Alpha channels (saved selections), run-length encoded. */
246
248
  channels?: SerializedAlphaChannel[];
247
249
  /** Document paths (the Paths panel). */
@@ -20,3 +20,4 @@ export * from './worker.cjs';
20
20
  export * from './inspect.cjs';
21
21
  export * from './uncertainty.cjs';
22
22
  export * from './diff.cjs';
23
+ export * from './recipe.cjs';
@@ -20,3 +20,4 @@ export * from './worker.js';
20
20
  export * from './inspect.js';
21
21
  export * from './uncertainty.js';
22
22
  export * from './diff.js';
23
+ export * from './recipe.js';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Live selections: a selection kept as the steps that made it (its recipe) beside its bitmap, so a
3
+ * step can be edited, inserted or removed afterwards and the selection replayed. The bitmap stays
4
+ * what everything else uses; a change no step can describe drops the recipe (the selection is then
5
+ * frozen). Steps are plain JSON: the arguments the selection commands took.
6
+ */
7
+ import type { CropBox, ImagePoint } from '../types.cjs';
8
+ import { type PixelBuffer, type SelectionMask, type SelectionMode } from './mask.cjs';
9
+ import { type ColorRangeSample } from './wand.cjs';
10
+ import { type SelectionModifyOp } from './modify.cjs';
11
+ import { type RefineEdgeParams } from './refine.cjs';
12
+ /** Where pixel steps sample (as the selection commands' `SampleOptions`). */
13
+ export interface RecipeSampling {
14
+ sample?: 'composite' | 'active-layer';
15
+ sampleMerged?: boolean;
16
+ }
17
+ /** How a new shape combines with what the earlier steps selected. */
18
+ export interface RecipeCombine {
19
+ mode?: SelectionMode;
20
+ /** Feather the new shape by this many pixels before combining. */
21
+ feather?: number;
22
+ /** Invert the new shape before combining. */
23
+ invert?: boolean;
24
+ }
25
+ /** One step of a selection's recipe. */
26
+ export type SelectionRecipeStep = ({
27
+ op: 'rect' | 'ellipse';
28
+ rect: CropBox;
29
+ antiAlias?: boolean;
30
+ } & RecipeCombine) | ({
31
+ op: 'polygon';
32
+ points: ImagePoint[];
33
+ antiAlias?: boolean;
34
+ } & RecipeCombine) | ({
35
+ op: 'magic-wand';
36
+ point: ImagePoint;
37
+ options?: Record<string, unknown>;
38
+ } & RecipeCombine & RecipeSampling) | ({
39
+ op: 'color-range';
40
+ samples: ColorRangeSample[];
41
+ options?: Record<string, unknown>;
42
+ } & RecipeCombine & RecipeSampling) | ({
43
+ op: 'color-range-preset';
44
+ preset: string;
45
+ falloff?: number;
46
+ } & RecipeCombine & RecipeSampling) | {
47
+ op: 'all';
48
+ } | {
49
+ op: 'invert';
50
+ } | {
51
+ op: 'modify';
52
+ modify: SelectionModifyOp;
53
+ radius: number;
54
+ } | ({
55
+ op: 'refine';
56
+ params: RefineEdgeParams;
57
+ } & RecipeSampling);
58
+ export type SelectionRecipeOp = SelectionRecipeStep['op'];
59
+ export declare const SELECTION_RECIPE_OPS: readonly SelectionRecipeOp[];
60
+ /** Steps that make a new shape (combined by `mode`); the rest change what is already selected. */
61
+ export declare function isShapeStep(step: SelectionRecipeStep): boolean;
62
+ /** Whether a step reads the image's pixels. */
63
+ export declare function stepNeedsPixels(step: SelectionRecipeStep): boolean;
64
+ /** A key for a pixel step's sampling (steps with the same key read the same pixels). */
65
+ export declare function samplingKey(step: SelectionRecipeStep): string;
66
+ /** Checks and copies one step (as JSON from an agent or a document); throws a useful error. */
67
+ export declare function readRecipeStep(raw: unknown, at?: string): SelectionRecipeStep;
68
+ /**
69
+ * The selection `steps` make on a `width × height` canvas, from nothing (null when nothing ends
70
+ * up selected). `pixelsFor` gives a pixel step its image (see `samplingKey`).
71
+ */
72
+ export declare function replaySelectionRecipe(steps: readonly SelectionRecipeStep[], width: number, height: number, pixelsFor: (step: SelectionRecipeStep) => PixelBuffer): SelectionMask | null;
73
+ /** A recipe as an agent reads it: each step with its index. */
74
+ export declare function describeRecipe(steps: readonly SelectionRecipeStep[]): Array<{
75
+ index: number;
76
+ } & SelectionRecipeStep>;
77
+ /** A step in a few words, for a recipe list ("Rectangle 30 × 20 at 10, 10", "Smooth 2 px"). */
78
+ export declare function describeRecipeStep(step: SelectionRecipeStep): string;
79
+ /**
80
+ * A step's main setting for a recipe list (feather, tolerance, fuzziness, falloff or radius) and
81
+ * the change that sets it, or null for steps with none.
82
+ */
83
+ export declare function recipeStepSetting(step: SelectionRecipeStep): {
84
+ label: string;
85
+ value: number;
86
+ min: number;
87
+ max: number;
88
+ change: (v: number) => Record<string, unknown>;
89
+ } | null;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Live selections: a selection kept as the steps that made it (its recipe) beside its bitmap, so a
3
+ * step can be edited, inserted or removed afterwards and the selection replayed. The bitmap stays
4
+ * what everything else uses; a change no step can describe drops the recipe (the selection is then
5
+ * frozen). Steps are plain JSON: the arguments the selection commands took.
6
+ */
7
+ import type { CropBox, ImagePoint } from '../types.js';
8
+ import { type PixelBuffer, type SelectionMask, type SelectionMode } from './mask.js';
9
+ import { type ColorRangeSample } from './wand.js';
10
+ import { type SelectionModifyOp } from './modify.js';
11
+ import { type RefineEdgeParams } from './refine.js';
12
+ /** Where pixel steps sample (as the selection commands' `SampleOptions`). */
13
+ export interface RecipeSampling {
14
+ sample?: 'composite' | 'active-layer';
15
+ sampleMerged?: boolean;
16
+ }
17
+ /** How a new shape combines with what the earlier steps selected. */
18
+ export interface RecipeCombine {
19
+ mode?: SelectionMode;
20
+ /** Feather the new shape by this many pixels before combining. */
21
+ feather?: number;
22
+ /** Invert the new shape before combining. */
23
+ invert?: boolean;
24
+ }
25
+ /** One step of a selection's recipe. */
26
+ export type SelectionRecipeStep = ({
27
+ op: 'rect' | 'ellipse';
28
+ rect: CropBox;
29
+ antiAlias?: boolean;
30
+ } & RecipeCombine) | ({
31
+ op: 'polygon';
32
+ points: ImagePoint[];
33
+ antiAlias?: boolean;
34
+ } & RecipeCombine) | ({
35
+ op: 'magic-wand';
36
+ point: ImagePoint;
37
+ options?: Record<string, unknown>;
38
+ } & RecipeCombine & RecipeSampling) | ({
39
+ op: 'color-range';
40
+ samples: ColorRangeSample[];
41
+ options?: Record<string, unknown>;
42
+ } & RecipeCombine & RecipeSampling) | ({
43
+ op: 'color-range-preset';
44
+ preset: string;
45
+ falloff?: number;
46
+ } & RecipeCombine & RecipeSampling) | {
47
+ op: 'all';
48
+ } | {
49
+ op: 'invert';
50
+ } | {
51
+ op: 'modify';
52
+ modify: SelectionModifyOp;
53
+ radius: number;
54
+ } | ({
55
+ op: 'refine';
56
+ params: RefineEdgeParams;
57
+ } & RecipeSampling);
58
+ export type SelectionRecipeOp = SelectionRecipeStep['op'];
59
+ export declare const SELECTION_RECIPE_OPS: readonly SelectionRecipeOp[];
60
+ /** Steps that make a new shape (combined by `mode`); the rest change what is already selected. */
61
+ export declare function isShapeStep(step: SelectionRecipeStep): boolean;
62
+ /** Whether a step reads the image's pixels. */
63
+ export declare function stepNeedsPixels(step: SelectionRecipeStep): boolean;
64
+ /** A key for a pixel step's sampling (steps with the same key read the same pixels). */
65
+ export declare function samplingKey(step: SelectionRecipeStep): string;
66
+ /** Checks and copies one step (as JSON from an agent or a document); throws a useful error. */
67
+ export declare function readRecipeStep(raw: unknown, at?: string): SelectionRecipeStep;
68
+ /**
69
+ * The selection `steps` make on a `width × height` canvas, from nothing (null when nothing ends
70
+ * up selected). `pixelsFor` gives a pixel step its image (see `samplingKey`).
71
+ */
72
+ export declare function replaySelectionRecipe(steps: readonly SelectionRecipeStep[], width: number, height: number, pixelsFor: (step: SelectionRecipeStep) => PixelBuffer): SelectionMask | null;
73
+ /** A recipe as an agent reads it: each step with its index. */
74
+ export declare function describeRecipe(steps: readonly SelectionRecipeStep[]): Array<{
75
+ index: number;
76
+ } & SelectionRecipeStep>;
77
+ /** A step in a few words, for a recipe list ("Rectangle 30 × 20 at 10, 10", "Smooth 2 px"). */
78
+ export declare function describeRecipeStep(step: SelectionRecipeStep): string;
79
+ /**
80
+ * A step's main setting for a recipe list (feather, tolerance, fuzziness, falloff or radius) and
81
+ * the change that sets it, or null for steps with none.
82
+ */
83
+ export declare function recipeStepSetting(step: SelectionRecipeStep): {
84
+ label: string;
85
+ value: number;
86
+ min: number;
87
+ max: number;
88
+ change: (v: number) => Record<string, unknown>;
89
+ } | null;
@@ -285,6 +285,7 @@ export type ImageEditorTool = 'select' | 'crop' | 'pen' | 'rect' | 'circle' | 'a
285
285
  | 'path-select' | 'direct-select' | 'path-pen';
286
286
  export type ImageEditorFit = 'contain' | 'margin';
287
287
  import type { DocumentColorMode, DocumentColorProfile, ProofSetup } from './color-management.cjs';
288
+ import type { SelectionRecipeStep } from './selection/recipe.cjs';
288
289
  import type { BitDepth } from '../raster/deep.cjs';
289
290
  export interface ImageEditorState {
290
291
  sourceUrl: string | null;
@@ -319,6 +320,12 @@ export interface ImageEditorState {
319
320
  * growing as the brush moves). View state: not an edit, not in history, not saved.
320
321
  */
321
322
  selectionPreview?: SelectionMask | null;
323
+ /**
324
+ * Live selections: the steps that made `selection` (see `selection/recipe`), so they can be
325
+ * edited and replayed; null or missing when it is frozen (made or changed by something no step
326
+ * describes). Recorded in history and saved.
327
+ */
328
+ selectionRecipe?: SelectionRecipeStep[] | null;
322
329
  /**
323
330
  * `compare_selections`' overlay: the added area tinted green, the removed red. Shown while the
324
331
  * selection is still `selection` (any change hides it). View state: not in history, not saved.
@@ -418,6 +425,8 @@ export interface LegacySerializedImageEditorState {
418
425
  annotations: Annotation[];
419
426
  /** The active selection, run-length encoded. Missing when nothing is selected. */
420
427
  selection?: SerializedSelection;
428
+ /** The selection's recipe (live selections): the steps that made it, as JSON. */
429
+ selectionRecipe?: unknown[];
421
430
  /** Alpha channels (saved selections), run-length encoded. */
422
431
  channels?: SerializedAlphaChannel[];
423
432
  /** Document paths (the Paths panel). */
@@ -285,6 +285,7 @@ export type ImageEditorTool = 'select' | 'crop' | 'pen' | 'rect' | 'circle' | 'a
285
285
  | 'path-select' | 'direct-select' | 'path-pen';
286
286
  export type ImageEditorFit = 'contain' | 'margin';
287
287
  import type { DocumentColorMode, DocumentColorProfile, ProofSetup } from './color-management.js';
288
+ import type { SelectionRecipeStep } from './selection/recipe.js';
288
289
  import type { BitDepth } from '../raster/deep.js';
289
290
  export interface ImageEditorState {
290
291
  sourceUrl: string | null;
@@ -319,6 +320,12 @@ export interface ImageEditorState {
319
320
  * growing as the brush moves). View state: not an edit, not in history, not saved.
320
321
  */
321
322
  selectionPreview?: SelectionMask | null;
323
+ /**
324
+ * Live selections: the steps that made `selection` (see `selection/recipe`), so they can be
325
+ * edited and replayed; null or missing when it is frozen (made or changed by something no step
326
+ * describes). Recorded in history and saved.
327
+ */
328
+ selectionRecipe?: SelectionRecipeStep[] | null;
322
329
  /**
323
330
  * `compare_selections`' overlay: the added area tinted green, the removed red. Shown while the
324
331
  * selection is still `selection` (any change hides it). View state: not in history, not saved.
@@ -418,6 +425,8 @@ export interface LegacySerializedImageEditorState {
418
425
  annotations: Annotation[];
419
426
  /** The active selection, run-length encoded. Missing when nothing is selected. */
420
427
  selection?: SerializedSelection;
428
+ /** The selection's recipe (live selections): the steps that made it, as JSON. */
429
+ selectionRecipe?: unknown[];
421
430
  /** Alpha channels (saved selections), run-length encoded. */
422
431
  channels?: SerializedAlphaChannel[];
423
432
  /** Document paths (the Paths panel). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.220.0",
3
+ "version": "0.221.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -104,17 +104,8 @@ Keep outlines as paths (`make_work_path`, `add_path`) and edit their anchors: se
104
104
 
105
105
  ### Painting
106
106
 
107
- 1. **Paint on its own layer** (`add_pixel_layer`), not on the Background (active on a new image, so
108
- strokes without `layerId` land there) unless asked: a layer can be faded, masked or deleted.
109
- 2. **Retouch through masks**, not the eraser (see `pixerate-image-selection-masking`).
110
- 3. **One stroke is one call and one undo step.** Send the path's points in order (a few points
111
- per brush diameter is enough; dabs are spaced by `brush.spacing`). Use `opacity` for the
112
- overall strength and `flow` for gradual build-up within the stroke.
113
- 4. **Limit paint with a selection**, or lock transparency (`set_layer_properties`
114
- `locks.transparency`) to paint only where the layer already has pixels.
115
- 5. **Mixing paint, the Mixer Brush and gradient paint**: `mix_colors` mixes named paints like
116
- pigment; `paint_stroke` `tool: "mixer"` picks up and smears the layer's colours; gradient paint
117
- changes colour along a stroke. Read [references/painting.md](references/painting.md) first.
107
+ Paint on its own layer (`add_pixel_layer`), one stroke per call and one undo step, limited by a
108
+ selection or locked transparency. Read [references/painting.md](references/painting.md) first.
118
109
 
119
110
  ### Colour management
120
111
 
@@ -207,6 +198,11 @@ in the source frame at the document's bit depth, colour mode and profile. Read
207
198
  | `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. |
208
199
  | `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`). |
209
200
  | `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. |
201
+ | `get_selection_recipe` | Read-only. Live selections: the steps that made the selection (marquees, polygons, magic wand, Color Range, modify, invert, refine) with indexes; `live: false` when frozen (made by a command steps cannot describe, e.g. `quick_select`). |
202
+ | `edit_selection_step` | Change a recipe step (`index`, `changes`: e.g. `options.tolerance`, `rect`, `radius`, `mode`; a new `op` replaces it) and replay. One undo step. Returns the selection and recipe. |
203
+ | `insert_selection_step` | Insert a step at `index` (length appends), e.g. a subtract rect before a refine, and replay. One undo step. |
204
+ | `remove_selection_step` | Remove a recipe step and replay (not the only one: deselect instead). One undo step. |
205
+ | `freeze_selection` | Keep the selection as it is and drop its recipe. One undo step. |
210
206
  | `list_channels` | Read the alpha channels (saved selections): id, name, bounds, percent, overlay and visibility, and whether Quick Mask mode is on. |
211
207
  | `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. |
212
208
  | `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. |
@@ -1,4 +1,20 @@
1
- # Painting: mixing paint, the Mixer Brush and gradient paint
1
+ # Painting: strokes, mixing paint, the Mixer Brush and gradient paint
2
+
3
+ ## Strokes
4
+
5
+ 1. **Paint on its own layer** (`add_pixel_layer`), not on the Background (active on a new image, so
6
+ strokes without `layerId` land there) unless asked: a layer can be faded, masked or deleted.
7
+ 2. **Retouch through masks**, not the eraser (see `pixerate-image-selection-masking`).
8
+ 3. **One stroke is one call and one undo step.** Send the path's points in order (a few points
9
+ per brush diameter is enough; dabs are spaced by `brush.spacing`). Use `opacity` for the
10
+ overall strength and `flow` for gradual build-up within the stroke.
11
+ 4. **Limit paint with a selection**, or lock transparency (`set_layer_properties`
12
+ `locks.transparency`) to paint only where the layer already has pixels.
13
+ 5. **Mixing paint, the Mixer Brush and gradient paint**: `mix_colors` mixes named paints like
14
+ pigment; `paint_stroke` `tool: "mixer"` picks up and smears the layer's colours; gradient paint
15
+ changes colour along a stroke. See below.
16
+
17
+ ## Mixing paint, the Mixer Brush and gradient paint
2
18
 
3
19
  Read this before `mix_colors`, a `paint_stroke` with `tool: "mixer"`, or gradient paint
4
20
  (`set_paint` / `paint_stroke` `paint`).
@@ -40,9 +40,9 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
40
40
  **Check:** `get_selection` → `empty: false` and a plausible `percent` (for a person in a
41
41
  portrait, typically 15–50 %). A wand or range above about 80 % has flooded: undo, then lower
42
42
  `tolerance` / `fuzziness`.
43
- 2. **Build it in steps** with `mode`: start with `replace`, `add` what was missed, `subtract`
44
- what leaked, and `intersect` to limit it to a region (for example `select_rect` around the
45
- subject).
43
+ 2. **Build it in steps** with `mode`: `replace`, then `add` what was missed, `subtract` what
44
+ leaked, `intersect` to limit it. The steps are kept (`get_selection_recipe`): fix one with
45
+ `edit_selection_step` (a wand's tolerance, a box) rather than starting over.
46
46
  **Check:** `bounds` hug the subject. A bounds as large as the image means a leak. Smart tools
47
47
  return `uncertainRegions` (edge with no image edge under it): look there, refine with `regions`.
48
48
  3. **Measure the edge, then look.** `inspect_mask_edge` scores it: `haloScore` ≥ 0.35 is old