509 lines
14 KiB
JavaScript
509 lines
14 KiB
JavaScript
/**
|
||
* File: comfy_shared.js
|
||
* Project: comfy_mtb
|
||
* Author: Mel Massadian
|
||
*
|
||
* Copyright (c) 2023 Mel Massadian
|
||
*
|
||
*/
|
||
|
||
import { app } from '../../scripts/app.js'
|
||
|
||
// - crude uuid
|
||
export function makeUUID() {
|
||
let dt = new Date().getTime()
|
||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||
const r = ((dt + Math.random() * 16) % 16) | 0
|
||
dt = Math.floor(dt / 16)
|
||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||
})
|
||
return uuid
|
||
}
|
||
|
||
//- local storage manager
|
||
export class LocalStorageManager {
|
||
constructor(namespace) {
|
||
this.namespace = namespace
|
||
}
|
||
|
||
_namespacedKey(key) {
|
||
return `${this.namespace}:${key}`
|
||
}
|
||
|
||
set(key, value) {
|
||
const serializedValue = JSON.stringify(value)
|
||
localStorage.setItem(this._namespacedKey(key), serializedValue)
|
||
}
|
||
|
||
get(key, default_val = null) {
|
||
const value = localStorage.getItem(this._namespacedKey(key))
|
||
return value ? JSON.parse(value) : default_val
|
||
}
|
||
|
||
remove(key) {
|
||
localStorage.removeItem(this._namespacedKey(key))
|
||
}
|
||
|
||
clear() {
|
||
for (const key of Object.keys(localStorage).filter((k) =>
|
||
k.startsWith(`${this.namespace}:`),
|
||
)) {
|
||
localStorage.removeItem(key)
|
||
}
|
||
}
|
||
}
|
||
|
||
// - log utilities
|
||
|
||
function createLogger(emoji, color, consoleMethod = 'log') {
|
||
return (message, ...args) => {
|
||
if (window.MTB?.DEBUG) {
|
||
console[consoleMethod](
|
||
`%c${emoji} ${message}`,
|
||
`color: ${color};`,
|
||
...args,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
export const infoLogger = createLogger('i', 'yellow')
|
||
export const warnLogger = createLogger('!', 'orange', 'warn')
|
||
export const errorLogger = createLogger('🔥', 'red', 'error')
|
||
export const successLogger = createLogger('✅', 'green')
|
||
|
||
export const log = (...args) => {
|
||
if (window.MTB?.DEBUG) {
|
||
console.debug(...args)
|
||
}
|
||
}
|
||
|
||
//- WIDGET UTILS
|
||
export const CONVERTED_TYPE = 'converted-widget'
|
||
|
||
export const hasWidgets = (node) => {
|
||
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
export const cleanupNode = (node) => {
|
||
if (!hasWidgets(node)) {
|
||
return
|
||
}
|
||
|
||
for (const w of node.widgets) {
|
||
if (w.canvas) {
|
||
w.canvas.remove()
|
||
}
|
||
if (w.inputEl) {
|
||
w.inputEl.remove()
|
||
}
|
||
// calls the widget remove callback
|
||
w.onRemoved?.()
|
||
}
|
||
}
|
||
|
||
export function offsetDOMWidget(
|
||
widget,
|
||
ctx,
|
||
node,
|
||
widgetWidth,
|
||
widgetY,
|
||
height,
|
||
) {
|
||
const margin = 10
|
||
const elRect = ctx.canvas.getBoundingClientRect()
|
||
const transform = new DOMMatrix()
|
||
.scaleSelf(
|
||
elRect.width / ctx.canvas.width,
|
||
elRect.height / ctx.canvas.height,
|
||
)
|
||
.multiplySelf(ctx.getTransform())
|
||
.translateSelf(margin, margin + widgetY)
|
||
|
||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||
Object.assign(widget.inputEl.style, {
|
||
transformOrigin: '0 0',
|
||
transform: scale,
|
||
left: `${transform.a + transform.e}px`,
|
||
top: `${transform.d + transform.f}px`,
|
||
width: `${widgetWidth - margin * 2}px`,
|
||
// height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`,
|
||
height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`,
|
||
|
||
position: 'absolute',
|
||
background: !node.color ? '' : node.color,
|
||
color: !node.color ? '' : 'white',
|
||
zIndex: 5, //app.graph._nodes.indexOf(node),
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Extracts the type and link type from a widget config object.
|
||
* @param {*} config
|
||
* @returns
|
||
*/
|
||
export function getWidgetType(config) {
|
||
// Special handling for COMBO so we restrict links based on the entries
|
||
let type = config?.[0]
|
||
let linkType = type
|
||
if (Array.isArray(type)) {
|
||
type = 'COMBO'
|
||
linkType = linkType.join(',')
|
||
}
|
||
return { type, linkType }
|
||
}
|
||
export const setupDynamicConnections = (nodeType, prefix, inputType) => {
|
||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||
// check if it's a list
|
||
const inputList = typeof inputType === 'object'
|
||
nodeType.prototype.onNodeCreated = function () {
|
||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined
|
||
this.addInput(`${prefix}_1`, inputList ? '*' : inputType)
|
||
return r
|
||
}
|
||
|
||
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||
nodeType.prototype.onConnectionsChange = function (
|
||
type,
|
||
index,
|
||
connected,
|
||
link_info,
|
||
) {
|
||
const r = onConnectionsChange
|
||
? onConnectionsChange.apply(this, arguments)
|
||
: undefined
|
||
dynamic_connection(this, index, connected, `${prefix}_`, inputList)
|
||
}
|
||
}
|
||
export const dynamic_connection = (
|
||
node,
|
||
index,
|
||
connected,
|
||
connectionPrefix = 'input_',
|
||
connectionType = 'PSDLAYER',
|
||
nameArray = [],
|
||
) => {
|
||
if (!node.inputs[index].name.startsWith(connectionPrefix)) {
|
||
return
|
||
}
|
||
const listConnection = typeof connectionType === 'object'
|
||
|
||
// remove all non connected inputs
|
||
if (!connected && node.inputs.length > 1) {
|
||
log(`Removing input ${index} (${node.inputs[index].name})`)
|
||
if (node.widgets) {
|
||
const w = node.widgets.find((w) => w.name === node.inputs[index].name)
|
||
if (w) {
|
||
w.onRemoved?.()
|
||
node.widgets.length = node.widgets.length - 1
|
||
}
|
||
}
|
||
node.removeInput(index)
|
||
|
||
// make inputs sequential again
|
||
for (let i = 0; i < node.inputs.length; i++) {
|
||
const name =
|
||
i < nameArray.length ? nameArray[i] : `${connectionPrefix}${i + 1}`
|
||
node.inputs[i].label = name
|
||
node.inputs[i].name = name
|
||
}
|
||
}
|
||
|
||
// add an extra input
|
||
if (node.inputs[node.inputs.length - 1].link != undefined) {
|
||
const nextIndex = node.inputs.length
|
||
const name =
|
||
nextIndex < nameArray.length
|
||
? nameArray[nextIndex]
|
||
: `${connectionPrefix}${nextIndex + 1}`
|
||
|
||
log(`Adding input ${nextIndex + 1} (${name})`)
|
||
|
||
node.addInput(name, listConnection ? '*' : connectionType)
|
||
}
|
||
}
|
||
|
||
export function calculateTotalChildrenHeight(parentElement) {
|
||
let totalHeight = 0
|
||
|
||
for (const child of parentElement.children) {
|
||
const style = window.getComputedStyle(child)
|
||
|
||
// Get height as an integer (without 'px')
|
||
const height = parseInt(style.height, 10)
|
||
|
||
// Get vertical margin as integers
|
||
const marginTop = parseInt(style.marginTop, 10)
|
||
const marginBottom = parseInt(style.marginBottom, 10)
|
||
|
||
// Sum up height and vertical margins
|
||
totalHeight += height + marginTop + marginBottom
|
||
}
|
||
|
||
return totalHeight
|
||
}
|
||
/**
|
||
* Appends a callback to the extra menu options of a given node type.
|
||
* @param {*} nodeType
|
||
* @param {*} cb
|
||
*/
|
||
export function addMenuHandler(nodeType, cb) {
|
||
const getOpts = nodeType.prototype.getExtraMenuOptions
|
||
nodeType.prototype.getExtraMenuOptions = function () {
|
||
const r = getOpts.apply(this, arguments)
|
||
cb.apply(this, arguments)
|
||
return r
|
||
}
|
||
}
|
||
|
||
export function hideWidget(node, widget, suffix = '') {
|
||
widget.origType = widget.type
|
||
widget.hidden = true
|
||
widget.origComputeSize = widget.computeSize
|
||
widget.origSerializeValue = widget.serializeValue
|
||
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
||
widget.type = CONVERTED_TYPE + suffix
|
||
widget.serializeValue = () => {
|
||
// Prevent serializing the widget if we have no input linked
|
||
const { link } = node.inputs.find((i) => i.widget?.name === widget.name)
|
||
if (link == null) {
|
||
return undefined
|
||
}
|
||
return widget.origSerializeValue
|
||
? widget.origSerializeValue()
|
||
: widget.value
|
||
}
|
||
|
||
// Hide any linked widgets, e.g. seed+seedControl
|
||
if (widget.linkedWidgets) {
|
||
for (const w of widget.linkedWidgets) {
|
||
hideWidget(node, w, ':' + widget.name)
|
||
}
|
||
}
|
||
}
|
||
|
||
export function showWidget(widget) {
|
||
widget.type = widget.origType
|
||
widget.computeSize = widget.origComputeSize
|
||
widget.serializeValue = widget.origSerializeValue
|
||
|
||
delete widget.origType
|
||
delete widget.origComputeSize
|
||
delete widget.origSerializeValue
|
||
|
||
// Hide any linked widgets, e.g. seed+seedControl
|
||
if (widget.linkedWidgets) {
|
||
for (const w of widget.linkedWidgets) {
|
||
showWidget(w)
|
||
}
|
||
}
|
||
}
|
||
|
||
export function convertToWidget(node, widget) {
|
||
showWidget(widget)
|
||
const sz = node.size
|
||
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name))
|
||
|
||
for (const widget of node.widgets) {
|
||
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT
|
||
}
|
||
|
||
// Restore original size but grow if needed
|
||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
|
||
}
|
||
|
||
export function convertToInput(node, widget, config) {
|
||
hideWidget(node, widget)
|
||
|
||
const { linkType } = getWidgetType(config)
|
||
|
||
// Add input and store widget config for creating on primitive node
|
||
const sz = node.size
|
||
node.addInput(widget.name, linkType, {
|
||
widget: { name: widget.name, config },
|
||
})
|
||
|
||
for (const widget of node.widgets) {
|
||
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT
|
||
}
|
||
|
||
// Restore original size but grow if needed
|
||
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
|
||
}
|
||
|
||
export function hideWidgetForGood(node, widget, suffix = '') {
|
||
widget.origType = widget.type
|
||
widget.origComputeSize = widget.computeSize
|
||
widget.origSerializeValue = widget.serializeValue
|
||
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
||
widget.type = CONVERTED_TYPE + suffix
|
||
// widget.serializeValue = () => {
|
||
// // Prevent serializing the widget if we have no input linked
|
||
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
|
||
// if (w?.link == null) {
|
||
// return undefined;
|
||
// }
|
||
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
||
// };
|
||
|
||
// Hide any linked widgets, e.g. seed+seedControl
|
||
if (widget.linkedWidgets) {
|
||
for (const w of widget.linkedWidgets) {
|
||
hideWidgetForGood(node, w, ':' + widget.name)
|
||
}
|
||
}
|
||
}
|
||
|
||
export function fixWidgets(node) {
|
||
if (node.inputs) {
|
||
for (const input of node.inputs) {
|
||
log(input)
|
||
if (input.widget || node.widgets) {
|
||
// if (newTypes.includes(input.type)) {
|
||
const matching_widget = node.widgets.find((w) => w.name === input.name)
|
||
if (matching_widget) {
|
||
// if (matching_widget.hidden) {
|
||
// log(`Already hidden skipping ${matching_widget.name}`)
|
||
// continue
|
||
// }
|
||
const w = node.widgets.find((w) => w.name === matching_widget.name)
|
||
if (w && w.type != CONVERTED_TYPE) {
|
||
log(w)
|
||
log(`hidding ${w.name}(${w.type}) from ${node.type}`)
|
||
log(node)
|
||
hideWidget(node, w)
|
||
} else {
|
||
log(`converting to widget ${w}`)
|
||
|
||
convertToWidget(node, input)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
export function inner_value_change(widget, value, event = undefined) {
|
||
if (widget.type == 'number' || widget.type == 'BBOX') {
|
||
value = Number(value)
|
||
} else if (widget.type == 'BOOL') {
|
||
value = Boolean(value)
|
||
}
|
||
widget.value = value
|
||
if (
|
||
widget.options &&
|
||
widget.options.property &&
|
||
node.properties[widget.options.property] !== undefined
|
||
) {
|
||
node.setProperty(widget.options.property, value)
|
||
}
|
||
if (widget.callback) {
|
||
widget.callback(widget.value, app.canvas, node, pos, event)
|
||
}
|
||
}
|
||
|
||
//- COLOR UTILS
|
||
export function isColorBright(rgb, threshold = 240) {
|
||
const brightess = getBrightness(rgb)
|
||
return brightess > threshold
|
||
}
|
||
|
||
function getBrightness(rgbObj) {
|
||
return Math.round(
|
||
(parseInt(rgbObj[0]) * 299 +
|
||
parseInt(rgbObj[1]) * 587 +
|
||
parseInt(rgbObj[2]) * 114) /
|
||
1000,
|
||
)
|
||
}
|
||
|
||
//- HTML / CSS UTILS
|
||
export const loadScript = (
|
||
FILE_URL,
|
||
async = true,
|
||
type = 'text/javascript',
|
||
) => {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// Check if the script already exists
|
||
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||
if (existingScript) {
|
||
resolve({ status: true, message: 'Script already loaded' })
|
||
return
|
||
}
|
||
|
||
const scriptEle = document.createElement('script')
|
||
scriptEle.type = type
|
||
scriptEle.async = async
|
||
scriptEle.src = FILE_URL
|
||
|
||
scriptEle.addEventListener('load', (ev) => {
|
||
resolve({ status: true })
|
||
})
|
||
|
||
scriptEle.addEventListener('error', (ev) => {
|
||
reject({
|
||
status: false,
|
||
message: `Failed to load the script ${FILE_URL}`,
|
||
})
|
||
})
|
||
|
||
document.body.appendChild(scriptEle)
|
||
} catch (error) {
|
||
reject(error)
|
||
}
|
||
})
|
||
}
|
||
|
||
export function defineClass(className, classStyles) {
|
||
const styleSheets = document.styleSheets
|
||
|
||
// Helper function to check if the class exists in a style sheet
|
||
function classExistsInStyleSheet(styleSheet) {
|
||
const rules = styleSheet.rules || styleSheet.cssRules
|
||
for (const rule of rules) {
|
||
if (rule.selectorText === `.${className}`) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Check if the class is already defined in any of the style sheets
|
||
let classExists = false
|
||
for (const styleSheet of styleSheets) {
|
||
if (classExistsInStyleSheet(styleSheet)) {
|
||
classExists = true
|
||
break
|
||
}
|
||
}
|
||
|
||
// If the class doesn't exist, add the new class definition to the first style sheet
|
||
if (!classExists) {
|
||
if (styleSheets[0].insertRule) {
|
||
styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0)
|
||
} else if (styleSheets[0].addRule) {
|
||
styleSheets[0].addRule(`.${className}`, classStyles, 0)
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Prefixes the node title with '[DEPRECATED]' and log the deprecation reason to the console.*/
|
||
export const addDeprecation = (nodeType, reason) => {
|
||
const title = nodeType.title
|
||
nodeType.title = '[DEPRECATED] ' + title
|
||
// console.log(nodeType)
|
||
|
||
const styles = {
|
||
title: 'font-size:1.3em;font-weight:900;color:yellow; background: black',
|
||
reason: 'font-size:1.2em',
|
||
}
|
||
console.log(
|
||
`%c! ${title} is deprecated:%c ${reason}`,
|
||
styles.title,
|
||
styles.reason,
|
||
)
|
||
}
|