A new node, Load Image. It loads a picture and sets its size in the same node, so a workflow needs no resize node and no size node after its loader. - Size: off, megapixels, longest side, scale, fit inside, or crop to fill. Each choice shows only the numbers it uses. Round to rounds the result to a multiple. Under More: the resize method, and Enlarge, which when off only ever makes a picture smaller. - Arrows under the file picker step through the input folder, with the picture's place in the list beside them. - The node shows the size going in and the size coming out before a run. The browser repeats the backend's arithmetic, and a table of 252 cases holds the two to the same answers. - Outputs: image, mask, width, height, filename, original_width, original_height. The mask is the picture's transparency, as the stock loader gives it, at the new size. - The picture on the node is ComfyUI's own preview, so the mask editor and clipspace work as they do on the stock loader. - With Size off the pixels are the file's own. Mask by Touch and the run check know the new loader as a source.
1131 lines
60 KiB
JavaScript
1131 lines
60 KiB
JavaScript
// The math behind Mask by Touch's face (js/shared/touch_math.mjs): the
|
|
// touches as text, the pieces a pointer lights up, and the edge steps the
|
|
// face works out itself so the picture answers without a run.
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
BRUSH_RADIUS,
|
|
OUTLINE,
|
|
OWN_LOADERS,
|
|
PIECES_PER_PLANE,
|
|
STROKE_MAX_POINTS,
|
|
TINT,
|
|
TOUCH_MAX_ITEMS,
|
|
TOUCH_MAX_POINTS,
|
|
candidatesAt,
|
|
chipGroups,
|
|
chipLabel,
|
|
cleanWord,
|
|
composeEdge,
|
|
decodeTouches,
|
|
encodeTouches,
|
|
extendStroke,
|
|
fillHoles,
|
|
fineChoice,
|
|
fineEdgeLabel,
|
|
fineMade,
|
|
fineMask,
|
|
goesBehind,
|
|
growMask,
|
|
loaderSignature,
|
|
maskShare,
|
|
movedTouches,
|
|
nameShown,
|
|
packPlanes,
|
|
paintCover,
|
|
paintStroke,
|
|
paintTint,
|
|
pieceAt,
|
|
pieceMask,
|
|
pieceOverlaps,
|
|
pieceRanks,
|
|
pieceWholes,
|
|
pieceSizes,
|
|
piecesInside,
|
|
pointCount,
|
|
prefixStamps,
|
|
previewPixels,
|
|
readoutText,
|
|
sameTouches,
|
|
shrinkMask,
|
|
smoothMask,
|
|
softenMask,
|
|
stampOf,
|
|
strokePieces,
|
|
thinStroke,
|
|
touchFits,
|
|
} from "../js/shared/touch_math.mjs";
|
|
import { OUT_OF_MEMORY, goesBehind as goesBehindAgain, piecesHolding, stopMessage, typedName } from "../js/shared/touch_math.mjs";
|
|
import {
|
|
GROUP_MAX,
|
|
areaKey,
|
|
brushStroke,
|
|
cleanItem,
|
|
dropIdleAways,
|
|
hardMask,
|
|
joinMasks,
|
|
maskOn,
|
|
nextGroup,
|
|
numberChip,
|
|
sharedPixels,
|
|
} from "../js/shared/touch_math.mjs";
|
|
|
|
const total = (mask) => mask.reduce((sum, value) => sum + value, 0);
|
|
|
|
function blank(width, height) {
|
|
return new Uint8Array(width * height);
|
|
}
|
|
|
|
function box(width, height, x0, y0, x1, y1) {
|
|
const mask = blank(width, height);
|
|
for (let y = y0; y < y1; y += 1) mask.fill(1, y * width + x0, y * width + x1);
|
|
return mask;
|
|
}
|
|
|
|
const TAP = { t: "tap", op: 1, x: 160, y: 1450 };
|
|
const FIND = { t: "find", op: 1, r: 30, pts: [[150, 1240], [230, 1010], [330, 1150]] };
|
|
const PAINT = { t: "paint", op: 0, r: 45, pts: [[640, 120], [760, 170]] };
|
|
|
|
test("touches survive the trip to text and back", () => {
|
|
const text = encodeTouches("abc", [TAP, FIND, PAINT]);
|
|
assert.equal(text, JSON.stringify({ v: 1, key: "abc", items: [TAP, FIND, PAINT] }));
|
|
assert.deepEqual(decodeTouches(text), { key: "abc", items: [TAP, FIND, PAINT] });
|
|
});
|
|
|
|
test("no touches is the empty text, and the empty text is no touches", () => {
|
|
assert.equal(encodeTouches("abc", []), "");
|
|
assert.deepEqual(decodeTouches(""), { key: "", items: [] });
|
|
assert.deepEqual(decodeTouches(" "), { key: "", items: [] });
|
|
});
|
|
|
|
test("text that is not touches gives none, and never throws", () => {
|
|
for (const text of ["{", "[]", "null", "42", '{"items": 3}', '{"v":1,"items":[{"t":"lasso"}]}', undefined, null, 7]) {
|
|
assert.deepEqual(decodeTouches(text).items, []);
|
|
}
|
|
});
|
|
|
|
test("a touch that is not whole is left out, the rest stay", () => {
|
|
const text = JSON.stringify({ v: 1, key: "k", items: [
|
|
TAP,
|
|
{ t: "tap", op: 1, x: "12", y: 3 },
|
|
{ t: "find", op: 1, r: 30, pts: [] },
|
|
{ t: "paint", op: 1, pts: [[1, 2]] },
|
|
{ t: "paint", op: 1, r: 9, pts: [[1, 2], ["a", 2]] },
|
|
] });
|
|
assert.deepEqual(decodeTouches(text).items, [TAP, { t: "paint", op: 1, r: 9, pts: [[1, 2]] }]);
|
|
});
|
|
|
|
test("touches are written as whole pixels and the brush is kept in range", () => {
|
|
const text = encodeTouches("k", [
|
|
{ t: "tap", op: 0, x: 10.6, y: 3.2 },
|
|
{ t: "paint", op: 1, r: 0.2, pts: [[1.4, 2.6]] },
|
|
{ t: "paint", op: 1, r: 99999, pts: [[1, 2]] },
|
|
]);
|
|
assert.deepEqual(decodeTouches(text).items, [
|
|
{ t: "tap", op: 0, x: 11, y: 3 },
|
|
{ t: "paint", op: 1, r: BRUSH_RADIUS[0], pts: [[1, 3]] },
|
|
{ t: "paint", op: 1, r: BRUSH_RADIUS[1], pts: [[1, 2]] },
|
|
]);
|
|
});
|
|
|
|
test("reading stops at the server's caps", () => {
|
|
const many = Array.from({ length: TOUCH_MAX_ITEMS + 30 }, (_, index) => ({ t: "tap", op: 1, x: index, y: 1 }));
|
|
assert.equal(decodeTouches(JSON.stringify({ v: 1, key: "k", items: many })).items.length, TOUCH_MAX_ITEMS);
|
|
const long = { t: "paint", op: 1, r: 5, pts: Array.from({ length: TOUCH_MAX_POINTS }, (_, index) => [index, 1]) };
|
|
const two = decodeTouches(JSON.stringify({ v: 1, key: "k", items: [long, TAP] })).items;
|
|
assert.equal(two.length, 1, "the tap after 20,000 points no longer fits");
|
|
assert.equal(pointCount(two), TOUCH_MAX_POINTS);
|
|
});
|
|
|
|
test("touchFits says whether one more touch stays under the caps", () => {
|
|
const full = Array.from({ length: TOUCH_MAX_ITEMS }, () => TAP);
|
|
assert.equal(touchFits(full.slice(1), TAP), true);
|
|
assert.equal(touchFits(full, TAP), false);
|
|
const long = { t: "paint", op: 1, r: 5, pts: Array.from({ length: TOUCH_MAX_POINTS }, () => [1, 1]) };
|
|
assert.equal(touchFits([long], TAP), false);
|
|
});
|
|
|
|
test("the stamp is 32-bit FNV-1a, the same the server writes", () => {
|
|
assert.equal(stampOf(""), "811c9dc5");
|
|
assert.equal(stampOf("a"), "e40c292c");
|
|
assert.equal(stampOf("foobar"), "bf9cf968");
|
|
assert.equal(stampOf(undefined), "811c9dc5");
|
|
});
|
|
|
|
test("prefixStamps gives the stamp of every shorter list of touches", () => {
|
|
const items = [TAP, FIND, PAINT];
|
|
const stamps = prefixStamps("abc", items);
|
|
assert.equal(stamps.length, 3);
|
|
for (let count = 1; count <= items.length; count += 1) {
|
|
assert.equal(stamps[count - 1], stampOf(encodeTouches("abc", items.slice(0, count))));
|
|
}
|
|
assert.notEqual(prefixStamps("abd", items)[2], stamps[2], "another picture, another stamp");
|
|
});
|
|
|
|
test("a stroke takes a point only once the pointer has moved on", () => {
|
|
const points = [];
|
|
assert.equal(extendStroke(points, 0, 0, 5), true);
|
|
assert.equal(extendStroke(points, 3, 0, 5), false);
|
|
assert.equal(extendStroke(points, 5, 0, 5), true);
|
|
assert.deepEqual(points, [[0, 0], [5, 0]]);
|
|
});
|
|
|
|
test("thinning a stroke keeps its ends and its corners", () => {
|
|
const straight = Array.from({ length: 50 }, (_, index) => [index * 4, 10]);
|
|
assert.deepEqual(thinStroke(straight, 1), [[0, 10], [196, 10]]);
|
|
const corner = [[0, 0], [10, 0], [20, 0], [20, 10], [20, 20]];
|
|
assert.deepEqual(thinStroke(corner, 1), [[0, 0], [20, 0], [20, 20]]);
|
|
assert.deepEqual(thinStroke([[4, 4]], 1), [[4, 4]]);
|
|
});
|
|
|
|
test("a very long stroke is thinned to the most one stroke may hold", () => {
|
|
const wiggle = Array.from({ length: 4000 }, (_, index) => [index, index % 2 ? 40 : 0]);
|
|
const thin = thinStroke(wiggle, 1);
|
|
assert.equal(thin.length, STROKE_MAX_POINTS);
|
|
assert.deepEqual(thin[0], [0, 0]);
|
|
assert.deepEqual(thin[thin.length - 1], [3999, 40]);
|
|
});
|
|
|
|
test("grow is a square brush, and shrink takes it back", () => {
|
|
const dot = box(21, 21, 10, 10, 11, 11);
|
|
const grown = growMask(dot, 21, 21, 3);
|
|
assert.equal(total(grown), 49);
|
|
assert.equal(grown[7 * 21 + 7], 1);
|
|
assert.equal(grown[6 * 21 + 7], 0);
|
|
assert.deepEqual(shrinkMask(grown, 21, 21, 3), dot);
|
|
assert.equal(growMask(dot, 21, 21, 0), dot, "no growth hands the mask back as it is");
|
|
});
|
|
|
|
test("the picture's own edge does not eat into a shrunk mask", () => {
|
|
const left = box(10, 10, 0, 0, 5, 10);
|
|
const shrunk = shrinkMask(left, 10, 10, 2);
|
|
assert.equal(shrunk[0], 1, "the corner on the picture's edge stays");
|
|
assert.equal(shrunk[4], 0, "the side facing the rest of the picture moves in");
|
|
assert.equal(total(shrunk), 30);
|
|
});
|
|
|
|
test("fill holes closes a pinhole and leaves a real opening", () => {
|
|
const solid = box(40, 40, 5, 5, 35, 35);
|
|
solid[20 * 40 + 20] = 0;
|
|
assert.equal(total(fillHoles(solid, 40, 40)), 900);
|
|
|
|
const ring = blank(40, 40);
|
|
for (let y = 0; y < 40; y += 1) {
|
|
for (let x = 0; x < 40; x += 1) {
|
|
const distance = Math.hypot(x - 20, y - 20);
|
|
if (distance < 15 && distance > 8) ring[y * 40 + x] = 1;
|
|
}
|
|
}
|
|
const before = total(ring);
|
|
assert.equal(total(fillHoles(ring, 40, 40)), before, "the ring's middle is far over 1% of the ring");
|
|
assert.ok(total(fillHoles(ring, 40, 40, Infinity)) > before, "with no cap the middle is filled");
|
|
});
|
|
|
|
test("fill holes never fills what is open to the picture's edge, and copies", () => {
|
|
const cup = blank(20, 20);
|
|
for (let y = 5; y < 15; y += 1) { cup[y * 20 + 5] = 1; cup[y * 20 + 14] = 1; }
|
|
for (let x = 5; x < 15; x += 1) cup[14 * 20 + x] = 1;
|
|
const filled = fillHoles(cup, 20, 20, Infinity);
|
|
assert.deepEqual(filled, cup);
|
|
assert.notEqual(filled, cup, "a new array, so the picked area itself is never changed");
|
|
assert.equal(total(fillHoles(blank(8, 8), 8, 8)), 0);
|
|
});
|
|
|
|
test("soften keeps the mask between 0 and 1 and spreads its edge", () => {
|
|
const half = box(40, 40, 0, 0, 20, 40);
|
|
const soft = softenMask(half, 40, 40, 3);
|
|
assert.ok(soft instanceof Float32Array);
|
|
assert.ok(soft.every((value) => value >= 0 && value <= 1.000001));
|
|
assert.ok(soft[20 * 40 + 2] > 0.99, "far inside stays full");
|
|
assert.ok(soft[20 * 40 + 37] < 0.01, "far outside stays empty");
|
|
const edge = soft[20 * 40 + 20];
|
|
assert.ok(edge > 0.2 && edge < 0.5, `just outside the edge is partly on (${edge})`);
|
|
assert.deepEqual([...softenMask(half, 40, 40, 0)], [...half]);
|
|
});
|
|
|
|
test("smooth drops a speck and keeps the edge hard", () => {
|
|
const mask = box(60, 60, 10, 10, 50, 50);
|
|
mask[2 * 60 + 2] = 1;
|
|
const smooth = smoothMask(mask, 60, 60, 2);
|
|
assert.ok(smooth instanceof Uint8Array);
|
|
assert.equal(smooth[2 * 60 + 2], 0, "the lone pixel is gone");
|
|
assert.equal(smooth[30 * 60 + 30], 1);
|
|
assert.ok(smooth.every((value) => value === 0 || value === 1));
|
|
assert.equal(smoothMask(mask, 60, 60, 0), mask);
|
|
});
|
|
|
|
test("the edge steps run in the server's order: fill, smooth, grow, soften", () => {
|
|
const raw = box(60, 60, 20, 20, 40, 40);
|
|
raw[30 * 60 + 30] = 0;
|
|
const plain = composeEdge({ raw, width: 60, height: 60 });
|
|
assert.equal(total(plain), 399);
|
|
const filled = composeEdge({ raw, width: 60, height: 60, fill: true });
|
|
assert.equal(total(filled), 400);
|
|
// Grown first, the pinhole would be closed by the growth and never count
|
|
// as a hole; filled first, growing adds exactly the border.
|
|
const grown = composeEdge({ raw, width: 60, height: 60, fill: true, grow: 2 });
|
|
assert.equal(total(grown), 24 * 24);
|
|
const shrunk = composeEdge({ raw, width: 60, height: 60, fill: true, grow: -2 });
|
|
assert.equal(total(shrunk), 16 * 16);
|
|
const soft = composeEdge({ raw, width: 60, height: 60, fill: true, soften: 2 });
|
|
assert.ok(soft[20 * 60 + 18] > 0 && soft[20 * 60 + 18] < 1);
|
|
});
|
|
|
|
test("a setting that is on never rounds away on the small copy", () => {
|
|
assert.equal(previewPixels(0, 0.5), 0);
|
|
assert.equal(previewPixels(1, 0.2), 1);
|
|
assert.equal(previewPixels(-1, 0.2), -1);
|
|
assert.equal(previewPixels(24, 0.627), 15);
|
|
assert.equal(previewPixels(-24, 0.627), -15);
|
|
assert.equal(previewPixels(8, 1), 8);
|
|
});
|
|
|
|
test("maskShare is the part of the picture a mask covers", () => {
|
|
assert.equal(maskShare(box(10, 10, 0, 0, 5, 10)), 0.5);
|
|
assert.equal(maskShare(new Float32Array([0.5, 0.5, 0, 1])), 0.5);
|
|
assert.equal(maskShare(new Uint8Array(0)), 0);
|
|
});
|
|
|
|
test("a dab of paint is a disc, a stroke is the brush dragged along", () => {
|
|
const dab = paintStroke(blank(41, 41), 41, 41, [[20, 20]], 10, 1);
|
|
const area = total(dab);
|
|
assert.ok(Math.abs(area - Math.PI * 100) < 20, `a disc of radius 10 (${area})`);
|
|
const line = paintStroke(blank(100, 41), 100, 41, [[20, 20], [80, 20]], 10, 1);
|
|
assert.ok(Math.abs(total(line) - (Math.PI * 100 + 60 * 20)) < 60, `a capsule (${total(line)})`);
|
|
assert.equal(line[20 * 100 + 50], 1);
|
|
assert.equal(line[5 * 100 + 50], 0);
|
|
});
|
|
|
|
test("paint that runs off the picture is cut at its edge, and 0 takes away", () => {
|
|
const corner = paintStroke(blank(20, 20), 20, 20, [[-5, -5], [40, 40]], 3, 1);
|
|
assert.equal(corner[0], 1);
|
|
assert.equal(corner[19 * 20 + 19], 1);
|
|
const cleared = paintStroke(box(20, 20, 0, 0, 20, 20), 20, 20, [[10, 10]], 4, 0);
|
|
assert.equal(cleared[10 * 20 + 10], 0);
|
|
assert.equal(cleared[0], 1);
|
|
assert.deepEqual(paintStroke(blank(4, 4), 4, 4, [], 3, 1), blank(4, 4));
|
|
});
|
|
|
|
// Three pieces, one inside the other, and a fourth on the second plane.
|
|
function nested() {
|
|
const width = 30;
|
|
const height = 30;
|
|
const small = box(width, height, 12, 12, 16, 16);
|
|
const middle = box(width, height, 8, 8, 20, 20);
|
|
const large = box(width, height, 2, 2, 28, 28);
|
|
const far = box(width, height, 24, 0, 30, 2);
|
|
const planes = [new Uint8ClampedArray(width * height * 4), new Uint8ClampedArray(width * height * 4)];
|
|
const put = (mask, piece) => {
|
|
const rgba = planes[Math.floor(piece / PIECES_PER_PLANE)];
|
|
const bit = piece % PIECES_PER_PLANE;
|
|
for (let index = 0; index < mask.length; index += 1) {
|
|
if (mask[index]) rgba[index * 4 + Math.floor(bit / 8)] |= 1 << (bit % 8);
|
|
}
|
|
};
|
|
put(small, 0);
|
|
put(middle, 9);
|
|
put(large, 17);
|
|
put(far, 25);
|
|
for (const rgba of planes) for (let at = 3; at < rgba.length; at += 4) rgba[at] = 255;
|
|
return { width, height, small, middle, large, far, packed: packPlanes(planes, width, height) };
|
|
}
|
|
|
|
test("pieces come back out of the planes bit for bit", () => {
|
|
const { width, height, small, middle, large, far, packed } = nested();
|
|
assert.deepEqual(pieceMask(packed, 0), small);
|
|
assert.deepEqual(pieceMask(packed, 9), middle);
|
|
assert.deepEqual(pieceMask(packed, 17), large);
|
|
assert.deepEqual(pieceMask(packed, 25), far);
|
|
assert.equal(pieceMask(packed, 60), null);
|
|
const sizes = pieceSizes(packed, 26);
|
|
assert.equal(sizes[0], 16);
|
|
assert.equal(sizes[9], 144);
|
|
assert.equal(sizes[17], 26 * 26);
|
|
assert.equal(sizes[25], 12);
|
|
assert.equal(total(sizes), 16 + 144 + 676 + 12);
|
|
assert.equal(width * height, small.length);
|
|
});
|
|
|
|
test("the pointer lights the smallest piece that would still add something", () => {
|
|
const { width, small, middle, packed } = nested();
|
|
const sizes = pieceSizes(packed, 26);
|
|
const centre = 14 * width + 14;
|
|
const nothing = piecesInside(packed, 26, null);
|
|
assert.equal(pieceAt(packed, centre, { sizes, inside: nothing }), 0);
|
|
// The small one is picked: the same spot now lights the one around it.
|
|
assert.equal(pieceAt(packed, centre, { sizes, inside: piecesInside(packed, 26, small) }), 9);
|
|
assert.equal(pieceAt(packed, centre, { sizes, inside: piecesInside(packed, 26, middle) }), 17);
|
|
assert.equal(pieceAt(packed, 0, { sizes, inside: nothing }), -1, "nothing there");
|
|
assert.equal(pieceAt(packed, 1 * width + 25, { sizes, inside: nothing }), 25, "a piece on the second plane");
|
|
});
|
|
|
|
test("taking away, the pointer lights the smallest piece with picked area in it", () => {
|
|
const { width, small, large, packed } = nested();
|
|
const sizes = pieceSizes(packed, 26);
|
|
const centre = 14 * width + 14;
|
|
assert.equal(pieceAt(packed, centre, { sizes, inside: piecesInside(packed, 26, null), adding: false }), -1);
|
|
assert.equal(pieceAt(packed, centre, { sizes, inside: piecesInside(packed, 26, small), adding: false }), 0);
|
|
// Only the outer band is picked: the two inside it have nothing to give up.
|
|
const band = Uint8Array.from(large, (value, index) => (value && !pieceMask(packed, 9)[index] ? 1 : 0));
|
|
assert.equal(pieceAt(packed, centre, { sizes, inside: piecesInside(packed, 26, band), adding: false }), 17);
|
|
});
|
|
|
|
test("the tint has a line on the mask's edge and a dark hairline outside it", () => {
|
|
const width = 20;
|
|
const height = 20;
|
|
const mask = Float32Array.from(box(width, height, 5, 5, 15, 15));
|
|
const pixels = new Uint8ClampedArray(width * height * 4).fill(9);
|
|
paintTint(pixels, mask, width, height);
|
|
const at = (x, y) => [...pixels.slice((y * width + x) * 4, (y * width + x) * 4 + 4)];
|
|
assert.deepEqual(at(5, 5), [...OUTLINE, 255], "the mask's own edge is the line");
|
|
assert.deepEqual(at(10, 10).slice(0, 3), TINT, "inside is the tint");
|
|
assert.ok(at(10, 10)[3] > 100 && at(10, 10)[3] < 160);
|
|
assert.deepEqual(at(4, 10).slice(0, 3), [0, 0, 0], "one pixel out is the hairline");
|
|
assert.ok(at(4, 10)[3] > 0);
|
|
assert.deepEqual(at(1, 1), [0, 0, 0, 0], "the rest is clear");
|
|
});
|
|
|
|
test("nothing picked paints nothing", () => {
|
|
const pixels = new Uint8ClampedArray(16 * 4).fill(200);
|
|
paintTint(pixels, new Float32Array(16), 4, 4);
|
|
assert.ok(pixels.every((value) => value === 0));
|
|
});
|
|
|
|
test("the mask view is black and white, the cut-out covers what is not picked", () => {
|
|
const mask = new Float32Array([1, 0.5, 0, 0]);
|
|
const pixels = new Uint8ClampedArray(16);
|
|
paintCover(pixels, mask, 2, 2, "mask");
|
|
assert.deepEqual([...pixels.slice(0, 8)], [255, 255, 255, 255, 128, 128, 128, 255]);
|
|
assert.deepEqual([...pixels.slice(8, 12)], [0, 0, 0, 255]);
|
|
paintCover(pixels, mask, 2, 2, "cut");
|
|
assert.equal(pixels[3], 0, "the picked pixel shows the picture");
|
|
assert.equal(pixels[11], 255, "an unpicked pixel is covered");
|
|
assert.ok(pixels[7] > 100 && pixels[7] < 150, "a half-picked pixel is half covered");
|
|
});
|
|
|
|
test("the readout says what is picked in plain words", () => {
|
|
assert.equal(readoutText({}), "nothing picked");
|
|
assert.equal(readoutText({ count: 2, share: 0 }), "nothing picked");
|
|
assert.equal(readoutText({ count: 1, share: 0.043, only: "thing" }), "1 thing · 4% of the picture");
|
|
assert.equal(readoutText({ count: 1, share: 0.24, only: "under" }), "the thing under your paint · 24%");
|
|
assert.equal(readoutText({ count: 1, share: 0.03, only: "paint" }), "your paint, as it is · 3%");
|
|
assert.equal(readoutText({ count: 3, share: 0.27 }), "3 picks · 27% of the picture");
|
|
assert.equal(readoutText({ count: 1, share: 0.001, only: "thing" }), "1 thing · under 1% of the picture");
|
|
assert.equal(readoutText({ count: 4, share: 0.2, waiting: true }), "finding it");
|
|
});
|
|
|
|
// ---- named things ----------------------------------------------------------
|
|
|
|
const NAMED = { t: "name", op: 1, w: "jacket", x: 412, y: 900 };
|
|
|
|
test("a named touch survives the trip, and its word is cleaned", () => {
|
|
const text = encodeTouches("k", [NAMED, TAP]);
|
|
assert.deepEqual(decodeTouches(text).items, [NAMED, TAP]);
|
|
assert.equal(cleanWord(" fur collar "), "fur collar");
|
|
assert.equal(cleanWord("pants:16, (shoes)"), "pants 16 shoes", "the text reader's own signs are taken out");
|
|
assert.equal(cleanWord("x".repeat(200)).length, 60);
|
|
assert.deepEqual(decodeTouches(JSON.stringify({ v: 1, key: "k", items: [
|
|
{ t: "name", op: 0, w: " teddy bear ", x: 3.6, y: 9 },
|
|
{ t: "name", op: 1, w: ",:", x: 1, y: 1 },
|
|
{ t: "name", op: 1, w: "hat" },
|
|
] })).items, [{ t: "name", op: 0, w: "teddy bear", x: 4, y: 9 }]);
|
|
assert.equal(pointCount([NAMED, TAP, FIND]), 5, "a named touch is one point");
|
|
});
|
|
|
|
// A person (large), the jacket they wear (inside it), a button on the jacket
|
|
// found at a point, a wall behind everything, and a "dress" SAM 3 is not
|
|
// sure of that covers the jacket too.
|
|
function scene() {
|
|
const width = 40;
|
|
const height = 40;
|
|
const masks = {
|
|
button: box(width, height, 18, 18, 20, 20),
|
|
jacket: box(width, height, 12, 12, 26, 28),
|
|
dress: box(width, height, 12, 12, 26, 30),
|
|
person: box(width, height, 10, 4, 28, 36),
|
|
wall: box(width, height, 0, 0, 40, 34),
|
|
};
|
|
// In the order a server sends them: smallest first.
|
|
const order = ["button", "jacket", "dress", "person", "wall"];
|
|
const list = [
|
|
{ kind: "point", name: "", sure: 0 },
|
|
{ kind: "name", name: "jacket", sure: 0.96 },
|
|
{ kind: "name", name: "dress", sure: 0.7 },
|
|
{ kind: "name", name: "person", sure: 0.98 },
|
|
{ kind: "name", name: "wall", sure: 0.9 },
|
|
];
|
|
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
order.forEach((name, piece) => {
|
|
masks[name].forEach((value, index) => { if (value) rgba[index * 4 + Math.floor(piece / 8)] |= 1 << (piece % 8); });
|
|
});
|
|
for (let at = 3; at < rgba.length; at += 4) rgba[at] = 255;
|
|
const planes = packPlanes([rgba], width, height);
|
|
const sizes = pieceSizes(planes, list.length);
|
|
return { width, height, masks, list, planes, sizes, ranks: pieceRanks(list, sizes, width * height) };
|
|
}
|
|
|
|
test("pieces are offered sure names first, then points, then unsure names, backdrops last", () => {
|
|
const { width, planes, ranks } = scene();
|
|
// On the button: all five cover it.
|
|
assert.deepEqual(candidatesAt(planes, 19 * width + 19, ranks), [1, 3, 0, 2, 4], "jacket, person, the point piece, dress, wall");
|
|
// On the jacket beside the button.
|
|
assert.deepEqual(candidatesAt(planes, 14 * width + 14, ranks), [1, 3, 2, 4]);
|
|
// On the person's legs: no jacket there.
|
|
assert.deepEqual(candidatesAt(planes, 32 * width + 14, ranks), [3, 4]);
|
|
assert.deepEqual(candidatesAt(planes, 39 * width + 39, ranks), []);
|
|
assert.deepEqual(candidatesAt(planes, 19 * width + 19, null), [0, 1, 2, 3, 4], "without an order: smallest first");
|
|
});
|
|
|
|
test("the pointer lights the jacket, not the person, and goes on when it is picked", () => {
|
|
const { width, masks, planes, sizes, ranks } = scene();
|
|
const spot = 14 * width + 14;
|
|
const none = piecesInside(planes, 5, null);
|
|
assert.equal(pieceAt(planes, spot, { sizes, inside: none, ranks }), 1, "the jacket");
|
|
assert.equal(pieceAt(planes, spot, { sizes, inside: piecesInside(planes, 5, masks.jacket), ranks }), 3, "then the person");
|
|
assert.equal(pieceAt(planes, spot, { sizes, inside: piecesInside(planes, 5, masks.person), ranks }), 4, "then the wall");
|
|
assert.equal(pieceAt(planes, spot, { sizes, inside: piecesInside(planes, 5, masks.person), ranks, adding: false }), 1, "taking away starts with the jacket again");
|
|
});
|
|
|
|
test("a stroke means the piece most of it lies in", () => {
|
|
const { width, height, list, planes, sizes, ranks } = scene();
|
|
const pixels = width * height;
|
|
// Across the jacket, inside the person.
|
|
const across = paintStroke(blank(width, height), width, height, [[14, 20], [24, 20]], 1.5, 1);
|
|
assert.deepEqual(strokePieces(planes, list, sizes, pixels, across, ranks), { candidates: [1, 3, 2, 4], pair: null, pairAt: 0 });
|
|
// Down the legs: the person, then the wall.
|
|
const legs = paintStroke(blank(width, height), width, height, [[19, 30], [19, 33]], 1.5, 1);
|
|
assert.deepEqual(strokePieces(planes, list, sizes, pixels, legs, ranks).candidates, [3, 4]);
|
|
// In the empty corner: nothing.
|
|
const corner = paintStroke(blank(width, height), width, height, [[37, 37], [39, 39]], 1, 1);
|
|
assert.deepEqual(strokePieces(planes, list, sizes, pixels, corner, ranks), { candidates: [], pair: null, pairAt: 0 });
|
|
assert.deepEqual(strokePieces(planes, list, sizes, pixels, blank(width, height), ranks), { candidates: [], pair: null, pairAt: 0 });
|
|
});
|
|
|
|
test("a stroke across a pair takes both", () => {
|
|
// Two shoes side by side with a gap, each a sure name; a stroke crosses
|
|
// both and lies mostly in neither.
|
|
const width = 60;
|
|
const height = 20;
|
|
const left = box(width, height, 5, 5, 25, 15);
|
|
const right = box(width, height, 35, 5, 55, 15);
|
|
const list = [{ kind: "name", name: "shoe", sure: 0.92 }, { kind: "name", name: "shoe", sure: 0.9 }];
|
|
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
left.forEach((value, index) => { if (value) rgba[index * 4] |= 1; });
|
|
right.forEach((value, index) => { if (value) rgba[index * 4] |= 2; });
|
|
for (let at = 3; at < rgba.length; at += 4) rgba[at] = 255;
|
|
const planes = packPlanes([rgba], width, height);
|
|
const sizes = pieceSizes(planes, 2);
|
|
const ranks = pieceRanks(list, sizes, width * height);
|
|
const stroke = paintStroke(blank(width, height), width, height, [[8, 10], [52, 10]], 1.5, 1);
|
|
assert.deepEqual(strokePieces(planes, list, sizes, width * height, stroke, ranks), { candidates: [], pair: [0, 1], pairAt: 0 });
|
|
// Not a pair when SAM 3 is unsure of one of them.
|
|
const unsure = [{ kind: "name", name: "shoe", sure: 0.92 }, { kind: "name", name: "shoe", sure: 0.6 }];
|
|
assert.equal(strokePieces(planes, unsure, sizes, width * height, stroke, pieceRanks(unsure, sizes, width * height)).pair, null);
|
|
// A stroke on one shoe is that shoe alone.
|
|
const one = paintStroke(blank(width, height), width, height, [[8, 10], [22, 10]], 1.5, 1);
|
|
assert.deepEqual(strokePieces(planes, list, sizes, width * height, one, ranks), { candidates: [0], pair: null, pairAt: 0 });
|
|
});
|
|
|
|
test("both shoes come before the person standing in them", () => {
|
|
// A person with two shoes at the bottom; the stroke crosses both shoes
|
|
// and lies wholly in the person.
|
|
const width = 60;
|
|
const height = 60;
|
|
const masks = [
|
|
box(width, height, 10, 44, 26, 54), // left shoe
|
|
box(width, height, 34, 44, 50, 54), // right shoe
|
|
box(width, height, 8, 4, 52, 56), // person
|
|
box(width, height, 0, 40, 60, 60), // floor: under the shoes and between them
|
|
];
|
|
const list = [
|
|
{ kind: "name", name: "shoe", sure: 0.95 },
|
|
{ kind: "name", name: "shoe", sure: 0.92 },
|
|
{ kind: "name", name: "person", sure: 0.98 },
|
|
{ kind: "name", name: "floor", sure: 0.9 },
|
|
];
|
|
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
masks.forEach((mask, piece) => mask.forEach((value, index) => { if (value) rgba[index * 4] |= 1 << piece; }));
|
|
for (let at = 3; at < rgba.length; at += 4) rgba[at] = 255;
|
|
const planes = packPlanes([rgba], width, height);
|
|
const sizes = pieceSizes(planes, 4);
|
|
const overlaps = pieceOverlaps(planes, 4);
|
|
const ranks = pieceRanks(list, sizes, width * height, overlaps);
|
|
const wholes = pieceWholes(list, sizes, width * height, overlaps);
|
|
const stroke = paintStroke(blank(width, height), width, height, [[13, 49], [47, 49]], 1.5, 1);
|
|
const found = strokePieces(planes, list, sizes, width * height, stroke, ranks, wholes);
|
|
// The person and the floor each hold the whole stroke; the two shoes
|
|
// together hold most of it and are smaller than either.
|
|
assert.deepEqual(found.candidates, [3, 2], "the floor is smaller than the person here");
|
|
assert.deepEqual(found.pair, [1, 0].sort((a, b) => ranks[a] - ranks[b]));
|
|
assert.equal(found.pairAt, 0, "the pair comes first");
|
|
});
|
|
|
|
test("a stroke over a jacket that crosses its sleeve means the jacket", () => {
|
|
const width = 50;
|
|
const height = 50;
|
|
const masks = [
|
|
box(width, height, 10, 14, 18, 34), // sleeve
|
|
box(width, height, 20, 20, 30, 30), // a pocket SAM 3 calls a bag
|
|
box(width, height, 10, 14, 30, 34), // jacket
|
|
];
|
|
const list = [
|
|
{ kind: "name", name: "sleeve", sure: 0.93 },
|
|
{ kind: "name", name: "bag", sure: 0.9 },
|
|
{ kind: "name", name: "jacket", sure: 0.96 },
|
|
];
|
|
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
masks.forEach((mask, piece) => mask.forEach((value, index) => { if (value) rgba[index * 4] |= 1 << piece; }));
|
|
for (let at = 3; at < rgba.length; at += 4) rgba[at] = 255;
|
|
const planes = packPlanes([rgba], width, height);
|
|
const sizes = pieceSizes(planes, 3);
|
|
const overlaps = pieceOverlaps(planes, 3);
|
|
const ranks = pieceRanks(list, sizes, width * height, overlaps);
|
|
const wholes = pieceWholes(list, sizes, width * height, overlaps);
|
|
assert.deepEqual(Array.from(wholes), [2, -1, -1], "the sleeve is part of the jacket; a bag is no part word");
|
|
// Half on the sleeve, half on the pocket: without the part rule the two
|
|
// would be a pair that comes before the jacket.
|
|
const stroke = paintStroke(blank(width, height), width, height, [[11, 24], [28, 24]], 1.5, 1);
|
|
assert.deepEqual(strokePieces(planes, list, sizes, width * height, stroke, ranks, wholes), { candidates: [2], pair: null, pairAt: 0 });
|
|
assert.notEqual(strokePieces(planes, list, sizes, width * height, stroke, ranks).pair, null, "without the table of wholes they pair up");
|
|
});
|
|
|
|
test("a skirt comes before the outfit SAM 3 also calls a dress", () => {
|
|
const width = 40;
|
|
const height = 60;
|
|
const masks = [
|
|
box(width, height, 10, 30, 30, 50), // skirt: 400 of the dress's 700
|
|
box(width, height, 10, 15, 30, 50), // "dress": the top and the skirt
|
|
box(width, height, 8, 2, 32, 58), // person
|
|
];
|
|
const list = [
|
|
{ kind: "name", name: "skirt", sure: 0.98 },
|
|
{ kind: "name", name: "dress", sure: 0.89 },
|
|
{ kind: "name", name: "person", sure: 0.98 },
|
|
];
|
|
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
masks.forEach((mask, piece) => mask.forEach((value, index) => { if (value) rgba[index * 4] |= 1 << piece; }));
|
|
for (let at = 3; at < rgba.length; at += 4) rgba[at] = 255;
|
|
const planes = packPlanes([rgba], width, height);
|
|
const sizes = pieceSizes(planes, 3);
|
|
const ranks = pieceRanks(list, sizes, width * height, pieceOverlaps(planes, 3));
|
|
assert.deepEqual(candidatesAt(planes, 40 * width + 20, ranks), [0, 1, 2], "skirt, dress, person");
|
|
});
|
|
|
|
test("one named pick says its name on the node's edge", () => {
|
|
assert.equal(readoutText({ count: 1, share: 0.18, only: "thing", name: "jacket" }), "jacket \u00b7 18% of the picture");
|
|
assert.equal(readoutText({ count: 2, share: 0.3, name: "jacket" }), "2 picks \u00b7 30% of the picture");
|
|
});
|
|
|
|
test("a big part goes behind the thing it is part of, but never behind the person", () => {
|
|
// A person, the jacket on them, a sleeve that is a good third of the
|
|
// jacket, and a small badge on the jacket. All sure.
|
|
const width = 50;
|
|
const height = 50;
|
|
const masks = [
|
|
box(width, height, 22, 22, 24, 24), // badge: tiny
|
|
box(width, height, 10, 14, 18, 34), // sleeve: 160 of the jacket's 400
|
|
box(width, height, 10, 14, 30, 34), // jacket
|
|
box(width, height, 8, 4, 32, 46), // person
|
|
];
|
|
const list = [
|
|
{ kind: "name", name: "badge", sure: 0.9 },
|
|
{ kind: "name", name: "sleeve", sure: 0.93 },
|
|
{ kind: "name", name: "jacket", sure: 0.96 },
|
|
{ kind: "name", name: "person", sure: 0.98 },
|
|
];
|
|
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
masks.forEach((mask, piece) => mask.forEach((value, index) => { if (value) rgba[index * 4] |= 1 << piece; }));
|
|
for (let at = 3; at < rgba.length; at += 4) rgba[at] = 255;
|
|
const planes = packPlanes([rgba], width, height);
|
|
const sizes = pieceSizes(planes, 4);
|
|
const overlaps = pieceOverlaps(planes, 4);
|
|
assert.equal(overlaps[1 * 4 + 2], 160, "the sleeve lies in the jacket");
|
|
assert.equal(overlaps[2 * 4 + 1], 160);
|
|
assert.equal(overlaps[0 * 4 + 1], 0, "the badge is not on the sleeve");
|
|
const ranks = pieceRanks(list, sizes, width * height, overlaps);
|
|
// On the sleeve: the jacket first, then the sleeve, then the person.
|
|
assert.deepEqual(candidatesAt(planes, 20 * width + 12, ranks), [2, 1, 3]);
|
|
// On the badge: a small part is meant as itself.
|
|
assert.deepEqual(candidatesAt(planes, 23 * width + 23, ranks), [0, 2, 3]);
|
|
// The jacket is nearly half the person and still comes before it.
|
|
assert.deepEqual(candidatesAt(planes, 30 * width + 26, ranks), [2, 3]);
|
|
// Without the table of overlaps nothing is moved: smallest first.
|
|
assert.deepEqual(candidatesAt(planes, 20 * width + 12, pieceRanks(list, sizes, width * height)), [1, 2, 3]);
|
|
});
|
|
|
|
test("a pick of every thing of a name survives the trip", () => {
|
|
const text = encodeTouches("k", [{ t: "all", op: 1, w: " neon, sign " }, { t: "all", op: 0, w: "bird" }, { t: "all", op: 1, w: " ,: " }]);
|
|
assert.deepEqual(decodeTouches(text).items, [{ t: "all", op: 1, w: "neon sign" }, { t: "all", op: 0, w: "bird" }]);
|
|
assert.equal(touchFits([], { t: "all", op: 1, w: "bird" }), true);
|
|
});
|
|
|
|
test("each pick has a short name for its chip", () => {
|
|
assert.equal(chipLabel({ t: "name", op: 1, w: "jacket", x: 1, y: 2 }), "jacket");
|
|
assert.equal(chipLabel({ t: "name", op: 0, w: "cup", x: 1, y: 2 }), "\u2212 cup");
|
|
assert.equal(chipLabel({ t: "all", op: 1, w: "bird" }), "every bird");
|
|
assert.equal(chipLabel({ t: "all", op: 1, w: "bird" }, 5), "5 \u00d7 bird");
|
|
assert.equal(chipLabel({ t: "tap", op: 1, x: 1, y: 2 }), "thing");
|
|
assert.equal(chipLabel({ t: "find", op: 1, r: 4, pts: [[1, 2]] }), "stroke");
|
|
assert.equal(chipLabel({ t: "paint", op: 0, r: 4, pts: [[1, 2]] }), "\u2212 paint");
|
|
});
|
|
|
|
test("two touches are the same when they are written the same", () => {
|
|
const cup = { t: "name", op: 1, w: "cup", x: 10, y: 20 };
|
|
// A touch fresh from a click and the same one read back from the widget: the key order differs.
|
|
assert.equal(sameTouches([cup], decodeTouches(encodeTouches("k", [{ y: 20, x: 10, w: "cup", op: 1, t: "name" }])).items), true);
|
|
assert.equal(sameTouches([cup], [{ ...cup, op: 0 }]), false);
|
|
assert.equal(sameTouches([cup], [{ ...cup, w: "mug" }]), false);
|
|
assert.equal(sameTouches([cup], [cup, cup]), false);
|
|
assert.equal(sameTouches([], []), true);
|
|
const stroke = { t: "find", op: 1, r: 12, pts: [[1, 2], [3, 4]] };
|
|
assert.equal(sameTouches([stroke], [{ ...stroke, pts: [[1, 2], [3, 4]] }]), true);
|
|
assert.equal(sameTouches([stroke], [{ ...stroke, t: "paint" }]), false);
|
|
});
|
|
|
|
test("a chip is an area: a pick, or paint, with the brush marks that joined it", () => {
|
|
const paint = (op, g) => ({ t: "paint", op, r: 4, pts: [[1, 2]], ...(g ? { g } : {}) });
|
|
const cup = (g) => ({ t: "name", op: 1, w: "cup", x: 1, y: 2, ...(g ? { g } : {}) });
|
|
assert.deepEqual(chipGroups([]), []);
|
|
// Marks painted onto a pick carry its number and share its chip, wherever they are in the list.
|
|
assert.deepEqual(chipGroups([cup(1), paint(1, 2), paint(1, 1), paint(1, 2)]), [
|
|
{ g: 1, first: 0, at: [0, 2], paint: false, marks: 1, away: false },
|
|
{ g: 2, first: 1, at: [1, 3], paint: true, marks: 2, away: false },
|
|
]);
|
|
// Two picks of the same name are two chips.
|
|
assert.equal(chipGroups([cup(1), cup(2)]).length, 2);
|
|
});
|
|
|
|
test("a thing taken away has a chip of its own, a brush mark that erases has none", () => {
|
|
const erase = { t: "paint", op: 0, r: 4, pts: [[1, 2]] };
|
|
const cup = { t: "name", op: 1, w: "cup", x: 1, y: 2, g: 1 };
|
|
const things = [{ t: "tap", op: 0, x: 1, y: 2 }, { t: "name", op: 0, w: "face", x: 1, y: 2 }, { t: "all", op: 0, w: "hair" }, { t: "subject", op: 0 }, { t: "find", op: 0, r: 4, pts: [[1, 2]] }];
|
|
assert.deepEqual(chipGroups([erase, erase]), []);
|
|
assert.deepEqual(chipGroups([cup, erase, things[1], erase]), [
|
|
{ g: 1, first: 0, at: [0], paint: false, marks: 0, away: false },
|
|
{ g: 0, first: 2, at: [2], paint: false, marks: 0, away: true },
|
|
]);
|
|
assert.deepEqual(chipGroups(things).map((chip) => [chip.first, chip.away]), things.map((_, index) => [index, true]));
|
|
});
|
|
|
|
test("touches saved before chips had numbers are told apart as they were", () => {
|
|
const paint = (op) => ({ t: "paint", op, r: 4, pts: [[1, 2]] });
|
|
const cup = { t: "name", op: 1, w: "cup", x: 1, y: 2 };
|
|
// Marks that follow each other share a chip, rubbing out or not.
|
|
assert.deepEqual(chipGroups([paint(1), paint(1), paint(0), paint(1)]), [{ g: 0, first: 0, at: [0, 1, 3], paint: true, marks: 3, away: false }]);
|
|
// A pick between two runs of paint keeps them apart.
|
|
assert.deepEqual(chipGroups([paint(1), paint(1), cup, paint(0), paint(1)]).map((chip) => [chip.first, chip.at.length, chip.paint]), [
|
|
[0, 2, true], [2, 1, false], [4, 1, true],
|
|
]);
|
|
assert.equal(chipGroups([cup, cup, { t: "find", op: 1, r: 4, pts: [[1, 2]] }]).length, 3);
|
|
});
|
|
|
|
test("a chip gets a number the moment something joins it", () => {
|
|
const items = [{ t: "name", op: 1, w: "cup", x: 1, y: 2 }, { t: "paint", op: 1, r: 4, pts: [[1, 2]], g: 7 }];
|
|
assert.equal(nextGroup(items), 8);
|
|
assert.equal(nextGroup(items, 12), 13, "a number that is spoken for is passed over");
|
|
assert.equal(nextGroup([]), 1);
|
|
const [cup, marks] = chipGroups(items);
|
|
const numbered = numberChip(items, cup);
|
|
assert.equal(numbered.g, 8);
|
|
assert.deepEqual(numbered.items.map((item) => item.g), [8, 7]);
|
|
assert.equal(items[0].g, undefined, "the list it was given is left alone");
|
|
const same = numberChip(items, marks);
|
|
assert.equal(same.g, 7);
|
|
assert.equal(same.items, items, "a chip with a number keeps it");
|
|
});
|
|
|
|
test("the chip a touch is in is kept, and only for a touch that adds", () => {
|
|
assert.deepEqual(cleanItem({ t: "tap", op: 1, x: 1, y: 2, g: 3 }), { t: "tap", op: 1, x: 1, y: 2, g: 3 });
|
|
assert.deepEqual(cleanItem({ t: "name", op: 1, w: "cup", x: 1, y: 2, q: 1, g: 3 }), { t: "name", op: 1, w: "cup", x: 1, y: 2, q: 1, g: 3 });
|
|
assert.deepEqual(cleanItem({ t: "all", op: 1, w: "cup", g: 3 }), { t: "all", op: 1, w: "cup", g: 3 });
|
|
assert.deepEqual(cleanItem({ t: "subject", op: 1, g: 3 }), { t: "subject", op: 1, g: 3 });
|
|
assert.deepEqual(cleanItem({ t: "paint", op: 1, r: 4, pts: [[1, 2]], g: 3 }), { t: "paint", op: 1, r: 4, pts: [[1, 2]], g: 3 });
|
|
// Taking away belongs to no chip.
|
|
assert.deepEqual(cleanItem({ t: "paint", op: 0, r: 4, pts: [[1, 2]], g: 3 }), { t: "paint", op: 0, r: 4, pts: [[1, 2]] });
|
|
for (const g of [0, -1, 1.5, "2", null, GROUP_MAX + 1]) {
|
|
assert.equal(cleanItem({ t: "tap", op: 1, x: 1, y: 2, g }).g, undefined, `${g} is no chip`);
|
|
}
|
|
// A touch written by an older version reads back exactly as it was.
|
|
const old = { t: "paint", op: 1, r: 4, pts: [[1, 2]] };
|
|
assert.deepEqual(cleanItem(old), old);
|
|
assert.equal(encodeTouches("k", [old]), '{"v":1,"key":"k","items":[{"t":"paint","op":1,"r":4,"pts":[[1,2]]}]}');
|
|
});
|
|
|
|
test("a brush mark keeps how soft its brush was", () => {
|
|
assert.equal(cleanItem({ t: "paint", op: 1, r: 4, pts: [[1, 2]], s: 40 }).s, 40);
|
|
assert.equal(cleanItem({ t: "paint", op: 0, r: 4, pts: [[1, 2]], s: 250 }).s, 100);
|
|
assert.equal(cleanItem({ t: "paint", op: 1, r: 4, pts: [[1, 2]], s: 0 }).s, undefined, "a hard brush has none");
|
|
assert.equal(cleanItem({ t: "paint", op: 1, r: 4, pts: [[1, 2]], s: "soft" }).s, undefined);
|
|
assert.equal(cleanItem({ t: "find", op: 1, r: 4, pts: [[1, 2]], s: 40 }).s, undefined, "a stroke that finds a thing has no edge of its own");
|
|
});
|
|
|
|
test("the same picks are the same touches, whichever chip they are in", () => {
|
|
const cup = { t: "name", op: 1, w: "cup", x: 1, y: 2 };
|
|
assert.equal(sameTouches([{ ...cup, g: 4 }], [cup]), true);
|
|
assert.equal(sameTouches([{ ...cup, g: 4 }], [{ ...cup, g: 9 }]), true);
|
|
assert.equal(sameTouches([{ ...cup, g: 4 }], [{ ...cup, op: 0 }]), false);
|
|
});
|
|
|
|
test("a touch that takes away with nothing before it is dropped", () => {
|
|
const add = { t: "tap", op: 1, x: 1, y: 2, g: 1 };
|
|
const away = { t: "paint", op: 0, r: 4, pts: [[1, 2]] };
|
|
assert.deepEqual(dropIdleAways([away, away, add, away]), [add, away]);
|
|
assert.deepEqual(dropIdleAways([away]), []);
|
|
assert.deepEqual(dropIdleAways([add]), [add]);
|
|
});
|
|
|
|
test("the area of a touch is known by what it picks, not by what it is used for", () => {
|
|
const cup = { t: "name", op: 1, w: "cup", x: 1, y: 2, g: 4 };
|
|
assert.equal(areaKey(cup), areaKey({ ...cup, op: 0, g: undefined, q: 1 }));
|
|
assert.notEqual(areaKey(cup), areaKey({ ...cup, x: 2 }));
|
|
assert.notEqual(areaKey({ t: "paint", op: 1, r: 4, pts: [[1, 2]] }), areaKey({ t: "paint", op: 1, r: 4, pts: [[1, 2]], s: 50 }));
|
|
assert.notEqual(areaKey({ t: "paint", op: 1, r: 4, pts: [[1, 2]] }), areaKey({ t: "find", op: 1, r: 4, pts: [[1, 2]] }));
|
|
});
|
|
|
|
// ---- a soft brush -------------------------------------------------------------
|
|
|
|
test("a hard brush mark is on or off, a soft one fades", () => {
|
|
const [width, height] = [120, 80];
|
|
const points = [[30, 40], [90, 40]];
|
|
const hard = brushStroke(width, height, points, 10, 0);
|
|
assert.ok(hard instanceof Uint8Array);
|
|
assert.deepEqual(hard, paintStroke(new Uint8Array(width * height), width, height, points, 10, 1));
|
|
const soft = brushStroke(width, height, points, 10, 100);
|
|
assert.ok(soft instanceof Float32Array);
|
|
const at = (x, y) => soft[y * width + x];
|
|
assert.ok(at(60, 40) > 0.9, "solid along the middle");
|
|
assert.ok(at(60, 34) > 0.25 && at(60, 34) < 0.85, "half way out it is partly there");
|
|
assert.ok(at(60, 28) < 0.1, "little is left at the ring");
|
|
assert.equal(at(60, 10), 0, "and nothing far off");
|
|
// Softer is wider at the faint end and thinner at the strong end.
|
|
const softer = brushStroke(width, height, points, 10, 50);
|
|
assert.ok(softer[36 * width + 60] > at(60, 36));
|
|
});
|
|
|
|
test("a soft mark that runs off the picture keeps its fade at the edge", () => {
|
|
const soft = brushStroke(60, 60, [[-30, 30], [30, 30]], 12, 100);
|
|
// The stroke goes on past the left edge, so the edge pixel is as strong as the middle of the line.
|
|
assert.ok(Math.abs(soft[30 * 60] - soft[30 * 60 + 15]) < 0.02);
|
|
assert.ok(soft[30 * 60] > 0.9);
|
|
});
|
|
|
|
test("joining areas: on/off stays on/off, anything soft makes it soft", () => {
|
|
const one = Uint8Array.from([1, 1, 0, 0]);
|
|
const other = Uint8Array.from([1, 0, 1, 0]);
|
|
assert.deepEqual(joinMasks(one, other, 1), Uint8Array.from([1, 1, 1, 0]));
|
|
assert.deepEqual(joinMasks(one, other, 0), Uint8Array.from([0, 1, 0, 0]));
|
|
const soft = Float32Array.from([0.5, 0.5, 0.5, 0.5]);
|
|
assert.deepEqual([...joinMasks(one, soft, 1)], [1, 1, 0.5, 0.5]);
|
|
assert.deepEqual([...joinMasks(one, soft, 0)], [0.5, 0.5, 0, 0]);
|
|
assert.deepEqual([...joinMasks(soft, other, 0)], [0, 0.5, 0, 0.5]);
|
|
assert.equal(maskOn(joinMasks(one, one, 0)), false);
|
|
assert.equal(maskOn(soft), true);
|
|
assert.deepEqual(hardMask(Float32Array.from([0.49, 0.5, 1, 0])), Uint8Array.from([0, 1, 1, 0]));
|
|
assert.equal(sharedPixels(Uint8Array.from([1, 1, 0]), Float32Array.from([0.6, 0.4, 1])), 1);
|
|
});
|
|
|
|
test("the edge settings give the same mask whether the pick is kept on/off or as numbers", () => {
|
|
const [width, height] = [90, 70];
|
|
const raw = paintStroke(new Uint8Array(width * height), width, height, [[20, 20], [70, 50], [30, 55]], 9, 1);
|
|
// A hole and a ragged bit, so Fill and Smooth have work.
|
|
for (const index of [35 * width + 45, 35 * width + 46, 36 * width + 45]) raw[index] = 0;
|
|
raw[8 * width + 20] = 1;
|
|
const numbers = Float32Array.from(raw);
|
|
for (const settings of [{}, { grow: 5 }, { grow: -3 }, { smooth: 4 }, { fill: true }, { soften: 2.5 }, { fill: true, smooth: 3, grow: 4, soften: 3 }]) {
|
|
const hard = composeEdge({ raw, width, height, ...settings });
|
|
const soft = composeEdge({ raw: numbers, width, height, ...settings });
|
|
for (let index = 0; index < hard.length; index += 1) {
|
|
assert.ok(Math.abs(hard[index] - soft[index]) < 1e-6, `${JSON.stringify(settings)} differs at ${index}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("Grow and Shrink move a soft edge and keep it soft", () => {
|
|
const [width, height] = [160, 120];
|
|
const soft = brushStroke(width, height, [[80, 60]], 40, 100);
|
|
const grown = composeEdge({ raw: soft, width, height, grow: 6 });
|
|
const shrunk = composeEdge({ raw: soft, width, height, grow: -6 });
|
|
const row = (mask) => Array.from({ length: width }, (_, x) => mask[60 * width + x]);
|
|
const half = (mask) => row(mask).filter((value) => value >= 0.5).length;
|
|
// Six pixels on each side, give or take the pixel the half line falls in.
|
|
assert.ok(Math.abs(half(grown) - half(soft) - 12) <= 2, `grew by ${half(grown) - half(soft)}`);
|
|
assert.ok(Math.abs(half(soft) - half(shrunk) - 12) <= 2, `shrank by ${half(soft) - half(shrunk)}`);
|
|
assert.ok(row(grown).some((value) => value > 0.05 && value < 0.95), "the fade is still there");
|
|
});
|
|
|
|
// ---- fine edges ---------------------------------------------------------------
|
|
|
|
test("Fine edges is on only while the input holds a model file", () => {
|
|
for (const value of [undefined, null, "", " ", "off", " off "]) assert.equal(fineChoice(value), "", JSON.stringify(value));
|
|
assert.equal(fineChoice("model.safetensors"), "model.safetensors");
|
|
assert.equal(fineChoice(" sub/model.safetensors "), "sub/model.safetensors");
|
|
});
|
|
|
|
test("the Fine edges choices read off and on, and by name only when there are several files", () => {
|
|
assert.equal(fineEdgeLabel("off", ["off", "model.safetensors"]), "off");
|
|
assert.equal(fineEdgeLabel("model.safetensors", ["off", "model.safetensors"]), "on");
|
|
// A workflow that names a file this machine does not have: still "on", so it reads as what was chosen.
|
|
assert.equal(fineEdgeLabel("model.safetensors", ["off"]), "on");
|
|
const several = ["off", "small/model.safetensors", "vitmatte_base.safetensors"];
|
|
assert.equal(fineEdgeLabel("small/model.safetensors", several), "model");
|
|
assert.equal(fineEdgeLabel("vitmatte_base.safetensors", several), "vitmatte_base");
|
|
assert.equal(fineEdgeLabel("off", several), "off");
|
|
});
|
|
|
|
test("the line of settings a fine edge was made with is the one the node writes", () => {
|
|
// The same strings as fine_made in nodes/node_mask_by_touch.py (tests/test_mask_by_touch.py holds that side).
|
|
assert.equal(fineMade({ grow: 0, soften: 0, smooth: 0, fill: true, invert: false, fine: "model.safetensors" }), "0|0.000|0|1|0|model.safetensors");
|
|
assert.equal(fineMade({ grow: 12, soften: 0.5, smooth: 3, fill: false, invert: true, fine: "a/b.safetensors" }), "12|0.500|3|0|1|a/b.safetensors");
|
|
assert.notEqual(fineMade({ grow: 1, fine: "m" }), fineMade({ grow: 2, fine: "m" }), "any setting that changes the mask changes the line");
|
|
assert.notEqual(fineMade({ soften: 1, fine: "m" }), fineMade({ soften: 1.5, fine: "m" }));
|
|
assert.notEqual(fineMade({ invert: true, fine: "m" }), fineMade({ invert: false, fine: "m" }));
|
|
});
|
|
|
|
test("a fine edge's mask is its picture's see-through channel", () => {
|
|
const rgba = new Uint8ClampedArray([10, 20, 30, 0, 40, 50, 60, 128, 70, 80, 90, 255]);
|
|
assert.deepEqual([...fineMask(rgba)].map((value) => Math.round(value * 1000) / 1000), [0, 0.502, 1]);
|
|
});
|
|
|
|
test("with clean colours the cut-out is drawn whole, and nothing of the picture shows through", () => {
|
|
const mask = new Float32Array([1, 0.5, 0, 1]);
|
|
const colours = new Uint8ClampedArray([200, 0, 0, 255, 200, 0, 0, 128, 0, 0, 0, 0, 0, 100, 0, 255]);
|
|
const pixels = paintCover(new Uint8ClampedArray(16), mask, 2, 2, "cut", colours);
|
|
assert.deepEqual([...pixels.slice(0, 4)], [200, 0, 0, 255], "all the thing: its clean colour, solid");
|
|
assert.equal(pixels[7], 255, "half see-through: still solid, the chequer mixed in under it");
|
|
assert.ok(pixels[4] > 100 && pixels[4] < 200 && pixels[5] > 0, "half the clean colour, half the chequer");
|
|
assert.equal(pixels[11], 255, "not the thing: the chequer, solid");
|
|
assert.ok(pixels[8] < 60 && pixels[9] < 60 && pixels[10] < 60);
|
|
// Without clean colours it is a cover with a hole, as before: the picture shows through where the mask is.
|
|
const plain = paintCover(new Uint8ClampedArray(16), mask, 2, 2, "cut");
|
|
assert.deepEqual([plain[3], plain[7], plain[11]], [0, 128, 255]);
|
|
// The mask view does not care about colours.
|
|
assert.deepEqual([...paintCover(new Uint8ClampedArray(16), mask, 2, 2, "mask", colours).slice(4, 8)], [128, 128, 128, 255]);
|
|
});
|
|
|
|
// ---- the subject, the background, and names SAM 3 is not sure of ---------------
|
|
|
|
test("the subject and the background are picks without a point or a word", () => {
|
|
const text = encodeTouches("k", [{ t: "subject", op: 1, x: 5, w: "junk" }, { t: "background", op: 0 }]);
|
|
assert.deepEqual(decodeTouches(text).items, [{ t: "subject", op: 1 }, { t: "background", op: 0 }]);
|
|
assert.equal(chipLabel({ t: "subject", op: 1 }), "subject");
|
|
assert.equal(chipLabel({ t: "background", op: 0 }), "\u2212 background");
|
|
assert.equal(touchFits([], { t: "subject", op: 1 }), true);
|
|
});
|
|
|
|
test("a word that was not shown is kept quiet: picked, saved, and called a thing", () => {
|
|
const quiet = { t: "name", op: 1, w: "foot", x: 10, y: 20, q: 1 };
|
|
const loud = { t: "name", op: 1, w: "hair", x: 10, y: 20 };
|
|
const round = decodeTouches(encodeTouches("k", [quiet, loud])).items;
|
|
assert.deepEqual(round, [quiet, loud], "the mark is saved, and only where it is set");
|
|
assert.equal(chipLabel(quiet), "thing");
|
|
assert.equal(chipLabel({ ...quiet, op: 0 }), "\u2212 thing");
|
|
assert.equal(chipLabel(loud), "hair");
|
|
assert.deepEqual(decodeTouches(encodeTouches("k", [{ ...loud, q: 0 }, { ...loud, q: "yes" }])).items, [loud, loud], "anything but 1 is not the mark");
|
|
assert.equal(sameTouches([quiet], [loud]), false);
|
|
});
|
|
|
|
test("a name is shown only when SAM 3 is sure of it, and surer still for a big thing", () => {
|
|
// The numbers come from 1137 names on 22 pictures: under 0.8 one name in three was wrong.
|
|
assert.equal(nameShown(0.79, 0.05), false);
|
|
assert.equal(nameShown(0.8, 0.05), true);
|
|
assert.equal(nameShown(0.99, 0.05), true);
|
|
// A big thing: "mirror" at 0.81 and "poster" at 0.898 were both wrong, the right big ones all 0.958 or more.
|
|
assert.equal(nameShown(0.898, 0.95), false);
|
|
assert.equal(nameShown(0.89, 0.3), false);
|
|
assert.equal(nameShown(0.9, 0.3), true);
|
|
assert.equal(nameShown(0.85, 0.29), true);
|
|
assert.equal(nameShown(undefined, undefined), false);
|
|
});
|
|
|
|
test("the pack's own picture loaders are the ones the node runs by itself", () => {
|
|
assert.deepEqual([...OWN_LOADERS].sort(), ["AUSBOSS_NODES_ImageCropRotatePad", "AUSBOSS_NODES_LoadImage", "AUSBOSS_NODES_LoadImagePad"]);
|
|
// A core Load Image is not in the list: its file is the picture, and the node shows it without a run.
|
|
assert.equal(OWN_LOADERS.has("LoadImage"), false);
|
|
});
|
|
|
|
test("a loader's file and settings make one text, and another text means another picture", () => {
|
|
const widgets = (over = {}) => [
|
|
{ name: "image", value: "photo.png" },
|
|
{ name: "rotation_degrees", value: 0 },
|
|
{ name: "crop_width", value: 0 },
|
|
{ name: "resize_to_megapixels", value: false },
|
|
// What is only drawn carries no setting: a panel, a card, a button.
|
|
{ name: "ausboss_panel", value: "", serialize: false },
|
|
{ name: "upload", value: {} },
|
|
{ value: 3 },
|
|
null,
|
|
].map((widget) => (widget && over[widget.name] !== undefined ? { ...widget, value: over[widget.name] } : widget));
|
|
const base = loaderSignature(widgets());
|
|
assert.equal(base, '[["image","photo.png"],["rotation_degrees",0],["crop_width",0],["resize_to_megapixels",false]]');
|
|
assert.equal(loaderSignature(widgets()), base);
|
|
assert.notEqual(loaderSignature(widgets({ image: "other.png" })), base);
|
|
assert.notEqual(loaderSignature(widgets({ crop_width: 512 })), base);
|
|
assert.notEqual(loaderSignature(widgets({ rotation_degrees: 90 })), base);
|
|
assert.equal(loaderSignature(undefined), "[]");
|
|
});
|
|
|
|
test("the node asks for its own loader's picture only in answer to the person, never while a workflow opens", () => {
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const stage = readFileSync(join(root, "js", "shared", "touch_stage.mjs"), "utf-8");
|
|
const show = stage.slice(stage.indexOf("function showOwnPicture"), stage.indexOf("function followOwnLoader"));
|
|
// the same run the Show picture button asks for
|
|
assert.match(show, /requestRun\(face, "scope"\)/);
|
|
// never with the look-on-button setting, never over picks that wait for their own press
|
|
assert.match(show, /face\.host\.lookOnButton\?\.\(\)/);
|
|
assert.match(show, /face\.pic && face\.held > 0/);
|
|
const follow = stage.slice(stage.indexOf("function followOwnLoader"), stage.indexOf("const OTHER_PICTURE"));
|
|
assert.match(follow, /!face\.host\.loading\?\.\(\)/);
|
|
assert.match(stage, /if \(!face\.host\.loading\?\.\(\)\) followOwnLoader\(face, true\)/);
|
|
const host = readFileSync(join(root, "js", "mask_by_touch", "index.js"), "utf-8");
|
|
assert.match(host, /loading: \(\) => Boolean\(app\.configuringGraph\)/);
|
|
});
|
|
|
|
test("a look at another picture goes behind the look at the old one, which ends it", () => {
|
|
const look = (epoch, queued = true) => ({ ask: "scope", epoch, queued });
|
|
// nothing on its way: anything is sent
|
|
assert.equal(goesBehind([], "scope", 1), true);
|
|
assert.equal(goesBehind(undefined, "look", 1), true);
|
|
// a click never waits for a look
|
|
assert.equal(goesBehind([look(1)], "look", 1), true);
|
|
// a second look at the same picture waits for the first
|
|
assert.equal(goesBehind([look(1)], "scope", 1), false);
|
|
// another picture was chosen while the first was looked at: its look is sent at once
|
|
assert.equal(goesBehind([look(1)], "scope", 2), true);
|
|
// and again for a third picture, behind the two that are now old
|
|
assert.equal(goesBehind([look(1), look(2)], "scope", 3), true);
|
|
// but not behind a look at the picture that is on the node now
|
|
assert.equal(goesBehind([look(1), look(2)], "scope", 2), false);
|
|
// a look ComfyUI does not have yet cannot be ended by a job behind it
|
|
assert.equal(goesBehind([look(1, false)], "scope", 2), false);
|
|
assert.equal(goesBehind([look(1, false)], "look", 1), false);
|
|
// a run that is no look does not give way
|
|
assert.equal(goesBehind([{ ask: "look", epoch: 1, queued: true }], "scope", 2), false);
|
|
assert.equal(goesBehind([{ ask: "look", epoch: 1, queued: true }], "look", 2), false);
|
|
});
|
|
|
|
test("both kinds of loader send the new picture's look without waiting for the old one", () => {
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const stage = readFileSync(join(root, "js", "shared", "touch_stage.mjs"), "utf-8");
|
|
const run = stage.slice(stage.indexOf("function requestRun"), stage.indexOf("function settle"));
|
|
assert.match(run, /goesBehind\(face\.flying, ask, face\.sourceEpoch\)/);
|
|
// the pack's own loaders: the picture counts as asked for only once its run was really sent
|
|
const show = stage.slice(stage.indexOf("function showOwnPicture"), stage.indexOf("function followOwnLoader"));
|
|
assert.match(show, /if \(requestRun\(face, "scope"\)\) face\.ownSeen = seen/);
|
|
assert.doesNotMatch(show, /if \(face\.inFlight\) \{\s*followOwnLoader/);
|
|
// a plain Load Image: another file while a look is on its way sends this file's look behind it
|
|
const source = stage.slice(stage.indexOf("function syncSource"), stage.indexOf("function imageOrigin"));
|
|
assert.match(source, /if \(face\.flying\.length\) requestRun\(face, "look"\)/);
|
|
});
|
|
|
|
test("while the node still looks at a picture it says so, a lit thing is taken at once, and one click can wait", () => {
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const stage = readFileSync(join(root, "js", "shared", "touch_stage.mjs"), "utf-8");
|
|
const wait = stage.slice(stage.indexOf("function waitForLook"), stage.indexOf("function dropPending"));
|
|
// what lights up is what a click takes: a click on a lit thing never waits, whatever its name is worth
|
|
assert.match(wait, /if \(pieceUnder\(face, \[x, y\], true\) >= 0\) \{[\s\S]*?return false;\s*\}/);
|
|
assert.ok(wait.indexOf("pieceUnder(face, [x, y], true)") < wait.indexOf("face.pending.push("));
|
|
// where nothing is lit, one click waits at a time: a second one is answered by the message, never added
|
|
assert.match(wait, /if \(face\.pending\.length\) \{\s*nudge\(face\);\s*return true;\s*\}/);
|
|
assert.equal((wait.match(/face\.pending\.push\(/g) ?? []).length, 1);
|
|
// hovering is not held back while it looks
|
|
const hover = stage.slice(stage.indexOf("function refreshHover"), stage.indexOf("const PAST_KEPT"));
|
|
assert.doesNotMatch(hover, /notReady/);
|
|
// a click on a lit thing does not rush the click that still waits
|
|
assert.match(stage, /if \(!notReady\(face\)\) flushPending\(face\);\s*pickAt\(face, x, y, op\);/);
|
|
// the message follows every change of state and of the line under the picture
|
|
const looking = stage.slice(stage.indexOf("function syncLooking"), stage.indexOf("function nudge"));
|
|
assert.match(looking, /notReady\(face\) && \(face\.inFlight \|\| waits( \|\| paused)?\)/);
|
|
// Said on the button under the picture, never on the picture itself.
|
|
assert.match(looking, /Looking at the picture/);
|
|
assert.match(looking, /Click to pause/);
|
|
assert.match(looking, /Paused at/);
|
|
assert.match(stage, /your click is in/);
|
|
assert.doesNotMatch(stage, /frame\.append\([^)]*look/);
|
|
assert.match(stage.slice(stage.indexOf("function syncReadout"), stage.indexOf("function removeChip")), /syncLooking\(face\)/);
|
|
assert.match(stage.slice(stage.indexOf("function syncState"), stage.indexOf("function setNote")), /syncLooking\(face\)/);
|
|
});
|
|
|
|
test("picks the node moved onto a new picture are taken only while nothing was picked since", () => {
|
|
// Issue 301: New picture on keep picks. The node moves the picks and sends
|
|
// the stamp of the ones it moved them from.
|
|
const before = encodeTouches("320x240-aa", [{ t: "tap", op: 1, x: 100, y: 50 }]);
|
|
const after = encodeTouches("160x120-bb", [{ t: "tap", op: 1, x: 50, y: 25 }]);
|
|
const note = { key: "160x120-bb", moved: { from: stampOf(before), touches: after } };
|
|
assert.equal(movedTouches(note, before), after, "the widget still holds what was moved: take the moved picks");
|
|
const since = encodeTouches("320x240-aa", [{ t: "tap", op: 1, x: 100, y: 50 }, { t: "tap", op: 1, x: 9, y: 9 }]);
|
|
assert.equal(movedTouches(note, since), null, "a pick made since is kept");
|
|
assert.equal(movedTouches({ key: "160x120-bb" }, before), null, "nothing was moved");
|
|
assert.equal(movedTouches({ moved: { from: 7, touches: after } }, before), null, "a note that is not this shape moves nothing");
|
|
assert.equal(movedTouches(null, before), null);
|
|
});
|
|
|
|
test("taking away from a spot that is not picked never offers a thing that holds a whole pick", () => {
|
|
const { width, masks, planes, sizes, ranks } = scene();
|
|
// The jacket is picked. The pointer is on the person's legs, which are not.
|
|
const inside = piecesInside(planes, 5, masks.jacket);
|
|
const holds = piecesHolding(planes, 5, [masks.jacket]);
|
|
assert.deepEqual([...holds], [0, 1, 1, 1, 1], "the jacket, the dress over it, the person and the wall hold all of it");
|
|
const legs = 32 * width + 14;
|
|
assert.equal(pieceAt(planes, legs, { sizes, inside, ranks, adding: false }), 3, "as before, when nothing is said about the spot");
|
|
assert.equal(pieceAt(planes, legs, { sizes, inside, ranks, adding: false, here: false, holds }), -1, "not the person: the jacket would go with it");
|
|
// On the jacket itself the spot is picked: the jacket is offered.
|
|
const onJacket = 14 * width + 14;
|
|
assert.equal(pieceAt(planes, onJacket, { sizes, inside, ranks, adding: false, here: true, holds }), 1);
|
|
// The person is picked, and the pointer is on the jacket: the jacket is a part of the pick, and is offered.
|
|
const person = piecesInside(planes, 5, masks.person);
|
|
const personHolds = piecesHolding(planes, 5, [masks.person]);
|
|
assert.equal(pieceAt(planes, onJacket, { sizes, inside: person, ranks, adding: false, here: true, holds: personHolds }), 1);
|
|
// A thing a pick only laps over is still offered from a spot that is not picked.
|
|
const lap = Uint8Array.from(masks.person, (value, index) => (value && Math.floor(index / width) >= 30 ? 1 : 0));
|
|
const lapInside = piecesInside(planes, 5, lap);
|
|
const lapHolds = piecesHolding(planes, 5, [masks.wall]);
|
|
assert.equal(lapHolds[3], 0, "the person does not hold a pick of the wall");
|
|
assert.equal(pieceAt(planes, onJacket + 0, { sizes, inside: lapInside, ranks, adding: false, here: false, holds: lapHolds }), 3, "the person has picked area in it and holds no whole pick");
|
|
// Adding is not changed by any of it.
|
|
assert.equal(pieceAt(planes, legs, { sizes, inside, ranks, here: false, holds }), 3);
|
|
assert.deepEqual([...piecesHolding(planes, 5, [null, new Uint8Array(masks.jacket.length)])], [0, 0, 0, 0, 0], "no area, nothing held");
|
|
});
|
|
|
|
test("a typed name is added or taken away by the sign, and a sign in the text wins", () => {
|
|
assert.deepEqual(typedName("face"), { word: "face", add: true });
|
|
assert.deepEqual(typedName("face", true), { word: "face", add: false });
|
|
assert.deepEqual(typedName("-face"), { word: "face", add: false });
|
|
assert.deepEqual(typedName(" - fur collar "), { word: "fur collar", add: false });
|
|
assert.deepEqual(typedName("\u2212hair"), { word: "hair", add: false }, "the minus a phone types");
|
|
assert.deepEqual(typedName("+hat", true), { word: "hat", add: true });
|
|
assert.deepEqual(typedName("t-shirt", false), { word: "t-shirt", add: true }, "a dash inside a word is the word's");
|
|
assert.deepEqual(typedName("-"), { word: "", add: false });
|
|
assert.deepEqual(typedName(null), { word: "", add: true });
|
|
});
|
|
|
|
test("looks at a picture the node no longer has are taken out of the queue before the next one is sent", () => {
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const stage = readFileSync(join(here, "..", "js", "shared", "touch_stage.mjs"), "utf8");
|
|
const entry = readFileSync(join(here, "..", "js", "mask_by_touch", "index.js"), "utf8");
|
|
const run = stage.slice(stage.indexOf("function requestRun"), stage.indexOf("function settle(face)"));
|
|
// the stale ones are counted before this run joins them, and dropped before it is sent
|
|
assert.ok(run.indexOf("const stale = face.flying.filter(") < run.indexOf("face.flying.push(run)"));
|
|
assert.match(run, /other\.queued && other\.epoch !== run\.epoch/);
|
|
assert.ok(run.indexOf("face.host.dropWaiting()") < run.indexOf("face.host.run("));
|
|
// only this node's own runs (they carry an `ask`) that still wait: never the one that runs, never a workflow run
|
|
const drop = entry.slice(entry.indexOf("async function dropWaiting"), entry.indexOf("// The picture goes above the settings"));
|
|
assert.match(drop, /queue_pending/);
|
|
assert.doesNotMatch(drop, /queue_running|interrupt/);
|
|
assert.match(drop, /class_type === NODE_CLASS && String\(step\.inputs\?\.ask \?\? ""\) !== ""/);
|
|
// a look still goes behind looks at an older picture, as before
|
|
assert.equal(goesBehindAgain([{ ask: "scope", queued: true, epoch: 1 }], "scope", 2), true);
|
|
});
|
|
|
|
test("a full graphics card is said in one plain line, anything else as it came", () => {
|
|
const comfy = "Allocation on device 0 would exceed allowed memory. (out of memory)\nCurrently allocated : 2.07 GiB\nRequested : 40.50 MiB";
|
|
assert.equal(stopMessage(comfy), OUT_OF_MEMORY);
|
|
assert.equal(stopMessage("torch.OutOfMemoryError: CUDA"), OUT_OF_MEMORY);
|
|
assert.equal(stopMessage("Nothing is picked on this picture."), "Nothing is picked on this picture.");
|
|
assert.equal(stopMessage(null), "");
|
|
});
|