Spaces:
Sleeping
Sleeping
/** | |
* 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) | |