@pixerate/editor 0.206.0 → 0.207.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/index.cjs +487 -84
- package/dist/image-editor/index.js +409 -6
- 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/refine-edits.d.cts +13 -4
- package/dist/image-editor/selection/refine-edits.d.ts +13 -4
- package/dist/image-editor/selection/select-and-mask.d.cts +126 -0
- package/dist/image-editor/selection/select-and-mask.d.ts +126 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-image-selection-masking/SKILL.md +4 -2
|
@@ -5320,7 +5320,7 @@ function segmentsToSelection(width, height, results) {
|
|
|
5320
5320
|
}
|
|
5321
5321
|
|
|
5322
5322
|
// src/image-editor/selection/refine-edits.ts
|
|
5323
|
-
var REFINE_EDIT_KINDS = ["quick-select", "brush", "refine-brush"];
|
|
5323
|
+
var REFINE_EDIT_KINDS = ["quick-select", "brush", "refine-brush", "lasso", "object"];
|
|
5324
5324
|
function brushStrokeMask(width, height, points, size, hardness = 1) {
|
|
5325
5325
|
const r = Math.max(0.5, size / 2);
|
|
5326
5326
|
const solid = r * Math.min(1, Math.max(0, hardness));
|
|
@@ -5361,7 +5361,19 @@ function applyRefineEdits(pixels, selection, edits, width, height) {
|
|
|
5361
5361
|
let sel = selection?.bounds ? selection : null;
|
|
5362
5362
|
let band = null;
|
|
5363
5363
|
for (const e of edits) {
|
|
5364
|
+
if (e.kind === "object") {
|
|
5365
|
+
if (!pixels) throw new Error("Object Selection needs the image pixels.");
|
|
5366
|
+
if (!e.rect === !e.point) throw new Error("An object edit takes rect (a box around the object) or point (on it), not both.");
|
|
5367
|
+
const region2 = selectObjectIn(pixels, e.rect ? { rect: e.rect } : { point: e.point });
|
|
5368
|
+
sel = combineSelections(sel, region2, e.mode ?? "add");
|
|
5369
|
+
continue;
|
|
5370
|
+
}
|
|
5364
5371
|
if (!e.points.length) throw new Error(`A ${e.kind} stroke needs at least one point.`);
|
|
5372
|
+
if (e.kind === "lasso") {
|
|
5373
|
+
if (e.points.length < 3) throw new Error("A lasso edit needs at least 3 points.");
|
|
5374
|
+
sel = combineSelections(sel, polygonSelection(width, height, e.points, {}), e.mode ?? "add");
|
|
5375
|
+
continue;
|
|
5376
|
+
}
|
|
5365
5377
|
if (e.kind === "refine-brush") {
|
|
5366
5378
|
band = combineSelections(band, brushStrokeMask(width, height, e.points, e.size ?? 40), "add");
|
|
5367
5379
|
continue;
|
|
@@ -13709,13 +13721,25 @@ function createImageSelectionTools(controller) {
|
|
|
13709
13721
|
items: {
|
|
13710
13722
|
type: "object",
|
|
13711
13723
|
properties: {
|
|
13712
|
-
kind: {
|
|
13713
|
-
|
|
13724
|
+
kind: {
|
|
13725
|
+
type: "string",
|
|
13726
|
+
enum: ["quick-select", "brush", "refine-brush", "lasso", "object"],
|
|
13727
|
+
description: "quick-select grows to edges; brush paints directly; refine-brush marks soft detail (hair) for the edge to be re-estimated; lasso adds or removes a polygon; object adds or removes the object in rect or under point."
|
|
13728
|
+
},
|
|
13729
|
+
points: { type: "array", minItems: 1, items: pointSchema, description: "The stroke path (or lasso outline, 3+ points) in natural image pixels; one point is a dab. Not for object." },
|
|
13730
|
+
rect: {
|
|
13731
|
+
type: "object",
|
|
13732
|
+
properties: rectProperties,
|
|
13733
|
+
required: ["x", "y", "width", "height"],
|
|
13734
|
+
additionalProperties: false,
|
|
13735
|
+
description: "object: a box around the object, in natural image pixels."
|
|
13736
|
+
},
|
|
13737
|
+
point: { ...pointSchema, description: "object: a point on the object, in natural image pixels." },
|
|
13714
13738
|
size: { type: "number", minimum: 1, maximum: 2500, description: "Brush diameter in natural image pixels (default 30 quick-select, 20 brush, 40 refine-brush)." },
|
|
13715
|
-
mode: { type: "string", enum: ["add", "subtract"], description: "quick-select and
|
|
13739
|
+
mode: { type: "string", enum: ["add", "subtract"], description: "quick-select, brush, lasso and object: add (default) or subtract." },
|
|
13716
13740
|
hardness: { type: "number", minimum: 0, maximum: 1, description: "brush: the solid share of the radius, 0..1 (default 1)." }
|
|
13717
13741
|
},
|
|
13718
|
-
required: ["kind"
|
|
13742
|
+
required: ["kind"],
|
|
13719
13743
|
additionalProperties: false
|
|
13720
13744
|
}
|
|
13721
13745
|
},
|
|
@@ -13725,8 +13749,13 @@ function createImageSelectionTools(controller) {
|
|
|
13725
13749
|
},
|
|
13726
13750
|
annotations: writeAnnotations("Refine the selection edge"),
|
|
13727
13751
|
handler: async (params) => {
|
|
13728
|
-
const edits = Array.isArray(params.edits) ? params.edits.map((e) =>
|
|
13752
|
+
const edits = Array.isArray(params.edits) ? params.edits.map((e) => e.kind === "object" ? { ...e } : { ...e, points: (e.points ?? []).map((p) => ({ x: num2(p?.x, NaN), y: num2(p?.y, NaN) })) }) : [];
|
|
13729
13753
|
for (const e of edits) {
|
|
13754
|
+
if (e.kind === "object") {
|
|
13755
|
+
if (!e.rect === !e.point) throw new Error("An object edit takes rect (a box around the object) or point (on it).");
|
|
13756
|
+
continue;
|
|
13757
|
+
}
|
|
13758
|
+
if (!e.points.length) throw new Error(`A ${e.kind} edit needs points.`);
|
|
13730
13759
|
if (!e.points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) throw new Error("Each edit needs points of { x, y } in natural image pixels.");
|
|
13731
13760
|
if (e.kind === "refine-brush" && e.mode !== void 0) throw new Error("refine-brush strokes have no mode; they mark where the edge is re-estimated.");
|
|
13732
13761
|
}
|
|
@@ -19127,6 +19156,374 @@ function downloadMaskImage(controller, source, name = "mask") {
|
|
|
19127
19156
|
return { fileName, width, height };
|
|
19128
19157
|
}
|
|
19129
19158
|
|
|
19159
|
+
// src/image-editor/selection/select-and-mask.ts
|
|
19160
|
+
var SELECT_AND_MASK_VIEWS = [
|
|
19161
|
+
{ id: "onion-skin", label: "Onion Skin", key: "o", opacity: 0.5 },
|
|
19162
|
+
{ id: "marching-ants", label: "Marching Ants", key: "m", opacity: 1 },
|
|
19163
|
+
{ id: "overlay", label: "Overlay", key: "v", opacity: 0.5 },
|
|
19164
|
+
{ id: "on-black", label: "On Black", key: "a", opacity: 1 },
|
|
19165
|
+
{ id: "on-white", label: "On White", key: "t", opacity: 1 },
|
|
19166
|
+
{ id: "black-white", label: "Black & White", key: "k", opacity: 1 },
|
|
19167
|
+
{ id: "on-layers", label: "On Layers", key: "y", opacity: 1 }
|
|
19168
|
+
];
|
|
19169
|
+
var SELECT_AND_MASK_TOOLS = [
|
|
19170
|
+
{ id: "quick-select", label: "Quick Selection", key: "w" },
|
|
19171
|
+
{ id: "refine-brush", label: "Refine Edge Brush", key: "r" },
|
|
19172
|
+
{ id: "brush", label: "Brush", key: "b" },
|
|
19173
|
+
{ id: "object", label: "Object Selection", key: "q" },
|
|
19174
|
+
{ id: "lasso", label: "Lasso", key: "l" },
|
|
19175
|
+
{ id: "hand", label: "Hand", key: "h" },
|
|
19176
|
+
{ id: "zoom", label: "Zoom", key: "z" }
|
|
19177
|
+
];
|
|
19178
|
+
var DEFAULT_SELECT_AND_MASK_SETTINGS = {
|
|
19179
|
+
params: { ...DEFAULT_REFINE_EDGE_PARAMS },
|
|
19180
|
+
output: "selection",
|
|
19181
|
+
decontaminate: 0,
|
|
19182
|
+
refineMode: "color",
|
|
19183
|
+
view: "onion-skin",
|
|
19184
|
+
opacity: 0.5,
|
|
19185
|
+
showEdge: false,
|
|
19186
|
+
showOriginal: false,
|
|
19187
|
+
realTime: true,
|
|
19188
|
+
highQuality: false,
|
|
19189
|
+
remember: false,
|
|
19190
|
+
tool: "quick-select",
|
|
19191
|
+
brushSize: 30,
|
|
19192
|
+
brushMode: "add"
|
|
19193
|
+
};
|
|
19194
|
+
var PREVIEW_SIZE = 1024;
|
|
19195
|
+
var remembered = null;
|
|
19196
|
+
function scalePixels(pixels, w, h) {
|
|
19197
|
+
if (w === pixels.width && h === pixels.height) return pixels;
|
|
19198
|
+
const { width: W, height: H, data } = pixels;
|
|
19199
|
+
const sum = new Float64Array(w * h * 4);
|
|
19200
|
+
const count = new Float64Array(w * h);
|
|
19201
|
+
for (let y = 0; y < H; y++) {
|
|
19202
|
+
const row = Math.min(h - 1, Math.floor(y * h / H)) * w;
|
|
19203
|
+
for (let x = 0; x < W; x++) {
|
|
19204
|
+
const o = row + Math.min(w - 1, Math.floor(x * w / W));
|
|
19205
|
+
const p = (y * W + x) * 4;
|
|
19206
|
+
for (let c = 0; c < 4; c++) sum[o * 4 + c] += data[p + c];
|
|
19207
|
+
count[o]++;
|
|
19208
|
+
}
|
|
19209
|
+
}
|
|
19210
|
+
const out = new Uint8ClampedArray(w * h * 4);
|
|
19211
|
+
for (let i = 0; i < w * h; i++) for (let c = 0; c < 4; c++) out[i * 4 + c] = sum[i * 4 + c] / (count[i] || 1);
|
|
19212
|
+
return { width: w, height: h, data: out };
|
|
19213
|
+
}
|
|
19214
|
+
function scaleEdit(edit, s) {
|
|
19215
|
+
const pt = (p) => ({ x: p.x * s, y: p.y * s });
|
|
19216
|
+
if (edit.kind === "object") {
|
|
19217
|
+
return { ...edit, rect: edit.rect && { x: edit.rect.x * s, y: edit.rect.y * s, width: edit.rect.width * s, height: edit.rect.height * s }, point: edit.point && pt(edit.point) };
|
|
19218
|
+
}
|
|
19219
|
+
const scaled = { ...edit, points: edit.points.map(pt) };
|
|
19220
|
+
if ("size" in edit && edit.size !== void 0) scaled.size = Math.max(1, edit.size * s);
|
|
19221
|
+
return scaled;
|
|
19222
|
+
}
|
|
19223
|
+
function scaleParams(p, s) {
|
|
19224
|
+
return { ...p, radius: p.radius * s, feather: p.feather * s };
|
|
19225
|
+
}
|
|
19226
|
+
var SelectAndMaskSession = class {
|
|
19227
|
+
constructor(controller) {
|
|
19228
|
+
this.controller = controller;
|
|
19229
|
+
this.base = controller.getSelection();
|
|
19230
|
+
this.settingsValue = remembered ? { ...remembered, params: { ...remembered.params }, showOriginal: false } : { ...DEFAULT_SELECT_AND_MASK_SETTINGS, params: { ...DEFAULT_SELECT_AND_MASK_SETTINGS.params } };
|
|
19231
|
+
}
|
|
19232
|
+
controller;
|
|
19233
|
+
settingsValue;
|
|
19234
|
+
editsList = [];
|
|
19235
|
+
/** The edit being painted, shown in the preview but not yet in `edits`. */
|
|
19236
|
+
live = null;
|
|
19237
|
+
dragStart = null;
|
|
19238
|
+
pixels = null;
|
|
19239
|
+
previewPixels = null;
|
|
19240
|
+
base;
|
|
19241
|
+
previewMask = null;
|
|
19242
|
+
baseMask = null;
|
|
19243
|
+
listeners = /* @__PURE__ */ new Set();
|
|
19244
|
+
/** Why the preview could not be made (no pixels), for the UI. */
|
|
19245
|
+
error = null;
|
|
19246
|
+
/** A note for the UI, such as object aware refinement applying only on OK. */
|
|
19247
|
+
note = null;
|
|
19248
|
+
/** Bumped each time the preview changes, so a UI can redraw. */
|
|
19249
|
+
revision = 0;
|
|
19250
|
+
get settings() {
|
|
19251
|
+
return this.settingsValue;
|
|
19252
|
+
}
|
|
19253
|
+
get edits() {
|
|
19254
|
+
return this.editsList;
|
|
19255
|
+
}
|
|
19256
|
+
/** Reads the image and makes the first preview. Resolves to false when the pixels are unavailable. */
|
|
19257
|
+
async open() {
|
|
19258
|
+
try {
|
|
19259
|
+
this.pixels = await this.controller.getSamplePixels({});
|
|
19260
|
+
} catch (err) {
|
|
19261
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
19262
|
+
}
|
|
19263
|
+
if (!this.pixels) {
|
|
19264
|
+
this.error ??= "The image pixels are not available, so Select and Mask cannot preview.";
|
|
19265
|
+
this.emit();
|
|
19266
|
+
return false;
|
|
19267
|
+
}
|
|
19268
|
+
this.rescale();
|
|
19269
|
+
this.recompute();
|
|
19270
|
+
return true;
|
|
19271
|
+
}
|
|
19272
|
+
subscribe(listener) {
|
|
19273
|
+
this.listeners.add(listener);
|
|
19274
|
+
return () => this.listeners.delete(listener);
|
|
19275
|
+
}
|
|
19276
|
+
emit() {
|
|
19277
|
+
this.revision++;
|
|
19278
|
+
for (const l of this.listeners) l();
|
|
19279
|
+
}
|
|
19280
|
+
/** Preview size and the scale from natural pixels to it. */
|
|
19281
|
+
get previewSize() {
|
|
19282
|
+
const p = this.previewPixels;
|
|
19283
|
+
if (!p || !this.pixels) return { width: 0, height: 0, scale: 1 };
|
|
19284
|
+
return { width: p.width, height: p.height, scale: p.width / this.pixels.width };
|
|
19285
|
+
}
|
|
19286
|
+
/** Changes settings; the preview follows. Choosing a view also sets its usual opacity. */
|
|
19287
|
+
update(changes) {
|
|
19288
|
+
const next = { ...this.settingsValue, ...changes, params: { ...this.settingsValue.params, ...changes.params ?? {} } };
|
|
19289
|
+
if (changes.view && changes.view !== this.settingsValue.view && changes.opacity === void 0) {
|
|
19290
|
+
next.opacity = SELECT_AND_MASK_VIEWS.find((v) => v.id === changes.view).opacity;
|
|
19291
|
+
}
|
|
19292
|
+
const rescale = next.highQuality !== this.settingsValue.highQuality;
|
|
19293
|
+
this.settingsValue = next;
|
|
19294
|
+
if (rescale) this.rescale();
|
|
19295
|
+
this.recompute();
|
|
19296
|
+
}
|
|
19297
|
+
rescale() {
|
|
19298
|
+
const px2 = this.pixels;
|
|
19299
|
+
if (!px2) return;
|
|
19300
|
+
const s = this.settingsValue.highQuality ? 1 : Math.min(1, PREVIEW_SIZE / Math.max(px2.width, px2.height));
|
|
19301
|
+
const w = Math.max(1, Math.round(px2.width * s));
|
|
19302
|
+
const h = Math.max(1, Math.round(px2.height * s));
|
|
19303
|
+
this.previewPixels = scalePixels(px2, w, h);
|
|
19304
|
+
this.baseMask = this.base?.bounds ? s === 1 ? this.base : resizeSelection(this.base, w, h) : null;
|
|
19305
|
+
}
|
|
19306
|
+
/** Recomputes the preview mask from the base selection, the strokes and the settings. */
|
|
19307
|
+
recompute() {
|
|
19308
|
+
const px2 = this.previewPixels;
|
|
19309
|
+
if (!px2) return;
|
|
19310
|
+
const { scale } = this.previewSize;
|
|
19311
|
+
const edits = [...this.editsList, ...this.live ? [this.live] : []].map((e) => scaleEdit(e, scale));
|
|
19312
|
+
this.note = this.settingsValue.refineMode === "object" ? "Object aware refinement asks the segmentation model on OK; the preview refines by colour." : null;
|
|
19313
|
+
try {
|
|
19314
|
+
const { selection, refineBrush } = applyRefineEdits(px2, this.baseMask, edits, px2.width, px2.height);
|
|
19315
|
+
this.previewMask = selection?.bounds ? refineSelection(px2, selection, scaleParams(this.settingsValue.params, scale), refineBrush) : null;
|
|
19316
|
+
this.error = null;
|
|
19317
|
+
} catch (err) {
|
|
19318
|
+
this.previewMask = null;
|
|
19319
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
19320
|
+
}
|
|
19321
|
+
this.emit();
|
|
19322
|
+
}
|
|
19323
|
+
/** Coverage 0..1 of the preview at (x, y) in preview pixels (for tests and hover readouts). */
|
|
19324
|
+
coverageAt(x, y) {
|
|
19325
|
+
const m = this.settingsValue.showOriginal ? this.baseMask : this.previewMask;
|
|
19326
|
+
if (!m) return 0;
|
|
19327
|
+
const xi = Math.floor(x);
|
|
19328
|
+
const yi = Math.floor(y);
|
|
19329
|
+
if (xi < 0 || yi < 0 || xi >= m.width || yi >= m.height) return 0;
|
|
19330
|
+
return m.data[yi * m.width + xi] / 255;
|
|
19331
|
+
}
|
|
19332
|
+
/** The preview drawn in the current view mode: RGBA at {@link previewSize}. */
|
|
19333
|
+
render() {
|
|
19334
|
+
const px2 = this.previewPixels;
|
|
19335
|
+
if (!px2) return new Uint8ClampedArray(0);
|
|
19336
|
+
const { width: w, height: h } = px2;
|
|
19337
|
+
const n = w * h;
|
|
19338
|
+
const s = this.settingsValue;
|
|
19339
|
+
const mask = (s.showOriginal ? this.baseMask : this.previewMask) ?? createSelectionMask(w, h);
|
|
19340
|
+
const m = mask.data;
|
|
19341
|
+
const out = new Uint8ClampedArray(n * 4);
|
|
19342
|
+
const k = Math.min(1, Math.max(0, s.opacity));
|
|
19343
|
+
let edgeBand = null;
|
|
19344
|
+
if (s.showEdge) {
|
|
19345
|
+
edgeBand = new Uint8Array(n);
|
|
19346
|
+
const r = Math.max(1, Math.round(Math.max(s.params.radius, 2) * this.previewSize.scale));
|
|
19347
|
+
for (let y = 0; y < h; y++) {
|
|
19348
|
+
for (let x = 0; x < w; x++) {
|
|
19349
|
+
const v = m[y * w + x];
|
|
19350
|
+
if (v <= 3 || v >= 252) continue;
|
|
19351
|
+
for (let j = Math.max(0, y - r); j <= Math.min(h - 1, y + r); j++) edgeBand.fill(1, j * w + Math.max(0, x - r), j * w + Math.min(w, x + r + 1));
|
|
19352
|
+
}
|
|
19353
|
+
}
|
|
19354
|
+
}
|
|
19355
|
+
for (let i = 0; i < n; i++) {
|
|
19356
|
+
const a = m[i] / 255;
|
|
19357
|
+
const p = i * 4;
|
|
19358
|
+
const r = px2.data[p];
|
|
19359
|
+
const g = px2.data[p + 1];
|
|
19360
|
+
const b = px2.data[p + 2];
|
|
19361
|
+
let o;
|
|
19362
|
+
const x = i % w;
|
|
19363
|
+
const y = (i - x) / w;
|
|
19364
|
+
const checker = ((x >> 3) + (y >> 3)) % 2 ? 204 : 255;
|
|
19365
|
+
const hide = (1 - a) * k;
|
|
19366
|
+
switch (s.view) {
|
|
19367
|
+
case "black-white":
|
|
19368
|
+
o = [m[i], m[i], m[i]];
|
|
19369
|
+
break;
|
|
19370
|
+
case "overlay":
|
|
19371
|
+
o = [r + (255 - r) * hide, g * (1 - hide), b * (1 - hide)];
|
|
19372
|
+
break;
|
|
19373
|
+
case "on-black":
|
|
19374
|
+
o = [r * (1 - hide), g * (1 - hide), b * (1 - hide)];
|
|
19375
|
+
break;
|
|
19376
|
+
case "on-white":
|
|
19377
|
+
o = [r + (255 - r) * hide, g + (255 - g) * hide, b + (255 - b) * hide];
|
|
19378
|
+
break;
|
|
19379
|
+
case "marching-ants": {
|
|
19380
|
+
o = [r, g, b];
|
|
19381
|
+
const inside = m[i] >= 128;
|
|
19382
|
+
const edge = x > 0 && m[i - 1] >= 128 !== inside || x + 1 < w && m[i + 1] >= 128 !== inside || y > 0 && m[i - w] >= 128 !== inside || y + 1 < h && m[i + w] >= 128 !== inside;
|
|
19383
|
+
if (edge) {
|
|
19384
|
+
const v = (x + y >> 2) % 2 ? 0 : 255;
|
|
19385
|
+
o = [v, v, v];
|
|
19386
|
+
}
|
|
19387
|
+
break;
|
|
19388
|
+
}
|
|
19389
|
+
default:
|
|
19390
|
+
o = [r + (checker - r) * hide, g + (checker - g) * hide, b + (checker - b) * hide];
|
|
19391
|
+
}
|
|
19392
|
+
if (edgeBand && !edgeBand[i]) o = [o[0] * 0.15, o[1] * 0.15, o[2] * 0.15];
|
|
19393
|
+
out[p] = o[0];
|
|
19394
|
+
out[p + 1] = o[1];
|
|
19395
|
+
out[p + 2] = o[2];
|
|
19396
|
+
out[p + 3] = 255;
|
|
19397
|
+
}
|
|
19398
|
+
return out;
|
|
19399
|
+
}
|
|
19400
|
+
/**
|
|
19401
|
+
* Pointer down at `point` (natural pixels) with the current tool: starts a stroke (Quick
|
|
19402
|
+
* Selection, brushes, lasso) or a box (Object Selection). Alt (or the subtract mode) removes.
|
|
19403
|
+
* Hand and Zoom are the UI's own; they return false here.
|
|
19404
|
+
*/
|
|
19405
|
+
begin(point, modifiers = {}) {
|
|
19406
|
+
const s = this.settingsValue;
|
|
19407
|
+
const mode = modifiers.altKey ? s.brushMode === "add" ? "subtract" : "add" : s.brushMode;
|
|
19408
|
+
this.dragStart = point;
|
|
19409
|
+
switch (s.tool) {
|
|
19410
|
+
case "quick-select":
|
|
19411
|
+
this.live = { kind: "quick-select", points: [point], size: s.brushSize, mode };
|
|
19412
|
+
break;
|
|
19413
|
+
case "brush":
|
|
19414
|
+
this.live = { kind: "brush", points: [point], size: s.brushSize, mode };
|
|
19415
|
+
break;
|
|
19416
|
+
case "refine-brush":
|
|
19417
|
+
this.live = { kind: "refine-brush", points: [point], size: s.brushSize };
|
|
19418
|
+
break;
|
|
19419
|
+
case "lasso":
|
|
19420
|
+
this.live = { kind: "lasso", points: [point], mode };
|
|
19421
|
+
break;
|
|
19422
|
+
case "object":
|
|
19423
|
+
this.live = { kind: "object", point, mode };
|
|
19424
|
+
break;
|
|
19425
|
+
default:
|
|
19426
|
+
this.dragStart = null;
|
|
19427
|
+
return false;
|
|
19428
|
+
}
|
|
19429
|
+
if (s.realTime && s.tool !== "lasso" && s.tool !== "object") this.recompute();
|
|
19430
|
+
return true;
|
|
19431
|
+
}
|
|
19432
|
+
/** Pointer move while a stroke is down. */
|
|
19433
|
+
move(point) {
|
|
19434
|
+
const live = this.live;
|
|
19435
|
+
if (!live || !this.dragStart) return;
|
|
19436
|
+
if (live.kind === "object") {
|
|
19437
|
+
const a = this.dragStart;
|
|
19438
|
+
const rect = { x: Math.min(a.x, point.x), y: Math.min(a.y, point.y), width: Math.abs(point.x - a.x), height: Math.abs(point.y - a.y) };
|
|
19439
|
+
this.live = rect.width >= 4 && rect.height >= 4 ? { kind: "object", rect, mode: live.mode } : { kind: "object", point: a, mode: live.mode };
|
|
19440
|
+
return;
|
|
19441
|
+
}
|
|
19442
|
+
const last = live.points[live.points.length - 1];
|
|
19443
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) < 1) return;
|
|
19444
|
+
live.points.push(point);
|
|
19445
|
+
if (this.settingsValue.realTime && live.kind !== "lasso") this.recompute();
|
|
19446
|
+
}
|
|
19447
|
+
/** Pointer up: the stroke becomes an edit (one step of the workspace's own undo). */
|
|
19448
|
+
end() {
|
|
19449
|
+
const live = this.live;
|
|
19450
|
+
this.live = null;
|
|
19451
|
+
this.dragStart = null;
|
|
19452
|
+
if (!live) return;
|
|
19453
|
+
if (live.kind === "lasso" && live.points.length < 3) return;
|
|
19454
|
+
this.editsList.push(live);
|
|
19455
|
+
this.recompute();
|
|
19456
|
+
}
|
|
19457
|
+
/** Removes the last stroke (Ctrl/⌘+Z in the workspace). Returns false when there is none. */
|
|
19458
|
+
undoEdit() {
|
|
19459
|
+
if (!this.editsList.length) return false;
|
|
19460
|
+
this.editsList.pop();
|
|
19461
|
+
this.recompute();
|
|
19462
|
+
return true;
|
|
19463
|
+
}
|
|
19464
|
+
/** Clears the strokes and goes back to the default settings (Reset). */
|
|
19465
|
+
reset() {
|
|
19466
|
+
this.editsList.length = 0;
|
|
19467
|
+
this.settingsValue = { ...DEFAULT_SELECT_AND_MASK_SETTINGS, params: { ...DEFAULT_SELECT_AND_MASK_SETTINGS.params }, tool: this.settingsValue.tool };
|
|
19468
|
+
this.rescale();
|
|
19469
|
+
this.recompute();
|
|
19470
|
+
}
|
|
19471
|
+
/**
|
|
19472
|
+
* Photoshop's keys in the workspace: a view letter, F (next view), X (all views off and on:
|
|
19473
|
+
* Show Original), J (Show Edge), P (Show Original), a tool letter, [ and ] (brush size).
|
|
19474
|
+
* Returns true when the key was used.
|
|
19475
|
+
*/
|
|
19476
|
+
handleKey(key) {
|
|
19477
|
+
const k = key.toLowerCase();
|
|
19478
|
+
const view = SELECT_AND_MASK_VIEWS.find((v) => v.key === k);
|
|
19479
|
+
if (view) {
|
|
19480
|
+
this.update({ view: view.id });
|
|
19481
|
+
return true;
|
|
19482
|
+
}
|
|
19483
|
+
const tool = SELECT_AND_MASK_TOOLS.find((t) => t.key === k);
|
|
19484
|
+
if (tool) {
|
|
19485
|
+
this.update({ tool: tool.id });
|
|
19486
|
+
return true;
|
|
19487
|
+
}
|
|
19488
|
+
if (k === "f") {
|
|
19489
|
+
const i = SELECT_AND_MASK_VIEWS.findIndex((v) => v.id === this.settingsValue.view);
|
|
19490
|
+
this.update({ view: SELECT_AND_MASK_VIEWS[(i + 1) % SELECT_AND_MASK_VIEWS.length].id });
|
|
19491
|
+
return true;
|
|
19492
|
+
}
|
|
19493
|
+
if (k === "p" || k === "x") {
|
|
19494
|
+
this.update({ showOriginal: !this.settingsValue.showOriginal });
|
|
19495
|
+
return true;
|
|
19496
|
+
}
|
|
19497
|
+
if (k === "j") {
|
|
19498
|
+
this.update({ showEdge: !this.settingsValue.showEdge });
|
|
19499
|
+
return true;
|
|
19500
|
+
}
|
|
19501
|
+
if (k === "[" || k === "]") {
|
|
19502
|
+
const size = this.settingsValue.brushSize;
|
|
19503
|
+
this.settingsValue = { ...this.settingsValue, brushSize: Math.max(1, Math.round(k === "]" ? size * 1.25 : size / 1.25)) };
|
|
19504
|
+
this.emit();
|
|
19505
|
+
return true;
|
|
19506
|
+
}
|
|
19507
|
+
return false;
|
|
19508
|
+
}
|
|
19509
|
+
/**
|
|
19510
|
+
* OK: applies the strokes and settings to the image as one undo step (the same call an agent
|
|
19511
|
+
* makes) and remembers the settings when asked to.
|
|
19512
|
+
*/
|
|
19513
|
+
async commit() {
|
|
19514
|
+
const s = this.settingsValue;
|
|
19515
|
+
remembered = s.remember ? { ...s, params: { ...s.params } } : null;
|
|
19516
|
+
return this.controller.refineSelectionTo(s.params, s.output, {
|
|
19517
|
+
edits: [...this.editsList],
|
|
19518
|
+
decontaminate: s.output === "new-layer" || s.output === "new-layer-with-mask" ? s.decontaminate : 0,
|
|
19519
|
+
refineMode: s.refineMode
|
|
19520
|
+
});
|
|
19521
|
+
}
|
|
19522
|
+
};
|
|
19523
|
+
function forgetSelectAndMaskSettings() {
|
|
19524
|
+
remembered = null;
|
|
19525
|
+
}
|
|
19526
|
+
|
|
19130
19527
|
// src/image-editor/mosaic-filter.ts
|
|
19131
19528
|
function resolveMosaicRegionBounds(region, imgWidth, imgHeight) {
|
|
19132
19529
|
if (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {
|
|
@@ -20826,6 +21223,7 @@ export {
|
|
|
20826
21223
|
DEFAULT_REFINE_EDGE_PARAMS,
|
|
20827
21224
|
DEFAULT_RETOUCH,
|
|
20828
21225
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
21226
|
+
DEFAULT_SELECT_AND_MASK_SETTINGS,
|
|
20829
21227
|
DEFAULT_SHAPE_TOOL_OPTIONS,
|
|
20830
21228
|
DEFAULT_SMUDGE,
|
|
20831
21229
|
DEFAULT_TEXT_STYLE,
|
|
@@ -20943,6 +21341,7 @@ export {
|
|
|
20943
21341
|
PIGMENT_WAVELENGTHS,
|
|
20944
21342
|
PLANE,
|
|
20945
21343
|
PLANE_BYTES,
|
|
21344
|
+
PREVIEW_SIZE,
|
|
20946
21345
|
PROOF_PROFILE_OPTIONS,
|
|
20947
21346
|
PSD_BLEND_KEYS,
|
|
20948
21347
|
PSD_FILE_ACCEPT,
|
|
@@ -20980,6 +21379,8 @@ export {
|
|
|
20980
21379
|
SELECTION_MODE_LABELS,
|
|
20981
21380
|
SELECTION_MODIFY_OPS,
|
|
20982
21381
|
SELECTION_TOOLS,
|
|
21382
|
+
SELECT_AND_MASK_TOOLS,
|
|
21383
|
+
SELECT_AND_MASK_VIEWS,
|
|
20983
21384
|
SET_PLANES,
|
|
20984
21385
|
SHADOW_AMBIENT,
|
|
20985
21386
|
SHAPE_KIND_LABELS,
|
|
@@ -20993,6 +21394,7 @@ export {
|
|
|
20993
21394
|
SRGB_TO_LINEAR,
|
|
20994
21395
|
STRIP,
|
|
20995
21396
|
SUPPORTED_ADJUSTMENT_KEYS,
|
|
21397
|
+
SelectAndMaskSession,
|
|
20996
21398
|
SelectionGesture,
|
|
20997
21399
|
ShapeDrawGesture,
|
|
20998
21400
|
SmudgeTip,
|
|
@@ -21314,6 +21716,7 @@ export {
|
|
|
21314
21716
|
focusArea,
|
|
21315
21717
|
followLayerMove,
|
|
21316
21718
|
fontOf,
|
|
21719
|
+
forgetSelectAndMaskSettings,
|
|
21317
21720
|
formatControlValue,
|
|
21318
21721
|
fullSelection,
|
|
21319
21722
|
gaussianBlur,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Select and Mask workspace's strokes as plain data, so an agent can send what a person
|
|
3
3
|
* paints: Quick Selection strokes (grow to the region's edges), Brush strokes (paint the
|
|
4
|
-
* selection in or out directly)
|
|
5
|
-
* re-estimated from the image, e.g. over hair).
|
|
4
|
+
* selection in or out directly), Refine Edge Brush strokes (mark where the edge should be
|
|
5
|
+
* re-estimated from the image, e.g. over hair), Lasso outlines and Object Selection boxes.
|
|
6
6
|
*/
|
|
7
|
-
import type { ImagePoint } from '../types.cjs';
|
|
7
|
+
import type { CropBox, ImagePoint } from '../types.cjs';
|
|
8
8
|
import { type PixelBuffer, type SelectionMask } from './mask.cjs';
|
|
9
9
|
/** One stroke in the workspace, in natural image pixels. */
|
|
10
10
|
export type RefineEdit = {
|
|
@@ -22,8 +22,17 @@ export type RefineEdit = {
|
|
|
22
22
|
kind: 'refine-brush';
|
|
23
23
|
points: ImagePoint[];
|
|
24
24
|
size?: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'lasso';
|
|
27
|
+
points: ImagePoint[];
|
|
28
|
+
mode?: 'add' | 'subtract';
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'object';
|
|
31
|
+
rect?: CropBox;
|
|
32
|
+
point?: ImagePoint;
|
|
33
|
+
mode?: 'add' | 'subtract';
|
|
25
34
|
};
|
|
26
|
-
export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush"];
|
|
35
|
+
export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush", "lasso", "object"];
|
|
27
36
|
/**
|
|
28
37
|
* Coverage of a round brush of diameter `size` dragged along `points` (one point is a dab), over
|
|
29
38
|
* a `width × height` image. `hardness` 0..1 (default 1): the solid share of the radius; the rest
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Select and Mask workspace's strokes as plain data, so an agent can send what a person
|
|
3
3
|
* paints: Quick Selection strokes (grow to the region's edges), Brush strokes (paint the
|
|
4
|
-
* selection in or out directly)
|
|
5
|
-
* re-estimated from the image, e.g. over hair).
|
|
4
|
+
* selection in or out directly), Refine Edge Brush strokes (mark where the edge should be
|
|
5
|
+
* re-estimated from the image, e.g. over hair), Lasso outlines and Object Selection boxes.
|
|
6
6
|
*/
|
|
7
|
-
import type { ImagePoint } from '../types.js';
|
|
7
|
+
import type { CropBox, ImagePoint } from '../types.js';
|
|
8
8
|
import { type PixelBuffer, type SelectionMask } from './mask.js';
|
|
9
9
|
/** One stroke in the workspace, in natural image pixels. */
|
|
10
10
|
export type RefineEdit = {
|
|
@@ -22,8 +22,17 @@ export type RefineEdit = {
|
|
|
22
22
|
kind: 'refine-brush';
|
|
23
23
|
points: ImagePoint[];
|
|
24
24
|
size?: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'lasso';
|
|
27
|
+
points: ImagePoint[];
|
|
28
|
+
mode?: 'add' | 'subtract';
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'object';
|
|
31
|
+
rect?: CropBox;
|
|
32
|
+
point?: ImagePoint;
|
|
33
|
+
mode?: 'add' | 'subtract';
|
|
25
34
|
};
|
|
26
|
-
export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush"];
|
|
35
|
+
export declare const REFINE_EDIT_KINDS: readonly ["quick-select", "brush", "refine-brush", "lasso", "object"];
|
|
27
36
|
/**
|
|
28
37
|
* Coverage of a round brush of diameter `size` dragged along `points` (one point is a dab), over
|
|
29
38
|
* a `width × height` image. `hardness` 0..1 (default 1): the solid share of the radius; the rest
|