import { app } from "../../scripts/app.js"; import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttNdropdown.js"; const widgets_to_ignore = ['control_after_generate', 'empty_latent_aspect', 'empty_latent_width', 'empty_latent_height', 'batch_size'] function getWidgetsOptions(node) { const widgetsOptions = {} const widgets = node.widgets if (!widgets) return for (const w of widgets) { if (!w.type || !w.options) continue const current_value = w.value if (widgets_to_ignore.includes(w.name)) continue //console.log(`WIDGET ${w.name}, ${w.type}, ${w.options}`) if (w.name === 'seed' || (w.name === 'value' && node.constructor.title.toLowerCase() == 'seed')) { widgetsOptions[w.name] = {'Random Seed': `${w.options.max}/${w.options.min}/${w.options.step}`} continue } if (w.type === 'ttNhidden') { if (w.options['max']) { widgetsOptions[w.name] = {[current_value]: null} continue } else if (!w.options['values']) { widgetsOptions[w.name] = {'string': null} continue } } if (w.type.startsWith('converted') || w.type === 'button') { continue } if (w.type === 'toggle') { widgetsOptions[w.name] = {'True': null, 'False': null} continue } if (['customtext', 'text', 'string'].includes(w.type)) { widgetsOptions[w.name] = {'string': null} continue } if (w.type === 'number') { widgetsOptions[w.name] = {[current_value]: null} continue } let valueDict = {} if (w.options.values) { let vals = w.options.values; if (typeof w.options.values === 'function') { vals = w.options.values() } for (const v of vals) { valueDict[v] = null } } widgetsOptions[w.name] = valueDict } //console.log('WIDGETS OPTIONS', widgetsOptions) if (Object.keys(widgetsOptions).length === 0) { return null } return widgetsOptions; } function _addInputIDs(node, inputIDs, IDsToCheck) { if (node.inputs) { for (const input of node.inputs) { if (input.link) { let originID = node.graph.links[input.link].origin_id inputIDs.push(originID); if (!IDsToCheck.includes(originID)) { IDsToCheck.push(originID); } } } } } function _recursiveGetInputIDs(node) { const inputIDs = []; const IDsToCheck = [node.id]; while (IDsToCheck.length > 0) { const currentID = IDsToCheck.pop(); const currentNode = node.graph._nodes_by_id[currentID]; if (currentNode.type === "ttN advanced xyPlot") { continue } _addInputIDs(currentNode, inputIDs, IDsToCheck); } return inputIDs; } function getNodesWidgetsDict(xyNode, plotLines=false) { const nodeWidgets = {}; if (plotLines) { nodeWidgets['Add Plot Line'] = {'Only Values Label': null, 'Title and Values Label': null, 'ID, Title and Values Label': null}; } const xyNodeLinks = xyNode.outputs[0]?.links if (!xyNodeLinks || xyNodeLinks.length == 0) { nodeWidgets['Connect to advanced xyPlot for options'] = null return nodeWidgets } const plotNodeLink = xyNodeLinks[0] const plotNodeID = xyNode.graph.links[plotNodeLink].target_id const plotNodeTitle = xyNode.graph._nodes_by_id[plotNodeID].constructor.title const plotNode = app.graph._nodes_by_id[plotNodeID] const options = getWidgetsOptions(plotNode) if (options) { nodeWidgets[`[${plotNodeID}] - ${plotNodeTitle}`] = options } const inputIDS = _recursiveGetInputIDs(plotNode) for (const iID of inputIDS) { const iNode = app.graph._nodes_by_id[iID]; const iNodeTitle = iNode.constructor.title if (iNodeTitle === 'advanced xyPlot') { continue } const options = getWidgetsOptions(iNode) if (!options) continue nodeWidgets[`[${iID}] - ${iNodeTitle}`] = getWidgetsOptions(iNode) } return nodeWidgets } function dropdownCreator(node) { if (node.widgets) { const widgets = node.widgets.filter( (n) => (n.type === "customtext") ); for (const w of widgets) { const onInput = function () { const nodeWidgets = getNodesWidgetsDict(node, true); const inputText = w.inputEl.value; const cursorPosition = w.inputEl.selectionStart; let lines = inputText.split('\n'); if (lines.length === 0) return; let cursorLineIndex = 0; let lineStartPosition = 0; for (let i = 0; i < lines.length; i++) { const lineEndPosition = lineStartPosition + lines[i].length; if (cursorPosition <= lineEndPosition) { cursorLineIndex = i; break; } lineStartPosition = lineEndPosition + 1; } ttN_CreateDropdown(w.inputEl, nodeWidgets, (selectedOption, fullpath) => { const data = fullpath.split('###'); const parts = data[0].split('/'); let output; if (parts[0] === 'Add Plot Line') { const labelType = parts[1]; let label; switch (labelType) { case 'Only Values Label': label = 'v_label'; break; case 'Title and Values Label': label = 'tv_label'; break; case 'ID, Title and Values Label': label = 'idtv_label'; break; } let lastOpeningAxisBracket = -1; let lastClosingAxisBracket = -1; let bracketCount = 0; for (let i = 0; i < inputText.length; i++) { if (inputText[i] === '[') { bracketCount++; } else if (inputText[i] === ']') { bracketCount--; } else if (inputText[i] === '<' && bracketCount === 0) { lastOpeningAxisBracket = i; } else if (inputText[i] === '>' && bracketCount === 0) { lastClosingAxisBracket = i; } } const lastAxisBracket = inputText.substring(lastOpeningAxisBracket + 1, lastClosingAxisBracket).split(':')[0]; let nextAxisBracketNumber; if (inputText.trim() === '') { w.inputEl.value = `<1:${label}>\n`; return } if (lastAxisBracket) { const lastAxisBracketNumber = Number(lastAxisBracket); if (!isNaN(lastAxisBracketNumber)) { nextAxisBracketNumber = lastAxisBracketNumber + 1; output = `<${nextAxisBracketNumber}:${label}>\n`; if (inputText[inputText.length - 1] === '\n') { w.inputEl.value = `${inputText}${output}` } else { w.inputEl.value = `${inputText}\n${output}` } return } } return } if (parts[0] === 'Connect to advanced xyPlot for options') { return } if (selectedOption === 'Random Seed') { const [max, min, step] = data[1].split('/'); const randMax = Math.min(1125899906842624, Number(max)); const randMin = Math.max(0, Number(min)); const randomRange = (randMax - Math.max(0, randMin)) / (Number(step) / 10); selectedOption = Math.floor(Math.random() * randomRange) * (Number(step) / 10) + randMin; } const nodeID = data[0].split(' - ')[0].replace('[', '').replace(']', ''); output = `[${nodeID}:${parts[1]}='${selectedOption}']`; if (inputText.trim() === '') { output = `<1:v_label>\n` + output; } if (lines[cursorLineIndex].trim() === '') { lines[cursorLineIndex] = output; } else { lines.splice(cursorLineIndex + 1, 0, output); } w.inputEl.value = lines.join('\n'); }, true); }; w.inputEl.removeEventListener('input', onInput); w.inputEl.addEventListener('input', onInput); w.inputEl.removeEventListener('mouseup', onInput); w.inputEl.addEventListener('mouseup', onInput); } } } function findUpstreamXYPlot(targetID) { const currentNode = app.graph._nodes_by_id[targetID]; if (!currentNode) { return } if (currentNode.constructor.title === 'advanced xyPlot') { return currentNode; } else { if (!currentNode.outputs) { return } for (const output of currentNode.outputs) { if (output.links?.length > 0) { for (const link of output.links) { const xyPlotNode = findUpstreamXYPlot(app.graph.links[link].target_id) if (xyPlotNode) { return xyPlotNode } } } } } } function setPlotNodeOptions(currentNode, targetID=null) { if (!targetID) { for (const output of currentNode.outputs) { if (output.links?.length > 0) { for (const link of output.links) { targetID = app.graph.links[link].target_id } } } } const xyPlotNode = findUpstreamXYPlot(targetID) if (!xyPlotNode) { return } const widgets_dict = getNodesWidgetsDict(xyPlotNode) const currentWidget = currentNode.widgets.find(w => w.name === 'node'); if (currentWidget) { currentWidget.options.values = Object.keys(widgets_dict) } } function setPlotWidgetOptions(currentNode, searchType) { const { value } = currentNode.widgets.find(w => w.name === 'node'); const nodeIdRegex = /\[(\d+)\]/; const match = value.match(nodeIdRegex); const nodeId = match ? parseInt(match[1], 10) : null; if (!nodeId) return; const optionNode = app.graph._nodes_by_id[nodeId]; if (!optionNode) return; const widgetsList = Object.values(optionNode.widgets) .filter( function(w) { if (searchType) { return searchType.includes(w.type) } } ) .map((w) => w.name); if (widgetsList) { for (const w of currentNode.widgets) { if (w.name === 'widget') { w.options.values = widgetsList } } } const widgetWidget = currentNode.widgets.find(w => w.name === 'widget'); const widgetWidgetValue = widgetWidget.value; if (searchType.includes('number')) { const int_widgets = [ 'seed', 'clip_skip', 'steps', 'start_at_step', 'end_at_step', 'empty_latent_width', 'empty_latent_height', 'noise_seed', ] const float_widgets = [ 'cfg', 'denoise', 'strength_model', 'strength_clip', 'strength', 'scale_by', 'lora_strength' ] const rangeModeWidget = currentNode.widgets.find(w => w.name === 'range_mode'); const rangeModeWidgetValue = rangeModeWidget.value; if (int_widgets.includes(widgetWidgetValue)) { rangeModeWidget.options.values = ['step_int', 'num_steps_int'] if (rangeModeWidgetValue === 'num_steps_float') { rangeModeWidget.value = 'num_steps_int' } if (rangeModeWidgetValue === 'step_float') { rangeModeWidget.value = 'step_int' } } else if (float_widgets.includes(widgetWidgetValue)) { rangeModeWidget.options.values = ['step_float', 'num_steps_float'] rangeModeWidget.value.replace('int', 'float') if (rangeModeWidgetValue === 'num_steps_int') { rangeModeWidget.value = 'num_steps_float' } if (rangeModeWidgetValue === 'step_int') { rangeModeWidget.value = 'step_float' } } else { rangeModeWidget.options.values = ['step_int', 'num_steps_int', 'step_float', 'num_steps_float'] } } if (searchType.includes('combo')) { const optionsWidget = optionNode.widgets.find(w => w.name === widgetWidgetValue) if (optionsWidget) { const values = optionsWidget.options.values currentNode.widgets.find(w => w.name === 'start_from').options.values = values currentNode.widgets.find(w => w.name === 'end_with').options.values = values currentNode.widgets.find(w => w.name === 'select').options.values = values } } } const getSetWidgets = [ "node", "widget", "start_from", "end_with", ] function getSetters(node, searchType) { if (node.widgets) { const gswidgets = node.widgets.filter(function(widget) { return getSetWidgets.includes(widget.name); }); for (const w of gswidgets) { setPlotWidgetOptions(node, searchType); let widgetValue = w.value; // Define getters and setters for widget values Object.defineProperty(w, 'value', { get() { return widgetValue; }, set(newVal) { if (newVal !== widgetValue) { widgetValue = newVal; setPlotWidgetOptions(node, searchType); } } }); } const selectWidget = node.widgets.find(w => w.name === 'select') if (selectWidget) { let widgetValue = selectWidget.value; let selectedWidget = node.widgets.find(w => w.name === 'selection'); Object.defineProperty(selectWidget, 'value', { get() { return widgetValue; }, set(newVal) { if (newVal !== widgetValue) { widgetValue = newVal; if (selectedWidget.inputEl.value.trim() === '') { selectedWidget.inputEl.value = newVal; } else { selectedWidget.inputEl.value += "\n" + newVal; } } } }) } } let mouseOver = node.mouseOver; Object.defineProperty(node, 'mouseOver', { get() { return mouseOver; }, set(newVal) { if (newVal !== mouseOver) { mouseOver = newVal; if (mouseOver) { setPlotWidgetOptions(node, searchType); setPlotNodeOptions(node); } } } }) } app.registerExtension({ name: "comfy.ttN.xyPlotAdv", beforeRegisterNodeDef(nodeType, nodeData, app) { /*if (nodeData.name === "ttN advPlot range") { const origOnConnectionsChange = nodeType.prototype.onConnectionsChange; nodeType.prototype.onConnectionsChange = function (type, slotIndex, isConnected, link_info, _ioSlot) { const r = origOnConnectionsChange ? origOnConnectionsChange.apply(this, arguments) : undefined; if (link_info && (slotIndex == 0 || slotIndex == 1)) { const originID = link_info?.origin_id const targetID = link_info?.target_id const currentNode = app.graph._nodes_by_id[originID]; setPlotNodeOptions(currentNode, targetID) } return r; }; }*/ }, nodeCreated(node) { const node_title = node.constructor.title; if (node_title === "advanced xyPlot") { dropdownCreator(node); } if (node_title === "advPlot range") { getSetters(node, ['number',]); } if (node_title === "advPlot string") { getSetters(node, ['text', 'customtext']); } if (node_title === "advPlot combo") { getSetters(node, ['combo',]); } }, });