mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-21 06:49:37 +00:00
merge main into rh-test
This commit is contained in:
@@ -1,136 +0,0 @@
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
interface CanvasTransformSyncOptions {
|
||||
/**
|
||||
* Whether to automatically start syncing when canvas is available
|
||||
* @default true
|
||||
*/
|
||||
autoStart?: boolean
|
||||
/**
|
||||
* Called when sync starts
|
||||
*/
|
||||
onStart?: () => void
|
||||
/**
|
||||
* Called when sync stops
|
||||
*/
|
||||
onStop?: () => void
|
||||
}
|
||||
|
||||
interface CanvasTransform {
|
||||
scale: number
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages requestAnimationFrame-based synchronization with LiteGraph canvas transforms.
|
||||
*
|
||||
* This composable provides a clean way to sync Vue transform state with LiteGraph canvas
|
||||
* on every frame. It handles RAF lifecycle management, and ensures proper cleanup.
|
||||
*
|
||||
* The sync function typically reads canvas.ds properties like offset and scale to keep
|
||||
* Vue components aligned with the canvas coordinate system.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const syncWithCanvas = (canvas: LGraphCanvas) => {
|
||||
* canvas.ds.scale
|
||||
* canvas.ds.offset
|
||||
* }
|
||||
*
|
||||
* const { isActive, startSync, stopSync } = useCanvasTransformSync(
|
||||
* syncWithCanvas,
|
||||
* {
|
||||
* autoStart: false,
|
||||
* onStart: () => emit('rafStatusChange', true),
|
||||
* onStop: () => emit('rafStatusChange', false)
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function useCanvasTransformSync(
|
||||
syncFn: (canvas: LGraphCanvas) => void,
|
||||
options: CanvasTransformSyncOptions = {}
|
||||
) {
|
||||
const { onStart, onStop, autoStart = true } = options
|
||||
const { getCanvas } = useCanvasStore()
|
||||
|
||||
const isActive = ref(false)
|
||||
let rafId: number | null = null
|
||||
let lastTransform: CanvasTransform = {
|
||||
scale: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0
|
||||
}
|
||||
|
||||
const hasTransformChanged = (canvas: LGraphCanvas): boolean => {
|
||||
const ds = canvas.ds
|
||||
return (
|
||||
ds.scale !== lastTransform.scale ||
|
||||
ds.offset[0] !== lastTransform.offsetX ||
|
||||
ds.offset[1] !== lastTransform.offsetY
|
||||
)
|
||||
}
|
||||
|
||||
const sync = () => {
|
||||
if (!isActive.value) return
|
||||
|
||||
const canvas = getCanvas()
|
||||
if (!canvas) return
|
||||
|
||||
try {
|
||||
// Only run sync if transform actually changed
|
||||
if (hasTransformChanged(canvas)) {
|
||||
lastTransform = {
|
||||
scale: canvas.ds.scale,
|
||||
offsetX: canvas.ds.offset[0],
|
||||
offsetY: canvas.ds.offset[1]
|
||||
}
|
||||
|
||||
syncFn(canvas)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Canvas transform sync error:', error)
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(sync)
|
||||
}
|
||||
|
||||
const startSync = () => {
|
||||
if (isActive.value) return
|
||||
|
||||
isActive.value = true
|
||||
onStart?.()
|
||||
|
||||
// Reset last transform to force initial sync
|
||||
lastTransform = { scale: 0, offsetX: 0, offsetY: 0 }
|
||||
|
||||
sync()
|
||||
}
|
||||
|
||||
const stopSync = () => {
|
||||
isActive.value = false
|
||||
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
}
|
||||
|
||||
onStop?.()
|
||||
}
|
||||
|
||||
onUnmounted(stopSync)
|
||||
|
||||
if (autoStart) {
|
||||
startSync()
|
||||
}
|
||||
|
||||
return {
|
||||
isActive,
|
||||
startSync,
|
||||
stopSync
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LGraphNode,
|
||||
Positionable,
|
||||
Reroute
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import {
|
||||
collectFromNodes,
|
||||
traverseNodesDepthFirst
|
||||
@@ -123,12 +119,14 @@ export function useSelectedLiteGraphItems() {
|
||||
for (const i in selectedNodes) {
|
||||
selectedNodeArray.push(selectedNodes[i])
|
||||
}
|
||||
const allNodesMatch = !selectedNodeArray.some(
|
||||
(selectedNode) => selectedNode.mode !== mode
|
||||
)
|
||||
const newModeForSelectedNode = allNodesMatch ? LGraphEventMode.ALWAYS : mode
|
||||
|
||||
// Process each selected node independently to determine its target state and apply to children
|
||||
selectedNodeArray.forEach((selectedNode) => {
|
||||
// Apply standard toggle logic to the selected node itself
|
||||
const newModeForSelectedNode =
|
||||
selectedNode.mode === mode ? LGraphEventMode.ALWAYS : mode
|
||||
|
||||
selectedNode.mode = newModeForSelectedNode
|
||||
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRafFn } from '@vueuse/core'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { createBounds } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { computeUnionBounds } from '@/utils/mathUtil'
|
||||
|
||||
/**
|
||||
* Manages the position of the selection toolbox independently.
|
||||
* Uses CSS custom properties for performant transform updates.
|
||||
*/
|
||||
|
||||
// Shared signals for auxiliary UI (e.g., MoreOptions) to coordinate hide/restore
|
||||
export const moreOptionsOpen = ref(false)
|
||||
export const forceCloseMoreOptionsSignal = ref(0)
|
||||
export const restoreMoreOptionsSignal = ref(0)
|
||||
export const moreOptionsRestorePending = ref(false)
|
||||
let moreOptionsWasOpenBeforeDrag = false
|
||||
let moreOptionsSelectionSignature: string | null = null
|
||||
|
||||
function buildSelectionSignature(
|
||||
store: ReturnType<typeof useCanvasStore>
|
||||
): string | null {
|
||||
const c = store.canvas
|
||||
if (!c) return null
|
||||
const items = Array.from(c.selectedItems)
|
||||
if (items.length !== 1) return null
|
||||
const item = items[0]
|
||||
if (isLGraphNode(item)) return `N:${item.id}`
|
||||
if (isLGraphGroup(item)) return `G:${item.id}`
|
||||
return null
|
||||
}
|
||||
|
||||
function currentSelectionMatchesSignature(
|
||||
store: ReturnType<typeof useCanvasStore>
|
||||
) {
|
||||
if (!moreOptionsSelectionSignature) return false
|
||||
return buildSelectionSignature(store) === moreOptionsSelectionSignature
|
||||
}
|
||||
|
||||
export function useSelectionToolboxPosition(
|
||||
toolboxRef: Ref<HTMLElement | undefined>
|
||||
) {
|
||||
const canvasStore = useCanvasStore()
|
||||
const lgCanvas = canvasStore.getCanvas()
|
||||
const { getSelectableItems } = useSelectedLiteGraphItems()
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
|
||||
// World position of selection center
|
||||
const worldPosition = ref({ x: 0, y: 0 })
|
||||
@@ -34,17 +69,42 @@ export function useSelectionToolboxPosition(
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
const bounds = createBounds(selectableItems)
|
||||
|
||||
if (!bounds) {
|
||||
return
|
||||
// Get bounds for all selected items
|
||||
const allBounds: ReadOnlyRect[] = []
|
||||
for (const item of selectableItems) {
|
||||
// Skip items without valid IDs
|
||||
if (item.id == null) continue
|
||||
|
||||
if (shouldRenderVueNodes.value && typeof item.id === 'string') {
|
||||
// Use layout store for Vue nodes (only works with string IDs)
|
||||
const layout = layoutStore.getNodeLayoutRef(item.id).value
|
||||
if (layout) {
|
||||
allBounds.push([
|
||||
layout.bounds.x,
|
||||
layout.bounds.y,
|
||||
layout.bounds.width,
|
||||
layout.bounds.height
|
||||
])
|
||||
}
|
||||
} else {
|
||||
// Fallback to LiteGraph bounds for regular nodes or non-string IDs
|
||||
if (item instanceof LGraphNode) {
|
||||
const bounds = item.getBounding()
|
||||
allBounds.push([bounds[0], bounds[1], bounds[2], bounds[3]] as const)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [xBase, y, width] = bounds
|
||||
// Compute union bounds
|
||||
const unionBounds = computeUnionBounds(allBounds)
|
||||
if (!unionBounds) return
|
||||
|
||||
worldPosition.value = {
|
||||
x: xBase + width / 2,
|
||||
y: y
|
||||
x: unionBounds.x + unionBounds.width / 2,
|
||||
// createBounds() applied a default padding of 10px
|
||||
// so adjust Y to maintain visual consistency
|
||||
y: unionBounds.y - 10
|
||||
}
|
||||
|
||||
updateTransform()
|
||||
@@ -68,19 +128,24 @@ export function useSelectionToolboxPosition(
|
||||
}
|
||||
|
||||
// Sync with canvas transform
|
||||
const { startSync, stopSync } = useCanvasTransformSync(updateTransform, {
|
||||
autoStart: false
|
||||
})
|
||||
const { resume: startSync, pause: stopSync } = useRafFn(updateTransform)
|
||||
|
||||
// Watch for selection changes
|
||||
watch(
|
||||
() => canvasStore.getCanvas().state.selectionChanged,
|
||||
(changed) => {
|
||||
if (changed) {
|
||||
if (moreOptionsRestorePending.value || moreOptionsSelectionSignature) {
|
||||
moreOptionsRestorePending.value = false
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
if (!moreOptionsOpen.value) {
|
||||
moreOptionsSelectionSignature = null
|
||||
} else {
|
||||
moreOptionsSelectionSignature = buildSelectionSignature(canvasStore)
|
||||
}
|
||||
}
|
||||
updateSelectionBounds()
|
||||
canvasStore.getCanvas().state.selectionChanged = false
|
||||
|
||||
// Start transform sync if we have selection
|
||||
if (visible.value) {
|
||||
startSync()
|
||||
} else {
|
||||
@@ -90,24 +155,102 @@ export function useSelectionToolboxPosition(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Watch for dragging state
|
||||
watch(
|
||||
() => canvasStore.canvas?.state?.draggingItems,
|
||||
(dragging) => {
|
||||
if (dragging) {
|
||||
// Hide during node dragging
|
||||
visible.value = false
|
||||
} else {
|
||||
// Update after dragging ends
|
||||
requestAnimationFrame(() => {
|
||||
updateSelectionBounds()
|
||||
})
|
||||
() => moreOptionsOpen.value,
|
||||
(v) => {
|
||||
if (v) {
|
||||
moreOptionsSelectionSignature = buildSelectionSignature(canvasStore)
|
||||
} else if (!canvasStore.canvas?.state?.draggingItems) {
|
||||
moreOptionsSelectionSignature = null
|
||||
if (moreOptionsRestorePending.value)
|
||||
moreOptionsRestorePending.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleDragStateChange = (dragging: boolean) => {
|
||||
if (dragging) {
|
||||
handleDragStart()
|
||||
return
|
||||
}
|
||||
|
||||
handleDragEnd()
|
||||
}
|
||||
|
||||
const handleDragStart = () => {
|
||||
visible.value = false
|
||||
|
||||
// Early return if more options wasn't open
|
||||
if (!moreOptionsOpen.value) {
|
||||
moreOptionsRestorePending.value = false
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
return
|
||||
}
|
||||
|
||||
// Handle more options cleanup
|
||||
const currentSig = buildSelectionSignature(canvasStore)
|
||||
const selectionChanged = currentSig !== moreOptionsSelectionSignature
|
||||
|
||||
if (selectionChanged) {
|
||||
moreOptionsSelectionSignature = null
|
||||
}
|
||||
moreOptionsOpen.value = false
|
||||
moreOptionsWasOpenBeforeDrag = true
|
||||
moreOptionsRestorePending.value = !!moreOptionsSelectionSignature
|
||||
|
||||
if (moreOptionsRestorePending.value) {
|
||||
forceCloseMoreOptionsSignal.value++
|
||||
return
|
||||
}
|
||||
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
requestAnimationFrame(() => {
|
||||
updateSelectionBounds()
|
||||
|
||||
const selectionMatches = currentSelectionMatchesSignature(canvasStore)
|
||||
const shouldRestore =
|
||||
moreOptionsWasOpenBeforeDrag &&
|
||||
visible.value &&
|
||||
moreOptionsRestorePending.value &&
|
||||
selectionMatches
|
||||
|
||||
// Single point of assignment for each ref
|
||||
moreOptionsRestorePending.value =
|
||||
shouldRestore && moreOptionsRestorePending.value
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
|
||||
if (shouldRestore) {
|
||||
restoreMoreOptionsSignal.value++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Unified dragging state - combines both LiteGraph and Vue node dragging
|
||||
const isDragging = computed((): boolean => {
|
||||
const litegraphDragging = canvasStore.canvas?.state?.draggingItems ?? false
|
||||
const vueNodeDragging =
|
||||
shouldRenderVueNodes.value && layoutStore.isDraggingVueNodes.value
|
||||
return litegraphDragging || vueNodeDragging
|
||||
})
|
||||
|
||||
watch(isDragging, handleDragStateChange)
|
||||
|
||||
onUnmounted(() => {
|
||||
resetMoreOptionsState()
|
||||
})
|
||||
|
||||
return {
|
||||
visible
|
||||
}
|
||||
}
|
||||
|
||||
// External cleanup utility to be called when SelectionToolbox component unmounts
|
||||
function resetMoreOptionsState() {
|
||||
moreOptionsOpen.value = false
|
||||
moreOptionsRestorePending.value = false
|
||||
moreOptionsWasOpenBeforeDrag = false
|
||||
moreOptionsSelectionSignature = null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user