@pixerate/editor 0.178.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 +516 -324
- package/dist/image-editor/index.js +238 -46
- package/dist/image-editor/layers/panel.d.cts +2 -1
- package/dist/image-editor/layers/panel.d.ts +2 -1
- package/dist/image-editor/layers/transform-ui.d.cts +15 -7
- package/dist/image-editor/layers/transform-ui.d.ts +15 -7
- package/dist/image-editor/renderer.d.cts +6 -1
- package/dist/image-editor/renderer.d.ts +6 -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();
|
|
@@ -1460,6 +1478,12 @@ var ImageEditorRenderer = class {
|
|
|
1460
1478
|
outline.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
|
|
1461
1479
|
ctx.closePath();
|
|
1462
1480
|
}
|
|
1481
|
+
const rotate = t.rotate && t.outline.length >= 2 ? scr(t.rotate) : null;
|
|
1482
|
+
if (rotate) {
|
|
1483
|
+
const n = scr({ x: (t.outline[0].x + t.outline[1].x) / 2, y: (t.outline[0].y + t.outline[1].y) / 2 });
|
|
1484
|
+
ctx.moveTo(n.x, n.y);
|
|
1485
|
+
ctx.lineTo(rotate.x, rotate.y);
|
|
1486
|
+
}
|
|
1463
1487
|
ctx.lineWidth = 3 * u;
|
|
1464
1488
|
ctx.strokeStyle = "rgba(0,0,0,0.25)";
|
|
1465
1489
|
ctx.stroke();
|
|
@@ -1467,6 +1491,15 @@ var ImageEditorRenderer = class {
|
|
|
1467
1491
|
ctx.strokeStyle = "#ffffff";
|
|
1468
1492
|
ctx.stroke();
|
|
1469
1493
|
drawOverlayHandles(ctx, [...t.handles, ...t.warp?.anchors ?? []].map(scr), u);
|
|
1494
|
+
if (rotate) {
|
|
1495
|
+
ctx.beginPath();
|
|
1496
|
+
ctx.arc(rotate.x, rotate.y, 5 * u, 0, Math.PI * 2);
|
|
1497
|
+
ctx.fillStyle = "#ffffff";
|
|
1498
|
+
ctx.fill();
|
|
1499
|
+
ctx.lineWidth = 1.5 * u;
|
|
1500
|
+
ctx.strokeStyle = OVERLAY_HANDLE_EDGE;
|
|
1501
|
+
ctx.stroke();
|
|
1502
|
+
}
|
|
1470
1503
|
for (const h of (t.warp?.handles ?? []).map((w) => scr(w.point))) {
|
|
1471
1504
|
ctx.beginPath();
|
|
1472
1505
|
ctx.arc(h.x, h.y, 3.5 * u, 0, Math.PI * 2);
|
|
@@ -1757,7 +1790,10 @@ var ImageEditorRenderer = class {
|
|
|
1757
1790
|
}
|
|
1758
1791
|
ctx.drawImage(canvas, 0, 0, selection.width, selection.height);
|
|
1759
1792
|
}
|
|
1760
|
-
/**
|
|
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
|
+
*/
|
|
1761
1797
|
drawAnts(ctx, selection, scale, offset) {
|
|
1762
1798
|
let segs = this.outlineCache.get(selection);
|
|
1763
1799
|
if (!segs) {
|
|
@@ -3067,6 +3103,35 @@ function hueRange(pixels, center) {
|
|
|
3067
3103
|
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3068
3104
|
}
|
|
3069
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
|
+
|
|
3070
3135
|
// src/image-editor/selection/refine.ts
|
|
3071
3136
|
var DEFAULT_REFINE_EDGE_PARAMS = {
|
|
3072
3137
|
radius: 0,
|
|
@@ -4654,6 +4719,8 @@ var ImageEditorController = class {
|
|
|
4654
4719
|
viewKey = [];
|
|
4655
4720
|
/** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */
|
|
4656
4721
|
compositor = new LayerCompositor();
|
|
4722
|
+
/** The segmentation provider behind `selectSubject` (see `setSegmentationProvider`). */
|
|
4723
|
+
segmentation = null;
|
|
4657
4724
|
/** The effects provider slot (see `setEffectsProvider`). */
|
|
4658
4725
|
effects = new EffectsSlot();
|
|
4659
4726
|
/** Renders provider effects: effect smart filter results and effect layers. */
|
|
@@ -4663,6 +4730,7 @@ var ImageEditorController = class {
|
|
|
4663
4730
|
this.effects.subscribe(() => this.queueEffectRefresh("all"));
|
|
4664
4731
|
this.effectsEngine.subscribe(() => this.queueEffectRefresh("incomplete"));
|
|
4665
4732
|
if (options.effects) this.effects.set(options.effects);
|
|
4733
|
+
if (options.segmentation) this.segmentation = options.segmentation;
|
|
4666
4734
|
this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
|
|
4667
4735
|
if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
|
|
4668
4736
|
this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
|
|
@@ -4844,6 +4912,16 @@ var ImageEditorController = class {
|
|
|
4844
4912
|
this.state.layers = next;
|
|
4845
4913
|
this.notify();
|
|
4846
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
|
+
}
|
|
4847
4925
|
/** The registered effects provider, or `null`. */
|
|
4848
4926
|
get effectsProvider() {
|
|
4849
4927
|
return this.effects.provider;
|
|
@@ -7600,23 +7678,50 @@ var ImageEditorController = class {
|
|
|
7600
7678
|
* RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.
|
|
7601
7679
|
*/
|
|
7602
7680
|
async loadSelectionFromImage(source, options = {}) {
|
|
7603
|
-
|
|
7604
|
-
if (typeof source === "string") {
|
|
7605
|
-
const img = await this.renderer.imageCache.get(source);
|
|
7606
|
-
const w = img.naturalWidth || img.width;
|
|
7607
|
-
const h = img.naturalHeight || img.height;
|
|
7608
|
-
const canvas = this.renderer.createCanvas(w, h);
|
|
7609
|
-
const ctx = canvas.getContext("2d");
|
|
7610
|
-
if (ctx) ctx.drawImage(img, 0, 0, w, h);
|
|
7611
|
-
const data = ctx?.getImageData?.(0, 0, w, h);
|
|
7612
|
-
if (!data) throw new Error("The mask image pixels are not available (no canvas access); pass pixels.");
|
|
7613
|
-
pixels = { width: w, height: h, data: data.data };
|
|
7614
|
-
} else {
|
|
7615
|
-
pixels = source;
|
|
7616
|
-
}
|
|
7681
|
+
const pixels = await this.decodeMaskImage(source);
|
|
7617
7682
|
this.setSelection(selectionFromPixels(pixels, options), options.mode ?? "replace");
|
|
7618
7683
|
return this.getSelection();
|
|
7619
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
|
+
}
|
|
7620
7725
|
/** Quick Mask view (tints the unselected area). View state: no undo step. */
|
|
7621
7726
|
setQuickMask(on) {
|
|
7622
7727
|
if (Boolean(this.state.quickMask) === on) return;
|
|
@@ -10007,7 +10112,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
10007
10112
|
// src/image-editor/playback.ts
|
|
10008
10113
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
10009
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)$/;
|
|
10010
|
-
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)$/;
|
|
10011
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)$/;
|
|
10012
10117
|
var CROP = /^(set_crop|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
10013
10118
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -10028,7 +10133,7 @@ function createImagePlaybackLocator(controller) {
|
|
|
10028
10133
|
};
|
|
10029
10134
|
}
|
|
10030
10135
|
var num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
10031
|
-
var
|
|
10136
|
+
var isPoint2 = (p) => !!p && typeof p === "object" && num(p.x) && num(p.y);
|
|
10032
10137
|
function trace(points, max = 32) {
|
|
10033
10138
|
const list = points.length <= max ? points : Array.from({ length: max }, (_, i) => points[Math.round(i * (points.length - 1) / (max - 1))]);
|
|
10034
10139
|
if (!list.length) return [];
|
|
@@ -10064,7 +10169,7 @@ function planImageToolPlayback(tool, args) {
|
|
|
10064
10169
|
const steps = [];
|
|
10065
10170
|
const panel = imagePlaybackPanel(tool);
|
|
10066
10171
|
if (panel) steps.push({ type: "open", ref: { kind: "panel", id: panel } });
|
|
10067
|
-
const points = Array.isArray(args.points) ? args.points.filter(
|
|
10172
|
+
const points = Array.isArray(args.points) ? args.points.filter(isPoint2) : [];
|
|
10068
10173
|
const box = boxOf(args);
|
|
10069
10174
|
if (points.length) steps.push(...trace(points));
|
|
10070
10175
|
else if (box) steps.push({ type: "move", to: box.from }, { type: "press" }, { type: "drag", to: box.to }, { type: "release" });
|
|
@@ -10197,15 +10302,26 @@ function createImageSelectionTools(controller) {
|
|
|
10197
10302
|
const selectRegion = {
|
|
10198
10303
|
name: "select_region",
|
|
10199
10304
|
title: "Select a region",
|
|
10200
|
-
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.',
|
|
10201
10306
|
inputSchema: {
|
|
10202
10307
|
type: "object",
|
|
10203
10308
|
properties: {
|
|
10204
|
-
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." },
|
|
10205
10310
|
x: { type: "number", description: "rect / ellipse: left edge in natural image pixels." },
|
|
10206
10311
|
y: { type: "number", description: "rect / ellipse: top edge in natural image pixels." },
|
|
10207
10312
|
width: { type: "number", description: "rect / ellipse: width in natural image pixels." },
|
|
10208
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
|
+
},
|
|
10209
10325
|
mode: modeSchema,
|
|
10210
10326
|
feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
|
|
10211
10327
|
},
|
|
@@ -10213,7 +10329,7 @@ function createImageSelectionTools(controller) {
|
|
|
10213
10329
|
additionalProperties: false
|
|
10214
10330
|
},
|
|
10215
10331
|
annotations: writeAnnotations("Select a region", true),
|
|
10216
|
-
handler: ({ shape, x, y, width, height, mode, feather }) => {
|
|
10332
|
+
handler: ({ shape, x, y, width, height, points, mode, feather }) => {
|
|
10217
10333
|
const opts = { mode: parseSelectionMode(mode), feather: num2(feather, 0) };
|
|
10218
10334
|
switch (shape) {
|
|
10219
10335
|
case "all":
|
|
@@ -10235,8 +10351,16 @@ function createImageSelectionTools(controller) {
|
|
|
10235
10351
|
else controller.selectEllipse(rect, opts);
|
|
10236
10352
|
break;
|
|
10237
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
|
+
}
|
|
10238
10362
|
default:
|
|
10239
|
-
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.`);
|
|
10240
10364
|
}
|
|
10241
10365
|
return summary(controller);
|
|
10242
10366
|
}
|
|
@@ -10376,6 +10500,53 @@ function createImageSelectionTools(controller) {
|
|
|
10376
10500
|
};
|
|
10377
10501
|
return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];
|
|
10378
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
|
+
}
|
|
10379
10550
|
|
|
10380
10551
|
// src/image-editor/capture.ts
|
|
10381
10552
|
var NO_CANVAS = "Rendering is not available in this environment.";
|
|
@@ -11161,7 +11332,7 @@ function createImageLayerTools(controller) {
|
|
|
11161
11332
|
const viaCopy = {
|
|
11162
11333
|
name: "layer_via_copy",
|
|
11163
11334
|
title: "New layer from the selection",
|
|
11164
|
-
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.",
|
|
11165
11336
|
inputSchema: {
|
|
11166
11337
|
type: "object",
|
|
11167
11338
|
properties: {
|
|
@@ -14731,6 +14902,7 @@ function layerPanels(layer, options = {}) {
|
|
|
14731
14902
|
if (layer.mask) {
|
|
14732
14903
|
panels.push({ id: "mask", title: "Mask Options" });
|
|
14733
14904
|
}
|
|
14905
|
+
panels.push({ id: "selection", title: "Selection" });
|
|
14734
14906
|
return panels;
|
|
14735
14907
|
}
|
|
14736
14908
|
|
|
@@ -14967,6 +15139,7 @@ async function downloadPsd(controller, name = "image") {
|
|
|
14967
15139
|
}
|
|
14968
15140
|
|
|
14969
15141
|
// src/image-editor/layers/transform-ui.ts
|
|
15142
|
+
var TRANSFORM_ROTATE_HANDLE_OFFSET = 24;
|
|
14970
15143
|
var CORNERS = ["nw", "ne", "se", "sw"];
|
|
14971
15144
|
var SIDES = ["n", "e", "s", "w"];
|
|
14972
15145
|
var CORNER_INDEX = { nw: 0, ne: 1, se: 2, sw: 3 };
|
|
@@ -15019,9 +15192,18 @@ var CURSOR = {
|
|
|
15019
15192
|
e: "ew-resize",
|
|
15020
15193
|
w: "ew-resize",
|
|
15021
15194
|
reference: "move",
|
|
15195
|
+
rotate: "grab",
|
|
15022
15196
|
inside: "move",
|
|
15023
15197
|
outside: "alias"
|
|
15024
15198
|
};
|
|
15199
|
+
function rotateHandleAt(nw, ne, centre, offset) {
|
|
15200
|
+
const len = Math.hypot(ne.x - nw.x, ne.y - nw.y);
|
|
15201
|
+
if (!(offset > 0) || !len) return void 0;
|
|
15202
|
+
const n = { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
|
|
15203
|
+
let dir = { x: (ne.y - nw.y) / len, y: -(ne.x - nw.x) / len };
|
|
15204
|
+
if (dir.x * (centre.x - n.x) + dir.y * (centre.y - n.y) > 0) dir = { x: -dir.x, y: -dir.y };
|
|
15205
|
+
return { x: n.x + dir.x * offset, y: n.y + dir.y * offset };
|
|
15206
|
+
}
|
|
15025
15207
|
var meshIndex = (h) => h && h.startsWith("mesh:") ? Number(h.slice(5)) : -1;
|
|
15026
15208
|
var mod = (m) => Boolean(m.ctrlKey || m.metaKey);
|
|
15027
15209
|
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
@@ -15054,30 +15236,38 @@ function insidePolygon(poly, p) {
|
|
|
15054
15236
|
}
|
|
15055
15237
|
return inside;
|
|
15056
15238
|
}
|
|
15057
|
-
function quadFrameOverlay(corners, handles = true) {
|
|
15239
|
+
function quadFrameOverlay(corners, handles = true, rotateOffset = 0) {
|
|
15058
15240
|
const [nw, ne, se, sw] = corners;
|
|
15059
15241
|
const mid = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
|
|
15060
15242
|
const at = { nw, ne, se, sw, n: mid(nw, ne), e: mid(ne, se), s: mid(se, sw), w: mid(sw, nw) };
|
|
15243
|
+
const reference = mid(nw, se);
|
|
15244
|
+
const rotate = handles ? rotateHandleAt(nw, ne, reference, rotateOffset) : void 0;
|
|
15061
15245
|
return {
|
|
15062
15246
|
outline: CORNERS.map((id) => ({ ...at[id] })),
|
|
15063
15247
|
handles: handles ? [...CORNERS, ...SIDES].map((id) => ({ id, x: at[id].x, y: at[id].y })) : [],
|
|
15064
|
-
reference
|
|
15248
|
+
reference,
|
|
15249
|
+
...rotate ? { rotate } : {}
|
|
15065
15250
|
};
|
|
15066
15251
|
}
|
|
15252
|
+
function nearestHandle(overlay, p, tolerance) {
|
|
15253
|
+
let handle = null;
|
|
15254
|
+
let distance = tolerance;
|
|
15255
|
+
const points = overlay.rotate ? [...overlay.handles, { id: "rotate", ...overlay.rotate }] : overlay.handles;
|
|
15256
|
+
for (const h of points) {
|
|
15257
|
+
const d = Math.hypot(h.x - p.x, h.y - p.y);
|
|
15258
|
+
if (d <= distance) {
|
|
15259
|
+
handle = h.id;
|
|
15260
|
+
distance = d;
|
|
15261
|
+
}
|
|
15262
|
+
}
|
|
15263
|
+
return { handle, distance };
|
|
15264
|
+
}
|
|
15067
15265
|
function boxFrameOverlay(box, handles = true) {
|
|
15068
15266
|
const { x, y, width: w, height: h } = box;
|
|
15069
15267
|
return quadFrameOverlay([{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], handles);
|
|
15070
15268
|
}
|
|
15071
15269
|
function boxFrameHit(overlay, p, tolerance, band) {
|
|
15072
|
-
|
|
15073
|
-
let bestD = tolerance;
|
|
15074
|
-
for (const h of overlay.handles) {
|
|
15075
|
-
const d = Math.hypot(h.x - p.x, h.y - p.y);
|
|
15076
|
-
if (d <= bestD) {
|
|
15077
|
-
best = h.id;
|
|
15078
|
-
bestD = d;
|
|
15079
|
-
}
|
|
15080
|
-
}
|
|
15270
|
+
const best = nearestHandle(overlay, p, tolerance).handle;
|
|
15081
15271
|
if (best) return best;
|
|
15082
15272
|
if (insideQuad(overlay.outline, p)) return "inside";
|
|
15083
15273
|
const o = overlay.outline;
|
|
@@ -15246,12 +15436,15 @@ var FreeTransformTool = class {
|
|
|
15246
15436
|
return { id, x: p.x, y: p.y };
|
|
15247
15437
|
});
|
|
15248
15438
|
const out = { outline: this.corners.map((p) => ({ x: p.x, y: p.y })), handles, reference: this.referencePoint() ?? { x: 0, y: 0 } };
|
|
15439
|
+
const rotate = rotateHandleAt(handles[0], handles[1], applyHomography(h, { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 }) ?? out.reference, TRANSFORM_ROTATE_HANDLE_OFFSET / this.viewScale);
|
|
15440
|
+
if (rotate) out.rotate = rotate;
|
|
15249
15441
|
if (this.modeValue === "warp" && this.mesh) {
|
|
15250
15442
|
const m = this.mesh;
|
|
15251
15443
|
const at = (p) => applyHomography(h, p) ?? p;
|
|
15252
15444
|
const pts = m.points.map(at);
|
|
15253
15445
|
const edit = warpMeshEditPoints(m);
|
|
15254
15446
|
out.handles = [];
|
|
15447
|
+
delete out.rotate;
|
|
15255
15448
|
out.outline = warpMeshGridCurves({ ...m, points: pts }).length ? this.meshOutline(pts, m) : out.outline;
|
|
15256
15449
|
out.warp = {
|
|
15257
15450
|
curves: warpMeshGridCurves({ ...m, points: pts }),
|
|
@@ -15293,15 +15486,7 @@ var FreeTransformTool = class {
|
|
|
15293
15486
|
if (best2 >= 0) return `mesh:${best2}`;
|
|
15294
15487
|
return insidePolygon(o.outline, p) ? "inside" : "outside";
|
|
15295
15488
|
}
|
|
15296
|
-
let best =
|
|
15297
|
-
let bestD = tolerance;
|
|
15298
|
-
for (const h of o.handles) {
|
|
15299
|
-
const d = Math.hypot(h.x - p.x, h.y - p.y);
|
|
15300
|
-
if (d <= bestD) {
|
|
15301
|
-
best = h.id;
|
|
15302
|
-
bestD = d;
|
|
15303
|
-
}
|
|
15304
|
-
}
|
|
15489
|
+
let { handle: best, distance: bestD } = nearestHandle(o, p, tolerance);
|
|
15305
15490
|
if (Math.hypot(o.reference.x - p.x, o.reference.y - p.y) < bestD) best = "reference";
|
|
15306
15491
|
if (best) return best;
|
|
15307
15492
|
return insideQuad(o.outline, p) ? "inside" : "outside";
|
|
@@ -15383,7 +15568,7 @@ var FreeTransformTool = class {
|
|
|
15383
15568
|
const handle = d.handle;
|
|
15384
15569
|
const isCorner = handle in CORNER_INDEX;
|
|
15385
15570
|
const isSide = handle in SIDE_CORNERS;
|
|
15386
|
-
const rotating = handle === "outside" || this.modeValue === "rotate" && (isCorner || isSide);
|
|
15571
|
+
const rotating = handle === "outside" || handle === "rotate" || this.modeValue === "rotate" && (isCorner || isSide);
|
|
15387
15572
|
if (handle === "inside") {
|
|
15388
15573
|
if (m.shiftKey) delta = Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y };
|
|
15389
15574
|
return map((q2) => add(q2, delta));
|
|
@@ -15852,6 +16037,7 @@ export {
|
|
|
15852
16037
|
LayerCompositor,
|
|
15853
16038
|
LayerEffectError,
|
|
15854
16039
|
LayerOperationError,
|
|
16040
|
+
MARCHING_ANTS_STEP_MS,
|
|
15855
16041
|
MAX_BATCH,
|
|
15856
16042
|
MAX_BRUSH_SIZE,
|
|
15857
16043
|
MAX_CANVAS_BACKING_SIDE,
|
|
@@ -15961,6 +16147,7 @@ export {
|
|
|
15961
16147
|
TONE_RANGES,
|
|
15962
16148
|
TONE_RANGE_PRESETS,
|
|
15963
16149
|
TRANSFORM_ANCHORS,
|
|
16150
|
+
TRANSFORM_ROTATE_HANDLE_OFFSET,
|
|
15964
16151
|
TextEditSession,
|
|
15965
16152
|
TileMedium,
|
|
15966
16153
|
TilePixelSource,
|
|
@@ -16130,9 +16317,11 @@ export {
|
|
|
16130
16317
|
createImagePlaybackLocator,
|
|
16131
16318
|
createImagePsdTools,
|
|
16132
16319
|
createImageRetouchTools,
|
|
16320
|
+
createImageSegmentationToolProvider,
|
|
16133
16321
|
createImageSelectionTools,
|
|
16134
16322
|
createImageShapeTools,
|
|
16135
16323
|
createImageSmartObjectTools,
|
|
16324
|
+
createImageSubjectSelectionTool,
|
|
16136
16325
|
createImageTransformTools,
|
|
16137
16326
|
createImageTypeTools,
|
|
16138
16327
|
createMetricTextShaper,
|
|
@@ -16573,6 +16762,8 @@ export {
|
|
|
16573
16762
|
scalePaintGradient,
|
|
16574
16763
|
seamlessClone,
|
|
16575
16764
|
seedFromString,
|
|
16765
|
+
segmentToSelection,
|
|
16766
|
+
segmentsToSelection,
|
|
16576
16767
|
selectionArea,
|
|
16577
16768
|
selectionFromCoverage,
|
|
16578
16769
|
selectionFromPixels,
|
|
@@ -16630,6 +16821,7 @@ export {
|
|
|
16630
16821
|
srgbEncode,
|
|
16631
16822
|
srgbToLinear,
|
|
16632
16823
|
standardBlendMode,
|
|
16824
|
+
startMarchingAnts,
|
|
16633
16825
|
stepSlotTs,
|
|
16634
16826
|
strokeGrainCatch,
|
|
16635
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[];
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* proportional; side: scale one axis;
|
|
11
11
|
* - Mod (Ctrl / Cmd) + corner: distort that corner; Mod + side: skew along the side (Alt: about
|
|
12
12
|
* the centre); Mod + Alt + Shift + corner: perspective;
|
|
13
|
-
* -
|
|
14
|
-
* along one axis); the reference
|
|
13
|
+
* - the rotate handle on its stem above the top edge, or outside the box: rotate about the
|
|
14
|
+
* reference point (Shift: 15° steps); inside: move (Shift: along one axis); the reference
|
|
15
|
+
* point: move it;
|
|
15
16
|
* - the mode switch (rotate, skew, distort, perspective) makes every handle do that;
|
|
16
17
|
* - Warp: drag a mesh anchor or handle (the grid stays smooth across its lines), or anywhere
|
|
17
18
|
* inside to pull the surface; presets and the grid size come from the options bar. The warp is
|
|
@@ -26,7 +27,9 @@ import type { Interpolation } from '../../raster/transform.cjs';
|
|
|
26
27
|
import type { CropBox, ImagePoint } from '../types.cjs';
|
|
27
28
|
import type { ShortcutEvent } from './paint-ui.cjs';
|
|
28
29
|
/** What a pointer is over (`mesh:<index>`: a warp mesh control point). */
|
|
29
|
-
export type TransformHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'reference' | 'inside' | 'outside' | `mesh:${number}`;
|
|
30
|
+
export type TransformHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'rotate' | 'reference' | 'inside' | 'outside' | `mesh:${number}`;
|
|
31
|
+
/** Screen pixels from the top edge's midpoint to the rotate handle (as the vector editor's). */
|
|
32
|
+
export declare const TRANSFORM_ROTATE_HANDLE_OFFSET = 24;
|
|
30
33
|
export interface TransformModifiers {
|
|
31
34
|
shiftKey?: boolean;
|
|
32
35
|
altKey?: boolean;
|
|
@@ -115,6 +118,8 @@ export interface TransformOverlay {
|
|
|
115
118
|
y: number;
|
|
116
119
|
}>;
|
|
117
120
|
reference: ImagePoint;
|
|
121
|
+
/** The rotate handle, on a stem out from the top edge's midpoint (none without handles or in Warp mode). */
|
|
122
|
+
rotate?: ImagePoint;
|
|
118
123
|
/** In Warp mode: the mesh's grid lines as cubics, its anchors, and its handles with the anchor each belongs to. */
|
|
119
124
|
warp?: {
|
|
120
125
|
curves: ImagePoint[][];
|
|
@@ -135,14 +140,17 @@ export interface FreeTransformToolOptions {
|
|
|
135
140
|
* A Free Transform frame with no transform open (a host's selected-layer frame) from where its
|
|
136
141
|
* corners lie (top-left, top-right, bottom-right, bottom-left, as `getLayerFrame` gives them): the
|
|
137
142
|
* outline, the eight handles (none with `handles = false`, e.g. for a layer locked in place) and the
|
|
138
|
-
* centre as the reference point.
|
|
143
|
+
* centre as the reference point. With handles and a `rotateOffset` (document pixels, usually
|
|
144
|
+
* `TRANSFORM_ROTATE_HANDLE_OFFSET / scale`) it also has the rotate handle. Same shape and handle
|
|
145
|
+
* order as `FreeTransformTool.overlay`.
|
|
139
146
|
*/
|
|
140
|
-
export declare function quadFrameOverlay(corners: readonly ImagePoint[], handles?: boolean): TransformOverlay;
|
|
147
|
+
export declare function quadFrameOverlay(corners: readonly ImagePoint[], handles?: boolean, rotateOffset?: number): TransformOverlay;
|
|
141
148
|
/** `quadFrameOverlay` for an axis-aligned box. */
|
|
142
149
|
export declare function boxFrameOverlay(box: CropBox, handles?: boolean): TransformOverlay;
|
|
143
150
|
/**
|
|
144
|
-
* What a document point is over on a frame from `quadFrameOverlay`: a handle
|
|
145
|
-
* `inside`, `outside` within `band` of the outline (where a drag rotates), or
|
|
151
|
+
* What a document point is over on a frame from `quadFrameOverlay`: a handle or the rotate handle
|
|
152
|
+
* (within `tolerance`), `inside`, `outside` within `band` of the outline (where a drag rotates), or
|
|
153
|
+
* `null` beyond that.
|
|
146
154
|
*/
|
|
147
155
|
export declare function boxFrameHit(overlay: TransformOverlay, p: ImagePoint, tolerance: number, band: number): TransformHandle | null;
|
|
148
156
|
/**
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* proportional; side: scale one axis;
|
|
11
11
|
* - Mod (Ctrl / Cmd) + corner: distort that corner; Mod + side: skew along the side (Alt: about
|
|
12
12
|
* the centre); Mod + Alt + Shift + corner: perspective;
|
|
13
|
-
* -
|
|
14
|
-
* along one axis); the reference
|
|
13
|
+
* - the rotate handle on its stem above the top edge, or outside the box: rotate about the
|
|
14
|
+
* reference point (Shift: 15° steps); inside: move (Shift: along one axis); the reference
|
|
15
|
+
* point: move it;
|
|
15
16
|
* - the mode switch (rotate, skew, distort, perspective) makes every handle do that;
|
|
16
17
|
* - Warp: drag a mesh anchor or handle (the grid stays smooth across its lines), or anywhere
|
|
17
18
|
* inside to pull the surface; presets and the grid size come from the options bar. The warp is
|
|
@@ -26,7 +27,9 @@ import type { Interpolation } from '../../raster/transform.js';
|
|
|
26
27
|
import type { CropBox, ImagePoint } from '../types.js';
|
|
27
28
|
import type { ShortcutEvent } from './paint-ui.js';
|
|
28
29
|
/** What a pointer is over (`mesh:<index>`: a warp mesh control point). */
|
|
29
|
-
export type TransformHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'reference' | 'inside' | 'outside' | `mesh:${number}`;
|
|
30
|
+
export type TransformHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'rotate' | 'reference' | 'inside' | 'outside' | `mesh:${number}`;
|
|
31
|
+
/** Screen pixels from the top edge's midpoint to the rotate handle (as the vector editor's). */
|
|
32
|
+
export declare const TRANSFORM_ROTATE_HANDLE_OFFSET = 24;
|
|
30
33
|
export interface TransformModifiers {
|
|
31
34
|
shiftKey?: boolean;
|
|
32
35
|
altKey?: boolean;
|
|
@@ -115,6 +118,8 @@ export interface TransformOverlay {
|
|
|
115
118
|
y: number;
|
|
116
119
|
}>;
|
|
117
120
|
reference: ImagePoint;
|
|
121
|
+
/** The rotate handle, on a stem out from the top edge's midpoint (none without handles or in Warp mode). */
|
|
122
|
+
rotate?: ImagePoint;
|
|
118
123
|
/** In Warp mode: the mesh's grid lines as cubics, its anchors, and its handles with the anchor each belongs to. */
|
|
119
124
|
warp?: {
|
|
120
125
|
curves: ImagePoint[][];
|
|
@@ -135,14 +140,17 @@ export interface FreeTransformToolOptions {
|
|
|
135
140
|
* A Free Transform frame with no transform open (a host's selected-layer frame) from where its
|
|
136
141
|
* corners lie (top-left, top-right, bottom-right, bottom-left, as `getLayerFrame` gives them): the
|
|
137
142
|
* outline, the eight handles (none with `handles = false`, e.g. for a layer locked in place) and the
|
|
138
|
-
* centre as the reference point.
|
|
143
|
+
* centre as the reference point. With handles and a `rotateOffset` (document pixels, usually
|
|
144
|
+
* `TRANSFORM_ROTATE_HANDLE_OFFSET / scale`) it also has the rotate handle. Same shape and handle
|
|
145
|
+
* order as `FreeTransformTool.overlay`.
|
|
139
146
|
*/
|
|
140
|
-
export declare function quadFrameOverlay(corners: readonly ImagePoint[], handles?: boolean): TransformOverlay;
|
|
147
|
+
export declare function quadFrameOverlay(corners: readonly ImagePoint[], handles?: boolean, rotateOffset?: number): TransformOverlay;
|
|
141
148
|
/** `quadFrameOverlay` for an axis-aligned box. */
|
|
142
149
|
export declare function boxFrameOverlay(box: CropBox, handles?: boolean): TransformOverlay;
|
|
143
150
|
/**
|
|
144
|
-
* What a document point is over on a frame from `quadFrameOverlay`: a handle
|
|
145
|
-
* `inside`, `outside` within `band` of the outline (where a drag rotates), or
|
|
151
|
+
* What a document point is over on a frame from `quadFrameOverlay`: a handle or the rotate handle
|
|
152
|
+
* (within `tolerance`), `inside`, `outside` within `band` of the outline (where a drag rotates), or
|
|
153
|
+
* `null` beyond that.
|
|
146
154
|
*/
|
|
147
155
|
export declare function boxFrameHit(overlay: TransformOverlay, p: ImagePoint, tolerance: number, band: number): TransformHandle | null;
|
|
148
156
|
/**
|