@pixerate/editor 0.219.2 → 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.
- package/dist/{chunk-S55JFWDA.cjs → chunk-R35YOUP2.cjs} +407 -161
- package/dist/{chunk-F7Z2MKY2.js → chunk-S6KKLFZO.js} +261 -15
- package/dist/image-editor/controller.d.cts +52 -5
- package/dist/image-editor/controller.d.ts +52 -5
- package/dist/image-editor/index.cjs +3065 -2536
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +1528 -999
- package/dist/image-editor/layers/hybrid-mask.d.cts +31 -0
- package/dist/image-editor/layers/hybrid-mask.d.ts +31 -0
- package/dist/image-editor/layers/model.d.cts +17 -0
- package/dist/image-editor/layers/model.d.ts +17 -0
- package/dist/image-editor/layers/serialize.d.cts +4 -0
- package/dist/image-editor/layers/serialize.d.ts +4 -0
- package/dist/image-editor/layers/vector-mask.d.cts +10 -5
- package/dist/image-editor/layers/vector-mask.d.ts +10 -5
- package/dist/image-editor/path-edit.d.cts +4 -0
- package/dist/image-editor/path-edit.d.ts +4 -0
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/selection/recipe.d.cts +89 -0
- package/dist/image-editor/selection/recipe.d.ts +89 -0
- package/dist/image-editor/types.d.cts +9 -0
- package/dist/image-editor/types.d.ts +9 -0
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +7 -11
- package/skills/pixerate-image-editor/references/painting.md +17 -1
- package/skills/pixerate-image-selection-masking/SKILL.md +3 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid vector masks: a vector mask whose segments each have an edge mode. Hard segments are
|
|
3
|
+
* the path's own anti-aliased edge; feather segments blur across the curve; matte segments solve
|
|
4
|
+
* a corridor between two rails (inside by `inner`, outside by `outer`) from the layer's pixels
|
|
5
|
+
* with the colour-guided filter, so hair and fur keep their detail while the rest stays sharp.
|
|
6
|
+
* Where modes meet, they blend over a few pixels so there is no seam. Rendered on demand from
|
|
7
|
+
* the path and the pixels, and cached with the layer like any vector mask.
|
|
8
|
+
*/
|
|
9
|
+
import type { ShapeComponent, VectorMask, VectorMaskEdge } from './model.cjs';
|
|
10
|
+
export type VectorMaskEdgeMode = 'hard' | 'feather' | 'matte';
|
|
11
|
+
/** Defaults for a segment switched to a soft mode. */
|
|
12
|
+
export declare const VECTOR_MASK_EDGE_DEFAULTS: {
|
|
13
|
+
readonly radius: 4;
|
|
14
|
+
readonly inner: 4;
|
|
15
|
+
readonly outer: 8;
|
|
16
|
+
};
|
|
17
|
+
/** The segments of a vector mask's path as `{ component, subpath, segment }` triples. */
|
|
18
|
+
export declare function vectorMaskSegments(components: readonly ShapeComponent[]): Array<{
|
|
19
|
+
component: number;
|
|
20
|
+
subpath: number;
|
|
21
|
+
segment: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** Edges whose segment still exists in `components` (after the path was edited). */
|
|
24
|
+
export declare function validVectorMaskEdges(edges: readonly VectorMaskEdge[] | undefined, components: readonly ShapeComponent[]): VectorMaskEdge[];
|
|
25
|
+
/**
|
|
26
|
+
* The hybrid coverage (0..1, `width × height`) from the path's hard coverage `hard` (as
|
|
27
|
+
* `componentsCoverage` gives it, before the mask's own feather and density) and the soft
|
|
28
|
+
* `edges`. `guide` is the layer's RGBA over the canvas, for matte corridors; without one a matte
|
|
29
|
+
* segment is feathered instead. Returns `hard` itself when no edge is soft.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hybridVectorMaskCoverage(vm: Pick<VectorMask, 'components' | 'edges'>, hard: Float32Array, width: number, height: number, guide: ArrayLike<number> | null): Float32Array;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid vector masks: a vector mask whose segments each have an edge mode. Hard segments are
|
|
3
|
+
* the path's own anti-aliased edge; feather segments blur across the curve; matte segments solve
|
|
4
|
+
* a corridor between two rails (inside by `inner`, outside by `outer`) from the layer's pixels
|
|
5
|
+
* with the colour-guided filter, so hair and fur keep their detail while the rest stays sharp.
|
|
6
|
+
* Where modes meet, they blend over a few pixels so there is no seam. Rendered on demand from
|
|
7
|
+
* the path and the pixels, and cached with the layer like any vector mask.
|
|
8
|
+
*/
|
|
9
|
+
import type { ShapeComponent, VectorMask, VectorMaskEdge } from './model.js';
|
|
10
|
+
export type VectorMaskEdgeMode = 'hard' | 'feather' | 'matte';
|
|
11
|
+
/** Defaults for a segment switched to a soft mode. */
|
|
12
|
+
export declare const VECTOR_MASK_EDGE_DEFAULTS: {
|
|
13
|
+
readonly radius: 4;
|
|
14
|
+
readonly inner: 4;
|
|
15
|
+
readonly outer: 8;
|
|
16
|
+
};
|
|
17
|
+
/** The segments of a vector mask's path as `{ component, subpath, segment }` triples. */
|
|
18
|
+
export declare function vectorMaskSegments(components: readonly ShapeComponent[]): Array<{
|
|
19
|
+
component: number;
|
|
20
|
+
subpath: number;
|
|
21
|
+
segment: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** Edges whose segment still exists in `components` (after the path was edited). */
|
|
24
|
+
export declare function validVectorMaskEdges(edges: readonly VectorMaskEdge[] | undefined, components: readonly ShapeComponent[]): VectorMaskEdge[];
|
|
25
|
+
/**
|
|
26
|
+
* The hybrid coverage (0..1, `width × height`) from the path's hard coverage `hard` (as
|
|
27
|
+
* `componentsCoverage` gives it, before the mask's own feather and density) and the soft
|
|
28
|
+
* `edges`. `guide` is the layer's RGBA over the canvas, for matte corridors; without one a matte
|
|
29
|
+
* segment is feathered instead. Returns `hard` itself when no edge is soft.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hybridVectorMaskCoverage(vm: Pick<VectorMask, 'components' | 'edges'>, hard: Float32Array, width: number, height: number, guide: ArrayLike<number> | null): Float32Array;
|
|
@@ -51,6 +51,23 @@ export interface VectorMask {
|
|
|
51
51
|
density?: number;
|
|
52
52
|
/** Gaussian feather σ in px (default 0). */
|
|
53
53
|
feather?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Hybrid mask: soft edge modes per path segment (see `hybridVectorMaskCoverage`); segments not
|
|
56
|
+
* listed are hard. Indices follow the path's components, subpaths and segments.
|
|
57
|
+
*/
|
|
58
|
+
edges?: VectorMaskEdge[];
|
|
59
|
+
}
|
|
60
|
+
/** One segment's edge mode in a hybrid vector mask. */
|
|
61
|
+
export interface VectorMaskEdge {
|
|
62
|
+
component: number;
|
|
63
|
+
subpath: number;
|
|
64
|
+
/** Segment index in the subpath (from anchor `segment` to the next). */
|
|
65
|
+
segment: number;
|
|
66
|
+
/** `feather`: a Gaussian falloff of `radius` px across the curve; `matte`: a corridor from `inner` px inside to `outer` px outside, solved from the layer's colours. */
|
|
67
|
+
mode: 'hard' | 'feather' | 'matte';
|
|
68
|
+
radius?: number;
|
|
69
|
+
inner?: number;
|
|
70
|
+
outer?: number;
|
|
54
71
|
}
|
|
55
72
|
/** The base sliders of the old document-level adjustments, as a filter on one layer. */
|
|
56
73
|
export interface DevelopSettings {
|
|
@@ -51,6 +51,23 @@ export interface VectorMask {
|
|
|
51
51
|
density?: number;
|
|
52
52
|
/** Gaussian feather σ in px (default 0). */
|
|
53
53
|
feather?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Hybrid mask: soft edge modes per path segment (see `hybridVectorMaskCoverage`); segments not
|
|
56
|
+
* listed are hard. Indices follow the path's components, subpaths and segments.
|
|
57
|
+
*/
|
|
58
|
+
edges?: VectorMaskEdge[];
|
|
59
|
+
}
|
|
60
|
+
/** One segment's edge mode in a hybrid vector mask. */
|
|
61
|
+
export interface VectorMaskEdge {
|
|
62
|
+
component: number;
|
|
63
|
+
subpath: number;
|
|
64
|
+
/** Segment index in the subpath (from anchor `segment` to the next). */
|
|
65
|
+
segment: number;
|
|
66
|
+
/** `feather`: a Gaussian falloff of `radius` px across the curve; `matte`: a corridor from `inner` px inside to `outer` px outside, solved from the layer's colours. */
|
|
67
|
+
mode: 'hard' | 'feather' | 'matte';
|
|
68
|
+
radius?: number;
|
|
69
|
+
inner?: number;
|
|
70
|
+
outer?: number;
|
|
54
71
|
}
|
|
55
72
|
/** The base sliders of the old document-level adjustments, as a filter on one layer. */
|
|
56
73
|
export interface DevelopSettings {
|
|
@@ -57,6 +57,8 @@ export interface SerializedVectorMask {
|
|
|
57
57
|
linked?: false;
|
|
58
58
|
density?: number;
|
|
59
59
|
feather?: number;
|
|
60
|
+
/** Hybrid mask: soft edge modes per segment. */
|
|
61
|
+
edges?: unknown[];
|
|
60
62
|
}
|
|
61
63
|
/** A layer's effects: the effect list as JSON plus the PSD block they came from (base64). */
|
|
62
64
|
export interface SerializedLayerEffects {
|
|
@@ -240,6 +242,8 @@ export interface SerializedLayeredImageEditorState {
|
|
|
240
242
|
/** Blur migrated from a v1 / v2 document with adjustment layers (runs after the layers). */
|
|
241
243
|
legacyOutputBlur?: number;
|
|
242
244
|
selection?: SerializedSelection;
|
|
245
|
+
/** The selection's recipe (live selections): the steps that made it, as JSON. */
|
|
246
|
+
selectionRecipe?: unknown[];
|
|
243
247
|
/** Alpha channels (saved selections), run-length encoded. */
|
|
244
248
|
channels?: SerializedAlphaChannel[];
|
|
245
249
|
/** Document paths (the Paths panel). */
|
|
@@ -57,6 +57,8 @@ export interface SerializedVectorMask {
|
|
|
57
57
|
linked?: false;
|
|
58
58
|
density?: number;
|
|
59
59
|
feather?: number;
|
|
60
|
+
/** Hybrid mask: soft edge modes per segment. */
|
|
61
|
+
edges?: unknown[];
|
|
60
62
|
}
|
|
61
63
|
/** A layer's effects: the effect list as JSON plus the PSD block they came from (base64). */
|
|
62
64
|
export interface SerializedLayerEffects {
|
|
@@ -240,6 +242,8 @@ export interface SerializedLayeredImageEditorState {
|
|
|
240
242
|
/** Blur migrated from a v1 / v2 document with adjustment layers (runs after the layers). */
|
|
241
243
|
legacyOutputBlur?: number;
|
|
242
244
|
selection?: SerializedSelection;
|
|
245
|
+
/** The selection's recipe (live selections): the steps that made it, as JSON. */
|
|
246
|
+
selectionRecipe?: unknown[];
|
|
243
247
|
/** Alpha channels (saved selections), run-length encoded. */
|
|
244
248
|
channels?: SerializedAlphaChannel[];
|
|
245
249
|
/** Document paths (the Paths panel). */
|
|
@@ -6,26 +6,31 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CropBox } from '../types.cjs';
|
|
8
8
|
import type { AffineMatrix } from '../../geom/types.cjs';
|
|
9
|
+
import { readRegion } from '../../raster/surface.cjs';
|
|
9
10
|
import type { ImageLayer, RasterMask, VectorMask } from './model.cjs';
|
|
10
11
|
/** Whether a vector mask takes part in compositing. */
|
|
11
12
|
export declare function isActiveVectorMask(vm: VectorMask | undefined): vm is VectorMask;
|
|
12
13
|
/**
|
|
13
|
-
* The vector mask's coverage (0..1) over the `width × height` canvas, with its
|
|
14
|
-
* density applied.
|
|
14
|
+
* The vector mask's coverage (0..1) over the `width × height` canvas, with its hybrid edges,
|
|
15
|
+
* feather and density applied.
|
|
15
16
|
*/
|
|
16
|
-
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number): Float32Array;
|
|
17
|
+
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number, guide?: ArrayLike<number> | null): Float32Array;
|
|
18
|
+
/** Where a layer's pixels really are (the Background's come from the source image). */
|
|
19
|
+
export type LayerSurfaceOf = (layer: ImageLayer) => Parameters<typeof readRegion>[0] | undefined;
|
|
20
|
+
/** A layer's own RGBA over the canvas (the guide for hybrid matte edges), or null without pixels. */
|
|
21
|
+
export declare function layerGuide(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): Uint8ClampedArray | null;
|
|
17
22
|
/**
|
|
18
23
|
* The raster mask a layer composites with: its raster mask times its vector mask (layer space,
|
|
19
24
|
* feather and density baked in), or the raster mask alone when there is no active vector mask.
|
|
20
25
|
* A depth (parametric) mask cannot be combined, so it wins.
|
|
21
26
|
*/
|
|
22
|
-
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number): RasterMask | undefined;
|
|
27
|
+
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): RasterMask | undefined;
|
|
23
28
|
/**
|
|
24
29
|
* The stack as the compositor should see it: each layer with an active vector mask replaced by a
|
|
25
30
|
* copy whose `mask` is `effectiveLayerMask` (cached per layer object and canvas size, so the
|
|
26
31
|
* engine's tile caches keep working). Returns `layers` itself when there are none.
|
|
27
32
|
*/
|
|
28
|
-
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number): readonly ImageLayer[];
|
|
33
|
+
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number, surfaceOf?: LayerSurfaceOf): readonly ImageLayer[];
|
|
29
34
|
/** A vector mask moved by (dx, dy) document pixels. */
|
|
30
35
|
export declare function offsetVectorMask(vm: VectorMask, dx: number, dy: number): VectorMask;
|
|
31
36
|
/** A vector mask with its paths mapped by `f` (document space); `affine` keeps live shapes live. */
|
|
@@ -6,26 +6,31 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CropBox } from '../types.js';
|
|
8
8
|
import type { AffineMatrix } from '../../geom/types.js';
|
|
9
|
+
import { readRegion } from '../../raster/surface.js';
|
|
9
10
|
import type { ImageLayer, RasterMask, VectorMask } from './model.js';
|
|
10
11
|
/** Whether a vector mask takes part in compositing. */
|
|
11
12
|
export declare function isActiveVectorMask(vm: VectorMask | undefined): vm is VectorMask;
|
|
12
13
|
/**
|
|
13
|
-
* The vector mask's coverage (0..1) over the `width × height` canvas, with its
|
|
14
|
-
* density applied.
|
|
14
|
+
* The vector mask's coverage (0..1) over the `width × height` canvas, with its hybrid edges,
|
|
15
|
+
* feather and density applied.
|
|
15
16
|
*/
|
|
16
|
-
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number): Float32Array;
|
|
17
|
+
export declare function vectorMaskCoverage(vm: VectorMask, width: number, height: number, guide?: ArrayLike<number> | null): Float32Array;
|
|
18
|
+
/** Where a layer's pixels really are (the Background's come from the source image). */
|
|
19
|
+
export type LayerSurfaceOf = (layer: ImageLayer) => Parameters<typeof readRegion>[0] | undefined;
|
|
20
|
+
/** A layer's own RGBA over the canvas (the guide for hybrid matte edges), or null without pixels. */
|
|
21
|
+
export declare function layerGuide(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): Uint8ClampedArray | null;
|
|
17
22
|
/**
|
|
18
23
|
* The raster mask a layer composites with: its raster mask times its vector mask (layer space,
|
|
19
24
|
* feather and density baked in), or the raster mask alone when there is no active vector mask.
|
|
20
25
|
* A depth (parametric) mask cannot be combined, so it wins.
|
|
21
26
|
*/
|
|
22
|
-
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number): RasterMask | undefined;
|
|
27
|
+
export declare function effectiveLayerMask(layer: ImageLayer, width: number, height: number, surfaceOf?: LayerSurfaceOf): RasterMask | undefined;
|
|
23
28
|
/**
|
|
24
29
|
* The stack as the compositor should see it: each layer with an active vector mask replaced by a
|
|
25
30
|
* copy whose `mask` is `effectiveLayerMask` (cached per layer object and canvas size, so the
|
|
26
31
|
* engine's tile caches keep working). Returns `layers` itself when there are none.
|
|
27
32
|
*/
|
|
28
|
-
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number): readonly ImageLayer[];
|
|
33
|
+
export declare function resolveVectorMasks(layers: readonly ImageLayer[], width: number, height: number, surfaceOf?: LayerSurfaceOf): readonly ImageLayer[];
|
|
29
34
|
/** A vector mask moved by (dx, dy) document pixels. */
|
|
30
35
|
export declare function offsetVectorMask(vm: VectorMask, dx: number, dy: number): VectorMask;
|
|
31
36
|
/** A vector mask with its paths mapped by `f` (document space); `affine` keeps live shapes live. */
|
|
@@ -22,6 +22,8 @@ export interface PathAnchorRef {
|
|
|
22
22
|
export interface PathEditOverlay {
|
|
23
23
|
/** The target's segments: 2 points for a line, 4 for a cubic. */
|
|
24
24
|
curves: ImagePoint[][];
|
|
25
|
+
/** A hybrid vector mask's edge mode per curve (same order); missing: all hard. */
|
|
26
|
+
curveModes?: Array<'hard' | 'feather' | 'matte'>;
|
|
25
27
|
/** Anchors shown; selected ones are drawn filled. */
|
|
26
28
|
anchors: Array<{
|
|
27
29
|
point: ImagePoint;
|
|
@@ -77,6 +79,8 @@ export declare class PathEditGesture {
|
|
|
77
79
|
private selectComponent;
|
|
78
80
|
private commit;
|
|
79
81
|
down(p: ImagePoint, tool: PathEditTool, mods?: PathEditModifiers): void;
|
|
82
|
+
/** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
|
|
83
|
+
private cycleEdge;
|
|
80
84
|
/** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
|
|
81
85
|
private convert;
|
|
82
86
|
private penDown;
|
|
@@ -22,6 +22,8 @@ export interface PathAnchorRef {
|
|
|
22
22
|
export interface PathEditOverlay {
|
|
23
23
|
/** The target's segments: 2 points for a line, 4 for a cubic. */
|
|
24
24
|
curves: ImagePoint[][];
|
|
25
|
+
/** A hybrid vector mask's edge mode per curve (same order); missing: all hard. */
|
|
26
|
+
curveModes?: Array<'hard' | 'feather' | 'matte'>;
|
|
25
27
|
/** Anchors shown; selected ones are drawn filled. */
|
|
26
28
|
anchors: Array<{
|
|
27
29
|
point: ImagePoint;
|
|
@@ -77,6 +79,8 @@ export declare class PathEditGesture {
|
|
|
77
79
|
private selectComponent;
|
|
78
80
|
private commit;
|
|
79
81
|
down(p: ImagePoint, tool: PathEditTool, mods?: PathEditModifiers): void;
|
|
82
|
+
/** Alt+Shift-click on a vector mask's segment: hard → feather → matte → hard (one undo step). */
|
|
83
|
+
private cycleEdge;
|
|
80
84
|
/** Alt-click: a corner anchor becomes smooth, a smooth one a corner (one undo step). */
|
|
81
85
|
private convert;
|
|
82
86
|
private penDown;
|
|
@@ -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). */
|
|
@@ -7,7 +7,7 @@ var _chunkZ64NRDEHcjs = require('../chunk-Z64NRDEH.cjs');
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _chunkR35YOUP2cjs = require('../chunk-R35YOUP2.cjs');
|
|
11
11
|
require('../chunk-A2IS7EHE.cjs');
|
|
12
12
|
require('../chunk-SS53NNZO.cjs');
|
|
13
13
|
|
|
@@ -1659,11 +1659,11 @@ function header(state, options) {
|
|
|
1659
1659
|
return out;
|
|
1660
1660
|
}
|
|
1661
1661
|
function serializePaintDocument(state, options = {}) {
|
|
1662
|
-
const { layers, assets } =
|
|
1662
|
+
const { layers, assets } = _chunkR35YOUP2cjs.serializeLayers.call(void 0, state.layers);
|
|
1663
1663
|
return { ...header(state, options), layers, assets };
|
|
1664
1664
|
}
|
|
1665
1665
|
async function serializePaintDocumentExternal(state, store, options = {}) {
|
|
1666
|
-
const { layers, assets } = await
|
|
1666
|
+
const { layers, assets } = await _chunkR35YOUP2cjs.serializeLayersExternal.call(void 0, state.layers, store);
|
|
1667
1667
|
return { ...header(state, options), layers, assets };
|
|
1668
1668
|
}
|
|
1669
1669
|
function paintLayersOnly(layers, problems) {
|
|
@@ -1700,7 +1700,7 @@ function deserializePaintDocument(raw) {
|
|
|
1700
1700
|
const canvas = normalizeCanvas({ width: r.width, height: r.height, dpi: _nullishCoalesce(r.dpi, () => ( void 0)) });
|
|
1701
1701
|
const paper = _chunkROVK6AXXcjs.normalizePaper.call(void 0, _nullishCoalesce(r.paper, () => ( {})));
|
|
1702
1702
|
const assets = r.assets && typeof r.assets === "object" ? r.assets : {};
|
|
1703
|
-
const layers = paintLayersOnly(
|
|
1703
|
+
const layers = paintLayersOnly(_chunkR35YOUP2cjs.deserializeLayers.call(void 0, r.layers, { assets, problems }), problems);
|
|
1704
1704
|
const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
|
|
1705
1705
|
const activeLayerId = wanted && _chunkB46UQUDCcjs.findLayer.call(void 0, layers, wanted) ? wanted : _nullishCoalesce(_optionalChain([_chunkB46UQUDCcjs.flattenLayers.call(void 0, layers), 'access', _70 => _70.filter, 'call', _71 => _71((l) => l.type === "pixel"), 'access', _72 => _72.pop, 'call', _73 => _73(), 'optionalAccess', _74 => _74.id]), () => ( null));
|
|
1706
1706
|
const wetEvents = Array.isArray(r.wetEvents) ? r.wetEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.clock) && typeof e.kind === "string") : [];
|
|
@@ -1716,7 +1716,7 @@ async function deserializePaintDocumentAsync(raw, store) {
|
|
|
1716
1716
|
if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
|
|
1717
1717
|
const r = raw;
|
|
1718
1718
|
const problems = [];
|
|
1719
|
-
const assets = await
|
|
1719
|
+
const assets = await _chunkR35YOUP2cjs.resolveExternalAssets.call(void 0, _nullishCoalesce(r.assets, () => ( {})), store, problems);
|
|
1720
1720
|
const loaded = deserializePaintDocument({ ...r, assets });
|
|
1721
1721
|
return { ...loaded, problems: [...problems, ...loaded.problems] };
|
|
1722
1722
|
}
|
|
@@ -5468,7 +5468,7 @@ var PaintEditorController = (_class10 = class _PaintEditorController {
|
|
|
5468
5468
|
return next.type === "group" ? { ...next, children: toStandard(next.children) } : next;
|
|
5469
5469
|
});
|
|
5470
5470
|
const layers = toStandard(dried);
|
|
5471
|
-
const result =
|
|
5471
|
+
const result = _chunkR35YOUP2cjs.exportPsd.call(void 0, { width, height, layers: [paperLayer, ...layers], merged, resolution: dpi });
|
|
5472
5472
|
for (const [mode, names] of changed) {
|
|
5473
5473
|
const label = _nullishCoalesce(_optionalChain([_chunkOLNIPTB3cjs.BLEND_MODES, 'access', _279 => _279.find, 'call', _280 => _280((m) => m.id === mode), 'optionalAccess', _281 => _281.label]), () => ( mode));
|
|
5474
5474
|
const std = _nullishCoalesce(_optionalChain([_chunkOLNIPTB3cjs.BLEND_MODES, 'access', _282 => _282.find, 'call', _283 => _283((m) => m.id === _chunkOLNIPTB3cjs.standardBlendMode.call(void 0, mode)), 'optionalAccess', _284 => _284.label]), () => ( _chunkOLNIPTB3cjs.standardBlendMode.call(void 0, mode)));
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
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. |
|