Added coordinates library

This commit is contained in:
Emir Karabeg
2025-01-13 16:59:47 -08:00
parent 57b1fce88c
commit 23610f4f7b
4 changed files with 182 additions and 31 deletions

View File

@@ -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<WorkflowBlock[]>([])
@@ -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

View File

@@ -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<HTMLDivElement>) => {
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 (
<div
data-connection-point
onClick={handleClick}
className={cn(
'absolute left-1/2 -translate-x-1/2 w-3 h-3',
'bg-white rounded-full border opacity-0 group-hover:opacity-100',

View File

@@ -54,6 +54,11 @@ export function WorkflowBlock({
const handleMouseDown = useCallback(
(e: MouseEvent) => {
// Don't handle drag if clicking on a connection point
if ((e.target as HTMLElement).closest('[data-connection-point]')) {
return
}
e.stopPropagation()
setIsDragging(true)

108
app/w/lib/coordinates.ts Normal file
View File

@@ -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,
}
}
}