Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 13 additions & 4 deletions src/components/searchbox/NodeSearchBoxPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -262,17 +262,26 @@ function cancelNextReset(e: CustomEvent<CanvasPointerEvent>) {
}

function handleDroppedOnCanvas(e: CustomEvent<CanvasPointerEvent>) {
disconnectOnReset = true
const action = e.detail.shiftKey
const pendingAction = searchBoxStore.pendingLinkDropAction
searchBoxStore.setPendingLinkDropAction(null)

const fallbackAction = e.detail.shiftKey
? linkReleaseActionShift.value
: linkReleaseAction.value

const action =
pendingAction ?? fallbackAction ?? LinkReleaseTriggerAction.NO_ACTION

disconnectOnReset = action !== LinkReleaseTriggerAction.NO_ACTION
if (!disconnectOnReset) return

cancelNextReset(e)

switch (action) {
case LinkReleaseTriggerAction.SEARCH_BOX:
cancelNextReset(e)
showSearchBox(e.detail)
break
case LinkReleaseTriggerAction.CONTEXT_MENU:
cancelNextReset(e)
showContextMenu(e.detail)
break
case LinkReleaseTriggerAction.NO_ACTION:
Expand Down
83 changes: 62 additions & 21 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ const cursors = {
NW: 'nwse-resize'
} as const

/** A lightweight converter for client<->canvas coordinate transforms. */
interface PositionConverter {
/** Convert a client/pointer position to canvas (graph) space. */
clientPosToCanvasPos(pos: Point): Point
/** Convert a canvas (graph) position to client space. */
canvasPosToClientPos(pos: Point): Point
/** Optional hook to refresh internal caches (e.g. bounding rect). */
update?(): void
}

/**
* This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
Expand Down Expand Up @@ -478,11 +488,19 @@ export class LGraphCanvas
return this._isLowQuality
}

/**
* Converts pointer/client positions to canvas positions without forcing layout reads.
* When present, {@link adjustMouseEvent} will use this instead of DOM queries.
*/
positionConverter?: PositionConverter

options: {
skip_events?: any
viewport?: any
skip_render?: any
autoresize?: any
/** Optional converter for client<->canvas position transforms. */
positionConverter?: PositionConverter
}

background_image: string
Expand Down Expand Up @@ -739,6 +757,8 @@ export class LGraphCanvas
) {
options ||= {}
this.options = options
if (options.positionConverter)
this.positionConverter = options.positionConverter

// if(graph === undefined)
// throw ("No graph assigned");
Expand Down Expand Up @@ -4453,33 +4473,54 @@ export class LGraphCanvas
adjustMouseEvent<T extends MouseEvent>(
e: T & Partial<CanvasPointerExtensions>
): asserts e is T & CanvasPointerEvent {
let clientX_rel = e.clientX
let clientY_rel = e.clientY
const { ds, positionConverter } = this

if (this.canvas) {
const b = this.canvas.getBoundingClientRect()
clientX_rel -= b.left
clientY_rel -= b.top
}
if (positionConverter) {
const [canvasX, canvasY] = positionConverter.clientPosToCanvasPos([
e.clientX,
e.clientY
])

e.safeOffsetX = clientX_rel
e.safeOffsetY = clientY_rel
// safeOffset is relative to the canvas element (like offsetX/Y), not page
const safeX = (canvasX + ds.offset[0]) * ds.scale
const safeY = (canvasY + ds.offset[1]) * ds.scale

// TODO: Find a less brittle way to do this
e.canvasX = canvasX
e.canvasY = canvasY
e.safeOffsetX = safeX
e.safeOffsetY = safeY

// Only set deltaX and deltaY if not already set.
// If deltaX and deltaY are already present, they are read-only.
// Setting them would result browser error => zoom in/out feature broken.
if (e.deltaX === undefined)
e.deltaX = clientX_rel - this.last_mouse_position[0]
if (e.deltaY === undefined)
e.deltaY = clientY_rel - this.last_mouse_position[1]
if (e.deltaX === undefined) e.deltaX = safeX - this.last_mouse_position[0]
if (e.deltaY === undefined) e.deltaY = safeY - this.last_mouse_position[1]

this.last_mouse_position[0] = clientX_rel
this.last_mouse_position[1] = clientY_rel
this.last_mouse_position[0] = safeX
this.last_mouse_position[1] = safeY
} else {
// Fallback to DOM rect (legacy path)
let clientX_rel = e.clientX
let clientY_rel = e.clientY

e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]
e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]
if (this.canvas) {
const b = this.canvas.getBoundingClientRect()
clientX_rel -= b.left
clientY_rel -= b.top
}

e.safeOffsetX = clientX_rel
e.safeOffsetY = clientY_rel

// Only set deltaX and deltaY if not already set.
if (e.deltaX === undefined)
e.deltaX = clientX_rel - this.last_mouse_position[0]
if (e.deltaY === undefined)
e.deltaY = clientY_rel - this.last_mouse_position[1]

this.last_mouse_position[0] = clientX_rel
this.last_mouse_position[1] = clientY_rel

e.canvasX = clientX_rel / ds.scale - ds.offset[0]
e.canvasY = clientY_rel / ds.scale - ds.offset[1]
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { onBeforeUnmount } from 'vue'

import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink } from '@/lib/litegraph/src/LLink'
import type { Reroute } from '@/lib/litegraph/src/Reroute'
Expand All @@ -11,7 +12,9 @@ import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { useSettingStore } from '@/platform/settings/settingStore'
import { createLinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
import type { LinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
import {
Expand All @@ -24,6 +27,8 @@ import type { Point } from '@/renderer/core/layout/types'
import { toPoint } from '@/renderer/core/layout/utils/geometry'
import { createSlotLinkDragSession } from '@/renderer/extensions/vueNodes/composables/slotLinkDragSession'
import { app } from '@/scripts/app'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
import { createRafBatch } from '@/utils/rafBatch'

interface SlotInteractionOptions {
Expand Down Expand Up @@ -98,6 +103,23 @@ export function useSlotLinkInteraction({
// Per-drag drag-state cache
const dragSession = createSlotLinkDragSession()

const settingStore = useSettingStore()
const searchBoxStore = useSearchBoxStore()

const resolveDropAction = (event: PointerEvent): LinkReleaseTriggerAction => {
const baseAction =
(settingStore.get('Comfy.LinkRelease.Action') as
| LinkReleaseTriggerAction
| null
| undefined) ?? LinkReleaseTriggerAction.NO_ACTION
const shiftAction = settingStore.get('Comfy.LinkRelease.ActionShift') as
| LinkReleaseTriggerAction
| null
| undefined

return event.shiftKey ? shiftAction ?? baseAction : baseAction
}

function candidateFromTarget(
target: EventTarget | null
): SlotDropCandidate | null {
Expand Down Expand Up @@ -502,29 +524,46 @@ export function useSlotLinkInteraction({
return
}

// Prefer using the snapped candidate captured during hover for perf + consistency
// Prefer using any snapped candidate captured during hover
const snappedCandidate = state.candidate?.compatible
? state.candidate
: null

let connected = tryConnectToCandidate(snappedCandidate)

// Fallback to DOM slot under pointer (if any), then node fallback, then reroute
// Then fallback to DOM slot under pointer
if (!connected) {
const domCandidate = candidateFromTarget(event.target)
connected = tryConnectToCandidate(domCandidate)
}

// Then fallback to node under pointer
if (!connected) {
const nodeCandidate = candidateFromNodeTarget(event.target)
connected = tryConnectToCandidate(nodeCandidate)
}

// Then fallback to reroute under pointer
if (!connected) connected = tryConnectViaRerouteAtPointer() || connected

// Drop on canvas: disconnect moving input link(s)
if (!connected && !snappedCandidate && state.source.type === 'input') {
ensureActiveAdapter()?.disconnectMovingLinks()
// Then fallback to dropping on canvas under pointer
if (!connected && !snappedCandidate) {
const canvas: LGraphCanvas | null = app.canvas
const adapter = ensureActiveAdapter()
if (adapter && canvas) {
const action = resolveDropAction(event)
if (action === LinkReleaseTriggerAction.NO_ACTION)
adapter.disconnectMovingLinks()

const adjustMouseEvent: (
e: PointerEvent
) => asserts e is PointerEvent & CanvasPointerEvent =
canvas.adjustMouseEvent.bind(canvas)
adjustMouseEvent(event)

searchBoxStore.setPendingLinkDropAction(action)
canvas.linkConnector?.dropOnNothing(event)
}
}

cleanupInteraction()
Expand Down
3 changes: 3 additions & 0 deletions src/scripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,9 @@ export class ComfyApp {
this.canvasContainer,
this.canvas
)

// Provide high-performance position converter to LGraphCanvas
this.canvas.positionConverter = this.#positionConversion
}

resizeCanvas() {
Expand Down
10 changes: 9 additions & 1 deletion src/stores/workspace/searchBoxStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { computed, ref, shallowRef } from 'vue'
import type NodeSearchBoxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'

export const useSearchBoxStore = defineStore('searchBox', () => {
const settingStore = useSettingStore()
Expand Down Expand Up @@ -41,10 +42,17 @@ export const useSearchBoxStore = defineStore('searchBox', () => {
)
}

const pendingLinkDropAction = ref<LinkReleaseTriggerAction | null>(null)
function setPendingLinkDropAction(action: LinkReleaseTriggerAction | null) {
pendingLinkDropAction.value = action
}

return {
newSearchBoxEnabled,
setPopoverRef,
toggleVisible,
visible
visible,
pendingLinkDropAction,
setPendingLinkDropAction
}
})