/* globals LiteGraph */ import {app} from "../../scripts/app.js" // import {api} from "../../scripts/api.js"; import {PresentationDetachInputs} from "./PresentationDetachInputs.js"; import {PresentationSettings} from "./PresentationSettings.js"; class WorkflowToTable { static presentationNodes = {}; static presentationPrimitiveNodes = { "PresentationDropDown": true, "Note": true, "MarkdownNote": true, }; static presentationInputNodes = { "PresentationFile": true, "PresentationAudio": true, "PresentationVideo": true, "PresentationImage": true, "PresentationSeed": true, "PresentationAddQueue": true, }; static presentationOutputNodes = { "PreviewImage": true, "SaveImage": true, "PreviewAudio": true, "SaveAudio": true, "SaveVideo": true, "VHS_VideoCombine": true, }; static presentationMiscNodes = { "PresentationProgress": true, "PresentationComponent": true, "PresentationAdvancedSection": true, } static ignoreNodes = { "PresentationTab": true }; static makePresentationNodes() { Object.assign( WorkflowToTable.presentationNodes, WorkflowToTable.presentationPrimitiveNodes, WorkflowToTable.presentationInputNodes, WorkflowToTable.presentationOutputNodes, WorkflowToTable.presentationMiscNodes, ); } findGroupOfNode(node, groups) { const x = node.pos[0] + node.size[0] / 2; const y = node.pos[1] + node.size[1] / 2; for (const group of groups) { const isInside = LiteGraph.isInsideRectangle( x, y, group.bounding[0], group.bounding[1], group.bounding[2], group.bounding[3] ); if (isInside) { return group; } } return null; } getBoxFromNode(node) { const left = node.pos[0]; const right = left + node.size[0]; const top = node.pos[1]; const bottom = top + node.size[1]; return {left, top, right, bottom}; } getHorzNodeGaps(nodes) { return nodes.map((node) => { const nodeBox = this.getBoxFromNode(node); return { left:nodeBox.top, right:nodeBox.bottom, top:nodeBox.left, bottom:nodeBox.right, }; }); } getVertNodeGaps(nodes) { return nodes.map((node) => { const nodeBox = this.getBoxFromNode(node); return { left:nodeBox.left, right:nodeBox.right, top:nodeBox.top, bottom:nodeBox.bottom, }; }); } findGaps(nodeGaps) { const vertGaps = nodeGaps.map((nodeBox) => { let closestBelow = Number.MAX_VALUE; for (const nBox of nodeGaps) { if ( (nodeBox.left <= nBox.left && nBox.left < nodeBox.right) || (nodeBox.left <= nBox.right && nBox.right < nodeBox.right) || (nBox.left < nodeBox.left && nodeBox.right < nBox.right) ) { if (nBox.top < closestBelow && nBox.top > nodeBox.bottom) { closestBelow = nBox.top; } } } return [nodeBox.bottom, closestBelow]; }); for (const gap of vertGaps) { for (const thinGap of vertGaps) { if (gap[0] < thinGap[0] && thinGap[0] < gap[1]) { gap[0] = thinGap[0]; } if (gap[0] < thinGap[1] && thinGap[1] < gap[1]) { gap[1] = thinGap[1]; } } } const vertGapsHash = vertGaps.reduce((a, gap) => { a[`${gap[0]},${gap[1]}`]=true; return a; }, {}); const finalVertGaps = Object.keys(vertGapsHash).map( (gapStr) => gapStr.split(',') ); finalVertGaps.sort((a,b) => { return a[0] - b[0]; }); return finalVertGaps; } findNthGap(pos, vertGaps) { let row = 0; for (const gap of vertGaps) { if (pos < gap[1]) { break; } ++row; } return row; } mapRowNumbers(nodes) { const nodeInfos = {} const rows = []; const horzGaps = this.findGaps(this.getHorzNodeGaps(nodes)); const vertGaps = this.findGaps(this.getVertNodeGaps(nodes)); for (const node of nodes) { const info = {node}; nodeInfos[node.id] = info; const nodeBox = this.getBoxFromNode(node); const topRow = this.findNthGap(nodeBox.top, vertGaps); const bottomRow = this.findNthGap(nodeBox.bottom, vertGaps); const leftCol = this.findNthGap(nodeBox.left, horzGaps); const rightCol = this.findNthGap(nodeBox.right, horzGaps); let rowArr = rows[topRow]; if (!rowArr) { rowArr = rows[topRow] = []; } info.row = topRow; info.rowSpan = (bottomRow - topRow) + 1; info.col = leftCol; info.colSpan = (rightCol - leftCol) + 1; rowArr[info.col] = info; } return { nodeInfos, rows, horzGaps, vertGaps }; } makeTable(nodes) { const {rows, horzGaps, vertGaps} = this.mapRowNumbers(nodes); const tableRows = []; for (const rowNodeInfos of rows) { const tableRow = []; for (const nodeInfo of rowNodeInfos) { tableRows.push(nodeInfo?.node); } tableRows.push(tableRow); } return {tableRows, rows, horzGaps, vertGaps}; } static getPresentationPrice(presentationTab) { return Number.parseInt(presentationTab?.widgets_values?.[1]); } static getPresentationTabName(presentationTab) { return presentationTab?.widgets_values?.[0]; } getGroupInfos(workflow) { const groupInfos = { }; const presentationTabs = workflow.nodes.filter((node) => { return (WorkflowToTable.ignoreNodes[node.type]); }); for (const presentationNodeId in presentationTabs) { const presentationTab = presentationTabs[presentationNodeId]; const group = this.findGroupOfNode(presentationTab, workflow.groups); if (group) { groupInfos[group.id] = { nodes: [], group, presentationTab, }; } } for (const node of workflow.nodes) { // if (node && WorkflowToTable.okNodes[node.type]) { if (node && !WorkflowToTable.ignoreNodes[node.type]) { const group = this.findGroupOfNode(node, workflow.groups); if (group) { const groupInfo = groupInfos[group.id]; if (groupInfo) { groupInfo.nodes.push(node); } } } } // console.log('groupInfos',groupInfos); return groupInfos; } } class ComfyToVueType { constructor() { const numberType = { type:'Number', html:(widget_id, v_input_name, input_name) => { return { html:``, } }, scriptLine:(widget_id, v_input_name, input_name) => { return `const size_${input_name} = props.size || props.value0?.toString()?.length`; }, }; const stringType = { type:'String', html:(widget_id, v_input_name, input_name) => { return { html:``, }; }, }; const textBoxType = { type:'String', html:(widget_id, v_input_name, input_name) => { return { html:``, }; }, scriptLine:(widget_id, v_input_name, input_name) => { if (input_name === 'value') { return `const r_${input_name} = ref(props.${v_input_name});\n`; } return ''; }, }; const toggleType = { type:'Boolean', html:(widget_id, v_input_name, input_name) => { return { html:``, }; }, }; const comboType = { type:'String', html:(widget_upto, v_input_name, input_name /*, widget */) => { const optionsId = `options_${input_name}`; return { html:``, }; }, }; this.comfyToVueTypeMap = { 'number':numberType, 'toggle':toggleType, 'text':stringType, 'customtext':textBoxType, 'combo':comboType, }; } comfyToVueType(type) { return this.comfyToVueTypeMap[type]; } } class PresentationTab { constructor() { this.onChangeTimeoutId = null; this.prevWorkflow = null; this.presentationNodes = {}; this.prevPromptStr = null; this.comfyToVueType = new ComfyToVueType(); // this.debug = true; } static getNodeClass() { return 'PresentationTab'; } getWidgetFromLinkId(graph, destLinkId) { const link = graph.links[destLinkId]; const destNodeId = link.target_id; const destNodeSlot = link.target_slot; const destNode = graph.getNodeById(destNodeId); if (!destNode) { return null; } const name = destNode.inputs[destNodeSlot]?.name; for (const widget of destNode.widgets) { if (widget.name === name) { return widget; } } return null; } fixTypeName(name) { return name.replace(/[^a-zA-Z0-9_]/g,''); } getCamelName(name) { return name.substring(0,1).toUpperCase() + name.substring(1).toLowerCase(); } getGraphNodeInfo(graphNode) { const inputsByName = {}; for (const input of graphNode.inputs) { inputsByName[input.name] = input; } const widgetsByName = {}; if (graphNode.widgets) { for (const widget of graphNode.widgets) { widgetsByName[widget.name] = widget; } } return { graphNode, inputsByName, widgetsByName, }; } makeComponent(workflowNode, graphNodeInfo /*, componentName*/) { const graphNode = graphNodeInfo.graphNode; const okWidgets = []; for (const widget of graphNode.widgets) { const input = graphNodeInfo.inputsByName[widget.name]; if (!input) { console.log('widget not found in inputs', widget.name, workflowNode, graphNode); continue; } if (input.link) { continue; } const vueType = this.comfyToVueType.comfyToVueType(widget.type); if (!vueType) { continue; } okWidgets.push({widget, vueType}); } const scripts = []; const scriptLines = []; const htmlInputs = []; let widget_upto = -1; const oneWidget = (okWidgets.length === 1); for (const widgetObj of okWidgets) { const {widget, vueType}=widgetObj; ++widget_upto; const input_name = widget.name; const title = oneWidget ? '{{title}}' : input_name ; const safe_input_name = input_name.replace(/[^a-zA-Z0-9_]/g,'') const v_input_name = `v_${safe_input_name}`; scripts.push(`"${v_input_name}": { type: ${vueType.type} }`); const options_input_name = `options_${safe_input_name}`; scripts.push(`"${options_input_name}": { type: Array }`); if(vueType.scriptLine) { scriptLines.push(vueType.scriptLine(widget_upto, v_input_name, safe_input_name, widget)); } const htmlInput = vueType.html(widget_upto, v_input_name, safe_input_name, widget); if (oneWidget) { htmlInputs.push(htmlInput.html); } else { htmlInputs.push(`${htmlInput.html}`); } } const extraScript = 'const idClass = `workflowid-${props.id}`;'; const scr = `const props = defineProps({ id: { type: String, required: true, }, title: { type: String, }, ${scripts.join(",\n")} }); ${scriptLines.join("\n")}\n ${extraScript}`; const htmlTable = oneWidget ? htmlInputs[0] : `\n${htmlInputs.join("\n")}\n
` ; const html = `
${htmlTable}
`; return { oneWidget, html:`\n` }; } quoteAttribute(value) { return value?.replace ? value.replace(/"/g,'"') : value; } workflowToHtml(rows, graph, camelTabName) { const newRows = []; const js = []; const customs = {}; let hasAddQueue = false; // value types that need : in the value const directWidgetTypes={ number: true, toggle: true }; const ignoreWidgetTypes={ button: true }; for (const row of rows) { const newRow = []; for (const nodeInfo of row) { if (!nodeInfo) { // newRow.push(''); continue; } let tdClasses = ''; let tdOptions = ""; if (nodeInfo.rowSpan > 1) { tdOptions += ` rowspan="${nodeInfo.rowSpan}"`; tdClasses += ` pres-rowspan-${nodeInfo.rowSpan}`; } const nodeType = LiteGraph.registered_node_types[nodeInfo.node.type]; const workflowNode = nodeInfo.node; const title = workflowNode.title || nodeType.title; const graphNode = graph.getNodeById(workflowNode.id); const graphNodeInfo = this.getGraphNodeInfo(graphNode); const optionsStrs = []; const valueStrs = []; const widgetsValuesLen = workflowNode.widgets_values?.length || 0; for (let widget_id = 0; widget_id < widgetsValuesLen; ++widget_id) { const widgetDef = graphNode?.widgets?.[widget_id]; const options = widgetDef?.options?.values; const widget_name = widgetDef.name; const value = workflowNode.widgets_values.length === 0 ? '' : workflowNode.widgets_values[widget_id]; if (options && (graphNodeInfo.inputsByName[widget_name] || graphNodeInfo.widgetsByName[widget_name]) ) { const optionsId = `options_${workflowNode.id}_${widget_name}`; optionsStrs.push( `:options_${widget_name}="${optionsId}"` ); js.push(`const ${optionsId} = ${JSON.stringify(options)};`); } //if (workflowNode.widgets_values.length > 0) { if (value !== "" && !ignoreWidgetTypes[widgetDef.type]) { valueStrs.push(`${directWidgetTypes[widgetDef.type] ? ':' : ''}v_${widget_name}="${this.quoteAttribute(value)}"`); } //} } const valueStr = valueStrs.join(' '); const optionsStr = optionsStrs.join(' '); let type = (workflowNode.type === "PresentationComponent") ? workflowNode.widgets_values[0] : workflowNode.type ; // type = this.getCamelName(this.fixTypeName(type)); type = this.fixTypeName(type); if (type === 'PresentationAddQueue') { hasAddQueue = true; } let oneWidget1 = null; let componentName = `Node${type}`; if (!WorkflowToTable.presentationNodes[workflowNode.type] && !WorkflowToTable.ignoreNodes[workflowNode.type]) { componentName = `Tabs${camelTabName}${type}`; const { html, oneWidget } = this.makeComponent(workflowNode, graphNodeInfo, componentName); oneWidget1 = oneWidget; customs[type] = html; } const nodeElem = `<${componentName} id="${workflowNode.id}" title="${title}" ${valueStr} ${optionsStr} />`; const colSpan = nodeInfo.colSpan * (oneWidget1?1:2); tdOptions += ` colspan="${colSpan}"`; if (colSpan > 1) { tdClasses += ' pres-colspan-${colSpan}'; } if (oneWidget1) { newRow.push(`${nodeElem}`); } else { newRow.push(`${nodeElem}`); } } newRows.push(newRow.join("\n")); } const addQueueElem = hasAddQueue? '': ''; return { js:js.join("\n"), customs, html:`
\n${newRows.join("\n")}
\n${addQueueElem}
` }; } getWorkflowIdsFromRows(rows) { const workflowIds = {}; for (const row of rows) { for (const nodeInfo of row) { if (nodeInfo) { workflowIds[nodeInfo.node.id] = true; } } } return workflowIds; } sendWorkflow(promptObj) { const { output, workflow } = promptObj; // this.cleanPresentationNodes(workflow); return this.getTableRows(window.app).then((rowsByGroup) => { for (const groupInfoId in rowsByGroup) { const obj = rowsByGroup[groupInfoId]; const {rows, groupInfo} = obj; console.log('workflow rows', rows); const workflowIds = this.getWorkflowIdsFromRows(rows); const price = WorkflowToTable.getPresentationPrice(groupInfo.presentationTab); const title = WorkflowToTable.getPresentationTabName(groupInfo.presentationTab); const name = this.fixTypeName(title); // const tabClassName = name.toLowerCase(); const camelTabName = this.getCamelName(name); const { html, js, customs } = this.workflowToHtml(rows, app.graph, camelTabName); const generatedMessage = 'Do not edit. Generated from presentations-ComfyUI. Will be wiped when someone changes the workflow.'; const template = `\n\n`; const toSave = { template, workflow:{ workflow_api:output, workflowIds, workflow, price, }, customs, title, camelTabName, name, }; console.log('toSave', toSave); return fetch('/custom_nodes/presentation-ComfyUI/save', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(toSave), }) .then((response) => { try { return response.json() } catch(e) { console.error('bad save response', e); return response.text().then((txt) => { console.log('text response', txt); }); } }) .then(data => console.log('saved', data)) .catch(error => console.error('Error:', error)); } }); } restartNuxt() { if ( this.restartingNuxt) { console.warn('Already restarting'); return; } this.restartingNuxt = true; return fetch('/custom_nodes/presentation-ComfyUI/restart', { }).then((resp) => { if (!resp.ok) { return {ok:false, message:resp.statusText}; } return resp.json(); }).finally(() => { this.restartingNuxt = false; }); } checkChanges() { return window.app.pres_origGraphToPrompt().then((promptObj) => { if (promptObj.output) { const workflowJson = JSON.stringify(promptObj.output); // if (workflowJson !== this.prevWorkflow) { this.prevWorkflow = workflowJson; return this.sendWorkflow(promptObj); // } } return Promise.resolve(); }); } getTableRows(app) { return app.pres_origGraphToPrompt().then((promptObj) => { const promptStr = JSON.stringify(promptObj); // if (this.prevPromptStr !== promptStr) { this.prevPromptStr = promptStr; const workflow = promptObj.workflow; const workflowToTable = new WorkflowToTable(); const groupInfos = workflowToTable.getGroupInfos(workflow); const rowsByGroup = {}; for (const groupInfoId in groupInfos) { const groupInfo = groupInfos[groupInfoId]; const obj = workflowToTable.makeTable(groupInfo.nodes); obj.groupInfo = groupInfo; rowsByGroup[groupInfoId] = obj; } this.prevRowsByGroup = rowsByGroup; // } return this.prevRowsByGroup; }); } onRender(app, canvas, ctx) { return this.getTableRows(app).then((rowsByGroup) => { for (const groupInfoId in rowsByGroup) { const obj = rowsByGroup[groupInfoId]; const { rows, groupInfo} = obj; for (const row of rows) { for (const nodeInfo of row) { if (!nodeInfo) { continue; } const node = nodeInfo.node; ctx.save(); app.canvas.ds.toCanvasContext(ctx); ctx.translate(node.pos[0], node.pos[1]-32); const tabName = WorkflowToTable.getPresentationTabName(groupInfo.presentationTab); if (this.debug) { ctx.fillText(`node: xy:${nodeInfo.col},${nodeInfo.row} xyspan:${nodeInfo.colSpan}x${nodeInfo.rowSpan}: name:${tabName}`, 0, 0) } ctx.restore(); } } } }); } initCanvas(app) { const oldOnRender = app.canvas.onRender; app.canvas.onRender = (canvas, ctx) => { this.onRender(app, canvas, ctx); if (oldOnRender) { return oldOnRender(canvas, ctx); } }; } init() { const t = this; // api.addEventListener("graphChanged", () => this.onChangeTimeout()); app.registerExtension({ name: 'PresentationTab', async beforeRegisterNodeDef(nodeType , nodeData, app ) { if (nodeType.comfyClass === PresentationTab.getNodeClass()) { if (t.debug) { t.initCanvas(app); } new PresentationDetachInputs().init(app); } }, async nodeCreated(node) { /* * TODO: window click node.onConnectionChange( (inOut, connected, link_info, input_info) => { console.log('onconenctionchange', arguments, node); // TODO }); */ if (node && node.comfyClass === PresentationTab.getNodeClass()) { try { node.addWidget('button', "Save", "", () => { console.log('presentation.save'); return t.checkChanges(); }); const restartName = 'Restart'; const restartButton = node.addWidget('button', restartName, "", async () => { console.log('presentation.restart'); restartButton.name = `⟳ ${restartName}`; const result = await t.restartNuxt(); if (!result.ok) { console.error('restart failed', result.message); } else { restartButton.name = `✓ ${restartName}`; setTimeout(() => { restartButton.name = restartName; }, 5000); } }); t.presentationNodes[node.id] = node; node.onRemoved = () => { delete t.presentationNodes[node.id]; console.log('removed', node); }; } catch(e) { console.error('nodeCreated error', e); } } } }); } } WorkflowToTable.makePresentationNodes(); new PresentationTab().init(); new PresentationSettings().init();