import { app } from '../../scripts/app.js' import * as shared from './comfy_shared.js' import { infoLogger } from './comfy_shared.js' import { MtbWidgets } from './mtb_widgets.js' import { ComfyWidgets } from '../../scripts/widgets.js' import * as mtb_widgets from './mtb_widgets.js' /** * @typedef {'number'|'string'|'vector2'|'vector3'|'vector4'|'color'} ConstantType * @typedef {import ("../../../web/types/litegraph.d.ts").LGraphNode} Node * @typedef {{x:number,y:number,z?:number,w?:number}} VectorValue * @typedef {} * */ /** * @param {number} size - The number of axis of the vector (2,3 or 4) * @param {number} val - The default scalar value to fill the vector with * @returns {VectorValue} vector * */ const initVector = (size, val = 0.0) => { const res = {} for (let i = 0; i < size; i++) { const axis = mtb_widgets.VECTOR_AXIS[i] res[axis] = val } return res } /** * * @extends {Node} * @classdesc Wrapper for the python node */ export class ConstantJs { constructor(python_node) { // this.uuid = shared.makeUUID() const wrapper = this python_node.shape = LiteGraph.BOX_SHAPE python_node.serialize_widgets = true const onNodeCreated = python_node.prototype.onNodeCreated python_node.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this) : undefined this.addProperty('type', 'number') this.addProperty('value', 0) this.removeInput(0) this.removeOutput(0) this.addOutput('Output', '*') // bind our wrapper this.configure = wrapper.configure.bind(this) // this.applyToGraph = wrapper.applyToGraph.bind(this) this.updateWidgets = wrapper.updateWidgets.bind(this) this.convertValue = wrapper.convertValue.bind(this) // this.updateOutput = wrapper.updateOutput.bind(this) this.updateOutputType = wrapper.updateOutputType.bind(this) // this.updateTargetWidgets = wrapper.updateTargetWidgets.bind(this) this.addWidget( 'combo', 'Type', this.properties.type, (value) => { this.properties.type = value this.updateWidgets() this.updateOutputType() }, { values: [ // 'number', 'float', 'int', 'string', 'vector2', 'vector3', 'vector4', 'color', ], }, ) this.updateWidgets() this.updateOutputType() for (let n = 0; n < this.inputs.length; n++) { this.removeInput(n) } this.inputs = [] return r } return } // NOTE: this is called onPrompt // applyToGraph() { // infoLogger('Updating values for backend') // this.updateTargetWidgets() // } // NOTE: deserialization happens here configure(info) { // super.configure(info) infoLogger('Configure Constant', { info, node: this }) this.properties.type = info.properties.type this.properties.value = info.properties.value this.pos = info.pos this.order = info.order this.updateWidgets() this.updateOutputType() } /** * Convert the old value type to the new one, falling back to some default * @param {ConstantType} propType - The target type */ convertValue(propType) { switch (propType) { case 'color': { if (typeof this.properties.value !== 'string') { this.properties.value = '#ffffff' } else if (this.properties.value[0] !== '#') { this.properties.value = '#ff0000' } break } case 'int': { if (typeof this.properties.value === 'object') { this.properties.value = Number.parseInt(this.properties.value.x) } else { this.properties.value = Number.parseInt(this.properties.value) || 0 } break } case 'float': { if (typeof this.properties.value === 'object') { this.properties.value = Number.parseFloat(this.properties.value.x) } else { this.properties.value = Number.parseFloat(this.properties.value) || 0.0 } break } case 'string': { if (typeof this.properties.value !== 'string') { this.properties.value = JSON.stringify(this.properties.value) } break } case 'vector2': case 'vector3': case 'vector4': { const numInputs = Number.parseInt(propType.charAt(6)) if (!this.properties.value) { this.properties.value = initVector(numInputs) // Array.from({ length: numInputs }, () => 0.0) } else if (typeof this.properties.value === 'string') { try { const parsed = JSON.parse(this.properties.value) const newVec = {} for ( let i = 0; i < Object.keys(mtb_widgets.VECTOR_AXIS).length; i++ ) { const axis = mtb_widgets.VECTOR_AXIS[i] if (Object.keys(parsed).includes(axis)) { newVec[axis] = parsed[axis] } } this.properties.value = newVec } catch (e) { shared.errorLogger(e) infoLogger( `Couldn't parse string to vec (${this.properties.value})`, ) this.properties.value = initVector(numInputs) } } else if (typeof this.properties.value === 'number') { const newVec = initVector(numInputs) newVec.x = Number.parseFloat(this.properties.value) this.properties.value = newVec } if ( typeof this.properties.value === 'object' && Object.keys(this.properties.value).length !== numInputs ) { const current = Object.keys(this.properties.value) if (current.length < numInputs) { infoLogger('current value smaller than target, adjusting') for (let index = current.length; index < numInputs; index++) { this.properties.value[mtb_widgets.VECTOR_AXIS[index]] = 0.0 } } else { infoLogger('current value greater than target, adjusting') const newVal = {} for (let index = 0; index < numInputs; index++) { newVal[mtb_widgets.VECTOR_AXIS[index]] = this.properties.value[mtb_widgets.VECTOR_AXIS[index]] } this.properties.value = newVal } } break } default: break } } /** * Remove all widgets but the comboBox for selecting the type * then recreate the appropriate widget from scratch */ updateWidgets() { // NOTE: Remove existing widgets for (let i = 1; i < this.widgets.length; i++) { const element = this.widgets[i] if (element.onRemove) { element.onRemove() } // element?.onRemove() } this.widgets.splice(1) this.widgets[0].value = this.properties.type this.convertValue(this.properties.type) switch (this.properties.type) { case 'color': { const col_widget = this.addCustomWidget( MtbWidgets.COLOR('Value', this.properties.value), ) col_widget.callback = (col) => { this.properties.value = col // this.updateOutput() } break } case 'int': { const f_widget = this.addCustomWidget( ComfyWidgets.INT( this, 'Value', [ '', { default: this.properties.value, callback: (val) => console.log('VALUE', val), }, ], app, ), ) f_widget.widget.callback = (val) => { this.properties.value = val } break } case 'float': { this.addWidget('number', 'Value', this.properties.value, (val) => { this.properties.value = val }) break } case 'string': { mtb_widgets.addMultilineWidget( this, 'Value', { defaultVal: this.properties.value, }, (v) => { this.properties.value = v // this.updateOutput() }, ) break } case 'vector2': case 'vector3': case 'vector4': { const numInputs = Number.parseInt(this.properties.type.charAt(6)) const node = this const v_widget = mtb_widgets.addVectorWidget( this, 'Value', this.properties.value, // value numInputs, // vector_size function (v) { node.properties.value = v // this.updateOutput() }, ) break } // NOTE: this is not reached anymore, kept for reference case 'number': { if (typeof this.properties.value !== 'number') { this.properties.value = 0.0 } const n_widget = this.addWidget( 'number', 'Value', this.properties.force_int ? Number.parseInt(this.properties.value) : this.properties.value, (value) => { this.properties.value = this.properties.force_int ? Number.parseInt(value) : value // this.updateOutput() }, ) //override the callback const origCallback = n_widget.callback const node = this n_widget.callback = function (val) { const r = origCallback ? origCallback.apply(this, [val]) : undefined if (node.properties.force_int) { // TODO: rework this, a it makes it harder to manipulate this.value = Number.parseInt(this.value) node.properties.value = Number.parseInt(this.value) } infoLogger('NEW NUMBER', this.value) return r } this.addWidget( 'toggle', 'Convert to Integer', this.properties.force_int, (value) => { this.properties.force_int = value this.updateOutputType() }, ) break } default: break } } onConnectionsChange(type, slotIndex, isConnected, link, ioSlot) { // super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot) if (isConnected) { this.updateTargetWidgets([link.id]) } } updateOutputType() { infoLogger('Updating output type') const rm_if_mismatch = (type) => { if (this.outputs[0].type !== type) { for (let i = 0; i < this.outputs.length; i++) { this.removeOutput(i) } this.addOutput('output', type) // this.setOutputDataType(0, type) } } switch (this.properties.type) { case 'color': rm_if_mismatch('COLOR') break case 'float': rm_if_mismatch('FLOAT') break case 'int': rm_if_mismatch('INT') break case 'number': if (this.properties.force_int) { rm_if_mismatch('INT') } else { rm_if_mismatch('FLOAT') } break case 'string': rm_if_mismatch('STRING') break // case 'vector2': // case 'vector3': // case 'vector4': // rm_if_mismatch('FLOAT') // break case 'vector2': rm_if_mismatch('VECTOR2') break case 'vector3': rm_if_mismatch('VECTOR3') break case 'vector4': rm_if_mismatch('VECTOR4') break default: break } // this.updateOutput() } /** * NOTE: This feels hacky but seems to work fine * since Constant is a virtual node. */ updateTargetWidgets(u_links) { infoLogger('Updating target widgets') if (!app.graph.links) return const links = u_links || this.outputs[0].links if (!links) return for (let i = 0; i < links.length; i++) { const link = app.graph.links[links[i]] const tgt_node = app.graph.getNodeById(link.target_id) if (!tgt_node || !tgt_node.inputs) return const tgt_input = tgt_node.inputs[link.target_slot] if (!tgt_input) return const tgt_widget = tgt_node.widgets.filter( (w) => w.name === tgt_input.name, ) // infoLogger('Constant Target Node', tgt_node) // infoLogger('Constant Target Input', tgt_input) if (!tgt_widget || tgt_widget.length === 0) return tgt_widget[0].value = this.properties.value } } updateOutput() { infoLogger('Updating output value') const value = this.properties.value switch (this.properties.type) { case 'color': this.setOutputData(0, value) break case 'number': if (this.properties.force_int) { this.setOutputData(0, Number.parseInt(value)) } else { this.setOutputData(0, Number.parseFloat(value)) } break case 'string': this.setOutputData(0, value.toString()) break case 'vector2': case 'vector3': case 'vector4': this.setOutputData(0, value) break // case 'vector2': // this.setOutputData(0, value.slice(0, 2)) // break // case 'vector3': // this.setOutputData(0, value.slice(0, 3)) // break // case 'vector4': // this.setOutputData(0, value.slice(0, 4)) // break default: break } infoLogger('New Value', this.value) this.updateTargetWidgets() } } app.registerExtension({ name: 'mtb.constant', async beforeRegisterNodeDef(nodeType, nodeData, _app) { if (nodeData.name === 'Constant (mtb)') { new ConstantJs(nodeType) } }, // NOTE: old js only registration // // registerCustomNodes() { // LiteGraph.registerNodeType('Constant (mtb)', Constant) // // Constant.category = 'mtb/utils' // Constant.title = 'Constant (mtb)' // }, })