From a97439da7b744c8f8cdc0dfc6b604e87c84c8298 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Thu, 25 Dec 2025 21:37:34 +0100 Subject: [PATCH] fix: Refactor KonvaComponents and remove unused rotation code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved KonvaComponents.tsx into PatternCanvas subfolder for better organization and removed the unused RotationHandle component (143 lines) that was replaced by Konva's native Transformer. Updated import paths accordingly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/PatternCanvas.tsx | 785 ------------------ .../{ => PatternCanvas}/KonvaComponents.tsx | 157 +--- .../PatternCanvas/PatternCanvas.tsx | 2 +- src/components/PatternCanvas/PatternLayer.tsx | 2 +- 4 files changed, 8 insertions(+), 938 deletions(-) delete mode 100644 src/components/PatternCanvas.tsx rename src/components/{ => PatternCanvas}/KonvaComponents.tsx (60%) diff --git a/src/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx deleted file mode 100644 index 5ad18e4..0000000 --- a/src/components/PatternCanvas.tsx +++ /dev/null @@ -1,785 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from "react"; -import { useShallow } from "zustand/react/shallow"; -import { useMachineStore, usePatternUploaded } from "../stores/useMachineStore"; -import { usePatternStore } from "../stores/usePatternStore"; -import { Stage, Layer, Group, Transformer } from "react-konva"; -import Konva from "konva"; -import type { KonvaEventObject } from "konva/lib/Node"; -import { - PlusIcon, - MinusIcon, - ArrowPathIcon, - LockClosedIcon, - PhotoIcon, - ArrowsPointingInIcon, -} from "@heroicons/react/24/solid"; -import type { PesPatternData } from "../formats/import/pesImporter"; -import { calculateInitialScale } from "../utils/konvaRenderers"; -import { - Grid, - Origin, - Hoop, - Stitches, - PatternBounds, - CurrentPosition, -} from "./KonvaComponents"; -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; - -export function PatternCanvas() { - // Machine store - const { sewingProgress, machineInfo, isUploading } = useMachineStore( - useShallow((state) => ({ - sewingProgress: state.sewingProgress, - machineInfo: state.machineInfo, - isUploading: state.isUploading, - })), - ); - - // Pattern store - const { - pesData, - patternOffset: initialPatternOffset, - patternRotation: initialPatternRotation, - uploadedPesData, - uploadedPatternOffset: initialUploadedPatternOffset, - setPatternOffset, - setPatternRotation, - } = usePatternStore( - useShallow((state) => ({ - pesData: state.pesData, - patternOffset: state.patternOffset, - patternRotation: state.patternRotation, - uploadedPesData: state.uploadedPesData, - uploadedPatternOffset: state.uploadedPatternOffset, - setPatternOffset: state.setPatternOffset, - setPatternRotation: state.setPatternRotation, - })), - ); - - // Derived state: pattern is uploaded if machine has pattern info - const patternUploaded = usePatternUploaded(); - const containerRef = useRef(null); - const stageRef = useRef(null); - const patternGroupRef = useRef(null); - const transformerRef = useRef(null); - - const [stagePos, setStagePos] = useState({ x: 0, y: 0 }); - const [stageScale, setStageScale] = useState(1); - const [localPatternOffset, setLocalPatternOffset] = useState( - initialPatternOffset || { x: 0, y: 0 }, - ); - const [localPatternRotation, setLocalPatternRotation] = useState( - initialPatternRotation || 0, - ); - const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); - const initialScaleRef = useRef(1); - const prevPesDataRef = useRef(null); - - // Update pattern offset when initialPatternOffset changes - if ( - initialPatternOffset && - (localPatternOffset.x !== initialPatternOffset.x || - localPatternOffset.y !== initialPatternOffset.y) - ) { - setLocalPatternOffset(initialPatternOffset); - console.log( - "[PatternCanvas] Restored pattern offset:", - initialPatternOffset, - ); - } - - // Update pattern rotation when initialPatternRotation changes - if ( - initialPatternRotation !== undefined && - localPatternRotation !== initialPatternRotation - ) { - setLocalPatternRotation(initialPatternRotation); - } - - // 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 and store initial scale when pattern or hoop changes - useEffect(() => { - // Use whichever pattern is available (uploaded or original) - const currentPattern = uploadedPesData || pesData; - if (!currentPattern || containerSize.width === 0) { - prevPesDataRef.current = null; - return; - } - - // Only recalculate if pattern changed - if (prevPesDataRef.current !== currentPattern) { - prevPesDataRef.current = currentPattern; - - const { bounds } = currentPattern; - 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; - - // Reset view when pattern changes - // eslint-disable-next-line react-hooks/set-state-in-effect - setStageScale(initialScale); - setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 }); - } - }, [pesData, uploadedPesData, machineInfo, containerSize]); - - // Wheel zoom handler - const handleWheel = useCallback((e: Konva.KonvaEventObject) => { - e.evt.preventDefault(); - - const stage = e.target.getStage(); - if (!stage) return; - - const pointer = stage.getPointerPosition(); - if (!pointer) return; - - const scaleBy = 1.1; - const direction = e.evt.deltaY > 0 ? -1 : 1; - - setStageScale((oldScale) => { - const newScale = Math.max( - 0.1, - Math.min(direction > 0 ? oldScale * scaleBy : oldScale / scaleBy, 2), - ); - - // Zoom towards pointer - setStagePos((prevPos) => { - const mousePointTo = { - x: (pointer.x - prevPos.x) / oldScale, - y: (pointer.y - prevPos.y) / oldScale, - }; - - return { - x: pointer.x - mousePointTo.x * newScale, - y: pointer.y - mousePointTo.y * newScale, - }; - }); - - return newScale; - }); - }, []); - - // Zoom control handlers - const handleZoomIn = useCallback(() => { - setStageScale((oldScale) => { - const newScale = Math.max(0.1, Math.min(oldScale * 1.2, 2)); - - // Zoom towards center of viewport - setStagePos((prevPos) => { - const centerX = containerSize.width / 2; - const centerY = containerSize.height / 2; - - const mousePointTo = { - x: (centerX - prevPos.x) / oldScale, - y: (centerY - prevPos.y) / oldScale, - }; - - return { - x: centerX - mousePointTo.x * newScale, - y: centerY - mousePointTo.y * newScale, - }; - }); - - return newScale; - }); - }, [containerSize]); - - const handleZoomOut = useCallback(() => { - setStageScale((oldScale) => { - const newScale = Math.max(0.1, Math.min(oldScale / 1.2, 2)); - - // Zoom towards center of viewport - setStagePos((prevPos) => { - const centerX = containerSize.width / 2; - const centerY = containerSize.height / 2; - - const mousePointTo = { - x: (centerX - prevPos.x) / oldScale, - y: (centerY - prevPos.y) / oldScale, - }; - - return { - x: centerX - mousePointTo.x * newScale, - y: centerY - mousePointTo.y * newScale, - }; - }); - - return newScale; - }); - }, [containerSize]); - - const handleZoomReset = useCallback(() => { - const initialScale = initialScaleRef.current; - setStageScale(initialScale); - setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 }); - }, [containerSize]); - - const handleCenterPattern = useCallback(() => { - if (!pesData) return; - - const { bounds } = pesData; - const centerOffsetX = -(bounds.minX + bounds.maxX) / 2; - const centerOffsetY = -(bounds.minY + bounds.maxY) / 2; - - setLocalPatternOffset({ x: centerOffsetX, y: centerOffsetY }); - setPatternOffset(centerOffsetX, centerOffsetY); - }, [pesData, setPatternOffset]); - - // Pattern drag handlers - const handlePatternDragEnd = useCallback( - (e: Konva.KonvaEventObject) => { - const newOffset = { - x: e.target.x(), - y: e.target.y(), - }; - setLocalPatternOffset(newOffset); - setPatternOffset(newOffset.x, newOffset.y); - }, - [setPatternOffset], - ); - - // Attach/detach transformer based on state - const attachTransformer = useCallback(() => { - if (!transformerRef.current || !patternGroupRef.current) { - console.log( - "[PatternCanvas] Cannot attach transformer - refs not ready", - { - hasTransformer: !!transformerRef.current, - hasPatternGroup: !!patternGroupRef.current, - }, - ); - return; - } - - if (!patternUploaded && !isUploading) { - console.log("[PatternCanvas] Attaching transformer"); - transformerRef.current.nodes([patternGroupRef.current]); - transformerRef.current.getLayer()?.batchDraw(); - } else { - console.log("[PatternCanvas] Detaching transformer"); - transformerRef.current.nodes([]); - } - }, [patternUploaded, isUploading]); - - // Call attachTransformer when conditions change - useEffect(() => { - attachTransformer(); - }, [attachTransformer, pesData]); - - // Sync node rotation with state (important for when rotation is reset to 0 after upload) - useEffect(() => { - if (patternGroupRef.current) { - patternGroupRef.current.rotation(localPatternRotation); - } - }, [localPatternRotation]); - - // Handle transformer rotation - just store the angle, apply at upload time - const handleTransformEnd = useCallback( - (e: KonvaEventObject) => { - if (!pesData) return; - - const node = e.target; - // Read rotation from the node - const totalRotation = node.rotation(); - const normalizedRotation = ((totalRotation % 360) + 360) % 360; - - setLocalPatternRotation(normalizedRotation); - - // Also read position in case the Transformer affected it - const newOffset = { - x: node.x(), - y: node.y(), - }; - setLocalPatternOffset(newOffset); - - // Store rotation angle and position - setPatternRotation(normalizedRotation); - setPatternOffset(newOffset.x, newOffset.y); - - console.log( - "[Canvas] Transform end - rotation:", - normalizedRotation, - "degrees, position:", - newOffset, - ); - }, - [setPatternRotation, setPatternOffset, pesData], - ); - - const hasPattern = pesData || uploadedPesData; - const borderColor = hasPattern - ? "border-tertiary-600 dark:border-tertiary-500" - : "border-gray-400 dark:border-gray-600"; - const iconColor = hasPattern - ? "text-tertiary-600 dark:text-tertiary-400" - : "text-gray-600 dark:text-gray-400"; - - return ( - - -
- -
- Pattern Preview - {hasPattern ? ( - - {(() => { - const displayPattern = uploadedPesData || pesData; - return displayPattern ? ( - <> - {( - (displayPattern.bounds.maxX - - displayPattern.bounds.minX) / - 10 - ).toFixed(1)}{" "} - ×{" "} - {( - (displayPattern.bounds.maxY - - displayPattern.bounds.minY) / - 10 - ).toFixed(1)}{" "} - mm - - ) : null; - })()} - - ) : ( - - No pattern loaded - - )} -
-
-
- -
- {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 */} - - {hasPattern && ( - <> - - - {machineInfo && } - - )} - - - {/* Original pattern layer: draggable with transformer (shown before upload starts) */} - - {pesData && - (() => { - const originalCenterX = - (pesData.bounds.minX + pesData.bounds.maxX) / 2; - const originalCenterY = - (pesData.bounds.minY + pesData.bounds.maxY) / 2; - console.log("[Canvas] Rendering original pattern:", { - position: localPatternOffset, - rotation: localPatternRotation, - center: { x: originalCenterX, y: originalCenterY }, - bounds: pesData.bounds, - }); - return ( - <> - { - patternGroupRef.current = node; - // Set initial rotation from state - if (node) { - node.rotation(localPatternRotation); - // Try to attach transformer when group is mounted - attachTransformer(); - } - }} - draggable={!isUploading} - x={localPatternOffset.x} - y={localPatternOffset.y} - offsetX={originalCenterX} - offsetY={originalCenterY} - onDragEnd={handlePatternDragEnd} - onTransformEnd={handleTransformEnd} - onMouseEnter={(e) => { - const stage = e.target.getStage(); - if (stage && !isUploading) - stage.container().style.cursor = "move"; - }} - onMouseLeave={(e) => { - const stage = e.target.getStage(); - if (stage && !isUploading) - stage.container().style.cursor = "grab"; - }} - > - { - // Convert PEN stitch format {x, y, flags, isJump} to PES format [x, y, cmd, colorIndex] - const cmd = s.isJump ? 0x10 : 0; // MOVE flag if jump - const colorIndex = - pesData.penStitches.colorBlocks.find( - (b) => - i >= b.startStitch && i <= b.endStitch, - )?.colorIndex ?? 0; - return [s.x, s.y, cmd, colorIndex]; - }, - )} - pesData={pesData} - currentStitchIndex={0} - showProgress={false} - /> - - - { - transformerRef.current = node; - // Try to attach transformer when transformer is mounted - if (node) { - attachTransformer(); - } - }} - enabledAnchors={[]} - rotateEnabled={true} - borderEnabled={true} - borderStroke="#FF6B6B" - borderStrokeWidth={2} - rotationSnaps={[0, 45, 90, 135, 180, 225, 270, 315]} - ignoreStroke={true} - rotateAnchorOffset={20} - /> - - ); - })()} - - - {/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */} - - {uploadedPesData && - (() => { - const uploadedCenterX = - (uploadedPesData.bounds.minX + - uploadedPesData.bounds.maxX) / - 2; - const uploadedCenterY = - (uploadedPesData.bounds.minY + - uploadedPesData.bounds.maxY) / - 2; - console.log("[Canvas] Rendering uploaded pattern:", { - position: initialUploadedPatternOffset, - center: { x: uploadedCenterX, y: uploadedCenterY }, - bounds: uploadedPesData.bounds, - }); - return ( - - { - const cmd = s.isJump ? 0x10 : 0; - const colorIndex = - uploadedPesData.penStitches.colorBlocks.find( - (b) => i >= b.startStitch && i <= b.endStitch, - )?.colorIndex ?? 0; - return [s.x, s.y, cmd, colorIndex]; - }, - )} - pesData={uploadedPesData} - currentStitchIndex={ - sewingProgress?.currentStitch || 0 - } - showProgress={true} - /> - - - ); - })()} - - - {/* Current position layer (for uploaded pattern during sewing) */} - - {uploadedPesData && - sewingProgress && - sewingProgress.currentStitch > 0 && ( - - { - const cmd = s.isJump ? 0x10 : 0; - const colorIndex = - uploadedPesData.penStitches.colorBlocks.find( - (b) => i >= b.startStitch && i <= b.endStitch, - )?.colorIndex ?? 0; - return [s.x, s.y, cmd, colorIndex]; - }, - )} - /> - - )} - - - )} - - {/* Placeholder overlay when no pattern is loaded */} - {!hasPattern && ( -
- Load a PES file to preview the pattern -
- )} - - {/* Pattern info overlays */} - {hasPattern && - (() => { - const displayPattern = uploadedPesData || pesData; - return ( - displayPattern && ( - <> - {/* Thread Legend Overlay */} -
-

- Colors -

- {displayPattern.uniqueColors.map((color, idx) => { - // Primary metadata: brand and catalog number - const primaryMetadata = [ - color.brand, - color.catalogNumber - ? `#${color.catalogNumber}` - : null, - ] - .filter(Boolean) - .join(" "); - - // Secondary metadata: chart and description - // Only show chart if it's different from catalogNumber - const secondaryMetadata = [ - color.chart && color.chart !== color.catalogNumber - ? color.chart - : null, - color.description, - ] - .filter(Boolean) - .join(" "); - - return ( -
-
-
-
- Color {idx + 1} -
- {(primaryMetadata || secondaryMetadata) && ( -
- {primaryMetadata} - {primaryMetadata && secondaryMetadata && ( - • - )} - {secondaryMetadata} -
- )} -
-
- ); - })} -
- - {/* Pattern Offset Indicator */} -
-
-
- Pattern Position: -
- {(isUploading || patternUploaded) && ( -
- - - {isUploading ? "UPLOADING" : "LOCKED"} - -
- )} -
-
- {isUploading || patternUploaded ? ( - <> - X:{" "} - {(initialUploadedPatternOffset.x / 10).toFixed(1)} - mm, Y:{" "} - {(initialUploadedPatternOffset.y / 10).toFixed(1)}mm - - ) : ( - <> - X: {(localPatternOffset.x / 10).toFixed(1)}mm, Y:{" "} - {(localPatternOffset.y / 10).toFixed(1)}mm - - )} -
- {!isUploading && - !patternUploaded && - localPatternRotation !== 0 && ( -
- Rotation: {localPatternRotation.toFixed(1)}° -
- )} -
- {isUploading - ? "Uploading pattern..." - : patternUploaded - ? "Pattern locked • Drag background to pan" - : "Drag pattern to move • Drag background to pan"} -
-
- - {/* Zoom Controls Overlay */} -
- - - - {Math.round(stageScale * 100)}% - - - -
- - ) - ); - })()} -
- - - ); -} diff --git a/src/components/KonvaComponents.tsx b/src/components/PatternCanvas/KonvaComponents.tsx similarity index 60% rename from src/components/KonvaComponents.tsx rename to src/components/PatternCanvas/KonvaComponents.tsx index a486628..d75430a 100644 --- a/src/components/KonvaComponents.tsx +++ b/src/components/PatternCanvas/KonvaComponents.tsx @@ -1,11 +1,10 @@ -import { memo, useMemo, useState, useCallback } from "react"; +import { memo, useMemo } from "react"; import { Group, Line, Rect, Text, Circle } from "react-konva"; -import type { KonvaEventObject } from "konva/lib/Node"; -import type { PesPatternData } from "../formats/import/pesImporter"; -import { getThreadColor } from "../formats/import/pesImporter"; -import type { MachineInfo } from "../types/machine"; -import { MOVE } from "../formats/import/constants"; -import { canvasColors } from "../utils/cssVariables"; +import type { PesPatternData } from "../../formats/import/pesImporter"; +import { getThreadColor } from "../../formats/import/pesImporter"; +import type { MachineInfo } from "../../types/machine"; +import { MOVE } from "../../formats/import/constants"; +import { canvasColors } from "../../utils/cssVariables"; interface GridProps { gridSize: number; @@ -294,147 +293,3 @@ export const CurrentPosition = memo( ); CurrentPosition.displayName = "CurrentPosition"; - -interface RotationHandleProps { - bounds: { minX: number; maxX: number; minY: number; maxY: number }; - rotation: number; - onRotationChange: (angle: number) => void; - onRotationEnd: (angle: number) => void; - disabled?: boolean; -} - -export const RotationHandle = memo( - ({ - bounds, - rotation, - onRotationChange, - onRotationEnd, - disabled, - }: RotationHandleProps) => { - const [isDragging, setIsDragging] = useState(false); - const [startAngle, setStartAngle] = useState(0); - - const centerX = (bounds.minX + bounds.maxX) / 2; - const centerY = (bounds.minY + bounds.maxY) / 2; - - // Calculate handle position based on rotation angle - // Start position is top-right corner (maxX, minY), which corresponds to -45° in standard coords - const radius = Math.sqrt( - Math.pow(bounds.maxX - centerX, 2) + Math.pow(bounds.minY - centerY, 2), - ); - const baseAngle = Math.atan2(bounds.minY - centerY, bounds.maxX - centerX); - const currentAngleRad = baseAngle + (rotation * Math.PI) / 180; - const handleX = centerX + radius * Math.cos(currentAngleRad); - const handleY = centerY + radius * Math.sin(currentAngleRad); - - const handleMouseDown = useCallback( - (e: KonvaEventObject) => { - if (disabled) return; - setIsDragging(true); - - const stage = e.target.getStage(); - if (!stage) return; - const pos = stage.getPointerPosition(); - if (!pos) return; - const angle = - (Math.atan2(pos.y - centerY, pos.x - centerX) * 180) / Math.PI; - setStartAngle(angle - rotation); - }, - [disabled, centerX, centerY, rotation], - ); - - const handleMouseMove = useCallback( - (e: KonvaEventObject) => { - if (disabled || !isDragging) return; - - const stage = e.target.getStage(); - if (!stage) return; - const pos = stage.getPointerPosition(); - if (!pos) return; - let angle = - (Math.atan2(pos.y - centerY, pos.x - centerX) * 180) / Math.PI; - angle = angle - startAngle; - - // Snap to 15° if Shift key held - if (e.evt.shiftKey) { - angle = Math.round(angle / 15) * 15; - } - - const normalized = ((angle % 360) + 360) % 360; - onRotationChange(normalized); - }, - [disabled, isDragging, centerX, centerY, startAngle, onRotationChange], - ); - - const handleMouseUp = useCallback( - (e: KonvaEventObject) => { - if (disabled || !isDragging) return; - setIsDragging(false); - - const stage = e.target.getStage(); - if (!stage) return; - const pos = stage.getPointerPosition(); - if (!pos) return; - let angle = - (Math.atan2(pos.y - centerY, pos.x - centerX) * 180) / Math.PI; - angle = angle - startAngle; - - const normalized = ((angle % 360) + 360) % 360; - onRotationEnd(normalized); - }, - [disabled, isDragging, centerX, centerY, startAngle, onRotationEnd], - ); - - if (disabled) return null; - - return ( - - {/* Line from center to handle */} - - - {/* Handle circle */} - { - const container = e.target.getStage()?.container(); - if (container) container.style.cursor = "grab"; - }} - onMouseLeave={(e) => { - const container = e.target.getStage()?.container(); - if (container) container.style.cursor = "default"; - }} - /> - - {/* Angle text */} - {isDragging && ( - - )} - - ); - }, -); - -RotationHandle.displayName = "RotationHandle"; diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx index 88c9c5f..b44d3a9 100644 --- a/src/components/PatternCanvas/PatternCanvas.tsx +++ b/src/components/PatternCanvas/PatternCanvas.tsx @@ -8,7 +8,7 @@ import { usePatternStore } from "../../stores/usePatternStore"; import { Stage, Layer } from "react-konva"; import Konva from "konva"; import { PhotoIcon } from "@heroicons/react/24/solid"; -import { Grid, Origin, Hoop } from "../KonvaComponents"; +import { Grid, Origin, Hoop } from "./KonvaComponents"; import { Card, CardHeader, diff --git a/src/components/PatternCanvas/PatternLayer.tsx b/src/components/PatternCanvas/PatternLayer.tsx index 7d1f4ff..5d6535e 100644 --- a/src/components/PatternCanvas/PatternLayer.tsx +++ b/src/components/PatternCanvas/PatternLayer.tsx @@ -14,7 +14,7 @@ import { calculatePatternCenter, convertPenStitchesToPesFormat, } from "./patternCanvasHelpers"; -import { Stitches, PatternBounds, CurrentPosition } from "../KonvaComponents"; +import { Stitches, PatternBounds, CurrentPosition } from "./KonvaComponents"; interface PatternLayerProps { pesData: PesPatternData;