Found in a sweep of the pack against ComfyUI's own nodes: things the normal
node does that the pack's node did not.
- Every number box takes a sum. Click a number, type 1024*1.5 or
(1920-64)/2 and press Enter, as in ComfyUI's own number boxes: + - * / %,
brackets and a sign. The reader is the pack's own (sumValue in
js/shared/scrub_input.mjs) and is only asked when the text is not a plain
number.
- Show Text takes any wire. Its input was STRING only. A number, an on/off
value or a list is shown too; a picture or a latent is shown by its size.
The output stays STRING and carries the text that is shown, so text
passes through unchanged and a number comes out as text.
- Save Image takes name tokens in the file name, as ComfyUI's own Save
Image does: %date:yyyy-MM-dd% and %Node.setting% are filled in the
browser when a run is sent (js/shared/name_tokens.mjs, now shared with
Save Video), %width% %height% and the time tokens on the server. The
path preview shows them. A linked filename is never read for tokens.
- Ctrl + Up and Ctrl + Down change a word's weight in the pack's prompt
boxes (Text, Krea 2 Encode). The card's text box stopped the key before
ComfyUI's own listener saw it; those two keys now go on from a multiline
box, and from nowhere else.
- Image Size shows what it measured beside its outputs after a run
("width: 1024"). The numbers go into the output labels, so the node keeps
its size. ComfyUI says nothing about a node whose answer came from its
cache, so the numbers are kept in the node's properties and come back
when a workflow is opened again; a changed picture wire clears them.
No input was added, renamed or reordered. Help pages, README lines, tests
and CHANGELOG entries are included.
217 lines
9.2 KiB
JavaScript
217 lines
9.2 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: \{[^\n]*\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\)/);
|
|
});
|