import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react" import type { CanvasPoint, CanvasSize } from "@/lib/canvas/types" import type { CanvasBox } from "./types" const MIN_SIZE = 160 const HANDLE_OFFSETS = ["top-left", "top-right", "bottom-left", "bottom-right"] as const type DragMode = "move" | "resize" type DragState = { id: string mode: DragMode handle?: (typeof HANDLE_OFFSETS)[number] origin: CanvasPoint startPosition: CanvasPoint startSize: CanvasSize latestRect?: { position: CanvasPoint size: CanvasSize } } export type CanvasBoardProps = { boxes: CanvasBox[] selectedBoxId: string | null onSelect: (id: string | null) => void onRectChange: (id: string, rect: { position: CanvasPoint; size: CanvasSize }) => void onRectCommit: (id: string, rect: { position: CanvasPoint; size: CanvasSize }) => void className?: string } export const CanvasBoard = ({ boxes, selectedBoxId, onSelect, onRectChange, onRectCommit, className, }: CanvasBoardProps) => { const [dragState, setDragState] = useState(null) const dragStateRef = useRef(null) const startDrag = useCallback( ( box: CanvasBox, mode: DragMode, origin: CanvasPoint, handle?: DragState["handle"], ) => { const state: DragState = { id: box.id, mode, handle, origin, startPosition: box.position, startSize: { width: box.width, height: box.height }, } dragStateRef.current = state setDragState(state) }, [], ) useEffect(() => { if (!dragState) return const handlePointerMove = (event: PointerEvent) => { const state = dragStateRef.current if (!state) return event.preventDefault() const dx = event.clientX - state.origin.x const dy = event.clientY - state.origin.y let nextPosition = state.startPosition let nextSize = state.startSize if (state.mode === "move") { nextPosition = { x: Math.max(0, Math.round(state.startPosition.x + dx)), y: Math.max(0, Math.round(state.startPosition.y + dy)), } } else if (state.mode === "resize" && state.handle) { const { size, position } = calculateResize(state, dx, dy) nextSize = size nextPosition = position } const rect = { position: nextPosition, size: nextSize } dragStateRef.current = { ...state, latestRect: rect } onRectChange(state.id, rect) } const handlePointerUp = () => { const state = dragStateRef.current if (state?.latestRect) { onRectCommit(state.id, state.latestRect) } dragStateRef.current = null setDragState(null) } window.addEventListener("pointermove", handlePointerMove) window.addEventListener("pointerup", handlePointerUp) return () => { window.removeEventListener("pointermove", handlePointerMove) window.removeEventListener("pointerup", handlePointerUp) } }, [dragState, onRectChange, onRectCommit]) const boardSize = useMemo(() => { const maxX = Math.max(...boxes.map((box) => box.position.x + box.width), 1600) const maxY = Math.max(...boxes.map((box) => box.position.y + box.height), 900) return { width: maxX + 480, height: maxY + 480 } }, [boxes]) return (
onSelect(null)} style={{ backgroundSize: "32px 32px" }} >
{boxes.map((box) => { const selected = box.id === selectedBoxId return (
{ event.stopPropagation() onSelect(box.id) }} onPointerDown={(event) => { if (event.button !== 0) return event.preventDefault() event.stopPropagation() startDrag(box, "move", { x: event.clientX, y: event.clientY }) }} style={{ transform: `translate(${box.position.x}px, ${box.position.y}px)`, width: box.width, height: box.height, userSelect: "none", cursor: dragState?.id === box.id ? "grabbing" : "grab", }} >
{box.name} {box.width}Ă—{box.height}
{box.imageData ? ( {box.name} ) : (
{box.prompt ? box.prompt : "Add a prompt and generate to see your artwork here."}
)} {box.isGenerating && (
Generating…
)}
{selected && ( <> {HANDLE_OFFSETS.map((handle) => ( { event.preventDefault() event.stopPropagation() startDrag(box, "resize", { x: event.clientX, y: event.clientY }, handle) }} /> ))} )}
) })}
) } type ResizeHandleProps = { handle: (typeof HANDLE_OFFSETS)[number] onPointerDown: (event: ReactPointerEvent) => void } const ResizeHandle = ({ handle, onPointerDown }: ResizeHandleProps) => { const positions: Record = { "top-left": "top-0 left-0 -translate-x-1/2 -translate-y-1/2", "top-right": "top-0 right-0 translate-x-1/2 -translate-y-1/2", "bottom-left": "bottom-0 left-0 -translate-x-1/2 translate-y-1/2", "bottom-right": "bottom-0 right-0 translate-x-1/2 translate-y-1/2", } const cursors: Record = { "top-left": "nwse-resize", "top-right": "nesw-resize", "bottom-left": "nesw-resize", "bottom-right": "nwse-resize", } return (
) } function calculateResize( state: DragState, dx: number, dy: number, ): { position: CanvasPoint; size: CanvasSize } { let { width, height } = state.startSize let { x, y } = state.startPosition const applyWidth = (next: number, anchorRight: boolean) => { const clamped = Math.max(MIN_SIZE, Math.round(next)) if (anchorRight) { x = state.startPosition.x + (state.startSize.width - clamped) } width = clamped } const applyHeight = (next: number, anchorBottom: boolean) => { const clamped = Math.max(MIN_SIZE, Math.round(next)) if (anchorBottom) { y = state.startPosition.y + (state.startSize.height - clamped) } height = clamped } switch (state.handle) { case "top-left": applyWidth(state.startSize.width - dx, true) applyHeight(state.startSize.height - dy, true) break case "top-right": applyWidth(state.startSize.width + dx, false) applyHeight(state.startSize.height - dy, true) break case "bottom-left": applyWidth(state.startSize.width - dx, true) applyHeight(state.startSize.height + dy, false) break case "bottom-right": applyWidth(state.startSize.width + dx, false) applyHeight(state.startSize.height + dy, false) break default: break } return { position: { x: Math.max(0, x), y: Math.max(0, y) }, size: { width, height }, } }