diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx
index 1a913cb..8ca3096 100644
--- a/src/components/PatternCanvas/PatternCanvas.tsx
+++ b/src/components/PatternCanvas/PatternCanvas.tsx
@@ -1,4 +1,4 @@
-import { useRef, useMemo, useState } from "react";
+import { useRef, useMemo, useState, useCallback } from "react";
import { useShallow } from "zustand/react/shallow";
import {
useMachineStore,
@@ -19,6 +19,7 @@ import {
} from "@/components/ui/card";
import { ThreadLegend } from "./ThreadLegend";
import { PatternPositionIndicator } from "./PatternPositionIndicator";
+import { PositionPresets } from "./PositionPresets";
import { ZoomControls } from "./ZoomControls";
import { PatternLayer } from "./PatternLayer";
import { Switch } from "@/components/ui/switch";
@@ -84,6 +85,14 @@ export function PatternCanvas() {
machineInfo,
});
+ // Handler for position preset selection
+ const handlePositionPreset = useCallback(
+ (offset: { x: number; y: number }) => {
+ setPatternOffset(offset.x, offset.y);
+ },
+ [setPatternOffset],
+ );
+
// Pattern transform (position, rotation, drag/transform)
const {
localPatternOffset,
@@ -91,7 +100,6 @@ export function PatternCanvas() {
patternGroupRef,
transformerRef,
attachTransformer,
- handleCenterPattern,
handlePatternDragEnd,
handleTransformEnd,
} = usePatternTransform({
@@ -264,6 +272,20 @@ export function PatternCanvas() {
<>
+ {pesData &&
+ machineInfo &&
+ !patternUploaded &&
+ !isUploading &&
+ !uploadedPesData && (
+
+ )}
+
>
)}
diff --git a/src/components/PatternCanvas/PositionPresets.tsx b/src/components/PatternCanvas/PositionPresets.tsx
new file mode 100644
index 0000000..e71bfb0
--- /dev/null
+++ b/src/components/PatternCanvas/PositionPresets.tsx
@@ -0,0 +1,192 @@
+/**
+ * PositionPresets Component
+ *
+ * Provides a 3x3 grid of buttons to quickly position the pattern
+ * at predefined locations within the hoop (corners, edge centers, and center)
+ */
+
+import type { MachineInfo } from "../../types/machine";
+import type { PesPatternData } from "../../formats/import/pesImporter";
+import { calculatePatternCenter } from "./patternCanvasHelpers";
+import { calculateRotatedBounds } from "../../utils/rotationUtils";
+
+export type PositionPreset =
+ | "top-left"
+ | "top-center"
+ | "top-right"
+ | "center-left"
+ | "center"
+ | "center-right"
+ | "bottom-left"
+ | "bottom-center"
+ | "bottom-right";
+
+interface PositionPresetsProps {
+ pesData: PesPatternData;
+ patternRotation: number;
+ machineInfo: MachineInfo;
+ onPositionSelect: (offset: { x: number; y: number }) => void;
+ disabled: boolean;
+}
+
+/**
+ * Calculate the offset needed to position the pattern at a given preset location.
+ *
+ * The offset represents the pattern center's position in world coordinates.
+ * When offset is {0,0}, the pattern center is at the hoop center.
+ */
+function calculatePresetOffset(
+ preset: PositionPreset,
+ pesData: PesPatternData,
+ patternRotation: number,
+ machineInfo: MachineInfo,
+): { x: number; y: number } {
+ // Use rotated bounds if rotation is applied
+ const bounds =
+ patternRotation !== 0
+ ? calculateRotatedBounds(pesData.bounds, patternRotation)
+ : pesData.bounds;
+
+ const center = calculatePatternCenter(bounds);
+ const hoopHalfW = machineInfo.maxWidth / 2;
+ const hoopHalfH = machineInfo.maxHeight / 2;
+
+ // Distance from pattern center to each edge
+ const leftHalf = center.x - bounds.minX;
+ const rightHalf = bounds.maxX - center.x;
+ const topHalf = center.y - bounds.minY;
+ const bottomHalf = bounds.maxY - center.y;
+
+ const xPositions = {
+ left: -hoopHalfW + leftHalf,
+ center: 0,
+ right: hoopHalfW - rightHalf,
+ };
+
+ const yPositions = {
+ top: -hoopHalfH + topHalf,
+ center: 0,
+ bottom: hoopHalfH - bottomHalf,
+ };
+
+ switch (preset) {
+ case "top-left":
+ return { x: xPositions.left, y: yPositions.top };
+ case "top-center":
+ return { x: xPositions.center, y: yPositions.top };
+ case "top-right":
+ return { x: xPositions.right, y: yPositions.top };
+ case "center-left":
+ return { x: xPositions.left, y: yPositions.center };
+ case "center":
+ return { x: xPositions.center, y: yPositions.center };
+ case "center-right":
+ return { x: xPositions.right, y: yPositions.center };
+ case "bottom-left":
+ return { x: xPositions.left, y: yPositions.bottom };
+ case "bottom-center":
+ return { x: xPositions.center, y: yPositions.bottom };
+ case "bottom-right":
+ return { x: xPositions.right, y: yPositions.bottom };
+ }
+}
+
+const presetGrid: { preset: PositionPreset; label: string }[][] = [
+ [
+ { preset: "top-left", label: "Top Left" },
+ { preset: "top-center", label: "Top Center" },
+ { preset: "top-right", label: "Top Right" },
+ ],
+ [
+ { preset: "center-left", label: "Center Left" },
+ { preset: "center", label: "Center" },
+ { preset: "center-right", label: "Center Right" },
+ ],
+ [
+ { preset: "bottom-left", label: "Bottom Left" },
+ { preset: "bottom-center", label: "Bottom Center" },
+ { preset: "bottom-right", label: "Bottom Right" },
+ ],
+];
+
+export function PositionPresets({
+ pesData,
+ patternRotation,
+ machineInfo,
+ onPositionSelect,
+ disabled,
+}: PositionPresetsProps) {
+ const handleClick = (preset: PositionPreset) => {
+ const offset = calculatePresetOffset(
+ preset,
+ pesData,
+ patternRotation,
+ machineInfo,
+ );
+ onPositionSelect(offset);
+ };
+
+ return (
+
+
+ Align
+
+
+ {presetGrid.map((row, rowIdx) =>
+ row.map(({ preset, label }) => (
+
+ )),
+ )}
+
+
+ );
+}
+
+/**
+ * Visual indicator showing the position within a rectangle.
+ * Renders a small rectangle outline with a dot at the corresponding position.
+ */
+function DotIndicator({
+ row,
+ preset,
+}: {
+ row: number;
+ preset: PositionPreset;
+}) {
+ // Map preset to dot position within a 12x12 viewBox
+ const dotX = preset.includes("left") ? 2 : preset.includes("right") ? 10 : 6;
+ const dotY = row === 0 ? 2 : row === 2 ? 10 : 6;
+
+ return (
+
+ );
+}
diff --git a/src/components/PatternCanvas/ZoomControls.tsx b/src/components/PatternCanvas/ZoomControls.tsx
index 9d5d59f..549c2b0 100644
--- a/src/components/PatternCanvas/ZoomControls.tsx
+++ b/src/components/PatternCanvas/ZoomControls.tsx
@@ -2,15 +2,10 @@
* ZoomControls Component
*
* Provides zoom and pan controls for the pattern canvas
- * Includes zoom in/out, reset zoom, and center pattern buttons
+ * Includes zoom in/out and reset zoom buttons
*/
-import {
- PlusIcon,
- MinusIcon,
- ArrowPathIcon,
- ArrowsPointingInIcon,
-} from "@heroicons/react/24/solid";
+import { PlusIcon, MinusIcon, ArrowPathIcon } from "@heroicons/react/24/solid";
import { Button } from "@/components/ui/button";
interface ZoomControlsProps {
@@ -18,8 +13,6 @@ interface ZoomControlsProps {
onZoomIn: () => void;
onZoomOut: () => void;
onZoomReset: () => void;
- onCenterPattern: () => void;
- canCenterPattern: boolean;
}
export function ZoomControls({
@@ -27,21 +20,9 @@ export function ZoomControls({
onZoomIn,
onZoomOut,
onZoomReset,
- onCenterPattern,
- canCenterPattern,
}: ZoomControlsProps) {
return (
-