From 469c08d45bbf92488a10ae97080d8533460c860e Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Thu, 25 Dec 2025 21:29:24 +0100 Subject: [PATCH] feature: Create unified PatternLayer component to eliminate duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unified three separate layer implementations into single PatternLayer component: **PatternLayer.tsx (156 lines)** - Handles both interactive (original) and locked (uploaded) pattern rendering - Props-driven configuration for dragging, rotation, transformer - Includes CurrentPosition indicator for uploaded patterns - Memoized stitches and center calculations for performance - Single source of truth for pattern rendering logic **Benefits:** - Eliminated ~140 lines of duplicated layer code - Reduced PatternCanvas from 388 to 271 lines (-117 lines, -30%) - Consistent rendering behavior across pattern states - Easier to maintain and test pattern rendering - Cleaner component composition in PatternCanvas **Removed from PatternCanvas:** - Original pattern layer implementation (74 lines) - Uploaded pattern layer implementation (33 lines) - Current position layer implementation (26 lines) - Duplicate Group, Transformer, Stitches setup - Redundant center calculations and stitch conversions **Complete refactoring impact (All Phases):** - Original: 786 lines in single monolithic file - After Phase 4: 271 lines main + hooks + components - **Total reduction: -515 lines (-65%)** File structure now: - PatternCanvas.tsx: 271 lines (orchestration) - PatternLayer.tsx: 156 lines (rendering) - useCanvasViewport.ts: 166 lines (viewport) - usePatternTransform.ts: 179 lines (transform) - 3 display components + 3 utilities 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../PatternCanvas/PatternCanvas.tsx | 171 +++--------------- src/components/PatternCanvas/PatternLayer.tsx | 156 ++++++++++++++++ 2 files changed, 183 insertions(+), 144 deletions(-) create mode 100644 src/components/PatternCanvas/PatternLayer.tsx diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx index 665fb69..88c9c5f 100644 --- a/src/components/PatternCanvas/PatternCanvas.tsx +++ b/src/components/PatternCanvas/PatternCanvas.tsx @@ -5,17 +5,10 @@ import { usePatternUploaded, } from "../../stores/useMachineStore"; import { usePatternStore } from "../../stores/usePatternStore"; -import { Stage, Layer, Group, Transformer } from "react-konva"; +import { Stage, Layer } from "react-konva"; import Konva from "konva"; import { PhotoIcon } from "@heroicons/react/24/solid"; -import { - Grid, - Origin, - Hoop, - Stitches, - PatternBounds, - CurrentPosition, -} from "../KonvaComponents"; +import { Grid, Origin, Hoop } from "../KonvaComponents"; import { Card, CardHeader, @@ -23,13 +16,10 @@ import { CardDescription, CardContent, } from "@/components/ui/card"; -import { - calculatePatternCenter, - convertPenStitchesToPesFormat, -} from "./patternCanvasHelpers"; import { ThreadLegend } from "./ThreadLegend"; import { PatternPositionIndicator } from "./PatternPositionIndicator"; import { ZoomControls } from "./ZoomControls"; +import { PatternLayer } from "./PatternLayer"; import { useCanvasViewport } from "../../hooks/useCanvasViewport"; import { usePatternTransform } from "../../hooks/usePatternTransform"; @@ -201,141 +191,34 @@ export function PatternCanvas() { {/* Original pattern layer: draggable with transformer (shown before upload starts) */} - {pesData && - (() => { - const originalCenter = calculatePatternCenter( - pesData.bounds, - ); - console.log("[Canvas] Rendering original pattern:", { - position: localPatternOffset, - rotation: localPatternRotation, - center: originalCenter, - 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={originalCenter.x} - offsetY={originalCenter.y} - 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"; - }} - > - - - - { - 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} - /> - - ); - })()} + {pesData && ( + + )} {/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */} - {uploadedPesData && - (() => { - const uploadedCenter = calculatePatternCenter( - uploadedPesData.bounds, - ); - console.log("[Canvas] Rendering uploaded pattern:", { - position: initialUploadedPatternOffset, - center: uploadedCenter, - bounds: uploadedPesData.bounds, - }); - return ( - - - - - ); - })()} - - - {/* Current position layer (for uploaded pattern during sewing) */} - - {uploadedPesData && - sewingProgress && - sewingProgress.currentStitch > 0 && - (() => { - const center = calculatePatternCenter( - uploadedPesData.bounds, - ); - return ( - - - - ); - })()} + {uploadedPesData && ( + + )} )} diff --git a/src/components/PatternCanvas/PatternLayer.tsx b/src/components/PatternCanvas/PatternLayer.tsx new file mode 100644 index 0000000..7d1f4ff --- /dev/null +++ b/src/components/PatternCanvas/PatternLayer.tsx @@ -0,0 +1,156 @@ +/** + * PatternLayer Component + * + * Unified component for rendering pattern layers (both original and uploaded) + * Handles both interactive (draggable/rotatable) and locked states + */ + +import { useMemo, type RefObject } from "react"; +import { Group, Transformer } from "react-konva"; +import type Konva from "konva"; +import type { KonvaEventObject } from "konva/lib/Node"; +import type { PesPatternData } from "../../formats/import/pesImporter"; +import { + calculatePatternCenter, + convertPenStitchesToPesFormat, +} from "./patternCanvasHelpers"; +import { Stitches, PatternBounds, CurrentPosition } from "../KonvaComponents"; + +interface PatternLayerProps { + pesData: PesPatternData; + offset: { x: number; y: number }; + rotation?: number; + isInteractive: boolean; + showProgress?: boolean; + currentStitchIndex?: number; + patternGroupRef?: RefObject; + transformerRef?: RefObject; + onDragEnd?: (e: Konva.KonvaEventObject) => void; + onTransformEnd?: (e: KonvaEventObject) => void; + attachTransformer?: () => void; +} + +export function PatternLayer({ + pesData, + offset, + rotation = 0, + isInteractive, + showProgress = false, + currentStitchIndex = 0, + patternGroupRef, + transformerRef, + onDragEnd, + onTransformEnd, + attachTransformer, +}: PatternLayerProps) { + const center = useMemo( + () => calculatePatternCenter(pesData.bounds), + [pesData.bounds], + ); + + const stitches = useMemo( + () => convertPenStitchesToPesFormat(pesData.penStitches), + [pesData.penStitches], + ); + + const groupName = isInteractive ? "pattern-group" : "uploaded-pattern-group"; + + console.log( + `[PatternLayer] Rendering ${isInteractive ? "original" : "uploaded"} pattern:`, + { + position: offset, + rotation: isInteractive ? rotation : "n/a", + center, + bounds: pesData.bounds, + }, + ); + + return ( + <> + { + if (patternGroupRef) { + patternGroupRef.current = node; + } + // Set initial rotation from state + if (node && isInteractive) { + node.rotation(rotation); + // Try to attach transformer when group is mounted + if (attachTransformer) { + attachTransformer(); + } + } + } + : undefined + } + draggable={isInteractive} + x={offset.x} + y={offset.y} + offsetX={center.x} + offsetY={center.y} + onDragEnd={isInteractive ? onDragEnd : undefined} + onTransformEnd={isInteractive ? onTransformEnd : undefined} + onMouseEnter={ + isInteractive + ? (e) => { + const stage = e.target.getStage(); + if (stage) stage.container().style.cursor = "move"; + } + : undefined + } + onMouseLeave={ + isInteractive + ? (e) => { + const stage = e.target.getStage(); + if (stage) stage.container().style.cursor = "grab"; + } + : undefined + } + > + + + + + {/* Transformer only for interactive layer */} + {isInteractive && transformerRef && ( + { + if (transformerRef) { + transformerRef.current = node; + } + // Try to attach transformer when transformer is mounted + if (node && attachTransformer) { + attachTransformer(); + } + }} + enabledAnchors={[]} + rotateEnabled={true} + borderEnabled={true} + borderStroke="#FF6B6B" + borderStrokeWidth={2} + rotationSnaps={[0, 45, 90, 135, 180, 225, 270, 315]} + ignoreStroke={true} + rotateAnchorOffset={20} + /> + )} + + {/* Current position indicator (only for uploaded pattern with progress) */} + {!isInteractive && showProgress && currentStitchIndex > 0 && ( + + + + )} + + ); +}