Compare commits

...

7 Commits

Author SHA1 Message Date
Benjamin Lu
ca663ff04a Remove logging 2025-09-05 14:52:50 -07:00
Benjamin Lu
2996a66e4a Fix slot elref timing issue 2025-09-05 12:54:07 -07:00
Benjamin Lu
9338a490d1 Add debug logging 2025-09-05 12:09:11 -07:00
Benjamin Lu
ed4627ad59 Fix lifecycle in watch 2025-09-05 11:58:46 -07:00
Benjamin Lu
1e5f7e7574 Merge remote-tracking branch 'origin/main' into fix-slot-layout-sync 2025-09-05 09:52:53 -07:00
Benjamin Lu
50e957e56f [refactor] Simplify conditionals and improve method naming per review
- Combine nested conditions into single check for canvas change
- Use guard clauses to reduce nesting in rendering mode watch
- Rename start() to attemptStart() to better reflect that it may not actually start
2025-09-05 09:45:57 -07:00
Benjamin Lu
942cec10b4 Fix slot layout validity on rendering system change 2025-09-04 23:36:45 -07:00
10 changed files with 161 additions and 65 deletions

View File

@@ -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>

View File

@@ -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()

View File

@@ -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
*/

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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 ?? '',

View File

@@ -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(() => {

View File

@@ -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 ?? '',