mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-31 05:19:53 +00:00
* Remove COMFY_VUE_NODE_DIMENSIONS * [refactor] Use getSlotPosition for Vue nodes in link rendering Replace direct node position calls with getSlotPosition utility when Vue nodes mode is enabled. This ensures consistent slot positioning across the canvas rendering system. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix getSlotPosition readonly return value (#5433) * Update accordingly to new type * Fix canvas/screen conversion formulas in useTransformState (#5406) * Fix conversion formulas * update test expectations * Remove unused type import --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
352 lines
10 KiB
TypeScript
352 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { useTransformState } from '@/renderer/core/layout/useTransformState'
|
|
|
|
// Create a mock canvas context for transform testing
|
|
function createMockCanvasContext() {
|
|
return {
|
|
canvas: {
|
|
width: 1280,
|
|
height: 720,
|
|
getBoundingClientRect: () => ({
|
|
left: 0,
|
|
top: 0,
|
|
width: 1280,
|
|
height: 720,
|
|
right: 1280,
|
|
bottom: 720,
|
|
x: 0,
|
|
y: 0
|
|
})
|
|
},
|
|
ds: {
|
|
offset: [0, 0],
|
|
scale: 1
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('useTransformState', () => {
|
|
let transformState: ReturnType<typeof useTransformState>
|
|
|
|
beforeEach(() => {
|
|
transformState = useTransformState()
|
|
})
|
|
|
|
describe('initial state', () => {
|
|
it('should initialize with default camera values', () => {
|
|
const { camera } = transformState
|
|
expect(camera.x).toBe(0)
|
|
expect(camera.y).toBe(0)
|
|
expect(camera.z).toBe(1)
|
|
})
|
|
|
|
it('should generate correct initial transform style', () => {
|
|
const { transformStyle } = transformState
|
|
expect(transformStyle.value).toEqual({
|
|
transform: 'scale(1) translate(0px, 0px)',
|
|
transformOrigin: '0 0'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('syncWithCanvas', () => {
|
|
it('should sync camera state with canvas transform', () => {
|
|
const { syncWithCanvas, camera } = transformState
|
|
const mockCanvas = createMockCanvasContext()
|
|
|
|
// Set mock canvas transform
|
|
mockCanvas.ds.offset = [100, 50]
|
|
mockCanvas.ds.scale = 2
|
|
|
|
syncWithCanvas(mockCanvas as any)
|
|
|
|
expect(camera.x).toBe(100)
|
|
expect(camera.y).toBe(50)
|
|
expect(camera.z).toBe(2)
|
|
})
|
|
|
|
it('should handle null canvas gracefully', () => {
|
|
const { syncWithCanvas, camera } = transformState
|
|
|
|
syncWithCanvas(null as any)
|
|
|
|
// Should remain at initial values
|
|
expect(camera.x).toBe(0)
|
|
expect(camera.y).toBe(0)
|
|
expect(camera.z).toBe(1)
|
|
})
|
|
|
|
it('should handle canvas without ds property', () => {
|
|
const { syncWithCanvas, camera } = transformState
|
|
const canvasWithoutDs = { canvas: {} }
|
|
|
|
syncWithCanvas(canvasWithoutDs as any)
|
|
|
|
// Should remain at initial values
|
|
expect(camera.x).toBe(0)
|
|
expect(camera.y).toBe(0)
|
|
expect(camera.z).toBe(1)
|
|
})
|
|
|
|
it('should update transform style after sync', () => {
|
|
const { syncWithCanvas, transformStyle } = transformState
|
|
const mockCanvas = createMockCanvasContext()
|
|
|
|
mockCanvas.ds.offset = [150, 75]
|
|
mockCanvas.ds.scale = 0.5
|
|
|
|
syncWithCanvas(mockCanvas as any)
|
|
|
|
expect(transformStyle.value).toEqual({
|
|
transform: 'scale(0.5) translate(150px, 75px)',
|
|
transformOrigin: '0 0'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('coordinate conversions', () => {
|
|
beforeEach(() => {
|
|
// Set up a known transform state
|
|
const mockCanvas = createMockCanvasContext()
|
|
mockCanvas.ds.offset = [100, 50]
|
|
mockCanvas.ds.scale = 2
|
|
transformState.syncWithCanvas(mockCanvas as any)
|
|
})
|
|
|
|
describe('canvasToScreen', () => {
|
|
it('should convert canvas coordinates to screen coordinates', () => {
|
|
const { canvasToScreen } = transformState
|
|
|
|
const canvasPoint = { x: 10, y: 20 }
|
|
const screenPoint = canvasToScreen(canvasPoint)
|
|
|
|
// screen = (canvas + offset) * scale
|
|
// x: (10 + 100) * 2 = 220
|
|
// y: (20 + 50) * 2 = 140
|
|
expect(screenPoint).toEqual({ x: 220, y: 140 })
|
|
})
|
|
|
|
it('should handle zero coordinates', () => {
|
|
const { canvasToScreen } = transformState
|
|
|
|
const screenPoint = canvasToScreen({ x: 0, y: 0 })
|
|
expect(screenPoint).toEqual({ x: 200, y: 100 })
|
|
})
|
|
|
|
it('should handle negative coordinates', () => {
|
|
const { canvasToScreen } = transformState
|
|
|
|
const screenPoint = canvasToScreen({ x: -10, y: -20 })
|
|
expect(screenPoint).toEqual({ x: 180, y: 60 })
|
|
})
|
|
})
|
|
|
|
describe('screenToCanvas', () => {
|
|
it('should convert screen coordinates to canvas coordinates', () => {
|
|
const { screenToCanvas } = transformState
|
|
|
|
const screenPoint = { x: 220, y: 140 }
|
|
const canvasPoint = screenToCanvas(screenPoint)
|
|
|
|
// canvas = screen / scale - offset
|
|
// x: 220 / 2 - 100 = 10
|
|
// y: 140 / 2 - 50 = 20
|
|
expect(canvasPoint).toEqual({ x: 10, y: 20 })
|
|
})
|
|
|
|
it('should be inverse of canvasToScreen', () => {
|
|
const { canvasToScreen, screenToCanvas } = transformState
|
|
|
|
const originalPoint = { x: 25, y: 35 }
|
|
const screenPoint = canvasToScreen(originalPoint)
|
|
const backToCanvas = screenToCanvas(screenPoint)
|
|
|
|
expect(backToCanvas.x).toBeCloseTo(originalPoint.x)
|
|
expect(backToCanvas.y).toBeCloseTo(originalPoint.y)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getNodeScreenBounds', () => {
|
|
beforeEach(() => {
|
|
const mockCanvas = createMockCanvasContext()
|
|
mockCanvas.ds.offset = [100, 50]
|
|
mockCanvas.ds.scale = 2
|
|
transformState.syncWithCanvas(mockCanvas as any)
|
|
})
|
|
|
|
it('should calculate correct screen bounds for a node', () => {
|
|
const { getNodeScreenBounds } = transformState
|
|
|
|
const nodePos = [10, 20]
|
|
const nodeSize = [200, 100]
|
|
const bounds = getNodeScreenBounds(nodePos, nodeSize)
|
|
|
|
// Top-left: canvasToScreen(10, 20) = (220, 140)
|
|
// Width: 200 * 2 = 400
|
|
// Height: 100 * 2 = 200
|
|
expect(bounds.x).toBe(220)
|
|
expect(bounds.y).toBe(140)
|
|
expect(bounds.width).toBe(400)
|
|
expect(bounds.height).toBe(200)
|
|
})
|
|
})
|
|
|
|
describe('isNodeInViewport', () => {
|
|
beforeEach(() => {
|
|
const mockCanvas = createMockCanvasContext()
|
|
mockCanvas.ds.offset = [0, 0]
|
|
mockCanvas.ds.scale = 1
|
|
transformState.syncWithCanvas(mockCanvas as any)
|
|
})
|
|
|
|
const viewport = { width: 1000, height: 600 }
|
|
|
|
it('should return true for nodes inside viewport', () => {
|
|
const { isNodeInViewport } = transformState
|
|
|
|
const nodePos = [100, 100]
|
|
const nodeSize = [200, 100]
|
|
|
|
expect(isNodeInViewport(nodePos, nodeSize, viewport)).toBe(true)
|
|
})
|
|
|
|
it('should return false for nodes completely outside viewport', () => {
|
|
const { isNodeInViewport } = transformState
|
|
|
|
// Node far to the right
|
|
expect(isNodeInViewport([2000, 100], [200, 100], viewport)).toBe(false)
|
|
|
|
// Node far to the left
|
|
expect(isNodeInViewport([-500, 100], [200, 100], viewport)).toBe(false)
|
|
|
|
// Node far below
|
|
expect(isNodeInViewport([100, 1000], [200, 100], viewport)).toBe(false)
|
|
|
|
// Node far above
|
|
expect(isNodeInViewport([100, -500], [200, 100], viewport)).toBe(false)
|
|
})
|
|
|
|
it('should return true for nodes partially in viewport with margin', () => {
|
|
const { isNodeInViewport } = transformState
|
|
|
|
// Node slightly outside but within margin
|
|
const nodePos = [-50, -50]
|
|
const nodeSize = [100, 100]
|
|
|
|
expect(isNodeInViewport(nodePos, nodeSize, viewport, 0.2)).toBe(true)
|
|
})
|
|
|
|
it('should return false for tiny nodes (size culling)', () => {
|
|
const { isNodeInViewport } = transformState
|
|
|
|
// Node is in viewport but too small
|
|
const nodePos = [100, 100]
|
|
const nodeSize = [3, 3] // Less than 4 pixels
|
|
|
|
expect(isNodeInViewport(nodePos, nodeSize, viewport)).toBe(false)
|
|
})
|
|
|
|
it('should adjust margin based on zoom level', () => {
|
|
const { isNodeInViewport, syncWithCanvas } = transformState
|
|
const mockCanvas = createMockCanvasContext()
|
|
|
|
// Test with very low zoom
|
|
mockCanvas.ds.scale = 0.05
|
|
syncWithCanvas(mockCanvas as any)
|
|
|
|
// Node at edge should still be visible due to increased margin
|
|
expect(isNodeInViewport([1100, 100], [200, 100], viewport)).toBe(true)
|
|
|
|
// Test with high zoom
|
|
mockCanvas.ds.scale = 4
|
|
syncWithCanvas(mockCanvas as any)
|
|
|
|
// Margin should be tighter
|
|
expect(isNodeInViewport([1100, 100], [200, 100], viewport)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('getViewportBounds', () => {
|
|
beforeEach(() => {
|
|
const mockCanvas = createMockCanvasContext()
|
|
mockCanvas.ds.offset = [100, 50]
|
|
mockCanvas.ds.scale = 2
|
|
transformState.syncWithCanvas(mockCanvas as any)
|
|
})
|
|
|
|
it('should calculate viewport bounds in canvas coordinates', () => {
|
|
const { getViewportBounds } = transformState
|
|
const viewport = { width: 1000, height: 600 }
|
|
|
|
const bounds = getViewportBounds(viewport, 0.2)
|
|
|
|
// With 20% margin:
|
|
// marginX = 1000 * 0.2 = 200
|
|
// marginY = 600 * 0.2 = 120
|
|
// topLeft in screen: (-200, -120)
|
|
// bottomRight in screen: (1200, 720)
|
|
|
|
// Convert to canvas coordinates (canvas = screen / scale - offset):
|
|
// topLeft: (-200 / 2 - 100, -120 / 2 - 50) = (-200, -110)
|
|
// bottomRight: (1200 / 2 - 100, 720 / 2 - 50) = (500, 310)
|
|
|
|
expect(bounds.x).toBe(-200)
|
|
expect(bounds.y).toBe(-110)
|
|
expect(bounds.width).toBe(700) // 500 - (-200)
|
|
expect(bounds.height).toBe(420) // 310 - (-110)
|
|
})
|
|
|
|
it('should handle zero margin', () => {
|
|
const { getViewportBounds } = transformState
|
|
const viewport = { width: 1000, height: 600 }
|
|
|
|
const bounds = getViewportBounds(viewport, 0)
|
|
|
|
// No margin, so viewport bounds are exact
|
|
expect(bounds.x).toBe(-100) // 0 / 2 - 100
|
|
expect(bounds.y).toBe(-50) // 0 / 2 - 50
|
|
expect(bounds.width).toBe(500) // 1000 / 2
|
|
expect(bounds.height).toBe(300) // 600 / 2
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle extreme zoom levels', () => {
|
|
const { syncWithCanvas, canvasToScreen } = transformState
|
|
const mockCanvas = createMockCanvasContext()
|
|
|
|
// Very small zoom
|
|
mockCanvas.ds.scale = 0.001
|
|
syncWithCanvas(mockCanvas as any)
|
|
|
|
const point1 = canvasToScreen({ x: 1000, y: 1000 })
|
|
expect(point1.x).toBeCloseTo(1)
|
|
expect(point1.y).toBeCloseTo(1)
|
|
|
|
// Very large zoom
|
|
mockCanvas.ds.scale = 100
|
|
syncWithCanvas(mockCanvas as any)
|
|
|
|
const point2 = canvasToScreen({ x: 1, y: 1 })
|
|
expect(point2.x).toBe(100)
|
|
expect(point2.y).toBe(100)
|
|
})
|
|
|
|
it('should handle zero scale in screenToCanvas', () => {
|
|
const { syncWithCanvas, screenToCanvas } = transformState
|
|
const mockCanvas = createMockCanvasContext()
|
|
|
|
// Scale of 0 gets converted to 1 by || operator
|
|
mockCanvas.ds.scale = 0
|
|
syncWithCanvas(mockCanvas as any)
|
|
|
|
// Should use scale of 1 due to camera.z || 1 in implementation
|
|
const result = screenToCanvas({ x: 100, y: 100 })
|
|
expect(result.x).toBe(100) // (100 - 0) / 1
|
|
expect(result.y).toBe(100) // (100 - 0) / 1
|
|
})
|
|
})
|
|
})
|