diff --git a/mtb/comfy_shared.js b/mtb/comfy_shared.js index b8a1332..a60602e 100644 --- a/mtb/comfy_shared.js +++ b/mtb/comfy_shared.js @@ -1,342 +1,486 @@ -/** - * File: comfy_shared.js - * Project: comfy_mtb - * Author: Mel Massadian - * - * Copyright (c) 2023 Mel Massadian - * - */ - -import { app } from '../../scripts/app.js' - -export const log = (...args) => { - if (window.MTB?.DEBUG) { - console.debug(...args) - } -} - -//- WIDGET UTILS -export const CONVERTED_TYPE = 'converted-widget' - -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 (type instanceof Array) { - type = 'COMBO' - linkType = linkType.join(',') - } - return { type, linkType } -} - -export const dynamic_connection = ( - node, - index, - connected, - connectionPrefix = 'input_', - connectionType = 'PSDLAYER' -) => { - // remove all non connected inputs - if (!connected && node.inputs.length > 1) { - log(`Removing input ${index} (${node.inputs[index].name})`) - if (node.widgets) { - const w = node.widgets.find((w) => w.name === node.inputs[index].name) - if (w) { - w.onRemoved?.() - node.widgets.length = node.widgets.length - 1 - } - } - node.removeInput(index) - - // make inputs sequential again - for (let i = 0; i < node.inputs.length; i++) { - node.inputs[i].label = `${connectionPrefix}${i + 1}` - } - } - - // add an extra input - if (node.inputs[node.inputs.length - 1].link != undefined) { - log( - `Adding input ${node.inputs.length + 1} (${connectionPrefix}${ - node.inputs.length + 1 - })` - ) - - node.addInput( - `${connectionPrefix}${node.inputs.length + 1}`, - connectionType - ) - } -} - -/** - * Appends a callback to the extra menu options of a given node type. - * @param {*} nodeType - * @param {*} cb - */ -export function addMenuHandler(nodeType, cb) { - const getOpts = nodeType.prototype.getExtraMenuOptions - nodeType.prototype.getExtraMenuOptions = function () { - const r = getOpts.apply(this, arguments) - cb.apply(this, arguments) - return r - } -} - -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) - } - } -} - -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, value, event = undefined) { - if (widget.type == 'number' || widget.type == 'BBOX') { - value = Number(value) - } else if (widget.type == 'BOOL') { - value = Boolean(value) - } - widget.value = value - if ( - widget.options && - 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) - } -} - -//- COLOR UTILS -export function isColorBright(rgb, threshold = 240) { - const brightess = getBrightness(rgb) - return brightess > threshold -} - -function getBrightness(rgbObj) { - return Math.round( - (parseInt(rgbObj[0]) * 299 + - parseInt(rgbObj[1]) * 587 + - parseInt(rgbObj[2]) * 114) / - 1000 - ) -} - -//- HTML / CSS UTILS -export function defineClass(className, classStyles) { - const styleSheets = document.styleSheets - - // Helper function to check if the class exists in a style sheet - function classExistsInStyleSheet(styleSheet) { - const rules = styleSheet.rules || styleSheet.cssRules - for (const rule of rules) { - if (rule.selectorText === `.${className}`) { - return true - } - } - return false - } - - // Check if the class is already defined in any of the style sheets - let classExists = false - for (const styleSheet of styleSheets) { - if (classExistsInStyleSheet(styleSheet)) { - classExists = true - break - } - } - - // If the class doesn't exist, add the new class definition to the first style sheet - if (!classExists) { - if (styleSheets[0].insertRule) { - styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0) - } else if (styleSheets[0].addRule) { - styleSheets[0].addRule(`.${className}`, classStyles, 0) - } - } -} +/** + * File: comfy_shared.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' + +// - 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() { + Object.keys(localStorage) + .filter(k => k.startsWith(this.namespace + ':')) + .forEach(k => localStorage.removeItem(k)); + } +} + + +// - log utilities + +function createLogger(emoji, color, consoleMethod = 'log') { + return function (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) + } +} + +//- WIDGET UTILS +export const CONVERTED_TYPE = 'converted-widget' + +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 (type instanceof Array) { + type = 'COMBO' + linkType = linkType.join(',') + } + return { type, linkType } +} +export const setupDynamicConnections = (nodeType, prefix, inputType) => { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined + this.addInput(`${prefix}_1`, inputType) + return r + } + + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + dynamic_connection(this, index, connected, `${prefix}_`, inputType) + } +} +export const dynamic_connection = ( + node, + index, + connected, + connectionPrefix = 'input_', + connectionType = 'PSDLAYER', + nameArray = [] +) => { + if (!node.inputs[index].name.startsWith(connectionPrefix)) { + return + } + // remove all non connected inputs + if (!connected && node.inputs.length > 1) { + log(`Removing input ${index} (${node.inputs[index].name})`) + if (node.widgets) { + const w = node.widgets.find((w) => w.name === node.inputs[index].name) + if (w) { + w.onRemoved?.() + node.widgets.length = node.widgets.length - 1 + } + } + node.removeInput(index) + + // make inputs sequential again + for (let i = 0; i < node.inputs.length; i++) { + const name = + i < nameArray.length ? nameArray[i] : `${connectionPrefix}${i + 1}` + node.inputs[i].label = name + node.inputs[i].name = name + } + } + + // add an extra input + if (node.inputs[node.inputs.length - 1].link != undefined) { + const nextIndex = node.inputs.length + const name = + nextIndex < nameArray.length + ? nameArray[nextIndex] + : `${connectionPrefix}${nextIndex + 1}` + + log(`Adding input ${nextIndex + 1} (${name})`) + + node.addInput(name, connectionType) + } +} + +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 = parseInt(style.height, 10) + + // Get vertical margin as integers + const marginTop = parseInt(style.marginTop, 10) + const marginBottom = parseInt(style.marginBottom, 10) + + // Sum up height and vertical margins + totalHeight += height + marginTop + marginBottom + } + + return totalHeight +} +/** + * Appends a callback to the extra menu options of a given node type. + * @param {*} nodeType + * @param {*} cb + */ +export function addMenuHandler(nodeType, cb) { + const getOpts = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function () { + const r = getOpts.apply(this, arguments) + cb.apply(this, arguments) + return r + } +} + +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) + } + } +} + +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, value, event = undefined) { + if (widget.type == 'number' || widget.type == 'BBOX') { + value = Number(value) + } else if (widget.type == 'BOOL') { + value = Boolean(value) + } + widget.value = value + if ( + widget.options && + 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) + } +} + +//- COLOR UTILS +export function isColorBright(rgb, threshold = 240) { + const brightess = getBrightness(rgb) + return brightess > threshold +} + +function getBrightness(rgbObj) { + return Math.round( + (parseInt(rgbObj[0]) * 299 + + parseInt(rgbObj[1]) * 587 + + parseInt(rgbObj[2]) * 114) / + 1000 + ) +} + +//- HTML / CSS UTILS +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) + } + }) +} + +export function defineClass(className, classStyles) { + const styleSheets = document.styleSheets + + // Helper function to check if the class exists in a style sheet + function classExistsInStyleSheet(styleSheet) { + const rules = styleSheet.rules || styleSheet.cssRules + for (const rule of rules) { + if (rule.selectorText === `.${className}`) { + return true + } + } + return false + } + + // Check if the class is already defined in any of the style sheets + let classExists = false + for (const styleSheet of styleSheets) { + if (classExistsInStyleSheet(styleSheet)) { + classExists = true + break + } + } + + // If the class doesn't exist, add the new class definition to the first style sheet + if (!classExists) { + if (styleSheets[0].insertRule) { + styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0) + } else if (styleSheets[0].addRule) { + styleSheets[0].addRule(`.${className}`, classStyles, 0) + } + } +} diff --git a/mtb/debug.js b/mtb/debug.js index cf7c89a..319bf1b 100644 --- a/mtb/debug.js +++ b/mtb/debug.js @@ -1,109 +1,122 @@ -/** - * File: debug.js - * Project: comfy_mtb - * Author: Mel Massadian - * - * Copyright (c) 2023 Mel Massadian - * - */ - -import { app } from '../../scripts/app.js' - -import * as shared from './comfy_shared.js' -import { log } from './comfy_shared.js' -import { MtbWidgets } from './mtb_widgets.js' - -// TODO: respect inputs order... - -function escapeHtml(unsafe) { - return unsafe - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} -app.registerExtension({ - name: 'mtb.Debug', - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === 'Debug (mtb)') { - const onConnectionsChange = nodeType.prototype.onConnectionsChange - nodeType.prototype.onConnectionsChange = function ( - type, - index, - connected, - link_info - ) { - const r = onConnectionsChange - ? onConnectionsChange.apply(this, arguments) - : undefined - // TODO: remove all widgets on disconnect once computed - shared.dynamic_connection(this, index, connected, 'anything_', '*') - - //- infer type - if (link_info) { - const fromNode = this.graph._nodes.find( - (otherNode) => otherNode.id == link_info.origin_id - ) - const type = fromNode.outputs[link_info.origin_slot].type - this.inputs[index].type = type - // this.inputs[index].label = type.toLowerCase() - } - //- restore dynamic input - if (!connected) { - this.inputs[index].type = '*' - this.inputs[index].label = `anything_${index + 1}` - } - } - - const onExecuted = nodeType.prototype.onExecuted - nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments) - - const prefix = 'anything_' - - if (this.widgets) { - // const pos = this.widgets.findIndex((w) => w.name === "anything_1"); - // if (pos !== -1) { - for (let i = 0; i < this.widgets.length; i++) { - this.widgets[i].onRemoved?.() - } - this.widgets.length = 0 - } - let widgetI = 1 - if (message.text) { - for (const txt of message.text) { - const w = this.addCustomWidget( - MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt)) - ) - w.parent = this - widgetI++ - } - } - if (message.b64_images) { - for (const img of message.b64_images) { - const w = this.addCustomWidget( - MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img) - ) - w.parent = this - widgetI++ - } - // this.onResize?.(this.size); - // this.resize?.(this.size) - } - - this.setSize(this.computeSize()) - - this.onRemoved = function () { - // When removing this node we need to remove the input from the DOM - for (let y in this.widgets) { - if (this.widgets[y].canvas) { - this.widgets[y].canvas.remove() - } - this.widgets[y].onRemoved?.() - } - } - } - } - }, -}) +/** + * File: debug.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' + +import * as shared from './comfy_shared.js' +import { log } from './comfy_shared.js' +import { MtbWidgets } from './mtb_widgets.js' + +// TODO: respect inputs order... + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} +app.registerExtension({ + name: 'mtb.Debug', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === 'Debug (mtb)') { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + this.addInput(`anything_1`, '*') + return r + } + + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + // TODO: remove all widgets on disconnect once computed + shared.dynamic_connection(this, index, connected, 'anything_', '*') + + //- infer type + if (link_info) { + const fromNode = this.graph._nodes.find( + (otherNode) => otherNode.id == link_info.origin_id + ) + const type = fromNode.outputs[link_info.origin_slot].type + this.inputs[index].type = type + // this.inputs[index].label = type.toLowerCase() + } + //- restore dynamic input + if (!connected) { + this.inputs[index].type = '*' + this.inputs[index].label = `anything_${index + 1}` + } + } + + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments) + + const prefix = 'anything_' + + if (this.widgets) { + // const pos = this.widgets.findIndex((w) => w.name === "anything_1"); + // if (pos !== -1) { + for (let i = 0; i < this.widgets.length; i++) { + if (this.widgets[i].name !== 'output_to_console') { + this.widgets[i].onRemoved?.() + } + } + this.widgets.length = 1 + } + let widgetI = 1 + + if (message.text) { + for (const txt of message.text) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt)) + ) + w.parent = this + widgetI++ + } + } + if (message.b64_images) { + for (const img of message.b64_images) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img) + ) + w.parent = this + widgetI++ + } + // this.onResize?.(this.size); + // this.resize?.(this.size) + } + + this.setSize(this.computeSize()) + + this.onRemoved = function () { + // When removing this node we need to remove the input from the DOM + for (let y in this.widgets) { + if (this.widgets[y].canvas) { + this.widgets[y].canvas.remove() + } + shared.cleanupNode(this) + this.widgets[y].onRemoved?.() + } + } + } + } + }, +}) diff --git a/mtb/mtb_widgets.js b/mtb/mtb_widgets.js index e4afc46..5f88a20 100644 --- a/mtb/mtb_widgets.js +++ b/mtb/mtb_widgets.js @@ -1,908 +1,976 @@ -/** - * File: mtb_widgets.js - * Project: comfy_mtb - * Author: Mel Massadian - * - * Copyright (c) 2023 Mel Massadian - * - */ - -import { app } from '../../scripts/app.js' -import { api } from '../../scripts/api.js' - -import parseCss from './parse-css.js' -import * as shared from './comfy_shared.js' -import { log } from './comfy_shared.js' - -const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX'] - -const withFont = (ctx, font, cb) => { - const oldFont = ctx.font - ctx.font = font - cb() - ctx.font = oldFont -} - -const calculateTextDimensions = (ctx, value, width, fontSize = 16) => { - const words = value.split(' ') - const lines = [] - let currentLine = '' - for (const word of words) { - const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}` - const testWidth = ctx.measureText(testLine).width - if (testWidth > width) { - lines.push(currentLine) - currentLine = word - } else { - currentLine = testLine - } - } - if (lines.length === 0) lines.push(value) - const textHeight = (lines.length + 1) * fontSize - const maxLineWidth = lines.reduce( - (maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), - 0 - ) - return { textHeight, maxLineWidth } -} - -export const MtbWidgets = { - BBOX: (key, val) => { - /** @type {import("./types/litegraph").IWidget} */ - const widget = { - name: key, - type: 'BBOX', - // options: val, - y: 0, - value: val?.default || [0, 0, 0, 0], - options: {}, - - draw: function (ctx, node, widget_width, widgetY, height) { - const hide = this.type !== 'BBOX' && app.canvas.ds.scale > 0.5 - - const show_text = true - const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR - const background_color = LiteGraph.WIDGET_BGCOLOR - const text_color = LiteGraph.WIDGET_TEXT_COLOR - const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR - const H = LiteGraph.NODE_WIDGET_HEIGHT - - let margin = 15 - let numWidgets = 4 // Number of stacked widgets - - if (hide) return - - for (let i = 0; i < numWidgets; i++) { - let currentY = widgetY + i * (H + margin) // Adjust Y position for each widget - - ctx.textAlign = 'left' - ctx.strokeStyle = outline_color - ctx.fillStyle = background_color - ctx.beginPath() - if (show_text) - ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [ - H * 0.5, - ]) - else ctx.rect(margin, currentY, widget_width - margin * 2, H) - ctx.fill() - if (show_text) { - if (!this.disabled) ctx.stroke() - ctx.fillStyle = text_color - if (!this.disabled) { - ctx.beginPath() - ctx.moveTo(margin + 16, currentY + 5) - ctx.lineTo(margin + 6, currentY + H * 0.5) - ctx.lineTo(margin + 16, currentY + H - 5) - ctx.fill() - ctx.beginPath() - ctx.moveTo(widget_width - margin - 16, currentY + 5) - ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5) - ctx.lineTo(widget_width - margin - 16, currentY + H - 5) - ctx.fill() - } - ctx.fillStyle = secondary_text_color - ctx.fillText( - this.label || this.name, - margin * 2 + 5, - currentY + H * 0.7 - ) - ctx.fillStyle = text_color - ctx.textAlign = 'right' - - ctx.fillText( - Number(this.value).toFixed( - this.options?.precision !== undefined - ? this.options.precision - : 3 - ), - widget_width - margin * 2 - 20, - currentY + H * 0.7 - ) - } - } - }, - mouse: function (event, pos, node) { - let old_value = this.value - let x = pos[0] - node.pos[0] - let y = pos[1] - node.pos[1] - let width = node.size[0] - let H = LiteGraph.NODE_WIDGET_HEIGHT - let margin = 5 - let numWidgets = 4 // Number of stacked widgets - - for (let i = 0; i < numWidgets; i++) { - let currentY = y + i * (H + margin) // Adjust Y position for each widget - - if ( - event.type == LiteGraph.pointerevents_method + 'move' && - this.type == 'BBOX' - ) { - if (event.deltaX) - this.value += event.deltaX * 0.1 * (this.options?.step || 1) - if (this.options.min != null && this.value < this.options.min) { - this.value = this.options.min - } - if (this.options.max != null && this.value > this.options.max) { - this.value = this.options.max - } - } else if (event.type == LiteGraph.pointerevents_method + 'down') { - let values = this.options?.values - if (values && values.constructor === Function) { - values = this.options.values(w, node) - } - let values_list = null - - let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 - if (this.type == 'BBOX') { - this.value += delta * 0.1 * (this.options.step || 1) - if (this.options.min != null && this.value < this.options.min) { - this.value = this.options.min - } - if (this.options.max != null && this.value > this.options.max) { - this.value = this.options.max - } - } else if (delta) { - //clicked in arrow, used for combos - let index = -1 - this.last_mouseclick = 0 //avoids dobl click event - if (values.constructor === Object) - index = values_list.indexOf(String(this.value)) + delta - else index = values_list.indexOf(this.value) + delta - if (index >= values_list.length) { - index = values_list.length - 1 - } - if (index < 0) { - index = 0 - } - if (values.constructor === Array) this.value = values[index] - else this.value = index - } - } //end mousedown - else if ( - event.type == LiteGraph.pointerevents_method + 'up' && - this.type == 'BBOX' - ) { - let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 - if (event.click_time < 200 && delta == 0) { - this.prompt( - 'Value', - this.value, - function (v) { - // check if v is a valid equation or a number - if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { - try { - //solve the equation if possible - v = eval(v) - } catch (e) {} - } - this.value = Number(v) - shared.inner_value_change(this, this.value, event) - }.bind(w), - event - ) - } - } - - if (old_value != this.value) - setTimeout( - function () { - shared.inner_value_change(this, this.value, event) - }.bind(this), - 20 - ) - - app.canvas.setDirty(true) - } - }, - computeSize: function (width) { - return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4] - }, - // onDrawBackground: function (ctx) { - // if (!this.flags.collapsed) return; - // this.inputEl.style.display = "block"; - // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; - // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; - // }, - // onInputChange: function (e) { - // const property = e.target.dataset.property; - // const bbox = this.getInputData(0); - // if (!bbox) return; - // bbox[property] = parseFloat(e.target.value); - // this.setOutputData(0, bbox); - // } - } - - widget.desc = 'Represents a Bounding Box with x, y, width, and height.' - return widget - }, - - COLOR: (key, val, compute = false) => { - /** @type {import("/types/litegraph").IWidget} */ - const widget = {} - widget.y = 0 - widget.name = key - widget.type = 'COLOR' - widget.options = { default: '#ff0000' } - widget.value = val || '#ff0000' - widget.draw = function (ctx, node, widgetWidth, widgetY, height) { - const hide = this.type !== 'COLOR' && app.canvas.ds.scale > 0.5 - if (hide) { - return - } - const border = 3 - ctx.fillStyle = '#000' - ctx.fillRect(0, widgetY, widgetWidth, height) - ctx.fillStyle = this.value - ctx.fillRect( - border, - widgetY + border, - widgetWidth - border * 2, - height - border * 2 - ) - const color = parseCss(this.value.default || this.value) - if (!color) { - return - } - ctx.fillStyle = shared.isColorBright(color.values, 125) ? '#000' : '#fff' - - ctx.font = '14px Arial' - ctx.textAlign = 'center' - ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14) - } - widget.mouse = function (e, pos, node) { - if (e.type === 'pointerdown') { - const widgets = node.widgets.filter((w) => w.type === 'COLOR') - - for (const w of widgets) { - // color picker - const rect = [w.last_y, w.last_y + 32] - if (pos[1] > rect[0] && pos[1] < rect[1]) { - const picker = document.createElement('input') - picker.type = 'color' - picker.value = this.value - - picker.style.position = 'absolute' - picker.style.left = '999999px' //(window.innerWidth / 2) + "px"; - picker.style.top = '999999px' //(window.innerHeight / 2) + "px"; - - document.body.appendChild(picker) - - picker.addEventListener('change', () => { - this.value = picker.value - node.graph._version++ - node.setDirtyCanvas(true, true) - picker.remove() - }) - - picker.click() - } - } - } - } - widget.computeSize = function (width) { - return [width, 32] - } - - return widget - }, - - DEBUG_IMG: (name, val) => { - const w = { - name, - type: 'image', - value: val, - draw: function (ctx, node, widgetWidth, widgetY, height) { - const [cw, ch] = this.computeSize(widgetWidth) - shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) - }, - computeSize: function (width) { - const ratio = this.inputRatio || 1 - if (width) { - return [width, width / ratio + 4] - } - return [128, 128] - }, - onRemoved: function () { - if (this.inputEl) { - this.inputEl.remove() - } - }, - } - - w.inputEl = document.createElement('img') - w.inputEl.src = w.value - w.inputEl.onload = function () { - w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight - } - document.body.appendChild(w.inputEl) - return w - }, - DEBUG_STRING: (name, val) => { - const fontSize = 16 - const w = { - name, - type: 'debug_text', - - draw: function (ctx, node, widgetWidth, widgetY, height) { - // const [cw, ch] = this.computeSize(widgetWidth) - shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) - }, - computeSize(width) { - if (!this.value) { - return [32, 32] - } - if (!width) { - console.debug(`No width ${this.parent.size}`) - } - let dimensions - withFont(app.ctx, `${fontSize}px monospace`, () => { - dimensions = calculateTextDimensions(app.ctx, this.value, width) - }) - const widgetWidth = Math.max( - width || this.width || 32, - dimensions.maxLineWidth - ) - const widgetHeight = dimensions.textHeight * 1.5 - return [widgetWidth, widgetHeight] - }, - onRemoved: function () { - if (this.inputEl) { - this.inputEl.remove() - } - }, - get value() { - return this.inputEl.innerHTML - }, - set value(val) { - this.inputEl.innerHTML = val - this.parent?.setSize?.(this.parent?.computeSize()) - }, - } - - w.inputEl = document.createElement('p') - w.inputEl.style = ` - text-align: center; - font-size: ${fontSize}px; - color: var(--input-text); - line-height: 0; - font-family: monospace; - ` - w.value = val - document.body.appendChild(w.inputEl) - - return w - }, -} - -/** - * @returns {import("./types/comfy").ComfyExtension} extension - */ -const mtb_widgets = { - name: 'mtb.widgets', - - init: async () => { - log('Registering mtb.widgets') - try { - const res = await api.fetchApi('/mtb/debug') - const msg = await res.json() - if (!window.MTB) { - window.MTB = {} - } - window.MTB.DEBUG = msg.enabled - } catch (e) { - console.error('Error:', error) - } - }, - - setup: () => { - app.ui.settings.addSetting({ - id: 'mtb.Debug.enabled', - name: '[mtb] Enable Debug (py and js)', - type: 'boolean', - defaultValue: false, - - tooltip: - 'This will enable debug messages in the console and in the python console respectively', - attrs: { - style: { - fontFamily: 'monospace', - }, - }, - async onChange(value) { - if (value) { - console.log('Enabled DEBUG mode') - } - if (!window.MTB) { - window.MTB = {} - } - window.MTB.DEBUG = value - await api - .fetchApi('/mtb/debug', { - method: 'POST', - body: JSON.stringify({ - enabled: value, - }), - }) - .then((response) => {}) - .catch((error) => { - console.error('Error:', error) - }) - }, - }) - }, - - getCustomWidgets: function () { - return { - BOOL: (node, inputName, inputData, app) => { - console.debug('Registering bool') - - return { - widget: node.addCustomWidget( - MtbWidgets.BOOL(inputName, inputData[1]?.default || false) - ), - minWidth: 150, - minHeight: 30, - } - }, - - COLOR: (node, inputName, inputData, app) => { - console.debug('Registering color') - return { - widget: node.addCustomWidget( - MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000') - ), - minWidth: 150, - minHeight: 30, - } - }, - // BBOX: (node, inputName, inputData, app) => { - // console.debug("Registering bbox") - // return { - // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), - // minWidth: 150, - // minHeight: 30, - // } - - // } - } - }, - /** - * @param {import("./types/comfy").NodeType} nodeType - * @param {import("./types/comfy").NodeDef} nodeData - * @param {import("./types/comfy").App} app - */ - async beforeRegisterNodeDef(nodeType, nodeData, app) { - // const rinputs = nodeData.input?.required - - let has_custom = false - if (nodeData.input && nodeData.input.required) { - for (const i of Object.keys(nodeData.input.required)) { - const input_type = nodeData.input.required[i][0] - - if (newTypes.includes(input_type)) { - has_custom = true - break - } - } - } - if (has_custom) { - //- Add widgets on node creation - const onNodeCreated = nodeType.prototype.onNodeCreated - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated - ? onNodeCreated.apply(this, arguments) - : undefined - this.serialize_widgets = true - this.setSize?.(this.computeSize()) - - this.onRemoved = function () { - // When removing this node we need to remove the input from the DOM - shared.cleanupNode(this) - } - return r - } - - //- Extra menus - const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions - nodeType.prototype.getExtraMenuOptions = function (_, options) { - const r = origGetExtraMenuOptions - ? origGetExtraMenuOptions.apply(this, arguments) - : undefined - if (this.widgets) { - let toInput = [] - let toWidget = [] - for (const w of this.widgets) { - if (w.type === shared.CONVERTED_TYPE) { - //- This is already handled by widgetinputs.js - // toWidget.push({ - // content: `Convert ${w.name} to widget`, - // callback: () => shared.convertToWidget(this, w), - // }); - } else if (newTypes.includes(w.type)) { - const config = nodeData?.input?.required[w.name] || - nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}] - - toInput.push({ - content: `Convert ${w.name} to input`, - callback: () => shared.convertToInput(this, w, config), - }) - } - } - if (toInput.length) { - options.push(...toInput, null) - } - - if (toWidget.length) { - options.push(...toWidget, null) - } - } - - return r - } - } - - //- Extending Python Nodes - switch (nodeData.name) { - case 'Psd Save (mtb)': { - const onConnectionsChange = nodeType.prototype.onConnectionsChange - nodeType.prototype.onConnectionsChange = function ( - type, - index, - connected, - link_info - ) { - const r = onConnectionsChange - ? onConnectionsChange.apply(this, arguments) - : undefined - shared.dynamic_connection(this, index, connected) - return r - } - break - } - //TODO: remove this non sense - case 'Get Batch From History (mtb)': { - const onNodeCreated = nodeType.prototype.onNodeCreated - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated - ? onNodeCreated.apply(this, arguments) - : undefined - const internal_count = this.widgets.find( - (w) => w.name === 'internal_count' - ) - shared.hideWidgetForGood(this, internal_count) - internal_count.afterQueued = function () { - this.value++ - } - - return r - } - - const onExecuted = nodeType.prototype.onExecuted - nodeType.prototype.onExecuted = function (message) { - const r = onExecuted ? onExecuted.apply(this, message) : undefined - return r - } - - break - } - case 'Save Gif (mtb)': - case 'Save Animated Image (mtb)': { - const onExecuted = nodeType.prototype.onExecuted - nodeType.prototype.onExecuted = function (message) { - const prefix = 'anything_' - const r = onExecuted ? onExecuted.apply(this, message) : undefined - - if (this.widgets) { - const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`) - if (pos !== -1) { - for (let i = pos; i < this.widgets.length; i++) { - this.widgets[i].onRemoved?.() - } - this.widgets.length = pos - } - - let imgURLs = [] - if (message) { - if (message.gif) { - imgURLs = imgURLs.concat( - message.gif.map((params) => { - return api.apiURL( - '/view?' + new URLSearchParams(params).toString() - ) - }) - ) - } - if (message.apng) { - imgURLs = imgURLs.concat( - message.apng.map((params) => { - return api.apiURL( - '/view?' + new URLSearchParams(params).toString() - ) - }) - ) - } - let i = 0 - for (const img of imgURLs) { - const w = this.addCustomWidget( - MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img) - ) - w.parent = this - i++ - } - } - const onRemoved = this.onRemoved - this.onRemoved = () => { - shared.cleanupNode(this) - return onRemoved?.() - } - } - this.setSize?.(this.computeSize()) - return r - } - - break - } - case 'Animation Builder (mtb)': { - const onNodeCreated = nodeType.prototype.onNodeCreated - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated - ? onNodeCreated.apply(this, arguments) - : undefined - - this.changeMode(LiteGraph.ALWAYS) - - const raw_iteration = this.widgets.find( - (w) => w.name === 'raw_iteration' - ) - const raw_loop = this.widgets.find((w) => w.name === 'raw_loop') - - const total_frames = this.widgets.find( - (w) => w.name === 'total_frames' - ) - const loop_count = this.widgets.find((w) => w.name === 'loop_count') - - shared.hideWidgetForGood(this, raw_iteration) - shared.hideWidgetForGood(this, raw_loop) - - raw_iteration._value = 0 - - const value_preview = this.addCustomWidget( - MtbWidgets['DEBUG_STRING']('value_preview', 'Idle') - ) - value_preview.parent = this - - const loop_preview = this.addCustomWidget( - MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle') - ) - loop_preview.parent = this - - const onReset = () => { - raw_iteration.value = 0 - raw_loop.value = 0 - - value_preview.value = 'Idle' - loop_preview.value = 'Iteration: Idle' - - app.canvas.setDirty(true) - } - - const reset_button = this.addWidget( - 'button', - `Reset`, - 'reset', - onReset - ) - - const run_button = this.addWidget('button', `Queue`, 'queue', () => { - onReset() // this could maybe be a setting or checkbox - app.queuePrompt(0, total_frames.value * loop_count.value) - window.MTB?.notify?.( - `Started a queue of ${total_frames.value} frames (for ${ - loop_count.value - } loop, so ${total_frames.value * loop_count.value})`, - 5000 - ) - }) - - this.onRemoved = () => { - shared.cleanupNode(this) - app.canvas.setDirty(true) - } - - raw_iteration.afterQueued = function () { - this.value++ - raw_loop.value = Math.floor(this.value / total_frames.value) - - value_preview.value = `frame: ${ - raw_iteration.value % total_frames.value - } / ${total_frames.value - 1}` - - if (raw_loop.value + 1 > loop_count.value) { - loop_preview.value = 'Done 😎!' - } else { - loop_preview.value = `current loop: ${raw_loop.value + 1}/${ - loop_count.value - }` - } - } - - return r - } - - break - } - case 'Text Encore Frames (mtb)': { - const onConnectionsChange = nodeType.prototype.onConnectionsChange - nodeType.prototype.onConnectionsChange = function ( - type, - index, - connected, - link_info - ) { - const r = onConnectionsChange - ? onConnectionsChange.apply(this, arguments) - : undefined - - shared.dynamic_connection(this, index, connected) - return r - } - break - } - case 'Interpolate Clip Sequential (mtb)': { - const onNodeCreated = nodeType.prototype.onNodeCreated - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated - ? onNodeCreated.apply(this, arguments) - : undefined - const addReplacement = () => { - const input = this.addInput( - `replacement_${this.widgets.length}`, - 'STRING', - '' - ) - console.log(input) - this.addWidget('STRING', `replacement_${this.widgets.length}`, '') - } - //- add - this.addWidget('button', '+', 'add', function (value, widget, node) { - console.log('Button clicked', value, widget, node) - addReplacement() - }) - //- remove - this.addWidget( - 'button', - '-', - 'remove', - function (value, widget, node) { - console.log(`Button clicked: ${value}`, widget, node) - } - ) - - return r - } - break - } - case 'Styles Loader (mtb)': { - const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions - nodeType.prototype.getExtraMenuOptions = function (_, options) { - const r = origGetExtraMenuOptions - ? origGetExtraMenuOptions.apply(this, arguments) - : undefined - - const getStyle = async (node) => { - try { - const getStyles = await api.fetchApi('/mtb/actions', { - method: 'POST', - body: JSON.stringify({ - name: 'getStyles', - args: - node.widgets && node.widgets[0].value - ? node.widgets[0].value - : '', - }), - }) - - const output = await getStyles.json() - return output?.result - } catch (e) { - console.error(e) - } - } - const extracters = [ - { - content: 'Extract Positive to Text node', - callback: async () => { - const style = await getStyle(this) - if (style && style.length >= 1) { - if (style[0]) { - window.MTB?.notify?.( - `Extracted positive from ${this.widgets[0].value}` - ) - const tn = LiteGraph.createNode('Text box') - app.graph.add(tn) - tn.title = `${this.widgets[0].value} (Positive)` - tn.widgets[0].value = style[0] - } else { - window.MTB?.notify?.( - `No positive to extract for ${this.widgets[0].value}` - ) - } - } - }, - }, - { - content: 'Extract Negative to Text node', - callback: async () => { - const style = await getStyle(this) - if (style && style.length >= 2) { - if (style[1]) { - window.MTB?.notify?.( - `Extracted negative from ${this.widgets[0].value}` - ) - const tn = LiteGraph.createNode('Text box') - app.graph.add(tn) - tn.title = `${this.widgets[0].value} (Negative)` - tn.widgets[0].value = style[1] - } else { - window.MTB.notify( - `No negative to extract for ${this.widgets[0].value}` - ) - } - } - }, - }, - ] - options.push(...extracters) - } - - break - } - case 'Save Tensors (mtb)': { - const onDrawBackground = nodeType.prototype.onDrawBackground - nodeType.prototype.onDrawBackground = function (ctx, canvas) { - const r = onDrawBackground - ? onDrawBackground.apply(this, arguments) - : undefined - // // draw a circle on the top right of the node, with text inside - // ctx.fillStyle = "#fff"; - // ctx.beginPath(); - // ctx.arc(this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5, this.node_width * 0.5, 0, Math.PI * 2); - // ctx.fill(); - - // ctx.fillStyle = "#000"; - // ctx.textAlign = "center"; - // ctx.font = "bold 12px Arial"; - // ctx.fillText("Save Tensors", this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5); - - return r - } - break - } - default: { - break - } - } - }, -} - -app.registerExtension(mtb_widgets) +/** + * File: mtb_widgets.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +// TODO: Use the builtin addDOMWidget everywhere appropriate + +import { app } from '../../scripts/app.js' +import { api } from '../../scripts/api.js' + +import parseCss from './extern/parse-css.js' +import * as shared from './comfy_shared.js' +import { log } from './comfy_shared.js' + +const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX'] + +const withFont = (ctx, font, cb) => { + const oldFont = ctx.font + ctx.font = font + cb() + ctx.font = oldFont +} + +const calculateTextDimensions = (ctx, value, width, fontSize = 16) => { + const words = value.split(' ') + const lines = [] + let currentLine = '' + for (const word of words) { + const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}` + const testWidth = ctx.measureText(testLine).width + if (testWidth > width) { + lines.push(currentLine) + currentLine = word + } else { + currentLine = testLine + } + } + if (lines.length === 0) lines.push(value) + const textHeight = (lines.length + 1) * fontSize + const maxLineWidth = lines.reduce( + (maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), + 0 + ) + return { textHeight, maxLineWidth } +} + +export const MtbWidgets = { + BBOX: (key, val) => { + /** @type {import("./types/litegraph").IWidget} */ + const widget = { + name: key, + type: 'BBOX', + // options: val, + y: 0, + value: val?.default || [0, 0, 0, 0], + options: {}, + + draw: function (ctx, node, widget_width, widgetY, height) { + const hide = this.type !== 'BBOX' && app.canvas.ds.scale > 0.5 + + const show_text = true + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR + const background_color = LiteGraph.WIDGET_BGCOLOR + const text_color = LiteGraph.WIDGET_TEXT_COLOR + const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR + const H = LiteGraph.NODE_WIDGET_HEIGHT + + let margin = 15 + let numWidgets = 4 // Number of stacked widgets + + if (hide) return + + for (let i = 0; i < numWidgets; i++) { + let currentY = widgetY + i * (H + margin) // Adjust Y position for each widget + + ctx.textAlign = 'left' + ctx.strokeStyle = outline_color + ctx.fillStyle = background_color + ctx.beginPath() + if (show_text) + ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [ + H * 0.5, + ]) + else ctx.rect(margin, currentY, widget_width - margin * 2, H) + ctx.fill() + if (show_text) { + if (!this.disabled) ctx.stroke() + ctx.fillStyle = text_color + if (!this.disabled) { + ctx.beginPath() + ctx.moveTo(margin + 16, currentY + 5) + ctx.lineTo(margin + 6, currentY + H * 0.5) + ctx.lineTo(margin + 16, currentY + H - 5) + ctx.fill() + ctx.beginPath() + ctx.moveTo(widget_width - margin - 16, currentY + 5) + ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5) + ctx.lineTo(widget_width - margin - 16, currentY + H - 5) + ctx.fill() + } + ctx.fillStyle = secondary_text_color + ctx.fillText( + this.label || this.name, + margin * 2 + 5, + currentY + H * 0.7 + ) + ctx.fillStyle = text_color + ctx.textAlign = 'right' + + ctx.fillText( + Number(this.value).toFixed( + this.options?.precision !== undefined + ? this.options.precision + : 3 + ), + widget_width - margin * 2 - 20, + currentY + H * 0.7 + ) + } + } + }, + mouse: function (event, pos, node) { + let old_value = this.value + let x = pos[0] - node.pos[0] + let y = pos[1] - node.pos[1] + let width = node.size[0] + let H = LiteGraph.NODE_WIDGET_HEIGHT + let margin = 5 + let numWidgets = 4 // Number of stacked widgets + + for (let i = 0; i < numWidgets; i++) { + let currentY = y + i * (H + margin) // Adjust Y position for each widget + + if ( + event.type == LiteGraph.pointerevents_method + 'move' && + this.type == 'BBOX' + ) { + if (event.deltaX) + this.value += event.deltaX * 0.1 * (this.options?.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } else if (event.type == LiteGraph.pointerevents_method + 'down') { + let values = this.options?.values + if (values && values.constructor === Function) { + values = this.options.values(w, node) + } + let values_list = null + + let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 + if (this.type == 'BBOX') { + this.value += delta * 0.1 * (this.options.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } else if (delta) { + //clicked in arrow, used for combos + let index = -1 + this.last_mouseclick = 0 //avoids dobl click event + if (values.constructor === Object) + index = values_list.indexOf(String(this.value)) + delta + else index = values_list.indexOf(this.value) + delta + if (index >= values_list.length) { + index = values_list.length - 1 + } + if (index < 0) { + index = 0 + } + if (values.constructor === Array) this.value = values[index] + else this.value = index + } + } //end mousedown + else if ( + event.type == LiteGraph.pointerevents_method + 'up' && + this.type == 'BBOX' + ) { + let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 + if (event.click_time < 200 && delta == 0) { + this.prompt( + 'Value', + this.value, + function (v) { + // check if v is a valid equation or a number + if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { + try { + //solve the equation if possible + v = eval(v) + } catch (e) { } + } + this.value = Number(v) + shared.inner_value_change(this, this.value, event) + }.bind(w), + event + ) + } + } + + if (old_value != this.value) + setTimeout( + function () { + shared.inner_value_change(this, this.value, event) + }.bind(this), + 20 + ) + + app.canvas.setDirty(true) + } + }, + computeSize: function (width) { + return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4] + }, + // onDrawBackground: function (ctx) { + // if (!this.flags.collapsed) return; + // this.inputEl.style.display = "block"; + // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; + // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; + // }, + // onInputChange: function (e) { + // const property = e.target.dataset.property; + // const bbox = this.getInputData(0); + // if (!bbox) return; + // bbox[property] = parseFloat(e.target.value); + // this.setOutputData(0, bbox); + // } + } + + widget.desc = 'Represents a Bounding Box with x, y, width, and height.' + return widget + }, + + COLOR: (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = {} + widget.y = 0 + widget.name = key + widget.type = 'COLOR' + widget.options = { default: '#ff0000' } + widget.value = val || '#ff0000' + widget.draw = function (ctx, node, widgetWidth, widgetY, height) { + const hide = this.type !== 'COLOR' && app.canvas.ds.scale > 0.5 + if (hide) { + return + } + const border = 3 + ctx.fillStyle = '#000' + ctx.fillRect(0, widgetY, widgetWidth, height) + ctx.fillStyle = this.value + ctx.fillRect( + border, + widgetY + border, + widgetWidth - border * 2, + height - border * 2 + ) + const color = parseCss(this.value.default || this.value) + if (!color) { + return + } + ctx.fillStyle = shared.isColorBright(color.values, 125) ? '#000' : '#fff' + + ctx.font = '14px Arial' + ctx.textAlign = 'center' + ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14) + } + widget.mouse = function (e, pos, node) { + if (e.type === 'pointerdown') { + const widgets = node.widgets.filter((w) => w.type === 'COLOR') + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32] + if (pos[1] > rect[0] && pos[1] < rect[1]) { + const picker = document.createElement('input') + picker.type = 'color' + picker.value = this.value + + picker.style.position = 'absolute' + picker.style.left = '999999px' //(window.innerWidth / 2) + "px"; + picker.style.top = '999999px' //(window.innerHeight / 2) + "px"; + + document.body.appendChild(picker) + + picker.addEventListener('change', () => { + this.value = picker.value + node.graph._version++ + node.setDirtyCanvas(true, true) + picker.remove() + }) + + picker.click() + } + } + } + } + widget.computeSize = function (width) { + return [width, 32] + } + + return widget + }, + + DEBUG_IMG: (name, val) => { + const w = { + name, + type: 'image', + value: val, + draw: function (ctx, node, widgetWidth, widgetY, height) { + const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) + }, + computeSize: function (width) { + const ratio = this.inputRatio || 1 + if (width) { + return [width, width / ratio + 4] + } + return [128, 128] + }, + onRemoved: function () { + if (this.inputEl) { + this.inputEl.remove() + } + }, + } + + w.inputEl = document.createElement('img') + w.inputEl.src = w.value + w.inputEl.onload = function () { + w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight + } + document.body.appendChild(w.inputEl) + return w + }, + DEBUG_STRING: (name, val) => { + const fontSize = 16 + const w = { + name, + type: 'debug_text', + + draw: function (ctx, node, widgetWidth, widgetY, height) { + // const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) + }, + computeSize(width) { + if (!this.value) { + return [32, 32] + } + if (!width) { + console.debug(`No width ${this.parent.size}`) + } + let dimensions + withFont(app.ctx, `${fontSize}px monospace`, () => { + dimensions = calculateTextDimensions(app.ctx, this.value, width) + }) + const widgetWidth = Math.max( + width || this.width || 32, + dimensions.maxLineWidth + ) + const widgetHeight = dimensions.textHeight * 1.5 + return [widgetWidth, widgetHeight] + }, + onRemoved: function () { + if (this.inputEl) { + this.inputEl.remove() + } + }, + get value() { + return this.inputEl.innerHTML + }, + set value(val) { + this.inputEl.innerHTML = val + this.parent?.setSize?.(this.parent?.computeSize()) + }, + } + + w.inputEl = document.createElement('p') + w.inputEl.style = ` + text-align: center; + font-size: ${fontSize}px; + color: var(--input-text); + line-height: 0; + font-family: monospace; + ` + w.value = val + document.body.appendChild(w.inputEl) + + return w + }, +} + +/** + * @returns {import("./types/comfy").ComfyExtension} extension + */ +const mtb_widgets = { + name: 'mtb.widgets', + + init: async () => { + log('Registering mtb.widgets') + try { + const res = await api.fetchApi('/mtb/debug') + const msg = await res.json() + if (!window.MTB) { + window.MTB = {} + } + window.MTB.DEBUG = msg.enabled + } catch (e) { + console.error('Error:', error) + } + }, + + setup: () => { + app.ui.settings.addSetting({ + id: 'mtb.Debug.enabled', + name: '[⚡mtb] Enable Debug (py and js)', + type: 'boolean', + defaultValue: false, + + tooltip: + 'This will enable debug messages in the console and in the python console respectively', + attrs: { + style: { + fontFamily: 'monospace', + }, + }, + async onChange(value) { + if (value) { + console.log('Enabled DEBUG mode') + } + if (!window.MTB) { + window.MTB = {} + } + window.MTB.DEBUG = value + await api + .fetchApi('/mtb/debug', { + method: 'POST', + body: JSON.stringify({ + enabled: value, + }), + }) + .then((response) => { }) + .catch((error) => { + console.error('Error:', error) + }) + }, + }) + }, + + getCustomWidgets: function () { + return { + BOOL: (node, inputName, inputData, app) => { + console.debug('Registering bool') + + return { + widget: node.addCustomWidget( + MtbWidgets.BOOL(inputName, inputData[1]?.default || false) + ), + minWidth: 150, + minHeight: 30, + } + }, + + COLOR: (node, inputName, inputData, app) => { + console.debug('Registering color') + return { + widget: node.addCustomWidget( + MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000') + ), + minWidth: 150, + minHeight: 30, + } + }, + // BBOX: (node, inputName, inputData, app) => { + // console.debug("Registering bbox") + // return { + // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), + // minWidth: 150, + // minHeight: 30, + // } + + // } + } + }, + /** + * @param {import("./types/comfy").NodeType} nodeType + * @param {import("./types/comfy").NodeDef} nodeData + * @param {import("./types/comfy").App} app + */ + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // const rinputs = nodeData.input?.required + + let has_custom = false + if (nodeData.input && nodeData.input.required) { + for (const i of Object.keys(nodeData.input.required)) { + const input_type = nodeData.input.required[i][0] + + if (newTypes.includes(input_type)) { + has_custom = true + break + } + } + } + if (has_custom) { + //- Add widgets on node creation + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + this.serialize_widgets = true + this.setSize?.(this.computeSize()) + + this.onRemoved = function () { + // When removing this node we need to remove the input from the DOM + shared.cleanupNode(this) + } + return r + } + + //- Extra menus + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions + ? origGetExtraMenuOptions.apply(this, arguments) + : undefined + if (this.widgets) { + let toInput = [] + let toWidget = [] + for (const w of this.widgets) { + if (w.type === shared.CONVERTED_TYPE) { + //- This is already handled by widgetinputs.js + // toWidget.push({ + // content: `Convert ${w.name} to widget`, + // callback: () => shared.convertToWidget(this, w), + // }); + } else if (newTypes.includes(w.type)) { + const config = nodeData?.input?.required[w.name] || + nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}] + + toInput.push({ + content: `Convert ${w.name} to input`, + callback: () => shared.convertToInput(this, w, config), + }) + } + } + if (toInput.length) { + options.push(...toInput, null) + } + + if (toWidget.length) { + options.push(...toWidget, null) + } + } + + return r + } + } + + if (!nodeData.name.endsWith('(mtb)')) { + return + } + + //- Extending Python Nodes + switch (nodeData.name) { + case 'Psd Save (mtb)': { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + shared.dynamic_connection(this, index, connected) + return r + } + break + } + //TODO: remove this non sense + case 'Get Batch From History (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + const internal_count = this.widgets.find( + (w) => w.name === 'internal_count' + ) + shared.hideWidgetForGood(this, internal_count) + internal_count.afterQueued = function () { + this.value++ + } + + return r + } + + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + const r = onExecuted ? onExecuted.apply(this, message) : undefined + return r + } + + break + } + case 'Save Gif (mtb)': + case 'Save Animated Image (mtb)': { + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + const prefix = 'anything_' + const r = onExecuted ? onExecuted.apply(this, message) : undefined + + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`) + if (pos !== -1) { + for (let i = pos; i < this.widgets.length; i++) { + this.widgets[i].onRemoved?.() + } + this.widgets.length = pos + } + + let imgURLs = [] + if (message) { + if (message.gif) { + imgURLs = imgURLs.concat( + message.gif.map((params) => { + return api.apiURL( + '/view?' + new URLSearchParams(params).toString() + ) + }) + ) + } + if (message.apng) { + imgURLs = imgURLs.concat( + message.apng.map((params) => { + return api.apiURL( + '/view?' + new URLSearchParams(params).toString() + ) + }) + ) + } + let i = 0 + for (const img of imgURLs) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img) + ) + w.parent = this + i++ + } + } + const onRemoved = this.onRemoved + this.onRemoved = () => { + shared.cleanupNode(this) + return onRemoved?.() + } + } + this.setSize?.(this.computeSize()) + return r + } + + break + } + case 'Animation Builder (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + + this.changeMode(LiteGraph.ALWAYS) + + const raw_iteration = this.widgets.find( + (w) => w.name === 'raw_iteration' + ) + const raw_loop = this.widgets.find((w) => w.name === 'raw_loop') + + const total_frames = this.widgets.find( + (w) => w.name === 'total_frames' + ) + const loop_count = this.widgets.find((w) => w.name === 'loop_count') + + shared.hideWidgetForGood(this, raw_iteration) + shared.hideWidgetForGood(this, raw_loop) + + raw_iteration._value = 0 + + const value_preview = this.addCustomWidget( + MtbWidgets['DEBUG_STRING']('value_preview', 'Idle') + ) + value_preview.parent = this + + const loop_preview = this.addCustomWidget( + MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle') + ) + loop_preview.parent = this + + const onReset = () => { + raw_iteration.value = 0 + raw_loop.value = 0 + + value_preview.value = 'Idle' + loop_preview.value = 'Iteration: Idle' + + app.canvas.setDirty(true) + } + + const reset_button = this.addWidget( + 'button', + `Reset`, + 'reset', + onReset + ) + + const run_button = this.addWidget('button', `Queue`, 'queue', () => { + onReset() // this could maybe be a setting or checkbox + app.queuePrompt(0, total_frames.value * loop_count.value) + window.MTB?.notify?.( + `Started a queue of ${total_frames.value} frames (for ${loop_count.value + } loop, so ${total_frames.value * loop_count.value})`, + 5000 + ) + }) + + this.onRemoved = () => { + shared.cleanupNode(this) + app.canvas.setDirty(true) + } + + raw_iteration.afterQueued = function () { + this.value++ + raw_loop.value = Math.floor(this.value / total_frames.value) + + value_preview.value = `frame: ${raw_iteration.value % total_frames.value + } / ${total_frames.value - 1}` + + if (raw_loop.value + 1 > loop_count.value) { + loop_preview.value = 'Done 😎!' + } else { + loop_preview.value = `current loop: ${raw_loop.value + 1}/${loop_count.value + }` + } + } + + return r + } + + break + } + case 'Text Encore Frames (mtb)': { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + + shared.dynamic_connection(this, index, connected) + return r + } + break + } + case 'Interpolate Clip Sequential (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + const addReplacement = () => { + const input = this.addInput( + `replacement_${this.widgets.length}`, + 'STRING', + '' + ) + console.log(input) + this.addWidget('STRING', `replacement_${this.widgets.length}`, '') + } + //- add + this.addWidget('button', '+', 'add', function (value, widget, node) { + console.log('Button clicked', value, widget, node) + addReplacement() + }) + //- remove + this.addWidget( + 'button', + '-', + 'remove', + function (value, widget, node) { + console.log(`Button clicked: ${value}`, widget, node) + } + ) + + return r + } + break + } + case 'Styles Loader (mtb)': { + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions + ? origGetExtraMenuOptions.apply(this, arguments) + : undefined + + const getStyle = async (node) => { + try { + const getStyles = await api.fetchApi('/mtb/actions', { + method: 'POST', + body: JSON.stringify({ + name: 'getStyles', + args: + node.widgets && node.widgets[0].value + ? node.widgets[0].value + : '', + }), + }) + + const output = await getStyles.json() + return output?.result + } catch (e) { + console.error(e) + } + } + const extracters = [ + { + content: 'Extract Positive to Text node', + callback: async () => { + const style = await getStyle(this) + if (style && style.length >= 1) { + if (style[0]) { + window.MTB?.notify?.( + `Extracted positive from ${this.widgets[0].value}` + ) + const tn = LiteGraph.createNode('Text box') + app.graph.add(tn) + tn.title = `${this.widgets[0].value} (Positive)` + tn.widgets[0].value = style[0] + } else { + window.MTB?.notify?.( + `No positive to extract for ${this.widgets[0].value}` + ) + } + } + }, + }, + { + content: 'Extract Negative to Text node', + callback: async () => { + const style = await getStyle(this) + if (style && style.length >= 2) { + if (style[1]) { + window.MTB?.notify?.( + `Extracted negative from ${this.widgets[0].value}` + ) + const tn = LiteGraph.createNode('Text box') + app.graph.add(tn) + tn.title = `${this.widgets[0].value} (Negative)` + tn.widgets[0].value = style[1] + } else { + window.MTB.notify( + `No negative to extract for ${this.widgets[0].value}` + ) + } + } + }, + }, + ] + options.push(...extracters) + } + + break + } + case 'Add To Playlist (mtb)': { + shared.setupDynamicConnections(nodeType, 'video', 'VIDEO') + break + } + case 'Stack Images (mtb)': + case 'Concat Images (mtb)': { + shared.setupDynamicConnections(nodeType, 'image', 'IMAGE') + + break + } + case 'Batch Float Assemble (mtb)': + case 'Plot Batch Float (mtb)': { + shared.setupDynamicConnections(nodeType, 'floats', 'FLOATS') + break + } + case 'Batch Merge (mtb)': { + shared.setupDynamicConnections(nodeType, 'batches', 'IMAGE') + + break + } + // TODO: remove this, recommend pythongoss's version that is much better + case 'Math Expression (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + this.addInput(`x`, '*') + return r + } + + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + shared.dynamic_connection(this, index, connected, 'var_', '*', [ + 'x', + 'y', + 'z', + ]) + + //- infer type + if (link_info) { + const fromNode = this.graph._nodes.find( + (otherNode) => otherNode.id == link_info.origin_id + ) + const type = fromNode.outputs[link_info.origin_slot].type + this.inputs[index].type = type + // this.inputs[index].label = type.toLowerCase() + } + //- restore dynamic input + if (!connected) { + this.inputs[index].type = '*' + this.inputs[index].label = `number_${index + 1}` + } + } + + break + } + case 'Save Tensors (mtb)': { + const onDrawBackground = nodeType.prototype.onDrawBackground + nodeType.prototype.onDrawBackground = function (ctx, canvas) { + const r = onDrawBackground + ? onDrawBackground.apply(this, arguments) + : undefined + // // draw a circle on the top right of the node, with text inside + // ctx.fillStyle = "#fff"; + // ctx.beginPath(); + // ctx.arc(this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5, this.node_width * 0.5, 0, Math.PI * 2); + // ctx.fill(); + + // ctx.fillStyle = "#000"; + // ctx.textAlign = "center"; + // ctx.font = "bold 12px Arial"; + // ctx.fillText("Save Tensors", this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5); + + return r + } + break + } + default: { + break + } + } + }, +} + +app.registerExtension(mtb_widgets)