multimodalart's picture
Squashing commit
4450790 verified
raw
history blame
37.1 kB
/**
* File: mtb_widgets.js
* Project: comfy_mtb
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
*
*/
/// <reference path="../types/typedefs.js" />
// TODO: Use the builtin addDOMWidget everywhere appropriate
import { app } from '../../scripts/app.js'
import { api } from '../../scripts/api.js'
import * as mtb_ui from './mtb_ui.js'
import parseCss from './extern/parse-css.js'
import * as shared from './comfy_shared.js'
import { infoLogger } from './comfy_shared.js'
import { NumberInputWidget } from './numberInput.js'
// NOTE: new widget types registered by MTB Widgets
const newTypes = [/*'BOOL'*/ , 'COLOR', 'BBOX']
const deprecated_nodes = {
// 'Animation Builder':
// 'Kept to avoid breaking older script but replaced by TimeEngine',
}
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 function addMultilineWidget(node, name, opts, callback) {
const inputEl = document.createElement('textarea')
inputEl.className = 'comfy-multiline-input'
inputEl.value = opts.defaultVal
inputEl.placeholder = opts.placeholder || name
const widget = node.addDOMWidget(name, 'textmultiline', inputEl, {
getValue() {
return inputEl.value
},
setValue(v) {
inputEl.value = v
},
})
widget.inputEl = inputEl
inputEl.addEventListener('input', () => {
callback?.(widget.value)
widget.callback?.(widget.value)
})
widget.onRemove = () => {
inputEl.remove()
}
return { minWidth: 400, minHeight: 200, widget }
}
export const VECTOR_AXIS = {
0: 'x',
1: 'y',
2: 'z',
3: 'w',
}
export function addVectorWidgetW(
node,
name,
value,
vector_size,
_callback,
app,
) {
// const inputEl = document.createElement('div')
// const vecEl = document.createElement('div')
//
// inputEl.style.background = 'red'
//
// inputEl.className = 'comfy-vector-container'
// vecEl.className = 'comfy-vector-input'
//
// vecEl.style.display = 'flex'
// inputEl.appendChild(vecEl)
const inputs = []
for (let i = 0; i < vector_size; i++) {
// const input = document.createElement('input')
// input.type = 'number'
// input.value = value[VECTOR_AXIS[i]]
const input = node.addWidget(
'number',
`${name}_${VECTOR_AXIS[i]}`,
value[VECTOR_AXIS[i]],
(val) => {},
)
inputs.push(input)
// vecEl.appendChild(input)
}
//
// const widget = node.addDOMWidget(name, 'vector', inputEl, {
// getValue() {
// return JSON.stringify(widget._value)
// },
// setValue(v) {
// widget._value = v
// },
// afterResize(node, widget) {
// console.log('After resize', { that: this, node, widget })
// },
// })
//
// console.log('prev callback', widget.callback)
// widget.callback = callback
// widget._value = value
//
// for (let i = 0; i < vector_size; i++) {
// const input = inputs[i]
// input.addEventListener('change', (event) => {
// widget._value[VECTOR_AXIS[i]] = Number.parseFloat(event.target.value)
// widget.callback?.(widget._value)
// node.graph._version++
// node.setDirtyCanvas(true, true)
// })
// }
// // document.body.append(inputEl)
//
// widget.inputEl = inputEl
// widget.vecEl = vecEl
//
// inputEl.addEventListener('input', () => {
// widget.callback?.(widget.value)
// })
//
return { minWidth: 400, minHeight: 200, widget }
}
export function addVectorWidget(node, name, value, vector_size, callback, app) {
const inputEl = document.createElement('div')
const vecEl = document.createElement('div')
inputEl.className = 'comfy-vector-container'
vecEl.className = 'comfy-vector-input'
vecEl.id = 'vecEl'
vecEl.style.display = 'flex'
vecEl.style.flexDirection = 'column'
inputEl.appendChild(vecEl)
const inputs = []
//
// for (let i = 0; i < vector_size; i++) {
// const input = document.createElement('input')
// input.type = 'number'
// input.value = value[VECTOR_AXIS[i]]
// inputs.push(input)
// vecEl.appendChild(input)
// }
const widget = node.addDOMWidget(name, 'vector', inputEl, {
getValue() {
return JSON.stringify(widget._value)
},
setValue(v) {
widget._value = v
},
})
const vec = new NumberInputWidget('vecEl', vector_size, true)
vec.setValue(...Object.values(value))
vec.onChange = (value) => {
for (let i = 0; i < value.length; i++) {
const val = value[i]
widget._value[VECTOR_AXIS[i]] = Number.parseFloat(val)
}
widget.callback?.(widget._value)
// widget._value[VECTOR_AXIS[index]] = Number.parseFloat(value)
}
console.log('prev callback', widget.callback)
widget.callback = callback
widget._value = value
// for (let i = 0; i < vector_size; i++) {
// const input = inputs[i]
// input.addEventListener('change', (event) => {
// widget._value[VECTOR_AXIS[i]] = Number.parseFloat(event.target.value)
// widget.callback?.(widget._value)
// node.graph._version++
// node.setDirtyCanvas(true, true)
// })
// }
widget.inputEl = inputEl
widget.vecEl = vecEl
widget.vec = vec
return { minWidth: 400, minHeight: 200 * vector_size, widget }
}
export const MtbWidgets = {
//TODO: complete this properly
/**
* Creates a vector widget.
* @param {string} key - The key for the widget.
* @param {number[]} [val] - The initial value for the widget.
* @param {number} size - The size of the vector.
* @returns {VectorWidget} The vector widget.
*/
VECTOR: (key, val, size) => {
shared.infoLogger('Adding VECTOR widget', { key, val, size })
/** @type {VectorWidget} */
const widget = {
name: key,
type: `vector${size}`,
y: 0,
options: { default: Array.from({ length: size }, () => 0.0) },
_value: val || Array.from({ length: size }, () => 0.0),
draw: (ctx, node, width, widgetY, height) => {
ctx.textAlign = 'left'
ctx.strokeStyle = outline_color
ctx.fillStyle = background_color
ctx.beginPath()
if (show_text)
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5])
else ctx.rect(margin, y, widget_width - margin * 2, H)
ctx.fill()
if (show_text) {
if (!w.disabled) ctx.stroke()
ctx.fillStyle = text_color
if (!w.disabled) {
ctx.beginPath()
ctx.moveTo(margin + 16, y + 5)
ctx.lineTo(margin + 6, y + H * 0.5)
ctx.lineTo(margin + 16, y + H - 5)
ctx.fill()
ctx.beginPath()
ctx.moveTo(widget_width - margin - 16, y + 5)
ctx.lineTo(widget_width - margin - 6, y + H * 0.5)
ctx.lineTo(widget_width - margin - 16, y + H - 5)
ctx.fill()
}
ctx.fillStyle = secondary_text_color
ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7)
ctx.fillStyle = text_color
ctx.textAlign = 'right'
if (w.type === 'number') {
ctx.fillText(
Number(w.value).toFixed(
w.options.precision !== undefined ? w.options.precision : 3,
),
widget_width - margin * 2 - 20,
y + H * 0.7,
)
} else {
let v = w.value
if (w.options.values) {
let values = w.options.values
if (values.constructor === Function) values = values()
if (values && values.constructor !== Array) v = values[w.value]
}
ctx.fillText(v, widget_width - margin * 2 - 20, y + H * 0.7)
}
}
},
get value() {
return this._value
},
set value(val) {
this._value = val
this.callback?.(this._value)
},
}
return widget
},
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
const margin = 15
const numWidgets = 4 // Number of stacked widgets
if (hide) return
for (let i = 0; i < numWidgets; i++) {
const 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
this.callback?.(this.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: 1em;
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 () => {
infoLogger('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:', e)
}
},
setup: () => {
app.ui.settings.addSetting({
id: 'mtb.Main.debug-enabled',
category: ['mtb', 'Main', 'debug-enabled'],
name: 'Enable Debug (py and js)',
type: 'boolean',
defaultValue: false,
tooltip:
'This will enable debug messages in the console and in the python console respectively, no need to restart the server, but do reload the webui',
attrs: {
style: {
// fontFamily: 'monospace',
},
},
async onChange(value) {
if (!window.MTB) {
window.MTB = {}
}
window.MTB.DEBUG = value
if (value) {
infoLogger('Enabled DEBUG mode')
}
await api
.fetchApi('/mtb/debug', {
method: 'POST',
body: JSON.stringify({
enabled: value,
}),
})
.then((_response) => {})
.catch((error) => {
console.error('Error:', error)
})
},
})
},
getCustomWidgets: () => {
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 {NodeType} nodeType
* @param {NodeData} nodeData
* @param {import("./types/comfy").App} app
*/
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// const rinputs = nodeData.input?.required
let has_custom = false
if (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 (...args) {
const r = onNodeCreated ? onNodeCreated.apply(this, args) : 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) {
const toInput = []
const 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
}
// console.log('MTB Node', { description: nodeData.description, nodeType })
shared.addDocumentation(nodeData, nodeType)
const deprecation = deprecated_nodes[nodeData.name.replace(' (mtb)', '')]
if (deprecation) {
shared.addDeprecation(nodeType, deprecation)
}
//- Extending Python Nodes
switch (nodeData.name) {
//TODO: remove this non sense
case 'Get Batch From History (mtb)':
case 'Get Batch From History V2 (mtb)': {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, []) : 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 (...args) {
const r = onNodeCreated ? onNodeCreated.apply(this, args) : undefined
this.changeMode(LiteGraph.ALWAYS)
const { raw_iteration, raw_loop, total_frames, loop_count } =
shared.getNamedWidget(
this,
'raw_iteration',
'raw_loop',
'total_frames',
'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'),
// )
const dom_value_preview = mtb_ui.makeElement('p', {
fontWeigth: '700',
textAlign: 'center',
fontSize: '1.5em',
margin: 0,
})
const value_preview = this.addDOMWidget(
'value_preview',
'DISPLAY',
dom_value_preview,
{
hideOnZoom: false,
setValue: (val) => {
if (val) {
value_preview.element.innerHTML = val
}
},
},
)
value_preview.value = 'Idle'
const dom_loop_preview = mtb_ui.makeElement('p', {
textAlign: 'center',
margin: 0,
})
const loop_preview = this.addDOMWidget(
'loop_preview',
'DISPLAY',
dom_loop_preview,
{
hideOnZoom: false,
setValue: (val) => {
if (val) {
dom_loop_preview.innerHTML = val
}
},
getValue: () => {
dom_loop_preview.innerHTML
},
},
)
loop_preview.value = 'Iteration: Idle'
const onReset = () => {
raw_iteration.value = 0
raw_loop.value = 0
value_preview.value = 'Idle'
loop_preview.value = 'Iteration: Idle'
app.canvas.setDirty(true)
}
// reset button
this.addWidget('button', 'Reset', 'reset', onReset)
// 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 'Interpolate Clip Sequential (mtb)': {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function (...args) {
const r = onNodeCreated
? onNodeCreated.apply(this, ...args)
: 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', (value, widget, node) => {
console.log('Button clicked', value, widget, node)
addReplacement()
})
//- remove
this.addWidget('button', '-', 'remove', (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?.[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')
const tn = LiteGraph.createNode('CLIPTextEncode')
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('CLIPTextEncode')
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
}
//NOTE: dynamic nodes
case 'Apply Text Template (mtb)': {
shared.setupDynamicConnections(nodeType, 'var', '*')
break
}
case 'Save Data Bundle (mtb)': {
shared.setupDynamicConnections(nodeType, 'data', '*') // [MASK,IMAGE]
break
}
case 'Add To Playlist (mtb)': {
shared.setupDynamicConnections(nodeType, 'video', 'VIDEO')
break
}
case 'Interpolate Condition (mtb)': {
shared.setupDynamicConnections(nodeType, 'condition', 'CONDITIONING')
break
}
case 'Psd Save (mtb)': {
shared.setupDynamicConnections(nodeType, 'input_', 'PSDLAYER')
break
}
// case 'Text Encode Frames (mtb)' : {
// shared.setupDynamicConnections(nodeType, 'input_', 'IMAGE')
// break
// }
case 'Stack Images (mtb)':
case 'Concat Images (mtb)': {
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
break
}
case 'Audio Sequence (mtb)':
case 'Audio Stack (mtb)': {
shared.setupDynamicConnections(nodeType, 'audio', 'AUDIO')
break
}
case 'Batch Float Assemble (mtb)':
case 'Batch Float Math (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_', '*', {
nameArray: ['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 'Batch Shape (mtb)':
case 'Mask To Image (mtb)':
case 'Text To Image (mtb)': {
shared.addMenuHandler(nodeType, function (_app, options) {
/** @type {ContextMenuItem} */
const item = {
content: 'swap colors',
title: 'Swap BG/FG Color ⚡',
callback: (_menuItem) => {
const color_w = this.widgets.find((w) => w.name === 'color')
const bg_w = this.widgets.find(
(w) => w.name === 'background' || w.name === 'bg_color',
)
const color = color_w.value
const bg = bg_w.value
color_w.value = bg
bg_w.value = color
},
}
options.push(item)
return [item]
})
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)