import { describe, it, expect, beforeEach } from "vitest"; import { usePatternStore, selectPatternCenter, selectUploadedPatternCenter, selectRotatedBounds, selectRotationCenterShift, selectPatternValidation, } from "./usePatternStore"; import type { PesPatternData } from "../formats/import/pesImporter"; // Mock pattern data for testing const createMockPesData = ( bounds = { minX: -100, maxX: 100, minY: -50, maxY: 50, }, ): PesPatternData => ({ stitches: [[0, 0, 0, 0]], threads: [], uniqueColors: [], penData: new Uint8Array(), penStitches: { stitches: [], colorBlocks: [], bounds: { minX: 0, maxX: 0, minY: 0, maxY: 0 }, }, colorCount: 1, stitchCount: 1, bounds, }); describe("usePatternStore selectors", () => { beforeEach(() => { // Reset store state before each test const state = usePatternStore.getState(); state.setPattern(createMockPesData(), "test.pes"); state.resetPatternOffset(); state.resetRotation(); state.clearUploadedPattern(); }); describe("selectPatternCenter", () => { it("should return null when no pattern is loaded", () => { // Clear the pattern usePatternStore.setState({ pesData: null }); const center = selectPatternCenter(usePatternStore.getState()); expect(center).toBeNull(); }); it("should calculate center correctly for symmetric bounds", () => { const state = usePatternStore.getState(); const center = selectPatternCenter(state); expect(center).not.toBeNull(); expect(center!.x).toBe(0); // (minX + maxX) / 2 = (-100 + 100) / 2 = 0 expect(center!.y).toBe(0); // (minY + maxY) / 2 = (-50 + 50) / 2 = 0 }); it("should calculate center correctly for asymmetric bounds", () => { const pesData = createMockPesData({ minX: 0, maxX: 200, minY: 0, maxY: 100, }); usePatternStore.setState({ pesData }); const state = usePatternStore.getState(); const center = selectPatternCenter(state); expect(center).not.toBeNull(); expect(center!.x).toBe(100); // (0 + 200) / 2 expect(center!.y).toBe(50); // (0 + 100) / 2 }); }); describe("selectUploadedPatternCenter", () => { it("should return null when no uploaded pattern", () => { const state = usePatternStore.getState(); const center = selectUploadedPatternCenter(state); expect(center).toBeNull(); }); it("should calculate center of uploaded pattern", () => { const uploadedData = createMockPesData({ minX: 50, maxX: 150, minY: 25, maxY: 75, }); usePatternStore .getState() .setUploadedPattern(uploadedData, { x: 0, y: 0 }); const state = usePatternStore.getState(); const center = selectUploadedPatternCenter(state); expect(center).not.toBeNull(); expect(center!.x).toBe(100); // (50 + 150) / 2 expect(center!.y).toBe(50); // (25 + 75) / 2 }); }); describe("selectRotatedBounds", () => { it("should return original bounds when no rotation", () => { const state = usePatternStore.getState(); const result = selectRotatedBounds(state); expect(result).not.toBeNull(); expect(result!.bounds).toEqual({ minX: -100, maxX: 100, minY: -50, maxY: 50, }); expect(result!.center).toEqual({ x: 0, y: 0 }); }); it("should return null when no pattern", () => { usePatternStore.setState({ pesData: null }); const state = usePatternStore.getState(); const result = selectRotatedBounds(state); expect(result).toBeNull(); }); it("should calculate rotated bounds for 90 degree rotation", () => { usePatternStore.getState().setPatternRotation(90); const state = usePatternStore.getState(); const result = selectRotatedBounds(state); expect(result).not.toBeNull(); // After 90° rotation, X and Y bounds should swap expect(result!.bounds.minX).toBeCloseTo(-50, 0); expect(result!.bounds.maxX).toBeCloseTo(50, 0); expect(result!.bounds.minY).toBeCloseTo(-100, 0); expect(result!.bounds.maxY).toBeCloseTo(100, 0); }); it("should expand bounds for 45 degree rotation", () => { usePatternStore.getState().setPatternRotation(45); const state = usePatternStore.getState(); const result = selectRotatedBounds(state); expect(result).not.toBeNull(); // After 45° rotation, bounds should expand expect(Math.abs(result!.bounds.minX)).toBeGreaterThan(100); expect(Math.abs(result!.bounds.minY)).toBeGreaterThan(50); }); }); describe("selectRotationCenterShift", () => { it("should return zero shift when no rotation", () => { const state = usePatternStore.getState(); const rotatedBounds = state.pesData!.bounds; const shift = selectRotationCenterShift(state, rotatedBounds); expect(shift).toEqual({ x: 0, y: 0 }); }); it("should return null when no pattern", () => { usePatternStore.setState({ pesData: null }); const state = usePatternStore.getState(); const shift = selectRotationCenterShift(state, { minX: 0, maxX: 100, minY: 0, maxY: 100, }); expect(shift).toBeNull(); }); it("should calculate center shift for asymmetric pattern", () => { const pesData = createMockPesData({ minX: 0, maxX: 200, minY: 0, maxY: 100, }); usePatternStore.setState({ pesData }); usePatternStore.getState().setPatternRotation(90); const state = usePatternStore.getState(); const rotatedBounds = selectRotatedBounds(state)!.bounds; const shift = selectRotationCenterShift(state, rotatedBounds); expect(shift).not.toBeNull(); // Original center: (100, 50) // After 90° rotation around center, new center should be slightly different // due to the asymmetric bounds expect(shift!.x).toBeCloseTo(0, 0); 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"); }); }); });