mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 17:10:07 +00:00
Add drop on canvas functionality
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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");
|
||||
@@ -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
|
||||
])
|
||||
|
||||
// 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
|
||||
|
||||
e.canvasX = canvasX
|
||||
e.canvasY = canvasY
|
||||
e.safeOffsetX = safeX
|
||||
e.safeOffsetY = safeY
|
||||
|
||||
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] = safeX
|
||||
this.last_mouse_position[1] = safeY
|
||||
} else {
|
||||
// Fallback to DOM rect (legacy path)
|
||||
let clientX_rel = e.clientX
|
||||
let clientY_rel = e.clientY
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
e.safeOffsetX = clientX_rel
|
||||
e.safeOffsetY = clientY_rel
|
||||
|
||||
// TODO: Find a less brittle way to do this
|
||||
|
||||
// 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]
|
||||
|
||||
this.last_mouse_position[0] = clientX_rel
|
||||
this.last_mouse_position[1] = clientY_rel
|
||||
|
||||
e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]
|
||||
e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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()
|
||||
|
||||
@@ -903,6 +903,9 @@ export class ComfyApp {
|
||||
this.canvasContainer,
|
||||
this.canvas
|
||||
)
|
||||
|
||||
// Provide high-performance position converter to LGraphCanvas
|
||||
this.canvas.positionConverter = this.#positionConversion
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user