Files
ComfyUI_frontend/src/components/graph/TransformPane.vue
bymyself cd3296f49b [feat] Add viewport debug overlay for TransformPane
- Add optional red border overlay showing viewport bounds with 10px inset
- Display viewport dimensions and device pixel ratio in overlay
- Enable via "Show Performance Overlay" checkbox in debug panel
- Helps visualize actual culling boundaries during development
2025-07-05 03:08:18 -07:00

203 lines
4.8 KiB
Vue

<template>
<div
class="transform-pane"
:class="{ 'transform-pane--interacting': isInteracting }"
:style="transformStyle"
@pointerdown="handlePointerDown"
>
<!-- Vue nodes will be rendered here -->
<slot />
<!-- Debug: Viewport bounds visualization -->
<div
v-if="props.showDebugOverlay"
class="viewport-debug-overlay"
:style="{
position: 'absolute',
left: '10px',
top: '10px',
border: '2px solid red',
width: (props.viewport?.width || 0) - 20 + 'px',
height: (props.viewport?.height || 0) - 20 + 'px',
pointerEvents: 'none',
opacity: 0.5
}"
>
<div style="position: absolute; top: 0; left: 0; background: red; color: white; padding: 2px 5px; font-size: 10px;">
Viewport: {{ props.viewport?.width }}x{{ props.viewport?.height }}
DPR: {{ devicePixelRatio }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { LGraphCanvas } from '@comfyorg/litegraph'
import { onMounted, onUnmounted, provide, ref } from 'vue'
import { useTransformState } from '@/composables/element/useTransformState'
interface TransformPaneProps {
canvas?: LGraphCanvas
viewport?: { width: number; height: number }
showDebugOverlay?: boolean
}
const props = defineProps<TransformPaneProps>()
// Get device pixel ratio for display
const devicePixelRatio = window.devicePixelRatio || 1
// Transform state management
const {
camera,
transformStyle,
syncWithCanvas,
canvasToScreen,
screenToCanvas,
isNodeInViewport
} = useTransformState()
// Interaction state
const isInteracting = ref(false)
let interactionTimeout: number | null = null
// Provide transform utilities to child components
provide('transformState', {
camera,
canvasToScreen,
screenToCanvas,
isNodeInViewport
})
// Handle will-change for performance
const setInteracting = (interactive: boolean) => {
isInteracting.value = interactive
if (!interactive && interactionTimeout !== null) {
clearTimeout(interactionTimeout)
interactionTimeout = null
}
if (!interactive) {
// Delay removing will-change to avoid thrashing
interactionTimeout = window.setTimeout(() => {
isInteracting.value = false
}, 200)
}
}
// Event delegation for node interactions
const handlePointerDown = (event: PointerEvent) => {
const target = event.target as HTMLElement
const nodeElement = target.closest('[data-node-id]')
if (nodeElement) {
const nodeId = nodeElement.getAttribute('data-node-id')
// TODO: Emit event for node interaction
console.log('Node interaction:', nodeId)
}
}
// Sync with canvas on RAF
let rafId: number | null = null
const emit = defineEmits<{
rafStatusChange: [active: boolean]
transformUpdate: [time: number]
}>()
const startSync = () => {
emit('rafStatusChange', true)
const sync = () => {
if (props.canvas) {
const startTime = performance.now()
syncWithCanvas(props.canvas)
const endTime = performance.now()
emit('transformUpdate', endTime - startTime)
}
rafId = requestAnimationFrame(sync)
}
sync()
}
const stopSync = () => {
if (rafId !== null) {
cancelAnimationFrame(rafId)
rafId = null
emit('rafStatusChange', false)
}
}
// Canvas event listeners
const handleCanvasInteractionStart = () => setInteracting(true)
const handleCanvasInteractionEnd = () => setInteracting(false)
onMounted(() => {
startSync()
// Listen to canvas interaction events if available
if (props.canvas) {
props.canvas.canvas.addEventListener('wheel', handleCanvasInteractionStart)
props.canvas.canvas.addEventListener(
'pointerdown',
handleCanvasInteractionStart
)
props.canvas.canvas.addEventListener(
'pointerup',
handleCanvasInteractionEnd
)
props.canvas.canvas.addEventListener(
'pointercancel',
handleCanvasInteractionEnd
)
}
})
onUnmounted(() => {
stopSync()
if (interactionTimeout !== null) {
clearTimeout(interactionTimeout)
}
// Clean up event listeners
if (props.canvas) {
props.canvas.canvas.removeEventListener(
'wheel',
handleCanvasInteractionStart
)
props.canvas.canvas.removeEventListener(
'pointerdown',
handleCanvasInteractionStart
)
props.canvas.canvas.removeEventListener(
'pointerup',
handleCanvasInteractionEnd
)
props.canvas.canvas.removeEventListener(
'pointercancel',
handleCanvasInteractionEnd
)
}
})
</script>
<style scoped>
.transform-pane {
position: absolute;
inset: 0;
contain: layout style paint;
transform-origin: 0 0;
pointer-events: none;
}
.transform-pane--interacting {
will-change: transform;
}
/* Allow pointer events on nodes */
.transform-pane :deep([data-node-id]) {
pointer-events: auto;
}
</style>