mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Compare commits
2 commits
6fbb3ebf1a
...
5296590a45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5296590a45 | ||
|
|
bcb5ea1786 |
3 changed files with 73 additions and 228 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { useMemo } from "react";
|
||||
import type { PesPatternData } from "../../formats/import/pesImporter";
|
||||
import type { MachineInfo } from "../../types/machine";
|
||||
import { usePatternValidationFromStore } from "../../stores/usePatternStore";
|
||||
import { calculateRotatedBounds } from "../../utils/rotationUtils";
|
||||
import { calculatePatternCenter } from "../../components/PatternCanvas/patternCanvasHelpers";
|
||||
|
||||
export interface PatternBoundsCheckResult {
|
||||
fits: boolean;
|
||||
|
|
@ -11,10 +12,8 @@ export interface PatternBoundsCheckResult {
|
|||
export interface UsePatternValidationParams {
|
||||
pesData: PesPatternData | null;
|
||||
machineInfo: MachineInfo | null;
|
||||
// Note: patternOffset and patternRotation are read from the store
|
||||
// These params are kept for backward compatibility but are not used
|
||||
patternOffset?: { x: number; y: number };
|
||||
patternRotation?: number;
|
||||
patternOffset: { x: number; y: number };
|
||||
patternRotation: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,39 +22,76 @@ export interface UsePatternValidationParams {
|
|||
* Checks if the pattern (with rotation and offset applied) fits within
|
||||
* the machine's hoop bounds and provides detailed error messages if not.
|
||||
*
|
||||
* This hook now uses the computed selector from the pattern store for
|
||||
* consistent validation logic across the application.
|
||||
*
|
||||
* @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 {
|
||||
// Use the computed selector from the store for validation
|
||||
// The store selector uses the current state (patternOffset, patternRotation)
|
||||
const validationFromStore = usePatternValidationFromStore(
|
||||
machineInfo
|
||||
? { maxWidth: machineInfo.maxWidth, maxHeight: machineInfo.maxHeight }
|
||||
: null,
|
||||
);
|
||||
|
||||
// Memoize the result to avoid unnecessary recalculations
|
||||
// Memoize the bounds check calculation to avoid unnecessary recalculations
|
||||
return useMemo((): PatternBoundsCheckResult => {
|
||||
if (!pesData || !machineInfo) {
|
||||
return { fits: true, error: null };
|
||||
}
|
||||
|
||||
// Use the validation from store which already has all the logic
|
||||
// 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: validationFromStore.fits,
|
||||
error: validationFromStore.error,
|
||||
fits: false,
|
||||
error: `Pattern exceeds hoop bounds: ${directions.join(", ")}. Adjust pattern position in preview.`,
|
||||
};
|
||||
}, [
|
||||
pesData,
|
||||
machineInfo,
|
||||
validationFromStore.fits,
|
||||
validationFromStore.error,
|
||||
]);
|
||||
}
|
||||
|
||||
return { fits: true, error: null };
|
||||
}, [pesData, machineInfo, patternOffset, patternRotation]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
selectUploadedPatternCenter,
|
||||
selectRotatedBounds,
|
||||
selectRotationCenterShift,
|
||||
selectPatternValidation,
|
||||
} from "./usePatternStore";
|
||||
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||
|
||||
|
|
@ -198,104 +197,4 @@ describe("usePatternStore selectors", () => {
|
|||
expect(shift!.y).toBeCloseTo(0, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectPatternValidation", () => {
|
||||
const machineInfo = { maxWidth: 1000, maxHeight: 800 };
|
||||
|
||||
it("should return fits=true when no pattern", () => {
|
||||
usePatternStore.setState({ pesData: null });
|
||||
const state = usePatternStore.getState();
|
||||
const result = selectPatternValidation(state, machineInfo);
|
||||
|
||||
expect(result.fits).toBe(true);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it("should return fits=true when no machine info", () => {
|
||||
const state = usePatternStore.getState();
|
||||
const result = selectPatternValidation(state, null);
|
||||
|
||||
expect(result.fits).toBe(true);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it("should return fits=true when pattern fits in hoop", () => {
|
||||
// Pattern bounds: -100 to 100 (200 wide), -50 to 50 (100 high)
|
||||
// Hoop: 1000 wide, 800 high (centered at origin)
|
||||
const state = usePatternStore.getState();
|
||||
const result = selectPatternValidation(state, machineInfo);
|
||||
|
||||
expect(result.fits).toBe(true);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it("should detect when pattern exceeds hoop bounds", () => {
|
||||
// Create a pattern that's too large
|
||||
const pesData = createMockPesData({
|
||||
minX: -600,
|
||||
maxX: 600,
|
||||
minY: -500,
|
||||
maxY: 500,
|
||||
});
|
||||
usePatternStore.setState({ pesData });
|
||||
|
||||
const state = usePatternStore.getState();
|
||||
const result = selectPatternValidation(state, machineInfo);
|
||||
|
||||
expect(result.fits).toBe(false);
|
||||
expect(result.error).not.toBeNull();
|
||||
expect(result.error).toContain("exceeds hoop bounds");
|
||||
});
|
||||
|
||||
it("should account for pattern offset when validating", () => {
|
||||
// Pattern bounds: -100 to 100 (200 wide), -50 to 50 (100 high)
|
||||
// Hoop: 1000 wide (-500 to 500), 800 high (-400 to 400)
|
||||
// Pattern fits, but when offset by 450, max edge is at 550 (exceeds 500)
|
||||
usePatternStore.getState().setPatternOffset(450, 0);
|
||||
|
||||
const state = usePatternStore.getState();
|
||||
const result = selectPatternValidation(state, machineInfo);
|
||||
|
||||
expect(result.fits).toBe(false);
|
||||
expect(result.error).toContain("right");
|
||||
});
|
||||
|
||||
it("should account for rotation when validating", () => {
|
||||
// Pattern that fits normally but exceeds when rotated 45°
|
||||
const pesData = createMockPesData({
|
||||
minX: -450,
|
||||
maxX: 450,
|
||||
minY: -50,
|
||||
maxY: 50,
|
||||
});
|
||||
usePatternStore.setState({ pesData });
|
||||
usePatternStore.getState().setPatternRotation(45);
|
||||
|
||||
const state = usePatternStore.getState();
|
||||
const result = selectPatternValidation(state, machineInfo);
|
||||
|
||||
// After 45° rotation, the bounds expand and may exceed
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("should provide detailed error messages with directions", () => {
|
||||
// Pattern that definitely exceeds on the left side
|
||||
// Hoop: -500 to 500 (X), -400 to 400 (Y)
|
||||
// Pattern with minX at -600 and maxX at 600 will exceed both bounds
|
||||
const pesData = createMockPesData({
|
||||
minX: -600,
|
||||
maxX: 600,
|
||||
minY: -50,
|
||||
maxY: 50,
|
||||
});
|
||||
usePatternStore.setState({ pesData });
|
||||
|
||||
const state = usePatternStore.getState();
|
||||
const result = selectPatternValidation(state, machineInfo);
|
||||
|
||||
expect(result.fits).toBe(false);
|
||||
expect(result.error).toContain("left");
|
||||
expect(result.error).toContain("mm");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { create } from "zustand";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||
import { onPatternDeleted } from "./storeEvents";
|
||||
import { calculatePatternCenter } from "../components/PatternCanvas/patternCanvasHelpers";
|
||||
|
|
@ -49,17 +50,6 @@ export interface TransformedBounds {
|
|||
center: PatternCenter;
|
||||
}
|
||||
|
||||
export interface PatternValidationResult {
|
||||
fits: boolean;
|
||||
error: string | null;
|
||||
worldBounds: {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const usePatternStore = create<PatternState>((set) => ({
|
||||
// Initial state - original pattern
|
||||
pesData: null,
|
||||
|
|
@ -219,102 +209,22 @@ export const selectRotationCenterShift = (
|
|||
};
|
||||
|
||||
/**
|
||||
* Select pattern validation against machine hoop bounds
|
||||
* Returns whether pattern fits and error message if not
|
||||
* Hook to get pattern center (memoized with shallow comparison)
|
||||
*/
|
||||
export const selectPatternValidation = (
|
||||
state: PatternState,
|
||||
machineInfo: { maxWidth: number; maxHeight: number } | null,
|
||||
): PatternValidationResult => {
|
||||
if (!state.pesData || !machineInfo) {
|
||||
return { fits: true, error: null, worldBounds: null };
|
||||
}
|
||||
|
||||
// Get rotated bounds
|
||||
const transformedBounds = selectRotatedBounds(state);
|
||||
if (!transformedBounds) {
|
||||
return { fits: true, error: null, worldBounds: null };
|
||||
}
|
||||
|
||||
const { bounds, center } = transformedBounds;
|
||||
const { maxWidth, maxHeight } = machineInfo;
|
||||
|
||||
// Calculate actual bounds in world coordinates
|
||||
// The patternOffset represents the pattern's CENTER position (due to offsetX/offsetY in canvas)
|
||||
const patternMinX = state.patternOffset.x - center.x + bounds.minX;
|
||||
const patternMaxX = state.patternOffset.x - center.x + bounds.maxX;
|
||||
const patternMinY = state.patternOffset.y - center.y + bounds.minY;
|
||||
const patternMaxY = state.patternOffset.y - center.y + bounds.maxY;
|
||||
|
||||
const worldBounds = {
|
||||
minX: patternMinX,
|
||||
maxX: patternMaxX,
|
||||
minY: patternMinY,
|
||||
maxY: patternMaxY,
|
||||
};
|
||||
|
||||
// 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.`,
|
||||
worldBounds,
|
||||
};
|
||||
}
|
||||
|
||||
return { fits: true, error: null, worldBounds };
|
||||
};
|
||||
export const usePatternCenter = () =>
|
||||
usePatternStore(useShallow(selectPatternCenter));
|
||||
|
||||
/**
|
||||
* Hook to get pattern center (memoized)
|
||||
*/
|
||||
export const usePatternCenter = () => usePatternStore(selectPatternCenter);
|
||||
|
||||
/**
|
||||
* Hook to get uploaded pattern center (memoized)
|
||||
* Hook to get uploaded pattern center (memoized with shallow comparison)
|
||||
*/
|
||||
export const useUploadedPatternCenter = () =>
|
||||
usePatternStore(selectUploadedPatternCenter);
|
||||
usePatternStore(useShallow(selectUploadedPatternCenter));
|
||||
|
||||
/**
|
||||
* Hook to get rotated bounds (memoized)
|
||||
* Hook to get rotated bounds (memoized with shallow comparison)
|
||||
*/
|
||||
export const useRotatedBounds = () => usePatternStore(selectRotatedBounds);
|
||||
|
||||
/**
|
||||
* Hook to get pattern validation result (requires machineInfo)
|
||||
* Use this with caution as it requires external state
|
||||
*/
|
||||
export const usePatternValidationFromStore = (
|
||||
machineInfo: { maxWidth: number; maxHeight: number } | null,
|
||||
) => usePatternStore((state) => selectPatternValidation(state, machineInfo));
|
||||
export const useRotatedBounds = () =>
|
||||
usePatternStore(useShallow(selectRotatedBounds));
|
||||
|
||||
// Subscribe to pattern deleted event.
|
||||
// This subscription is intended to persist for the lifetime of the application,
|
||||
|
|
|
|||
Loading…
Reference in a new issue