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") { // Ignore delete w.computedHeight; } else if (w.computeSize) { widgetHeight += w.computeSize()[1] + 4; } else if (w.element) { // Extract DOM widget size info 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; // Calculate sizes with all widgets at their min height const prefGrow = []; // Nodes that want to grow to their prefd size const canGrow = []; // Nodes that can grow to auto size 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)) { // Allocate space for image freeSpace -= 220; } this.freeWidgetSpace = freeSpace; if (freeSpace < 0) { // Not enough space for all widgets so we need to grow size[1] -= freeSpace; this.graph.setDirtyCanvas(true); } else { // Share the space between each const growDiff = freeSpace - growBy; if (growDiff > 0) { // All pref sizes can be fulfilled freeSpace = growDiff; for (const d of prefGrow) { d.w.computedHeight = d.prefHeight; } } else { // We need to grow evenly const shared = -growDiff / prefGrow.length; for (const d of prefGrow) { d.w.computedHeight = d.prefHeight - shared; } freeSpace = 0; } if (freeSpace > 0 && canGrow.length) { // Grow any that are auto height const shared = freeSpace / canGrow.length; for (const d of canGrow) { d.w.computedHeight += shared; } } } // Position each of the widgets 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; } } } // 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 () { 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; };