diff --git a/web/comfyui/DomWidget.vue b/web/comfyui/DomWidget.vue deleted file mode 100644 index 245030fd..00000000 --- a/web/comfyui/DomWidget.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - - - diff --git a/web/comfyui/domWidget.ts b/web/comfyui/domWidget.ts deleted file mode 100644 index e2429e06..00000000 --- a/web/comfyui/domWidget.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { LGraphNode, LiteGraph } from '@comfyorg/litegraph' -import type { - ICustomWidget, - IWidget, - IWidgetOptions -} from '@comfyorg/litegraph/dist/types/widgets' -import _ from 'lodash' -import { type Component, toRaw } from 'vue' - -import { useChainCallback } from '@/composables/functional/useChainCallback' -import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' -import { useDomWidgetStore } from '@/stores/domWidgetStore' -import { generateUUID } from '@/utils/formatUtil' - -export interface BaseDOMWidget - extends ICustomWidget { - // ICustomWidget properties - type: 'custom' - options: DOMWidgetOptions - value: V - callback?: (value: V) => void - - // BaseDOMWidget properties - /** The unique ID of the widget. */ - readonly id: string - /** The node that the widget belongs to. */ - readonly node: LGraphNode - /** Whether the widget is visible. */ - isVisible(): boolean - /** The margin of the widget. */ - margin: number -} - -/** - * A DOM widget that wraps a custom HTML element as a litegraph widget. - */ -export interface DOMWidget - extends BaseDOMWidget { - element: T - /** - * @deprecated Legacy property used by some extensions for customtext - * (textarea) widgets. Use {@link element} instead as it provides the same - * functionality and works for all DOMWidget types. - */ - inputEl?: T -} - -/** - * A DOM widget that wraps a Vue component as a litegraph widget. - */ -export interface ComponentWidget - extends BaseDOMWidget { - readonly component: Component - readonly inputSpec: InputSpec -} - -export interface DOMWidgetOptions - extends IWidgetOptions { - /** - * Whether to render a placeholder rectangle when zoomed out. - */ - hideOnZoom?: boolean - selectOn?: string[] - onHide?: (widget: BaseDOMWidget) => void - getValue?: () => V - setValue?: (value: V) => void - getMinHeight?: () => number - getMaxHeight?: () => number - getHeight?: () => string | number - onDraw?: (widget: BaseDOMWidget) => void - margin?: number - /** - * @deprecated Use `afterResize` instead. This callback is a legacy API - * that fires before resize happens, but it is no longer supported. Now it - * fires after resize happens. - * The resize logic has been upstreamed to litegraph in - * https://github.com/Comfy-Org/ComfyUI_frontend/pull/2557 - */ - beforeResize?: (this: BaseDOMWidget, node: LGraphNode) => void - afterResize?: (this: BaseDOMWidget, node: LGraphNode) => void -} - -export const isDOMWidget = ( - widget: IWidget -): widget is DOMWidget => 'element' in widget && !!widget.element - -export const isComponentWidget = ( - widget: IWidget -): widget is ComponentWidget => 'component' in widget && !!widget.component - -abstract class BaseDOMWidgetImpl - implements BaseDOMWidget -{ - static readonly DEFAULT_MARGIN = 10 - readonly type: 'custom' - readonly name: string - readonly options: DOMWidgetOptions - computedHeight?: number - y: number = 0 - callback?: (value: V) => void - - readonly id: string - readonly node: LGraphNode - - constructor(obj: { - id: string - node: LGraphNode - name: string - type: string - options: DOMWidgetOptions - }) { - // @ts-expect-error custom widget type - this.type = obj.type - this.name = obj.name - this.options = obj.options - - this.id = obj.id - this.node = obj.node - } - - get value(): V { - return this.options.getValue?.() ?? ('' as V) - } - - set value(v: V) { - this.options.setValue?.(v) - this.callback?.(this.value) - } - - get margin(): number { - return this.options.margin ?? BaseDOMWidgetImpl.DEFAULT_MARGIN - } - - isVisible(): boolean { - return ( - !_.isNil(this.computedHeight) && - this.computedHeight > 0 && - !['converted-widget', 'hidden'].includes(this.type) && - !this.node.collapsed - ) - } - - draw( - ctx: CanvasRenderingContext2D, - _node: LGraphNode, - widget_width: number, - y: number, - widget_height: number, - lowQuality?: boolean - ): void { - if (this.options.hideOnZoom && lowQuality && this.isVisible()) { - // Draw a placeholder rectangle - const originalFillStyle = ctx.fillStyle - ctx.beginPath() - ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR - ctx.rect( - this.margin, - y + this.margin, - widget_width - this.margin * 2, - (this.computedHeight ?? widget_height) - 2 * this.margin - ) - ctx.fill() - ctx.fillStyle = originalFillStyle - } - this.options.onDraw?.(this) - } - - onRemove(): void { - useDomWidgetStore().unregisterWidget(this.id) - } -} - -export class DOMWidgetImpl - extends BaseDOMWidgetImpl - implements DOMWidget -{ - readonly element: T - - constructor(obj: { - id: string - node: LGraphNode - name: string - type: string - element: T - options: DOMWidgetOptions - }) { - super(obj) - this.element = obj.element - } - - /** Extract DOM widget size info */ - computeLayoutSize(node: LGraphNode) { - // @ts-expect-error custom widget type - if (this.type === 'hidden') { - return { - minHeight: 0, - maxHeight: 0, - minWidth: 0 - } - } - - const styles = getComputedStyle(this.element) - let minHeight = - this.options.getMinHeight?.() ?? - parseInt(styles.getPropertyValue('--comfy-widget-min-height')) - let maxHeight = - this.options.getMaxHeight?.() ?? - parseInt(styles.getPropertyValue('--comfy-widget-max-height')) - - let prefHeight: string | number = - this.options.getHeight?.() ?? - styles.getPropertyValue('--comfy-widget-height') - - if (typeof prefHeight === 'string' && prefHeight.endsWith?.('%')) { - prefHeight = - node.size[1] * - (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100) - } else { - prefHeight = - typeof prefHeight === 'number' ? prefHeight : parseInt(prefHeight) - - if (isNaN(minHeight)) { - minHeight = prefHeight - } - } - - return { - minHeight: isNaN(minHeight) ? 50 : minHeight, - maxHeight: isNaN(maxHeight) ? undefined : maxHeight, - minWidth: 0 - } - } -} - -export class ComponentWidgetImpl - extends BaseDOMWidgetImpl - implements ComponentWidget -{ - readonly component: Component - readonly inputSpec: InputSpec - - constructor(obj: { - id: string - node: LGraphNode - name: string - component: Component - inputSpec: InputSpec - options: DOMWidgetOptions - }) { - super({ - ...obj, - type: 'custom' - }) - this.component = obj.component - this.inputSpec = obj.inputSpec - } - - computeLayoutSize() { - const minHeight = this.options.getMinHeight?.() ?? 50 - const maxHeight = this.options.getMaxHeight?.() - return { - minHeight, - maxHeight, - minWidth: 0 - } - } - - serializeValue(): V { - return toRaw(this.value) - } -} - -export const addWidget = >( - node: LGraphNode, - widget: W -) => { - node.addCustomWidget(widget) - node.onRemoved = useChainCallback(node.onRemoved, () => { - widget.onRemove?.() - }) - - node.onResize = useChainCallback(node.onResize, () => { - widget.options.beforeResize?.call(widget, node) - widget.options.afterResize?.call(widget, node) - }) - - useDomWidgetStore().registerWidget(widget) -} - -LGraphNode.prototype.addDOMWidget = function < - T extends HTMLElement, - V extends object | string ->( - this: LGraphNode, - name: string, - type: string, - element: T, - options: DOMWidgetOptions = {} -): DOMWidget { - const widget = new DOMWidgetImpl({ - id: generateUUID(), - node: this, - name, - type, - element, - options: { hideOnZoom: true, ...options } - }) - // Note: Before `LGraphNode.configure` is called, `this.id` is always `-1`. - addWidget(this, widget as unknown as BaseDOMWidget) - - // Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493 - // Some custom nodes are explicitly expecting getter and setter of `value` - // property to be on instance instead of prototype. - Object.defineProperty(widget, 'value', { - get(this: DOMWidgetImpl): V { - return this.options.getValue?.() ?? ('' as V) - }, - set(this: DOMWidgetImpl, v: V) { - this.options.setValue?.(v) - this.callback?.(this.value) - } - }) - - return widget -} diff --git a/web/comfyui/legacyDomWidget.ts b/web/comfyui/legacyDomWidget.ts deleted file mode 100644 index be59be18..00000000 --- a/web/comfyui/legacyDomWidget.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { LGraphCanvas, LGraphNode } from '@comfyorg/litegraph' -import type { Size, Vector4 } from '@comfyorg/litegraph' -import type { ISerialisedNode } from '@comfyorg/litegraph/dist/types/serialisation' -import type { - ICustomWidget, - IWidgetOptions -} from '@comfyorg/litegraph/dist/types/widgets' - -import { useSettingStore } from '@/stores/settingStore' - -import { app } from './app' - -const SIZE = Symbol() - -interface Rect { - height: number - width: number - x: number - y: number -} - -export interface DOMWidget - extends ICustomWidget { - // All unrecognized types will be treated the same way as 'custom' in litegraph internally. - type: 'custom' - name: string - element: T - options: DOMWidgetOptions - value: V - y?: number - /** - * @deprecated Legacy property used by some extensions for customtext - * (textarea) widgets. Use `element` instead as it provides the same - * functionality and works for all DOMWidget types. - */ - inputEl?: T - callback?: (value: V) => void - /** - * Draw the widget on the canvas. - */ - draw?: ( - ctx: CanvasRenderingContext2D, - node: LGraphNode, - widgetWidth: number, - y: number, - widgetHeight: number - ) => void - /** - * TODO(huchenlei): Investigate when is this callback fired. `onRemove` is - * on litegraph's IBaseWidget definition, but not called in litegraph. - * Currently only called in widgetInputs.ts. - */ - onRemove?: () => void -} - -export interface DOMWidgetOptions< - T extends HTMLElement, - V extends object | string -> extends IWidgetOptions { - hideOnZoom?: boolean - selectOn?: string[] - onHide?: (widget: DOMWidget) => void - getValue?: () => V - setValue?: (value: V) => void - getMinHeight?: () => number - getMaxHeight?: () => number - getHeight?: () => string | number - onDraw?: (widget: DOMWidget) => void - beforeResize?: (this: DOMWidget, node: LGraphNode) => void - afterResize?: (this: DOMWidget, node: LGraphNode) => void -} - -function intersect(a: Rect, b: Rect): Vector4 | null { - const x = Math.max(a.x, b.x) - const num1 = Math.min(a.x + a.width, b.x + b.width) - const y = Math.max(a.y, b.y) - const num2 = Math.min(a.y + a.height, b.y + b.height) - if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y] - else return null -} - -function getClipPath( - node: LGraphNode, - element: HTMLElement, - canvasRect: DOMRect -): string { - const selectedNode: LGraphNode = Object.values( - app.canvas.selected_nodes ?? {} - )[0] as LGraphNode - if (selectedNode && selectedNode !== node) { - const elRect = element.getBoundingClientRect() - const MARGIN = 4 - const { offset, scale } = app.canvas.ds - const { renderArea } = selectedNode - - // Get intersection in browser space - const intersection = intersect( - { - x: elRect.left - canvasRect.left, - y: elRect.top - canvasRect.top, - width: elRect.width, - height: elRect.height - }, - { - x: (renderArea[0] + offset[0] - MARGIN) * scale, - y: (renderArea[1] + offset[1] - MARGIN) * scale, - width: (renderArea[2] + 2 * MARGIN) * scale, - height: (renderArea[3] + 2 * MARGIN) * scale - } - ) - - if (!intersection) { - return '' - } - - // Convert intersection to canvas scale (element has scale transform) - const clipX = - (intersection[0] - elRect.left + canvasRect.left) / scale + 'px' - const clipY = (intersection[1] - elRect.top + canvasRect.top) / scale + 'px' - const clipWidth = intersection[2] / scale + 'px' - const clipHeight = intersection[3] / scale + 'px' - const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)` - return path - } - return '' -} - -// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen -const elementWidgets = new Set() -const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes -LGraphCanvas.prototype.computeVisibleNodes = function ( - nodes?: LGraphNode[], - out?: LGraphNode[] -): LGraphNode[] { - const visibleNodes = computeVisibleNodes.call(this, nodes, out) - - for (const node of app.graph.nodes) { - if (elementWidgets.has(node)) { - const hidden = visibleNodes.indexOf(node) === -1 - for (const w of node.widgets ?? []) { - if (w.element) { - w.element.dataset.isInVisibleNodes = hidden ? 'false' : 'true' - const shouldOtherwiseHide = w.element.dataset.shouldHide === 'true' - const isCollapsed = w.element.dataset.collapsed === 'true' - const wasHidden = w.element.hidden - const actualHidden = hidden || shouldOtherwiseHide || isCollapsed - w.element.hidden = actualHidden - w.element.style.display = actualHidden ? 'none' : '' - if (actualHidden && !wasHidden) { - w.options.onHide?.(w as DOMWidget) - } - } - } - } - } - - return visibleNodes -} - -export class DOMWidgetImpl - implements DOMWidget -{ - type: 'custom' - name: string - element: T - options: DOMWidgetOptions - computedHeight?: number - callback?: (value: V) => void - private mouseDownHandler?: (event: MouseEvent) => void - - constructor( - name: string, - type: string, - element: T, - options: DOMWidgetOptions = {} - ) { - // @ts-expect-error custom widget type - this.type = type - this.name = name - this.element = element - this.options = options - - if (element.blur) { - this.mouseDownHandler = (event) => { - if (!element.contains(event.target as HTMLElement)) { - element.blur() - } - } - document.addEventListener('mousedown', this.mouseDownHandler) - } - } - - get value(): V { - return this.options.getValue?.() ?? ('' as V) - } - - set value(v: V) { - this.options.setValue?.(v) - this.callback?.(this.value) - } - - /** Extract DOM widget size info */ - computeLayoutSize(node: LGraphNode) { - // @ts-expect-error custom widget type - if (this.type === 'hidden') { - return { - minHeight: 0, - maxHeight: 0, - minWidth: 0 - } - } - - const styles = getComputedStyle(this.element) - let minHeight = - this.options.getMinHeight?.() ?? - parseInt(styles.getPropertyValue('--comfy-widget-min-height')) - let maxHeight = - this.options.getMaxHeight?.() ?? - parseInt(styles.getPropertyValue('--comfy-widget-max-height')) - - let prefHeight: string | number = - this.options.getHeight?.() ?? - styles.getPropertyValue('--comfy-widget-height') - - if (typeof prefHeight === 'string' && prefHeight.endsWith?.('%')) { - prefHeight = - node.size[1] * - (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100) - } else { - prefHeight = - typeof prefHeight === 'number' ? prefHeight : parseInt(prefHeight) - - if (isNaN(minHeight)) { - minHeight = prefHeight - } - } - - return { - minHeight: isNaN(minHeight) ? 50 : minHeight, - maxHeight: isNaN(maxHeight) ? undefined : maxHeight, - minWidth: 0 - } - } - - draw( - ctx: CanvasRenderingContext2D, - node: LGraphNode, - widgetWidth: number, - y: number - ): void { - const { offset, scale } = app.canvas.ds - const hidden = - (!!this.options.hideOnZoom && app.canvas.low_quality) || - (this.computedHeight ?? 0) <= 0 || - // @ts-expect-error custom widget type - this.type === 'converted-widget' || - // @ts-expect-error custom widget type - this.type === 'hidden' - - this.element.dataset.shouldHide = hidden ? 'true' : 'false' - const isInVisibleNodes = this.element.dataset.isInVisibleNodes === 'true' - const isCollapsed = this.element.dataset.collapsed === 'true' - const actualHidden = hidden || !isInVisibleNodes || isCollapsed - const wasHidden = this.element.hidden - this.element.hidden = actualHidden - this.element.style.display = actualHidden ? 'none' : '' - - if (actualHidden && !wasHidden) { - this.options.onHide?.(this) - } - if (actualHidden) { - return - } - - const elRect = ctx.canvas.getBoundingClientRect() - const margin = 10 - const top = node.pos[0] + offset[0] + margin - const left = node.pos[1] + offset[1] + margin + y - - Object.assign(this.element.style, { - transformOrigin: '0 0', - transform: `scale(${scale})`, - left: `${top * scale}px`, - top: `${left * scale}px`, - width: `${widgetWidth - margin * 2}px`, - height: `${(this.computedHeight ?? 50) - margin * 2}px`, - position: 'absolute', - zIndex: app.graph.nodes.indexOf(node), - pointerEvents: app.canvas.read_only ? 'none' : 'auto' - }) - - if (useSettingStore().get('Comfy.DOMClippingEnabled')) { - const clipPath = getClipPath(node, this.element, elRect) - this.element.style.clipPath = clipPath ?? 'none' - this.element.style.willChange = 'clip-path' - } - - this.options.onDraw?.(this) - } - - onRemove(): void { - if (this.mouseDownHandler) { - document.removeEventListener('mousedown', this.mouseDownHandler) - } - this.element.remove() - } -} - -LGraphNode.prototype.addDOMWidget = function < - T extends HTMLElement, - V extends object | string ->( - this: LGraphNode, - name: string, - type: string, - element: T, - options: DOMWidgetOptions = {} -): DOMWidget { - options = { hideOnZoom: true, selectOn: ['focus', 'click'], ...options } - - if (!element.parentElement) { - app.canvasContainer.append(element) - } - element.hidden = true - element.style.display = 'none' - - const { nodeData } = this.constructor - const tooltip = (nodeData?.input.required?.[name] ?? - nodeData?.input.optional?.[name])?.[1]?.tooltip - if (tooltip && !element.title) { - element.title = tooltip - } - - const widget = new DOMWidgetImpl(name, type, element, options) - // Workaround for https://github.com/Comfy-Org/ComfyUI_frontend/issues/2493 - // Some custom nodes are explicitly expecting getter and setter of `value` - // property to be on instance instead of prototype. - Object.defineProperty(widget, 'value', { - get(this: DOMWidgetImpl): V { - return this.options.getValue?.() ?? ('' as V) - }, - set(this: DOMWidgetImpl, v: V) { - this.options.setValue?.(v) - this.callback?.(this.value) - } - }) - - // Ensure selectOn exists before iteration - const selectEvents = options.selectOn ?? ['focus', 'click'] - for (const evt of selectEvents) { - element.addEventListener(evt, () => { - app.canvas.selectNode(this) - app.canvas.bringToFront(this) - }) - } - - this.addCustomWidget(widget) - elementWidgets.add(this) - - const collapse = this.collapse - this.collapse = function (this: LGraphNode, force?: boolean) { - collapse.call(this, force) - if (this.collapsed) { - element.hidden = true - element.style.display = 'none' - } - element.dataset.collapsed = this.collapsed ? 'true' : 'false' - } - - const { onConfigure } = this - this.onConfigure = function ( - this: LGraphNode, - serializedNode: ISerialisedNode - ) { - onConfigure?.call(this, serializedNode) - element.dataset.collapsed = this.collapsed ? 'true' : 'false' - } - - const onRemoved = this.onRemoved - this.onRemoved = function (this: LGraphNode) { - element.remove() - elementWidgets.delete(this) - onRemoved?.call(this) - } - - // @ts-ignore index with symbol - if (!this[SIZE]) { - // @ts-ignore index with symbol - this[SIZE] = true - const onResize = this.onResize - this.onResize = function (this: LGraphNode, size: Size) { - options.beforeResize?.call(widget, this) - onResize?.call(this, size) - options.afterResize?.call(widget, this) - } - } - - return widget -}