@pixerate/editor 0.216.0 → 0.217.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 +33 -0
- package/dist/image-editor/controller.d.ts +33 -0
- package/dist/image-editor/index.cjs +812 -565
- package/dist/image-editor/index.js +249 -2
- package/dist/image-editor/renderer.d.cts +2 -0
- package/dist/image-editor/renderer.d.ts +2 -0
- package/dist/image-editor/selection/diff.d.cts +59 -0
- package/dist/image-editor/selection/diff.d.ts +59 -0
- package/dist/image-editor/selection/index.d.cts +1 -0
- package/dist/image-editor/selection/index.d.ts +1 -0
- package/dist/image-editor/types.d.cts +9 -0
- package/dist/image-editor/types.d.ts +9 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +2 -2
- package/skills/pixerate-image-selection-masking/SKILL.md +3 -3
|
@@ -1070,6 +1070,105 @@ function startMarchingAnts(tick, stepMs = MARCHING_ANTS_STEP_MS) {
|
|
|
1070
1070
|
return () => clearInterval(id);
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
|
+
// src/image-editor/selection/diff.ts
|
|
1074
|
+
function compareSelectionMasks(a, b, width, height, options = {}) {
|
|
1075
|
+
const minArea = Math.max(1, options.minArea ?? 4);
|
|
1076
|
+
const maxRegions = Math.max(1, options.maxRegions ?? 10);
|
|
1077
|
+
const n = width * height;
|
|
1078
|
+
const ad = a?.data;
|
|
1079
|
+
const bd = b?.data;
|
|
1080
|
+
const added = new Uint8Array(n);
|
|
1081
|
+
const removed = new Uint8Array(n);
|
|
1082
|
+
let areaA = 0;
|
|
1083
|
+
let areaB = 0;
|
|
1084
|
+
let inter = 0;
|
|
1085
|
+
let change = 0;
|
|
1086
|
+
let addedArea = 0;
|
|
1087
|
+
let removedArea = 0;
|
|
1088
|
+
for (let i = 0; i < n; i++) {
|
|
1089
|
+
const va = ad ? ad[i] : 0;
|
|
1090
|
+
const vb = bd ? bd[i] : 0;
|
|
1091
|
+
if (va !== vb) change += Math.abs(vb - va);
|
|
1092
|
+
const ia = va >= 128;
|
|
1093
|
+
const ib = vb >= 128;
|
|
1094
|
+
if (ia) areaA++;
|
|
1095
|
+
if (ib) areaB++;
|
|
1096
|
+
if (ia && ib) inter++;
|
|
1097
|
+
else if (ib) {
|
|
1098
|
+
added[i] = 255;
|
|
1099
|
+
addedArea++;
|
|
1100
|
+
} else if (ia) {
|
|
1101
|
+
removed[i] = 255;
|
|
1102
|
+
removedArea++;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const union = areaA + areaB - inter;
|
|
1106
|
+
const addedMask = createSelectionMask(width, height, added);
|
|
1107
|
+
const removedMask = createSelectionMask(width, height, removed);
|
|
1108
|
+
return {
|
|
1109
|
+
a: { area: areaA, bounds: a?.bounds ?? null },
|
|
1110
|
+
b: { area: areaB, bounds: b?.bounds ?? null },
|
|
1111
|
+
added: { area: addedArea, ...pieces(added, width, height, addedMask.bounds, minArea, maxRegions) },
|
|
1112
|
+
removed: { area: removedArea, ...pieces(removed, width, height, removedMask.bounds, minArea, maxRegions) },
|
|
1113
|
+
iou: union ? Math.round(inter / union * 1e4) / 1e4 : 1,
|
|
1114
|
+
coverageChange: Math.round(change / 255),
|
|
1115
|
+
identical: change === 0,
|
|
1116
|
+
addedMask,
|
|
1117
|
+
removedMask
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
function pieces(bits, W, H, bounds, minArea, maxRegions) {
|
|
1121
|
+
if (!bounds) return { count: 0, regions: [] };
|
|
1122
|
+
const { x: x0, y: y0, width: w, height: h } = bounds;
|
|
1123
|
+
const seen = new Uint8Array(w * h);
|
|
1124
|
+
const stack = new Int32Array(w * h);
|
|
1125
|
+
const found = [];
|
|
1126
|
+
for (let start = 0; start < w * h; start++) {
|
|
1127
|
+
if (seen[start] || !bits[(y0 + Math.floor(start / w)) * W + x0 + start % w]) continue;
|
|
1128
|
+
let area = 0;
|
|
1129
|
+
let bx0 = w;
|
|
1130
|
+
let by0 = h;
|
|
1131
|
+
let bx1 = -1;
|
|
1132
|
+
let by1 = -1;
|
|
1133
|
+
let top = 0;
|
|
1134
|
+
stack[top++] = start;
|
|
1135
|
+
seen[start] = 1;
|
|
1136
|
+
while (top) {
|
|
1137
|
+
const k = stack[--top];
|
|
1138
|
+
const i = k % w;
|
|
1139
|
+
const j = (k - i) / w;
|
|
1140
|
+
area++;
|
|
1141
|
+
if (i < bx0) bx0 = i;
|
|
1142
|
+
if (i > bx1) bx1 = i;
|
|
1143
|
+
if (j < by0) by0 = j;
|
|
1144
|
+
if (j > by1) by1 = j;
|
|
1145
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1146
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1147
|
+
const ni = i + dx;
|
|
1148
|
+
const nj = j + dy;
|
|
1149
|
+
if (ni < 0 || nj < 0 || ni >= w || nj >= h) continue;
|
|
1150
|
+
const nk = nj * w + ni;
|
|
1151
|
+
if (seen[nk] || !bits[(y0 + nj) * W + x0 + ni]) continue;
|
|
1152
|
+
seen[nk] = 1;
|
|
1153
|
+
stack[top++] = nk;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (area >= minArea) found.push({ area, bounds: { x: x0 + bx0, y: y0 + by0, width: bx1 - bx0 + 1, height: by1 - by0 + 1 } });
|
|
1158
|
+
}
|
|
1159
|
+
found.sort((p, q) => q.area - p.area);
|
|
1160
|
+
return { count: found.length, regions: found.slice(0, maxRegions) };
|
|
1161
|
+
}
|
|
1162
|
+
function selectionDiffPixels(added, removed) {
|
|
1163
|
+
const n = added.width * added.height;
|
|
1164
|
+
const out = new Uint8ClampedArray(n * 4);
|
|
1165
|
+
for (let i = 0; i < n; i++) {
|
|
1166
|
+
if (added.data[i]) out.set([34, 197, 94, 140], i * 4);
|
|
1167
|
+
else if (removed.data[i]) out.set([239, 68, 68, 140], i * 4);
|
|
1168
|
+
}
|
|
1169
|
+
return out;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1073
1172
|
// src/image-editor/layers/render.ts
|
|
1074
1173
|
function isQuarterTurn(rotate) {
|
|
1075
1174
|
return normalizeAngle(rotate) % 90 === 0;
|
|
@@ -1515,6 +1614,19 @@ var ImageEditorRenderer = class {
|
|
|
1515
1614
|
}
|
|
1516
1615
|
ctx.restore();
|
|
1517
1616
|
}
|
|
1617
|
+
const diff = state.selectionDiff;
|
|
1618
|
+
if (diff && diff.selection === (state.selection ?? null)) {
|
|
1619
|
+
ctx.save();
|
|
1620
|
+
try {
|
|
1621
|
+
applyImageProjectionToContext(ctx, projection);
|
|
1622
|
+
ctx.beginPath();
|
|
1623
|
+
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1624
|
+
ctx.clip();
|
|
1625
|
+
this.drawSelectionDiff(ctx, diff);
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
ctx.restore();
|
|
1629
|
+
}
|
|
1518
1630
|
if (options.brushCursor) {
|
|
1519
1631
|
const b = options.brushCursor;
|
|
1520
1632
|
ctx.save();
|
|
@@ -1970,6 +2082,7 @@ var ImageEditorRenderer = class {
|
|
|
1970
2082
|
this.onPreviewHistogram?.(histogram);
|
|
1971
2083
|
}
|
|
1972
2084
|
quickMaskCache = /* @__PURE__ */ new WeakMap();
|
|
2085
|
+
diffCache = /* @__PURE__ */ new WeakMap();
|
|
1973
2086
|
outlineCache = /* @__PURE__ */ new WeakMap();
|
|
1974
2087
|
emptyMaskCache = null;
|
|
1975
2088
|
/** An empty selection of the image's size (Quick Mask mode with nothing selected). */
|
|
@@ -2023,6 +2136,19 @@ var ImageEditorRenderer = class {
|
|
|
2023
2136
|
ctx.drawImage(cached.canvas, 0, 0, mask.width, mask.height);
|
|
2024
2137
|
}
|
|
2025
2138
|
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
2139
|
+
drawSelectionDiff(ctx, diff) {
|
|
2140
|
+
let canvas = this.diffCache.get(diff);
|
|
2141
|
+
if (!canvas) {
|
|
2142
|
+
canvas = this.createCanvas(diff.added.width, diff.added.height);
|
|
2143
|
+
const mctx = canvas.getContext("2d");
|
|
2144
|
+
if (!mctx || typeof mctx.createImageData !== "function") return;
|
|
2145
|
+
const img = mctx.createImageData(diff.added.width, diff.added.height);
|
|
2146
|
+
img.data.set(selectionDiffPixels(diff.added, diff.removed));
|
|
2147
|
+
mctx.putImageData(img, 0, 0);
|
|
2148
|
+
this.diffCache.set(diff, canvas);
|
|
2149
|
+
}
|
|
2150
|
+
ctx.drawImage(canvas, 0, 0, diff.added.width, diff.added.height);
|
|
2151
|
+
}
|
|
2026
2152
|
drawQuickMask(ctx, selection) {
|
|
2027
2153
|
let canvas = this.quickMaskCache.get(selection);
|
|
2028
2154
|
if (!canvas) {
|
|
@@ -2987,10 +3113,10 @@ function regions(a, W, H, b, minArea) {
|
|
|
2987
3113
|
const kept = list.filter((r) => r.area >= minArea).sort((p, q) => q.area - p.area);
|
|
2988
3114
|
return { count: kept.length, totalArea: kept.reduce((s, r) => s + r.area, 0), largest: kept.slice(0, 10).map((r) => ({ area: r.area, bounds: box(r) })) };
|
|
2989
3115
|
};
|
|
2990
|
-
const
|
|
3116
|
+
const pieces2 = found.filter((r) => r.fg).sort((p, q) => q.area - p.area);
|
|
2991
3117
|
return {
|
|
2992
3118
|
holes: summarize(found.filter((r) => !r.fg && !r.outer)),
|
|
2993
|
-
islands: summarize(
|
|
3119
|
+
islands: summarize(pieces2.slice(1))
|
|
2994
3120
|
};
|
|
2995
3121
|
}
|
|
2996
3122
|
|
|
@@ -10381,6 +10507,65 @@ var ImageEditorController = class {
|
|
|
10381
10507
|
const region = { x, y, width: Math.min(width, b.x + b.width + m) - x, height: Math.min(height, b.y + b.height + m) - y };
|
|
10382
10508
|
return selectionUncertainty(sel, { ...this.compositePixels(region), x, y }, options);
|
|
10383
10509
|
}
|
|
10510
|
+
/**
|
|
10511
|
+
* A selection to compare (`compareSelections`): the current one, the one `steps` undo steps
|
|
10512
|
+
* ago, a saved channel, or a layer's painted or vector mask. `null` is nothing selected.
|
|
10513
|
+
*/
|
|
10514
|
+
selectionFrom(source) {
|
|
10515
|
+
const { width, height } = this.naturalSize;
|
|
10516
|
+
switch (source?.kind) {
|
|
10517
|
+
case "selection":
|
|
10518
|
+
return this.getSelection();
|
|
10519
|
+
case "history": {
|
|
10520
|
+
const steps = source.steps ?? 1;
|
|
10521
|
+
const depth = this.undoStack.length;
|
|
10522
|
+
if (!Number.isInteger(steps) || steps < 1) throw new LayerOperationError(`steps must be a whole number of undo steps back, at least 1; got ${String(source.steps)}.`);
|
|
10523
|
+
if (steps > depth) throw new LayerOperationError(`There ${depth === 1 ? "is 1 undo step" : `are ${depth} undo steps`}; steps must be 1..${depth || 1}${depth ? "" : " once something has been done"}.`);
|
|
10524
|
+
return this.undoStack[depth - steps].selection ?? null;
|
|
10525
|
+
}
|
|
10526
|
+
case "channel":
|
|
10527
|
+
return this.requireChannel(source.channelId).mask;
|
|
10528
|
+
case "layer-mask": {
|
|
10529
|
+
const layer = this.getLayer(source.layerId);
|
|
10530
|
+
if (!layer) throw new LayerOperationError(`There is no layer "${source.layerId}" (see list_layers).`);
|
|
10531
|
+
const mask = this.layerMaskCoverage(layer.id);
|
|
10532
|
+
if (!mask) throw new LayerOperationError(`Layer "${layer.name || layer.id}" has no painted mask; add one with add_layer_mask, or compare its vector-mask.`);
|
|
10533
|
+
return mask;
|
|
10534
|
+
}
|
|
10535
|
+
case "vector-mask": {
|
|
10536
|
+
const layer = this.getLayer(source.layerId);
|
|
10537
|
+
if (!layer) throw new LayerOperationError(`There is no layer "${source.layerId}" (see list_layers).`);
|
|
10538
|
+
const { vm } = this.requireVectorMask(layer.id);
|
|
10539
|
+
const mask = pathToSelection(vm.components, width, height, { fillRule: vm.fillRule });
|
|
10540
|
+
return vm.inverted ? invertSelection(mask) : mask;
|
|
10541
|
+
}
|
|
10542
|
+
default:
|
|
10543
|
+
throw new LayerOperationError(`Unknown selection source ${JSON.stringify(source)}. Use selection, history, channel, layer-mask or vector-mask.`);
|
|
10544
|
+
}
|
|
10545
|
+
}
|
|
10546
|
+
/**
|
|
10547
|
+
* What changed from selection `a` (default: before the last undo step) to `b` (default: the
|
|
10548
|
+
* current selection): added and removed area with their largest pieces, IoU and the coverage
|
|
10549
|
+
* change. With `show` (default true) the canvas tints added area green and removed red until the
|
|
10550
|
+
* selection next changes (view state, no undo step). Read-only otherwise.
|
|
10551
|
+
*/
|
|
10552
|
+
compareSelections(a = { kind: "history", steps: 1 }, b = { kind: "selection" }, options = {}) {
|
|
10553
|
+
const { width, height } = this.naturalSize;
|
|
10554
|
+
const ma = this.selectionFrom(a);
|
|
10555
|
+
const mb = this.selectionFrom(b);
|
|
10556
|
+
const { addedMask, removedMask, ...diff } = compareSelectionMasks(ma, mb, width, height, options);
|
|
10557
|
+
if (options.show !== false) {
|
|
10558
|
+
this.state.selectionDiff = diff.identical ? null : { added: addedMask, removed: removedMask, selection: this.state.selection ?? null };
|
|
10559
|
+
this.notify();
|
|
10560
|
+
}
|
|
10561
|
+
return diff;
|
|
10562
|
+
}
|
|
10563
|
+
/** Hides the `compareSelections` overlay. */
|
|
10564
|
+
clearSelectionDiff() {
|
|
10565
|
+
if (!this.state.selectionDiff) return;
|
|
10566
|
+
this.state.selectionDiff = null;
|
|
10567
|
+
this.notify();
|
|
10568
|
+
}
|
|
10384
10569
|
/** A layer's painted mask over the canvas as a selection mask; null without one (or for a depth mask). */
|
|
10385
10570
|
layerMaskCoverage(layerId) {
|
|
10386
10571
|
const layer = this.getLayer(layerId);
|
|
@@ -13146,6 +13331,65 @@ function createImageSelectionTools(controller) {
|
|
|
13146
13331
|
return controller.inspectMaskEdge(t, { layerId, minArea });
|
|
13147
13332
|
}
|
|
13148
13333
|
};
|
|
13334
|
+
const sourceSchema = (which, dflt) => ({
|
|
13335
|
+
type: "object",
|
|
13336
|
+
properties: {
|
|
13337
|
+
source: { type: "string", enum: ["selection", "history", "channel", "layer-mask", "vector-mask"], description: "selection: the current one; history: as it was steps undo steps ago; channel: a saved selection (channelId, see list_channels); layer-mask / vector-mask: a layer's mask (layerId)." },
|
|
13338
|
+
steps: { type: "integer", minimum: 1, description: "history only: how many undo steps back (default 1, before the last edit)." },
|
|
13339
|
+
channelId: { type: "string", description: "channel only: the channel id from list_channels." },
|
|
13340
|
+
layerId: { type: "string", description: "layer-mask and vector-mask only: the layer id from list_layers." }
|
|
13341
|
+
},
|
|
13342
|
+
required: ["source"],
|
|
13343
|
+
additionalProperties: false,
|
|
13344
|
+
description: `${which} (default ${dflt}).`
|
|
13345
|
+
});
|
|
13346
|
+
const toSource = (v, fallback) => {
|
|
13347
|
+
if (v === void 0) return fallback;
|
|
13348
|
+
const o = v;
|
|
13349
|
+
const extra = (keys) => {
|
|
13350
|
+
const bad = Object.keys(o).filter((k) => k !== "source" && !keys.includes(k));
|
|
13351
|
+
if (bad.length) throw new Error(`${bad.join(", ")} ${bad.length === 1 ? "does" : "do"} not apply to source ${o.source}.`);
|
|
13352
|
+
};
|
|
13353
|
+
switch (o.source) {
|
|
13354
|
+
case "selection":
|
|
13355
|
+
extra([]);
|
|
13356
|
+
return { kind: "selection" };
|
|
13357
|
+
case "history":
|
|
13358
|
+
extra(["steps"]);
|
|
13359
|
+
return { kind: "history", steps: o.steps ?? 1 };
|
|
13360
|
+
case "channel":
|
|
13361
|
+
extra(["channelId"]);
|
|
13362
|
+
if (!o.channelId) throw new Error("source channel needs channelId (see list_channels).");
|
|
13363
|
+
return { kind: "channel", channelId: o.channelId };
|
|
13364
|
+
case "layer-mask":
|
|
13365
|
+
case "vector-mask":
|
|
13366
|
+
extra(["layerId"]);
|
|
13367
|
+
if (!o.layerId) throw new Error(`source ${o.source} needs layerId (see list_layers).`);
|
|
13368
|
+
return { kind: o.source, layerId: o.layerId };
|
|
13369
|
+
default:
|
|
13370
|
+
throw new Error(`Unknown source "${String(o.source)}". Use selection, history, channel, layer-mask or vector-mask.`);
|
|
13371
|
+
}
|
|
13372
|
+
};
|
|
13373
|
+
const compareTool = {
|
|
13374
|
+
name: "compare_selections",
|
|
13375
|
+
title: "Compare two selections",
|
|
13376
|
+
description: "Shows what changed between two selections, to check that a modify, refine or select step did what you meant. a (default: the selection before the last undo step) and b (default: the current selection) can each be the current selection, a history step, a saved channel or a layer's mask. Returns a and b (area, bounds), added and removed (area in pixels, count and the 10 largest pieces with bounds, ready for capture_view), iou 0-1, coverageChange (pixels of coverage changed, counting edges that only softened) and identical. With show (default true) the canvas tints added area green and removed red until the selection next changes (view only, no undo step).",
|
|
13377
|
+
inputSchema: {
|
|
13378
|
+
type: "object",
|
|
13379
|
+
properties: {
|
|
13380
|
+
a: sourceSchema("The selection before", "history, 1 step back"),
|
|
13381
|
+
b: sourceSchema("The selection after", "the current selection"),
|
|
13382
|
+
minArea: { type: "number", minimum: 1, maximum: 1e6, description: "Pieces smaller than this many pixels are counted but not listed (default 4)." },
|
|
13383
|
+
show: { type: "boolean", description: "Tint the change on the canvas (default true)." }
|
|
13384
|
+
},
|
|
13385
|
+
additionalProperties: false
|
|
13386
|
+
},
|
|
13387
|
+
annotations: { title: "Compare two selections", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
13388
|
+
handler: ({ a, b, minArea, show }) => controller.compareSelections(toSource(a, { kind: "history", steps: 1 }), toSource(b, { kind: "selection" }), {
|
|
13389
|
+
...minArea !== void 0 ? { minArea } : {},
|
|
13390
|
+
show: show !== false
|
|
13391
|
+
})
|
|
13392
|
+
};
|
|
13149
13393
|
const selectRegion = {
|
|
13150
13394
|
name: "select_region",
|
|
13151
13395
|
title: "Select a region",
|
|
@@ -14200,6 +14444,7 @@ function createImageSelectionTools(controller) {
|
|
|
14200
14444
|
return [
|
|
14201
14445
|
getSelection,
|
|
14202
14446
|
inspectEdge,
|
|
14447
|
+
compareTool,
|
|
14203
14448
|
selectAll,
|
|
14204
14449
|
deselect,
|
|
14205
14450
|
invert2,
|
|
@@ -22227,6 +22472,7 @@ export {
|
|
|
22227
22472
|
colorRange,
|
|
22228
22473
|
colorRangePreset,
|
|
22229
22474
|
combineSelections,
|
|
22475
|
+
compareSelectionMasks,
|
|
22230
22476
|
compileAdjustmentLayer,
|
|
22231
22477
|
componentPath,
|
|
22232
22478
|
componentsCoverage,
|
|
@@ -22772,6 +23018,7 @@ export {
|
|
|
22772
23018
|
selectObjectIn,
|
|
22773
23019
|
selectSimilar,
|
|
22774
23020
|
selectionArea,
|
|
23021
|
+
selectionDiffPixels,
|
|
22775
23022
|
selectionFromCoverage,
|
|
22776
23023
|
selectionFromPixels,
|
|
22777
23024
|
selectionFromRegion,
|
|
@@ -176,6 +176,7 @@ export declare class ImageEditorRenderer {
|
|
|
176
176
|
private applyPost;
|
|
177
177
|
private publishHistogram;
|
|
178
178
|
private quickMaskCache;
|
|
179
|
+
private diffCache;
|
|
179
180
|
private outlineCache;
|
|
180
181
|
private emptyMaskCache;
|
|
181
182
|
/** An empty selection of the image's size (Quick Mask mode with nothing selected). */
|
|
@@ -190,6 +191,7 @@ export declare class ImageEditorRenderer {
|
|
|
190
191
|
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
191
192
|
private drawMaskOverlay;
|
|
192
193
|
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
194
|
+
private drawSelectionDiff;
|
|
193
195
|
private drawQuickMask;
|
|
194
196
|
/**
|
|
195
197
|
* Marching ants: a white line under a black dashed one, one screen pixel wide. `scale` is
|
|
@@ -176,6 +176,7 @@ export declare class ImageEditorRenderer {
|
|
|
176
176
|
private applyPost;
|
|
177
177
|
private publishHistogram;
|
|
178
178
|
private quickMaskCache;
|
|
179
|
+
private diffCache;
|
|
179
180
|
private outlineCache;
|
|
180
181
|
private emptyMaskCache;
|
|
181
182
|
/** An empty selection of the image's size (Quick Mask mode with nothing selected). */
|
|
@@ -190,6 +191,7 @@ export declare class ImageEditorRenderer {
|
|
|
190
191
|
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
191
192
|
private drawMaskOverlay;
|
|
192
193
|
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
194
|
+
private drawSelectionDiff;
|
|
193
195
|
private drawQuickMask;
|
|
194
196
|
/**
|
|
195
197
|
* Marching ants: a white line under a black dashed one, one screen pixel wide. `scale` is
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What changed between two selections (`compare_selections`): the area added and removed, where,
|
|
3
|
+
* and how much they overlap (IoU), so an agent can check that a modify or refine step did what it
|
|
4
|
+
* meant to and a person can see it as a two-colour overlay.
|
|
5
|
+
*/
|
|
6
|
+
import type { CropBox } from '../types.cjs';
|
|
7
|
+
import { type SelectionMask } from './mask.cjs';
|
|
8
|
+
/** One piece of added or removed area. */
|
|
9
|
+
export interface SelectionDiffRegion {
|
|
10
|
+
area: number;
|
|
11
|
+
bounds: CropBox;
|
|
12
|
+
}
|
|
13
|
+
export interface SelectionDiffSide {
|
|
14
|
+
/** Pixels at least half selected. */
|
|
15
|
+
area: number;
|
|
16
|
+
bounds: CropBox | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SelectionDiff {
|
|
19
|
+
a: SelectionDiffSide;
|
|
20
|
+
b: SelectionDiffSide;
|
|
21
|
+
/** Selected in b but not in a: area, and the largest pieces (at most `maxRegions`). */
|
|
22
|
+
added: {
|
|
23
|
+
area: number;
|
|
24
|
+
count: number;
|
|
25
|
+
regions: SelectionDiffRegion[];
|
|
26
|
+
};
|
|
27
|
+
/** Selected in a but not in b. */
|
|
28
|
+
removed: {
|
|
29
|
+
area: number;
|
|
30
|
+
count: number;
|
|
31
|
+
regions: SelectionDiffRegion[];
|
|
32
|
+
};
|
|
33
|
+
/** Intersection over union, 0–1 (1 when both are empty). */
|
|
34
|
+
iou: number;
|
|
35
|
+
/** Σ |coverage b − coverage a| in pixels: also counts edges that only softened or hardened. */
|
|
36
|
+
coverageChange: number;
|
|
37
|
+
/** Every pixel's coverage is the same. */
|
|
38
|
+
identical: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface CompareSelectionsOptions {
|
|
41
|
+
/** Pieces smaller than this many pixels are counted in the area but not listed (default 4). */
|
|
42
|
+
minArea?: number;
|
|
43
|
+
/** At most this many pieces are listed per side (default 10). */
|
|
44
|
+
maxRegions?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compares selection `a` (before) with `b` (after) over a `width × height` canvas; `null` is
|
|
48
|
+
* nothing selected. Area, pieces and IoU are judged at half coverage. Also returns the added and
|
|
49
|
+
* removed masks for an overlay.
|
|
50
|
+
*/
|
|
51
|
+
export declare function compareSelectionMasks(a: SelectionMask | null, b: SelectionMask | null, width: number, height: number, options?: CompareSelectionsOptions): SelectionDiff & {
|
|
52
|
+
addedMask: SelectionMask;
|
|
53
|
+
removedMask: SelectionMask;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The diff overlay's RGBA: added area tinted green, removed area red, the rest transparent
|
|
57
|
+
* (`width × height`, the masks' size).
|
|
58
|
+
*/
|
|
59
|
+
export declare function selectionDiffPixels(added: SelectionMask, removed: SelectionMask): Uint8ClampedArray;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What changed between two selections (`compare_selections`): the area added and removed, where,
|
|
3
|
+
* and how much they overlap (IoU), so an agent can check that a modify or refine step did what it
|
|
4
|
+
* meant to and a person can see it as a two-colour overlay.
|
|
5
|
+
*/
|
|
6
|
+
import type { CropBox } from '../types.js';
|
|
7
|
+
import { type SelectionMask } from './mask.js';
|
|
8
|
+
/** One piece of added or removed area. */
|
|
9
|
+
export interface SelectionDiffRegion {
|
|
10
|
+
area: number;
|
|
11
|
+
bounds: CropBox;
|
|
12
|
+
}
|
|
13
|
+
export interface SelectionDiffSide {
|
|
14
|
+
/** Pixels at least half selected. */
|
|
15
|
+
area: number;
|
|
16
|
+
bounds: CropBox | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SelectionDiff {
|
|
19
|
+
a: SelectionDiffSide;
|
|
20
|
+
b: SelectionDiffSide;
|
|
21
|
+
/** Selected in b but not in a: area, and the largest pieces (at most `maxRegions`). */
|
|
22
|
+
added: {
|
|
23
|
+
area: number;
|
|
24
|
+
count: number;
|
|
25
|
+
regions: SelectionDiffRegion[];
|
|
26
|
+
};
|
|
27
|
+
/** Selected in a but not in b. */
|
|
28
|
+
removed: {
|
|
29
|
+
area: number;
|
|
30
|
+
count: number;
|
|
31
|
+
regions: SelectionDiffRegion[];
|
|
32
|
+
};
|
|
33
|
+
/** Intersection over union, 0–1 (1 when both are empty). */
|
|
34
|
+
iou: number;
|
|
35
|
+
/** Σ |coverage b − coverage a| in pixels: also counts edges that only softened or hardened. */
|
|
36
|
+
coverageChange: number;
|
|
37
|
+
/** Every pixel's coverage is the same. */
|
|
38
|
+
identical: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface CompareSelectionsOptions {
|
|
41
|
+
/** Pieces smaller than this many pixels are counted in the area but not listed (default 4). */
|
|
42
|
+
minArea?: number;
|
|
43
|
+
/** At most this many pieces are listed per side (default 10). */
|
|
44
|
+
maxRegions?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compares selection `a` (before) with `b` (after) over a `width × height` canvas; `null` is
|
|
48
|
+
* nothing selected. Area, pieces and IoU are judged at half coverage. Also returns the added and
|
|
49
|
+
* removed masks for an overlay.
|
|
50
|
+
*/
|
|
51
|
+
export declare function compareSelectionMasks(a: SelectionMask | null, b: SelectionMask | null, width: number, height: number, options?: CompareSelectionsOptions): SelectionDiff & {
|
|
52
|
+
addedMask: SelectionMask;
|
|
53
|
+
removedMask: SelectionMask;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The diff overlay's RGBA: added area tinted green, removed area red, the rest transparent
|
|
57
|
+
* (`width × height`, the masks' size).
|
|
58
|
+
*/
|
|
59
|
+
export declare function selectionDiffPixels(added: SelectionMask, removed: SelectionMask): Uint8ClampedArray;
|
|
@@ -319,6 +319,15 @@ export interface ImageEditorState {
|
|
|
319
319
|
* growing as the brush moves). View state: not an edit, not in history, not saved.
|
|
320
320
|
*/
|
|
321
321
|
selectionPreview?: SelectionMask | null;
|
|
322
|
+
/**
|
|
323
|
+
* `compare_selections`' overlay: the added area tinted green, the removed red. Shown while the
|
|
324
|
+
* selection is still `selection` (any change hides it). View state: not in history, not saved.
|
|
325
|
+
*/
|
|
326
|
+
selectionDiff?: {
|
|
327
|
+
added: SelectionMask;
|
|
328
|
+
removed: SelectionMask;
|
|
329
|
+
selection: SelectionMask | null;
|
|
330
|
+
} | null;
|
|
322
331
|
/** Quick Mask view: tint the unselected area instead of drawing marching ants (view state). */
|
|
323
332
|
quickMask?: boolean;
|
|
324
333
|
/** How the canvas shows the selection (view state; `quickMask` mirrors `'quickMask'`). */
|
|
@@ -319,6 +319,15 @@ export interface ImageEditorState {
|
|
|
319
319
|
* growing as the brush moves). View state: not an edit, not in history, not saved.
|
|
320
320
|
*/
|
|
321
321
|
selectionPreview?: SelectionMask | null;
|
|
322
|
+
/**
|
|
323
|
+
* `compare_selections`' overlay: the added area tinted green, the removed red. Shown while the
|
|
324
|
+
* selection is still `selection` (any change hides it). View state: not in history, not saved.
|
|
325
|
+
*/
|
|
326
|
+
selectionDiff?: {
|
|
327
|
+
added: SelectionMask;
|
|
328
|
+
removed: SelectionMask;
|
|
329
|
+
selection: SelectionMask | null;
|
|
330
|
+
} | null;
|
|
322
331
|
/** Quick Mask view: tint the unselected area instead of drawing marching ants (view state). */
|
|
323
332
|
quickMask?: boolean;
|
|
324
333
|
/** How the canvas shows the selection (view state; `quickMask` mirrors `'quickMask'`). */
|
package/package.json
CHANGED
|
@@ -120,8 +120,7 @@ Keep outlines as paths (`make_work_path`, `add_path`) and edit their anchors: se
|
|
|
120
120
|
|
|
121
121
|
### Colour management
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
and colour conversions for print briefs: read [references/color-management.md](references/color-management.md).
|
|
123
|
+
Working space, profiles, soft proofing and print conversions: read [references/color-management.md](references/color-management.md).
|
|
125
124
|
### Retouching
|
|
126
125
|
|
|
127
126
|
Clone Stamp, Healing Brush, Spot Healing, Smudge, Dodge, Burn, Sponge, Blur, Sharpen and the
|
|
@@ -209,6 +208,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
|
|
|
209
208
|
| `list_controls` | Get every base control (id, label, section, min, max, step, default, `requiresProcess`), each layer kind's settable controls (plus `opacity`), the blend mode ids and the `layers.<layerId>.<controlId>` path format. Call once per session before writing. |
|
|
210
209
|
| `get_selection` | Read the active selection: `empty`, `bounds` (natural pixels), `area` in pixels, `percent` of the image and a description. Check it after every select or modify step and before masking: a wand that covered 90% of the image is the wrong selection. |
|
|
211
210
|
| `inspect_mask_edge` | Measure an edge without looking: the selection (default) or a layer's `layer-mask` / `vector-mask` (`layerId`). Returns `haloScore`, `edgeAlignment`, `jaggedness`, `softFraction`, `holes`, `islands`, `worst` regions for `capture_view` and `issues` with fixes. Inspect, fix, inspect again (see `pixerate-image-selection-masking`). |
|
|
211
|
+
| `compare_selections` | What changed between two selections: `a` (default the one before the last undo step) and `b` (default current), each `selection`, `history` (`steps`), `channel` or a layer's `layer-mask` / `vector-mask`. Returns `added` / `removed` (area, largest pieces with bounds), `iou` and `identical`; `show` tints the change on the canvas. Check every modify or refine step. |
|
|
212
212
|
| `list_channels` | Read the alpha channels (saved selections): id, name, bounds, percent, overlay and visibility, and whether Quick Mask mode is on. |
|
|
213
213
|
| `select_subject` | Select › Subject. With a host segmentation model, `target` describes what to select ("the dog", "the sky") and the model's outlines are selected (`maxSegments` 1 for one). Without one, the subject that stands out most is found classically; `target` can't choose it (`method: "saliency"`, `note`). Combined by `mode`. Returns the selection, `found` (label, bounds) and `method`; nothing found leaves the selection unchanged. |
|
|
214
214
|
| `list_layers` | Read the layer stack bottom to top as a tree: id, type (`pixel`, `adjustment`, `group`, `shape`, `annotation`, `smart`; smart objects add their `contents`, `smartFilters` and `filterMask`; shapes a `shape` summary of component ops and kinds and paint types, and `annotation` when migrated from one), name, document `bounds` of pixel layers, visibility, opacity, `fillOpacity`, blend, `clipped`, locks, a `mask` summary, an `effects` summary (master switch, scale, effect ids and kinds), pixel-layer `filters` (the Background's `develop` filter is the base adjustments), adjustment `kind` and `params`, group `children`, and which layer is `active`. Call it to understand a document with more than the Background before changing it; adjustment layer ids work with every adjustment-layer tool. |
|
|
@@ -65,8 +65,8 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
|
|
|
65
65
|
can open the same edits in the workspace. With a host model, `refineMode: "object"`
|
|
66
66
|
refines from the model's outline.
|
|
67
67
|
- `shiftEdge` −10..−30 % tightens a refined edge that picked up background.
|
|
68
|
-
**Check:** `
|
|
69
|
-
cleanup).
|
|
68
|
+
**Check:** `compare_selections` after the step: `added` / `removed` are what you meant and
|
|
69
|
+
`iou` stays near 1 for a cleanup (it tints the change on the canvas).
|
|
70
70
|
5. **Output.** Choose by goal:
|
|
71
71
|
- A local adjustment: `add_adjustment_layer` while the selection is active (it is masked
|
|
72
72
|
by it), or `apply_selection_as_mask` with `kind`. Filters (`apply_effect`,
|
|
@@ -239,7 +239,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
|
|
|
239
239
|
- `modify_selection`: expand, contract, feather, border, smooth
|
|
240
240
|
- `refine_selection`: snap the edge to the image
|
|
241
241
|
- `transform_selection`: move or scale the outline only
|
|
242
|
-
- `inspect_mask_edge`:
|
|
242
|
+
- `inspect_mask_edge`, `compare_selections`: measure an edge (halo, alignment, jaggedness, holes, islands) and what a step changed (IoU)
|
|
243
243
|
- `set_edge_decontamination`, `set_edge_despill`, `set_edge_ambient`, `get_edge_color`: live edge colour on a masked cutout
|
|
244
244
|
- `capture_view`, `render_preview`: look at the edge at pixel level; check the result
|
|
245
245
|
- `apply_selection_as_mask`: a masked adjustment layer in one step
|