mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
refactor: Extract color block calculation logic to utility module
Extract color block calculation from ProgressMonitor component to colorBlockHelpers utility module for better testability and reusability. Changes: - Created colorBlockHelpers.ts with calculateColorBlocks() and findCurrentBlockIndex() - Added comprehensive unit tests (11 test cases, all passing) - Updated ProgressMonitor to use new utility functions - Reduced component complexity by removing embedded business logic Benefits: - Logic can be tested in isolation - Can be reused elsewhere if needed - Cleaner component code - Better separation of concerns Fixes #44 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d213ab49e2
commit
101f46e627
3 changed files with 311 additions and 28 deletions
|
|
@ -12,6 +12,10 @@ import { usePatternStore } from "../../stores/usePatternStore";
|
||||||
import { ChartBarIcon } from "@heroicons/react/24/solid";
|
import { ChartBarIcon } from "@heroicons/react/24/solid";
|
||||||
import { MachineStatus } from "../../types/machine";
|
import { MachineStatus } from "../../types/machine";
|
||||||
import { calculatePatternTime } from "../../utils/timeCalculation";
|
import { calculatePatternTime } from "../../utils/timeCalculation";
|
||||||
|
import {
|
||||||
|
calculateColorBlocks,
|
||||||
|
findCurrentBlockIndex,
|
||||||
|
} from "../../utils/colorBlockHelpers";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
|
@ -23,7 +27,6 @@ import { ProgressStats } from "./ProgressStats";
|
||||||
import { ProgressSection } from "./ProgressSection";
|
import { ProgressSection } from "./ProgressSection";
|
||||||
import { ColorBlockList } from "./ColorBlockList";
|
import { ColorBlockList } from "./ColorBlockList";
|
||||||
import { ProgressActions } from "./ProgressActions";
|
import { ProgressActions } from "./ProgressActions";
|
||||||
import type { ColorBlock } from "./types";
|
|
||||||
|
|
||||||
export function ProgressMonitor() {
|
export function ProgressMonitor() {
|
||||||
// Machine store
|
// Machine store
|
||||||
|
|
@ -69,36 +72,14 @@ export function ProgressMonitor() {
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Calculate color block information from decoded penStitches
|
// Calculate color block information from decoded penStitches
|
||||||
const colorBlocks = useMemo(() => {
|
const colorBlocks = useMemo(
|
||||||
if (!displayPattern || !displayPattern.penStitches) return [];
|
() => calculateColorBlocks(displayPattern),
|
||||||
|
[displayPattern],
|
||||||
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]);
|
|
||||||
|
|
||||||
// Determine current color block based on current stitch
|
// Determine current color block based on current stitch
|
||||||
const currentStitch = sewingProgress?.currentStitch || 0;
|
const currentStitch = sewingProgress?.currentStitch || 0;
|
||||||
const currentBlockIndex = colorBlocks.findIndex(
|
const currentBlockIndex = findCurrentBlockIndex(colorBlocks, currentStitch);
|
||||||
(block) =>
|
|
||||||
currentStitch >= block.startStitch && currentStitch < block.endStitch,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate time based on color blocks (matches Brother app calculation)
|
// Calculate time based on color blocks (matches Brother app calculation)
|
||||||
const { totalMinutes, elapsedMinutes } = useMemo(() => {
|
const { totalMinutes, elapsedMinutes } = useMemo(() => {
|
||||||
|
|
|
||||||
241
src/utils/colorBlockHelpers.test.ts
Normal file
241
src/utils/colorBlockHelpers.test.ts
Normal file
|
|
@ -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<PesPatternData> = {
|
||||||
|
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<PesPatternData> = {
|
||||||
|
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<PesPatternData> = {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
61
src/utils/colorBlockHelpers.ts
Normal file
61
src/utils/colorBlockHelpers.ts
Normal file
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue