Add files via upload

This commit is contained in:
ZHO-ZHO-ZHO
2023-11-26 00:55:49 +08:00
committed by GitHub
parent ec54055dc5
commit 36be7a8abf
4 changed files with 1721 additions and 0 deletions
+342
View File
@@ -0,0 +1,342 @@
/**
* 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)
}
}
}
+109
View File
@@ -0,0 +1,109 @@
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
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?.()
}
}
}
}
},
})
+908
View File
@@ -0,0 +1,908 @@
/**
* 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)
+362
View File
@@ -0,0 +1,362 @@
// #region patterns
const float = '-?\\d*(?:\\.\\d+)';
export const number = `(${float}?)`;
export const percentage = `(${float}?%)`;
export const numberOrPercentage = `(${float}?%?)`;
const clamp = (num, min, max) => Math.min(Math.max(min, num), max);
const hexCharacters = 'a-f\\d';
const match3or4Hex = `#?[${hexCharacters}]{3}[${hexCharacters}]?`;
const match6or8Hex = `#?[${hexCharacters}]{6}([${hexCharacters}]{2})?`;
const nonHexChars = new RegExp(`[^#${hexCharacters}]`, 'gi');
const validHexSize = new RegExp(`^${match3or4Hex}$|^${match6or8Hex}$`, 'i');
export const hex_pattern = new RegExp(/^#([a-f0-9]{3,4}|[a-f0-9]{4}(?:[a-f0-9]{2}){1,2})\b$/, "i");
export const hsl3_pattern = new RegExp(`^
hsla?\\(
\\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s*,
\\s*${percentage}\\s*,
\\s*${percentage}\\s*
(?:,\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''))
export const hsl4_pattern = new RegExp(`^
hsla?\\(
\\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s*
\\s+${percentage}
\\s+${percentage}
\\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''))
export const rgb3_pattern = new RegExp(`^
rgba?\\(
\\s*${number}\\s*,
\\s*${number}\\s*,
\\s*${number}\\s*
(?:,\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''))
export const rgb4_pattern = new RegExp(`^
rgba?\\(
\\s*${number}
\\s+${number}
\\s+${number}
\\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''));
export const transparent_pattern = new RegExp(/^transparent$/, 'i');
// #endregion
// #region utils
/* 500 => 255, -10 => 0, 128 => 128 */
const parseRGB = (num) => {
let n = num;
if (typeof n !== 'number') {
n = n.endsWith('%') ? (parseFloat(n) * 255) / 100 : parseFloat(n);
}
return clamp(Math.round(n), 0, 255);
};
/* 200 => 100, -100 => 0, 50 => 50 */
const parsePercentage = (percentage) => clamp(parseFloat(percentage), 0, 100);
/* '50%' => 5.0, 200 => 1, -10 => 0 */
function parseAlpha(alpha) {
let a = alpha;
if (typeof a !== 'number') {
a = a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a);
}
return clamp(a, 0, 1);
}
export function getHEX(hex) {
const [r, g, b, a] = hex2Rgb(hex, { format: 'array' });
return getRGB([null, ...[r, g, b, a]]);
}
export function getHSL([, h, s, l, a = 1]) {
let hh = h;
if (hh.endsWith('turn')) {
hh = (parseFloat(hh) * 360) / 1;
} else if (hh.endsWith('rad')) {
hh = Math.round((parseFloat(hh) * 180) / Math.PI);
} else {
hh = parseFloat(hh);
}
return {
type: 'hsl',
values: [hh, parsePercentage(s), parsePercentage(l)],
alpha: parseAlpha(a === null ? 1 : a)
};
}
export function getRGB([, r, g, b, a = 1]) {
return {
type: 'rgb',
values: [r, g, b].map(parseRGB),
alpha: parseAlpha(a === null ? 1 : a)
};
}
export function hex2Rgb(hex, options = {}) {
if (typeof hex !== 'string' || nonHexChars.test(hex) || !validHexSize.test(hex)) {
throw new TypeError('Expected a valid hex string');
}
hex = hex.replace(/^#/, '');
let alphaFromHex = 1;
if (hex.length === 8) {
alphaFromHex = Number.parseInt(hex.slice(6, 8), 16) / 255;
hex = hex.slice(0, 6);
}
if (hex.length === 4) {
alphaFromHex = Number.parseInt(hex.slice(3, 4).repeat(2), 16) / 255;
hex = hex.slice(0, 3);
}
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const number = Number.parseInt(hex, 16);
const red = number >> 16;
const green = (number >> 8) & 255;
const blue = number & 255;
const alpha = typeof options.alpha === 'number' ? options.alpha : alphaFromHex;
if (options.format === 'array') {
return [red, green, blue, alpha];
}
if (options.format === 'css') {
const alphaString = alpha === 1 ? '' : ` / ${Number((alpha * 100).toFixed(2))}%`;
return `rgb(${red} ${green} ${blue}${alphaString})`;
}
return {red, green, blue, alpha};
}
// #endregion
// #region colorNames
export const colorName = {
aliceblue: [240, 248, 255],
antiquewhite: [250, 235, 215],
aqua: [0, 255, 255],
aquamarine: [127, 255, 212],
azure: [240, 255, 255],
beige: [245, 245, 220],
bisque: [255, 228, 196],
black: [0, 0, 0],
blanchedalmond: [255, 235, 205],
blue: [0, 0, 255],
blueviolet: [138, 43, 226],
brown: [165, 42, 42],
burlywood: [222, 184, 135],
cadetblue: [95, 158, 160],
chartreuse: [127, 255, 0],
chocolate: [210, 105, 30],
coral: [255, 127, 80],
cornflowerblue: [100, 149, 237],
cornsilk: [255, 248, 220],
crimson: [220, 20, 60],
cyan: [0, 255, 255],
darkblue: [0, 0, 139],
darkcyan: [0, 139, 139],
darkgoldenrod: [184, 134, 11],
darkgray: [169, 169, 169],
darkgreen: [0, 100, 0],
darkgrey: [169, 169, 169],
darkkhaki: [189, 183, 107],
darkmagenta: [139, 0, 139],
darkolivegreen: [85, 107, 47],
darkorange: [255, 140, 0],
darkorchid: [153, 50, 204],
darkred: [139, 0, 0],
darksalmon: [233, 150, 122],
darkseagreen: [143, 188, 143],
darkslateblue: [72, 61, 139],
darkslategray: [47, 79, 79],
darkslategrey: [47, 79, 79],
darkturquoise: [0, 206, 209],
darkviolet: [148, 0, 211],
deeppink: [255, 20, 147],
deepskyblue: [0, 191, 255],
dimgray: [105, 105, 105],
dimgrey: [105, 105, 105],
dodgerblue: [30, 144, 255],
firebrick: [178, 34, 34],
floralwhite: [255, 250, 240],
forestgreen: [34, 139, 34],
fuchsia: [255, 0, 255],
gainsboro: [220, 220, 220],
ghostwhite: [248, 248, 255],
gold: [255, 215, 0],
goldenrod: [218, 165, 32],
gray: [128, 128, 128],
green: [0, 128, 0],
greenyellow: [173, 255, 47],
grey: [128, 128, 128],
honeydew: [240, 255, 240],
hotpink: [255, 105, 180],
indianred: [205, 92, 92],
indigo: [75, 0, 130],
ivory: [255, 255, 240],
khaki: [240, 230, 140],
lavender: [230, 230, 250],
lavenderblush: [255, 240, 245],
lawngreen: [124, 252, 0],
lemonchiffon: [255, 250, 205],
lightblue: [173, 216, 230],
lightcoral: [240, 128, 128],
lightcyan: [224, 255, 255],
lightgoldenrodyellow: [250, 250, 210],
lightgray: [211, 211, 211],
lightgreen: [144, 238, 144],
lightgrey: [211, 211, 211],
lightpink: [255, 182, 193],
lightsalmon: [255, 160, 122],
lightseagreen: [32, 178, 170],
lightskyblue: [135, 206, 250],
lightslategray: [119, 136, 153],
lightslategrey: [119, 136, 153],
lightsteelblue: [176, 196, 222],
lightyellow: [255, 255, 224],
lime: [0, 255, 0],
limegreen: [50, 205, 50],
linen: [250, 240, 230],
magenta: [255, 0, 255],
maroon: [128, 0, 0],
mediumaquamarine: [102, 205, 170],
mediumblue: [0, 0, 205],
mediumorchid: [186, 85, 211],
mediumpurple: [147, 112, 219],
mediumseagreen: [60, 179, 113],
mediumslateblue: [123, 104, 238],
mediumspringgreen: [0, 250, 154],
mediumturquoise: [72, 209, 204],
mediumvioletred: [199, 21, 133],
midnightblue: [25, 25, 112],
mintcream: [245, 255, 250],
mistyrose: [255, 228, 225],
moccasin: [255, 228, 181],
navajowhite: [255, 222, 173],
navy: [0, 0, 128],
oldlace: [253, 245, 230],
olive: [128, 128, 0],
olivedrab: [107, 142, 35],
orange: [255, 165, 0],
orangered: [255, 69, 0],
orchid: [218, 112, 214],
palegoldenrod: [238, 232, 170],
palegreen: [152, 251, 152],
paleturquoise: [175, 238, 238],
palevioletred: [219, 112, 147],
papayawhip: [255, 239, 213],
peachpuff: [255, 218, 185],
peru: [205, 133, 63],
pink: [255, 192, 203],
plum: [221, 160, 221],
powderblue: [176, 224, 230],
purple: [128, 0, 128],
rebeccapurple: [102, 51, 153],
red: [255, 0, 0],
rosybrown: [188, 143, 143],
royalblue: [65, 105, 225],
saddlebrown: [139, 69, 19],
salmon: [250, 128, 114],
sandybrown: [244, 164, 96],
seagreen: [46, 139, 87],
seashell: [255, 245, 238],
sienna: [160, 82, 45],
silver: [192, 192, 192],
skyblue: [135, 206, 235],
slateblue: [106, 90, 205],
slategray: [112, 128, 144],
slategrey: [112, 128, 144],
snow: [255, 250, 250],
springgreen: [0, 255, 127],
steelblue: [70, 130, 180],
tan: [210, 180, 140],
teal: [0, 128, 128],
thistle: [216, 191, 216],
tomato: [255, 99, 71],
turquoise: [64, 224, 208],
violet: [238, 130, 238],
wheat: [245, 222, 179],
white: [255, 255, 255],
whitesmoke: [245, 245, 245],
yellow: [255, 255, 0],
yellowgreen: [154, 205, 50]
}
// #endregion
export const parseCSSColor = (str, debug=false) => {
if (typeof str !== 'string') {
console.error(`parseCSSColor: expected a string found ${typeof str}`,str);
return null;
}
const hex = hex_pattern.exec(str);
if (hex) {
if (debug){
console.debug('parseCSSColor: hex', hex);
}
return getHEX(hex[0]);
}
const hsl = hsl4_pattern.exec(str) || hsl3_pattern.exec(str);
if (hsl) {
if (debug){
console.debug('parseCSSColor: hsl', hsl);
}
return getHSL(hsl);
}
const rgb =
rgb4_pattern.exec(str) ||
rgb3_pattern.exec(str)
if (rgb) {
if (debug){
console.debug('parseCSSColor: rgb', rgb);
}
return getRGB(rgb);
}
if (transparent_pattern.exec(str)) {
if (debug){
console.debug('parseCSSColor: transparent');
}
return getRGB([null, 0, 0, 0, 0]);
}
const cn = colorName[str.toLowerCase()];
if (cn) {
if (debug){
console.debug('parseCSSColor: colorName', cn);
}
return getRGB([null, cn[0], cn[1], cn[2], 1]);
}
console.error('parseCSSColor: unknown color', str);
return null;
};
export default parseCSSColor;