mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 06:58:07 -05:00
Added coordinates library
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
108
app/w/lib/coordinates.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user