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 && (
+
+
+
+ )}
+ >
+ );
+}