diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index e0e0031d9..9e7ef226f 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -6,6 +6,7 @@ import { type LinkRenderContext, LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' +import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { CanvasPointer } from './CanvasPointer' @@ -5559,7 +5560,9 @@ export class LGraphCanvas const link = graph._links.get(link_id) if (!link) continue - const endPos = node.getInputPos(i) + const endPos: Point = LiteGraph.vueNodesMode // TODO: still use LG get pos if vue nodes is off until stable + ? getSlotPosition(node, i, true) + : node.getInputPos(i) // find link info const start_node = graph.getNodeById(link.origin_id) @@ -5569,7 +5572,9 @@ export class LGraphCanvas const startPos: Point = outputId === -1 ? [start_node.pos[0] + 10, start_node.pos[1] + 10] - : start_node.getOutputPos(outputId) + : LiteGraph.vueNodesMode // TODO: still use LG get pos if vue nodes is off until stable + ? getSlotPosition(start_node, outputId, false) + : start_node.getOutputPos(outputId) const output = start_node.outputs[outputId] if (!output) continue diff --git a/src/renderer/core/canvas/litegraph/slotCalculations.ts b/src/renderer/core/canvas/litegraph/slotCalculations.ts index f181267b3..8b66b68a8 100644 --- a/src/renderer/core/canvas/litegraph/slotCalculations.ts +++ b/src/renderer/core/canvas/litegraph/slotCalculations.ts @@ -9,8 +9,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import type { INodeInputSlot, INodeOutputSlot, - Point, - ReadOnlyPoint + Point } from '@/lib/litegraph/src/interfaces' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { isWidgetInputSlot } from '@/lib/litegraph/src/node/slotUtils' @@ -138,7 +137,7 @@ export function getSlotPosition( node: LGraphNode, slotIndex: number, isInput: boolean -): ReadOnlyPoint { +): Point { // Try to get precise position from slot layout (DOM-registered) const slotKey = getSlotKey(String(node.id), slotIndex, isInput) const slotLayout = layoutStore.getSlotLayout(slotKey) diff --git a/src/renderer/core/layout/useTransformState.ts b/src/renderer/core/layout/useTransformState.ts index 515984567..d0ff76d4e 100644 --- a/src/renderer/core/layout/useTransformState.ts +++ b/src/renderer/core/layout/useTransformState.ts @@ -74,6 +74,10 @@ export const useTransformState = () => { // 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' })) @@ -103,15 +107,15 @@ export const useTransformState = () => { * Applies the same transform that LiteGraph uses for rendering. * Essential for positioning Vue components to align with canvas elements. * - * Formula: screen = canvas * scale + offset + * 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.z + camera.x, - y: point.y * camera.z + camera.y + x: (point.x + camera.x) * camera.z, + y: (point.y + camera.y) * camera.z } } @@ -121,15 +125,15 @@ export const useTransformState = () => { * Inverse of canvasToScreen. Useful for hit testing and converting * mouse events back to canvas space. * - * Formula: canvas = (screen - offset) / scale + * 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.x) / camera.z, - y: (point.y - camera.y) / camera.z + x: point.x / camera.z - camera.x, + y: point.y / camera.z - camera.y } } diff --git a/tests-ui/tests/composables/element/useTransformState.test.ts b/tests-ui/tests/composables/element/useTransformState.test.ts index 409ea2e47..e22f34242 100644 --- a/tests-ui/tests/composables/element/useTransformState.test.ts +++ b/tests-ui/tests/composables/element/useTransformState.test.ts @@ -121,24 +121,24 @@ describe('useTransformState', () => { const canvasPoint = { x: 10, y: 20 } const screenPoint = canvasToScreen(canvasPoint) - // screen = canvas * scale + offset - // x: 10 * 2 + 100 = 120 - // y: 20 * 2 + 50 = 90 - expect(screenPoint).toEqual({ x: 120, y: 90 }) + // 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: 100, y: 50 }) + 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: 80, y: 10 }) + expect(screenPoint).toEqual({ x: 180, y: 60 }) }) }) @@ -146,12 +146,12 @@ describe('useTransformState', () => { it('should convert screen coordinates to canvas coordinates', () => { const { screenToCanvas } = transformState - const screenPoint = { x: 120, y: 90 } + const screenPoint = { x: 220, y: 140 } const canvasPoint = screenToCanvas(screenPoint) - // canvas = (screen - offset) / scale - // x: (120 - 100) / 2 = 10 - // y: (90 - 50) / 2 = 20 + // canvas = screen / scale - offset + // x: 220 / 2 - 100 = 10 + // y: 140 / 2 - 50 = 20 expect(canvasPoint).toEqual({ x: 10, y: 20 }) }) @@ -183,11 +183,11 @@ describe('useTransformState', () => { const nodeSize = [200, 100] const bounds = getNodeScreenBounds(nodePos, nodeSize) - // Top-left: canvasToScreen(10, 20) = (120, 90) + // Top-left: canvasToScreen(10, 20) = (220, 140) // Width: 200 * 2 = 400 // Height: 100 * 2 = 200 - expect(bounds.x).toBe(120) - expect(bounds.y).toBe(90) + expect(bounds.x).toBe(220) + expect(bounds.y).toBe(140) expect(bounds.width).toBe(400) expect(bounds.height).toBe(200) }) @@ -288,14 +288,14 @@ describe('useTransformState', () => { // topLeft in screen: (-200, -120) // bottomRight in screen: (1200, 720) - // Convert to canvas coordinates: - // topLeft: ((-200 - 100) / 2, (-120 - 50) / 2) = (-150, -85) - // bottomRight: ((1200 - 100) / 2, (720 - 50) / 2) = (550, 335) + // 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(-150) - expect(bounds.y).toBe(-85) - expect(bounds.width).toBe(700) // 550 - (-150) - expect(bounds.height).toBe(420) // 335 - (-85) + 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', () => { @@ -305,8 +305,8 @@ describe('useTransformState', () => { const bounds = getViewportBounds(viewport, 0) // No margin, so viewport bounds are exact - expect(bounds.x).toBe(-50) // (0 - 100) / 2 - expect(bounds.y).toBe(-25) // (0 - 50) / 2 + 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 })