| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| import { app } from '../../scripts/app.js' |
| import { api } from '../../scripts/api.js' |
|
|
| |
|
|
| |
| 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 |
| } |
|
|
| |
| 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) |
| } |
| } |
| } |
|
|
| |
|
|
| 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('ℹ️', '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) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function deepMerge(target, ...sources) { |
| if (!sources.length) return target |
| const source = sources.shift() |
|
|
| for (const key in source) { |
| if (source[key] instanceof Object) { |
| if (!target[key]) Object.assign(target, { [key]: {} }) |
| deepMerge(target[key], source[key]) |
| } else { |
| Object.assign(target, { [key]: source[key] }) |
| } |
| } |
|
|
| return deepMerge(target, ...sources) |
| } |
|
|
| |
|
|
| |
| export const CONVERTED_TYPE = 'converted-widget' |
|
|
| 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] |
| widget.type = CONVERTED_TYPE + suffix |
| widget.serializeValue = () => { |
| |
| const { link } = node.inputs.find((i) => i.widget?.name === widget.name) |
| if (link == null) { |
| return undefined |
| } |
| return widget.origSerializeValue |
| ? widget.origSerializeValue() |
| : widget.value |
| } |
|
|
| |
| 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 |
|
|
| |
| 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 |
| } |
|
|
| |
| 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) |
|
|
| |
| 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 |
| } |
|
|
| |
| 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] |
| widget.type = CONVERTED_TYPE + suffix |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| 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) { |
| |
| const matching_widget = node.widgets.find((w) => w.name === input.name) |
| if (matching_widget) { |
| |
| |
| |
| |
| 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, val, event = undefined) { |
| let value = val |
| if (widget.type === 'number' || widget.type === 'BBOX') { |
| value = Number(value) |
| } else if (widget.type === 'BOOL') { |
| value = Boolean(value) |
| } |
| widget.value = corrected_value |
| if ( |
| 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) |
| } |
| } |
|
|
| export const getNamedWidget = (node, ...names) => { |
| const out = {} |
|
|
| for (const name of names) { |
| out[name] = node.widgets.find((w) => w.name === name) |
| } |
|
|
| return out |
| } |
|
|
| |
| |
| |
| |
| |
| export const nodesFromLink = (node, link) => { |
| const fromNode = app.graph.getNodeById(link.origin_id) |
| const toNode = app.graph.getNodeById(link.target_id) |
|
|
| let tp = 'error' |
|
|
| if (fromNode.id === node.id) { |
| tp = 'outgoing' |
| } else if (toNode.id === node.id) { |
| tp = 'incoming' |
| } |
|
|
| return { to: toNode, from: fromNode, type: tp } |
| } |
|
|
| 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() |
| } |
| |
| 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: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`, |
|
|
| position: 'absolute', |
| background: !node.color ? '' : node.color, |
| color: !node.color ? '' : 'white', |
| zIndex: 5, |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| export function getWidgetType(config) { |
| |
| 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, opts) => { |
| infoLogger( |
| 'Setting up dynamic connections for', |
| Object.getOwnPropertyDescriptors(nodeType).title.value, |
| ) |
|
|
| |
| const options = opts || {} |
| const onNodeCreated = nodeType.prototype.onNodeCreated |
| const inputList = typeof inputType === 'object' |
|
|
| nodeType.prototype.onNodeCreated = function () { |
| const r = onNodeCreated ? onNodeCreated.apply(this, []) : undefined |
| this.addInput(`${prefix}_1`, inputList ? '*' : inputType) |
| return r |
| } |
|
|
| const onConnectionsChange = nodeType.prototype.onConnectionsChange |
| |
| |
| |
| nodeType.prototype.onConnectionsChange = function (...args) { |
| const [type, slotIndex, isConnected, link, ioSlot] = args |
|
|
| options.link = link |
| options.ioSlot = ioSlot |
| const r = onConnectionsChange |
| ? onConnectionsChange.apply(this, [ |
| type, |
| slotIndex, |
| isConnected, |
| link, |
| ioSlot, |
| ]) |
| : undefined |
| options.DEBUG = { |
| node: this, |
| type, |
| slotIndex, |
| isConnected, |
| link, |
| ioSlot, |
| } |
|
|
| dynamic_connection( |
| this, |
| slotIndex, |
| isConnected, |
| `${prefix}_`, |
| inputType, |
| options, |
| ) |
| return r |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const dynamic_connection = ( |
| node, |
| index, |
| connected, |
| connectionPrefix = 'input_', |
| connectionType = '*', |
| opts = undefined, |
| ) => { |
| |
| const options = opts || {} |
|
|
| if ( |
| node.inputs.length > 0 && |
| !node.inputs[index].name.startsWith(connectionPrefix) |
| ) { |
| return |
| } |
|
|
| const listConnection = typeof connectionType === 'object' |
|
|
| const conType = listConnection ? '*' : connectionType |
| const nameArray = options.nameArray || [] |
|
|
| const clean_inputs = () => { |
| if (node.inputs.length === 0) return |
|
|
| let w_count = node.widgets?.length || 0 |
| let i_count = node.inputs?.length || 0 |
| infoLogger(`Cleaning inputs: [BEFORE] (w: ${w_count} | inputs: ${i_count})`) |
|
|
| const to_remove = [] |
| for (let n = 1; n < node.inputs.length; n++) { |
| const element = node.inputs[n] |
| if (!element.link) { |
| if (node.widgets) { |
| const w = node.widgets.find((w) => w.name === element.name) |
| if (w) { |
| w.onRemoved?.() |
| node.widgets.length = node.widgets.length - 1 |
| } |
| } |
| infoLogger(`Removing input ${n}`) |
| to_remove.push(n) |
| } |
| } |
| for (let i = 0; i < to_remove.length; i++) { |
| const id = to_remove[i] |
|
|
| node.removeInput(id) |
| i_count -= 1 |
| } |
| node.inputs.length = i_count |
|
|
| w_count = node.widgets?.length || 0 |
| i_count = node.inputs?.length || 0 |
| infoLogger(`Cleaning inputs: [AFTER] (w: ${w_count} | inputs: ${i_count})`) |
|
|
| infoLogger('Cleaning inputs: making it sequential again') |
| |
| for (let i = 0; i < node.inputs.length; i++) { |
| let name = `${connectionPrefix}${i + 1}` |
|
|
| if (nameArray.length > 0) { |
| name = i < nameArray.length ? nameArray[i] : name |
| } |
|
|
| node.inputs[i].label = name |
| node.inputs[i].name = name |
| } |
| } |
| if (!connected) { |
| if (!options.link) { |
| infoLogger('Disconnecting', { options }) |
|
|
| clean_inputs() |
| } else { |
| if (!options.ioSlot.link) { |
| node.connectionTransit = true |
| } else { |
| node.connectionTransit = false |
| clean_inputs() |
| } |
| infoLogger('Reconnecting', { options }) |
| } |
| } |
|
|
| if (connected) { |
| if (options.link) { |
| const { from, to, type } = nodesFromLink(node, options.link) |
| if (type === 'outgoing') return |
| infoLogger('Connecting', { options, from, to, type }) |
| } else { |
| infoLogger('Connecting', { options }) |
| } |
|
|
| if (node.connectionTransit) { |
| infoLogger('In Transit') |
| node.connectionTransit = false |
| } |
|
|
| |
| clean_inputs() |
|
|
| if (node.inputs.length === 0) return |
| |
| if (node.inputs[node.inputs.length - 1].link !== null) { |
| const nextIndex = node.inputs.length |
| const name = |
| nextIndex < nameArray.length |
| ? nameArray[nextIndex] |
| : `${connectionPrefix}${nextIndex + 1}` |
|
|
| infoLogger(`Adding input ${nextIndex + 1} (${name})`) |
| node.addInput(name, conType) |
| } |
| } |
| } |
| |
|
|
| |
| export function isColorBright(rgb, threshold = 240) { |
| const brightess = getBrightness(rgb) |
| return brightess > threshold |
| } |
|
|
| function getBrightness(rgbObj) { |
| return Math.round( |
| (Number.parseInt(rgbObj[0]) * 299 + |
| Number.parseInt(rgbObj[1]) * 587 + |
| Number.parseInt(rgbObj[2]) * 114) / |
| 1000, |
| ) |
| } |
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| export function calculateTotalChildrenHeight(parentElement) { |
| let totalHeight = 0 |
|
|
| for (const child of parentElement.children) { |
| const style = window.getComputedStyle(child) |
|
|
| |
| const height = Number.parseInt(style.height, 10) |
|
|
| |
| const marginTop = Number.parseInt(style.marginTop, 10) |
| const marginBottom = Number.parseInt(style.marginBottom, 10) |
|
|
| |
| totalHeight += height + marginTop + marginBottom |
| } |
|
|
| return totalHeight |
| } |
|
|
| export const loadScript = ( |
| FILE_URL, |
| async = true, |
| type = 'text/javascript', |
| ) => { |
| return new Promise((resolve, reject) => { |
| try { |
| |
| 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) |
| } |
| }) |
| } |
|
|
| |
|
|
| |
|
|
| const create_documentation_stylesheet = () => { |
| const tag = 'mtb-documentation-stylesheet' |
|
|
| let styleTag = document.head.querySelector(tag) |
|
|
| if (!styleTag) { |
| styleTag = document.createElement('style') |
| styleTag.type = 'text/css' |
| styleTag.id = tag |
|
|
| styleTag.innerHTML = ` |
| .documentation-popup { |
| background: var(--comfy-menu-bg); |
| position: absolute; |
| color: var(--fg-color); |
| font: 12px monospace; |
| line-height: 1.5em; |
| padding: 10px; |
| border-radius: 6px; |
| pointer-events: "inherit"; |
| z-index: 5; |
| overflow: hidden; |
| } |
| .documentation-wrapper { |
| padding: 0 2em; |
| overflow: auto; |
| max-height: 100%; |
| /* Scrollbar styling for Chrome */ |
| &::-webkit-scrollbar { |
| width: 6px; |
| } |
| &::-webkit-scrollbar-track { |
| background: var(--bg-color); |
| } |
| &::-webkit-scrollbar-thumb { |
| background-color: var(--fg-color); |
| border-radius: 6px; |
| border: 3px solid var(--bg-color); |
| } |
| |
| /* Scrollbar styling for Firefox */ |
| scrollbar-width: thin; |
| scrollbar-color: var(--fg-color) var(--bg-color); |
| a { |
| color: yellow; |
| } |
| a:visited { |
| color: orange; |
| } |
| a:hover { |
| color: red; |
| } |
| } |
| |
| .documentation-popup img { |
| max-width: 100%; |
| } |
| .documentation-popup table { |
| border-collapse: collapse; |
| border: 1px var(--border-color) solid; |
| } |
| .documentation-popup th, |
| .documentation-popup td { |
| border: 1px var(--border-color) solid; |
| } |
| .documentation-popup th { |
| background-color: var(--comfy-input-bg); |
| }` |
| document.head.appendChild(styleTag) |
| } |
| } |
| let parserPromise |
| const callbackQueue = [] |
|
|
| function runQueuedCallbacks() { |
| while (callbackQueue.length) { |
| const cb = callbackQueue.shift() |
| cb(window.MTB.mdParser) |
| } |
| } |
|
|
| function loadParser(shiki) { |
| if (!parserPromise) { |
| parserPromise = import( |
| shiki |
| ? '/mtb_async/mtb_markdown_plus.umd.js' |
| : '/mtb_async/mtb_markdown.umd.js' |
| ) |
| .then((_module) => |
| shiki ? MTBMarkdownPlus.getParser() : MTBMarkdown.getParser(), |
| ) |
| .then((instance) => { |
| window.MTB.mdParser = instance |
| runQueuedCallbacks() |
| return instance |
| }) |
| .catch((error) => { |
| console.error('Error loading the parser:', error) |
| }) |
| } |
| return parserPromise |
| } |
|
|
| export const ensureMarkdownParser = async (callback) => { |
| infoLogger('Ensuring md parser') |
| let use_shiki = false |
| try { |
| use_shiki = await api.getSetting('mtb.Use Shiki') |
| } catch (e) { |
| console.warn('Option not available yet', e) |
| } |
|
|
| if (window.MTB?.mdParser) { |
| infoLogger('Markdown parser found') |
| callback?.(window.MTB.mdParser) |
| return window.MTB.mdParser |
| } |
|
|
| if (!parserPromise) { |
| infoLogger('Running promise to fetch parser') |
|
|
| try { |
| loadParser(use_shiki) |
| |
| |
| } catch (error) { |
| console.error('Error loading the parser:', error) |
| } |
| } else { |
| infoLogger('A similar promise is already running, waiting for it to finish') |
| } |
| if (callback) { |
| callbackQueue.push(callback) |
| } |
|
|
| await parserPromise |
| await parserPromise |
|
|
| return window.MTB.mdParser |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const addDocumentation = ( |
| nodeData, |
| nodeType, |
| opts = { icon_size: 14, icon_margin: 4 }, |
| ) => { |
| if (!nodeData.description) { |
| infoLogger( |
| `Skipping ${nodeData.name} doesn't have a description, skipping...`, |
| ) |
| return |
| } |
|
|
| const options = opts || {} |
| const iconSize = options.icon_size || 14 |
| const iconMargin = options.icon_margin || 4 |
|
|
| let docElement = null |
| let wrapper = null |
|
|
| const onRem = nodeType.prototype.onRemoved |
|
|
| nodeType.prototype.onRemoved = function () { |
| const r = onRem ? onRem.apply(this, []) : undefined |
|
|
| if (docElement) { |
| docElement.remove() |
| docElement = null |
| } |
|
|
| if (wrapper) { |
| wrapper.remove() |
| wrapper = null |
| } |
| return r |
| } |
|
|
| const drawFg = nodeType.prototype.onDrawForeground |
|
|
| |
| |
| |
| nodeType.prototype.onDrawForeground = function (...args) { |
| const [ctx, _canvas] = args |
| const r = drawFg ? drawFg.apply(this, args) : undefined |
|
|
| if (this.flags.collapsed) return r |
|
|
| |
| const x = this.size[0] - iconSize - iconMargin |
|
|
| let resizeHandle |
| |
| if (this.show_doc && docElement === null) { |
| create_documentation_stylesheet() |
|
|
| docElement = document.createElement('div') |
| docElement.classList.add('documentation-popup') |
| document.body.appendChild(docElement) |
|
|
| wrapper = document.createElement('div') |
| wrapper.classList.add('documentation-wrapper') |
| docElement.appendChild(wrapper) |
|
|
| |
|
|
| ensureMarkdownParser().then(() => { |
| MTB.mdParser.parse(nodeData.description).then((e) => { |
| wrapper.innerHTML = e |
| |
| resizeHandle = document.createElement('div') |
| resizeHandle.classList.add('doc-resize-handle') |
| resizeHandle.style.width = '0' |
| resizeHandle.style.height = '0' |
| resizeHandle.style.position = 'absolute' |
| resizeHandle.style.bottom = '0' |
| resizeHandle.style.right = '0' |
|
|
| resizeHandle.style.cursor = 'se-resize' |
| resizeHandle.style.userSelect = 'none' |
|
|
| resizeHandle.style.borderWidth = '15px' |
| resizeHandle.style.borderStyle = 'solid' |
|
|
| resizeHandle.style.borderColor = |
| 'transparent var(--border-color) var(--border-color) transparent' |
|
|
| wrapper.appendChild(resizeHandle) |
| let isResizing = false |
|
|
| let startX |
| let startY |
| let startWidth |
| let startHeight |
|
|
| resizeHandle.addEventListener( |
| 'mousedown', |
| (e) => { |
| e.stopPropagation() |
| isResizing = true |
| startX = e.clientX |
| startY = e.clientY |
| startWidth = Number.parseInt( |
| document.defaultView.getComputedStyle(docElement).width, |
| 10, |
| ) |
| startHeight = Number.parseInt( |
| document.defaultView.getComputedStyle(docElement).height, |
| 10, |
| ) |
| }, |
|
|
| { signal: this.docCtrl.signal }, |
| ) |
|
|
| document.addEventListener( |
| 'mousemove', |
| (e) => { |
| if (!isResizing) return |
| const scale = app.canvas.ds.scale |
| const newWidth = startWidth + (e.clientX - startX) / scale |
| const newHeight = startHeight + (e.clientY - startY) / scale |
|
|
| docElement.style.width = `${newWidth}px` |
| docElement.style.height = `${newHeight}px` |
|
|
| this.docPos = { |
| width: `${newWidth}px`, |
| height: `${newHeight}px`, |
| } |
| }, |
| { signal: this.docCtrl.signal }, |
| ) |
|
|
| document.addEventListener( |
| 'mouseup', |
| () => { |
| isResizing = false |
| }, |
| { signal: this.docCtrl.signal }, |
| ) |
| }) |
| }) |
| } else if (!this.show_doc && docElement !== null) { |
| docElement.remove() |
| docElement = null |
| } |
|
|
| |
| if (this.show_doc && docElement !== null) { |
| const rect = ctx.canvas.getBoundingClientRect() |
|
|
| const dpi = Math.max(1.0, window.devicePixelRatio) |
| const scaleX = rect.width / ctx.canvas.width |
| const scaleY = rect.height / ctx.canvas.height |
| const transform = new DOMMatrix() |
| .scaleSelf(scaleX, scaleY) |
| .multiplySelf(ctx.getTransform()) |
| .translateSelf(this.size[0] * scaleX * dpi, 0) |
| .translateSelf(10, -32) |
|
|
| const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) |
|
|
| Object.assign(docElement.style, { |
| transformOrigin: '0 0', |
| transform: scale, |
| left: `${transform.a + rect.x + transform.e}px`, |
| top: `${transform.d + rect.y + transform.f}px`, |
| width: this.docPos ? this.docPos.width : `${this.size[0] * 1.5}px`, |
| height: this.docPos?.height, |
| }) |
|
|
| if (this.docPos === undefined) { |
| this.docPos = { |
| width: docElement.style.width, |
| height: docElement.style.height, |
| } |
| } |
| } |
|
|
| ctx.save() |
| ctx.translate(x, iconSize - 34) |
| ctx.scale(iconSize / 32, iconSize / 32) |
| ctx.strokeStyle = 'rgba(255,255,255,0.3)' |
|
|
| ctx.lineCap = 'round' |
| ctx.lineJoin = 'round' |
|
|
| ctx.lineWidth = 2.4 |
| ctx.font = 'bold 36px monospace' |
| ctx.fillText('?', 0, 24) |
|
|
| |
| |
| ctx.restore() |
|
|
| return r |
| } |
| const mouseDown = nodeType.prototype.onMouseDown |
|
|
| |
| |
| |
| nodeType.prototype.onMouseDown = function (...args) { |
| const [_event, localPos, _graphCanvas] = args |
| const r = mouseDown ? mouseDown.apply(this, args) : undefined |
| const iconX = this.size[0] - iconSize - iconMargin |
| const iconY = iconSize - 34 |
| if ( |
| localPos[0] > iconX && |
| localPos[0] < iconX + iconSize && |
| localPos[1] > iconY && |
| localPos[1] < iconY + iconSize |
| ) { |
| |
| |
| if (this.show_doc === undefined) { |
| this.show_doc = true |
| } else { |
| this.show_doc = !this.show_doc |
| } |
| if (this.show_doc) { |
| this.docCtrl = new AbortController() |
| } else { |
| this.docCtrl.abort() |
| } |
| return true |
| } |
|
|
| return r |
|
|
| |
| } |
| } |
|
|
| |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| export function extendPrototype(object, property, callback) { |
| if (object === undefined) { |
| console.error('Could not extend undefined object', { object, property }) |
| return |
| } |
| if (property in object) { |
| const callback_orig = object[property] |
| object[property] = function (...args) { |
| const r = callback_orig.apply(this, args) |
| callback.apply(this, args) |
| return r |
| } |
| } else { |
| object[property] = callback |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function addMenuHandler(nodeType, cb) { |
| const getOpts = nodeType.prototype.getExtraMenuOptions |
| |
| |
| |
| nodeType.prototype.getExtraMenuOptions = function (app, options) { |
| const r = getOpts.apply(this, [app, options]) || [] |
| const newItems = cb.apply(this, [app, options]) || [] |
| return [...r, ...newItems] |
| } |
| } |
|
|
| |
| export const addDeprecation = (nodeType, reason) => { |
| const title = nodeType.title |
| nodeType.title = `[DEPRECATED] ${title}` |
| |
|
|
| 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, |
| ) |
| } |
|
|
| |
|
|
| |
| export const getAPIInputs = () => { |
| const inputs = {} |
| let counter = 1 |
| for (const node of getNodes(true)) { |
| const widgets = node.widgets |
|
|
| if (node.properties.mtb_api && node.properties.useAPI) { |
| if (node.properties.mtb_api.inputs) { |
| for (const currentName in node.properties.mtb_api.inputs) { |
| const current = node.properties.mtb_api.inputs[currentName] |
| if (current.enabled) { |
| const inputName = current.name || currentName |
| const widget = widgets.find((w) => w.name === currentName) |
| if (!widget) continue |
| if (!(inputName in inputs)) { |
| inputs[inputName] = { |
| ...current, |
| id: counter, |
| name: inputName, |
| type: current.type, |
| node_id: node.id, |
| widgets: [], |
| } |
| } |
| inputs[inputName].widgets.push(widget) |
| counter = counter + 1 |
| } |
| } |
| } |
| } |
| } |
| return inputs |
| } |
|
|
| export const getNodes = (skip_unused) => { |
| const nodes = [] |
| for (const outerNode of app.graph.computeExecutionOrder(false)) { |
| const skipNode = |
| (outerNode.mode === 2 || outerNode.mode === 4) && skip_unused |
| const innerNodes = |
| !skipNode && outerNode.getInnerNodes |
| ? outerNode.getInnerNodes() |
| : [outerNode] |
| for (const node of innerNodes) { |
| if ((node.mode === 2 || node.mode === 4) && skip_unused) { |
| continue |
| } |
| nodes.push(node) |
| } |
| } |
| return nodes |
| } |
|
|