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 (
-