Skip to content
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3112dba
add dom element resize observer registry for vue node components
christian-byrne Sep 8, 2025
b6269c0
Update src/renderer/extensions/vueNodes/composables/useVueNodeResizeT…
christian-byrne Sep 9, 2025
4287526
refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeO…
benceruleanlu Sep 9, 2025
110ecf3
chore: make TransformState interface non-exported to satisfy knip pre…
benceruleanlu Sep 9, 2025
9786ecf
Revert "chore: make TransformState interface non-exported to satisfy …
benceruleanlu Sep 10, 2025
dbacbc5
Revert "refactor(vue-nodes): typed TransformState InjectionKey, safer…
benceruleanlu Sep 10, 2025
d4c2e83
[refactor] Improve resize tracking composable documentation and test …
christian-byrne Sep 9, 2025
db5a68b
remove typo comment
christian-byrne Sep 9, 2025
c39cdaf
convert to functional bounds collection
christian-byrne Sep 9, 2025
d39bf36
remove inline import
christian-byrne Sep 10, 2025
ea93135
add interfaces for bounds mutations
christian-byrne Sep 10, 2025
ea9c121
remove change log
christian-byrne Sep 10, 2025
ed94827
fix bounds collection when vue nodes turned off
christian-byrne Sep 10, 2025
ffede77
fix title offset on y
christian-byrne Sep 10, 2025
5022f14
move from resize observer to selection toolbox bounds
christian-byrne Sep 10, 2025
f63118b
refactor(vue-nodes): typed TransformState InjectionKey, safer ResizeO…
benceruleanlu Sep 9, 2025
cb2069c
Fix conversion
benceruleanlu Sep 10, 2025
a0ed9d9
Readd padding
benceruleanlu Sep 10, 2025
3a7cc3f
revert churn reducings from layoutStore.ts
benceruleanlu Sep 10, 2025
0ba660f
Rely on RO for resize, and batch
benceruleanlu Sep 10, 2025
ed7a4e9
Improve churn
benceruleanlu Sep 11, 2025
121221d
Cache canvas offset
benceruleanlu Sep 11, 2025
4de2c2f
Merge remote-tracking branch 'origin/main' into bl-update-slots
benceruleanlu Sep 11, 2025
57a1359
rename from measure
benceruleanlu Sep 12, 2025
cb211eb
Merge remote-tracking branch 'origin/main' into bl-update-slots
benceruleanlu Sep 12, 2025
1dfa72c
remove unused
benceruleanlu Sep 12, 2025
69f5391
address review comments
benceruleanlu Sep 15, 2025
9203ed5
Merge remote-tracking branch 'origin/main' into bl-update-slots
benceruleanlu Sep 15, 2025
16ddd4d
allow dragging out links and creating connections
benceruleanlu Sep 17, 2025
777e419
Merge remote-tracking branch 'origin/main' into bl-make-slots-work
benceruleanlu Sep 17, 2025
f91dc82
knip
benceruleanlu Sep 17, 2025
6586ef4
nit
benceruleanlu Sep 17, 2025
ef709d1
nit
benceruleanlu Sep 17, 2025
f26d1e7
nit
benceruleanlu Sep 17, 2025
1e0bb75
nit
benceruleanlu Sep 18, 2025
9d55954
nit
benceruleanlu Sep 18, 2025
196c0c4
excessively unhelpful commit message
benceruleanlu Sep 18, 2025
402f734
tentatively fix numerical enum bug
benceruleanlu Sep 18, 2025
bcce2c8
Revert "tentatively fix numerical enum bug"
benceruleanlu Sep 18, 2025
6eb1f5e
Decisively fix numerical enum bug
benceruleanlu Sep 18, 2025
75d3788
Undecsively fix the decisive fix for the numerical enum bug
benceruleanlu Sep 18, 2025
da3a467
Decisively fix the undecisive fix for the decisive fix for the numeri…
benceruleanlu Sep 18, 2025
6653c92
Switch to useEventListener
benceruleanlu Sep 18, 2025
20eb500
Switch to onDrawForeground
benceruleanlu Sep 18, 2025
9fe3170
Switch to full mutable pointer state
benceruleanlu Sep 18, 2025
7fd3ea1
Merge remote-tracking branch 'origin/bl-fix-monkey' into bl-make-slot…
benceruleanlu Sep 18, 2025
98afaf8
nit
benceruleanlu Sep 18, 2025
18e37cf
use canvasStore
benceruleanlu Sep 18, 2025
0eacdb8
use NodeId type
benceruleanlu Sep 18, 2025
cabcd58
move folders to links
benceruleanlu Sep 18, 2025
3980484
remove unneeded type assertations
benceruleanlu Sep 18, 2025
a093c29
use cn
benceruleanlu Sep 18, 2025
70c9bbf
remove unneeded type assertations
benceruleanlu Sep 18, 2025
5ca4cd8
Force return if readonly
benceruleanlu Sep 18, 2025
bfcf72c
Merge remote-tracking branch 'origin/main' into bl-make-slots-work
benceruleanlu Sep 18, 2025
4baee30
use Point over MutablePoint
benceruleanlu Sep 19, 2025
2bc5993
Refactor readonly noop
benceruleanlu Sep 19, 2025
6044394
Group into pointer session
benceruleanlu Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
Expand Down Expand Up @@ -404,6 +405,7 @@ onMounted(async () => {

// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
attachSlotLinkPreviewRenderer(comfyApp.canvas)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false
Expand Down
14 changes: 3 additions & 11 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import type { PickNevers } from './types/utility'
import type { IBaseWidget } from './types/widgets'
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
import { findFirstNode, getAllNestedItems } from './utils/collections'
import { resolveConnectingLinkColor } from './utils/linkColors'
import type { UUID } from './utils/uuid'
import { BaseWidget } from './widgets/BaseWidget'
import { toConcreteWidget } from './widgets/widgetMap'
Expand Down Expand Up @@ -4716,29 +4717,20 @@ export class LGraphCanvas
const connShape = fromSlot.shape
const connType = fromSlot.type

const colour =
connType === LiteGraph.EVENT
? LiteGraph.EVENT_LINK_COLOR
: LiteGraph.CONNECTING_LINK_COLOR
const colour = resolveConnectingLinkColor(connType)

// the connection being dragged by the mouse
if (this.linkRenderer) {
this.linkRenderer.renderLinkDirect(
this.linkRenderer.renderDraggingLink(
ctx,
pos,
highlightPos,
null,
false,
null,
colour,
fromDirection,
dragDirection,
{
...this.buildLinkRenderContext(),
linkMarkerShape: LinkMarkerShape.None
},
{
disabled: false
}
)
}
Expand Down
13 changes: 13 additions & 0 deletions src/lib/litegraph/src/utils/linkColors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { CanvasColour, ISlotType } from '../interfaces'
import { LiteGraph } from '../litegraph'

/**
* Resolve the colour used while rendering or previewing a connection of a given slot type.
*/
export function resolveConnectingLinkColor(
type: ISlotType | undefined
): CanvasColour {
return type === LiteGraph.EVENT
? LiteGraph.EVENT_LINK_COLOR
: LiteGraph.CONNECTING_LINK_COLOR
}
73 changes: 73 additions & 0 deletions src/renderer/core/canvas/links/slotLinkCompatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { getActivePinia } from 'pinia'

import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type {
SlotDragSource,
SlotDropCandidate
} from '@/renderer/core/canvas/links/slotLinkDragState'
import { app } from '@/scripts/app'

interface CompatibilityResult {
allowable: boolean
targetNode?: LGraphNode
targetSlot?: INodeInputSlot | INodeOutputSlot
}

function resolveNode(nodeId: NodeId) {
const pinia = getActivePinia()
const canvasStore = pinia ? useCanvasStore() : null
const graph = canvasStore?.canvas?.graph ?? app.canvas?.graph
if (!graph) return null
const id = typeof nodeId === 'string' ? Number(nodeId) : nodeId
if (Number.isNaN(id)) return null
return graph.getNodeById(id)
}

export function evaluateCompatibility(
source: SlotDragSource,
candidate: SlotDropCandidate
): CompatibilityResult {
if (candidate.layout.nodeId === source.nodeId) {
return { allowable: false }
}

const isOutputToInput =
source.type === 'output' && candidate.layout.type === 'input'
const isInputToOutput =
source.type === 'input' && candidate.layout.type === 'output'

if (!isOutputToInput && !isInputToOutput) {
return { allowable: false }
}

const sourceNode = resolveNode(source.nodeId)
const targetNode = resolveNode(candidate.layout.nodeId)
if (!sourceNode || !targetNode) {
return { allowable: false }
}

if (isOutputToInput) {
const outputSlot = sourceNode.outputs?.[source.slotIndex]
const inputSlot = targetNode.inputs?.[candidate.layout.index]
if (!outputSlot || !inputSlot) {
return { allowable: false }
}

const allowable = sourceNode.canConnectTo(targetNode, inputSlot, outputSlot)
return { allowable, targetNode, targetSlot: inputSlot }
}

const inputSlot = sourceNode.inputs?.[source.slotIndex]
const outputSlot = targetNode.outputs?.[candidate.layout.index]
if (!inputSlot || !outputSlot) {
return { allowable: false }
}

const allowable = targetNode.canConnectTo(sourceNode, inputSlot, outputSlot)
return { allowable, targetNode, targetSlot: outputSlot }
}
100 changes: 100 additions & 0 deletions src/renderer/core/canvas/links/slotLinkDragState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { reactive, readonly } from 'vue'

import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { SlotLayout } from '@/renderer/core/layout/types'

type SlotDragType = 'input' | 'output'

export interface SlotDragSource {
nodeId: string
slotIndex: number
type: SlotDragType
direction: LinkDirection
position: Readonly<{ x: number; y: number }>
}

export interface SlotDropCandidate {
layout: SlotLayout
compatible: boolean
}

interface MutablePoint {
x: number
y: number
}

interface PointerPosition {
client: MutablePoint
canvas: MutablePoint
}

interface SlotDragState {
active: boolean
pointerId: number | null
source: SlotDragSource | null
pointer: PointerPosition
candidate: SlotDropCandidate | null
}

const state = reactive<SlotDragState>({
active: false,
pointerId: null,
source: null,
pointer: {
client: { x: 0, y: 0 },
canvas: { x: 0, y: 0 }
},
candidate: null
})

function updatePointerPosition(
clientX: number,
clientY: number,
canvasX: number,
canvasY: number
) {
state.pointer.client.x = clientX
state.pointer.client.y = clientY
state.pointer.canvas.x = canvasX
state.pointer.canvas.y = canvasY
}

function setCandidate(candidate: SlotDropCandidate | null) {
state.candidate = candidate
}

function beginDrag(source: SlotDragSource, pointerId: number) {
state.active = true
state.source = source
state.pointerId = pointerId
state.candidate = null
}

function endDrag() {
state.active = false
state.pointerId = null
state.source = null
state.pointer.client.x = 0
state.pointer.client.y = 0
state.pointer.canvas.x = 0
state.pointer.canvas.y = 0
state.candidate = null
}

function getSlotLayout(nodeId: string, slotIndex: number, isInput: boolean) {
const slotKey = getSlotKey(nodeId, slotIndex, isInput)
return layoutStore.getSlotLayout(slotKey)
}

export function useSlotLinkDragState() {
return {
state: readonly(state),
beginDrag,
endDrag,
updatePointerPosition,
setCandidate,
getSlotLayout
}
}
95 changes: 95 additions & 0 deletions src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type {
INodeInputSlot,
INodeOutputSlot,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors'
import {
type SlotDragSource,
useSlotLinkDragState
} from '@/renderer/core/canvas/links/slotLinkDragState'
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'

function buildContext(canvas: LGraphCanvas): LinkRenderContext {
return {
renderMode: canvas.links_render_mode,
connectionWidth: canvas.connections_width,
renderBorder: canvas.render_connections_border,
lowQuality: canvas.low_quality,
highQualityRender: canvas.highquality_render,
scale: canvas.ds.scale,
linkMarkerShape: canvas.linkMarkerShape,
renderConnectionArrows: canvas.render_connection_arrows,
highlightedLinks: new Set(Object.keys(canvas.highlighted_links)),
defaultLinkColor: canvas.default_link_color,
linkTypeColors: (canvas.constructor as typeof LGraphCanvas)
.link_type_colors,
disabledPattern: canvas._pattern
}
}

export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
const originalOnDrawForeground = canvas.onDrawForeground?.bind(canvas)
const patched = (
ctx: CanvasRenderingContext2D,
area: LGraphCanvas['visible_area']
) => {
originalOnDrawForeground?.(ctx, area)

const { state } = useSlotLinkDragState()
if (!state.active || !state.source) return

const { pointer, source } = state
const start = source.position
const sourceSlot = resolveSourceSlot(canvas, source)

const linkRenderer = canvas.linkRenderer
if (!linkRenderer) return

const context = buildContext(canvas)

const from: ReadOnlyPoint = [start.x, start.y]
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]

const startDir = source.direction ?? LinkDirection.RIGHT
const endDir = LinkDirection.CENTER

const colour = resolveConnectingLinkColor(sourceSlot?.type)

ctx.save()

linkRenderer.renderDraggingLink(
ctx,
from,
to,
colour,
startDir,
endDir,
context
)

ctx.restore()
}

canvas.onDrawForeground = patched
}

function resolveSourceSlot(
canvas: LGraphCanvas,
source: SlotDragSource
): INodeInputSlot | INodeOutputSlot | undefined {
const graph = canvas.graph
if (!graph) return undefined

const nodeId = Number(source.nodeId)
if (!Number.isFinite(nodeId)) return undefined

const node = graph.getNodeById(nodeId)
if (!node) return undefined

return source.type === 'output'
? node.outputs?.[source.slotIndex]
: node.inputs?.[source.slotIndex]
}
Loading