mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
301 lines
9.4 KiB
TypeScript
301 lines
9.4 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|