/** * DesignCanvas — View 2 container. * * Layout: top CanvasToolbar, left canvas (KonvaStage), right panel area * containing AlignmentBar, ObjectPanel, and ShapeProperties. * Manages tool state, artboard setup flow, zoom, grid, and imported SVG loading. */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type Konva from 'konva'; import type { TraceMetadata } from '../types/engine'; import type { ArtboardConfig, CanvasObject } from '../types/canvas'; import { useCanvasState } from '../hooks/useCanvasState'; import ArtboardSetup from '../components/canvas/ArtboardSetup'; import KonvaStage from '../components/canvas/KonvaStage'; import type { CanvasTool } from '../components/canvas/KonvaStage'; import CanvasToolbar from '../components/canvas/CanvasToolbar'; import ObjectPanel from '../components/canvas/ObjectPanel'; import AlignmentBar from '../components/canvas/AlignmentBar'; import ShapeProperties from '../components/canvas/ShapeProperties'; import { toPx } from '../utils/artboardShapes'; import styles from './DesignCanvas.module.css'; interface DesignCanvasProps { svgData: string | null; traceMetadata: TraceMetadata | null; } export default function DesignCanvas({ svgData, traceMetadata, }: DesignCanvasProps) { const { state, addObject, removeObject: _removeObject, updateObject, selectObjects, deselectAll, reorderObject, toggleVisibility, toggleLock, setArtboard, undo, redo, canUndo, canRedo, } = useCanvasState(traceMetadata); const [activeTool, setActiveTool] = useState('pointer'); const [showArtboardSetup, setShowArtboardSetup] = useState(true); const [svgImported, setSvgImported] = useState(false); const [showGrid, setShowGrid] = useState(false); const [zoomLevel, setZoomLevel] = useState(1); const stageRef = useRef(null); const canvasContainerRef = useRef(null); const [stageSize, setStageSize] = useState({ width: 800, height: 600 }); // -- Resize observer for canvas container -------------------------------- useEffect(() => { const container = canvasContainerRef.current; if (!container) return; const updateSize = () => { setStageSize({ width: container.clientWidth, height: container.clientHeight, }); }; updateSize(); const observer = new ResizeObserver(updateSize); observer.observe(container); return () => observer.disconnect(); }, [showArtboardSetup]); // -- Artboard setup ------------------------------------------------------- const handleArtboardConfirm = useCallback( (config: ArtboardConfig) => { setArtboard(config); setShowArtboardSetup(false); }, [setArtboard], ); // -- Import SVG from View 1 ----------------------------------------------- useEffect(() => { if (!svgData || svgImported || !state.artboard) return; const blob = new Blob([svgData], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); // Load the image to get natural dimensions const img = new window.Image(); img.onload = () => { const artW = toPx(state.artboard!.width, state.artboard!.unit); const artH = toPx(state.artboard!.height, state.artboard!.unit); // Scale SVG to fit within artboard const scale = Math.min(artW / img.width, artH / img.height, 1); const scaledW = img.width * scale; const scaledH = img.height * scale; const imageObj: CanvasObject = { type: 'image', id: `imported-svg-${Date.now()}`, name: 'Imported SVG', x: (artW - scaledW) / 2, y: (artH - scaledH) / 2, width: scaledW, height: scaledH, rotation: 0, visible: true, locked: false, opacity: 1, src: url, }; addObject(imageObj); setSvgImported(true); }; img.src = url; }, [svgData, svgImported, state.artboard, addObject]); // -- Selection handling --------------------------------------------------- const handleSelect = useCallback( (ids: string[], additive: boolean) => { if (additive) { const current = new Set(state.selectedIds); for (const id of ids) { if (current.has(id)) { current.delete(id); } else { current.add(id); } } selectObjects([...current]); } else { selectObjects(ids); } }, [state.selectedIds, selectObjects], ); // -- Rename handler (dispatches updateObject with name change) ----------- const handleRename = useCallback( (id: string, newName: string) => { updateObject(id, { name: newName }); }, [updateObject], ); // -- Zoom controls -------------------------------------------------------- const handleZoomIn = useCallback(() => { setZoomLevel((z) => Math.min(z + 0.25, 4)); }, []); const handleZoomOut = useCallback(() => { setZoomLevel((z) => Math.max(z - 0.25, 0.25)); }, []); const handleZoomFit = useCallback(() => { setZoomLevel(1); }, []); // -- Selected object for properties panel --------------------------------- const selectedObject = useMemo(() => { if (state.selectedIds.length !== 1) return null; return state.objects.find((o) => o.id === state.selectedIds[0]) ?? null; }, [state.objects, state.selectedIds]); // -- Render --------------------------------------------------------------- if (showArtboardSetup) { return ; } return (
{/* Top toolbar */}
setShowGrid((g) => !g)} zoomLevel={zoomLevel} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onZoomFit={handleZoomFit} />
{/* Main area: canvas + right panel */}
{/* Canvas */}
{/* Right panel: alignment bar, object panel, shape properties */}
{/* Alignment bar — visible when selection exists */} {/* Object / layer panel */} {/* Shape properties — visible when exactly 1 object selected */} {selectedObject && ( )}
); }