Two changes that ship together because the visual rework was masking the real bug: 1. `readSlots` (and the title/category/description reads in `renderPreviewCard`) were looking at static `nodeClass.INPUT_TYPES` / `RETURN_TYPES` properties, which **don't exist** on ComfyUI-registered nodes — ComfyUI parks the original V1 def at `nodeClass.nodeData` (set by `litegraphService.ts`'s `registerNodeDef` right before `LiteGraph.registerNodeType`). Every card was rendering as just header-and-badge because the slot arrays came back empty for every ComfyUI node. Reading from `nodeData` first (with the old property names as a legacy fallback for any non-ComfyUI registered nodes) fixes the empty-card class. 2. Visual rework to look like a real node mock instead of a generic info card. Mirrors upstream NodePreview.vue more faithfully: flat header row (small colored dot + display name) over a red PREVIEW badge; slot rows in a 5-column grid (`[dot] [input-name] [spacer] [output-name] [dot]`) so inputs and outputs line up horizontally as parallel sockets; widgets rendered as individual rounded pills with `◀ name [spacer] value ▶` chrome to mimic LiteGraph widget UI. Closes #78 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d021e8c8da
commit
75b7e76f11
@@ -144,6 +144,27 @@ The format is inspired by Keep a Changelog and SemVer.
|
||||
|
||||
[np-vue]: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/components/node/NodePreview.vue
|
||||
|
||||
### Changed
|
||||
- **Hover preview rebuilt to read like a real node mock.** The original
|
||||
layout (HSL-hashed title strip + two-column slot table + flat widget
|
||||
text list) read as a generic info card. Reworked to mirror upstream
|
||||
`NodePreview.vue` more faithfully: a flat header row (small colored
|
||||
dot + node display name) over a red **PREVIEW** badge, slot rows
|
||||
laid out as a 5-column grid (`[dot] [input-name] [spacer] [output-name] [dot]`)
|
||||
so inputs and outputs line up horizontally as parallel sockets, and
|
||||
widgets rendered as individual rounded pills with `◀ name [spacer]
|
||||
value ▶` chrome to mimic LiteGraph widget UI. **Bigger fix
|
||||
underneath:** `readSlots` and the title / category / description
|
||||
reads were looking at static `nodeClass.INPUT_TYPES` /
|
||||
`RETURN_TYPES` properties, which **don't exist** on
|
||||
ComfyUI-registered nodes — ComfyUI parks the original V1 def at
|
||||
`nodeClass.nodeData` (set by `litegraphService.ts`'s
|
||||
`registerNodeDef` right before `LiteGraph.registerNodeType`). Every
|
||||
card was rendering as just-the-header-and-badge because the slot
|
||||
arrays came back empty. Reading from `nodeData` first (with the old
|
||||
property names as a legacy fallback for non-ComfyUI registered
|
||||
nodes) fixes the empty-card class. (#78)
|
||||
|
||||
### Fixed
|
||||
- **`pinExpanded` paths now expand on the immediate render** instead of
|
||||
being delayed by one. `buildFolder`'s expansion-resolution chain gained
|
||||
|
||||
+20
-19
@@ -181,25 +181,26 @@ export function ensureStyle() {
|
||||
.koolook-mode-toggle-btn.koolook-mode-active { background: rgba(80,140,235,0.25); opacity: 1; }
|
||||
.koolook-leaf-unresolved { opacity: 0.55; font-style: italic; }
|
||||
.koolook-leaf-crumb { opacity: 0.5; font-size: 11px; margin-right: 1px; }
|
||||
.koolook-preview-card { position: fixed; z-index: 10000; background: var(--comfy-menu-bg, #2a2a2a); border: 1px solid var(--border-color, rgba(255,255,255,0.18)); border-radius: 6px; box-shadow: 0 6px 24px rgba(0,0,0,0.55); color: var(--input-text, #ddd); font-size: 12px; width: 320px; max-width: 90vw; max-height: calc(100vh - 16px); pointer-events: none; overflow: hidden auto; }
|
||||
.koolook-preview-title { padding: 6px 10px; font-size: 13px; font-weight: 600; color: #fff; }
|
||||
.koolook-preview-title-unresolved { background: rgba(180,80,80,0.4); }
|
||||
.koolook-preview-cat { padding: 3px 10px 5px; font-size: 10px; opacity: 0.6; border-bottom: 1px solid rgba(255,255,255,0.05); font-family: monospace; }
|
||||
.koolook-preview-stub { padding: 10px; font-style: italic; opacity: 0.7; }
|
||||
.koolook-preview-io { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: rgba(255,255,255,0.06); }
|
||||
.koolook-preview-col { padding: 7px 9px; background: var(--comfy-menu-bg, #2a2a2a); }
|
||||
.koolook-preview-section-title { font-size: 10px; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 4px; }
|
||||
.koolook-preview-slot-row { display: flex; align-items: center; gap: 5px; padding: 1px 0; line-height: 1.35; }
|
||||
.koolook-preview-slot-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; box-shadow: 0 0 0 1px rgba(0,0,0,0.4); }
|
||||
.koolook-preview-slot-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-slot-name.koolook-preview-slot-optional { opacity: 0.7; font-style: italic; }
|
||||
.koolook-preview-slot-type { font-size: 10px; opacity: 0.5; flex-shrink: 0; font-family: monospace; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-empty { opacity: 0.4; font-size: 11px; padding: 1px 0; }
|
||||
.koolook-preview-widgets { padding: 6px 9px; border-top: 1px solid rgba(255,255,255,0.06); }
|
||||
.koolook-preview-widget-row { display: flex; gap: 6px; padding: 1px 0; line-height: 1.35; }
|
||||
.koolook-preview-widget-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-widget-default { opacity: 0.5; font-size: 10px; font-family: monospace; max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0; }
|
||||
.koolook-preview-desc { padding: 6px 9px; border-top: 1px solid rgba(255,255,255,0.06); font-size: 11px; opacity: 0.78; line-height: 1.4; max-height: 100px; overflow: hidden; }
|
||||
.koolook-preview-card { position: fixed; z-index: 10000; background: var(--comfy-menu-bg, #232323); border: 1px solid var(--border-color, rgba(255,255,255,0.22)); border-radius: 12px; box-shadow: 0 6px 24px rgba(0,0,0,0.55); color: var(--input-text, #ddd); font-size: 12px; min-width: 300px; max-width: 90vw; max-height: calc(100vh - 16px); pointer-events: none; overflow: hidden auto; padding-bottom: 10px; }
|
||||
.koolook-preview-header { display: flex; align-items: center; gap: 9px; padding: 8px 13px 7px; font-size: 14px; line-height: 1; white-space: nowrap; overflow: hidden; }
|
||||
.koolook-preview-headtitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
|
||||
.koolook-preview-header-unresolved .koolook-preview-headtitle { font-style: italic; opacity: 0.75; }
|
||||
.koolook-preview-headdot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; box-shadow: 0 0 0 1px rgba(0,0,0,0.4); }
|
||||
.koolook-preview-badge { padding: 0 13px 7px; color: #ff5f6d; font-size: 11px; letter-spacing: 0.08em; font-weight: 600; text-transform: uppercase; }
|
||||
.koolook-preview-stub { margin: 4px 10px 6px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-radius: 6px; font-style: italic; opacity: 0.78; line-height: 1.4; }
|
||||
.koolook-preview-row { display: grid; grid-template-columns: 14px 1fr 14px 1fr 14px; column-gap: 8px; align-items: center; padding: 0 9px; line-height: 1.55; min-height: 18px; }
|
||||
.koolook-preview-row-slot { padding: 2px 9px; }
|
||||
.koolook-preview-col { overflow: hidden; min-width: 0; }
|
||||
.koolook-preview-col-input { text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-col-output { text-align: right; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-col-middle { /* spacer, intentionally empty */ }
|
||||
.koolook-preview-slot-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; box-shadow: 0 0 0 1px rgba(0,0,0,0.4); vertical-align: middle; }
|
||||
.koolook-preview-slot-optional { opacity: 0.7; font-style: italic; }
|
||||
.koolook-preview-row-widget { background: rgba(0,0,0,0.28); border: 1px solid rgba(255,255,255,0.09); margin: 5px 6px 0; border-radius: 9px; padding: 0 6px; line-height: 1.65; }
|
||||
.koolook-preview-arrow { color: rgba(255,255,255,0.6); font-size: 9px; text-align: center; user-select: none; }
|
||||
.koolook-preview-widget-name { text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-widget-value { text-align: right; font-size: 11px; opacity: 0.85; font-family: monospace; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-desc { margin: 10px 9px 0; padding: 7px 9px; background: rgba(255,255,255,0.06); border-radius: 6px; font-style: italic; font-weight: 500; font-size: 11px; line-height: 1.45; word-break: break-word; opacity: 0.85; max-height: 120px; overflow: hidden; }
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
+201
-104
@@ -129,11 +129,9 @@ export function renderPreviewCard(nodeClass, type) {
|
||||
// Pack not currently loaded. Same source-of-truth as the
|
||||
// (unresolved) bucket in category-mode gather. The stub keeps the
|
||||
// hover-preview affordance consistent across resolved/unresolved
|
||||
// picks rather than silently no-oping for missing packs.
|
||||
const title = document.createElement("div");
|
||||
title.className = "koolook-preview-title koolook-preview-title-unresolved";
|
||||
title.textContent = type;
|
||||
card.appendChild(title);
|
||||
// picks rather than silently no-oping for missing packs. Renders
|
||||
// header + stub only, no slot grid — there's nothing to mock.
|
||||
appendHeader(card, type, /* unresolved */ true);
|
||||
const stub = document.createElement("div");
|
||||
stub.className = "koolook-preview-stub";
|
||||
stub.textContent = "Pack not loaded — use the ↓ Install missing button in the Tools row.";
|
||||
@@ -141,58 +139,48 @@ export function renderPreviewCard(nodeClass, type) {
|
||||
return card;
|
||||
}
|
||||
|
||||
const cat = (nodeClass.category && String(nodeClass.category)) || "";
|
||||
const titleText = (nodeClass.title && String(nodeClass.title)) || type;
|
||||
// Category / title / description — try the constructor-level properties
|
||||
// first (set by some non-ComfyUI registered nodes), then fall through to
|
||||
// `nodeData` (where ComfyUI's `registerNodeDef` parks the original def).
|
||||
const def = nodeClass.nodeData || null;
|
||||
const cat = (nodeClass.category && String(nodeClass.category))
|
||||
|| (def && def.category && String(def.category))
|
||||
|| "";
|
||||
const titleText = (nodeClass.title && String(nodeClass.title))
|
||||
|| (def && def.display_name && String(def.display_name))
|
||||
|| type;
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "koolook-preview-title";
|
||||
title.textContent = titleText;
|
||||
title.style.background = categoryColor(cat);
|
||||
card.appendChild(title);
|
||||
appendHeader(card, titleText, /* unresolved */ false, cat);
|
||||
|
||||
if (cat) {
|
||||
const catEl = document.createElement("div");
|
||||
catEl.className = "koolook-preview-cat";
|
||||
catEl.textContent = cat;
|
||||
card.appendChild(catEl);
|
||||
}
|
||||
// "PREVIEW" badge — purely a visual marker that this is a static mock,
|
||||
// not a live node. Mirrors upstream NodePreview.vue's `_sb_preview_badge`.
|
||||
const badge = document.createElement("div");
|
||||
badge.className = "koolook-preview-badge";
|
||||
badge.textContent = "Preview";
|
||||
card.appendChild(badge);
|
||||
|
||||
const slots = readSlots(nodeClass);
|
||||
if (slots.inputs.length > 0 || slots.outputs.length > 0) {
|
||||
const ioGrid = document.createElement("div");
|
||||
ioGrid.className = "koolook-preview-io";
|
||||
ioGrid.appendChild(renderSlotsColumn("Inputs", slots.inputs));
|
||||
ioGrid.appendChild(renderSlotsColumn("Outputs", slots.outputs));
|
||||
card.appendChild(ioGrid);
|
||||
// Slot rows pair inputs and outputs horizontally — row i shows
|
||||
// `[dot] [input[i].name] [spacer] [output[i].name] [dot]`. When one
|
||||
// side is shorter, the other side's later cells are blank. This is
|
||||
// what makes the card read as a node mock: the sockets line up the
|
||||
// same way they do on the real node.
|
||||
const pairCount = Math.max(slots.inputs.length, slots.outputs.length);
|
||||
for (let i = 0; i < pairCount; i++) {
|
||||
appendSlotPairRow(card, slots.inputs[i] || null, slots.outputs[i] || null);
|
||||
}
|
||||
|
||||
if (slots.widgets.length > 0) {
|
||||
const wSec = document.createElement("div");
|
||||
wSec.className = "koolook-preview-widgets";
|
||||
const wTitle = document.createElement("div");
|
||||
wTitle.className = "koolook-preview-section-title";
|
||||
wTitle.textContent = "Widgets";
|
||||
wSec.appendChild(wTitle);
|
||||
for (const w of slots.widgets) {
|
||||
const wRow = document.createElement("div");
|
||||
wRow.className = "koolook-preview-widget-row";
|
||||
const name = document.createElement("span");
|
||||
name.className = "koolook-preview-widget-name";
|
||||
name.textContent = w.name;
|
||||
wRow.appendChild(name);
|
||||
const def = document.createElement("span");
|
||||
def.className = "koolook-preview-widget-default";
|
||||
const dStr = w.default !== undefined && w.default !== null
|
||||
? ` = ${truncate(String(w.default), 32)}`
|
||||
: "";
|
||||
def.textContent = `${w.type}${dStr}`;
|
||||
wRow.appendChild(def);
|
||||
wSec.appendChild(wRow);
|
||||
}
|
||||
card.appendChild(wSec);
|
||||
// Widget rows — each rendered as a rounded "pill" with ◄ ► chrome to
|
||||
// mimic LiteGraph widget UI. The triangles are static glyphs, no
|
||||
// interaction; the value column shows the widget's default (or its
|
||||
// first choice for COMBOs).
|
||||
for (const w of slots.widgets) {
|
||||
appendWidgetRow(card, w);
|
||||
}
|
||||
|
||||
const description = nodeClass.description ? String(nodeClass.description).trim() : "";
|
||||
const description = (nodeClass.description && String(nodeClass.description).trim())
|
||||
|| (def && def.description && String(def.description).trim())
|
||||
|| "";
|
||||
if (description) {
|
||||
const desc = document.createElement("div");
|
||||
desc.className = "koolook-preview-desc";
|
||||
@@ -203,51 +191,145 @@ export function renderPreviewCard(nodeClass, type) {
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderSlotsColumn(label, slots) {
|
||||
const col = document.createElement("div");
|
||||
col.className = "koolook-preview-col";
|
||||
// Header — colored dot + display name. The dot color is derived from the
|
||||
// node's category via `categoryColor` so cards from the same category share
|
||||
// a hue, making them visually groupable at a glance. Unresolved cards get
|
||||
// the same shape with a muted dot and an "italic" title to read as a stub.
|
||||
function appendHeader(card, title, unresolved, category) {
|
||||
const header = document.createElement("div");
|
||||
header.className = "koolook-preview-header";
|
||||
if (unresolved) header.classList.add("koolook-preview-header-unresolved");
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "koolook-preview-headdot";
|
||||
dot.style.background = unresolved ? "rgba(180,120,120,0.7)" : categoryColor(category || "");
|
||||
header.appendChild(dot);
|
||||
const titleEl = document.createElement("span");
|
||||
titleEl.className = "koolook-preview-headtitle";
|
||||
titleEl.textContent = title;
|
||||
header.appendChild(titleEl);
|
||||
card.appendChild(header);
|
||||
}
|
||||
|
||||
const t = document.createElement("div");
|
||||
t.className = "koolook-preview-section-title";
|
||||
t.textContent = label;
|
||||
col.appendChild(t);
|
||||
// One row in the 5-column slot grid: `[dot] [in-name] [middle] [out-name] [dot]`.
|
||||
// Either slot may be null when the input/output counts are unequal — the
|
||||
// corresponding cells render as empty placeholders so the grid alignment
|
||||
// holds.
|
||||
function appendSlotPairRow(card, inputSlot, outputSlot) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "koolook-preview-row koolook-preview-row-slot";
|
||||
|
||||
if (slots.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "koolook-preview-empty";
|
||||
empty.textContent = "—";
|
||||
col.appendChild(empty);
|
||||
return col;
|
||||
}
|
||||
for (const slot of slots) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "koolook-preview-slot-row";
|
||||
// Column 1: input dot
|
||||
const inDot = document.createElement("div");
|
||||
inDot.className = "koolook-preview-col";
|
||||
if (inputSlot) {
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "koolook-preview-slot-dot";
|
||||
dot.style.background = slotColor(slot.type);
|
||||
row.appendChild(dot);
|
||||
const nameEl = document.createElement("span");
|
||||
nameEl.className = "koolook-preview-slot-name";
|
||||
if (slot.optional) nameEl.classList.add("koolook-preview-slot-optional");
|
||||
nameEl.textContent = slot.name;
|
||||
row.appendChild(nameEl);
|
||||
const tEl = document.createElement("span");
|
||||
tEl.className = "koolook-preview-slot-type";
|
||||
tEl.textContent = slot.type;
|
||||
row.appendChild(tEl);
|
||||
col.appendChild(row);
|
||||
dot.style.background = slotColor(inputSlot.type);
|
||||
inDot.appendChild(dot);
|
||||
}
|
||||
return col;
|
||||
row.appendChild(inDot);
|
||||
|
||||
// Column 2: input name (left-aligned)
|
||||
const inName = document.createElement("div");
|
||||
inName.className = "koolook-preview-col koolook-preview-col-input";
|
||||
if (inputSlot) {
|
||||
inName.textContent = inputSlot.name;
|
||||
if (inputSlot.optional) inName.classList.add("koolook-preview-slot-optional");
|
||||
inName.title = `${inputSlot.name}: ${inputSlot.type}`;
|
||||
}
|
||||
row.appendChild(inName);
|
||||
|
||||
// Column 3: middle spacer (empty by design)
|
||||
const mid = document.createElement("div");
|
||||
mid.className = "koolook-preview-col koolook-preview-col-middle";
|
||||
row.appendChild(mid);
|
||||
|
||||
// Column 4: output name (right-aligned)
|
||||
const outName = document.createElement("div");
|
||||
outName.className = "koolook-preview-col koolook-preview-col-output";
|
||||
if (outputSlot) {
|
||||
outName.textContent = outputSlot.name;
|
||||
outName.title = `${outputSlot.name}: ${outputSlot.type}`;
|
||||
}
|
||||
row.appendChild(outName);
|
||||
|
||||
// Column 5: output dot
|
||||
const outDot = document.createElement("div");
|
||||
outDot.className = "koolook-preview-col";
|
||||
if (outputSlot) {
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "koolook-preview-slot-dot";
|
||||
dot.style.background = slotColor(outputSlot.type);
|
||||
outDot.appendChild(dot);
|
||||
}
|
||||
row.appendChild(outDot);
|
||||
|
||||
card.appendChild(row);
|
||||
}
|
||||
|
||||
// Widget pill — same 5-column grid shape as slot rows but with `◄` / `►`
|
||||
// triangle glyphs in the dot columns and a rounded-bordered background to
|
||||
// read as a LiteGraph widget. Value column truncates long defaults.
|
||||
function appendWidgetRow(card, widget) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "koolook-preview-row koolook-preview-row-widget";
|
||||
|
||||
const arrowL = document.createElement("div");
|
||||
arrowL.className = "koolook-preview-col koolook-preview-arrow";
|
||||
arrowL.textContent = "◀";
|
||||
row.appendChild(arrowL);
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "koolook-preview-col koolook-preview-widget-name";
|
||||
name.textContent = widget.name;
|
||||
name.title = `${widget.name} (${widget.type})`;
|
||||
row.appendChild(name);
|
||||
|
||||
const mid = document.createElement("div");
|
||||
mid.className = "koolook-preview-col koolook-preview-col-middle";
|
||||
row.appendChild(mid);
|
||||
|
||||
const value = document.createElement("div");
|
||||
value.className = "koolook-preview-col koolook-preview-widget-value";
|
||||
if (widget.default !== undefined && widget.default !== null) {
|
||||
value.textContent = truncate(String(widget.default), 24);
|
||||
} else {
|
||||
value.textContent = widget.type;
|
||||
}
|
||||
row.appendChild(value);
|
||||
|
||||
const arrowR = document.createElement("div");
|
||||
arrowR.className = "koolook-preview-col koolook-preview-arrow";
|
||||
arrowR.textContent = "▶";
|
||||
row.appendChild(arrowR);
|
||||
|
||||
card.appendChild(row);
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Metadata extraction
|
||||
//
|
||||
// ComfyUI's INPUT_TYPES is convention-driven: each input is `[type, config?]`
|
||||
// where `type` is either a string (slot type like "IMAGE", "MODEL", or a
|
||||
// scalar widget type like "INT") or an array of choices (a COMBO widget).
|
||||
// The same convention is mirrored on the JS side because LiteGraph reads
|
||||
// the Python-derived definitions verbatim.
|
||||
// ComfyUI doesn't keep `INPUT_TYPES` / `RETURN_TYPES` as static properties on
|
||||
// the registered constructor. Instead, the original Python-side node def
|
||||
// (the V1 shape returned by `/object_info`) is stashed at `nodeClass.nodeData`
|
||||
// during registration (see Comfy-Org/ComfyUI_frontend `litegraphService.ts`
|
||||
// `registerNodeDef` — `node.nodeData = nodeDef` right before
|
||||
// `LiteGraph.registerNodeType`). `nodeData` exposes:
|
||||
//
|
||||
// nodeData.input.required { name: ["TYPE", config?], ... } V1 shape
|
||||
// nodeData.input.optional { name: ["TYPE", config?], ... } V1 shape
|
||||
// nodeData.output ["TYPE1", "TYPE2", ...] V1 shape
|
||||
// nodeData.output_name ["name1", "name2", ...] V1 shape
|
||||
// nodeData.description string
|
||||
// nodeData.category string
|
||||
// nodeData.display_name string
|
||||
//
|
||||
// Plus a parallel V2 transform (`inputs` record / `outputs` array) — we read
|
||||
// V1 because it's a 1-to-1 with the Python convention and matches the
|
||||
// upstream `INPUT_TYPES` shape exactly. Fall back to direct
|
||||
// `nodeClass.INPUT_TYPES` / `RETURN_TYPES` for any future or non-ComfyUI
|
||||
// LiteGraph nodes that follow the old convention.
|
||||
//
|
||||
// "Widget" classification matches ComfyUI's frontend rule: scalar types
|
||||
// (INT, FLOAT, STRING, BOOLEAN) and arrays-of-choices (COMBO). Anything
|
||||
@@ -261,28 +343,34 @@ function readSlots(nodeClass) {
|
||||
const widgets = [];
|
||||
const outputs = [];
|
||||
|
||||
let inputTypes = null;
|
||||
try {
|
||||
if (typeof nodeClass.INPUT_TYPES === "function") {
|
||||
inputTypes = nodeClass.INPUT_TYPES();
|
||||
} else if (nodeClass.INPUT_TYPES && typeof nodeClass.INPUT_TYPES === "object") {
|
||||
inputTypes = nodeClass.INPUT_TYPES;
|
||||
const def = nodeClass.nodeData || null;
|
||||
|
||||
// Inputs: prefer `nodeData.input` (V1 shape ComfyUI preserves verbatim);
|
||||
// fall back to direct `INPUT_TYPES` for non-ComfyUI registered nodes.
|
||||
let inputBlock = null;
|
||||
if (def && def.input && typeof def.input === "object") {
|
||||
inputBlock = def.input;
|
||||
} else {
|
||||
try {
|
||||
if (typeof nodeClass.INPUT_TYPES === "function") {
|
||||
inputBlock = nodeClass.INPUT_TYPES();
|
||||
} else if (nodeClass.INPUT_TYPES && typeof nodeClass.INPUT_TYPES === "object") {
|
||||
inputBlock = nodeClass.INPUT_TYPES;
|
||||
}
|
||||
} catch (e) {
|
||||
// Some legacy packs throw from INPUT_TYPES under unusual
|
||||
// conditions (missing sibling deps, lazy imports). Log so the
|
||||
// failure is visible during pack debugging instead of silently
|
||||
// rendering an empty-looking node. Matches the project-wide
|
||||
// convention (see picks_store.js, canvas_io.js, installer.js).
|
||||
console.warn("[Kforge Labs preview] INPUT_TYPES() threw for", nodeClass.title || "<no title>", e);
|
||||
inputBlock = null;
|
||||
}
|
||||
} catch (e) {
|
||||
// Some custom-node packs throw from INPUT_TYPES under unusual
|
||||
// conditions (missing sibling deps, lazy imports). Treat as
|
||||
// "no input metadata available" and continue — the card still
|
||||
// renders the title + outputs + description. Log so the failure
|
||||
// is visible during pack debugging instead of silently rendering
|
||||
// an empty-looking node. Matches the project-wide convention
|
||||
// (see picks_store.js, canvas_io.js, installer.js).
|
||||
console.warn("[Kforge Labs preview] INPUT_TYPES() threw for", nodeClass.title || "<no title>", e);
|
||||
inputTypes = null;
|
||||
}
|
||||
|
||||
if (inputTypes && typeof inputTypes === "object") {
|
||||
if (inputBlock && typeof inputBlock === "object") {
|
||||
for (const section of ["required", "optional"]) {
|
||||
const block = inputTypes[section];
|
||||
const block = inputBlock[section];
|
||||
if (!block || typeof block !== "object") continue;
|
||||
for (const [name, spec] of Object.entries(block)) {
|
||||
if (!Array.isArray(spec) || spec.length === 0) continue;
|
||||
@@ -310,8 +398,17 @@ function readSlots(nodeClass) {
|
||||
}
|
||||
}
|
||||
|
||||
const returnTypes = Array.isArray(nodeClass.RETURN_TYPES) ? nodeClass.RETURN_TYPES : [];
|
||||
const returnNames = Array.isArray(nodeClass.RETURN_NAMES) ? nodeClass.RETURN_NAMES : [];
|
||||
// Outputs: prefer `nodeData.output` (parallel `output_name` for labels);
|
||||
// fall back to direct `RETURN_TYPES` / `RETURN_NAMES` on the constructor.
|
||||
let returnTypes = [];
|
||||
let returnNames = [];
|
||||
if (def && Array.isArray(def.output)) {
|
||||
returnTypes = def.output;
|
||||
returnNames = Array.isArray(def.output_name) ? def.output_name : [];
|
||||
} else if (Array.isArray(nodeClass.RETURN_TYPES)) {
|
||||
returnTypes = nodeClass.RETURN_TYPES;
|
||||
returnNames = Array.isArray(nodeClass.RETURN_NAMES) ? nodeClass.RETURN_NAMES : [];
|
||||
}
|
||||
for (let i = 0; i < returnTypes.length; i++) {
|
||||
const t = returnTypes[i];
|
||||
const tStr = typeof t === "string" ? t : String(t);
|
||||
|
||||
Reference in New Issue
Block a user