import { useEffect, useRef, useState, useCallback } from 'react'; import { Stage, Layer, Group } from 'react-konva'; import Konva from 'konva'; import { PlusIcon, MinusIcon, ArrowPathIcon, LockClosedIcon } from '@heroicons/react/24/solid'; import type { PesPatternData } from '../utils/pystitchConverter'; import type { SewingProgress, MachineInfo } from '../types/machine'; import { calculateInitialScale } from '../utils/konvaRenderers'; import { Grid, Origin, Hoop, Stitches, PatternBounds, CurrentPosition } from './KonvaComponents'; interface PatternCanvasProps { pesData: PesPatternData | null; sewingProgress: SewingProgress | null; machineInfo: MachineInfo | null; initialPatternOffset?: { x: number; y: number }; onPatternOffsetChange?: (offsetX: number, offsetY: number) => void; patternUploaded?: boolean; isUploading?: boolean; } export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPatternOffset, onPatternOffsetChange, patternUploaded = false, isUploading = false }: PatternCanvasProps) { const containerRef = useRef(null); const stageRef = useRef(null); const [stagePos, setStagePos] = useState({ x: 0, y: 0 }); const [stageScale, setStageScale] = useState(1); const [patternOffset, setPatternOffset] = useState(initialPatternOffset || { x: 0, y: 0 }); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const initialScaleRef = useRef(1); // Update pattern offset when initialPatternOffset changes useEffect(() => { if (initialPatternOffset) { setPatternOffset(initialPatternOffset); console.log('[PatternCanvas] Restored pattern offset:', initialPatternOffset); } }, [initialPatternOffset]); // Track container size useEffect(() => { if (!containerRef.current) return; const updateSize = () => { if (containerRef.current) { const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; setContainerSize({ width, height }); } }; // Initial size updateSize(); // Watch for resize const resizeObserver = new ResizeObserver(updateSize); resizeObserver.observe(containerRef.current); return () => resizeObserver.disconnect(); }, []); // Calculate initial scale when pattern or hoop changes useEffect(() => { if (!pesData || containerSize.width === 0) return; const { bounds } = pesData; const viewWidth = machineInfo ? machineInfo.maxWidth : bounds.maxX - bounds.minX; const viewHeight = machineInfo ? machineInfo.maxHeight : bounds.maxY - bounds.minY; const initialScale = calculateInitialScale(containerSize.width, containerSize.height, viewWidth, viewHeight); initialScaleRef.current = initialScale; // Set initial scale and center position when pattern loads setStageScale(initialScale); setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 }); }, [pesData, machineInfo, containerSize]); // Wheel zoom handler const handleWheel = useCallback((e: Konva.KonvaEventObject) => { e.evt.preventDefault(); const stage = e.target.getStage(); if (!stage) return; const oldScale = stage.scaleX(); const pointer = stage.getPointerPosition(); if (!pointer) return; const scaleBy = 1.1; const direction = e.evt.deltaY > 0 ? -1 : 1; let newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy; // Apply constraints newScale = Math.max(0.1, Math.min(10, newScale)); // Zoom towards pointer const mousePointTo = { x: (pointer.x - stage.x()) / oldScale, y: (pointer.y - stage.y()) / oldScale, }; const newPos = { x: pointer.x - mousePointTo.x * newScale, y: pointer.y - mousePointTo.y * newScale, }; setStageScale(newScale); setStagePos(newPos); }, []); // Zoom control handlers const handleZoomIn = useCallback(() => { const oldScale = stageScale; const newScale = Math.min(oldScale * 1.2, 10); // Zoom towards center of viewport const centerX = containerSize.width / 2; const centerY = containerSize.height / 2; const mousePointTo = { x: (centerX - stagePos.x) / oldScale, y: (centerY - stagePos.y) / oldScale, }; const newPos = { x: centerX - mousePointTo.x * newScale, y: centerY - mousePointTo.y * newScale, }; setStageScale(newScale); setStagePos(newPos); }, [stageScale, stagePos, containerSize]); const handleZoomOut = useCallback(() => { const oldScale = stageScale; const newScale = Math.max(oldScale / 1.2, 0.1); // Zoom towards center of viewport const centerX = containerSize.width / 2; const centerY = containerSize.height / 2; const mousePointTo = { x: (centerX - stagePos.x) / oldScale, y: (centerY - stagePos.y) / oldScale, }; const newPos = { x: centerX - mousePointTo.x * newScale, y: centerY - mousePointTo.y * newScale, }; setStageScale(newScale); setStagePos(newPos); }, [stageScale, stagePos, containerSize]); const handleZoomReset = useCallback(() => { const initialScale = initialScaleRef.current; setStageScale(initialScale); setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 }); }, [containerSize]); // Pattern drag handlers const handlePatternDragEnd = useCallback((e: Konva.KonvaEventObject) => { const newOffset = { x: e.target.x(), y: e.target.y(), }; setPatternOffset(newOffset); if (onPatternOffsetChange) { onPatternOffsetChange(newOffset.x, newOffset.y); } }, [onPatternOffsetChange]); return (

Pattern Preview

{containerSize.width > 0 && ( { if (stageRef.current) { stageRef.current.container().style.cursor = 'grabbing'; } }} onDragEnd={() => { if (stageRef.current) { stageRef.current.container().style.cursor = 'grab'; } }} ref={(node) => { stageRef.current = node; if (node) { node.container().style.cursor = 'grab'; } }} > {/* Background layer: grid, origin, hoop */} {pesData && ( <> {machineInfo && } )} {/* Pattern layer: draggable stitches and bounds */} {pesData && ( { const stage = e.target.getStage(); if (stage && !patternUploaded && !isUploading) stage.container().style.cursor = 'move'; }} onMouseLeave={(e) => { const stage = e.target.getStage(); if (stage && !patternUploaded && !isUploading) stage.container().style.cursor = 'grab'; }} > )} {/* Current position layer */} {pesData && sewingProgress && sewingProgress.currentStitch > 0 && ( )} )} {/* Placeholder overlay when no pattern is loaded */} {!pesData && (
Load a PES file to preview the pattern
)} {/* Pattern info overlays */} {pesData && ( <> {/* Thread Legend Overlay */}

Threads

{pesData.threads.map((thread, index) => (
Thread {index + 1}
))}
{/* Pattern Dimensions Overlay */}
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '} {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
{/* Pattern Offset Indicator */}
Pattern Position:
{patternUploaded && (
LOCKED
)}
X: {(patternOffset.x / 10).toFixed(1)}mm, Y: {(patternOffset.y / 10).toFixed(1)}mm
{patternUploaded ? 'Pattern locked • Drag background to pan' : 'Drag pattern to move • Drag background to pan'}
{/* Zoom Controls Overlay */}
{Math.round(stageScale * 100)}%
)}
); }