Restoring links after a failed change checked the source node but not its output slot, so a link from another output of the same node could be kept. 29 new tests cover scrub math, graph links, panel sizing, Civitai parsing, brush math and widget card values.
285 lines
12 KiB
JavaScript
285 lines
12 KiB
JavaScript
// The shared scrub control's pure gesture math: dead zone, step travel,
|
|
// fine steps, snapped steps, clamping, and precision.
|
|
|
|
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 {
|
|
SCRUB_DEAD_ZONE,
|
|
SCRUB_PIXELS_PER_STEP,
|
|
formatScrubValue,
|
|
isScrubGesture,
|
|
quantizeScrubValue,
|
|
scrubbedValue,
|
|
stepDecimals,
|
|
steppedValue,
|
|
sumValue,
|
|
SCRUB_SLOW_FACTOR,
|
|
} from "../js/shared/scrub_input.mjs";
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
const MP = { min: 0.01, max: 16, step: 0.05, fineStep: 0.01, decimals: 2 };
|
|
|
|
test("inside the dead zone nothing changes, so a click stays a click", () => {
|
|
assert.equal(scrubbedValue(1, SCRUB_DEAD_ZONE, false, MP), 1);
|
|
assert.equal(scrubbedValue(1, -SCRUB_DEAD_ZONE, false, MP), 1);
|
|
});
|
|
|
|
test("travel past the dead zone steps by the coarse step", () => {
|
|
// 43px right: (43-3)/4 = 10 steps of 0.05.
|
|
assert.equal(scrubbedValue(1, 43, false, MP), 1.5);
|
|
assert.equal(scrubbedValue(1, -43, false, MP), 0.5);
|
|
});
|
|
|
|
test("shift scrubs by the fine step", () => {
|
|
assert.equal(scrubbedValue(1, 43, true, MP), 1.1);
|
|
});
|
|
|
|
test("scrubs clamp at the range edges", () => {
|
|
assert.equal(scrubbedValue(15.9, 400, false, MP), 16);
|
|
assert.equal(scrubbedValue(0.1, -400, false, MP), 0.01);
|
|
});
|
|
|
|
test("values come back rounded to the control's precision", () => {
|
|
assert.equal(quantizeScrubValue(0.1 + 0.2, MP), 0.3);
|
|
assert.equal(quantizeScrubValue(7.777, { decimals: 0, min: 1, max: 256 }), 8);
|
|
});
|
|
|
|
test("junk quantizes to the range floor, not NaN", () => {
|
|
assert.equal(quantizeScrubValue("wide", MP), 0.01);
|
|
});
|
|
|
|
test("a mostly-vertical drag is not a scrub", () => {
|
|
assert.equal(isScrubGesture(10, 4), true);
|
|
assert.equal(isScrubGesture(6, 30), false);
|
|
assert.equal(isScrubGesture(2, 0), false);
|
|
});
|
|
|
|
test("integer controls step whole numbers", () => {
|
|
const STEPS = { min: 1, max: 256, step: 1, decimals: 0 };
|
|
assert.equal(scrubbedValue(64, 3 + SCRUB_PIXELS_PER_STEP * 4, false, STEPS), 68);
|
|
assert.equal(scrubbedValue(1, -400, false, STEPS), 1);
|
|
});
|
|
|
|
test("Shift slows a whole-number box that has no finer step", () => {
|
|
// Feather: 1 px steps, no fine step. Shift used to change nothing.
|
|
const FEATHER = { step: 1, decimals: 0, min: 0, max: 4096 };
|
|
assert.equal(scrubbedValue(24, 40, false, FEATHER), 33);
|
|
const slow = scrubbedValue(24, 40, true, FEATHER);
|
|
assert.ok(slow > 24 && slow < 33, String(slow));
|
|
assert.equal(scrubbedValue(24, 3 + SCRUB_PIXELS_PER_STEP * SCRUB_SLOW_FACTOR * 2, true, FEATHER), 26);
|
|
// A box with a finer step still uses it at the normal speed.
|
|
assert.equal(scrubbedValue(1, 43, true, { step: 0.05, fineStep: 0.01, decimals: 2 }), 1.1);
|
|
});
|
|
|
|
// Divisible by (canvas_multiple): 1 is off, and the sizes people want are
|
|
// multiples of 8. Stepping used to add 8 to 1 and land on 9, 17, 25.
|
|
const DIVISIBLE = { min: 1, max: 4096, step: 8, fineStep: 1, decimals: 0, snap: true };
|
|
// What one chevron click or arrow key commits (the DOM factory's path).
|
|
const click = (value, direction, fine = false) => {
|
|
const size = fine ? DIVISIBLE.fineStep : DIVISIBLE.step;
|
|
return quantizeScrubValue(steppedValue(value, direction, size, !fine), DIVISIBLE);
|
|
};
|
|
|
|
test("a snap box steps from 1 to 8, 16, 24 and back down to 1", () => {
|
|
const up = [];
|
|
for (let value = 1; up.length < 4;) up.push(value = click(value, 1));
|
|
assert.deepEqual(up, [8, 16, 24, 32]);
|
|
const down = [];
|
|
for (let value = 32; down.length < 5;) down.push(value = click(value, -1));
|
|
assert.deepEqual(down, [24, 16, 8, 1, 1]);
|
|
});
|
|
|
|
test("a snap box off the grid steps to the nearest multiple that way", () => {
|
|
assert.equal(click(9, 1), 16);
|
|
assert.equal(click(9, -1), 8);
|
|
assert.equal(click(12, 1), 16);
|
|
assert.equal(click(12, -1), 8);
|
|
assert.equal(click(4095, 1), 4096);
|
|
});
|
|
|
|
test("Shift steps a snap box by 1, off the grid", () => {
|
|
assert.equal(click(8, 1, true), 9);
|
|
assert.equal(click(8, -1, true), 7);
|
|
// ... and the next plain step goes back onto it.
|
|
assert.equal(click(click(8, 1, true), 1), 16);
|
|
});
|
|
|
|
test("dragging a snap box passes only multiples of the step", () => {
|
|
const passed = new Set();
|
|
for (let dx = 0; dx <= 64; dx++) passed.add(scrubbedValue(1, dx, false, DIVISIBLE));
|
|
assert.deepEqual([...passed], [1, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120]);
|
|
assert.equal(scrubbedValue(24, -43, false, DIVISIBLE), 1);
|
|
// A Shift drag moves by 1 from wherever it is.
|
|
assert.equal(scrubbedValue(8, 3 + SCRUB_PIXELS_PER_STEP * 3, true, DIVISIBLE), 11);
|
|
});
|
|
|
|
test("without snap a step still adds the step to the value", () => {
|
|
assert.equal(steppedValue(1, 1, 8), 9);
|
|
assert.equal(scrubbedValue(1, 3 + SCRUB_PIXELS_PER_STEP, false, { ...DIVISIBLE, snap: false }), 9);
|
|
});
|
|
|
|
test("a value on a fractional grid counts as on it", () => {
|
|
// 0.3 / 0.1 is 2.9999999999999996 in floating point.
|
|
assert.equal(quantizeScrubValue(steppedValue(0.3, 1, 0.1, true), { decimals: 2 }), 0.4);
|
|
assert.equal(quantizeScrubValue(steppedValue(0.3, -1, 0.1, true), { decimals: 2 }), 0.2);
|
|
});
|
|
|
|
test("every divisible-by box on the transform nodes and Load Image + Pad snaps", () => {
|
|
// Divisible by and the resize Step, on the node face and in the editor.
|
|
const editor = readFileSync(join(ROOT, "js", "shared", "transform_editor.mjs"), "utf-8");
|
|
const boxes = editor.split("makeScrubInput(").slice(1)
|
|
.map((rest) => rest.slice(0, rest.indexOf("onChange")))
|
|
.filter((options) => /"(canvas_multiple|resolution_steps)"/.test(options));
|
|
assert.equal(boxes.length, 4);
|
|
for (const options of boxes) assert.match(options, /\bsnap: true\b/);
|
|
const cards = readFileSync(join(ROOT, "js", "widget_cards", "index.js"), "utf-8");
|
|
assert.match(cards, /\{ widget: "canvas_multiple", [^}]*\bsnap: true\b/);
|
|
});
|
|
|
|
test("a step reaches as many decimals as it is written with", () => {
|
|
assert.equal(stepDecimals(0.05), 2);
|
|
assert.equal(stepDecimals(0.01), 2);
|
|
assert.equal(stepDecimals(0.25), 2);
|
|
assert.equal(stepDecimals(0.5), 1);
|
|
assert.equal(stepDecimals(0.1), 1);
|
|
assert.equal(stepDecimals(0.001), 3);
|
|
assert.equal(stepDecimals(1), 0);
|
|
assert.equal(stepDecimals(8), 0);
|
|
});
|
|
|
|
test("a box shows every decimal it holds unless told how few are enough", () => {
|
|
assert.equal(formatScrubValue(1, { decimals: 3 }), "1.000");
|
|
assert.equal(formatScrubValue(0.5, { decimals: 2 }), "0.50");
|
|
assert.equal(formatScrubValue(512, { decimals: 0 }), "512");
|
|
assert.equal(formatScrubValue(null, { decimals: 3 }), "");
|
|
});
|
|
|
|
test("zeroes the steps never reach are dropped, a value that uses them is shown in full", () => {
|
|
// Float wired to denoise: three decimals held, steps of 0.05 and 0.01.
|
|
const denoise = { decimals: 3, minDecimals: 2 };
|
|
assert.equal(formatScrubValue(1, denoise), "1.00");
|
|
assert.equal(formatScrubValue(0.85, denoise), "0.85");
|
|
assert.equal(formatScrubValue(0.855, denoise), "0.855");
|
|
assert.equal(formatScrubValue(0, denoise), "0.00");
|
|
// A step of 1 (CFG) still reads as a number with decimals.
|
|
assert.equal(formatScrubValue(8, { decimals: 3, minDecimals: 1 }), "8.0");
|
|
assert.equal(formatScrubValue(7.25, { decimals: 3, minDecimals: 1 }), "7.25");
|
|
// Nothing to keep: the dot goes with the last zero.
|
|
assert.equal(formatScrubValue(8, { decimals: 3, minDecimals: 0 }), "8");
|
|
assert.equal(formatScrubValue(100, { decimals: 2, minDecimals: 0 }), "100");
|
|
assert.equal(formatScrubValue(-0.5, { decimals: 3, minDecimals: 2 }), "-0.50");
|
|
// Asking for more than the box holds changes nothing.
|
|
assert.equal(formatScrubValue(1, { decimals: 2, minDecimals: 4 }), "1.00");
|
|
});
|
|
|
|
test("Float 🆎 drops the zeroes its steps never reach", () => {
|
|
const cards = readFileSync(join(ROOT, "js", "widget_cards", "index.js"), "utf-8");
|
|
assert.match(cards, /AUSBOSS_NODES_Float: \{[\s\S]{0,200}?\btrimZeros: true\b/);
|
|
});
|
|
|
|
test("a sum typed into a number box comes to its answer", () => {
|
|
assert.equal(sumValue("1024*1.5"), 1536);
|
|
assert.equal(sumValue("512+64"), 576);
|
|
assert.equal(sumValue("(1920 - 64) / 2"), 928);
|
|
assert.equal(sumValue("10/4"), 2.5);
|
|
assert.equal(sumValue("7 % 4"), 3);
|
|
assert.equal(sumValue("2+3*4"), 14);
|
|
assert.equal(sumValue("(2+3)*4"), 20);
|
|
assert.equal(sumValue("-(2+3)"), -5);
|
|
assert.equal(sumValue("2*-3"), -6);
|
|
assert.equal(sumValue("--4"), 4);
|
|
assert.equal(sumValue(".5+.25"), 0.75);
|
|
assert.equal(sumValue("3."), 3);
|
|
assert.equal(sumValue(" 64 "), 64);
|
|
assert.equal(sumValue("((((1))))"), 1);
|
|
assert.equal(Object.is(sumValue("0*-1"), 0), true);
|
|
});
|
|
|
|
test("text that is not a sum is no answer", () => {
|
|
for (const text of ["", " ", "abc", "2+", "*2", "(2+3", "2+3)", "2 3", "1,5", "1e3*2", "2**3", "2^3", "1/0", "0/0", "5%0", "()", ".", "1..2"]) {
|
|
assert.equal(sumValue(text), null, JSON.stringify(text));
|
|
}
|
|
// A very long text is refused before it is read, so it cannot run deep.
|
|
assert.equal(sumValue("(".repeat(5000) + "1" + ")".repeat(5000)), null);
|
|
assert.equal(sumValue("-".repeat(199) + "1"), -1);
|
|
assert.equal(sumValue(null), null);
|
|
});
|
|
|
|
test("a number box reads a typed sum only where a plain number fails", () => {
|
|
const source = readFileSync(join(ROOT, "js", "shared", "scrub_input.mjs"), "utf-8");
|
|
assert.match(source, /Number\.isFinite\(number\) \? number : sumValue\(input\.value\)/);
|
|
});
|
|
|
|
test("formatting keeps significant digits while trimming only fractional zeroes", () => {
|
|
for (const [value, options, expected] of [
|
|
[undefined, {}, ""],
|
|
["12.340", { decimals: 3, minDecimals: 0 }, "12.34"],
|
|
[-120, { decimals: 3, minDecimals: 0 }, "-120"],
|
|
[-1.2346, { decimals: 3, minDecimals: 1 }, "-1.235"],
|
|
[0.0001, { decimals: 4, minDecimals: 0 }, "0.0001"],
|
|
[12, { decimals: -2 }, "12"],
|
|
[1, { decimals: 2, minDecimals: -1 }, "1"],
|
|
]) {
|
|
assert.equal(formatScrubValue(value, options), expected);
|
|
}
|
|
});
|
|
|
|
test("invalid scrub values use a finite floor or zero before rounding", () => {
|
|
for (const value of [undefined, NaN, Infinity, -Infinity, "not a number"]) {
|
|
assert.equal(quantizeScrubValue(value), 0);
|
|
assert.equal(quantizeScrubValue(value, { min: -2, max: 4 }), -2);
|
|
}
|
|
assert.equal(quantizeScrubValue("2.718", { decimals: 2 }), 2.72);
|
|
assert.equal(quantizeScrubValue(-10, { min: -2, max: 4 }), -2);
|
|
assert.equal(quantizeScrubValue(10, { min: -2, max: 4 }), 4);
|
|
});
|
|
|
|
test("snapped steps cross zero and take several steps from an off-grid value", () => {
|
|
for (const [start, count, expected] of [
|
|
[-9, 1, -8], [-9, -1, -16], [-9, 3, 8],
|
|
[9, -3, -8], [9, 3, 32], [0, -1, -8], [9, 0, 9],
|
|
]) {
|
|
assert.equal(steppedValue(start, count, 8, true), expected, `${start}, ${count}`);
|
|
}
|
|
assert.equal(steppedValue("9", -2, 8), -7);
|
|
});
|
|
|
|
test("gesture boundaries accept equal diagonal travel only past the dead zone", () => {
|
|
for (const direction of [-1, 1]) {
|
|
assert.equal(isScrubGesture(direction * SCRUB_DEAD_ZONE, 0), false);
|
|
assert.equal(isScrubGesture(direction * 4, -4), true);
|
|
assert.equal(isScrubGesture(direction * 4, 5), false);
|
|
}
|
|
});
|
|
|
|
test("Shift slows dragging when the supplied fine step is equal or larger", () => {
|
|
const delta = SCRUB_DEAD_ZONE + SCRUB_PIXELS_PER_STEP * SCRUB_SLOW_FACTOR * 2;
|
|
for (const fineStep of [null, 1, 2]) {
|
|
const options = { step: 1, fineStep, decimals: 0 };
|
|
assert.equal(scrubbedValue(10, delta, true, options), 12);
|
|
assert.equal(scrubbedValue(10, -delta, true, options), 8);
|
|
assert.equal(scrubbedValue(10, delta, false, options), 18);
|
|
}
|
|
});
|
|
|
|
test("step precision accepts negative and string increments and rounds at six decimals", () => {
|
|
for (const [step, expected] of [[-0.025, 3], ["0.000001", 6], [0.1234567, 6], [0, 0], [undefined, 0]]) {
|
|
assert.equal(stepDecimals(step), expected, String(step));
|
|
}
|
|
});
|
|
|
|
test("typed sums associate division and subtraction left to right", () => {
|
|
assert.equal(sumValue("24/4/2"), 3);
|
|
assert.equal(sumValue("10-3-2"), 5);
|
|
assert.equal(sumValue("20%6*2"), 4);
|
|
assert.equal(sumValue("\t-(2 + +3) * -2\t"), 10);
|
|
assert.equal(sumValue("1" + " ".repeat(199)), 1);
|
|
assert.equal(sumValue("1" + " ".repeat(200)), null);
|
|
assert.equal(sumValue("1/(2-2)"), null);
|
|
});
|