/** * File: comfy_shared.js * Project: comfy_mtb * Author: Mel Massadian * * Copyright (c) 2023-2024 Mel Massadian * */ // Reference the shared typedefs file /// import { app } from '../../scripts/app.js' import { api } from '../../scripts/api.js' // #region base utils // - crude uuid export function makeUUID() { let dt = new Date().getTime() const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = ((dt + Math.random() * 16) % 16) | 0 dt = Math.floor(dt / 16) return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) }) return uuid } //- local storage manager export class LocalStorageManager { constructor(namespace) { this.namespace = namespace } _namespacedKey(key) { return `${this.namespace}:${key}` } set(key, value) { const serializedValue = JSON.stringify(value) localStorage.setItem(this._namespacedKey(key), serializedValue) } get(key, default_val = null) { const value = localStorage.getItem(this._namespacedKey(key)) return value ? JSON.parse(value) : default_val } remove(key) { localStorage.removeItem(this._namespacedKey(key)) } clear() { for (const key of Object.keys(localStorage).filter((k) => k.startsWith(`${this.namespace}:`), )) { localStorage.removeItem(key) } } } // - log utilities function createLogger(emoji, color, consoleMethod = 'log') { return (message, ...args) => { if (window.MTB?.DEBUG) { console[consoleMethod]( `%c${emoji} ${message}`, `color: ${color};`, ...args, ) } } } export const infoLogger = createLogger('ℹ️', 'yellow') export const warnLogger = createLogger('⚠️', 'orange', 'warn') export const errorLogger = createLogger('🔥', 'red', 'error') export const successLogger = createLogger('✅', 'green') export const log = (...args) => { if (window.MTB?.DEBUG) { console.debug(...args) } } /** * Deep merge two objects. * @param {Object} target - The target object to merge into. * @param {...Object} sources - The source objects to merge from. * @returns {Object} - The merged object. */ export function deepMerge(target, ...sources) { if (!sources.length) return target const source = sources.shift() for (const key in source) { if (source[key] instanceof Object) { if (!target[key]) Object.assign(target, { [key]: {} }) deepMerge(target[key], source[key]) } else { Object.assign(target, { [key]: source[key] }) } } return deepMerge(target, ...sources) } // #endregion // #region widget utils export const CONVERTED_TYPE = 'converted-widget' export function hideWidget(node, widget, suffix = '') { widget.origType = widget.type widget.hidden = true widget.origComputeSize = widget.computeSize widget.origSerializeValue = widget.serializeValue widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically widget.type = CONVERTED_TYPE + suffix widget.serializeValue = () => { // Prevent serializing the widget if we have no input linked const { link } = node.inputs.find((i) => i.widget?.name === widget.name) if (link == null) { return undefined } return widget.origSerializeValue ? widget.origSerializeValue() : widget.value } // Hide any linked widgets, e.g. seed+seedControl if (widget.linkedWidgets) { for (const w of widget.linkedWidgets) { hideWidget(node, w, `:${widget.name}`) } } } /** * Show widget * * @param {import("../../../web/types/litegraph.d.ts").IWidget} widget - target widget */ export function showWidget(widget) { widget.type = widget.origType widget.computeSize = widget.origComputeSize widget.serializeValue = widget.origSerializeValue delete widget.origType delete widget.origComputeSize delete widget.origSerializeValue // Hide any linked widgets, e.g. seed+seedControl if (widget.linkedWidgets) { for (const w of widget.linkedWidgets) { showWidget(w) } } } export function convertToWidget(node, widget) { showWidget(widget) const sz = node.size node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)) for (const widget of node.widgets) { widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT } // Restore original size but grow if needed node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) } export function convertToInput(node, widget, config) { hideWidget(node, widget) const { linkType } = getWidgetType(config) // Add input and store widget config for creating on primitive node const sz = node.size node.addInput(widget.name, linkType, { widget: { name: widget.name, config }, }) for (const widget of node.widgets) { widget.last_y += LiteGraph.NODE_SLOT_HEIGHT } // Restore original size but grow if needed node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) } export function hideWidgetForGood(node, widget, suffix = '') { widget.origType = widget.type widget.origComputeSize = widget.computeSize widget.origSerializeValue = widget.serializeValue widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically widget.type = CONVERTED_TYPE + suffix // widget.serializeValue = () => { // // Prevent serializing the widget if we have no input linked // const w = node.inputs?.find((i) => i.widget?.name === widget.name); // if (w?.link == null) { // return undefined; // } // return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; // }; // Hide any linked widgets, e.g. seed+seedControl if (widget.linkedWidgets) { for (const w of widget.linkedWidgets) { hideWidgetForGood(node, w, `:${widget.name}`) } } } export function fixWidgets(node) { if (node.inputs) { for (const input of node.inputs) { log(input) if (input.widget || node.widgets) { // if (newTypes.includes(input.type)) { const matching_widget = node.widgets.find((w) => w.name === input.name) if (matching_widget) { // if (matching_widget.hidden) { // log(`Already hidden skipping ${matching_widget.name}`) // continue // } const w = node.widgets.find((w) => w.name === matching_widget.name) if (w && w.type !== CONVERTED_TYPE) { log(w) log(`hidding ${w.name}(${w.type}) from ${node.type}`) log(node) hideWidget(node, w) } else { log(`converting to widget ${w}`) convertToWidget(node, input) } } } } } } export function inner_value_change(widget, val, event = undefined) { let value = val if (widget.type === 'number' || widget.type === 'BBOX') { value = Number(value) } else if (widget.type === 'BOOL') { value = Boolean(value) } widget.value = corrected_value if ( widget.options?.property && node.properties[widget.options.property] !== undefined ) { node.setProperty(widget.options.property, value) } if (widget.callback) { widget.callback(widget.value, app.canvas, node, pos, event) } } export const getNamedWidget = (node, ...names) => { const out = {} for (const name of names) { out[name] = node.widgets.find((w) => w.name === name) } return out } /** * @param {LGraphNode} node * @param {LLink} link * @returns {{to:LGraphNode, from:LGraphNode, type:'error' | 'incoming' | 'outgoing'}} */ export const nodesFromLink = (node, link) => { const fromNode = app.graph.getNodeById(link.origin_id) const toNode = app.graph.getNodeById(link.target_id) let tp = 'error' if (fromNode.id === node.id) { tp = 'outgoing' } else if (toNode.id === node.id) { tp = 'incoming' } return { to: toNode, from: fromNode, type: tp } } export const hasWidgets = (node) => { if (!node.widgets || !node.widgets?.[Symbol.iterator]) { return false } return true } export const cleanupNode = (node) => { if (!hasWidgets(node)) { return } for (const w of node.widgets) { if (w.canvas) { w.canvas.remove() } if (w.inputEl) { w.inputEl.remove() } // calls the widget remove callback w.onRemoved?.() } } export function offsetDOMWidget( widget, ctx, node, widgetWidth, widgetY, height, ) { const margin = 10 const elRect = ctx.canvas.getBoundingClientRect() const transform = new DOMMatrix() .scaleSelf( elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height, ) .multiplySelf(ctx.getTransform()) .translateSelf(margin, margin + widgetY) const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) Object.assign(widget.inputEl.style, { transformOrigin: '0 0', transform: scale, left: `${transform.a + transform.e}px`, top: `${transform.d + transform.f}px`, width: `${widgetWidth - margin * 2}px`, // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`, height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`, position: 'absolute', background: !node.color ? '' : node.color, color: !node.color ? '' : 'white', zIndex: 5, //app.graph._nodes.indexOf(node), }) } /** * Extracts the type and link type from a widget config object. * @param {*} config * @returns */ export function getWidgetType(config) { // Special handling for COMBO so we restrict links based on the entries let type = config?.[0] let linkType = type if (Array.isArray(type)) { type = 'COMBO' linkType = linkType.join(',') } return { type, linkType } } // #endregion // #region dynamic connections /** * @param {NodeType} nodeType The nodetype to attach the documentation to * @param {str} prefix A prefix added to each dynamic inputs * @param {str | [str]} inputType The datatype(s) of those dynamic inputs * @param {{link?:LLink, ioSlot?:INodeInputSlot | INodeOutputSlot}?} opts * @returns */ export const setupDynamicConnections = (nodeType, prefix, inputType, opts) => { infoLogger( 'Setting up dynamic connections for', Object.getOwnPropertyDescriptors(nodeType).title.value, ) /** @type {{link?:LLink, ioSlot?:INodeInputSlot | INodeOutputSlot}} */ const options = opts || {} const onNodeCreated = nodeType.prototype.onNodeCreated const inputList = typeof inputType === 'object' nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, []) : undefined this.addInput(`${prefix}_1`, inputList ? '*' : inputType) return r } const onConnectionsChange = nodeType.prototype.onConnectionsChange /** * @param {OnConnectionsChangeParams} args */ nodeType.prototype.onConnectionsChange = function (...args) { const [type, slotIndex, isConnected, link, ioSlot] = args options.link = link options.ioSlot = ioSlot const r = onConnectionsChange ? onConnectionsChange.apply(this, [ type, slotIndex, isConnected, link, ioSlot, ]) : undefined options.DEBUG = { node: this, type, slotIndex, isConnected, link, ioSlot, } dynamic_connection( this, slotIndex, isConnected, `${prefix}_`, inputType, options, ) return r } } /** * Main logic around dynamic inputs * * @param {LGraphNode} node - The target node * @param {number} index - The slot index of the currently changed connection * @param {bool} connected - Was this event connecting or disconnecting * @param {string} [connectionPrefix] - The common prefix of the dynamic inputs * @param {string|[string]} [connectionType] - The type of the dynamic connection * @param {{link?:LLink, ioSlot?:INodeInputSlot | INodeOutputSlot}} [opts] - extra options */ export const dynamic_connection = ( node, index, connected, connectionPrefix = 'input_', connectionType = '*', opts = undefined, ) => { /* @type {{link?:LLink, ioSlot?:INodeInputSlot | INodeOutputSlot}} [opts] - extra options*/ const options = opts || {} if ( node.inputs.length > 0 && !node.inputs[index].name.startsWith(connectionPrefix) ) { return } const listConnection = typeof connectionType === 'object' const conType = listConnection ? '*' : connectionType const nameArray = options.nameArray || [] const clean_inputs = () => { if (node.inputs.length === 0) return let w_count = node.widgets?.length || 0 let i_count = node.inputs?.length || 0 infoLogger(`Cleaning inputs: [BEFORE] (w: ${w_count} | inputs: ${i_count})`) const to_remove = [] for (let n = 1; n < node.inputs.length; n++) { const element = node.inputs[n] if (!element.link) { if (node.widgets) { const w = node.widgets.find((w) => w.name === element.name) if (w) { w.onRemoved?.() node.widgets.length = node.widgets.length - 1 } } infoLogger(`Removing input ${n}`) to_remove.push(n) } } for (let i = 0; i < to_remove.length; i++) { const id = to_remove[i] node.removeInput(id) i_count -= 1 } node.inputs.length = i_count w_count = node.widgets?.length || 0 i_count = node.inputs?.length || 0 infoLogger(`Cleaning inputs: [AFTER] (w: ${w_count} | inputs: ${i_count})`) infoLogger('Cleaning inputs: making it sequential again') // make inputs sequential again for (let i = 0; i < node.inputs.length; i++) { let name = `${connectionPrefix}${i + 1}` if (nameArray.length > 0) { name = i < nameArray.length ? nameArray[i] : name } node.inputs[i].label = name node.inputs[i].name = name } } if (!connected) { if (!options.link) { infoLogger('Disconnecting', { options }) clean_inputs() } else { if (!options.ioSlot.link) { node.connectionTransit = true } else { node.connectionTransit = false clean_inputs() } infoLogger('Reconnecting', { options }) } } if (connected) { if (options.link) { const { from, to, type } = nodesFromLink(node, options.link) if (type === 'outgoing') return infoLogger('Connecting', { options, from, to, type }) } else { infoLogger('Connecting', { options }) } if (node.connectionTransit) { infoLogger('In Transit') node.connectionTransit = false } // Remove inputs and their widget if not linked. clean_inputs() if (node.inputs.length === 0) return // add an extra input if (node.inputs[node.inputs.length - 1].link !== null) { const nextIndex = node.inputs.length const name = nextIndex < nameArray.length ? nameArray[nextIndex] : `${connectionPrefix}${nextIndex + 1}` infoLogger(`Adding input ${nextIndex + 1} (${name})`) node.addInput(name, conType) } } } // #endregion // #region color utils export function isColorBright(rgb, threshold = 240) { const brightess = getBrightness(rgb) return brightess > threshold } function getBrightness(rgbObj) { return Math.round( (Number.parseInt(rgbObj[0]) * 299 + Number.parseInt(rgbObj[1]) * 587 + Number.parseInt(rgbObj[2]) * 114) / 1000, ) } // #endregion // #region html/css utils /** * Calculate total height of DOM element child * * @param {HTMLElement} parentElement - The target dom element * @returns {number} the total height */ export function calculateTotalChildrenHeight(parentElement) { let totalHeight = 0 for (const child of parentElement.children) { const style = window.getComputedStyle(child) // Get height as an integer (without 'px') const height = Number.parseInt(style.height, 10) // Get vertical margin as integers const marginTop = Number.parseInt(style.marginTop, 10) const marginBottom = Number.parseInt(style.marginBottom, 10) // Sum up height and vertical margins totalHeight += height + marginTop + marginBottom } return totalHeight } export const loadScript = ( FILE_URL, async = true, type = 'text/javascript', ) => { return new Promise((resolve, reject) => { try { // Check if the script already exists const existingScript = document.querySelector(`script[src="${FILE_URL}"]`) if (existingScript) { resolve({ status: true, message: 'Script already loaded' }) return } const scriptEle = document.createElement('script') scriptEle.type = type scriptEle.async = async scriptEle.src = FILE_URL scriptEle.addEventListener('load', (_ev) => { resolve({ status: true }) }) scriptEle.addEventListener('error', (_ev) => { reject({ status: false, message: `Failed to load the script ${FILE_URL}`, }) }) document.body.appendChild(scriptEle) } catch (error) { reject(error) } }) } // #endregion // #region documentation widget const create_documentation_stylesheet = () => { const tag = 'mtb-documentation-stylesheet' let styleTag = document.head.querySelector(tag) if (!styleTag) { styleTag = document.createElement('style') styleTag.type = 'text/css' styleTag.id = tag styleTag.innerHTML = ` .documentation-popup { background: var(--comfy-menu-bg); position: absolute; color: var(--fg-color); font: 12px monospace; line-height: 1.5em; padding: 10px; border-radius: 6px; pointer-events: "inherit"; z-index: 5; overflow: hidden; } .documentation-wrapper { padding: 0 2em; overflow: auto; max-height: 100%; /* Scrollbar styling for Chrome */ &::-webkit-scrollbar { width: 6px; } &::-webkit-scrollbar-track { background: var(--bg-color); } &::-webkit-scrollbar-thumb { background-color: var(--fg-color); border-radius: 6px; border: 3px solid var(--bg-color); } /* Scrollbar styling for Firefox */ scrollbar-width: thin; scrollbar-color: var(--fg-color) var(--bg-color); a { color: yellow; } a:visited { color: orange; } a:hover { color: red; } } .documentation-popup img { max-width: 100%; } .documentation-popup table { border-collapse: collapse; border: 1px var(--border-color) solid; } .documentation-popup th, .documentation-popup td { border: 1px var(--border-color) solid; } .documentation-popup th { background-color: var(--comfy-input-bg); }` document.head.appendChild(styleTag) } } let parserPromise const callbackQueue = [] function runQueuedCallbacks() { while (callbackQueue.length) { const cb = callbackQueue.shift() cb(window.MTB.mdParser) } } function loadParser(shiki) { if (!parserPromise) { parserPromise = import( shiki ? '/mtb_async/mtb_markdown_plus.umd.js' : '/mtb_async/mtb_markdown.umd.js' ) .then((_module) => shiki ? MTBMarkdownPlus.getParser() : MTBMarkdown.getParser(), ) .then((instance) => { window.MTB.mdParser = instance runQueuedCallbacks() return instance }) .catch((error) => { console.error('Error loading the parser:', error) }) } return parserPromise } export const ensureMarkdownParser = async (callback) => { infoLogger('Ensuring md parser') let use_shiki = false try { use_shiki = await api.getSetting('mtb.Use Shiki') } catch (e) { console.warn('Option not available yet', e) } if (window.MTB?.mdParser) { infoLogger('Markdown parser found') callback?.(window.MTB.mdParser) return window.MTB.mdParser } if (!parserPromise) { infoLogger('Running promise to fetch parser') try { loadParser(use_shiki) //.then(() => { // callback?.(window.MTB.mdParser) // }) } catch (error) { console.error('Error loading the parser:', error) } } else { infoLogger('A similar promise is already running, waiting for it to finish') } if (callback) { callbackQueue.push(callback) } await parserPromise await parserPromise return window.MTB.mdParser } /** * Add documentation widget to the given node. * * This method will add a `docCtrl` property to the node * that contains the AbortController that manages all the events * defined inside it (global and instance ones) without explicit * cleanup method for each. * * @param {NodeData} nodeData * @param {NodeType} nodeType * @param {DocumentationOptions} opts */ export const addDocumentation = ( nodeData, nodeType, opts = { icon_size: 14, icon_margin: 4 }, ) => { if (!nodeData.description) { infoLogger( `Skipping ${nodeData.name} doesn't have a description, skipping...`, ) return } const options = opts || {} const iconSize = options.icon_size || 14 const iconMargin = options.icon_margin || 4 let docElement = null let wrapper = null const onRem = nodeType.prototype.onRemoved nodeType.prototype.onRemoved = function () { const r = onRem ? onRem.apply(this, []) : undefined if (docElement) { docElement.remove() docElement = null } if (wrapper) { wrapper.remove() wrapper = null } return r } const drawFg = nodeType.prototype.onDrawForeground /** * @param {OnDrawForegroundParams} args */ nodeType.prototype.onDrawForeground = function (...args) { const [ctx, _canvas] = args const r = drawFg ? drawFg.apply(this, args) : undefined if (this.flags.collapsed) return r // icon position const x = this.size[0] - iconSize - iconMargin let resizeHandle // create it if (this.show_doc && docElement === null) { create_documentation_stylesheet() docElement = document.createElement('div') docElement.classList.add('documentation-popup') document.body.appendChild(docElement) wrapper = document.createElement('div') wrapper.classList.add('documentation-wrapper') docElement.appendChild(wrapper) // wrapper.innerHTML = documentationConverter.makeHtml(nodeData.description) ensureMarkdownParser().then(() => { MTB.mdParser.parse(nodeData.description).then((e) => { wrapper.innerHTML = e // resize handle resizeHandle = document.createElement('div') resizeHandle.classList.add('doc-resize-handle') resizeHandle.style.width = '0' resizeHandle.style.height = '0' resizeHandle.style.position = 'absolute' resizeHandle.style.bottom = '0' resizeHandle.style.right = '0' resizeHandle.style.cursor = 'se-resize' resizeHandle.style.userSelect = 'none' resizeHandle.style.borderWidth = '15px' resizeHandle.style.borderStyle = 'solid' resizeHandle.style.borderColor = 'transparent var(--border-color) var(--border-color) transparent' wrapper.appendChild(resizeHandle) let isResizing = false let startX let startY let startWidth let startHeight resizeHandle.addEventListener( 'mousedown', (e) => { e.stopPropagation() isResizing = true startX = e.clientX startY = e.clientY startWidth = Number.parseInt( document.defaultView.getComputedStyle(docElement).width, 10, ) startHeight = Number.parseInt( document.defaultView.getComputedStyle(docElement).height, 10, ) }, { signal: this.docCtrl.signal }, ) document.addEventListener( 'mousemove', (e) => { if (!isResizing) return const scale = app.canvas.ds.scale const newWidth = startWidth + (e.clientX - startX) / scale const newHeight = startHeight + (e.clientY - startY) / scale docElement.style.width = `${newWidth}px` docElement.style.height = `${newHeight}px` this.docPos = { width: `${newWidth}px`, height: `${newHeight}px`, } }, { signal: this.docCtrl.signal }, ) document.addEventListener( 'mouseup', () => { isResizing = false }, { signal: this.docCtrl.signal }, ) }) }) } else if (!this.show_doc && docElement !== null) { docElement.remove() docElement = null } // reposition if (this.show_doc && docElement !== null) { const rect = ctx.canvas.getBoundingClientRect() const dpi = Math.max(1.0, window.devicePixelRatio) const scaleX = rect.width / ctx.canvas.width const scaleY = rect.height / ctx.canvas.height const transform = new DOMMatrix() .scaleSelf(scaleX, scaleY) .multiplySelf(ctx.getTransform()) .translateSelf(this.size[0] * scaleX * dpi, 0) .translateSelf(10, -32) const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) Object.assign(docElement.style, { transformOrigin: '0 0', transform: scale, left: `${transform.a + rect.x + transform.e}px`, top: `${transform.d + rect.y + transform.f}px`, width: this.docPos ? this.docPos.width : `${this.size[0] * 1.5}px`, height: this.docPos?.height, }) if (this.docPos === undefined) { this.docPos = { width: docElement.style.width, height: docElement.style.height, } } } ctx.save() ctx.translate(x, iconSize - 34) ctx.scale(iconSize / 32, iconSize / 32) ctx.strokeStyle = 'rgba(255,255,255,0.3)' ctx.lineCap = 'round' ctx.lineJoin = 'round' ctx.lineWidth = 2.4 ctx.font = 'bold 36px monospace' ctx.fillText('?', 0, 24) // ctx.font = `bold ${this.show_doc ? 36 : 24}px monospace` // ctx.fillText(`${this.show_doc ? '▼' : '▶'}`, 24, 24) ctx.restore() return r } const mouseDown = nodeType.prototype.onMouseDown /** * @param {OnMouseDownParams} args */ nodeType.prototype.onMouseDown = function (...args) { const [_event, localPos, _graphCanvas] = args const r = mouseDown ? mouseDown.apply(this, args) : undefined const iconX = this.size[0] - iconSize - iconMargin const iconY = iconSize - 34 if ( localPos[0] > iconX && localPos[0] < iconX + iconSize && localPos[1] > iconY && localPos[1] < iconY + iconSize ) { // Pencil icon was clicked, open the editor // this.openEditorDialog(); if (this.show_doc === undefined) { this.show_doc = true } else { this.show_doc = !this.show_doc } if (this.show_doc) { this.docCtrl = new AbortController() } else { this.docCtrl.abort() } return true // Return true to indicate the event was handled } return r // Return false to let the event propagate // return r; } } // #endregion // #region node extensions /** * Extend an object, either replacing the original property or extending it. * @param {Object} object - The object to which the property belongs. * @param {string} property - The name of the property to chain the callback to. * @param {Function} callback - The callback function to be chained. */ export function extendPrototype(object, property, callback) { if (object === undefined) { console.error('Could not extend undefined object', { object, property }) return } if (property in object) { const callback_orig = object[property] object[property] = function (...args) { const r = callback_orig.apply(this, args) callback.apply(this, args) return r } } else { object[property] = callback } } /** * Appends a callback to the extra menu options of a given node type. * @param {NodeType} nodeType * @param {(app,options) => ContextMenuItem[]} cb */ export function addMenuHandler(nodeType, cb) { const getOpts = nodeType.prototype.getExtraMenuOptions /** * @returns {ContextMenuItem[]} items */ nodeType.prototype.getExtraMenuOptions = function (app, options) { const r = getOpts.apply(this, [app, options]) || [] const newItems = cb.apply(this, [app, options]) || [] return [...r, ...newItems] } } /** Prefixes the node title with '[DEPRECATED]' and log the deprecation reason to the console.*/ export const addDeprecation = (nodeType, reason) => { const title = nodeType.title nodeType.title = `[DEPRECATED] ${title}` // console.log(nodeType) const styles = { title: 'font-size:1.3em;font-weight:900;color:yellow; background: black', reason: 'font-size:1.2em', } console.log( `%c! ${title} is deprecated:%c ${reason}`, styles.title, styles.reason, ) } // #endregion // #region API / graph utilities export const getAPIInputs = () => { const inputs = {} let counter = 1 for (const node of getNodes(true)) { const widgets = node.widgets if (node.properties.mtb_api && node.properties.useAPI) { if (node.properties.mtb_api.inputs) { for (const currentName in node.properties.mtb_api.inputs) { const current = node.properties.mtb_api.inputs[currentName] if (current.enabled) { const inputName = current.name || currentName const widget = widgets.find((w) => w.name === currentName) if (!widget) continue if (!(inputName in inputs)) { inputs[inputName] = { ...current, id: counter, name: inputName, type: current.type, node_id: node.id, widgets: [], } } inputs[inputName].widgets.push(widget) counter = counter + 1 } } } } } return inputs } export const getNodes = (skip_unused) => { const nodes = [] for (const outerNode of app.graph.computeExecutionOrder(false)) { const skipNode = (outerNode.mode === 2 || outerNode.mode === 4) && skip_unused const innerNodes = !skipNode && outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode] for (const node of innerNodes) { if ((node.mode === 2 || node.mode === 4) && skip_unused) { continue } nodes.push(node) } } return nodes }