Files
IxMxAMAR e8e673caf6 v0.3.1: collapsed-node stripe bleed + wire render mode dropdown off-by-one
- Skip theme decoration (accent stripe + drawForeground/drawBackground)
  when node.flags.collapsed. LiteGraph keeps node.size[1] at the expanded
  height when collapsed and does NOT clip per-node draws, so the 4px
  left-edge accent stripe was painting a vertical bar past the bottom of
  the collapsed title pill.

- Rewrite wire render mode dropdown with explicit label->value pairs.
  v0.3.0 used indexOf on a label-only array which mapped 1:1 to the
  setting value, but LiteGraph's constants are 0-based and non-contiguous
  (HIDDEN=-1, STRAIGHT=0, LINEAR=1, SPLINE=2). So picking "straight"
  actually set LINEAR, picking "linear" set SPLINE, and the bezier/
  manhattan options sent invalid 3/4 that LiteGraph fell back to default
  for. v0.3.1 maps each label to the correct constant, drops the
  fictional "manhattan (L-shape)" option, and adds "hidden".

All 33 JS tests still pass.
2026-05-21 16:19:57 +05:30

260 lines
9.4 KiB
JavaScript
Raw Permalink 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.
import { app } from "../../scripts/app.js";
import { ThemeRegistry } from "./theme-registry.js";
import { ThemeEngine, applyGlobalTweaks, applyThemeToGroup } from "./theme-engine.js";
import { createThemeApi } from "./theme-api.js";
import { resolveTheme } from "./resolve-theme.js";
import {
applyWirePalette,
applyWireRenderMode,
applyWireThickness,
setWireOpacity,
ensureRenderLinkWrapped,
} from "./wire-engine.js";
import cyberpunk from "./themes/cyberpunk.js";
import minimalist from "./themes/minimalist.js";
import glassmorphic from "./themes/glassmorphic.js";
import retro from "./themes/retro.js";
import defaultTheme from "./themes/default.js";
import holographic from "./themes/holographic.js";
import paper from "./themes/paper.js";
import brutalist from "./themes/brutalist.js";
import solarized from "./themes/solarized.js";
import dracula from "./themes/dracula.js";
import highContrast from "./themes/high_contrast.js";
// v0.2.0 additions:
import blueprint from "./themes/blueprint.js";
import nord from "./themes/nord.js";
import synthwave from "./themes/synthwave.js";
import eInk from "./themes/e_ink.js";
export const THEME_API_VERSION = 2;
const registry = new ThemeRegistry();
for (const theme of [
cyberpunk, minimalist, glassmorphic, retro, defaultTheme,
holographic, paper, brutalist, solarized, dracula, highContrast,
blueprint, nord, synthwave, eInk,
]) {
registry.register(theme);
}
const settings = {
packDefault: "default",
globalOverrideEnabled: false,
globalTheme: "default",
respectExistingCustomDrawing: true,
reduceMotion: false,
heavyThemeNodeBudget: 25,
// v0.2.0
wireRenderMode: null, // null = leave ComfyUI's setting alone; otherwise a LiteGraph constant (-1, 0, 1, or 2)
wireThickness: 1.0, // multiplier on default line width
wireOpacity: 1.0, // 0..1
// v0.3.0
autoContrastText: true,
rightClickThemeMenu: true,
};
const themedNodes = new Set();
const themeApi = createThemeApi({
settings,
getVisibleThemedCount: () => themedNodes.size,
});
const engine = new ThemeEngine({ themeApi });
function activeGlobalTheme() {
// The theme that drives global tweaks (wires, groups, canvas bg). When
// override is on, that's the global theme; otherwise it's the pack default.
const id = settings.globalOverrideEnabled ? settings.globalTheme : settings.packDefault;
return id && id !== "default" ? registry.get(id) : null;
}
function syncGlobalTweaks() {
const theme = activeGlobalTheme();
applyWirePalette(app, theme?.wirePalette ?? null);
applyGlobalTweaks(app, theme);
}
function applyResolved(node) {
const themeId = resolveTheme(node, settings);
const theme = themeId ? registry.get(themeId) : null;
if (theme) theme.themeApi = themeApi;
engine.applyTheme(node, theme, { autoContrast: settings.autoContrastText });
if (theme) themedNodes.add(node);
else themedNodes.delete(node);
}
function reapplyAll() {
if (!app.graph || !app.graph._nodes) return;
// Apply per-node themes first (controls accent stripes + foregrounds).
for (const node of app.graph._nodes) applyResolved(node);
// Then sync global tweaks (wire palette, group colors, canvas bg).
syncGlobalTweaks();
}
const THEME_OPTIONS = registry.ids();
// Wire render modes — explicit label→value pairs because LiteGraph's constants
// are 0-based AND non-contiguous (HIDDEN=-1, STRAIGHT=0, LINEAR=1, SPLINE=2).
// v0.3.0 used `indexOf` on a label-only array which was off-by-one and also
// included a fictional "manhattan" option. The result: 3 of 4 options either
// did the wrong thing or did nothing. The selected "straight" looked plausible
// because it actually set LINEAR.
const WIRE_RENDER_MODES = [
{ label: "off (leave ComfyUI's setting)", value: null },
{ label: "hidden", value: -1 },
{ label: "straight", value: 0 },
{ label: "linear", value: 1 },
{ label: "spline (bezier)", value: 2 },
];
const WIRE_RENDER_MODE_OPTIONS = WIRE_RENDER_MODES.map((m) => m.label);
const wireRenderValueFor = (label) =>
WIRE_RENDER_MODES.find((m) => m.label === label)?.value ?? null;
function defineSetting(id, name, type, defaultValue, onChange, options) {
const setting = {
id: `megapack.${id}`,
name,
type,
defaultValue,
onChange,
};
if (type === "combo" && Array.isArray(options)) {
setting.options = options;
}
app.ui.settings.addSetting(setting);
}
app.registerExtension({
name: "UtilityMegaPack.ThemeEngine",
async setup() {
// Wire-engine patch is installed ONCE at startup. It reads live settings,
// so subsequent setting changes take effect without re-patching.
if (typeof window !== "undefined" && window.LGraphCanvas) {
ensureRenderLinkWrapped(window.LGraphCanvas);
}
// Auto-theme any groups created AFTER a global theme is active. v0.2.x
// only applied group colors on theme switch, so new groups showed
// LiteGraph defaults until the user re-picked the theme.
if (app.graph) {
const origOnGroupAdded = app.graph.onGroupAdded;
app.graph.onGroupAdded = function (group) {
applyThemeToGroup(group);
if (origOnGroupAdded) origOnGroupAdded.call(this, group);
};
}
defineSetting("packDefault", "Utility-MegaPack — Pack default theme", "combo",
"default",
(v) => { settings.packDefault = v; reapplyAll(); },
THEME_OPTIONS);
defineSetting("globalOverrideEnabled", "Utility-MegaPack — Theme ALL ComfyUI nodes", "boolean",
false,
(v) => { settings.globalOverrideEnabled = v; reapplyAll(); });
defineSetting("globalTheme", "Utility-MegaPack — Global theme", "combo",
"default",
(v) => { settings.globalTheme = v; reapplyAll(); },
THEME_OPTIONS);
defineSetting("respectExistingCustomDrawing", "Utility-MegaPack — Respect existing custom drawing", "boolean",
true,
(v) => { settings.respectExistingCustomDrawing = v; reapplyAll(); });
defineSetting("reduceMotion", "Utility-MegaPack — Reduce motion", "boolean",
false,
(v) => { settings.reduceMotion = v; reapplyAll(); });
defineSetting("heavyThemeNodeBudget", "Utility-MegaPack — Heavy theme node budget", "number",
25,
(v) => { settings.heavyThemeNodeBudget = v; app.canvas?.setDirty?.(true, true); });
// v0.2.0: wire/connection theming
defineSetting("wireRenderMode", "Utility-MegaPack — Wire render mode", "combo",
WIRE_RENDER_MODE_OPTIONS[0],
(v) => {
const val = wireRenderValueFor(v);
settings.wireRenderMode = val;
if (val !== null) {
applyWireRenderMode(app, val);
}
},
WIRE_RENDER_MODE_OPTIONS);
defineSetting("wireThickness", "Utility-MegaPack — Wire thickness (×)", "number",
1.0,
(v) => { settings.wireThickness = Number(v) || 1.0; applyWireThickness(app, settings.wireThickness); });
defineSetting("wireOpacity", "Utility-MegaPack — Wire opacity", "slider",
1.0,
(v) => { settings.wireOpacity = Number(v) || 1.0; setWireOpacity(settings.wireOpacity); app.canvas?.setDirty?.(true, false); });
// v0.3.0: UX
defineSetting("autoContrastText", "Utility-MegaPack — Auto-contrast title text", "boolean",
true,
(v) => { settings.autoContrastText = !!v; reapplyAll(); });
defineSetting("rightClickThemeMenu", "Utility-MegaPack — Right-click 'Theme…' menu on nodes", "boolean",
true,
(v) => { settings.rightClickThemeMenu = !!v; });
},
async nodeCreated(node) {
applyResolved(node);
const themeWidget = node.widgets?.find((w) => w.name === "theme");
if (themeWidget) {
const orig = themeWidget.callback;
themeWidget.callback = (v, ...rest) => {
node.properties = node.properties ?? {};
node.properties.megapack_theme = v;
applyResolved(node);
syncGlobalTweaks();
return orig?.(v, ...rest);
};
}
// Right-click → "Theme…" submenu. Lets users theme ANY node without
// a widget (vanilla ComfyUI nodes don't have a theme widget at all).
const origMenu = node.getExtraMenuOptions;
node.getExtraMenuOptions = function (canvas, options) {
if (origMenu) origMenu.call(this, canvas, options);
if (!settings.rightClickThemeMenu) return;
const submenu = THEME_OPTIONS.map((id) => ({
content: id === "default" ? "(default — no theme)" : id,
callback: () => {
this.properties = this.properties ?? {};
this.properties.megapack_theme = id;
applyResolved(this);
syncGlobalTweaks();
this.setDirtyCanvas?.(true, true);
},
}));
const active = this.properties?.megapack_theme;
for (const item of submenu) {
if (item.content.startsWith(active || "")) item.content = "• " + item.content;
}
options.push(null); // visual separator
options.push({
content: "Theme…",
has_submenu: true,
submenu: { options: submenu },
});
};
},
async loadedGraphNode(node) {
applyResolved(node);
// After the full graph loads, sync global tweaks once (group colors etc.)
syncGlobalTweaks();
},
// Without this, deleted nodes lingered in `themedNodes` forever — leaking
// references AND permanently inflating the heavyThemeNodeBudget check, which
// would eventually disable all motion across the session.
nodeRemoved(node) {
themedNodes.delete(node);
},
});