mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
7 Commits
test/subgr
...
fix-slot-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca663ff04a | ||
|
|
2996a66e4a | ||
|
|
9338a490d1 | ||
|
|
ed4627ad59 | ||
|
|
1e5f7e7574 | ||
|
|
50e957e56f | ||
|
|
942cec10b4 |
@@ -181,9 +181,12 @@ const isVueNodesEnabled = computed(() => shouldRenderVueNodes.value)
|
||||
let nodeManager: ReturnType<typeof useGraphNodeManager> | null = null
|
||||
let cleanupNodeManager: (() => void) | null = null
|
||||
|
||||
// Slot layout sync management
|
||||
let slotSync: ReturnType<typeof useSlotLayoutSync> | null = null
|
||||
let linkSync: ReturnType<typeof useLinkLayoutSync> | null = null
|
||||
// Slot/link layout sync management
|
||||
const slotSync = useSlotLayoutSync()
|
||||
let slotSyncStarted = false
|
||||
const linkSync = useLinkLayoutSync()
|
||||
let linkSyncStarted = false
|
||||
const layoutSync = useLayoutSync()
|
||||
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(new Map())
|
||||
const nodeState = ref<ReadonlyMap<string, NodeState>>(new Map())
|
||||
const nodePositions = ref<ReadonlyMap<string, { x: number; y: number }>>(
|
||||
@@ -237,19 +240,12 @@ const initializeNodeManager = () => {
|
||||
}
|
||||
|
||||
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
||||
const { startSync } = useLayoutSync()
|
||||
startSync(canvasStore.canvas)
|
||||
|
||||
// Initialize slot layout sync for hit detection
|
||||
slotSync = useSlotLayoutSync()
|
||||
if (canvasStore.canvas) {
|
||||
slotSync.start(canvasStore.canvas as LGraphCanvas)
|
||||
}
|
||||
layoutSync.startSync(canvasStore.canvas)
|
||||
|
||||
// Initialize link layout sync for event-driven updates
|
||||
linkSync = useLinkLayoutSync()
|
||||
if (canvasStore.canvas) {
|
||||
linkSync.start(canvasStore.canvas as LGraphCanvas)
|
||||
linkSyncStarted = true
|
||||
}
|
||||
|
||||
// Force computed properties to re-evaluate
|
||||
@@ -266,17 +262,14 @@ const disposeNodeManagerAndSyncs = () => {
|
||||
nodeManager = null
|
||||
cleanupNodeManager = null
|
||||
|
||||
// Clean up slot layout sync
|
||||
if (slotSync) {
|
||||
slotSync.stop()
|
||||
slotSync = null
|
||||
// Clean up link layout sync
|
||||
if (linkSyncStarted) {
|
||||
linkSync.stop()
|
||||
linkSyncStarted = false
|
||||
}
|
||||
|
||||
// Clean up link layout sync
|
||||
if (linkSync) {
|
||||
linkSync.stop()
|
||||
linkSync = null
|
||||
}
|
||||
// Stop layout sync when leaving Vue nodes mode
|
||||
layoutSync.stopSync()
|
||||
|
||||
// Reset reactive maps to inert defaults
|
||||
vueNodeData.value = new Map()
|
||||
@@ -298,6 +291,65 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Ensure slot layout sync starts whenever a canvas is available (LiteGraph mode)
|
||||
watch(
|
||||
() => canvasStore.canvas,
|
||||
(canvas, oldCanvas) => {
|
||||
if (!canvas) {
|
||||
// Canvas was removed - stop sync if active
|
||||
if (slotSync && slotSyncStarted) {
|
||||
slotSync.stop()
|
||||
slotSyncStarted = false
|
||||
}
|
||||
// Clear any stale slot layouts when canvas is torn down
|
||||
layoutStore.clearAllSlotLayouts()
|
||||
return
|
||||
}
|
||||
|
||||
// Canvas changed - restart sync
|
||||
if (oldCanvas && oldCanvas !== canvas && slotSync && slotSyncStarted) {
|
||||
slotSync.stop()
|
||||
slotSyncStarted = false
|
||||
}
|
||||
|
||||
// Start sync if not in Vue mode and not already started
|
||||
if (!slotSyncStarted && !isVueNodesEnabled.value) {
|
||||
const started = slotSync.attemptStart(canvas as LGraphCanvas)
|
||||
slotSyncStarted = started
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// On rendering mode change, clear slot layouts and manage slot sync
|
||||
watch(
|
||||
() => isVueNodesEnabled.value,
|
||||
(enabled) => {
|
||||
// Always clear invalid slot layouts from the prior mode
|
||||
layoutStore.clearAllSlotLayouts()
|
||||
|
||||
if (enabled) {
|
||||
// Switching TO Vue: Stop slot sync to avoid duplicate registration
|
||||
if (slotSync && slotSyncStarted) {
|
||||
slotSync.stop()
|
||||
slotSyncStarted = false
|
||||
}
|
||||
// DOM will re-register via useDomSlotRegistration
|
||||
return
|
||||
}
|
||||
|
||||
// Switching TO LiteGraph
|
||||
if (!canvasStore.canvas || !comfyApp.graph) return
|
||||
|
||||
// Ensure slot sync is active
|
||||
if (!slotSyncStarted) {
|
||||
const started = slotSync.attemptStart(canvasStore.canvas as LGraphCanvas)
|
||||
slotSyncStarted = started
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
// Transform state for viewport culling
|
||||
const { syncWithCanvas } = useTransformState()
|
||||
|
||||
@@ -723,13 +775,15 @@ onUnmounted(() => {
|
||||
nodeManager.cleanup()
|
||||
nodeManager = null
|
||||
}
|
||||
if (slotSync) {
|
||||
if (slotSyncStarted) {
|
||||
slotSync.stop()
|
||||
slotSync = null
|
||||
slotSyncStarted = false
|
||||
}
|
||||
if (linkSync) {
|
||||
if (linkSyncStarted) {
|
||||
linkSync.stop()
|
||||
linkSync = null
|
||||
linkSyncStarted = false
|
||||
}
|
||||
// Ensure layout sync stops
|
||||
layoutSync.stopSync()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -84,10 +84,14 @@ export function useDomSlotRegistration(options: SlotRegistrationOptions) {
|
||||
// Measure DOM and cache offset (expensive, minimize calls)
|
||||
const measureAndCacheOffset = () => {
|
||||
// Skip if component was unmounted
|
||||
if (!mountedComponents.has(componentToken)) return
|
||||
if (!mountedComponents.has(componentToken)) {
|
||||
return
|
||||
}
|
||||
|
||||
const el = elRef.value
|
||||
if (!el || !transform?.screenToCanvas) return
|
||||
if (!el || !transform?.screenToCanvas) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
|
||||
|
||||
@@ -371,13 +371,18 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
updateSlotLayout(key: string, layout: SlotLayout): void {
|
||||
const existing = this.slotLayouts.get(key)
|
||||
|
||||
if (!existing) {
|
||||
logger.debug('Adding slot:', {
|
||||
nodeId: layout.nodeId,
|
||||
type: layout.type,
|
||||
index: layout.index,
|
||||
bounds: layout.bounds
|
||||
})
|
||||
if (existing) {
|
||||
// Short-circuit if bounds and position unchanged (prevents spatial index churn)
|
||||
if (
|
||||
existing.bounds.x === layout.bounds.x &&
|
||||
existing.bounds.y === layout.bounds.y &&
|
||||
existing.bounds.width === layout.bounds.width &&
|
||||
existing.bounds.height === layout.bounds.height &&
|
||||
existing.position.x === layout.position.x &&
|
||||
existing.position.y === layout.position.y
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
@@ -419,6 +424,15 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all slot layouts and their spatial index (O(1) operations)
|
||||
* Used when switching rendering modes (Vue ↔ LiteGraph)
|
||||
*/
|
||||
clearAllSlotLayouts(): void {
|
||||
this.slotLayouts.clear()
|
||||
this.slotSpatialIndex.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update reroute layout data
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,11 @@ export function useLayoutSync() {
|
||||
* This is one-way: Layout → LiteGraph only
|
||||
*/
|
||||
function startSync(canvas: any) {
|
||||
// Ensure previous subscription is cleared before starting a new one
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
unsubscribe = null
|
||||
}
|
||||
if (!canvas?.graph) return
|
||||
|
||||
// Subscribe to layout changes
|
||||
|
||||
@@ -258,6 +258,8 @@ export function useLinkLayoutSync() {
|
||||
* Start link layout sync with event-driven functionality
|
||||
*/
|
||||
function start(canvasInstance: LGraphCanvas): void {
|
||||
// Avoid duplicate subscriptions/handlers if start is called again
|
||||
stop()
|
||||
canvas = canvasInstance
|
||||
graph = canvas.graph
|
||||
if (!graph) return
|
||||
|
||||
@@ -16,7 +16,7 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
* Compute and register slot layouts for a node
|
||||
* @param node LiteGraph node to process
|
||||
*/
|
||||
function computeAndRegisterSlots(node: LGraphNode): void {
|
||||
export function computeAndRegisterSlots(node: LGraphNode): void {
|
||||
const nodeId = String(node.id)
|
||||
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
|
||||
@@ -57,17 +57,18 @@ export function useSlotLayoutSync() {
|
||||
let restoreHandlers: (() => void) | null = null
|
||||
|
||||
/**
|
||||
* Start slot layout sync with full event-driven functionality
|
||||
* Attempt to start slot layout sync with full event-driven functionality
|
||||
* @param canvas LiteGraph canvas instance
|
||||
* @returns true if sync was actually started, false if early-returned
|
||||
*/
|
||||
function start(canvas: LGraphCanvas): void {
|
||||
function attemptStart(canvas: LGraphCanvas): boolean {
|
||||
// When Vue nodes are enabled, slot DOM registers exact positions.
|
||||
// Skip calculated registration to avoid conflicts.
|
||||
if (LiteGraph.vueNodesMode) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
const graph = canvas?.graph
|
||||
if (!graph) return
|
||||
if (!graph) return false
|
||||
|
||||
// Initial registration for all nodes in the current graph
|
||||
for (const node of graph.nodes) {
|
||||
@@ -135,6 +136,8 @@ export function useSlotLayoutSync() {
|
||||
graph.onTrigger = origTrigger || undefined
|
||||
graph.onAfterChange = origAfterChange || undefined
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,7 +160,7 @@ export function useSlotLayoutSync() {
|
||||
})
|
||||
|
||||
return {
|
||||
start,
|
||||
attemptStart,
|
||||
stop
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ export interface LayoutStore {
|
||||
deleteSlotLayout(key: string): void
|
||||
deleteNodeSlotLayouts(nodeId: NodeId): void
|
||||
deleteRerouteLayout(rerouteId: RerouteId): void
|
||||
clearAllSlotLayouts(): void
|
||||
|
||||
// Get layout data
|
||||
getLinkLayout(linkId: LinkId): LinkLayout | null
|
||||
|
||||
@@ -32,7 +32,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Ref, computed, inject, onErrorCaptured, ref, watch } from 'vue'
|
||||
import {
|
||||
type ComponentPublicInstance,
|
||||
computed,
|
||||
inject,
|
||||
onErrorCaptured,
|
||||
ref,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { getSlotColor } from '@/constants/slotColors'
|
||||
@@ -83,19 +90,17 @@ const transformState = inject<TransformState | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const connectionDotRef = ref<{ slotElRef: Ref<HTMLElement> }>()
|
||||
const connectionDotRef = ref<ComponentPublicInstance<{
|
||||
slotElRef: HTMLElement | undefined
|
||||
}> | null>(null)
|
||||
const slotElRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Watch for connection dot ref changes and sync the element ref
|
||||
watch(
|
||||
connectionDotRef,
|
||||
(newValue) => {
|
||||
if (newValue?.slotElRef) {
|
||||
slotElRef.value = newValue.slotElRef.value
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
// Watch for when the child component's ref becomes available
|
||||
// Vue automatically unwraps the Ref when exposing it
|
||||
watchEffect(() => {
|
||||
const el = connectionDotRef.value?.slotElRef
|
||||
slotElRef.value = el || null
|
||||
})
|
||||
|
||||
useDomSlotRegistration({
|
||||
nodeId: props.nodeId ?? '',
|
||||
|
||||
@@ -48,7 +48,10 @@ interface NodeSlotsProps {
|
||||
|
||||
const props = defineProps<NodeSlotsProps>()
|
||||
|
||||
const nodeInfo = computed(() => props.nodeData || props.node || null)
|
||||
const nodeInfo = computed(() => {
|
||||
const info = props.nodeData || props.node || null
|
||||
return info
|
||||
})
|
||||
|
||||
// Filter out input slots that have corresponding widgets
|
||||
const filteredInputs = computed(() => {
|
||||
|
||||
@@ -33,7 +33,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Ref, computed, inject, onErrorCaptured, ref, watch } from 'vue'
|
||||
import {
|
||||
type ComponentPublicInstance,
|
||||
computed,
|
||||
inject,
|
||||
onErrorCaptured,
|
||||
ref,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { getSlotColor } from '@/constants/slotColors'
|
||||
@@ -82,19 +89,17 @@ const transformState = inject<TransformState | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const connectionDotRef = ref<{ slotElRef: Ref<HTMLElement> }>()
|
||||
const connectionDotRef = ref<ComponentPublicInstance<{
|
||||
slotElRef: HTMLElement | undefined
|
||||
}> | null>(null)
|
||||
const slotElRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Watch for connection dot ref changes and sync the element ref
|
||||
watch(
|
||||
connectionDotRef,
|
||||
(newValue) => {
|
||||
if (newValue?.slotElRef) {
|
||||
slotElRef.value = newValue.slotElRef.value
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
// Watch for when the child component's ref becomes available
|
||||
// Vue automatically unwraps the Ref when exposing it
|
||||
watchEffect(() => {
|
||||
const el = connectionDotRef.value?.slotElRef
|
||||
slotElRef.value = el || null
|
||||
})
|
||||
|
||||
useDomSlotRegistration({
|
||||
nodeId: props.nodeId ?? '',
|
||||
|
||||
Reference in New Issue
Block a user