mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
Snaps for Comfy 🫰 (#239)
* Add Snaps for Comfy * Add snap visual effects * Update node measure to work everywhere * Fix findConnectByTypeSlot does not work for "*" * Fix regression in fast widget conversion
This commit is contained in:
@@ -294,9 +294,7 @@ export class LGraphCanvas {
|
||||
last_mouse_dragging: boolean
|
||||
onMouseDown: (arg0: CanvasMouseEvent) => void
|
||||
_highlight_pos?: Point
|
||||
_highlight_input?: Point
|
||||
_highlight_input_slot?: INodeInputSlot
|
||||
_highlight_output?: Point
|
||||
_highlight_input?: INodeInputSlot
|
||||
// TODO: Check if panels are used
|
||||
node_panel
|
||||
options_panel
|
||||
@@ -1762,6 +1760,7 @@ export class LGraphCanvas {
|
||||
nodes[i].mouseOver = null
|
||||
this._highlight_input = null
|
||||
this._highlight_pos = null
|
||||
this.link_over_widget = null
|
||||
|
||||
// Hover transitions
|
||||
// TODO: Implement single lerp ease factor for current progress on hover in/out. In drawNode, multiply by ease factor and differential value (e.g. bg alpha +0.5).
|
||||
@@ -2328,8 +2327,6 @@ export class LGraphCanvas {
|
||||
* Called when a mouse move event has to be processed
|
||||
**/
|
||||
processMouseMove(e: CanvasMouseEvent): boolean {
|
||||
this.link_over_widget = null
|
||||
|
||||
if (this.autoresize) this.resize()
|
||||
|
||||
if (this.set_canvas_dirty_on_mouse_event)
|
||||
@@ -2405,15 +2402,18 @@ export class LGraphCanvas {
|
||||
this.dirty_canvas = true
|
||||
|
||||
// For input/output hovering
|
||||
const pos: Point = [0, 0] //to store the output of isOverNodeInput
|
||||
//to store the output of isOverNodeInput
|
||||
const pos: Point = [0, 0]
|
||||
const inputId = this.isOverNodeInput(node, e.canvasX, e.canvasY, pos)
|
||||
const outputId = this.isOverNodeOutput(node, e.canvasX, e.canvasY, pos)
|
||||
const overWidget = this.getWidgetAtCursor(node)
|
||||
|
||||
if (!node.mouseOver) {
|
||||
//mouse enter
|
||||
node.mouseOver = {
|
||||
inputId: inputId,
|
||||
outputId: outputId
|
||||
inputId: null,
|
||||
outputId: null,
|
||||
overWidget: null,
|
||||
}
|
||||
this.node_over = node
|
||||
this.dirty_canvas = true
|
||||
@@ -2421,64 +2421,84 @@ export class LGraphCanvas {
|
||||
node.onMouseEnter?.(e)
|
||||
}
|
||||
|
||||
// The input the mouse is over has changed
|
||||
if (node.mouseOver.inputId !== inputId || node.mouseOver.outputId !== outputId) {
|
||||
node.mouseOver.inputId = inputId
|
||||
node.mouseOver.outputId = outputId
|
||||
this.dirty_canvas = true
|
||||
}
|
||||
|
||||
//in case the node wants to do something
|
||||
node.onMouseMove?.(e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this)
|
||||
|
||||
//if dragging a link
|
||||
if (this.connecting_links) {
|
||||
const firstLink = this.connecting_links[0]
|
||||
// The input the mouse is over has changed
|
||||
if (node.mouseOver.inputId !== inputId || node.mouseOver.outputId !== outputId || node.mouseOver.overWidget !== overWidget) {
|
||||
node.mouseOver.inputId = inputId
|
||||
node.mouseOver.outputId = outputId
|
||||
node.mouseOver.overWidget = overWidget
|
||||
|
||||
if (firstLink.output) {
|
||||
// Check if link is over anything it could connect to - record position of valid target for snap / highlight
|
||||
if (this.connecting_links) {
|
||||
const firstLink = this.connecting_links[0]
|
||||
|
||||
//on top of input
|
||||
if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) {
|
||||
//mouse on top of the corner box, don't know what to do
|
||||
} else {
|
||||
//check if I have a slot below de mouse
|
||||
if (inputId != -1 && node.inputs[inputId] && LiteGraph.isValidConnection(firstLink.output.type, node.inputs[inputId].type)) {
|
||||
this._highlight_input = pos
|
||||
this._highlight_input_slot = node.inputs[inputId] // XXX CHECK THIS
|
||||
} else {
|
||||
// Default: nothing highlighted
|
||||
let highlightPos: Point = null
|
||||
let highlightInput: INodeInputSlot = null
|
||||
let linkOverWidget: IWidget = null
|
||||
|
||||
if (firstLink.node === node) {
|
||||
// Cannot connect link from a node to itself
|
||||
} else if (firstLink.output) {
|
||||
|
||||
// Connecting from an output to an input
|
||||
|
||||
if (inputId === -1 && outputId === -1) {
|
||||
// Allow support for linking to widgets, handled externally to LiteGraph
|
||||
if (this.getWidgetLinkType) {
|
||||
const overWidget = this.getWidgetAtCursor(node)
|
||||
|
||||
if (overWidget) {
|
||||
const widgetLinkType = this.getWidgetLinkType(overWidget, node)
|
||||
if (widgetLinkType && LiteGraph.isValidConnection(firstLink.output.type, widgetLinkType)) {
|
||||
if (firstLink.node.isValidWidgetLink?.(firstLink.output.slot_index, node, overWidget) !== false) {
|
||||
this.link_over_widget = overWidget
|
||||
this.link_over_widget_type = widgetLinkType
|
||||
}
|
||||
if (this.getWidgetLinkType && overWidget) {
|
||||
const widgetLinkType = this.getWidgetLinkType(overWidget, node)
|
||||
if (widgetLinkType && LiteGraph.isValidConnection(firstLink.output.type, widgetLinkType)) {
|
||||
if (firstLink.node.isValidWidgetLink?.(firstLink.output.slot_index, node, overWidget) !== false) {
|
||||
linkOverWidget = overWidget
|
||||
this.link_over_widget_type = widgetLinkType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._highlight_input = null
|
||||
this._highlight_input_slot = null // XXX CHECK THIS
|
||||
}
|
||||
}
|
||||
|
||||
} else if (firstLink.input) {
|
||||
//on top of output
|
||||
if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) {
|
||||
//mouse on top of the corner box, don't know what to do
|
||||
} else {
|
||||
//check if I have a slot below de mouse
|
||||
if (outputId != -1 && node.outputs[outputId] && LiteGraph.isValidConnection(firstLink.input.type, node.outputs[outputId].type)) {
|
||||
this._highlight_output = pos
|
||||
// Node background / title under the pointer
|
||||
if (!linkOverWidget) {
|
||||
const targetSlotId = firstLink.node.findConnectByTypeSlot(true, node, firstLink.output.type)
|
||||
if (targetSlotId !== null && targetSlotId >= 0) {
|
||||
node.getConnectionPos(true, targetSlotId, pos)
|
||||
highlightPos = pos
|
||||
highlightInput = node.inputs[targetSlotId]
|
||||
}
|
||||
}
|
||||
} else if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) {
|
||||
//mouse on top of the corner box, don't know what to do
|
||||
} else {
|
||||
this._highlight_output = null
|
||||
//check if I have a slot below de mouse
|
||||
if (inputId != -1 && node.inputs[inputId] && LiteGraph.isValidConnection(firstLink.output.type, node.inputs[inputId].type)) {
|
||||
highlightPos = pos
|
||||
highlightInput = node.inputs[inputId] // XXX CHECK THIS
|
||||
}
|
||||
}
|
||||
|
||||
} else if (firstLink.input) {
|
||||
|
||||
// Connecting from an input to an output
|
||||
if (inputId === -1 && outputId === -1) {
|
||||
const targetSlotId = firstLink.node.findConnectByTypeSlot(false, node, firstLink.input.type)
|
||||
if (targetSlotId !== null && targetSlotId >= 0) {
|
||||
node.getConnectionPos(false, targetSlotId, pos)
|
||||
highlightPos = pos
|
||||
}
|
||||
} else if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) {
|
||||
//mouse on top of the corner box, don't know what to do
|
||||
} else {
|
||||
//check if I have a slot below de mouse
|
||||
if (outputId != -1 && node.outputs[outputId] && LiteGraph.isValidConnection(firstLink.input.type, node.outputs[outputId].type)) {
|
||||
highlightPos = pos
|
||||
}
|
||||
}
|
||||
}
|
||||
this._highlight_pos = highlightPos
|
||||
this._highlight_input = highlightInput
|
||||
this.link_over_widget = linkOverWidget
|
||||
}
|
||||
|
||||
this.dirty_canvas = true
|
||||
}
|
||||
|
||||
//Search for corner
|
||||
@@ -3769,11 +3789,12 @@ export class LGraphCanvas {
|
||||
link_color = LiteGraph.CONNECTING_LINK_COLOR
|
||||
}
|
||||
|
||||
const highlightPos: Point = this.#getHighlightPosition()
|
||||
//the connection being dragged by the mouse
|
||||
this.renderLink(
|
||||
ctx,
|
||||
link.pos,
|
||||
[this.graph_mouse[0], this.graph_mouse[1]],
|
||||
highlightPos,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
@@ -3825,43 +3846,8 @@ export class LGraphCanvas {
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
ctx.fillStyle = "#ffcc00"
|
||||
if (this._highlight_input) {
|
||||
ctx.beginPath()
|
||||
if (this._highlight_input_slot?.shape === LiteGraph.ARROW_SHAPE) {
|
||||
ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5)
|
||||
ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5)
|
||||
ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5)
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.arc(
|
||||
this._highlight_input[0],
|
||||
this._highlight_input[1],
|
||||
6,
|
||||
0,
|
||||
Math.PI * 2
|
||||
)
|
||||
}
|
||||
ctx.fill()
|
||||
}
|
||||
if (this._highlight_output) {
|
||||
ctx.beginPath()
|
||||
if (this._highlight_input_slot?.shape === LiteGraph.ARROW_SHAPE) {
|
||||
ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5)
|
||||
ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5)
|
||||
ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5)
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.arc(
|
||||
this._highlight_output[0],
|
||||
this._highlight_output[1],
|
||||
6,
|
||||
0,
|
||||
Math.PI * 2
|
||||
)
|
||||
}
|
||||
ctx.fill()
|
||||
}
|
||||
// Gradient half-border over target node
|
||||
this.#renderSnapHighlight(ctx, highlightPos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3905,6 +3891,104 @@ export class LGraphCanvas {
|
||||
// @ts-expect-error
|
||||
if (ctx.finish2D) ctx.finish2D()
|
||||
}
|
||||
|
||||
/** Get the target snap / highlight point in graph space */
|
||||
#getHighlightPosition(): Point {
|
||||
return LiteGraph.snaps_for_comfy
|
||||
? this._highlight_pos ?? this.graph_mouse
|
||||
: this.graph_mouse
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders indicators showing where a link will connect if released.
|
||||
* Partial border over target node and a highlight over the slot itself.
|
||||
* @param ctx Canvas 2D context
|
||||
*/
|
||||
#renderSnapHighlight(ctx: CanvasRenderingContext2D, highlightPos: Point): void {
|
||||
if (!this._highlight_pos) return
|
||||
|
||||
ctx.fillStyle = "#ffcc00"
|
||||
ctx.beginPath()
|
||||
const shape = this._highlight_input?.shape
|
||||
|
||||
if (shape === RenderShape.ARROW) {
|
||||
ctx.moveTo(highlightPos[0] + 8, highlightPos[1] + 0.5)
|
||||
ctx.lineTo(highlightPos[0] - 4, highlightPos[1] + 6 + 0.5)
|
||||
ctx.lineTo(highlightPos[0] - 4, highlightPos[1] - 6 + 0.5)
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.arc(
|
||||
highlightPos[0],
|
||||
highlightPos[1],
|
||||
6,
|
||||
0,
|
||||
Math.PI * 2
|
||||
)
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
if (!LiteGraph.snap_highlights_node) return
|
||||
|
||||
// Ensure we're mousing over a node and connecting a link
|
||||
const node = this.node_over
|
||||
if (!(node && this.connecting_links?.[0])) return
|
||||
|
||||
const { strokeStyle, lineWidth } = ctx
|
||||
|
||||
const area = LGraphCanvas.#tmp_area
|
||||
node.measure(area)
|
||||
node.onBounding?.(area)
|
||||
const gap = 3
|
||||
const radius = this.round_radius + gap
|
||||
|
||||
const x = area[0] - gap
|
||||
const y = area[1] - gap
|
||||
const width = area[2] + (gap * 2)
|
||||
const height = area[3] + (gap * 2)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, width, height, radius)
|
||||
|
||||
// TODO: Currently works on LTR slots only. Add support for other directions.
|
||||
const start = this.connecting_links[0].output === null ? 0 : 1
|
||||
const inverter = start ? -1 : 1
|
||||
|
||||
// Radial highlight centred on highlight pos
|
||||
const hx = highlightPos[0]
|
||||
const hy = highlightPos[1]
|
||||
const gRadius = width < height
|
||||
? width
|
||||
: width * Math.max(height / width, 0.5)
|
||||
|
||||
const gradient = ctx.createRadialGradient(hx, hy, 0, hx, hy, gRadius)
|
||||
gradient.addColorStop(1, "#00000000")
|
||||
gradient.addColorStop(0, "#ffcc00aa")
|
||||
|
||||
// Linear gradient over half the node.
|
||||
const linearGradient = ctx.createLinearGradient(x, y, x + width, y)
|
||||
linearGradient.addColorStop(0.5, "#00000000")
|
||||
linearGradient.addColorStop(start + (0.67 * inverter), "#ddeeff33")
|
||||
linearGradient.addColorStop(start + inverter, "#ffcc0055")
|
||||
|
||||
/**
|
||||
* Workaround for a canvas render issue.
|
||||
* In Chromium 129 (2024-10-15), rounded corners can be rendered with the wrong part of a gradient colour.
|
||||
* Occurs only at certain thicknesses / arc sizes.
|
||||
*/
|
||||
ctx.setLineDash([radius, radius * 0.001])
|
||||
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = linearGradient
|
||||
ctx.stroke()
|
||||
|
||||
ctx.strokeStyle = gradient
|
||||
ctx.stroke()
|
||||
|
||||
ctx.setLineDash([])
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.strokeStyle = strokeStyle
|
||||
}
|
||||
|
||||
/**
|
||||
* draws the panel in the corner that shows subgraph properties
|
||||
**/
|
||||
@@ -4683,11 +4767,12 @@ export class LGraphCanvas {
|
||||
? false
|
||||
: true
|
||||
|
||||
// Normalised node dimensions
|
||||
const area = LGraphCanvas.#tmp_area
|
||||
area[0] = 0 //x
|
||||
area[1] = render_title ? -title_height : 0 //y
|
||||
area[2] = size[0] + 1 //w
|
||||
area[3] = render_title ? size[1] + title_height : size[1] //h
|
||||
node.measure(area)
|
||||
area[0] -= node.pos[0]
|
||||
area[1] -= node.pos[1]
|
||||
area[2]++
|
||||
|
||||
const old_alpha = ctx.globalAlpha
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ export type INodeProperties = Dictionary<unknown> & {
|
||||
}
|
||||
|
||||
interface IMouseOverData {
|
||||
inputId: number
|
||||
outputId: number
|
||||
inputId: number | null
|
||||
outputId: number | null
|
||||
overWidget: IWidget | null
|
||||
}
|
||||
|
||||
interface ConnectByTypeOptions {
|
||||
@@ -1421,6 +1422,27 @@ export class LGraphNode {
|
||||
return custom_widget
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the node for rendering, populating {@link out} with the results in graph space.
|
||||
* @param out Results (x, y, width, height) are inserted into this array.
|
||||
* @param pad Expands the area by this amount on each side. Default: 0
|
||||
*/
|
||||
measure(out: Rect, pad = 0): void {
|
||||
const titleMode = this.constructor.title_mode
|
||||
const renderTitle = titleMode != LiteGraph.TRANSPARENT_TITLE && titleMode != LiteGraph.NO_TITLE
|
||||
const titleHeight = renderTitle ? LiteGraph.NODE_TITLE_HEIGHT : 0
|
||||
|
||||
out[0] = this.pos[0] - pad
|
||||
out[1] = this.pos[1] + -titleHeight - pad
|
||||
if (!this.flags?.collapsed) {
|
||||
out[2] = this.size[0] + (2 * pad)
|
||||
out[3] = this.size[1] + titleHeight + (2 * pad)
|
||||
} else {
|
||||
out[2] = (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + (2 * pad)
|
||||
out[3] = LiteGraph.NODE_TITLE_HEIGHT + (2 * pad)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the bounding of the object, used for rendering purposes
|
||||
* @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage
|
||||
@@ -1429,36 +1451,16 @@ export class LGraphNode {
|
||||
*/
|
||||
getBounding(out?: Float32Array, compute_outer?: boolean): Float32Array {
|
||||
out = out || new Float32Array(4)
|
||||
const nodePos = this.pos
|
||||
const isCollapsed = this.flags.collapsed
|
||||
const nodeSize = this.size
|
||||
|
||||
let left_offset = 0
|
||||
// 1 offset due to how nodes are rendered
|
||||
let right_offset = 1
|
||||
let top_offset = 0
|
||||
let bottom_offset = 0
|
||||
|
||||
this.measure(out)
|
||||
if (compute_outer) {
|
||||
// 4 offset for collapsed node connection points
|
||||
left_offset = 4
|
||||
// 6 offset for right shadow and collapsed node connection points
|
||||
right_offset = 6 + left_offset
|
||||
// 4 offset for collapsed nodes top connection points
|
||||
top_offset = 4
|
||||
// 5 offset for bottom shadow and collapsed node connection points
|
||||
bottom_offset = 5 + top_offset
|
||||
out[0] -= 4
|
||||
out[1] -= 4
|
||||
// Add shadow & left offset
|
||||
out[2] += 6 + 4
|
||||
// Add shadow & top offsets
|
||||
out[3] += 5 + 4
|
||||
}
|
||||
|
||||
out[0] = nodePos[0] - left_offset
|
||||
out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset
|
||||
out[2] = isCollapsed
|
||||
? (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset
|
||||
: nodeSize[0] + right_offset
|
||||
out[3] = isCollapsed
|
||||
? LiteGraph.NODE_TITLE_HEIGHT + bottom_offset
|
||||
: nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset
|
||||
|
||||
this.onBounding?.(out)
|
||||
return out
|
||||
}
|
||||
@@ -1746,8 +1748,10 @@ export class LGraphNode {
|
||||
}
|
||||
// connect to the first free input slot if not found a specific type and this output is general
|
||||
if (opts.wildcardToTyped && (slotType == 0 || slotType == "*" || slotType == "")) {
|
||||
const find = findInputs ? node.findInputSlotFree : node.findOutputSlotFree
|
||||
const nonEventSlot = find({ typesNotAccepted: [LiteGraph.EVENT] })
|
||||
const opt = { typesNotAccepted: [LiteGraph.EVENT] }
|
||||
const nonEventSlot = findInputs
|
||||
? node.findInputSlotFree(opt)
|
||||
: node.findOutputSlotFree(opt)
|
||||
if (nonEventSlot >= 0) return nonEventSlot
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -130,6 +130,8 @@ export class LiteGraphGlobal {
|
||||
shift_click_do_break_link_from = false // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys
|
||||
click_do_break_link_to = false // [false!]prefer false, way too easy to break links
|
||||
ctrl_alt_click_do_break_link = true // [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who!
|
||||
snaps_for_comfy = true // [true!] snaps links when dragging connections over valid targets
|
||||
snap_highlights_node = true // [true!] renders a partial border to highlight when a dragged link is snapped to a node
|
||||
|
||||
search_hide_on_mouse_leave = true // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
|
||||
search_filter_enabled = false // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]
|
||||
|
||||
Reference in New Issue
Block a user