diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx
index 8b0d213..948332b 100644
--- a/src/components/PatternCanvas/PatternCanvas.tsx
+++ b/src/components/PatternCanvas/PatternCanvas.tsx
@@ -8,14 +8,7 @@ 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 { PhotoIcon } from "@heroicons/react/24/solid";
import type { PesPatternData } from "../../formats/import/pesImporter";
import { calculateInitialScale } from "../../utils/konvaRenderers";
import {
@@ -33,12 +26,14 @@ import {
CardDescription,
CardContent,
} from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
import {
calculatePatternCenter,
convertPenStitchesToPesFormat,
calculateZoomToPoint,
} from "./patternCanvasHelpers";
+import { ThreadLegend } from "./ThreadLegend";
+import { PatternPositionIndicator } from "./PatternPositionIndicator";
+import { ZoomControls } from "./ZoomControls";
export function PatternCanvas() {
// Machine store
@@ -579,156 +574,29 @@ export function PatternCanvas() {
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"}
-
-
+ ? initialUploadedPatternOffset
+ : localPatternOffset
+ }
+ rotation={localPatternRotation}
+ isLocked={patternUploaded}
+ isUploading={isUploading}
+ />
- {/* Zoom Controls Overlay */}
-
-
-
-
- {Math.round(stageScale * 100)}%
-
-
-
-
+
>
)
);
diff --git a/src/components/PatternCanvas/PatternPositionIndicator.tsx b/src/components/PatternCanvas/PatternPositionIndicator.tsx
new file mode 100644
index 0000000..6e7474c
--- /dev/null
+++ b/src/components/PatternCanvas/PatternPositionIndicator.tsx
@@ -0,0 +1,61 @@
+/**
+ * PatternPositionIndicator Component
+ *
+ * Displays the current pattern position and rotation
+ * Shows locked state when pattern is uploaded or being uploaded
+ */
+
+import { LockClosedIcon } from "@heroicons/react/24/solid";
+
+interface PatternPositionIndicatorProps {
+ offset: { x: number; y: number };
+ rotation?: number;
+ isLocked: boolean;
+ isUploading: boolean;
+}
+
+export function PatternPositionIndicator({
+ offset,
+ rotation = 0,
+ isLocked,
+ isUploading,
+}: PatternPositionIndicatorProps) {
+ return (
+
+
+
+ Pattern Position:
+
+ {(isUploading || isLocked) && (
+
+
+
+ {isUploading ? "UPLOADING" : "LOCKED"}
+
+
+ )}
+
+
+ X: {(offset.x / 10).toFixed(1)}mm, Y: {(offset.y / 10).toFixed(1)}mm
+
+ {!isUploading && !isLocked && rotation !== 0 && (
+
+ Rotation: {rotation.toFixed(1)}°
+
+ )}
+
+ {isUploading
+ ? "Uploading pattern..."
+ : isLocked
+ ? "Pattern locked • Drag background to pan"
+ : "Drag pattern to move • Drag background to pan"}
+
+
+ );
+}
diff --git a/src/components/PatternCanvas/ThreadLegend.tsx b/src/components/PatternCanvas/ThreadLegend.tsx
new file mode 100644
index 0000000..4f07738
--- /dev/null
+++ b/src/components/PatternCanvas/ThreadLegend.tsx
@@ -0,0 +1,74 @@
+/**
+ * ThreadLegend Component
+ *
+ * Displays a legend of thread colors used in the embroidery pattern
+ * Shows color swatches with brand, catalog number, and description metadata
+ */
+
+interface ThreadColor {
+ hex: string;
+ brand?: string | null;
+ catalogNumber?: string | null;
+ chart?: string | null;
+ description?: string | null;
+}
+
+interface ThreadLegendProps {
+ colors: ThreadColor[];
+}
+
+export function ThreadLegend({ colors }: ThreadLegendProps) {
+ return (
+
+
+ Colors
+
+ {colors.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}
+
+ )}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/PatternCanvas/ZoomControls.tsx b/src/components/PatternCanvas/ZoomControls.tsx
new file mode 100644
index 0000000..9d5d59f
--- /dev/null
+++ b/src/components/PatternCanvas/ZoomControls.tsx
@@ -0,0 +1,77 @@
+/**
+ * ZoomControls Component
+ *
+ * Provides zoom and pan controls for the pattern canvas
+ * Includes zoom in/out, reset zoom, and center pattern buttons
+ */
+
+import {
+ PlusIcon,
+ MinusIcon,
+ ArrowPathIcon,
+ ArrowsPointingInIcon,
+} from "@heroicons/react/24/solid";
+import { Button } from "@/components/ui/button";
+
+interface ZoomControlsProps {
+ scale: number;
+ onZoomIn: () => void;
+ onZoomOut: () => void;
+ onZoomReset: () => void;
+ onCenterPattern: () => void;
+ canCenterPattern: boolean;
+}
+
+export function ZoomControls({
+ scale,
+ onZoomIn,
+ onZoomOut,
+ onZoomReset,
+ onCenterPattern,
+ canCenterPattern,
+}: ZoomControlsProps) {
+ return (
+
+
+
+
+ {Math.round(scale * 100)}%
+
+
+
+
+ );
+}