// SPDX-FileCopyrightText: 2025-2026 Kforge Labs // SPDX-License-Identifier: GPL-3.0-only function isNumber(value) { return typeof value === "number" && Number.isFinite(value); } function rectFromGroup(group) { if (!group || typeof group !== "object") return null; if (Array.isArray(group.bounding) && group.bounding.length >= 4) { if (!group.bounding.slice(0, 4).every(isNumber)) return null; return { x: group.bounding[0], y: group.bounding[1], w: group.bounding[2], h: group.bounding[3], }; } if (Array.isArray(group.pos) && Array.isArray(group.size)) { if (!group.pos.slice(0, 2).every(isNumber) || !group.size.slice(0, 2).every(isNumber)) { return null; } return { x: group.pos[0], y: group.pos[1], w: group.size[0], h: group.size[1], }; } return null; } function rectFromNode(node) { if (!node || typeof node !== "object" || !Array.isArray(node.pos)) return null; const size = Array.isArray(node.size) ? node.size : [200, 100]; if (!node.pos.slice(0, 2).every(isNumber) || !size.slice(0, 2).every(isNumber)) { return null; } return { x: node.pos[0], y: node.pos[1], w: size[0], h: size[1], }; } function rectsOverlap(a, b) { if (!a || !b || a.w <= 0 || a.h <= 0 || b.w <= 0 || b.h <= 0) return false; return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; } function nodeSummary(node) { const type = typeof node?.type === "string" ? node.type : ""; const title = typeof node?.title === "string" && node.title ? node.title : (type || String(node?.id)); return { id: String(node?.id), type, title, }; } const PUBLISH_INPUT_CLASS = "Koolook_PublishInput"; const PUBLISH_OUTPUT_CLASS = "Koolook_PublishOutput"; const PUBLISH_RESULT_CLASS = "Koolook_PublishResult"; const WIDGET_NAMES_BY_CLASS = { [PUBLISH_INPUT_CLASS]: ["mode", "sequence_folder", "qt_file", "single_file", "prompt"], [PUBLISH_OUTPUT_CLASS]: ["folder", "name", "version"], [PUBLISH_RESULT_CLASS]: ["result"], }; const PUBLISH_INPUT_FIELDS = [ ["sequence_folder", "Sequence folder", true], ["qt_file", "QT file", true], ["single_file", "Single file", true], ]; // The prompt is an always-on field, independent of the EXR/QT/Img source // switch: an external user picks a source AND can describe the shot. The // author's prompt-widget text becomes the placeholder hint; the submitted // default is empty so an untouched hint is never sent as the real prompt. const PUBLISH_PROMPT_HELP = "Describe the shot in one simple line: subject + action + setting."; const PUBLISH_INPUT_MODES = [ [0, "EXR", "sequence_folder"], [1, "QT", "qt_file"], [2, "Img", "single_file"], [3, "Prompt", "prompt"], ]; const PUBLISH_OUTPUT_FIELDS = [ ["folder", "Output folder", true], ["name", "Output name", true], ["version", "Version", true], ]; const PUBLISH_RESULT_FIELDS = [ ["result", "Result", true], ]; function groupedNodes(graph, title) { const groups = Array.isArray(graph?.groups) ? graph.groups : []; const nodes = Array.isArray(graph?.nodes) ? graph.nodes : []; const nodeRects = nodes .filter(node => node && typeof node === "object") .map(node => ({ node, rect: rectFromNode(node) })); return groups .filter(group => group && typeof group === "object" && group.title === title) .map(group => ({ group: title, nodes: nodeRects .filter(({ rect }) => rectsOverlap(rectFromGroup(group), rect)) .map(({ node }) => nodeSummary(node)), })) .filter(entry => entry.nodes.length > 0); } function nodesInGroup(graph, title) { const groups = Array.isArray(graph?.groups) ? graph.groups : []; const nodes = Array.isArray(graph?.nodes) ? graph.nodes : []; const nodeRects = nodes .filter(node => node && typeof node === "object") .map(node => ({ node, rect: rectFromNode(node) })); const out = []; for (const group of groups) { if (!group || typeof group !== "object" || group.title !== title) continue; const groupRect = rectFromGroup(group); for (const { node, rect } of nodeRects) { if (rectsOverlap(groupRect, rect)) out.push(node); } } return out; } function firstNodeOfType(nodes, classType) { return nodes.find(node => node?.type === classType) || null; } function widgetValue(node, key) { const values = node?.widgets_values; if (values && typeof values === "object" && !Array.isArray(values)) return values[key]; if (!Array.isArray(values)) return null; const names = WIDGET_NAMES_BY_CLASS[node?.type] || []; const index = names.indexOf(key); return index >= 0 && index < values.length ? values[index] : null; } function fieldSpecs(node, specs) { if (!node) return []; return specs.map(([key, label, visible]) => ({ key, label, visible, target: { node: String(node.id), input: key }, default: widgetValue(node, key), })); } function inputModeIndex(value) { if (Number.isInteger(value)) return value; const text = String(value ?? "").trim().toLowerCase(); const found = PUBLISH_INPUT_MODES.find(([_value, label]) => label.toLowerCase() === text); return found ? found[0] : 2; } function inputSwitch(node, inputs) { const inputsByKey = Object.fromEntries(inputs.map(item => [item.key, item])); return { key: "switch", label: "Input type", visible: true, target: { node: String(node.id), input: "mode" }, default: inputModeIndex(widgetValue(node, "mode")), options: PUBLISH_INPUT_MODES.map(([value, label, input]) => ({ value, label, // Standalone fields (e.g. the always-on prompt) are not source // modes, so they never appear as a switch option. visible: Boolean(inputsByKey[input]?.visible) && !inputsByKey[input]?.standalone, input, })), }; } function promptField(node) { if (!node) return null; const hint = widgetValue(node, "prompt"); return { key: "prompt", label: "Prompt", visible: true, standalone: true, multiline: true, target: { node: String(node.id), input: "prompt" }, default: "", placeholder: typeof hint === "string" ? hint : "", help: PUBLISH_PROMPT_HELP, }; } function inferAppSurface(graph) { const inputNode = firstNodeOfType(nodesInGroup(graph, "Koolook Input"), PUBLISH_INPUT_CLASS); const outputNodes = nodesInGroup(graph, "Koolook Output"); const outputNode = firstNodeOfType(outputNodes, PUBLISH_OUTPUT_CLASS); const resultNode = firstNodeOfType(outputNodes, PUBLISH_RESULT_CLASS); const inputs = fieldSpecs(inputNode, PUBLISH_INPUT_FIELDS); const prompt = promptField(inputNode); if (prompt) inputs.push(prompt); const app = { inputs, outputs: fieldSpecs(outputNode, PUBLISH_OUTPUT_FIELDS), results: fieldSpecs(resultNode, PUBLISH_RESULT_FIELDS), }; if (inputNode) app.switch = inputSwitch(inputNode, inputs); return app; } export function inferSetupSurface(graph) { return { sourceInputs: groupedNodes(graph, "Koolook Input"), outputs: groupedNodes(graph, "Koolook Output"), controls: [], app: inferAppSurface(graph), }; }