You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
8.6 KiB
323 lines
8.6 KiB
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; |
|
} |
|
|
|
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; |
|
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) { |
|
console.log("enableDomClipping", enableDomClipping); |
|
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; |
|
};
|
|
|