diff --git a/src/components/ProgressMonitor/ProgressMonitor.tsx b/src/components/ProgressMonitor/ProgressMonitor.tsx index b15e1dc..78ed296 100644 --- a/src/components/ProgressMonitor/ProgressMonitor.tsx +++ b/src/components/ProgressMonitor/ProgressMonitor.tsx @@ -12,6 +12,10 @@ import { usePatternStore } from "../../stores/usePatternStore"; import { ChartBarIcon } from "@heroicons/react/24/solid"; import { MachineStatus } from "../../types/machine"; import { calculatePatternTime } from "../../utils/timeCalculation"; +import { + calculateColorBlocks, + findCurrentBlockIndex, +} from "../../utils/colorBlockHelpers"; import { Card, CardHeader, @@ -23,7 +27,6 @@ import { ProgressStats } from "./ProgressStats"; import { ProgressSection } from "./ProgressSection"; import { ColorBlockList } from "./ColorBlockList"; import { ProgressActions } from "./ProgressActions"; -import type { ColorBlock } from "./types"; export function ProgressMonitor() { // Machine store @@ -69,36 +72,14 @@ export function ProgressMonitor() { : 0; // Calculate color block information from decoded penStitches - const colorBlocks = useMemo(() => { - if (!displayPattern || !displayPattern.penStitches) return []; - - const blocks: ColorBlock[] = []; - - // Use the pre-computed color blocks from decoded PEN data - for (const penBlock of displayPattern.penStitches.colorBlocks) { - const thread = displayPattern.threads[penBlock.colorIndex]; - blocks.push({ - colorIndex: penBlock.colorIndex, - threadHex: thread?.hex || "#000000", - threadCatalogNumber: thread?.catalogNumber ?? null, - threadBrand: thread?.brand ?? null, - threadDescription: thread?.description ?? null, - threadChart: thread?.chart ?? null, - startStitch: penBlock.startStitchIndex, - endStitch: penBlock.endStitchIndex, - stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex, - }); - } - - return blocks; - }, [displayPattern]); + const colorBlocks = useMemo( + () => calculateColorBlocks(displayPattern), + [displayPattern], + ); // Determine current color block based on current stitch const currentStitch = sewingProgress?.currentStitch || 0; - const currentBlockIndex = colorBlocks.findIndex( - (block) => - currentStitch >= block.startStitch && currentStitch < block.endStitch, - ); + const currentBlockIndex = findCurrentBlockIndex(colorBlocks, currentStitch); // Calculate time based on color blocks (matches Brother app calculation) const { totalMinutes, elapsedMinutes } = useMemo(() => { diff --git a/src/utils/colorBlockHelpers.test.ts b/src/utils/colorBlockHelpers.test.ts new file mode 100644 index 0000000..8eb4af2 --- /dev/null +++ b/src/utils/colorBlockHelpers.test.ts @@ -0,0 +1,241 @@ +import { describe, it, expect } from "vitest"; +import { + calculateColorBlocks, + findCurrentBlockIndex, +} from "./colorBlockHelpers"; +import type { PesPatternData } from "../formats/import/client"; + +describe("colorBlockHelpers", () => { + describe("calculateColorBlocks", () => { + it("should return empty array when displayPattern is null", () => { + const result = calculateColorBlocks(null); + expect(result).toEqual([]); + }); + + it("should return empty array when penStitches is undefined", () => { + const pattern = { + penStitches: undefined, + } as unknown as PesPatternData; + + const result = calculateColorBlocks(pattern); + expect(result).toEqual([]); + }); + + it("should calculate color blocks from PEN data", () => { + const pattern: Partial = { + threads: [ + { + color: 1, + hex: "#FF0000", + brand: "Brother", + catalogNumber: "001", + description: "Red", + chart: "A", + }, + { + color: 2, + hex: "#00FF00", + brand: "Brother", + catalogNumber: "002", + description: "Green", + chart: "B", + }, + ], + penStitches: { + colorBlocks: [ + { + startStitchIndex: 0, + endStitchIndex: 100, + colorIndex: 0, + startStitch: 0, + endStitch: 100, + }, + { + startStitchIndex: 100, + endStitchIndex: 250, + colorIndex: 1, + startStitch: 100, + endStitch: 250, + }, + ], + stitches: [], + bounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 }, + }, + }; + + const result = calculateColorBlocks(pattern as PesPatternData); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + colorIndex: 0, + threadHex: "#FF0000", + threadCatalogNumber: "001", + threadBrand: "Brother", + threadDescription: "Red", + threadChart: "A", + startStitch: 0, + endStitch: 100, + stitchCount: 100, + }); + expect(result[1]).toEqual({ + colorIndex: 1, + threadHex: "#00FF00", + threadCatalogNumber: "002", + threadBrand: "Brother", + threadDescription: "Green", + threadChart: "B", + startStitch: 100, + endStitch: 250, + stitchCount: 150, + }); + }); + + it("should use fallback values when thread data is missing", () => { + const pattern: Partial = { + threads: [], + penStitches: { + colorBlocks: [ + { + startStitchIndex: 0, + endStitchIndex: 50, + colorIndex: 0, + startStitch: 0, + endStitch: 50, + }, + ], + stitches: [], + bounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 }, + }, + }; + + const result = calculateColorBlocks(pattern as PesPatternData); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + colorIndex: 0, + threadHex: "#000000", // Fallback for missing thread + threadCatalogNumber: null, + threadBrand: null, + threadDescription: null, + threadChart: null, + startStitch: 0, + endStitch: 50, + stitchCount: 50, + }); + }); + + it("should handle null thread metadata fields", () => { + const pattern: Partial = { + threads: [ + { + color: 1, + hex: "#0000FF", + brand: null, + catalogNumber: null, + description: null, + chart: null, + }, + ], + penStitches: { + colorBlocks: [ + { + startStitchIndex: 0, + endStitchIndex: 30, + colorIndex: 0, + startStitch: 0, + endStitch: 30, + }, + ], + stitches: [], + bounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 }, + }, + }; + + const result = calculateColorBlocks(pattern as PesPatternData); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + colorIndex: 0, + threadHex: "#0000FF", + threadCatalogNumber: null, + threadBrand: null, + threadDescription: null, + threadChart: null, + startStitch: 0, + endStitch: 30, + stitchCount: 30, + }); + }); + }); + + describe("findCurrentBlockIndex", () => { + const colorBlocks = [ + { + colorIndex: 0, + threadHex: "#FF0000", + threadCatalogNumber: "001", + threadBrand: "Brother", + threadDescription: "Red", + threadChart: "A", + startStitch: 0, + endStitch: 100, + stitchCount: 100, + }, + { + colorIndex: 1, + threadHex: "#00FF00", + threadCatalogNumber: "002", + threadBrand: "Brother", + threadDescription: "Green", + threadChart: "B", + startStitch: 100, + endStitch: 250, + stitchCount: 150, + }, + { + colorIndex: 2, + threadHex: "#0000FF", + threadCatalogNumber: "003", + threadBrand: "Brother", + threadDescription: "Blue", + threadChart: "C", + startStitch: 250, + endStitch: 400, + stitchCount: 150, + }, + ]; + + it("should find block containing stitch at start boundary", () => { + expect(findCurrentBlockIndex(colorBlocks, 0)).toBe(0); + expect(findCurrentBlockIndex(colorBlocks, 100)).toBe(1); + expect(findCurrentBlockIndex(colorBlocks, 250)).toBe(2); + }); + + it("should find block containing stitch in middle", () => { + expect(findCurrentBlockIndex(colorBlocks, 50)).toBe(0); + expect(findCurrentBlockIndex(colorBlocks, 150)).toBe(1); + expect(findCurrentBlockIndex(colorBlocks, 300)).toBe(2); + }); + + it("should return -1 for stitch before first block", () => { + expect(findCurrentBlockIndex(colorBlocks, -1)).toBe(-1); + }); + + it("should return -1 for stitch at or after last block end", () => { + expect(findCurrentBlockIndex(colorBlocks, 400)).toBe(-1); + expect(findCurrentBlockIndex(colorBlocks, 500)).toBe(-1); + }); + + it("should return -1 for empty color blocks array", () => { + expect(findCurrentBlockIndex([], 50)).toBe(-1); + }); + + it("should find block with single color block", () => { + const singleBlock = [colorBlocks[0]]; + expect(findCurrentBlockIndex(singleBlock, 0)).toBe(0); + expect(findCurrentBlockIndex(singleBlock, 50)).toBe(0); + expect(findCurrentBlockIndex(singleBlock, 99)).toBe(0); + expect(findCurrentBlockIndex(singleBlock, 100)).toBe(-1); + }); + }); +}); diff --git a/src/utils/colorBlockHelpers.ts b/src/utils/colorBlockHelpers.ts new file mode 100644 index 0000000..20d2b22 --- /dev/null +++ b/src/utils/colorBlockHelpers.ts @@ -0,0 +1,61 @@ +/** + * Color Block Helpers + * + * Utility functions for calculating color block information from pattern data. + * Extracted from ProgressMonitor component for better testability and reusability. + */ + +import type { PesPatternData } from "../formats/import/client"; +import type { ColorBlock } from "../components/ProgressMonitor/types"; + +/** + * Calculate color blocks from decoded PEN pattern data + * + * Transforms PEN color blocks into enriched ColorBlock objects with thread metadata. + * Returns an empty array if pattern or penStitches data is unavailable. + * + * @param displayPattern - The PES pattern data containing penStitches and threads + * @returns Array of ColorBlock objects with thread information and stitch counts + */ +export function calculateColorBlocks( + displayPattern: PesPatternData | null, +): ColorBlock[] { + if (!displayPattern || !displayPattern.penStitches) return []; + + const blocks: ColorBlock[] = []; + + // Use the pre-computed color blocks from decoded PEN data + for (const penBlock of displayPattern.penStitches.colorBlocks) { + const thread = displayPattern.threads[penBlock.colorIndex]; + blocks.push({ + colorIndex: penBlock.colorIndex, + threadHex: thread?.hex || "#000000", + threadCatalogNumber: thread?.catalogNumber ?? null, + threadBrand: thread?.brand ?? null, + threadDescription: thread?.description ?? null, + threadChart: thread?.chart ?? null, + startStitch: penBlock.startStitchIndex, + endStitch: penBlock.endStitchIndex, + stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex, + }); + } + + return blocks; +} + +/** + * Find the index of the color block containing a specific stitch + * + * @param colorBlocks - Array of color blocks to search + * @param currentStitch - The stitch index to find + * @returns The index of the containing block, or -1 if not found + */ +export function findCurrentBlockIndex( + colorBlocks: ColorBlock[], + currentStitch: number, +): number { + return colorBlocks.findIndex( + (block) => + currentStitch >= block.startStitch && currentStitch < block.endStitch, + ); +}