mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Merge pull request #53 from jhbruhn/refactor/39-separate-fileupload-business-logic
refactor: Extract business logic from FileUpload into custom hooks
This commit is contained in:
commit
03ba6c77e9
4 changed files with 361 additions and 199 deletions
|
|
@ -5,24 +5,14 @@ import { useMachineUploadStore } from "../stores/useMachineUploadStore";
|
||||||
import { useMachineCacheStore } from "../stores/useMachineCacheStore";
|
import { useMachineCacheStore } from "../stores/useMachineCacheStore";
|
||||||
import { usePatternStore } from "../stores/usePatternStore";
|
import { usePatternStore } from "../stores/usePatternStore";
|
||||||
import { useUIStore } from "../stores/useUIStore";
|
import { useUIStore } from "../stores/useUIStore";
|
||||||
import {
|
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||||
convertPesToPen,
|
|
||||||
type PesPatternData,
|
|
||||||
} from "../formats/import/pesImporter";
|
|
||||||
import {
|
import {
|
||||||
canUploadPattern,
|
canUploadPattern,
|
||||||
getMachineStateCategory,
|
getMachineStateCategory,
|
||||||
} from "../utils/machineStateHelpers";
|
} from "../utils/machineStateHelpers";
|
||||||
import {
|
import { useFileUpload } from "../hooks/useFileUpload";
|
||||||
transformStitchesRotation,
|
import { usePatternRotationUpload } from "../hooks/usePatternRotationUpload";
|
||||||
calculateRotatedBounds,
|
import { usePatternValidation } from "../hooks/usePatternValidation";
|
||||||
} from "../utils/rotationUtils";
|
|
||||||
import { encodeStitchesToPen } from "../formats/pen/encoder";
|
|
||||||
import { decodePenData } from "../formats/pen/decoder";
|
|
||||||
import {
|
|
||||||
calculatePatternCenter,
|
|
||||||
calculateBoundsFromDecodedStitches,
|
|
||||||
} from "./PatternCanvas/patternCanvasHelpers";
|
|
||||||
import { PatternInfoSkeleton } from "./SkeletonLoader";
|
import { PatternInfoSkeleton } from "./SkeletonLoader";
|
||||||
import { PatternInfo } from "./PatternInfo";
|
import { PatternInfo } from "./PatternInfo";
|
||||||
import {
|
import {
|
||||||
|
|
@ -111,207 +101,53 @@ export function FileUpload() {
|
||||||
const pesData = pesDataProp || localPesData;
|
const pesData = pesDataProp || localPesData;
|
||||||
// Use currentFileName from App state, or local fileName, or resumeFileName for display
|
// Use currentFileName from App state, or local fileName, or resumeFileName for display
|
||||||
const displayFileName = currentFileName || fileName || resumeFileName || "";
|
const displayFileName = currentFileName || fileName || resumeFileName || "";
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleFileChange = useCallback(
|
// File upload hook - handles file selection and conversion
|
||||||
async (event?: React.ChangeEvent<HTMLInputElement>) => {
|
const { isLoading, handleFileChange } = useFileUpload({
|
||||||
setIsLoading(true);
|
fileService,
|
||||||
try {
|
pyodideReady,
|
||||||
// Wait for Pyodide if it's still loading
|
initializePyodide,
|
||||||
if (!pyodideReady) {
|
onFileLoaded: useCallback(
|
||||||
console.log("[FileUpload] Waiting for Pyodide to finish loading...");
|
(data: PesPatternData, name: string) => {
|
||||||
await initializePyodide();
|
|
||||||
console.log("[FileUpload] Pyodide ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
let file: File | null = null;
|
|
||||||
|
|
||||||
// In Electron, use native file dialogs
|
|
||||||
if (fileService.hasNativeDialogs()) {
|
|
||||||
file = await fileService.openFileDialog({ accept: ".pes" });
|
|
||||||
} else {
|
|
||||||
// In browser, use the input element
|
|
||||||
file = event?.target.files?.[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await convertPesToPen(file);
|
|
||||||
setLocalPesData(data);
|
setLocalPesData(data);
|
||||||
setFileName(file.name);
|
setFileName(name);
|
||||||
setPattern(data, file.name);
|
setPattern(data, name);
|
||||||
} catch (err) {
|
},
|
||||||
alert(
|
[setPattern],
|
||||||
`Failed to load PES file: ${
|
),
|
||||||
err instanceof Error ? err.message : "Unknown error"
|
});
|
||||||
}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fileService, setPattern, pyodideReady, initializePyodide],
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Pattern rotation and upload hook - handles rotation transformation
|
||||||
|
const { handleUpload: handlePatternUpload } = usePatternRotationUpload({
|
||||||
|
uploadPattern,
|
||||||
|
setUploadedPattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wrapper to call upload with current pattern data
|
||||||
const handleUpload = useCallback(async () => {
|
const handleUpload = useCallback(async () => {
|
||||||
if (pesData && displayFileName) {
|
if (pesData && displayFileName) {
|
||||||
let penDataToUpload = pesData.penData;
|
await handlePatternUpload(
|
||||||
let pesDataForUpload = pesData;
|
pesData,
|
||||||
|
|
||||||
// Apply rotation if needed
|
|
||||||
if (patternRotation && patternRotation !== 0) {
|
|
||||||
// Transform stitches
|
|
||||||
const rotatedStitches = transformStitchesRotation(
|
|
||||||
pesData.stitches,
|
|
||||||
patternRotation,
|
|
||||||
pesData.bounds,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Encode to PEN (this will round coordinates)
|
|
||||||
const penResult = encodeStitchesToPen(rotatedStitches);
|
|
||||||
penDataToUpload = new Uint8Array(penResult.penBytes);
|
|
||||||
|
|
||||||
// Decode back to get the ACTUAL pattern (after PEN rounding)
|
|
||||||
const decoded = decodePenData(penDataToUpload);
|
|
||||||
|
|
||||||
// Calculate bounds from the DECODED stitches (the actual data that will be rendered)
|
|
||||||
const rotatedBounds = calculateBoundsFromDecodedStitches(decoded);
|
|
||||||
|
|
||||||
// Calculate the center of the rotated pattern
|
|
||||||
const originalCenter = calculatePatternCenter(pesData.bounds);
|
|
||||||
const rotatedCenter = calculatePatternCenter(rotatedBounds);
|
|
||||||
const centerShiftX = rotatedCenter.x - originalCenter.x;
|
|
||||||
const centerShiftY = rotatedCenter.y - originalCenter.y;
|
|
||||||
|
|
||||||
// CRITICAL: Adjust position to compensate for the center shift!
|
|
||||||
// In Konva, visual position = (x - offsetX, y - offsetY).
|
|
||||||
// Original visual pos: (x - originalCenter.x, y - originalCenter.y)
|
|
||||||
// New visual pos: (newX - rotatedCenter.x, newY - rotatedCenter.y)
|
|
||||||
// For same visual position: newX = x + (rotatedCenter.x - originalCenter.x)
|
|
||||||
// So we need to add (rotatedCenter - originalCenter) to the position.
|
|
||||||
const adjustedOffset = {
|
|
||||||
x: patternOffset.x + centerShiftX,
|
|
||||||
y: patternOffset.y + centerShiftY,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create rotated PesPatternData for upload
|
|
||||||
pesDataForUpload = {
|
|
||||||
...pesData,
|
|
||||||
stitches: rotatedStitches,
|
|
||||||
penData: penDataToUpload,
|
|
||||||
penStitches: decoded,
|
|
||||||
bounds: rotatedBounds,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save uploaded pattern to store for preview BEFORE starting upload
|
|
||||||
// This allows the preview to show immediately when isUploading becomes true
|
|
||||||
setUploadedPattern(pesDataForUpload, adjustedOffset);
|
|
||||||
|
|
||||||
// Upload the pattern with offset
|
|
||||||
// IMPORTANT: Pass original unrotated pesData for caching, rotated pesData for upload
|
|
||||||
uploadPattern(
|
|
||||||
penDataToUpload,
|
|
||||||
pesDataForUpload,
|
|
||||||
displayFileName,
|
|
||||||
adjustedOffset,
|
|
||||||
patternRotation,
|
|
||||||
pesData, // Original unrotated pattern for caching
|
|
||||||
);
|
|
||||||
|
|
||||||
return; // Early return to skip the upload below
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save uploaded pattern to store BEFORE starting upload
|
|
||||||
// (same as original since no rotation)
|
|
||||||
setUploadedPattern(pesDataForUpload, patternOffset);
|
|
||||||
|
|
||||||
// Upload the pattern (no rotation case)
|
|
||||||
uploadPattern(
|
|
||||||
penDataToUpload,
|
|
||||||
pesDataForUpload,
|
|
||||||
displayFileName,
|
displayFileName,
|
||||||
patternOffset,
|
patternOffset,
|
||||||
0, // No rotation
|
patternRotation,
|
||||||
// No need to pass originalPesData since it's the same as pesDataForUpload
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
pesData,
|
pesData,
|
||||||
displayFileName,
|
displayFileName,
|
||||||
uploadPattern,
|
|
||||||
patternOffset,
|
patternOffset,
|
||||||
patternRotation,
|
patternRotation,
|
||||||
setUploadedPattern,
|
handlePatternUpload,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if pattern (with offset and rotation) fits within hoop bounds
|
// Pattern validation hook - checks if pattern fits in hoop
|
||||||
const checkPatternFitsInHoop = useCallback(() => {
|
const boundsCheck = usePatternValidation({
|
||||||
if (!pesData || !machineInfo) {
|
pesData,
|
||||||
return { fits: true, error: null };
|
machineInfo,
|
||||||
}
|
patternOffset,
|
||||||
|
patternRotation,
|
||||||
// Calculate rotated bounds if rotation is applied
|
});
|
||||||
let bounds = pesData.bounds;
|
|
||||||
if (patternRotation && patternRotation !== 0) {
|
|
||||||
bounds = calculateRotatedBounds(pesData.bounds, patternRotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { maxWidth, maxHeight } = machineInfo;
|
|
||||||
|
|
||||||
// The patternOffset represents the pattern's CENTER position (due to offsetX/offsetY in canvas)
|
|
||||||
// So we need to calculate bounds relative to the center
|
|
||||||
const center = calculatePatternCenter(bounds);
|
|
||||||
|
|
||||||
// Calculate actual bounds in world coordinates
|
|
||||||
const patternMinX = patternOffset.x - center.x + bounds.minX;
|
|
||||||
const patternMaxX = patternOffset.x - center.x + bounds.maxX;
|
|
||||||
const patternMinY = patternOffset.y - center.y + bounds.minY;
|
|
||||||
const patternMaxY = patternOffset.y - center.y + bounds.maxY;
|
|
||||||
|
|
||||||
// Hoop bounds (centered at origin)
|
|
||||||
const hoopMinX = -maxWidth / 2;
|
|
||||||
const hoopMaxX = maxWidth / 2;
|
|
||||||
const hoopMinY = -maxHeight / 2;
|
|
||||||
const hoopMaxY = maxHeight / 2;
|
|
||||||
|
|
||||||
// Check if pattern exceeds hoop bounds
|
|
||||||
const exceedsLeft = patternMinX < hoopMinX;
|
|
||||||
const exceedsRight = patternMaxX > hoopMaxX;
|
|
||||||
const exceedsTop = patternMinY < hoopMinY;
|
|
||||||
const exceedsBottom = patternMaxY > hoopMaxY;
|
|
||||||
|
|
||||||
if (exceedsLeft || exceedsRight || exceedsTop || exceedsBottom) {
|
|
||||||
const directions = [];
|
|
||||||
if (exceedsLeft)
|
|
||||||
directions.push(
|
|
||||||
`left by ${((hoopMinX - patternMinX) / 10).toFixed(1)}mm`,
|
|
||||||
);
|
|
||||||
if (exceedsRight)
|
|
||||||
directions.push(
|
|
||||||
`right by ${((patternMaxX - hoopMaxX) / 10).toFixed(1)}mm`,
|
|
||||||
);
|
|
||||||
if (exceedsTop)
|
|
||||||
directions.push(
|
|
||||||
`top by ${((hoopMinY - patternMinY) / 10).toFixed(1)}mm`,
|
|
||||||
);
|
|
||||||
if (exceedsBottom)
|
|
||||||
directions.push(
|
|
||||||
`bottom by ${((patternMaxY - hoopMaxY) / 10).toFixed(1)}mm`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
fits: false,
|
|
||||||
error: `Pattern exceeds hoop bounds: ${directions.join(", ")}. Adjust pattern position in preview.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { fits: true, error: null };
|
|
||||||
}, [pesData, machineInfo, patternOffset, patternRotation]);
|
|
||||||
|
|
||||||
const boundsCheck = checkPatternFitsInHoop();
|
|
||||||
|
|
||||||
const borderColor = pesData
|
const borderColor = pesData
|
||||||
? "border-secondary-600 dark:border-secondary-500"
|
? "border-secondary-600 dark:border-secondary-500"
|
||||||
|
|
|
||||||
84
src/hooks/useFileUpload.ts
Normal file
84
src/hooks/useFileUpload.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
convertPesToPen,
|
||||||
|
type PesPatternData,
|
||||||
|
} from "../formats/import/pesImporter";
|
||||||
|
import type { IFileService } from "../platform/interfaces/IFileService";
|
||||||
|
|
||||||
|
export interface UseFileUploadParams {
|
||||||
|
fileService: IFileService;
|
||||||
|
pyodideReady: boolean;
|
||||||
|
initializePyodide: () => Promise<void>;
|
||||||
|
onFileLoaded: (data: PesPatternData, fileName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseFileUploadReturn {
|
||||||
|
isLoading: boolean;
|
||||||
|
handleFileChange: (
|
||||||
|
event?: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for handling file upload and PES to PEN conversion
|
||||||
|
*
|
||||||
|
* Manages file selection (native dialog or browser input), Pyodide initialization,
|
||||||
|
* PES file conversion, and error handling.
|
||||||
|
*
|
||||||
|
* @param params - File service, Pyodide state, and callback
|
||||||
|
* @returns Loading state and file change handler
|
||||||
|
*/
|
||||||
|
export function useFileUpload({
|
||||||
|
fileService,
|
||||||
|
pyodideReady,
|
||||||
|
initializePyodide,
|
||||||
|
onFileLoaded,
|
||||||
|
}: UseFileUploadParams): UseFileUploadReturn {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFileChange = useCallback(
|
||||||
|
async (event?: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Wait for Pyodide if it's still loading
|
||||||
|
if (!pyodideReady) {
|
||||||
|
console.log("[FileUpload] Waiting for Pyodide to finish loading...");
|
||||||
|
await initializePyodide();
|
||||||
|
console.log("[FileUpload] Pyodide ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
let file: File | null = null;
|
||||||
|
|
||||||
|
// In Electron, use native file dialogs
|
||||||
|
if (fileService.hasNativeDialogs()) {
|
||||||
|
file = await fileService.openFileDialog({ accept: ".pes" });
|
||||||
|
} else {
|
||||||
|
// In browser, use the input element
|
||||||
|
file = event?.target.files?.[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await convertPesToPen(file);
|
||||||
|
onFileLoaded(data, file.name);
|
||||||
|
} catch (err) {
|
||||||
|
alert(
|
||||||
|
`Failed to load PES file: ${
|
||||||
|
err instanceof Error ? err.message : "Unknown error"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fileService, pyodideReady, initializePyodide, onFileLoaded],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
handleFileChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
145
src/hooks/usePatternRotationUpload.ts
Normal file
145
src/hooks/usePatternRotationUpload.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||||
|
import { transformStitchesRotation } from "../utils/rotationUtils";
|
||||||
|
import { encodeStitchesToPen } from "../formats/pen/encoder";
|
||||||
|
import { decodePenData } from "../formats/pen/decoder";
|
||||||
|
import {
|
||||||
|
calculatePatternCenter,
|
||||||
|
calculateBoundsFromDecodedStitches,
|
||||||
|
} from "../components/PatternCanvas/patternCanvasHelpers";
|
||||||
|
|
||||||
|
export interface UsePatternRotationUploadParams {
|
||||||
|
uploadPattern: (
|
||||||
|
penData: Uint8Array,
|
||||||
|
uploadedPesData: PesPatternData,
|
||||||
|
fileName: string,
|
||||||
|
patternOffset?: { x: number; y: number },
|
||||||
|
patternRotation?: number,
|
||||||
|
originalPesData?: PesPatternData,
|
||||||
|
) => Promise<void>;
|
||||||
|
setUploadedPattern: (
|
||||||
|
pesData: PesPatternData,
|
||||||
|
offset: { x: number; y: number },
|
||||||
|
fileName?: string,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsePatternRotationUploadReturn {
|
||||||
|
handleUpload: (
|
||||||
|
pesData: PesPatternData,
|
||||||
|
displayFileName: string,
|
||||||
|
patternOffset: { x: number; y: number },
|
||||||
|
patternRotation: number,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for handling pattern rotation transformation and upload
|
||||||
|
*
|
||||||
|
* Manages the complex rotation logic including:
|
||||||
|
* - Stitch transformation with rotation
|
||||||
|
* - PEN encoding/decoding for coordinate rounding
|
||||||
|
* - Center shift calculation to maintain visual position
|
||||||
|
* - Upload orchestration with proper caching
|
||||||
|
*
|
||||||
|
* @param params - Upload and store functions
|
||||||
|
* @returns Upload handler function
|
||||||
|
*/
|
||||||
|
export function usePatternRotationUpload({
|
||||||
|
uploadPattern,
|
||||||
|
setUploadedPattern,
|
||||||
|
}: UsePatternRotationUploadParams): UsePatternRotationUploadReturn {
|
||||||
|
const handleUpload = useCallback(
|
||||||
|
async (
|
||||||
|
pesData: PesPatternData,
|
||||||
|
displayFileName: string,
|
||||||
|
patternOffset: { x: number; y: number },
|
||||||
|
patternRotation: number,
|
||||||
|
) => {
|
||||||
|
let penDataToUpload = pesData.penData;
|
||||||
|
let pesDataForUpload = pesData;
|
||||||
|
|
||||||
|
// Apply rotation if needed
|
||||||
|
if (patternRotation && patternRotation !== 0) {
|
||||||
|
// Transform stitches
|
||||||
|
const rotatedStitches = transformStitchesRotation(
|
||||||
|
pesData.stitches,
|
||||||
|
patternRotation,
|
||||||
|
pesData.bounds,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Encode to PEN (this will round coordinates)
|
||||||
|
const penResult = encodeStitchesToPen(rotatedStitches);
|
||||||
|
penDataToUpload = new Uint8Array(penResult.penBytes);
|
||||||
|
|
||||||
|
// Decode back to get the ACTUAL pattern (after PEN rounding)
|
||||||
|
const decoded = decodePenData(penDataToUpload);
|
||||||
|
|
||||||
|
// Calculate bounds from the DECODED stitches (the actual data that will be rendered)
|
||||||
|
const rotatedBounds = calculateBoundsFromDecodedStitches(decoded);
|
||||||
|
|
||||||
|
// Calculate the center of the rotated pattern
|
||||||
|
const originalCenter = calculatePatternCenter(pesData.bounds);
|
||||||
|
const rotatedCenter = calculatePatternCenter(rotatedBounds);
|
||||||
|
const centerShiftX = rotatedCenter.x - originalCenter.x;
|
||||||
|
const centerShiftY = rotatedCenter.y - originalCenter.y;
|
||||||
|
|
||||||
|
// CRITICAL: Adjust position to compensate for the center shift!
|
||||||
|
// In Konva, visual position = (x - offsetX, y - offsetY).
|
||||||
|
// Original visual pos: (x - originalCenter.x, y - originalCenter.y)
|
||||||
|
// New visual pos: (newX - rotatedCenter.x, newY - rotatedCenter.y)
|
||||||
|
// For same visual position: newX = x + (rotatedCenter.x - originalCenter.x)
|
||||||
|
// So we need to add (rotatedCenter - originalCenter) to the position.
|
||||||
|
const adjustedOffset = {
|
||||||
|
x: patternOffset.x + centerShiftX,
|
||||||
|
y: patternOffset.y + centerShiftY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create rotated PesPatternData for upload
|
||||||
|
pesDataForUpload = {
|
||||||
|
...pesData,
|
||||||
|
stitches: rotatedStitches,
|
||||||
|
penData: penDataToUpload,
|
||||||
|
penStitches: decoded,
|
||||||
|
bounds: rotatedBounds,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save uploaded pattern to store for preview BEFORE starting upload
|
||||||
|
// This allows the preview to show immediately when isUploading becomes true
|
||||||
|
setUploadedPattern(pesDataForUpload, adjustedOffset);
|
||||||
|
|
||||||
|
// Upload the pattern with offset
|
||||||
|
// IMPORTANT: Pass original unrotated pesData for caching, rotated pesData for upload
|
||||||
|
await uploadPattern(
|
||||||
|
penDataToUpload,
|
||||||
|
pesDataForUpload,
|
||||||
|
displayFileName,
|
||||||
|
adjustedOffset,
|
||||||
|
patternRotation,
|
||||||
|
pesData, // Original unrotated pattern for caching
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No rotation case
|
||||||
|
// Save uploaded pattern to store BEFORE starting upload
|
||||||
|
setUploadedPattern(pesDataForUpload, patternOffset);
|
||||||
|
|
||||||
|
// Upload the pattern
|
||||||
|
await uploadPattern(
|
||||||
|
penDataToUpload,
|
||||||
|
pesDataForUpload,
|
||||||
|
displayFileName,
|
||||||
|
patternOffset,
|
||||||
|
0, // No rotation
|
||||||
|
// No need to pass originalPesData since it's the same as pesDataForUpload
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[uploadPattern, setUploadedPattern],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUpload,
|
||||||
|
};
|
||||||
|
}
|
||||||
97
src/hooks/usePatternValidation.ts
Normal file
97
src/hooks/usePatternValidation.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||||
|
import type { MachineInfo } from "../types/machine";
|
||||||
|
import { calculateRotatedBounds } from "../utils/rotationUtils";
|
||||||
|
import { calculatePatternCenter } from "../components/PatternCanvas/patternCanvasHelpers";
|
||||||
|
|
||||||
|
export interface PatternBoundsCheckResult {
|
||||||
|
fits: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsePatternValidationParams {
|
||||||
|
pesData: PesPatternData | null;
|
||||||
|
machineInfo: MachineInfo | null;
|
||||||
|
patternOffset: { x: number; y: number };
|
||||||
|
patternRotation: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for validating pattern bounds against hoop size
|
||||||
|
*
|
||||||
|
* Checks if the pattern (with rotation and offset applied) fits within
|
||||||
|
* the machine's hoop bounds and provides detailed error messages if not.
|
||||||
|
*
|
||||||
|
* @param params - Pattern and machine configuration
|
||||||
|
* @returns Bounds check result with fit status and error message
|
||||||
|
*/
|
||||||
|
export function usePatternValidation({
|
||||||
|
pesData,
|
||||||
|
machineInfo,
|
||||||
|
patternOffset,
|
||||||
|
patternRotation,
|
||||||
|
}: UsePatternValidationParams): PatternBoundsCheckResult {
|
||||||
|
// Memoize the bounds check calculation to avoid unnecessary recalculations
|
||||||
|
return useMemo((): PatternBoundsCheckResult => {
|
||||||
|
if (!pesData || !machineInfo) {
|
||||||
|
return { fits: true, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate rotated bounds if rotation is applied
|
||||||
|
let bounds = pesData.bounds;
|
||||||
|
if (patternRotation && patternRotation !== 0) {
|
||||||
|
bounds = calculateRotatedBounds(pesData.bounds, patternRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { maxWidth, maxHeight } = machineInfo;
|
||||||
|
|
||||||
|
// The patternOffset represents the pattern's CENTER position (due to offsetX/offsetY in canvas)
|
||||||
|
// So we need to calculate bounds relative to the center
|
||||||
|
const center = calculatePatternCenter(bounds);
|
||||||
|
|
||||||
|
// Calculate actual bounds in world coordinates
|
||||||
|
const patternMinX = patternOffset.x - center.x + bounds.minX;
|
||||||
|
const patternMaxX = patternOffset.x - center.x + bounds.maxX;
|
||||||
|
const patternMinY = patternOffset.y - center.y + bounds.minY;
|
||||||
|
const patternMaxY = patternOffset.y - center.y + bounds.maxY;
|
||||||
|
|
||||||
|
// Hoop bounds (centered at origin)
|
||||||
|
const hoopMinX = -maxWidth / 2;
|
||||||
|
const hoopMaxX = maxWidth / 2;
|
||||||
|
const hoopMinY = -maxHeight / 2;
|
||||||
|
const hoopMaxY = maxHeight / 2;
|
||||||
|
|
||||||
|
// Check if pattern exceeds hoop bounds
|
||||||
|
const exceedsLeft = patternMinX < hoopMinX;
|
||||||
|
const exceedsRight = patternMaxX > hoopMaxX;
|
||||||
|
const exceedsTop = patternMinY < hoopMinY;
|
||||||
|
const exceedsBottom = patternMaxY > hoopMaxY;
|
||||||
|
|
||||||
|
if (exceedsLeft || exceedsRight || exceedsTop || exceedsBottom) {
|
||||||
|
const directions = [];
|
||||||
|
if (exceedsLeft)
|
||||||
|
directions.push(
|
||||||
|
`left by ${((hoopMinX - patternMinX) / 10).toFixed(1)}mm`,
|
||||||
|
);
|
||||||
|
if (exceedsRight)
|
||||||
|
directions.push(
|
||||||
|
`right by ${((patternMaxX - hoopMaxX) / 10).toFixed(1)}mm`,
|
||||||
|
);
|
||||||
|
if (exceedsTop)
|
||||||
|
directions.push(
|
||||||
|
`top by ${((hoopMinY - patternMinY) / 10).toFixed(1)}mm`,
|
||||||
|
);
|
||||||
|
if (exceedsBottom)
|
||||||
|
directions.push(
|
||||||
|
`bottom by ${((patternMaxY - hoopMaxY) / 10).toFixed(1)}mm`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fits: false,
|
||||||
|
error: `Pattern exceeds hoop bounds: ${directions.join(", ")}. Adjust pattern position in preview.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fits: true, error: null };
|
||||||
|
}, [pesData, machineInfo, patternOffset, patternRotation]);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue