| |
| |
|
|
| 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], |
| ]; |
| |
| |
| |
| |
| 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, |
| |
| |
| 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), |
| }; |
| } |
|
|