@pixerate/editor 0.179.0 → 0.180.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/image-editor/controller.d.cts +25 -0
- package/dist/image-editor/controller.d.ts +25 -0
- package/dist/image-editor/index.cjs +466 -303
- package/dist/image-editor/index.js +188 -25
- package/dist/image-editor/layers/panel.d.cts +2 -1
- package/dist/image-editor/layers/panel.d.ts +2 -1
- package/dist/image-editor/renderer.d.cts +4 -1
- package/dist/image-editor/renderer.d.ts +4 -1
- 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/overlay.d.cts +8 -0
- package/dist/image-editor/selection/overlay.d.ts +8 -0
- package/dist/image-editor/selection/segmentation.d.cts +49 -0
- package/dist/image-editor/selection/segmentation.d.ts +49 -0
- package/dist/image-editor/selection-tools.d.cts +18 -1
- package/dist/image-editor/selection-tools.d.ts +18 -1
- package/dist/image-editor/types.d.cts +7 -0
- package/dist/image-editor/types.d.ts +7 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +5 -3
|
@@ -948,6 +948,24 @@ function quickMaskPixels(mask, style = {}) {
|
|
|
948
948
|
}
|
|
949
949
|
return out;
|
|
950
950
|
}
|
|
951
|
+
var MARCHING_ANTS_STEP_MS = 100;
|
|
952
|
+
function prefersReducedMotion() {
|
|
953
|
+
try {
|
|
954
|
+
return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
955
|
+
} catch {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function startMarchingAnts(tick, stepMs = MARCHING_ANTS_STEP_MS) {
|
|
960
|
+
if (prefersReducedMotion()) return () => {
|
|
961
|
+
};
|
|
962
|
+
let offset = 0;
|
|
963
|
+
const id = setInterval(() => {
|
|
964
|
+
offset = (offset + 1) % 8;
|
|
965
|
+
tick(offset);
|
|
966
|
+
}, stepMs);
|
|
967
|
+
return () => clearInterval(id);
|
|
968
|
+
}
|
|
951
969
|
|
|
952
970
|
// src/image-editor/layers/render.ts
|
|
953
971
|
function isQuarterTurn(rotate) {
|
|
@@ -1353,8 +1371,8 @@ var ImageEditorRenderer = class {
|
|
|
1353
1371
|
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1354
1372
|
ctx.clip();
|
|
1355
1373
|
if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
|
|
1356
|
-
else if (state.selection?.bounds && display === "ants") this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);
|
|
1357
|
-
if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);
|
|
1374
|
+
else if (state.selection?.bounds && display === "ants") this.drawAnts(ctx, state.selection, projection.scale / u, options.antsOffset ?? 0);
|
|
1375
|
+
if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale / u);
|
|
1358
1376
|
} catch {
|
|
1359
1377
|
}
|
|
1360
1378
|
ctx.restore();
|
|
@@ -1772,7 +1790,10 @@ var ImageEditorRenderer = class {
|
|
|
1772
1790
|
}
|
|
1773
1791
|
ctx.drawImage(canvas, 0, 0, selection.width, selection.height);
|
|
1774
1792
|
}
|
|
1775
|
-
/**
|
|
1793
|
+
/**
|
|
1794
|
+
* Marching ants: a white line under a black dashed one, one screen pixel wide. `scale` is
|
|
1795
|
+
* screen pixels per natural pixel (the projection scale times the host's CSS scale).
|
|
1796
|
+
*/
|
|
1776
1797
|
drawAnts(ctx, selection, scale, offset) {
|
|
1777
1798
|
let segs = this.outlineCache.get(selection);
|
|
1778
1799
|
if (!segs) {
|
|
@@ -3082,6 +3103,35 @@ function hueRange(pixels, center) {
|
|
|
3082
3103
|
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3083
3104
|
}
|
|
3084
3105
|
|
|
3106
|
+
// src/image-editor/selection/segmentation.ts
|
|
3107
|
+
var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
|
|
3108
|
+
function polygonsOf(result) {
|
|
3109
|
+
const p = result.polygon;
|
|
3110
|
+
if (!Array.isArray(p) || !p.length) return [];
|
|
3111
|
+
const rings = Array.isArray(p[0]) ? p : [p];
|
|
3112
|
+
return rings.map((ring) => Array.isArray(ring) ? ring.filter(isPoint) : []).filter((ring) => ring.length >= 3);
|
|
3113
|
+
}
|
|
3114
|
+
function segmentToSelection(width, height, result) {
|
|
3115
|
+
let out = null;
|
|
3116
|
+
for (const ring of polygonsOf(result)) {
|
|
3117
|
+
out = combineSelections(out, polygonSelection(width, height, ring), "add");
|
|
3118
|
+
}
|
|
3119
|
+
if (result.mask && result.mask.width > 0 && result.mask.height > 0) {
|
|
3120
|
+
const raw = selectionFromPixels(result.mask, { channel: "luminance" });
|
|
3121
|
+
const sized = raw.width === width && raw.height === height ? raw : resizeSelection(raw, width, height);
|
|
3122
|
+
out = combineSelections(out, sized, "add");
|
|
3123
|
+
}
|
|
3124
|
+
return out?.bounds ? out : null;
|
|
3125
|
+
}
|
|
3126
|
+
function segmentsToSelection(width, height, results) {
|
|
3127
|
+
let out = null;
|
|
3128
|
+
for (const r of results) {
|
|
3129
|
+
const sel = segmentToSelection(width, height, r);
|
|
3130
|
+
if (sel) out = combineSelections(out, sel, "add");
|
|
3131
|
+
}
|
|
3132
|
+
return out;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3085
3135
|
// src/image-editor/selection/refine.ts
|
|
3086
3136
|
var DEFAULT_REFINE_EDGE_PARAMS = {
|
|
3087
3137
|
radius: 0,
|
|
@@ -4669,6 +4719,8 @@ var ImageEditorController = class {
|
|
|
4669
4719
|
viewKey = [];
|
|
4670
4720
|
/** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */
|
|
4671
4721
|
compositor = new LayerCompositor();
|
|
4722
|
+
/** The segmentation provider behind `selectSubject` (see `setSegmentationProvider`). */
|
|
4723
|
+
segmentation = null;
|
|
4672
4724
|
/** The effects provider slot (see `setEffectsProvider`). */
|
|
4673
4725
|
effects = new EffectsSlot();
|
|
4674
4726
|
/** Renders provider effects: effect smart filter results and effect layers. */
|
|
@@ -4678,6 +4730,7 @@ var ImageEditorController = class {
|
|
|
4678
4730
|
this.effects.subscribe(() => this.queueEffectRefresh("all"));
|
|
4679
4731
|
this.effectsEngine.subscribe(() => this.queueEffectRefresh("incomplete"));
|
|
4680
4732
|
if (options.effects) this.effects.set(options.effects);
|
|
4733
|
+
if (options.segmentation) this.segmentation = options.segmentation;
|
|
4681
4734
|
this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
|
|
4682
4735
|
if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
|
|
4683
4736
|
this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
|
|
@@ -4859,6 +4912,16 @@ var ImageEditorController = class {
|
|
|
4859
4912
|
this.state.layers = next;
|
|
4860
4913
|
this.notify();
|
|
4861
4914
|
}
|
|
4915
|
+
/** The registered segmentation provider, or `null` (then `selectSubject` is unavailable). */
|
|
4916
|
+
get segmentationProvider() {
|
|
4917
|
+
return this.segmentation;
|
|
4918
|
+
}
|
|
4919
|
+
/** Registers (or with `null` removes) the segmentation provider. Not an edit: no undo step. */
|
|
4920
|
+
setSegmentationProvider(provider) {
|
|
4921
|
+
if (this.segmentation === provider) return;
|
|
4922
|
+
this.segmentation = provider;
|
|
4923
|
+
this.notify();
|
|
4924
|
+
}
|
|
4862
4925
|
/** The registered effects provider, or `null`. */
|
|
4863
4926
|
get effectsProvider() {
|
|
4864
4927
|
return this.effects.provider;
|
|
@@ -7615,23 +7678,50 @@ var ImageEditorController = class {
|
|
|
7615
7678
|
* RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.
|
|
7616
7679
|
*/
|
|
7617
7680
|
async loadSelectionFromImage(source, options = {}) {
|
|
7618
|
-
|
|
7619
|
-
if (typeof source === "string") {
|
|
7620
|
-
const img = await this.renderer.imageCache.get(source);
|
|
7621
|
-
const w = img.naturalWidth || img.width;
|
|
7622
|
-
const h = img.naturalHeight || img.height;
|
|
7623
|
-
const canvas = this.renderer.createCanvas(w, h);
|
|
7624
|
-
const ctx = canvas.getContext("2d");
|
|
7625
|
-
if (ctx) ctx.drawImage(img, 0, 0, w, h);
|
|
7626
|
-
const data = ctx?.getImageData?.(0, 0, w, h);
|
|
7627
|
-
if (!data) throw new Error("The mask image pixels are not available (no canvas access); pass pixels.");
|
|
7628
|
-
pixels = { width: w, height: h, data: data.data };
|
|
7629
|
-
} else {
|
|
7630
|
-
pixels = source;
|
|
7631
|
-
}
|
|
7681
|
+
const pixels = await this.decodeMaskImage(source);
|
|
7632
7682
|
this.setSelection(selectionFromPixels(pixels, options), options.mode ?? "replace");
|
|
7633
7683
|
return this.getSelection();
|
|
7634
7684
|
}
|
|
7685
|
+
/** A mask image (data URL / URL, or pixels as they are) as RGBA pixels. */
|
|
7686
|
+
async decodeMaskImage(source) {
|
|
7687
|
+
if (typeof source !== "string") return source;
|
|
7688
|
+
const img = await this.renderer.imageCache.get(source);
|
|
7689
|
+
const w = img.naturalWidth || img.width;
|
|
7690
|
+
const h = img.naturalHeight || img.height;
|
|
7691
|
+
const canvas = this.renderer.createCanvas(w, h);
|
|
7692
|
+
const ctx = canvas.getContext("2d");
|
|
7693
|
+
if (ctx) ctx.drawImage(img, 0, 0, w, h);
|
|
7694
|
+
const data = ctx?.getImageData?.(0, 0, w, h);
|
|
7695
|
+
if (!data) throw new Error("The mask image pixels are not available (no canvas access); pass pixels.");
|
|
7696
|
+
return { width: w, height: h, data: data.data };
|
|
7697
|
+
}
|
|
7698
|
+
/**
|
|
7699
|
+
* Select › Subject: asks the segmentation provider for the objects matching `target` ("the
|
|
7700
|
+
* dog", "the sky") in the flattened image and selects their shapes, combined with the
|
|
7701
|
+
* selection by `mode`, as one undo step. Returns what was found (labels and bounds in natural
|
|
7702
|
+
* pixels); an empty list leaves the selection as it was. Throws without a provider.
|
|
7703
|
+
*/
|
|
7704
|
+
async selectSubject(target, options = {}) {
|
|
7705
|
+
const provider = this.segmentation;
|
|
7706
|
+
if (!provider) throw new Error("Select Subject needs a segmentation provider (setSegmentationProvider).");
|
|
7707
|
+
const description = target.trim();
|
|
7708
|
+
if (!description) throw new Error("Describe the subject to select.");
|
|
7709
|
+
const { width, height } = this.naturalSize;
|
|
7710
|
+
const image = await this.toDataURL({ format: "image/png" });
|
|
7711
|
+
const results = await provider.segment({ image, width, height, target: description, maxSegments: options.maxSegments, signal: options.signal });
|
|
7712
|
+
options.signal?.throwIfAborted();
|
|
7713
|
+
const decoded = [];
|
|
7714
|
+
for (const r of results ?? []) {
|
|
7715
|
+
decoded.push({ ...r, mask: r.mask !== void 0 ? await this.decodeMaskImage(r.mask) : void 0 });
|
|
7716
|
+
}
|
|
7717
|
+
const segments = decoded.map((r) => {
|
|
7718
|
+
const one = segmentsToSelection(width, height, [r]);
|
|
7719
|
+
return { label: r.label || description, bounds: one?.bounds ?? null, ...r.score !== void 0 ? { score: r.score } : {} };
|
|
7720
|
+
});
|
|
7721
|
+
const mask = segmentsToSelection(width, height, decoded);
|
|
7722
|
+
if (mask) this.shapeCommand(mask, options);
|
|
7723
|
+
return { segments: segments.filter((s) => s.bounds) };
|
|
7724
|
+
}
|
|
7635
7725
|
/** Quick Mask view (tints the unselected area). View state: no undo step. */
|
|
7636
7726
|
setQuickMask(on) {
|
|
7637
7727
|
if (Boolean(this.state.quickMask) === on) return;
|
|
@@ -10022,7 +10112,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
10022
10112
|
// src/image-editor/playback.ts
|
|
10023
10113
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
10024
10114
|
var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
|
|
10025
|
-
var SELECT = /^(select_region|select_by_color|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy)$/;
|
|
10115
|
+
var SELECT = /^(select_region|select_subject|select_by_color|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy)$/;
|
|
10026
10116
|
var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
|
|
10027
10117
|
var CROP = /^(set_crop|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
10028
10118
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -10043,7 +10133,7 @@ function createImagePlaybackLocator(controller) {
|
|
|
10043
10133
|
};
|
|
10044
10134
|
}
|
|
10045
10135
|
var num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
10046
|
-
var
|
|
10136
|
+
var isPoint2 = (p) => !!p && typeof p === "object" && num(p.x) && num(p.y);
|
|
10047
10137
|
function trace(points, max = 32) {
|
|
10048
10138
|
const list = points.length <= max ? points : Array.from({ length: max }, (_, i) => points[Math.round(i * (points.length - 1) / (max - 1))]);
|
|
10049
10139
|
if (!list.length) return [];
|
|
@@ -10079,7 +10169,7 @@ function planImageToolPlayback(tool, args) {
|
|
|
10079
10169
|
const steps = [];
|
|
10080
10170
|
const panel = imagePlaybackPanel(tool);
|
|
10081
10171
|
if (panel) steps.push({ type: "open", ref: { kind: "panel", id: panel } });
|
|
10082
|
-
const points = Array.isArray(args.points) ? args.points.filter(
|
|
10172
|
+
const points = Array.isArray(args.points) ? args.points.filter(isPoint2) : [];
|
|
10083
10173
|
const box = boxOf(args);
|
|
10084
10174
|
if (points.length) steps.push(...trace(points));
|
|
10085
10175
|
else if (box) steps.push({ type: "move", to: box.from }, { type: "press" }, { type: "drag", to: box.to }, { type: "release" });
|
|
@@ -10212,15 +10302,26 @@ function createImageSelectionTools(controller) {
|
|
|
10212
10302
|
const selectRegion = {
|
|
10213
10303
|
name: "select_region",
|
|
10214
10304
|
title: "Select a region",
|
|
10215
|
-
description: 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), everything ("all"), nothing ("none") or the inverse of the current selection ("invert"). One undoable step.',
|
|
10305
|
+
description: 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), a polygon (the lasso: points along an outline, closed automatically), everything ("all"), nothing ("none") or the inverse of the current selection ("invert"). To cut out a subject, trace its contour with polygon (then refine_selection for soft edges such as fur or foliage) rather than selecting its bounding box. One undoable step.',
|
|
10216
10306
|
inputSchema: {
|
|
10217
10307
|
type: "object",
|
|
10218
10308
|
properties: {
|
|
10219
|
-
shape: { type: "string", enum: ["rect", "ellipse", "all", "none", "invert"], description: "What to select." },
|
|
10309
|
+
shape: { type: "string", enum: ["rect", "ellipse", "polygon", "all", "none", "invert"], description: "What to select." },
|
|
10220
10310
|
x: { type: "number", description: "rect / ellipse: left edge in natural image pixels." },
|
|
10221
10311
|
y: { type: "number", description: "rect / ellipse: top edge in natural image pixels." },
|
|
10222
10312
|
width: { type: "number", description: "rect / ellipse: width in natural image pixels." },
|
|
10223
10313
|
height: { type: "number", description: "rect / ellipse: height in natural image pixels." },
|
|
10314
|
+
points: {
|
|
10315
|
+
type: "array",
|
|
10316
|
+
minItems: 3,
|
|
10317
|
+
items: {
|
|
10318
|
+
type: "object",
|
|
10319
|
+
properties: { x: { type: "number" }, y: { type: "number" } },
|
|
10320
|
+
required: ["x", "y"],
|
|
10321
|
+
additionalProperties: false
|
|
10322
|
+
},
|
|
10323
|
+
description: "polygon: at least 3 vertices along the outline, in order, in natural image pixels. Follow the edge closely; more points give a tighter contour."
|
|
10324
|
+
},
|
|
10224
10325
|
mode: modeSchema,
|
|
10225
10326
|
feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
|
|
10226
10327
|
},
|
|
@@ -10228,7 +10329,7 @@ function createImageSelectionTools(controller) {
|
|
|
10228
10329
|
additionalProperties: false
|
|
10229
10330
|
},
|
|
10230
10331
|
annotations: writeAnnotations("Select a region", true),
|
|
10231
|
-
handler: ({ shape, x, y, width, height, mode, feather }) => {
|
|
10332
|
+
handler: ({ shape, x, y, width, height, points, mode, feather }) => {
|
|
10232
10333
|
const opts = { mode: parseSelectionMode(mode), feather: num2(feather, 0) };
|
|
10233
10334
|
switch (shape) {
|
|
10234
10335
|
case "all":
|
|
@@ -10250,8 +10351,16 @@ function createImageSelectionTools(controller) {
|
|
|
10250
10351
|
else controller.selectEllipse(rect, opts);
|
|
10251
10352
|
break;
|
|
10252
10353
|
}
|
|
10354
|
+
case "polygon": {
|
|
10355
|
+
const list = Array.isArray(points) ? points.map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) : [];
|
|
10356
|
+
if (list.length < 3 || !list.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) {
|
|
10357
|
+
throw new Error("polygon needs points: at least 3 { x, y } vertices.");
|
|
10358
|
+
}
|
|
10359
|
+
controller.selectPolygon(list, opts);
|
|
10360
|
+
break;
|
|
10361
|
+
}
|
|
10253
10362
|
default:
|
|
10254
|
-
throw new Error(`Unknown shape "${String(shape)}". Use rect, ellipse, all, none or invert.`);
|
|
10363
|
+
throw new Error(`Unknown shape "${String(shape)}". Use rect, ellipse, polygon, all, none or invert.`);
|
|
10255
10364
|
}
|
|
10256
10365
|
return summary(controller);
|
|
10257
10366
|
}
|
|
@@ -10391,6 +10500,53 @@ function createImageSelectionTools(controller) {
|
|
|
10391
10500
|
};
|
|
10392
10501
|
return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];
|
|
10393
10502
|
}
|
|
10503
|
+
function createImageSubjectSelectionTool(controller) {
|
|
10504
|
+
return {
|
|
10505
|
+
name: "select_subject",
|
|
10506
|
+
title: "Select a subject",
|
|
10507
|
+
description: 'Select \u203A Subject: an AI segmentation model finds the objects matching target (a description such as "the dog", "the house", "the sky") in the flattened image and selects their outlines, combined with the selection by mode. Use it to cut a subject out by its contour (then layer_via_copy for a layer of just that subject); describe one object precisely to get only that one. One undoable step. Returns the selection and the objects found (label, bounds); none found leaves the selection unchanged.',
|
|
10508
|
+
inputSchema: {
|
|
10509
|
+
type: "object",
|
|
10510
|
+
properties: {
|
|
10511
|
+
target: { type: "string", minLength: 1, description: "What to select, in words." },
|
|
10512
|
+
maxSegments: {
|
|
10513
|
+
type: "integer",
|
|
10514
|
+
minimum: 1,
|
|
10515
|
+
maximum: 32,
|
|
10516
|
+
description: "Select at most this many matching objects (default the model\u2019s; 1 for a single subject)."
|
|
10517
|
+
},
|
|
10518
|
+
mode: modeSchema,
|
|
10519
|
+
feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
|
|
10520
|
+
},
|
|
10521
|
+
required: ["target"],
|
|
10522
|
+
additionalProperties: false
|
|
10523
|
+
},
|
|
10524
|
+
annotations: { ...writeAnnotations("Select a subject"), openWorldHint: true },
|
|
10525
|
+
handler: async ({ target, maxSegments, mode, feather }) => {
|
|
10526
|
+
if (typeof target !== "string" || !target.trim()) throw new Error("target must describe what to select.");
|
|
10527
|
+
const { segments } = await controller.selectSubject(target, {
|
|
10528
|
+
mode: parseSelectionMode(mode),
|
|
10529
|
+
feather: num2(feather, 0),
|
|
10530
|
+
maxSegments: typeof maxSegments === "number" ? Math.max(1, Math.round(maxSegments)) : void 0
|
|
10531
|
+
});
|
|
10532
|
+
return { ...summary(controller), found: segments };
|
|
10533
|
+
}
|
|
10534
|
+
};
|
|
10535
|
+
}
|
|
10536
|
+
function createImageSegmentationToolProvider(controller, options = {}) {
|
|
10537
|
+
const tool = createImageSubjectSelectionTool(controller);
|
|
10538
|
+
return withImagePlayback(
|
|
10539
|
+
{
|
|
10540
|
+
id: options.id ?? "image-segmentation",
|
|
10541
|
+
namespace: options.namespace,
|
|
10542
|
+
title: "Image segmentation",
|
|
10543
|
+
description: "Select \u203A Subject: select objects in the image by description through an AI segmentation model.",
|
|
10544
|
+
skill: "pixerate-image-editor",
|
|
10545
|
+
tools: () => controller.segmentationProvider ? [tool] : []
|
|
10546
|
+
},
|
|
10547
|
+
options.playbackSurface
|
|
10548
|
+
);
|
|
10549
|
+
}
|
|
10394
10550
|
|
|
10395
10551
|
// src/image-editor/capture.ts
|
|
10396
10552
|
var NO_CANVAS = "Rendering is not available in this environment.";
|
|
@@ -11176,7 +11332,7 @@ function createImageLayerTools(controller) {
|
|
|
11176
11332
|
const viaCopy = {
|
|
11177
11333
|
name: "layer_via_copy",
|
|
11178
11334
|
title: "New layer from the selection",
|
|
11179
|
-
description: "Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection. One undo step. Returns the new layer.",
|
|
11335
|
+
description: "Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection; the new layer takes the selection\u2019s shape, so select the subject\u2019s outline (select_region polygon, select_by_color) to isolate it, not its bounding box. One undo step. Returns the new layer.",
|
|
11180
11336
|
inputSchema: {
|
|
11181
11337
|
type: "object",
|
|
11182
11338
|
properties: {
|
|
@@ -14746,6 +14902,7 @@ function layerPanels(layer, options = {}) {
|
|
|
14746
14902
|
if (layer.mask) {
|
|
14747
14903
|
panels.push({ id: "mask", title: "Mask Options" });
|
|
14748
14904
|
}
|
|
14905
|
+
panels.push({ id: "selection", title: "Selection" });
|
|
14749
14906
|
return panels;
|
|
14750
14907
|
}
|
|
14751
14908
|
|
|
@@ -15880,6 +16037,7 @@ export {
|
|
|
15880
16037
|
LayerCompositor,
|
|
15881
16038
|
LayerEffectError,
|
|
15882
16039
|
LayerOperationError,
|
|
16040
|
+
MARCHING_ANTS_STEP_MS,
|
|
15883
16041
|
MAX_BATCH,
|
|
15884
16042
|
MAX_BRUSH_SIZE,
|
|
15885
16043
|
MAX_CANVAS_BACKING_SIDE,
|
|
@@ -16159,9 +16317,11 @@ export {
|
|
|
16159
16317
|
createImagePlaybackLocator,
|
|
16160
16318
|
createImagePsdTools,
|
|
16161
16319
|
createImageRetouchTools,
|
|
16320
|
+
createImageSegmentationToolProvider,
|
|
16162
16321
|
createImageSelectionTools,
|
|
16163
16322
|
createImageShapeTools,
|
|
16164
16323
|
createImageSmartObjectTools,
|
|
16324
|
+
createImageSubjectSelectionTool,
|
|
16165
16325
|
createImageTransformTools,
|
|
16166
16326
|
createImageTypeTools,
|
|
16167
16327
|
createMetricTextShaper,
|
|
@@ -16602,6 +16762,8 @@ export {
|
|
|
16602
16762
|
scalePaintGradient,
|
|
16603
16763
|
seamlessClone,
|
|
16604
16764
|
seedFromString,
|
|
16765
|
+
segmentToSelection,
|
|
16766
|
+
segmentsToSelection,
|
|
16605
16767
|
selectionArea,
|
|
16606
16768
|
selectionFromCoverage,
|
|
16607
16769
|
selectionFromPixels,
|
|
@@ -16659,6 +16821,7 @@ export {
|
|
|
16659
16821
|
srgbEncode,
|
|
16660
16822
|
srgbToLinear,
|
|
16661
16823
|
standardBlendMode,
|
|
16824
|
+
startMarchingAnts,
|
|
16662
16825
|
stepSlotTs,
|
|
16663
16826
|
strokeGrainCatch,
|
|
16664
16827
|
styleAt,
|
|
@@ -123,6 +123,7 @@ export interface LayerPanelsOptions {
|
|
|
123
123
|
*
|
|
124
124
|
* `properties` ("Layer Properties") is first for every layer and includes the layer's Layer Style
|
|
125
125
|
* (drop shadow, stroke, glow…: `layerEffectsPanel`) for layers that can take one, with or without a
|
|
126
|
-
* provider. There is no separate `effects` (Layer Effects) or `smart-filters` panel.
|
|
126
|
+
* provider. There is no separate `effects` (Layer Effects) or `smart-filters` panel. `selection`
|
|
127
|
+
* is last for every layer, and with no active layer follows `canvas-size`.
|
|
127
128
|
*/
|
|
128
129
|
export declare function layerPanels(layer: ImageLayer | null, options?: LayerPanelsOptions): LayerPanelDescriptor[];
|
|
@@ -123,6 +123,7 @@ export interface LayerPanelsOptions {
|
|
|
123
123
|
*
|
|
124
124
|
* `properties` ("Layer Properties") is first for every layer and includes the layer's Layer Style
|
|
125
125
|
* (drop shadow, stroke, glow…: `layerEffectsPanel`) for layers that can take one, with or without a
|
|
126
|
-
* provider. There is no separate `effects` (Layer Effects) or `smart-filters` panel.
|
|
126
|
+
* provider. There is no separate `effects` (Layer Effects) or `smart-filters` panel. `selection`
|
|
127
|
+
* is last for every layer, and with no active layer follows `canvas-size`.
|
|
127
128
|
*/
|
|
128
129
|
export declare function layerPanels(layer: ImageLayer | null, options?: LayerPanelsOptions): LayerPanelDescriptor[];
|
|
@@ -176,7 +176,10 @@ export declare class ImageEditorRenderer {
|
|
|
176
176
|
private outlineCache;
|
|
177
177
|
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
178
178
|
private drawQuickMask;
|
|
179
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Marching ants: a white line under a black dashed one, one screen pixel wide. `scale` is
|
|
181
|
+
* screen pixels per natural pixel (the projection scale times the host's CSS scale).
|
|
182
|
+
*/
|
|
180
183
|
private drawAnts;
|
|
181
184
|
private drawDraftPath;
|
|
182
185
|
private getSourceId;
|
|
@@ -176,7 +176,10 @@ export declare class ImageEditorRenderer {
|
|
|
176
176
|
private outlineCache;
|
|
177
177
|
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
178
178
|
private drawQuickMask;
|
|
179
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Marching ants: a white line under a black dashed one, one screen pixel wide. `scale` is
|
|
181
|
+
* screen pixels per natural pixel (the projection scale times the host's CSS scale).
|
|
182
|
+
*/
|
|
180
183
|
private drawAnts;
|
|
181
184
|
private drawDraftPath;
|
|
182
185
|
private getSourceId;
|
|
@@ -16,3 +16,11 @@ export interface QuickMaskStyle {
|
|
|
16
16
|
* selection shows through clear, as Photoshop's rubylith overlay.
|
|
17
17
|
*/
|
|
18
18
|
export declare function quickMaskPixels(mask: SelectionMask, style?: QuickMaskStyle): Uint8ClampedArray;
|
|
19
|
+
/** How often marching ants step (ms); each step moves the dashes one screen pixel. */
|
|
20
|
+
export declare const MARCHING_ANTS_STEP_MS = 100;
|
|
21
|
+
/**
|
|
22
|
+
* Marches the ants: calls `tick` with the next `RenderOptions.antsOffset` every `stepMs` until the
|
|
23
|
+
* returned stop function is called. Hosts run it while a selection is shown and render on each
|
|
24
|
+
* tick. With reduced motion requested the ants stand still and `tick` is never called.
|
|
25
|
+
*/
|
|
26
|
+
export declare function startMarchingAnts(tick: (offset: number) => void, stepMs?: number): () => void;
|
|
@@ -16,3 +16,11 @@ export interface QuickMaskStyle {
|
|
|
16
16
|
* selection shows through clear, as Photoshop's rubylith overlay.
|
|
17
17
|
*/
|
|
18
18
|
export declare function quickMaskPixels(mask: SelectionMask, style?: QuickMaskStyle): Uint8ClampedArray;
|
|
19
|
+
/** How often marching ants step (ms); each step moves the dashes one screen pixel. */
|
|
20
|
+
export declare const MARCHING_ANTS_STEP_MS = 100;
|
|
21
|
+
/**
|
|
22
|
+
* Marches the ants: calls `tick` with the next `RenderOptions.antsOffset` every `stepMs` until the
|
|
23
|
+
* returned stop function is called. Hosts run it while a selection is shown and render on each
|
|
24
|
+
* tick. With reduced motion requested the ants stand still and `tick` is never called.
|
|
25
|
+
*/
|
|
26
|
+
export declare function startMarchingAnts(tick: (offset: number) => void, stepMs?: number): () => void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CropBox, ImagePoint } from '../types.cjs';
|
|
2
|
+
import { type PixelBuffer, type SelectionMask } from './mask.cjs';
|
|
3
|
+
/** What a segmentation provider is asked for (see `SegmentationProvider`). */
|
|
4
|
+
export interface SegmentationRequest {
|
|
5
|
+
/** The flattened image at its natural size, as a PNG data URL. */
|
|
6
|
+
image: string;
|
|
7
|
+
/** The image's natural size in pixels. */
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
/** What to find, in words ("the dog", "the sky", "every separate object"). */
|
|
11
|
+
target: string;
|
|
12
|
+
/** Return at most this many objects (the provider's default when absent). */
|
|
13
|
+
maxSegments?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One object a provider found, in natural image pixels: an outline `polygon` (or several, for
|
|
18
|
+
* an object in pieces), or a `mask` covering the whole image (white = the object, any size;
|
|
19
|
+
* it is resampled), or both, which are combined.
|
|
20
|
+
*/
|
|
21
|
+
export interface SegmentationResult {
|
|
22
|
+
label?: string;
|
|
23
|
+
polygon?: ImagePoint[] | ImagePoint[][];
|
|
24
|
+
mask?: string | PixelBuffer;
|
|
25
|
+
/** The object's bounding box, when the provider knows it. */
|
|
26
|
+
box?: CropBox;
|
|
27
|
+
/** The provider's confidence, 0..1. */
|
|
28
|
+
score?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Finds objects in an image by a description and returns their shapes: an AI segmentation
|
|
32
|
+
* model (SAM, Gemini, a background remover…) behind `ImageEditorController.selectSubject`.
|
|
33
|
+
*/
|
|
34
|
+
export interface SegmentationProvider {
|
|
35
|
+
/** Shown in the UI and in tool results (e.g. "Gemini"). */
|
|
36
|
+
readonly name?: string;
|
|
37
|
+
segment(request: SegmentationRequest): Promise<SegmentationResult[]>;
|
|
38
|
+
}
|
|
39
|
+
/** A segmentation result with its `mask` already decoded to pixels (strings are decoded by the controller). */
|
|
40
|
+
export type DecodedSegmentationResult = Omit<SegmentationResult, 'mask'> & {
|
|
41
|
+
mask?: PixelBuffer;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* One segmentation result as a selection over a `width × height` image, or null when it has no
|
|
45
|
+
* usable shape. Polygons are anti-aliased; several polygons and a mask are added together.
|
|
46
|
+
*/
|
|
47
|
+
export declare function segmentToSelection(width: number, height: number, result: DecodedSegmentationResult): SelectionMask | null;
|
|
48
|
+
/** Every usable result added into one selection (null when none has a shape). */
|
|
49
|
+
export declare function segmentsToSelection(width: number, height: number, results: readonly DecodedSegmentationResult[]): SelectionMask | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CropBox, ImagePoint } from '../types.js';
|
|
2
|
+
import { type PixelBuffer, type SelectionMask } from './mask.js';
|
|
3
|
+
/** What a segmentation provider is asked for (see `SegmentationProvider`). */
|
|
4
|
+
export interface SegmentationRequest {
|
|
5
|
+
/** The flattened image at its natural size, as a PNG data URL. */
|
|
6
|
+
image: string;
|
|
7
|
+
/** The image's natural size in pixels. */
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
/** What to find, in words ("the dog", "the sky", "every separate object"). */
|
|
11
|
+
target: string;
|
|
12
|
+
/** Return at most this many objects (the provider's default when absent). */
|
|
13
|
+
maxSegments?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One object a provider found, in natural image pixels: an outline `polygon` (or several, for
|
|
18
|
+
* an object in pieces), or a `mask` covering the whole image (white = the object, any size;
|
|
19
|
+
* it is resampled), or both, which are combined.
|
|
20
|
+
*/
|
|
21
|
+
export interface SegmentationResult {
|
|
22
|
+
label?: string;
|
|
23
|
+
polygon?: ImagePoint[] | ImagePoint[][];
|
|
24
|
+
mask?: string | PixelBuffer;
|
|
25
|
+
/** The object's bounding box, when the provider knows it. */
|
|
26
|
+
box?: CropBox;
|
|
27
|
+
/** The provider's confidence, 0..1. */
|
|
28
|
+
score?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Finds objects in an image by a description and returns their shapes: an AI segmentation
|
|
32
|
+
* model (SAM, Gemini, a background remover…) behind `ImageEditorController.selectSubject`.
|
|
33
|
+
*/
|
|
34
|
+
export interface SegmentationProvider {
|
|
35
|
+
/** Shown in the UI and in tool results (e.g. "Gemini"). */
|
|
36
|
+
readonly name?: string;
|
|
37
|
+
segment(request: SegmentationRequest): Promise<SegmentationResult[]>;
|
|
38
|
+
}
|
|
39
|
+
/** A segmentation result with its `mask` already decoded to pixels (strings are decoded by the controller). */
|
|
40
|
+
export type DecodedSegmentationResult = Omit<SegmentationResult, 'mask'> & {
|
|
41
|
+
mask?: PixelBuffer;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* One segmentation result as a selection over a `width × height` image, or null when it has no
|
|
45
|
+
* usable shape. Polygons are anti-aliased; several polygons and a mask are added together.
|
|
46
|
+
*/
|
|
47
|
+
export declare function segmentToSelection(width: number, height: number, result: DecodedSegmentationResult): SelectionMask | null;
|
|
48
|
+
/** Every usable result added into one selection (null when none has a shape). */
|
|
49
|
+
export declare function segmentsToSelection(width: number, height: number, results: readonly DecodedSegmentationResult[]): SelectionMask | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentTool } from '../tools/registry.cjs';
|
|
1
|
+
import type { AgentTool, AgentToolProvider } from '../tools/registry.cjs';
|
|
2
2
|
import type { ImageEditorController } from './controller.cjs';
|
|
3
3
|
/**
|
|
4
4
|
* Agent tools for selections and masks (part of `createImageEditorToolProvider`):
|
|
@@ -6,3 +6,20 @@ import type { ImageEditorController } from './controller.cjs';
|
|
|
6
6
|
* and `apply_selection_as_mask`. Coordinates are natural pixels of the source image.
|
|
7
7
|
*/
|
|
8
8
|
export declare function createImageSelectionTools(controller: ImageEditorController): AgentTool[];
|
|
9
|
+
/** Options for `createImageSegmentationToolProvider`. */
|
|
10
|
+
export interface ImageSegmentationToolProviderOptions {
|
|
11
|
+
/** Provider id (default `image-segmentation`). */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** Tool-name prefix (`<namespace>__<name>`). */
|
|
14
|
+
namespace?: string;
|
|
15
|
+
/** Agent playback surface (default the image editor's). */
|
|
16
|
+
playbackSurface?: string;
|
|
17
|
+
}
|
|
18
|
+
/** The `select_subject` tool: Select › Subject through the controller's segmentation provider. */
|
|
19
|
+
export declare function createImageSubjectSelectionTool(controller: ImageEditorController): AgentTool;
|
|
20
|
+
/**
|
|
21
|
+
* Agent tools that need the controller's segmentation provider (`select_subject`). The list is
|
|
22
|
+
* empty while no provider is set, so a host can register this once and the tool appears when a
|
|
23
|
+
* model is available.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createImageSegmentationToolProvider(controller: ImageEditorController, options?: ImageSegmentationToolProviderOptions): AgentToolProvider;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentTool } from '../tools/registry.js';
|
|
1
|
+
import type { AgentTool, AgentToolProvider } from '../tools/registry.js';
|
|
2
2
|
import type { ImageEditorController } from './controller.js';
|
|
3
3
|
/**
|
|
4
4
|
* Agent tools for selections and masks (part of `createImageEditorToolProvider`):
|
|
@@ -6,3 +6,20 @@ import type { ImageEditorController } from './controller.js';
|
|
|
6
6
|
* and `apply_selection_as_mask`. Coordinates are natural pixels of the source image.
|
|
7
7
|
*/
|
|
8
8
|
export declare function createImageSelectionTools(controller: ImageEditorController): AgentTool[];
|
|
9
|
+
/** Options for `createImageSegmentationToolProvider`. */
|
|
10
|
+
export interface ImageSegmentationToolProviderOptions {
|
|
11
|
+
/** Provider id (default `image-segmentation`). */
|
|
12
|
+
id?: string;
|
|
13
|
+
/** Tool-name prefix (`<namespace>__<name>`). */
|
|
14
|
+
namespace?: string;
|
|
15
|
+
/** Agent playback surface (default the image editor's). */
|
|
16
|
+
playbackSurface?: string;
|
|
17
|
+
}
|
|
18
|
+
/** The `select_subject` tool: Select › Subject through the controller's segmentation provider. */
|
|
19
|
+
export declare function createImageSubjectSelectionTool(controller: ImageEditorController): AgentTool;
|
|
20
|
+
/**
|
|
21
|
+
* Agent tools that need the controller's segmentation provider (`select_subject`). The list is
|
|
22
|
+
* empty while no provider is set, so a host can register this once and the tool appears when a
|
|
23
|
+
* model is available.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createImageSegmentationToolProvider(controller: ImageEditorController, options?: ImageSegmentationToolProviderOptions): AgentToolProvider;
|
|
@@ -395,6 +395,7 @@ export interface ExportOptions {
|
|
|
395
395
|
layerIds?: string[];
|
|
396
396
|
}
|
|
397
397
|
import type { EffectProvider } from '../effects/types.cjs';
|
|
398
|
+
import type { SegmentationProvider } from './selection/segmentation.cjs';
|
|
398
399
|
export interface ImageEditorOptions {
|
|
399
400
|
/**
|
|
400
401
|
* What lays out and draws type layers. Default: the browser's fonts through Canvas 2D
|
|
@@ -421,6 +422,12 @@ export interface ImageEditorOptions {
|
|
|
421
422
|
* with `setEffectsProvider`.
|
|
422
423
|
*/
|
|
423
424
|
effects?: EffectProvider | null;
|
|
425
|
+
/**
|
|
426
|
+
* A segmentation provider (an AI model that finds objects by description) for Select ›
|
|
427
|
+
* Subject (`selectSubject`, the `select_subject` tool). Optional; change it later with
|
|
428
|
+
* `setSegmentationProvider`.
|
|
429
|
+
*/
|
|
430
|
+
segmentation?: SegmentationProvider | null;
|
|
424
431
|
}
|
|
425
432
|
export interface InpaintOptions {
|
|
426
433
|
prompt: string;
|
|
@@ -395,6 +395,7 @@ export interface ExportOptions {
|
|
|
395
395
|
layerIds?: string[];
|
|
396
396
|
}
|
|
397
397
|
import type { EffectProvider } from '../effects/types.js';
|
|
398
|
+
import type { SegmentationProvider } from './selection/segmentation.js';
|
|
398
399
|
export interface ImageEditorOptions {
|
|
399
400
|
/**
|
|
400
401
|
* What lays out and draws type layers. Default: the browser's fonts through Canvas 2D
|
|
@@ -421,6 +422,12 @@ export interface ImageEditorOptions {
|
|
|
421
422
|
* with `setEffectsProvider`.
|
|
422
423
|
*/
|
|
423
424
|
effects?: EffectProvider | null;
|
|
425
|
+
/**
|
|
426
|
+
* A segmentation provider (an AI model that finds objects by description) for Select ›
|
|
427
|
+
* Subject (`selectSubject`, the `select_subject` tool). Optional; change it later with
|
|
428
|
+
* `setSegmentationProvider`.
|
|
429
|
+
*/
|
|
430
|
+
segmentation?: SegmentationProvider | null;
|
|
424
431
|
}
|
|
425
432
|
export interface InpaintOptions {
|
|
426
433
|
prompt: string;
|