From 23610f4f7b5785978c74ffa6cd84f578db9577e2 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Mon, 13 Jan 2025 16:59:47 -0800 Subject: [PATCH] Added coordinates library --- app/w/[id]/workflow.tsx | 66 ++++++----- .../connection/connection-point.tsx | 34 ++++++ .../workflow-block/workflow-block.tsx | 5 + app/w/lib/coordinates.ts | 108 ++++++++++++++++++ 4 files changed, 182 insertions(+), 31 deletions(-) create mode 100644 app/w/lib/coordinates.ts diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx index 9504fdef9c..a7079c3415 100644 --- a/app/w/[id]/workflow.tsx +++ b/app/w/[id]/workflow.tsx @@ -4,6 +4,7 @@ import { useState, useCallback, useEffect } from 'react' import { BlockConfig } from '../components/blocks/types/block' import { WorkflowBlock } from '../components/blocks/components/workflow-block/workflow-block' import { getBlock } from '../components/blocks/configs' +import { CoordinateTransformer } from '../lib/coordinates' interface WorkflowBlock { id: string @@ -15,7 +16,7 @@ interface WorkflowBlock { const ZOOM_SPEED = 0.005 const MIN_ZOOM = 0.5 const MAX_ZOOM = 2 -const CANVAS_SIZE = 5000 // 5000px x 5000px virtual canvas +const CANVAS_SIZE = 5000 export default function Workflow() { const [blocks, setBlocks] = useState([]) @@ -26,33 +27,28 @@ export default function Workflow() { // Initialize pan position after mount useEffect(() => { - const viewportWidth = window.innerWidth - 344 // Account for sidebar - const viewportHeight = window.innerHeight - 56 // Account for header + const { width, height } = CoordinateTransformer.getViewportDimensions() setPan({ - x: (viewportWidth - CANVAS_SIZE) / 2, - y: (viewportHeight - CANVAS_SIZE) / 2, + x: (width - CANVAS_SIZE) / 2, + y: (height - CANVAS_SIZE) / 2, }) }, []) const constrainPan = useCallback( (newPan: { x: number; y: number }, currentZoom: number) => { - // Calculate the visible area dimensions - const viewportWidth = window.innerWidth - const viewportHeight = window.innerHeight - 56 // Adjust for header height + const { width, height } = CoordinateTransformer.getViewportDimensions() // Calculate the scaled canvas size const scaledCanvasWidth = CANVAS_SIZE * currentZoom const scaledCanvasHeight = CANVAS_SIZE * currentZoom // Calculate the maximum allowed pan values - const maxX = 0 - const minX = viewportWidth - scaledCanvasWidth - const maxY = 0 - const minY = viewportHeight - scaledCanvasHeight + const minX = Math.min(0, width - scaledCanvasWidth) + const minY = Math.min(0, height - scaledCanvasHeight) return { - x: Math.min(maxX, Math.max(minX, newPan.x)), - y: Math.min(maxY, Math.max(minY, newPan.y)), + x: Math.min(0, Math.max(minX, newPan.x)), + y: Math.min(0, Math.max(minY, newPan.y)), } }, [] @@ -75,19 +71,24 @@ export default function Workflow() { return } - const rect = e.currentTarget.getBoundingClientRect() - const mouseX = e.clientX - rect.left - const mouseY = e.clientY - rect.top + const canvasElement = e.currentTarget as HTMLElement + const dropPoint = { + x: e.clientX, + y: e.clientY, + } - const x = mouseX / zoom - const y = mouseY / zoom + // Convert drop coordinates to canvas space + const canvasPoint = CoordinateTransformer.viewportToCanvas( + CoordinateTransformer.clientToViewport(dropPoint), + canvasElement + ) setBlocks((prev) => [ ...prev, { id: crypto.randomUUID(), type, - position: { x, y }, + position: canvasPoint, config: blockConfig, }, ]) @@ -98,12 +99,10 @@ export default function Workflow() { const handleWheel = useCallback( (e: React.WheelEvent) => { - // Prevent browser zooming if (e.ctrlKey || e.metaKey) { e.preventDefault() const delta = -e.deltaY * ZOOM_SPEED setZoom((prevZoom) => { - // If we're at max/min zoom and trying to zoom further, return current zoom if ( (prevZoom >= MAX_ZOOM && delta > 0) || (prevZoom <= MIN_ZOOM && delta < 0) @@ -114,12 +113,10 @@ export default function Workflow() { MAX_ZOOM, Math.max(MIN_ZOOM, prevZoom + delta) ) - // Adjust pan when zooming to keep the point under cursor fixed setPan((prevPan) => constrainPan(prevPan, newZoom)) return newZoom }) } else { - // Regular scrolling for pan setPan((prevPan) => constrainPan( { @@ -137,9 +134,15 @@ export default function Workflow() { const handleMouseDown = useCallback( (e: React.MouseEvent) => { if (e.button === 1 || e.button === 0) { - // Middle mouse or left click setIsPanning(true) - setStartPanPos({ x: e.clientX - pan.x, y: e.clientY - pan.y }) + const viewportPoint = CoordinateTransformer.clientToViewport({ + x: e.clientX, + y: e.clientY, + }) + setStartPanPos({ + x: viewportPoint.x - pan.x, + y: viewportPoint.y - pan.y, + }) } }, [pan] @@ -148,11 +151,15 @@ export default function Workflow() { const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (isPanning) { + const viewportPoint = CoordinateTransformer.clientToViewport({ + x: e.clientX, + y: e.clientY, + }) setPan((prevPan) => constrainPan( { - x: e.clientX - startPanPos.x, - y: e.clientY - startPanPos.y, + x: viewportPoint.x - startPanPos.x, + y: viewportPoint.y - startPanPos.y, }, zoom ) @@ -166,7 +173,6 @@ export default function Workflow() { setIsPanning(false) }, []) - // Add this useEffect to prevent browser zoom useEffect(() => { const preventDefaultZoom = (e: WheelEvent) => { if (e.ctrlKey || e.metaKey) { @@ -178,7 +184,6 @@ export default function Workflow() { return () => document.removeEventListener('wheel', preventDefaultZoom) }, []) - // Add this new function to handle block position updates const updateBlockPosition = useCallback( (id: string, newPosition: { x: number; y: number }) => { setBlocks((prevBlocks) => @@ -213,7 +218,6 @@ export default function Workflow() { onDrop={handleDrop} > {blocks.map((block, index) => { - // Count how many blocks of this type appear before the current index const typeCount = blocks .slice(0, index + 1) .filter((b) => b.type === block.type).length diff --git a/app/w/components/blocks/components/workflow-block/components/connection/connection-point.tsx b/app/w/components/blocks/components/workflow-block/components/connection/connection-point.tsx index 0e6b1d47c4..b75afb908e 100644 --- a/app/w/components/blocks/components/workflow-block/components/connection/connection-point.tsx +++ b/app/w/components/blocks/components/workflow-block/components/connection/connection-point.tsx @@ -1,12 +1,46 @@ import { cn } from '@/lib/utils' +import { CoordinateTransformer } from '@/app/w/lib/coordinates' interface ConnectionPointProps { position: 'top' | 'bottom' } export function ConnectionPoint({ position }: ConnectionPointProps) { + const handleClick = (e: React.MouseEvent) => { + const canvasElement = e.currentTarget.closest( + '[style*="transform"]' + ) as HTMLElement + if (!canvasElement) return + + const elementPosition = CoordinateTransformer.getElementCanvasPosition( + e.currentTarget, + canvasElement + ) + + // Test distance calculation + const testPoint = { + x: e.clientX + 100, + y: e.clientY + 100, + } + + const distance = CoordinateTransformer.getTransformedDistance( + { x: e.clientX, y: e.clientY }, + testPoint, + canvasElement + ) + + console.log({ + viewport: CoordinateTransformer.getViewportDimensions(), + canvasTransform: CoordinateTransformer.getCanvasTransform(canvasElement), + elementPosition, + transformedDistance: distance, + }) + } + return (
{ + // Don't handle drag if clicking on a connection point + if ((e.target as HTMLElement).closest('[data-connection-point]')) { + return + } + e.stopPropagation() setIsDragging(true) diff --git a/app/w/lib/coordinates.ts b/app/w/lib/coordinates.ts new file mode 100644 index 0000000000..bdac09db8e --- /dev/null +++ b/app/w/lib/coordinates.ts @@ -0,0 +1,108 @@ +/** + * Represents a point in 2D space + */ +interface Point { + x: number + y: number +} + +/** + * Represents the current transform state of the canvas + */ +interface CanvasTransform { + scale: number + translateX: number + translateY: number +} + +/** + * Utility class for handling coordinate transformations in the workflow + */ +export class CoordinateTransformer { + private static readonly SIDEBAR_WIDTH = 344 + private static readonly HEADER_HEIGHT = 56 + + /** + * Gets the current transform state from a canvas element + */ + static getCanvasTransform(element: HTMLElement): CanvasTransform { + const matrix = new DOMMatrix(window.getComputedStyle(element).transform) + return { + scale: matrix.a, + translateX: matrix.e, + translateY: matrix.f, + } + } + + /** + * Converts viewport coordinates to canvas coordinates + */ + static viewportToCanvas(point: Point, canvasElement: HTMLElement): Point { + const matrix = new DOMMatrix(window.getComputedStyle(canvasElement).transform) + const domPoint = new DOMPoint(point.x, point.y) + const transformed = domPoint.matrixTransform(matrix.inverse()) + + return { + x: transformed.x, + y: transformed.y, + } + } + + /** + * Converts canvas coordinates to viewport coordinates + */ + static canvasToViewport(point: Point, canvasElement: HTMLElement): Point { + const matrix = new DOMMatrix(window.getComputedStyle(canvasElement).transform) + const domPoint = new DOMPoint(point.x, point.y) + const transformed = domPoint.matrixTransform(matrix) + + return { + x: transformed.x, + y: transformed.y, + } + } + + /** + * Converts client coordinates to viewport coordinates by removing sidebar and header offsets + */ + static clientToViewport(point: Point): Point { + return { + x: point.x - this.SIDEBAR_WIDTH, + y: point.y - this.HEADER_HEIGHT, + } + } + + /** + * Gets the viewport dimensions + */ + static getViewportDimensions(): { width: number; height: number } { + return { + width: window.innerWidth - this.SIDEBAR_WIDTH, + height: window.innerHeight - this.HEADER_HEIGHT, + } + } + + /** + * Gets the relative position of an element within the canvas + */ + static getElementCanvasPosition(element: HTMLElement, canvasElement: HTMLElement): Point { + const rect = element.getBoundingClientRect() + const point = { + x: rect.left, + y: rect.top, + } + + return this.viewportToCanvas(this.clientToViewport(point), canvasElement) + } + + /** + * Calculates the distance between two points accounting for canvas transform + */ + static getTransformedDistance(point1: Point, point2: Point, canvasElement: HTMLElement): Point { + const transform = this.getCanvasTransform(canvasElement) + return { + x: (point2.x - point1.x) / transform.scale, + y: (point2.y - point1.y) / transform.scale, + } + } +} \ No newline at end of file