| import { app, ANIM_PREVIEW_WIDGET } from "./app.js"; |
|
|
| const SIZE = Symbol(); |
|
|
| function intersect(a, b) { |
| 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, element, elRect) { |
| const selectedNode = Object.values(app.canvas.selected_nodes)[0]; |
| if (selectedNode && selectedNode !== node) { |
| const MARGIN = 7; |
| const scale = app.canvas.ds.scale; |
|
|
| const bounding = selectedNode.getBounding(); |
| const intersection = intersect( |
| { x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale }, |
| { |
| x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN, |
| y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN, |
| width: bounding[2] + MARGIN + MARGIN, |
| height: bounding[3] + MARGIN + MARGIN, |
| } |
| ); |
|
|
| if (!intersection) { |
| return ""; |
| } |
|
|
| const widgetRect = element.getBoundingClientRect(); |
| const clipX = intersection[0] - widgetRect.x / scale + "px"; |
| const clipY = intersection[1] - widgetRect.y / scale + "px"; |
| const clipWidth = intersection[2] + "px"; |
| const clipHeight = intersection[3] + "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 ""; |
| } |
|
|
| function computeSize(size) { |
| if (this.widgets?.[0]?.last_y == null) return; |
|
|
| let y = this.widgets[0].last_y; |
| let freeSpace = size[1] - y; |
|
|
| let widgetHeight = 0; |
| let dom = []; |
| for (const w of this.widgets) { |
| if (w.type === "converted-widget") { |
| |
| delete w.computedHeight; |
| } else if (w.computeSize) { |
| widgetHeight += w.computeSize()[1] + 4; |
| } else if (w.element) { |
| |
| const styles = getComputedStyle(w.element); |
| let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height")); |
| let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height")); |
|
|
| let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height"); |
| if (prefHeight.endsWith?.("%")) { |
| prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100); |
| } else { |
| prefHeight = parseInt(prefHeight); |
| if (isNaN(minHeight)) { |
| minHeight = prefHeight; |
| } |
| } |
| if (isNaN(minHeight)) { |
| minHeight = 50; |
| } |
| if (!isNaN(maxHeight)) { |
| if (!isNaN(prefHeight)) { |
| prefHeight = Math.min(prefHeight, maxHeight); |
| } else { |
| prefHeight = maxHeight; |
| } |
| } |
| dom.push({ |
| minHeight, |
| prefHeight, |
| w, |
| }); |
| } else { |
| widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; |
| } |
| } |
|
|
| freeSpace -= widgetHeight; |
|
|
| |
| const prefGrow = []; |
| const canGrow = []; |
| let growBy = 0; |
| for (const d of dom) { |
| freeSpace -= d.minHeight; |
| if (isNaN(d.prefHeight)) { |
| canGrow.push(d); |
| d.w.computedHeight = d.minHeight; |
| } else { |
| const diff = d.prefHeight - d.minHeight; |
| if (diff > 0) { |
| prefGrow.push(d); |
| growBy += diff; |
| d.diff = diff; |
| } else { |
| d.w.computedHeight = d.minHeight; |
| } |
| } |
| } |
|
|
| if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) { |
| |
| freeSpace -= 220; |
| } |
|
|
| this.freeWidgetSpace = freeSpace; |
|
|
| if (freeSpace < 0) { |
| |
| size[1] -= freeSpace; |
| this.graph.setDirtyCanvas(true); |
| } else { |
| |
| const growDiff = freeSpace - growBy; |
| if (growDiff > 0) { |
| |
| freeSpace = growDiff; |
| for (const d of prefGrow) { |
| d.w.computedHeight = d.prefHeight; |
| } |
| } else { |
| |
| const shared = -growDiff / prefGrow.length; |
| for (const d of prefGrow) { |
| d.w.computedHeight = d.prefHeight - shared; |
| } |
| freeSpace = 0; |
| } |
|
|
| if (freeSpace > 0 && canGrow.length) { |
| |
| const shared = freeSpace / canGrow.length; |
| for (const d of canGrow) { |
| d.w.computedHeight += shared; |
| } |
| } |
| } |
|
|
| |
| for (const w of this.widgets) { |
| w.y = y; |
| if (w.computedHeight) { |
| y += w.computedHeight; |
| } else if (w.computeSize) { |
| y += w.computeSize()[1] + 4; |
| } else { |
| y += LiteGraph.NODE_WIDGET_HEIGHT + 4; |
| } |
| } |
| } |
|
|
| |
| const elementWidgets = new Set(); |
| const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes; |
| LGraphCanvas.prototype.computeVisibleNodes = function () { |
| const visibleNodes = computeVisibleNodes.apply(this, arguments); |
| 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.hidden = hidden; |
| w.element.style.display = hidden ? "none" : undefined; |
| if (hidden) { |
| w.options.onHide?.(w); |
| } |
| } |
| } |
| } |
| } |
|
|
| return visibleNodes; |
| }; |
|
|
| let enableDomClipping = true; |
|
|
| export function addDomClippingSetting() { |
| app.ui.settings.addSetting({ |
| id: "Comfy.DOMClippingEnabled", |
| name: "Enable DOM element clipping (enabling may reduce performance)", |
| type: "boolean", |
| defaultValue: enableDomClipping, |
| onChange(value) { |
| enableDomClipping = !!value; |
| }, |
| }); |
| } |
|
|
| LGraphNode.prototype.addDOMWidget = function (name, type, element, options) { |
| options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options }; |
|
|
| if (!element.parentElement) { |
| document.body.append(element); |
| } |
|
|
| let mouseDownHandler; |
| if (element.blur) { |
| mouseDownHandler = (event) => { |
| if (!element.contains(event.target)) { |
| element.blur(); |
| } |
| }; |
| document.addEventListener("mousedown", mouseDownHandler); |
| } |
|
|
| const widget = { |
| type, |
| name, |
| get value() { |
| return options.getValue?.() ?? undefined; |
| }, |
| set value(v) { |
| options.setValue?.(v); |
| widget.callback?.(widget.value); |
| }, |
| draw: function (ctx, node, widgetWidth, y, widgetHeight) { |
| if (widget.computedHeight == null) { |
| computeSize.call(node, node.size); |
| } |
|
|
| const hidden = |
| node.flags?.collapsed || |
| (!!options.hideOnZoom && app.canvas.ds.scale < 0.5) || |
| widget.computedHeight <= 0 || |
| widget.type === "converted-widget"; |
| element.hidden = hidden; |
| element.style.display = hidden ? "none" : null; |
| if (hidden) { |
| widget.options.onHide?.(widget); |
| return; |
| } |
|
|
| 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 + y); |
|
|
| const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); |
|
|
| Object.assign(element.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.computedHeight ?? 50) - margin * 2}px`, |
| position: "absolute", |
| zIndex: app.graph._nodes.indexOf(node), |
| }); |
|
|
| if (enableDomClipping) { |
| element.style.clipPath = getClipPath(node, element, elRect); |
| element.style.willChange = "clip-path"; |
| } |
|
|
| this.options.onDraw?.(widget); |
| }, |
| element, |
| options, |
| onRemove() { |
| if (mouseDownHandler) { |
| document.removeEventListener("mousedown", mouseDownHandler); |
| } |
| element.remove(); |
| }, |
| }; |
|
|
| for (const evt of options.selectOn) { |
| element.addEventListener(evt, () => { |
| app.canvas.selectNode(this); |
| app.canvas.bringToFront(this); |
| }); |
| } |
|
|
| this.addCustomWidget(widget); |
| elementWidgets.add(this); |
|
|
| const collapse = this.collapse; |
| this.collapse = function() { |
| collapse.apply(this, arguments); |
| if(this.flags?.collapsed) { |
| element.hidden = true; |
| element.style.display = "none"; |
| } |
| } |
|
|
| const onRemoved = this.onRemoved; |
| this.onRemoved = function () { |
| element.remove(); |
| elementWidgets.delete(this); |
| onRemoved?.apply(this, arguments); |
| }; |
|
|
| if (!this[SIZE]) { |
| this[SIZE] = true; |
| const onResize = this.onResize; |
| this.onResize = function (size) { |
| options.beforeResize?.call(widget, this); |
| computeSize.call(this, size); |
| onResize?.apply(this, arguments); |
| options.afterResize?.call(widget, this); |
| }; |
| } |
|
|
| return widget; |
| }; |
|
|