mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-11 02:20:08 +00:00
fix: remove unused dependencies and files flagged by knip
This commit is contained in:
13
package.json
13
package.json
@@ -40,11 +40,6 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
|
||||
"@babel/plugin-transform-typescript": "^7.28.0",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@iconify-json/lucide": "^1.2.66",
|
||||
"@iconify/tailwind": "^1.2.0",
|
||||
@@ -69,15 +64,7 @@
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"@vue/babel-plugin-jsx": "^1.5.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"babel-helper-vue-jsx-merge-props": "^2.0.3",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"babel-plugin-transform-import-ignore": "^1.1.0",
|
||||
"babel-plugin-transform-vue-jsx": "^3.7.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-typescript-vue3": "^2.1.1",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="transform-pane"
|
||||
:class="{ 'transform-pane--interacting': isInteracting }"
|
||||
:style="transformStyle"
|
||||
@pointerdown="handlePointerDown"
|
||||
>
|
||||
<!-- Vue nodes will be rendered here -->
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, provide } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/graph/useCanvasTransformSync'
|
||||
import { useTransformSettling } from '@/composables/graph/useTransformSettling'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useTransformState } from '@/renderer/core/layout/useTransformState'
|
||||
|
||||
interface TransformPaneProps {
|
||||
canvas?: LGraphCanvas
|
||||
}
|
||||
|
||||
const props = defineProps<TransformPaneProps>()
|
||||
|
||||
const {
|
||||
camera,
|
||||
transformStyle,
|
||||
syncWithCanvas,
|
||||
canvasToScreen,
|
||||
screenToCanvas,
|
||||
isNodeInViewport
|
||||
} = useTransformState()
|
||||
|
||||
const canvasElement = computed(() => props.canvas?.canvas)
|
||||
const { isTransforming: isInteracting } = useTransformSettling(canvasElement, {
|
||||
settleDelay: 200,
|
||||
trackPan: true
|
||||
})
|
||||
|
||||
provide('transformState', {
|
||||
camera,
|
||||
canvasToScreen,
|
||||
screenToCanvas,
|
||||
isNodeInViewport
|
||||
})
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
const nodeElement = target.closest('[data-node-id]')
|
||||
|
||||
if (nodeElement) {
|
||||
// TODO: Emit event for node interaction
|
||||
// Node interaction with nodeId will be handled in future implementation
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
rafStatusChange: [active: boolean]
|
||||
transformUpdate: [time: number]
|
||||
}>()
|
||||
|
||||
useCanvasTransformSync(props.canvas, syncWithCanvas, {
|
||||
onStart: () => emit('rafStatusChange', true),
|
||||
onUpdate: (duration) => emit('transformUpdate', duration),
|
||||
onStop: () => emit('rafStatusChange', false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.transform-pane {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: 0 0;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.transform-pane--interacting {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Allow pointer events on nodes */
|
||||
.transform-pane :deep([data-node-id]) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,229 +0,0 @@
|
||||
/**
|
||||
* DOM-based slot registration with performance optimization
|
||||
*
|
||||
* Measures the actual DOM position of a Vue slot connector and registers it
|
||||
* into the LayoutStore so hit-testing and link rendering use the true position.
|
||||
*
|
||||
* Performance strategy:
|
||||
* - Cache slot offset relative to node (avoids DOM reads during drag)
|
||||
* - No measurements during pan/zoom (camera transforms don't change canvas coords)
|
||||
* - Batch DOM reads via requestAnimationFrame
|
||||
* - Only remeasure on structural changes (resize, collapse, LOD)
|
||||
*/
|
||||
import {
|
||||
type Ref,
|
||||
type WatchStopHandle,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch
|
||||
} from 'vue'
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { Point as LayoutPoint } from '@/renderer/core/layout/types'
|
||||
|
||||
import { getSlotKey } from './slotIdentifier'
|
||||
|
||||
export type TransformState = {
|
||||
screenToCanvas: (p: LayoutPoint) => LayoutPoint
|
||||
}
|
||||
|
||||
// Shared RAF queue for batching measurements
|
||||
const measureQueue = new Set<() => void>()
|
||||
let rafId: number | null = null
|
||||
// Track mounted components to prevent execution on unmounted ones
|
||||
const mountedComponents = new WeakSet<object>()
|
||||
|
||||
function scheduleMeasurement(fn: () => void) {
|
||||
measureQueue.add(fn)
|
||||
if (rafId === null) {
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = null
|
||||
const batch = Array.from(measureQueue)
|
||||
measureQueue.clear()
|
||||
batch.forEach((measure) => measure())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupFunctions = new WeakMap<
|
||||
Ref<HTMLElement | null>,
|
||||
{
|
||||
stopWatcher?: WatchStopHandle
|
||||
handleResize?: () => void
|
||||
}
|
||||
>()
|
||||
|
||||
interface SlotRegistrationOptions {
|
||||
nodeId: string
|
||||
slotIndex: number
|
||||
isInput: boolean
|
||||
element: Ref<HTMLElement | null>
|
||||
transform?: TransformState
|
||||
}
|
||||
|
||||
export function useDomSlotRegistration(options: SlotRegistrationOptions) {
|
||||
const { nodeId, slotIndex, isInput, element: elRef, transform } = options
|
||||
|
||||
// Early return if no nodeId
|
||||
if (!nodeId || nodeId === '') {
|
||||
return {
|
||||
remeasure: () => {}
|
||||
}
|
||||
}
|
||||
const slotKey = getSlotKey(nodeId, slotIndex, isInput)
|
||||
// Track if this component is mounted
|
||||
const componentToken = {}
|
||||
|
||||
// Cached offset from node position (avoids DOM reads during drag)
|
||||
const cachedOffset = ref<LayoutPoint | null>(null)
|
||||
const lastMeasuredBounds = ref<DOMRect | null>(null)
|
||||
|
||||
// Measure DOM and cache offset (expensive, minimize calls)
|
||||
const measureAndCacheOffset = () => {
|
||||
// Skip if component was unmounted
|
||||
if (!mountedComponents.has(componentToken)) return
|
||||
|
||||
const el = elRef.value
|
||||
if (!el || !transform?.screenToCanvas) return
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
|
||||
// Skip if bounds haven't changed significantly (within 0.5px)
|
||||
if (lastMeasuredBounds.value) {
|
||||
const prev = lastMeasuredBounds.value
|
||||
if (
|
||||
Math.abs(rect.left - prev.left) < 0.5 &&
|
||||
Math.abs(rect.top - prev.top) < 0.5 &&
|
||||
Math.abs(rect.width - prev.width) < 0.5 &&
|
||||
Math.abs(rect.height - prev.height) < 0.5
|
||||
) {
|
||||
return // No significant change - skip update
|
||||
}
|
||||
}
|
||||
|
||||
lastMeasuredBounds.value = rect
|
||||
|
||||
// Center of the visual connector (dot) in screen coords
|
||||
const centerScreen = {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
}
|
||||
const centerCanvas = transform.screenToCanvas(centerScreen)
|
||||
|
||||
// Cache offset from node position for fast updates during drag
|
||||
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
if (nodeLayout) {
|
||||
cachedOffset.value = {
|
||||
x: centerCanvas.x - nodeLayout.position.x,
|
||||
y: centerCanvas.y - nodeLayout.position.y
|
||||
}
|
||||
}
|
||||
|
||||
updateSlotPosition(centerCanvas)
|
||||
}
|
||||
|
||||
// Fast update using cached offset (no DOM read)
|
||||
const updateFromCachedOffset = () => {
|
||||
if (!cachedOffset.value) {
|
||||
// No cached offset yet, need to measure
|
||||
scheduleMeasurement(measureAndCacheOffset)
|
||||
return
|
||||
}
|
||||
|
||||
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
if (!nodeLayout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate absolute position from node position + cached offset
|
||||
const centerCanvas = {
|
||||
x: nodeLayout.position.x + cachedOffset.value.x,
|
||||
y: nodeLayout.position.y + cachedOffset.value.y
|
||||
}
|
||||
|
||||
updateSlotPosition(centerCanvas)
|
||||
}
|
||||
|
||||
// Update slot position in layout store
|
||||
const updateSlotPosition = (centerCanvas: LayoutPoint) => {
|
||||
const size = LiteGraph.NODE_SLOT_HEIGHT
|
||||
const half = size / 2
|
||||
|
||||
layoutStore.updateSlotLayout(slotKey, {
|
||||
nodeId,
|
||||
index: slotIndex,
|
||||
type: isInput ? 'input' : 'output',
|
||||
position: { x: centerCanvas.x, y: centerCanvas.y },
|
||||
bounds: {
|
||||
x: centerCanvas.x - half,
|
||||
y: centerCanvas.y - half,
|
||||
width: size,
|
||||
height: size
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Mark component as mounted
|
||||
mountedComponents.add(componentToken)
|
||||
|
||||
// Initial measure after mount
|
||||
await nextTick()
|
||||
measureAndCacheOffset()
|
||||
|
||||
// Subscribe to node position changes for fast cached updates
|
||||
const nodeRef = layoutStore.getNodeLayoutRef(nodeId)
|
||||
|
||||
const stopWatcher = watch(
|
||||
nodeRef,
|
||||
(newLayout) => {
|
||||
if (newLayout) {
|
||||
// Node moved/resized - update using cached offset
|
||||
updateFromCachedOffset()
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
// Store cleanup functions without type assertions
|
||||
const cleanup = cleanupFunctions.get(elRef) || {}
|
||||
cleanup.stopWatcher = stopWatcher
|
||||
|
||||
// Window resize - remeasure as viewport changed
|
||||
const handleResize = () => {
|
||||
scheduleMeasurement(measureAndCacheOffset)
|
||||
}
|
||||
window.addEventListener('resize', handleResize, { passive: true })
|
||||
cleanup.handleResize = handleResize
|
||||
cleanupFunctions.set(elRef, cleanup)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Mark component as unmounted
|
||||
mountedComponents.delete(componentToken)
|
||||
|
||||
// Clean up watchers and listeners
|
||||
const cleanup = cleanupFunctions.get(elRef)
|
||||
if (cleanup) {
|
||||
if (cleanup.stopWatcher) cleanup.stopWatcher()
|
||||
if (cleanup.handleResize) {
|
||||
window.removeEventListener('resize', cleanup.handleResize)
|
||||
}
|
||||
cleanupFunctions.delete(elRef)
|
||||
}
|
||||
|
||||
// Remove from layout store
|
||||
layoutStore.deleteSlotLayout(slotKey)
|
||||
|
||||
// Remove from measurement queue if pending
|
||||
measureQueue.delete(measureAndCacheOffset)
|
||||
})
|
||||
|
||||
return {
|
||||
// Expose for forced remeasure on structural changes
|
||||
remeasure: () => scheduleMeasurement(measureAndCacheOffset)
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
/**
|
||||
* Composable for managing transform state synchronized with LiteGraph canvas
|
||||
*
|
||||
* This composable is a critical part of the hybrid rendering architecture that
|
||||
* allows Vue components to render in perfect alignment with LiteGraph's canvas.
|
||||
*
|
||||
* ## Core Concept
|
||||
*
|
||||
* LiteGraph uses a canvas for rendering connections, grid, and handling interactions.
|
||||
* Vue components need to render nodes on top of this canvas. The challenge is
|
||||
* synchronizing the coordinate systems:
|
||||
*
|
||||
* - LiteGraph: Uses canvas coordinates with its own transform matrix
|
||||
* - Vue/DOM: Uses screen coordinates with CSS transforms
|
||||
*
|
||||
* ## Solution: Transform Container Pattern
|
||||
*
|
||||
* Instead of transforming individual nodes (O(n) complexity), we:
|
||||
* 1. Mirror LiteGraph's transform matrix to a single CSS container
|
||||
* 2. Place all Vue nodes as children with simple absolute positioning
|
||||
* 3. Achieve O(1) transform updates regardless of node count
|
||||
*
|
||||
* ## Coordinate Systems
|
||||
*
|
||||
* - **Canvas coordinates**: LiteGraph's internal coordinate system
|
||||
* - **Screen coordinates**: Browser's viewport coordinate system
|
||||
* - **Transform sync**: camera.x/y/z mirrors canvas.ds.offset/scale
|
||||
*
|
||||
* ## Performance Benefits
|
||||
*
|
||||
* - GPU acceleration via CSS transforms
|
||||
* - No layout thrashing (only transform changes)
|
||||
* - Efficient viewport culling calculations
|
||||
* - Scales to 1000+ nodes while maintaining 60 FPS
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { camera, transformStyle, canvasToScreen } = useTransformState()
|
||||
*
|
||||
* // In template
|
||||
* <div :style="transformStyle">
|
||||
* <NodeComponent
|
||||
* v-for="node in nodes"
|
||||
* :style="{ left: node.x + 'px', top: node.y + 'px' }"
|
||||
* />
|
||||
* </div>
|
||||
*
|
||||
* // Convert coordinates
|
||||
* const screenPos = canvasToScreen({ x: nodeX, y: nodeY })
|
||||
* ```
|
||||
*/
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface Camera {
|
||||
x: number
|
||||
y: number
|
||||
z: number // scale/zoom
|
||||
}
|
||||
|
||||
export const useTransformState = () => {
|
||||
// Reactive state mirroring LiteGraph's canvas transform
|
||||
const camera = reactive<Camera>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 1
|
||||
})
|
||||
|
||||
// Computed transform string for CSS
|
||||
const transformStyle = computed(() => ({
|
||||
// Match LiteGraph DragAndScale.toCanvasContext():
|
||||
// ctx.scale(scale); ctx.translate(offset)
|
||||
// CSS applies right-to-left, so "scale() translate()" -> translate first, then scale
|
||||
// Effective mapping: screen = (canvas + offset) * scale
|
||||
transform: `scale(${camera.z}) translate(${camera.x}px, ${camera.y}px)`,
|
||||
transformOrigin: '0 0'
|
||||
}))
|
||||
|
||||
/**
|
||||
* Synchronizes Vue's reactive camera state with LiteGraph's canvas transform
|
||||
*
|
||||
* Called every frame via RAF to ensure Vue components stay aligned with canvas.
|
||||
* This is the heart of the hybrid rendering system - it bridges the gap between
|
||||
* LiteGraph's canvas transforms and Vue's reactive system.
|
||||
*
|
||||
* @param canvas - LiteGraph canvas instance with DragAndScale (ds) transform state
|
||||
*/
|
||||
const syncWithCanvas = (canvas: LGraphCanvas) => {
|
||||
if (!canvas || !canvas.ds) return
|
||||
|
||||
// Mirror LiteGraph's transform state to Vue's reactive state
|
||||
// ds.offset = pan offset, ds.scale = zoom level
|
||||
camera.x = canvas.ds.offset[0]
|
||||
camera.y = canvas.ds.offset[1]
|
||||
camera.z = canvas.ds.scale || 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts canvas coordinates to screen coordinates
|
||||
*
|
||||
* Applies the same transform that LiteGraph uses for rendering.
|
||||
* Essential for positioning Vue components to align with canvas elements.
|
||||
*
|
||||
* Formula: screen = (canvas + offset) * scale
|
||||
*
|
||||
* @param point - Point in canvas coordinate system
|
||||
* @returns Point in screen coordinate system
|
||||
*/
|
||||
const canvasToScreen = (point: Point): Point => {
|
||||
return {
|
||||
x: (point.x + camera.x) * camera.z,
|
||||
y: (point.y + camera.y) * camera.z
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts screen coordinates to canvas coordinates
|
||||
*
|
||||
* Inverse of canvasToScreen. Useful for hit testing and converting
|
||||
* mouse events back to canvas space.
|
||||
*
|
||||
* Formula: canvas = screen / scale - offset
|
||||
*
|
||||
* @param point - Point in screen coordinate system
|
||||
* @returns Point in canvas coordinate system
|
||||
*/
|
||||
const screenToCanvas = (point: Point): Point => {
|
||||
return {
|
||||
x: point.x / camera.z - camera.x,
|
||||
y: point.y / camera.z - camera.y
|
||||
}
|
||||
}
|
||||
|
||||
// Get node's screen bounds for culling
|
||||
const getNodeScreenBounds = (
|
||||
pos: ArrayLike<number>,
|
||||
size: ArrayLike<number>
|
||||
): DOMRect => {
|
||||
const topLeft = canvasToScreen({ x: pos[0], y: pos[1] })
|
||||
const width = size[0] * camera.z
|
||||
const height = size[1] * camera.z
|
||||
|
||||
return new DOMRect(topLeft.x, topLeft.y, width, height)
|
||||
}
|
||||
|
||||
// Helper: Calculate zoom-adjusted margin for viewport culling
|
||||
const calculateAdjustedMargin = (baseMargin: number): number => {
|
||||
if (camera.z < 0.1) return Math.min(baseMargin * 5, 2.0)
|
||||
if (camera.z > 3.0) return Math.max(baseMargin * 0.5, 0.05)
|
||||
return baseMargin
|
||||
}
|
||||
|
||||
// Helper: Check if node is too small to be visible at current zoom
|
||||
const isNodeTooSmall = (nodeSize: ArrayLike<number>): boolean => {
|
||||
const nodeScreenSize = Math.max(nodeSize[0], nodeSize[1]) * camera.z
|
||||
return nodeScreenSize < 4
|
||||
}
|
||||
|
||||
// Helper: Calculate expanded viewport bounds with margin
|
||||
const getExpandedViewportBounds = (
|
||||
viewport: { width: number; height: number },
|
||||
margin: number
|
||||
) => {
|
||||
const marginX = viewport.width * margin
|
||||
const marginY = viewport.height * margin
|
||||
return {
|
||||
left: -marginX,
|
||||
right: viewport.width + marginX,
|
||||
top: -marginY,
|
||||
bottom: viewport.height + marginY
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Test if node intersects with viewport bounds
|
||||
const testViewportIntersection = (
|
||||
screenPos: { x: number; y: number },
|
||||
nodeSize: ArrayLike<number>,
|
||||
bounds: { left: number; right: number; top: number; bottom: number }
|
||||
): boolean => {
|
||||
const nodeRight = screenPos.x + nodeSize[0] * camera.z
|
||||
const nodeBottom = screenPos.y + nodeSize[1] * camera.z
|
||||
|
||||
return !(
|
||||
nodeRight < bounds.left ||
|
||||
screenPos.x > bounds.right ||
|
||||
nodeBottom < bounds.top ||
|
||||
screenPos.y > bounds.bottom
|
||||
)
|
||||
}
|
||||
|
||||
// Check if node is within viewport with frustum and size-based culling
|
||||
const isNodeInViewport = (
|
||||
nodePos: ArrayLike<number>,
|
||||
nodeSize: ArrayLike<number>,
|
||||
viewport: { width: number; height: number },
|
||||
margin: number = 0.2
|
||||
): boolean => {
|
||||
// Early exit for tiny nodes
|
||||
if (isNodeTooSmall(nodeSize)) return false
|
||||
|
||||
const screenPos = canvasToScreen({ x: nodePos[0], y: nodePos[1] })
|
||||
const adjustedMargin = calculateAdjustedMargin(margin)
|
||||
const bounds = getExpandedViewportBounds(viewport, adjustedMargin)
|
||||
|
||||
return testViewportIntersection(screenPos, nodeSize, bounds)
|
||||
}
|
||||
|
||||
// Get viewport bounds in canvas coordinates (for spatial index queries)
|
||||
const getViewportBounds = (
|
||||
viewport: { width: number; height: number },
|
||||
margin: number = 0.2
|
||||
) => {
|
||||
const marginX = viewport.width * margin
|
||||
const marginY = viewport.height * margin
|
||||
|
||||
const topLeft = screenToCanvas({ x: -marginX, y: -marginY })
|
||||
const bottomRight = screenToCanvas({
|
||||
x: viewport.width + marginX,
|
||||
y: viewport.height + marginY
|
||||
})
|
||||
|
||||
return {
|
||||
x: topLeft.x,
|
||||
y: topLeft.y,
|
||||
width: bottomRight.x - topLeft.x,
|
||||
height: bottomRight.y - topLeft.y
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
camera: readonly(camera),
|
||||
transformStyle,
|
||||
syncWithCanvas,
|
||||
canvasToScreen,
|
||||
screenToCanvas,
|
||||
getNodeScreenBounds,
|
||||
isNodeInViewport,
|
||||
getViewportBounds
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user