| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { app } from "../../../../scripts/app.js"; |
|
|
| const HOVER_DELAY_MS = 250; |
| const ROW_OFFSET_PX = 12; |
| const VIEWPORT_PADDING_PX = 8; |
|
|
| |
| |
| |
| let activeCard = null; |
| let hoverTimer = null; |
|
|
| export function teardownPreview() { |
| if (hoverTimer) { |
| clearTimeout(hoverTimer); |
| hoverTimer = null; |
| } |
| if (activeCard) { |
| activeCard.remove(); |
| activeCard = null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| if (typeof document !== "undefined") { |
| document.addEventListener("visibilitychange", () => { |
| if (document.hidden) teardownPreview(); |
| }); |
| } |
| if (typeof window !== "undefined") { |
| window.addEventListener("blur", teardownPreview); |
| } |
|
|
| export function attachHoverPreview(rowEl, type) { |
| rowEl.addEventListener("mouseenter", () => { |
| teardownPreview(); |
| hoverTimer = setTimeout(() => { |
| hoverTimer = null; |
| const nodeClass = readNodeClass(type); |
| const card = renderPreviewCard(nodeClass, type); |
| if (!card) return; |
| |
| |
| |
| card.style.position = "fixed"; |
| card.style.visibility = "hidden"; |
| card.style.left = "-9999px"; |
| card.style.top = "0"; |
| document.body.appendChild(card); |
| const cardRect = card.getBoundingClientRect(); |
| const rowRect = rowEl.getBoundingClientRect(); |
| const vw = window.innerWidth; |
| const vh = window.innerHeight; |
| |
| |
| |
| |
| |
| const wouldOverflowRight = rowRect.right + ROW_OFFSET_PX + cardRect.width > vw; |
| const left = wouldOverflowRight |
| ? Math.max(VIEWPORT_PADDING_PX, rowRect.left - ROW_OFFSET_PX - cardRect.width) |
| : rowRect.right + ROW_OFFSET_PX; |
| const top = Math.max( |
| VIEWPORT_PADDING_PX, |
| Math.min(vh - cardRect.height - VIEWPORT_PADDING_PX, rowRect.top), |
| ); |
| card.style.left = `${left}px`; |
| card.style.top = `${top}px`; |
| card.style.visibility = "visible"; |
| activeCard = card; |
| }, HOVER_DELAY_MS); |
| }); |
| rowEl.addEventListener("mouseleave", teardownPreview); |
| } |
|
|
| |
| |
| |
|
|
| function readNodeClass(type) { |
| if (typeof LiteGraph === "undefined") return null; |
| const reg = LiteGraph.registered_node_types || {}; |
| return reg[type] || null; |
| } |
|
|
| export function renderPreviewCard(nodeClass, type) { |
| const card = document.createElement("div"); |
| card.className = "koolook-preview-card"; |
|
|
| if (!nodeClass) { |
| |
| |
| |
| |
| |
| appendHeader(card, type, 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."; |
| card.appendChild(stub); |
| return card; |
| } |
|
|
| |
| |
| |
| 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; |
|
|
| appendHeader(card, titleText, false, cat); |
|
|
| |
| |
| const badge = document.createElement("div"); |
| badge.className = "koolook-preview-badge"; |
| badge.textContent = "Preview"; |
| card.appendChild(badge); |
|
|
| const slots = readSlots(nodeClass); |
| |
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| |
| for (const w of slots.widgets) { |
| appendWidgetRow(card, w); |
| } |
|
|
| 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"; |
| desc.textContent = description; |
| card.appendChild(desc); |
| } |
|
|
| return card; |
| } |
|
|
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| |
| function appendSlotPairRow(card, inputSlot, outputSlot) { |
| const row = document.createElement("div"); |
| row.className = "koolook-preview-row koolook-preview-row-slot"; |
|
|
| |
| 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(inputSlot.type); |
| inDot.appendChild(dot); |
| } |
| row.appendChild(inDot); |
|
|
| |
| 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); |
|
|
| |
| const mid = document.createElement("div"); |
| mid.className = "koolook-preview-col koolook-preview-col-middle"; |
| row.appendChild(mid); |
|
|
| |
| 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); |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
| 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); |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const SCALAR_WIDGET_TYPES = new Set(["INT", "FLOAT", "STRING", "BOOLEAN"]); |
|
|
| function readSlots(nodeClass) { |
| const inputs = []; |
| const widgets = []; |
| const outputs = []; |
|
|
| const def = nodeClass.nodeData || null; |
|
|
| |
| |
| 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) { |
| |
| |
| |
| |
| |
| console.warn("[Kforge Labs preview] INPUT_TYPES() threw for", nodeClass.title || "<no title>", e); |
| inputBlock = null; |
| } |
| } |
|
|
| if (inputBlock && typeof inputBlock === "object") { |
| for (const section of ["required", "optional"]) { |
| 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; |
| const t = spec[0]; |
| const config = (spec[1] && typeof spec[1] === "object") ? spec[1] : {}; |
| if (Array.isArray(t)) { |
| |
| |
| const fallback = t.length > 0 ? t[0] : undefined; |
| widgets.push({ |
| name, |
| type: "COMBO", |
| default: config.default !== undefined ? config.default : fallback, |
| }); |
| } else if (typeof t === "string" && SCALAR_WIDGET_TYPES.has(t)) { |
| widgets.push({ name, type: t, default: config.default }); |
| } else { |
| inputs.push({ |
| name, |
| type: typeof t === "string" ? t : String(t), |
| optional: section === "optional", |
| }); |
| } |
| } |
| } |
| } |
|
|
| |
| |
| 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); |
| const name = returnNames[i] != null ? String(returnNames[i]) : tStr; |
| outputs.push({ name, type: tStr }); |
| } |
| return { inputs, widgets, outputs }; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| function categoryColor(category) { |
| if (!category) return "rgba(120,120,120,0.6)"; |
| let hash = 0; |
| for (let i = 0; i < category.length; i++) { |
| hash = ((hash << 5) - hash + category.charCodeAt(i)) | 0; |
| } |
| const hue = Math.abs(hash) % 360; |
| return `hsl(${hue}, 38%, 32%)`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function slotColor(type) { |
| const key = String(type); |
| |
| |
| |
| |
| |
| |
| |
| const runtime = (app && app.canvas && app.canvas.default_connection_color_byType) || null; |
| if (runtime && typeof runtime[key] === "string") return runtime[key]; |
| const staticMap = (typeof LiteGraph !== "undefined" |
| && LiteGraph.LGraphCanvas |
| && LiteGraph.LGraphCanvas.link_type_colors) || {}; |
| if (typeof staticMap[key] === "string") return staticMap[key]; |
| return "rgba(180,180,180,0.5)"; |
| } |
|
|
| function truncate(s, n) { |
| return s.length > n ? `${s.slice(0, n - 1)}…` : s; |
| } |
|
|