mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Merge pull request #59 from jhbruhn/refactor/extract-color-block-helpers
refactor: Extract color block calculation logic to utility module
This commit is contained in:
commit
e49a63a4b1
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 { 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(() => {
|
||||
|
|
|
|||
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