From 2dedfc5513390734474d9cd4ee511632c7c83127 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Thu, 26 Mar 2026 13:38:40 +0100 Subject: [PATCH] feature: Add pattern positioning presets and arrow key movement Add a 3x3 alignment grid to position the pattern at corners, edge centers, or center of the hoop. Support arrow key movement (1mm per press, 0.1mm with Shift held). Remove redundant center button from zoom controls. Co-Authored-By: Claude Opus 4.6 --- .../PatternCanvas/PatternCanvas.tsx | 33 ++- .../PatternCanvas/PositionPresets.tsx | 192 ++++++++++++++++++ src/components/PatternCanvas/ZoomControls.tsx | 23 +-- src/hooks/ui/usePatternTransform.ts | 39 ++++ src/stores/usePatternStore.ts | 3 - 5 files changed, 257 insertions(+), 33 deletions(-) create mode 100644 src/components/PatternCanvas/PositionPresets.tsx 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 (
-