-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Major] Split layout and draw of LGraphNode (Part1) #510
Conversation
Seems like when doing layout of a node with multiple DOM widgets, the location of DOM widget is not correctly captured. The root issue is that DOMWidget is performing layout during drawing. https://github.com/Comfy-Org/ComfyUI_frontend/blob/aaca5191ab4451ae9edf41a71b363cb482161b56/src/scripts/domWidget.ts#L241-L251 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shiny! The more we split measure / arrange / draw, the easier our lives will be. Code is looking good - haven't tested yet though. Let me know if you want a hand w/that.
|
||
slot.draw(ctx, { | ||
pos, | ||
slot.data.draw(ctx, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to go hunting for the meaning of data.draw
here. From the way the interface is written, I assume there is a larger plan to use this in many other ways.
Not really requesting anything here, just wondering if there is friendlier or more explicit way to do this?
export interface LayoutPoint { | ||
x: number | ||
y: number | ||
} | ||
|
||
export interface LayoutSize { | ||
width: number | ||
height: number | ||
} | ||
|
||
export interface LayoutRect extends LayoutPoint, LayoutSize { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 Is this a new design for litegraph structs? I do think we should stick to the same data types everywhere.
If it is, we should probably actually have a quick look at real world perf - size/pos/rect easily get to millions of ops per second, and we're only going to be increasing this as we implement more layout containers. We're GPU-constrained on most systems atm, but that will likely change at some point.
import type { LayoutRect } from "../types/layout" | ||
|
||
export function computeBounds(elements: LayoutRect[]): LayoutRect { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
measure.ts
turned into a layout-function dumping ground. Some of it can probably move in with this guy?
const minX = Math.min(...elements.map(e => e.x)) | ||
const minY = Math.min(...elements.map(e => e.y)) | ||
const maxX = Math.max(...elements.map(e => e.x + e.width)) | ||
const maxY = Math.max(...elements.map(e => e.y + e.height)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const minX = Math.min(...elements.map(e => e.x)) | |
const minY = Math.min(...elements.map(e => e.y)) | |
const maxX = Math.max(...elements.map(e => e.x + e.width)) | |
const maxY = Math.max(...elements.map(e => e.y + e.height)) | |
let minX = Infinity | |
let minY = Infinity | |
let maxX = -Infinity | |
let maxY = -Infinity | |
for (const e of elements) { | |
if (e.x < minX) minX = e.x | |
if (e.y < minY) minY = e.y | |
if (e.x + e.width > maxX) maxX = e.x + e.width | |
if (e.y + e.height > maxY) maxY = e.y + e.height | |
} |
Prefer single loop to uhm, 12 haha. 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
n.b. in an error state (e.g. no elements have a valid "width"), this will have +/- Infinity instead of NaN.
@@ -3242,36 +3238,117 @@ export class LGraphNode implements Positionable, IPinnable { | |||
highlight, | |||
}) | |||
} | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
} | |
#measureSlots<T extends INodeInputSlot | INodeOutputSlot, C extends NodeInputSlot | NodeOutputSlot>( | |
layoutSlots: ElementGroupLayout<C>, | |
slots: T[], | |
slotClass: new (plain: T) => C, | |
connectingLink: ConnectingLink | null, | |
) { | |
if (!slots?.length) return | |
for (const [index, input] of slots.entries()) { | |
const slot = toClass(slotClass, input) | |
const pos = this.getConnectionPos(true, index) | |
const isValid = slot.isValidTarget(connectingLink) | |
const highlight = isValid && this.mouseOver?.inputId === index | |
layoutSlots.elements.push({ | |
type: "slot", | |
id: index, | |
x: pos[0] - this.pos[0], | |
y: pos[1] - this.pos[1], | |
width: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | |
height: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | |
data: slot, | |
highlight, | |
invalid: !isValid, | |
}) | |
} | |
} |
I couldn't stop myself!
(replaces forEach
below)
this.inputs?.forEach((input, index) => { | ||
const slot = toClass(NodeInputSlot, input) | ||
const pos = this.getConnectionPos(true, index) | ||
const isValid = slot.isValidTarget(connectingLink) | ||
const highlight = isValid && this.mouseOver?.inputId === index | ||
|
||
layout.inputSlots.elements.push({ | ||
type: "slot", | ||
id: index, | ||
x: pos[0] - this.pos[0], | ||
y: pos[1] - this.pos[1], | ||
width: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | ||
height: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | ||
data: slot, | ||
highlight, | ||
invalid: !isValid, | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.inputs?.forEach((input, index) => { | |
const slot = toClass(NodeInputSlot, input) | |
const pos = this.getConnectionPos(true, index) | |
const isValid = slot.isValidTarget(connectingLink) | |
const highlight = isValid && this.mouseOver?.inputId === index | |
layout.inputSlots.elements.push({ | |
type: "slot", | |
id: index, | |
x: pos[0] - this.pos[0], | |
y: pos[1] - this.pos[1], | |
width: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | |
height: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | |
data: slot, | |
highlight, | |
invalid: !isValid, | |
}) | |
}) | |
this.#measureSlots(layout.inputSlots, this.inputs, NodeInputSlot, connectingLink) |
this.outputs?.forEach((output, index) => { | ||
const slot = toClass(NodeOutputSlot, output) | ||
const pos = this.getConnectionPos(false, index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.outputs?.forEach((output, index) => { | |
const slot = toClass(NodeOutputSlot, output) | |
const pos = this.getConnectionPos(false, index) |
outputs 1/3
// change opacity of incompatible slots when dragging a connection | ||
this.outputs?.forEach((output, index) => { | ||
const slot = toClass(NodeOutputSlot, output) | ||
const pos = this.getConnectionPos(false, index) | ||
const isValid = slot.isValidTarget(connectingLink) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const isValid = slot.isValidTarget(connectingLink) |
outputs 2/3
const highlight = isValid && this.mouseOver?.outputId === index | ||
|
||
layout.outputSlots.elements.push({ | ||
type: "slot", | ||
id: index, | ||
x: pos[0] - this.pos[0], | ||
y: pos[1] - this.pos[1], | ||
width: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | ||
height: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | ||
data: slot, | ||
highlight, | ||
invalid: !isValid, | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const highlight = isValid && this.mouseOver?.outputId === index | |
layout.outputSlots.elements.push({ | |
type: "slot", | |
id: index, | |
x: pos[0] - this.pos[0], | |
y: pos[1] - this.pos[1], | |
width: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | |
height: LiteGraph.NODE_SLOT_HEIGHT * 0.5, | |
data: slot, | |
highlight, | |
invalid: !isValid, | |
}) | |
}) | |
this.#measureSlots(layout.outputSlots, this.outputs, NodeOutputSlot, connectingLink) |
outputs 3/3
let posY = this.widgets_up ? 2 : (this.widgets_start_y ?? widgetsStartY + 2) | ||
if (this.horizontal) { | ||
posY = 2 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let posY = this.widgets_up ? 2 : (this.widgets_start_y ?? widgetsStartY + 2) | |
if (this.horizontal) { | |
posY = 2 | |
} | |
let posY = this.widgets_up || this.horizontal ? 2 : (this.widgets_start_y ?? widgetsStartY + 2) |
nit
Splits layout and draw of LGraphNode.
This PR splits the layout and draw of LGraphNode so we can perform layout operations easier. Also this would help us better handle which element should interact with the cursor.