Files
ausboss-ComfyUI-AusBoss/js/shared/widget_card_math.mjs
T

107 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Pure decisions behind js/shared/widget_card.mjs: which rows a card shows,
// how tall it is, and how a number scrubs. No DOM, no app import, so
// tests/widget_card.test.mjs runs it under node:test.
export const ROW_HEIGHT = 26;
export const ROW_GAP = 5;
export const SECTION_HEIGHT = 17;
export const GROUP_HEIGHT = 22;
export const CARD_PADDING = 7;
// Every scrub field in a card reserves this much for its unit ("px", "MP",
// "×") whether it has one or not, so the numbers share one centre line.
export const UNIT_SLOT_WIDTH = 22;
// A widget input socket is drawn at the widget's y plus half a slot.
export const SLOT_OFFSET = 10;
// A segmented pill only reads when every label fits its slot.
const SEGMENT_MAX_OPTIONS = 4;
const SEGMENT_MAX_CHARS = 26;
// Which rows the current values leave visible. `groups` maps a group name to
// its open state; a row inside a closed group is out, a row whose `when`
// says no is out, a group header stays as long as any row under it exists.
export function visibleRows(rows, values, groups = {}) {
return rows.filter((row) => {
if (row.group !== undefined && row.widget === undefined && row.pair === undefined && row.section === undefined) {
return true; // the disclosure header itself
}
if (row.when && row.when(values) === false) return false;
if (row.inGroup && !groups[row.inGroup]) return false;
return true;
});
}
// One row's height: a caption, a group header, a control row, or the
// height a textarea row asks for.
export function rowHeight(row) {
if (row.section !== undefined) return SECTION_HEIGHT;
if (row.widget === undefined && row.pair === undefined) return GROUP_HEIGHT;
return Number(row.height) > 0 ? Number(row.height) : ROW_HEIGHT;
}
// Card height for a list of visible rows: rows, section captions and group
// headers each add their own height plus the gap between them.
export function cardHeight(rows) {
if (!rows.length) return CARD_PADDING * 2;
return CARD_PADDING * 2 + rows.reduce((sum, row) => sum + rowHeight(row), 0) + ROW_GAP * (rows.length - 1);
}
// Where each visible row starts, measured from the card's top edge.
export function rowTops(rows) {
let y = CARD_PADDING;
return rows.map((row) => {
const top = y;
y += rowHeight(row) + ROW_GAP;
return top;
});
}
// The y a hidden widget reports so its input socket sits level with the
// card row standing in for it: the socket is drawn SLOT_OFFSET below the
// widget's y, and it belongs on the row's centre line - or, for a tall
// textarea row, just under its top edge, where the frontend puts it too.
export function socketWidgetY(cardY, insetTop, rowTop, height) {
return cardY + insetTop + rowTop + Math.min(height / 2, 13) - SLOT_OFFSET;
}
// A combo is a segmented pill when the labels are few and short enough to
// sit side by side; otherwise it is a select.
export function segmentFits(labels) {
if (!Array.isArray(labels) || labels.length < 2 || labels.length > SEGMENT_MAX_OPTIONS) return false;
return labels.reduce((sum, label) => sum + String(label).length, 0) <= SEGMENT_MAX_CHARS;
}
// Scrub steps from a widget's options: the fine step is the widget's own
// increment; the coarse step covers ground - a 0..1 float moves by 0.05, a
// wide float by ten increments, a wide integer by 8.
export function scrubSteps(options = {}, isFloat = true) {
const fine = Number(options.step2 ?? (options.step ? options.step / 10 : null) ?? (isFloat ? 0.01 : 1)) || (isFloat ? 0.01 : 1);
const range = Number(options.max ?? Infinity) - Number(options.min ?? -Infinity);
let step;
if (!isFloat) step = Number.isFinite(range) && range > 2048 ? 8 : Math.max(1, fine);
else if (Number.isFinite(range) && range <= 2) step = Math.max(fine, 0.05);
else step = fine * 10;
const decimals = isFloat ? Number(options.precision ?? Math.max(0, -Math.floor(Math.log10(fine)))) : 0;
return { step, fineStep: fine, decimals };
}
// Row kind from the widget the row mirrors, unless the config names one.
export function rowKind(row, widget) {
if (row.kind) return row.kind;
const type = String(widget?.type ?? "");
if (type === "number" || type === "slider") return "scrub";
if (type === "combo") return segmentFits(comboLabels(row, widget)) ? "segment" : "select";
if (type === "toggle") return "switch";
if (type === "customtext") return "skip";
if (/color/i.test(String(row.widget ?? "")) && /^#[0-9a-f]{3,8}$/i.test(String(widget?.value ?? ""))) return "color";
return "text";
}
export function comboValues(widget) {
let values = widget?.options?.values;
if (typeof values === "function") values = values(widget);
return Array.isArray(values) ? values : [];
}
function comboLabels(row, widget) {
return comboValues(widget).map((value) => row.labels?.[value] ?? String(value));
}