Compare commits

..

No commits in common. "91bc0285e0367bb11f9dbcddf6e217d3366fcea3" and "7fd31d209cbd5784ffe5c515c8a0506b74f3eed2" have entirely different histories.

18 changed files with 340 additions and 867 deletions

View file

@ -4,12 +4,7 @@
"Bash(npm run build:*)", "Bash(npm run build:*)",
"Bash(npm run lint)", "Bash(npm run lint)",
"Bash(cat:*)", "Bash(cat:*)",
"Bash(npm run dev:electron:*)", "Bash(npm run dev:electron:*)"
"Bash(npm run lint:*)",
"Bash(npm test:*)",
"Bash(npm run:*)",
"Bash(gh issue create:*)",
"Bash(gh label create:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View file

@ -1,7 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { useMachineStore } from "./stores/useMachineStore"; import { useMachineStore } from "./stores/useMachineStore";
import { useMachineCacheStore } from "./stores/useMachineCacheStore";
import { usePatternStore } from "./stores/usePatternStore"; import { usePatternStore } from "./stores/usePatternStore";
import { useUIStore } from "./stores/useUIStore"; import { useUIStore } from "./stores/useUIStore";
import { AppHeader } from "./components/AppHeader"; import { AppHeader } from "./components/AppHeader";
@ -9,13 +8,6 @@ import { LeftSidebar } from "./components/LeftSidebar";
import { PatternCanvas } from "./components/PatternCanvas"; import { PatternCanvas } from "./components/PatternCanvas";
import { PatternCanvasPlaceholder } from "./components/PatternCanvasPlaceholder"; import { PatternCanvasPlaceholder } from "./components/PatternCanvasPlaceholder";
import { BluetoothDevicePicker } from "./components/BluetoothDevicePicker"; import { BluetoothDevicePicker } from "./components/BluetoothDevicePicker";
import { transformStitchesRotation } from "./utils/rotationUtils";
import { encodeStitchesToPen } from "./formats/pen/encoder";
import { decodePenData } from "./formats/pen/decoder";
import {
calculatePatternCenter,
calculateBoundsFromDecodedStitches,
} from "./components/PatternCanvas/patternCanvasHelpers";
import "./App.css"; import "./App.css";
function App() { function App() {
@ -24,13 +16,8 @@ function App() {
document.title = `Respira v${__APP_VERSION__}`; document.title = `Respira v${__APP_VERSION__}`;
}, []); }, []);
// Initialize machine store subscriptions (once on mount) // Machine store - for auto-loading cached pattern
useEffect(() => { const { resumedPattern, resumeFileName } = useMachineStore(
useMachineStore.getState().initialize();
}, []);
// Machine cache store - for auto-loading cached pattern
const { resumedPattern, resumeFileName } = useMachineCacheStore(
useShallow((state) => ({ useShallow((state) => ({
resumedPattern: state.resumedPattern, resumedPattern: state.resumedPattern,
resumeFileName: state.resumeFileName, resumeFileName: state.resumeFileName,
@ -38,20 +25,10 @@ function App() {
); );
// Pattern store - for auto-loading cached pattern // Pattern store - for auto-loading cached pattern
const { const { pesData, setPattern, setPatternOffset } = usePatternStore(
pesData,
uploadedPesData,
setPattern,
setUploadedPattern,
setPatternRotation,
setPatternOffset,
} = usePatternStore(
useShallow((state) => ({ useShallow((state) => ({
pesData: state.pesData, pesData: state.pesData,
uploadedPesData: state.uploadedPesData,
setPattern: state.setPattern, setPattern: state.setPattern,
setUploadedPattern: state.setUploadedPattern,
setPatternRotation: state.setPatternRotation,
setPatternOffset: state.setPatternOffset, setPatternOffset: state.setPatternOffset,
})), })),
); );
@ -70,130 +47,23 @@ function App() {
// Auto-load cached pattern when available // Auto-load cached pattern when available
useEffect(() => { useEffect(() => {
// Only auto-load if we have a resumed pattern and haven't already loaded it if (resumedPattern && !pesData) {
if (resumedPattern && !uploadedPesData && !pesData) {
if (!resumedPattern.pesData) {
console.error(
"[App] ERROR: resumedPattern has no pesData!",
resumedPattern,
);
return;
}
console.log( console.log(
"[App] Loading resumed pattern:", "[App] Loading resumed pattern:",
resumeFileName, resumeFileName,
"Offset:", "Offset:",
resumedPattern.patternOffset, resumedPattern.patternOffset,
"Rotation:",
resumedPattern.patternRotation,
"Has stitches:",
resumedPattern.pesData.stitches?.length || 0,
"Has cached uploaded data:",
!!resumedPattern.uploadedPesData,
); );
setPattern(resumedPattern.pesData, resumeFileName || "");
const originalPesData = resumedPattern.pesData; // Restore the cached pattern offset
const cachedUploadedPesData = resumedPattern.uploadedPesData; if (resumedPattern.patternOffset) {
const rotation = resumedPattern.patternRotation || 0; setPatternOffset(
const originalOffset = resumedPattern.patternOffset || { x: 0, y: 0 }; resumedPattern.patternOffset.x,
resumedPattern.patternOffset.y,
// Set the original pattern data for editing
setPattern(originalPesData, resumeFileName || "");
// Restore the original offset (setPattern resets it to 0,0)
setPatternOffset(originalOffset.x, originalOffset.y);
// Set rotation if present
if (rotation !== 0) {
setPatternRotation(rotation);
}
// Use cached uploadedPesData if available, otherwise recalculate
if (cachedUploadedPesData) {
// Use the exact uploaded data from cache
// Calculate the adjusted offset (same logic as upload)
if (rotation !== 0) {
const originalCenter = calculatePatternCenter(originalPesData.bounds);
const rotatedCenter = calculatePatternCenter(
cachedUploadedPesData.bounds,
);
const centerShiftX = rotatedCenter.x - originalCenter.x;
const centerShiftY = rotatedCenter.y - originalCenter.y;
const adjustedOffset = {
x: originalOffset.x + centerShiftX,
y: originalOffset.y + centerShiftY,
};
setUploadedPattern(
cachedUploadedPesData,
adjustedOffset,
resumeFileName || undefined,
);
} else {
setUploadedPattern(
cachedUploadedPesData,
originalOffset,
resumeFileName || undefined,
);
}
} else if (rotation !== 0) {
// Fallback: recalculate if no cached uploaded data (shouldn't happen for new uploads)
console.warn("[App] No cached uploaded data, recalculating rotation");
const rotatedStitches = transformStitchesRotation(
originalPesData.stitches,
rotation,
originalPesData.bounds,
);
const penResult = encodeStitchesToPen(rotatedStitches);
const penData = new Uint8Array(penResult.penBytes);
const decoded = decodePenData(penData);
const rotatedBounds = calculateBoundsFromDecodedStitches(decoded);
const originalCenter = calculatePatternCenter(originalPesData.bounds);
const rotatedCenter = calculatePatternCenter(rotatedBounds);
const centerShiftX = rotatedCenter.x - originalCenter.x;
const centerShiftY = rotatedCenter.y - originalCenter.y;
const adjustedOffset = {
x: originalOffset.x + centerShiftX,
y: originalOffset.y + centerShiftY,
};
const rotatedPesData = {
...originalPesData,
stitches: rotatedStitches,
penData,
penStitches: decoded,
bounds: rotatedBounds,
};
setUploadedPattern(
rotatedPesData,
adjustedOffset,
resumeFileName || undefined,
);
} else {
// No rotation - uploaded pattern is same as original
setUploadedPattern(
originalPesData,
originalOffset,
resumeFileName || undefined,
); );
} }
} }
}, [ }, [resumedPattern, resumeFileName, pesData, setPattern, setPatternOffset]);
resumedPattern,
resumeFileName,
uploadedPesData,
pesData,
setPattern,
setUploadedPattern,
setPatternRotation,
setPatternOffset,
]);
return ( return (
<div className="h-screen flex flex-col bg-gray-100 dark:bg-gray-900 overflow-hidden"> <div className="h-screen flex flex-col bg-gray-100 dark:bg-gray-900 overflow-hidden">
@ -206,11 +76,7 @@ function App() {
{/* Right Column - Pattern Preview */} {/* Right Column - Pattern Preview */}
<div className="flex flex-col lg:overflow-hidden lg:h-full"> <div className="flex flex-col lg:overflow-hidden lg:h-full">
{pesData || uploadedPesData ? ( {pesData ? <PatternCanvas /> : <PatternCanvasPlaceholder />}
<PatternCanvas />
) : (
<PatternCanvasPlaceholder />
)}
</div> </div>
</div> </div>

View file

@ -1,8 +1,6 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { useMachineStore, usePatternUploaded } from "../stores/useMachineStore"; import { useMachineStore, usePatternUploaded } from "../stores/useMachineStore";
import { useMachineUploadStore } from "../stores/useMachineUploadStore";
import { useMachineCacheStore } from "../stores/useMachineCacheStore";
import { usePatternStore } from "../stores/usePatternStore"; import { usePatternStore } from "../stores/usePatternStore";
import { useUIStore } from "../stores/useUIStore"; import { useUIStore } from "../stores/useUIStore";
import { import {
@ -42,28 +40,25 @@ import { cn } from "@/lib/utils";
export function FileUpload() { export function FileUpload() {
// Machine store // Machine store
const { isConnected, machineStatus, machineInfo } = useMachineStore( const {
isConnected,
machineStatus,
uploadProgress,
isUploading,
machineInfo,
resumeAvailable,
resumeFileName,
uploadPattern,
} = useMachineStore(
useShallow((state) => ({ useShallow((state) => ({
isConnected: state.isConnected, isConnected: state.isConnected,
machineStatus: state.machineStatus, machineStatus: state.machineStatus,
machineInfo: state.machineInfo,
})),
);
// Machine upload store
const { uploadProgress, isUploading, uploadPattern } = useMachineUploadStore(
useShallow((state) => ({
uploadProgress: state.uploadProgress, uploadProgress: state.uploadProgress,
isUploading: state.isUploading, isUploading: state.isUploading,
uploadPattern: state.uploadPattern, machineInfo: state.machineInfo,
})),
);
// Machine cache store
const { resumeAvailable, resumeFileName } = useMachineCacheStore(
useShallow((state) => ({
resumeAvailable: state.resumeAvailable, resumeAvailable: state.resumeAvailable,
resumeFileName: state.resumeFileName, resumeFileName: state.resumeFileName,
uploadPattern: state.uploadPattern,
})), })),
); );
@ -211,14 +206,11 @@ export function FileUpload() {
setUploadedPattern(pesDataForUpload, adjustedOffset); setUploadedPattern(pesDataForUpload, adjustedOffset);
// Upload the pattern with offset // Upload the pattern with offset
// IMPORTANT: Pass original unrotated pesData for caching, rotated pesData for upload
uploadPattern( uploadPattern(
penDataToUpload, penDataToUpload,
pesDataForUpload, pesDataForUpload,
displayFileName, displayFileName,
adjustedOffset, adjustedOffset,
patternRotation,
pesData, // Original unrotated pattern for caching
); );
return; // Early return to skip the upload below return; // Early return to skip the upload below
@ -234,8 +226,6 @@ export function FileUpload() {
pesDataForUpload, pesDataForUpload,
displayFileName, displayFileName,
patternOffset, patternOffset,
0, // No rotation
// No need to pass originalPesData since it's the same as pesDataForUpload
); );
} }
}, [ }, [

View file

@ -13,16 +13,14 @@ export function LeftSidebar() {
})), })),
); );
const { pesData, uploadedPesData } = usePatternStore( const { pesData } = usePatternStore(
useShallow((state) => ({ useShallow((state) => ({
pesData: state.pesData, pesData: state.pesData,
uploadedPesData: state.uploadedPesData,
})), })),
); );
// Derived state: pattern is uploaded if machine has pattern info // Derived state: pattern is uploaded if machine has pattern info
const patternUploaded = usePatternUploaded(); const patternUploaded = usePatternUploaded();
const hasPattern = pesData || uploadedPesData;
return ( return (
<div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden"> <div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
@ -33,7 +31,7 @@ export function LeftSidebar() {
{isConnected && !patternUploaded && <FileUpload />} {isConnected && !patternUploaded && <FileUpload />}
{/* Compact Pattern Summary - Show after upload (during sewing stages) */} {/* Compact Pattern Summary - Show after upload (during sewing stages) */}
{isConnected && patternUploaded && hasPattern && <PatternSummaryCard />} {isConnected && patternUploaded && pesData && <PatternSummaryCard />}
{/* Progress Monitor - Show when pattern is uploaded */} {/* Progress Monitor - Show when pattern is uploaded */}
{isConnected && patternUploaded && ( {isConnected && patternUploaded && (

View file

@ -157,77 +157,6 @@ export const Stitches = memo(
currentStitchIndex, currentStitchIndex,
showProgress = false, showProgress = false,
}: StitchesProps) => { }: StitchesProps) => {
// PERFORMANCE OPTIMIZATION:
// Separate static group structure (doesn't change during sewing)
// from dynamic completion status (changes with currentStitchIndex).
// This prevents recalculating all groups on every progress update.
//
// For very large patterns (>100k stitches), consider:
// - Virtualization: render only visible stitches based on viewport
// - LOD (Level of Detail): reduce stitch density when zoomed out
// - Web Workers: offload grouping calculations to background thread
interface StaticStitchGroup {
color: string;
points: number[];
isJump: boolean;
startIndex: number; // First stitch index in this group
endIndex: number; // Last stitch index in this group
}
// Static grouping - only recalculates when stitches or colors change
const staticGroups = useMemo(() => {
const groups: StaticStitchGroup[] = [];
let currentGroup: StaticStitchGroup | null = null;
let prevX = 0;
let prevY = 0;
for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i];
const [x, y, cmd, colorIndex] = stitch;
const isJump = (cmd & MOVE) !== 0;
const color = getThreadColor(pesData, colorIndex);
// Start new group if color or type changes (NOT completion status)
if (
!currentGroup ||
currentGroup.color !== color ||
currentGroup.isJump !== isJump
) {
// For jump stitches, include previous position
if (isJump && i > 0) {
currentGroup = {
color,
points: [prevX, prevY, x, y],
isJump,
startIndex: i,
endIndex: i,
};
} else {
currentGroup = {
color,
points: [x, y],
isJump,
startIndex: i,
endIndex: i,
};
}
groups.push(currentGroup);
} else {
currentGroup.points.push(x, y);
currentGroup.endIndex = i;
}
prevX = x;
prevY = y;
}
return groups;
}, [stitches, pesData]);
// Dynamic grouping - adds completion status based on currentStitchIndex
// Only needs to check group boundaries, not rebuild everything
const stitchGroups = useMemo(() => { const stitchGroups = useMemo(() => {
interface StitchGroup { interface StitchGroup {
color: string; color: string;
@ -237,75 +166,53 @@ export const Stitches = memo(
} }
const groups: StitchGroup[] = []; const groups: StitchGroup[] = [];
let currentGroup: StitchGroup | null = null;
for (const staticGroup of staticGroups) { let prevX = 0;
// Check if this group needs to be split based on completion let prevY = 0;
for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i];
const [x, y, cmd, colorIndex] = stitch;
const isCompleted = i < currentStitchIndex;
const isJump = (cmd & MOVE) !== 0;
const color = getThreadColor(pesData, colorIndex);
// Start new group if color/status/type changes
if ( if (
currentStitchIndex > staticGroup.startIndex && !currentGroup ||
currentStitchIndex <= staticGroup.endIndex currentGroup.color !== color ||
currentGroup.completed !== isCompleted ||
currentGroup.isJump !== isJump
) { ) {
// Group is partially completed - need to split // For jump stitches, we need to create a line from previous position to current position
// This is rare during sewing (only happens when crossing group boundaries) // So we include both the previous point and current point
if (isJump && i > 0) {
// Rebuild this group with completion split currentGroup = {
let currentSubGroup: StitchGroup | null = null; color,
const groupStitches = stitches.slice(
staticGroup.startIndex,
staticGroup.endIndex + 1,
);
let prevX =
staticGroup.startIndex > 0
? stitches[staticGroup.startIndex - 1][0]
: 0;
let prevY =
staticGroup.startIndex > 0
? stitches[staticGroup.startIndex - 1][1]
: 0;
for (let i = 0; i < groupStitches.length; i++) {
const absoluteIndex = staticGroup.startIndex + i;
const stitch = groupStitches[i];
const [x, y] = stitch;
const isCompleted = absoluteIndex < currentStitchIndex;
if (!currentSubGroup || currentSubGroup.completed !== isCompleted) {
if (staticGroup.isJump && i > 0) {
currentSubGroup = {
color: staticGroup.color,
points: [prevX, prevY, x, y], points: [prevX, prevY, x, y],
completed: isCompleted, completed: isCompleted,
isJump: staticGroup.isJump, isJump,
}; };
} else { } else {
currentSubGroup = { currentGroup = {
color: staticGroup.color, color,
points: [x, y], points: [x, y],
completed: isCompleted, completed: isCompleted,
isJump: staticGroup.isJump, isJump,
}; };
} }
groups.push(currentSubGroup); groups.push(currentGroup);
} else { } else {
currentSubGroup.points.push(x, y); currentGroup.points.push(x, y);
} }
prevX = x; prevX = x;
prevY = y; prevY = y;
} }
} else {
// Group is fully completed or fully incomplete
groups.push({
color: staticGroup.color,
points: staticGroup.points,
completed: currentStitchIndex > staticGroup.endIndex,
isJump: staticGroup.isJump,
});
}
}
return groups; return groups;
}, [staticGroups, currentStitchIndex, stitches]); }, [stitches, pesData, currentStitchIndex]);
return ( return (
<Group name="stitches"> <Group name="stitches">
@ -332,16 +239,6 @@ export const Stitches = memo(
</Group> </Group>
); );
}, },
// Custom comparison to prevent unnecessary re-renders
(prevProps, nextProps) => {
// Re-render only if these values actually changed
return (
prevProps.stitches === nextProps.stitches &&
prevProps.pesData === nextProps.pesData &&
prevProps.currentStitchIndex === nextProps.currentStitchIndex &&
prevProps.showProgress === nextProps.showProgress
);
},
); );
Stitches.displayName = "Stitches"; Stitches.displayName = "Stitches";

View file

@ -4,7 +4,6 @@ import {
useMachineStore, useMachineStore,
usePatternUploaded, usePatternUploaded,
} from "../../stores/useMachineStore"; } from "../../stores/useMachineStore";
import { useMachineUploadStore } from "../../stores/useMachineUploadStore";
import { usePatternStore } from "../../stores/usePatternStore"; import { usePatternStore } from "../../stores/usePatternStore";
import { Stage, Layer } from "react-konva"; import { Stage, Layer } from "react-konva";
import Konva from "konva"; import Konva from "konva";
@ -26,16 +25,10 @@ import { usePatternTransform } from "../../hooks/usePatternTransform";
export function PatternCanvas() { export function PatternCanvas() {
// Machine store // Machine store
const { sewingProgress, machineInfo } = useMachineStore( const { sewingProgress, machineInfo, isUploading } = useMachineStore(
useShallow((state) => ({ useShallow((state) => ({
sewingProgress: state.sewingProgress, sewingProgress: state.sewingProgress,
machineInfo: state.machineInfo, machineInfo: state.machineInfo,
})),
);
// Machine upload store
const { isUploading } = useMachineUploadStore(
useShallow((state) => ({
isUploading: state.isUploading, isUploading: state.isUploading,
})), })),
); );
@ -197,9 +190,7 @@ export function PatternCanvas() {
</Layer> </Layer>
{/* Original pattern layer: draggable with transformer (shown before upload starts) */} {/* Original pattern layer: draggable with transformer (shown before upload starts) */}
<Layer <Layer visible={!isUploading && !patternUploaded}>
visible={!isUploading && !patternUploaded && !uploadedPesData}
>
{pesData && ( {pesData && (
<PatternLayer <PatternLayer
pesData={pesData} pesData={pesData}
@ -218,9 +209,7 @@ export function PatternCanvas() {
</Layer> </Layer>
{/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */} {/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */}
<Layer <Layer visible={isUploading || patternUploaded}>
visible={isUploading || patternUploaded || !!uploadedPesData}
>
{uploadedPesData && ( {uploadedPesData && (
<PatternLayer <PatternLayer
pesData={uploadedPesData} pesData={uploadedPesData}
@ -252,12 +241,12 @@ export function PatternCanvas() {
<PatternPositionIndicator <PatternPositionIndicator
offset={ offset={
isUploading || patternUploaded || uploadedPesData isUploading || patternUploaded
? initialUploadedPatternOffset ? initialUploadedPatternOffset
: localPatternOffset : localPatternOffset
} }
rotation={localPatternRotation} rotation={localPatternRotation}
isLocked={patternUploaded || !!uploadedPesData} isLocked={patternUploaded}
isUploading={isUploading} isUploading={isUploading}
/> />
@ -268,10 +257,7 @@ export function PatternCanvas() {
onZoomReset={handleZoomReset} onZoomReset={handleZoomReset}
onCenterPattern={handleCenterPattern} onCenterPattern={handleCenterPattern}
canCenterPattern={ canCenterPattern={
!!pesData && !!pesData && !patternUploaded && !isUploading
!patternUploaded &&
!isUploading &&
!uploadedPesData
} }
/> />
</> </>

View file

@ -25,16 +25,14 @@ export function PatternSummaryCard() {
); );
// Pattern store // Pattern store
const { pesData, uploadedPesData, currentFileName } = usePatternStore( const { pesData, currentFileName } = usePatternStore(
useShallow((state) => ({ useShallow((state) => ({
pesData: state.pesData, pesData: state.pesData,
uploadedPesData: state.uploadedPesData,
currentFileName: state.currentFileName, currentFileName: state.currentFileName,
})), })),
); );
const displayPattern = uploadedPesData || pesData; if (!pesData) return null;
if (!displayPattern) return null;
const canDelete = canDeletePattern(machineStatus); const canDelete = canDeletePattern(machineStatus);
return ( return (
@ -54,7 +52,7 @@ export function PatternSummaryCard() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pt-0 pb-4"> <CardContent className="px-4 pt-0 pb-4">
<PatternInfo pesData={displayPattern} /> <PatternInfo pesData={pesData} />
{canDelete && ( {canDelete && (
<Button <Button

View file

@ -52,8 +52,6 @@ export function ProgressMonitor() {
// Pattern store // Pattern store
const pesData = usePatternStore((state) => state.pesData); const pesData = usePatternStore((state) => state.pesData);
const uploadedPesData = usePatternStore((state) => state.uploadedPesData);
const displayPattern = uploadedPesData || pesData;
const currentBlockRef = useRef<HTMLDivElement>(null); const currentBlockRef = useRef<HTMLDivElement>(null);
// State indicators // State indicators
@ -62,8 +60,8 @@ export function ProgressMonitor() {
// Use PEN stitch count as fallback when machine reports 0 total stitches // Use PEN stitch count as fallback when machine reports 0 total stitches
const totalStitches = patternInfo const totalStitches = patternInfo
? patternInfo.totalStitches === 0 && displayPattern?.penStitches ? patternInfo.totalStitches === 0 && pesData?.penStitches
? displayPattern.penStitches.stitches.length ? pesData.penStitches.stitches.length
: patternInfo.totalStitches : patternInfo.totalStitches
: 0; : 0;
@ -74,7 +72,7 @@ export function ProgressMonitor() {
// 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 []; if (!pesData || !pesData.penStitches) return [];
const blocks: Array<{ const blocks: Array<{
colorIndex: number; colorIndex: number;
@ -89,8 +87,8 @@ export function ProgressMonitor() {
}> = []; }> = [];
// Use the pre-computed color blocks from decoded PEN data // Use the pre-computed color blocks from decoded PEN data
for (const penBlock of displayPattern.penStitches.colorBlocks) { for (const penBlock of pesData.penStitches.colorBlocks) {
const thread = displayPattern.threads[penBlock.colorIndex]; const thread = pesData.threads[penBlock.colorIndex];
blocks.push({ blocks.push({
colorIndex: penBlock.colorIndex, colorIndex: penBlock.colorIndex,
threadHex: thread?.hex || "#000000", threadHex: thread?.hex || "#000000",
@ -105,7 +103,7 @@ export function ProgressMonitor() {
} }
return blocks; return blocks;
}, [displayPattern]); }, [pesData]);
// 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;

View file

@ -5,7 +5,13 @@
* Handles wheel zoom and button zoom operations * Handles wheel zoom and button zoom operations
*/ */
import { useState, useEffect, useCallback, type RefObject } from "react"; import {
useState,
useEffect,
useCallback,
useRef,
type RefObject,
} from "react";
import type Konva from "konva"; import type Konva from "konva";
import type { PesPatternData } from "../formats/import/pesImporter"; import type { PesPatternData } from "../formats/import/pesImporter";
import type { MachineInfo } from "../types/machine"; import type { MachineInfo } from "../types/machine";
@ -28,11 +34,8 @@ export function useCanvasViewport({
const [stagePos, setStagePos] = useState({ x: 0, y: 0 }); const [stagePos, setStagePos] = useState({ x: 0, y: 0 });
const [stageScale, setStageScale] = useState(1); const [stageScale, setStageScale] = useState(1);
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
const [initialScale, setInitialScale] = useState(1); const initialScaleRef = useRef<number>(1);
const prevPesDataRef = useRef<PesPatternData | null>(null);
// Track the last processed pattern to detect changes during render
const [lastProcessedPattern, setLastProcessedPattern] =
useState<PesPatternData | null>(null);
// Track container size with ResizeObserver // Track container size with ResizeObserver
useEffect(() => { useEffect(() => {
@ -56,14 +59,19 @@ export function useCanvasViewport({
return () => resizeObserver.disconnect(); return () => resizeObserver.disconnect();
}, [containerRef]); }, [containerRef]);
// Reset viewport when pattern changes (during render, not in effect) // Calculate and store initial scale when pattern or hoop changes
// This follows the React-recommended pattern for deriving state from props useEffect(() => {
// Use whichever pattern is available (uploaded or original)
const currentPattern = uploadedPesData || pesData; const currentPattern = uploadedPesData || pesData;
if ( if (!currentPattern || containerSize.width === 0) {
currentPattern && prevPesDataRef.current = null;
currentPattern !== lastProcessedPattern && return;
containerSize.width > 0 }
) {
// Only recalculate if pattern changed
if (prevPesDataRef.current !== currentPattern) {
prevPesDataRef.current = currentPattern;
const { bounds } = currentPattern; const { bounds } = currentPattern;
const viewWidth = machineInfo const viewWidth = machineInfo
? machineInfo.maxWidth ? machineInfo.maxWidth
@ -72,20 +80,20 @@ export function useCanvasViewport({
? machineInfo.maxHeight ? machineInfo.maxHeight
: bounds.maxY - bounds.minY; : bounds.maxY - bounds.minY;
const newInitialScale = calculateInitialScale( const initialScale = calculateInitialScale(
containerSize.width, containerSize.width,
containerSize.height, containerSize.height,
viewWidth, viewWidth,
viewHeight, viewHeight,
); );
initialScaleRef.current = initialScale;
// Update state during render when pattern changes // Reset view when pattern changes
// This is the recommended React pattern for resetting state based on props // eslint-disable-next-line react-hooks/set-state-in-effect
setLastProcessedPattern(currentPattern); setStageScale(initialScale);
setInitialScale(newInitialScale);
setStageScale(newInitialScale);
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 }); setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
} }
}, [pesData, uploadedPesData, machineInfo, containerSize]);
// Wheel zoom handler // Wheel zoom handler
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => { const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
@ -151,9 +159,10 @@ export function useCanvasViewport({
}, [containerSize]); }, [containerSize]);
const handleZoomReset = useCallback(() => { const handleZoomReset = useCallback(() => {
const initialScale = initialScaleRef.current;
setStageScale(initialScale); setStageScale(initialScale);
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 }); setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
}, [initialScale, containerSize]); }, [containerSize]);
return { return {
// State // State

View file

@ -39,41 +39,22 @@ export function usePatternTransform({
const patternGroupRef = useRef<Konva.Group | null>(null); const patternGroupRef = useRef<Konva.Group | null>(null);
const transformerRef = useRef<Konva.Transformer | null>(null); const transformerRef = useRef<Konva.Transformer | null>(null);
// Track previous prop values to detect external changes // Update pattern offset when initialPatternOffset changes
const prevOffsetRef = useRef(initialPatternOffset);
const prevRotationRef = useRef(initialPatternRotation);
// Sync local state with parent props when they change externally
// This implements a "partially controlled" pattern needed for Konva drag interactions:
// - Local state enables optimistic updates during drag/transform (immediate visual feedback)
// - Parent props sync when external changes occur (e.g., pattern upload resets position)
// - Previous value refs prevent sync loops by only updating when props genuinely change
useEffect(() => {
if ( if (
initialPatternOffset && initialPatternOffset &&
(prevOffsetRef.current.x !== initialPatternOffset.x || (localPatternOffset.x !== initialPatternOffset.x ||
prevOffsetRef.current.y !== initialPatternOffset.y) localPatternOffset.y !== initialPatternOffset.y)
) { ) {
// This setState in effect is intentional and safe: it only runs when the parent
// prop changes, not in response to our own local updates
// eslint-disable-next-line react-hooks/set-state-in-effect
setLocalPatternOffset(initialPatternOffset); setLocalPatternOffset(initialPatternOffset);
prevOffsetRef.current = initialPatternOffset;
} }
}, [initialPatternOffset]);
useEffect(() => { // Update pattern rotation when initialPatternRotation changes
if ( if (
initialPatternRotation !== undefined && initialPatternRotation !== undefined &&
prevRotationRef.current !== initialPatternRotation localPatternRotation !== initialPatternRotation
) { ) {
// This setState in effect is intentional and safe: it only runs when the parent
// prop changes, not in response to our own local updates
// eslint-disable-next-line react-hooks/set-state-in-effect
setLocalPatternRotation(initialPatternRotation); setLocalPatternRotation(initialPatternRotation);
prevRotationRef.current = initialPatternRotation;
} }
}, [initialPatternRotation]);
// Attach/detach transformer based on state // Attach/detach transformer based on state
const attachTransformer = useCallback(() => { const attachTransformer = useCallback(() => {

View file

@ -15,17 +15,8 @@ export class BrowserStorageService implements IStorageService {
pesData: PesPatternData, pesData: PesPatternData,
fileName: string, fileName: string,
patternOffset?: { x: number; y: number }, patternOffset?: { x: number; y: number },
patternRotation?: number,
uploadedPesData?: PesPatternData,
): Promise<void> { ): Promise<void> {
PatternCacheService.savePattern( PatternCacheService.savePattern(uuid, pesData, fileName, patternOffset);
uuid,
pesData,
fileName,
patternOffset,
patternRotation,
uploadedPesData,
);
} }
async getPatternByUUID(uuid: string): Promise<ICachedPattern | null> { async getPatternByUUID(uuid: string): Promise<ICachedPattern | null> {

View file

@ -20,8 +20,6 @@ export class ElectronStorageService implements IStorageService {
pesData: PesPatternData, pesData: PesPatternData,
fileName: string, fileName: string,
patternOffset?: { x: number; y: number }, patternOffset?: { x: number; y: number },
patternRotation?: number,
uploadedPesData?: PesPatternData,
): Promise<void> { ): Promise<void> {
// Convert Uint8Array to array for JSON serialization over IPC // Convert Uint8Array to array for JSON serialization over IPC
const serializable = { const serializable = {
@ -30,16 +28,9 @@ export class ElectronStorageService implements IStorageService {
...pesData, ...pesData,
penData: Array.from(pesData.penData), penData: Array.from(pesData.penData),
}, },
uploadedPesData: uploadedPesData
? {
...uploadedPesData,
penData: Array.from(uploadedPesData.penData),
}
: undefined,
fileName, fileName,
timestamp: Date.now(), timestamp: Date.now(),
patternOffset, patternOffset,
patternRotation,
}; };
// Fire and forget (sync-like behavior to match interface) // Fire and forget (sync-like behavior to match interface)
@ -60,17 +51,6 @@ export class ElectronStorageService implements IStorageService {
pattern.pesData.penData = new Uint8Array(pattern.pesData.penData); pattern.pesData.penData = new Uint8Array(pattern.pesData.penData);
} }
if (
pattern &&
pattern.uploadedPesData &&
Array.isArray(pattern.uploadedPesData.penData)
) {
// Restore Uint8Array from array for uploadedPesData
pattern.uploadedPesData.penData = new Uint8Array(
pattern.uploadedPesData.penData,
);
}
return pattern; return pattern;
} catch (err) { } catch (err) {
console.error("[ElectronStorage] Failed to get pattern:", err); console.error("[ElectronStorage] Failed to get pattern:", err);
@ -89,17 +69,6 @@ export class ElectronStorageService implements IStorageService {
pattern.pesData.penData = new Uint8Array(pattern.pesData.penData); pattern.pesData.penData = new Uint8Array(pattern.pesData.penData);
} }
if (
pattern &&
pattern.uploadedPesData &&
Array.isArray(pattern.uploadedPesData.penData)
) {
// Restore Uint8Array from array for uploadedPesData
pattern.uploadedPesData.penData = new Uint8Array(
pattern.uploadedPesData.penData,
);
}
return pattern; return pattern;
} catch (err) { } catch (err) {
console.error("[ElectronStorage] Failed to get latest pattern:", err); console.error("[ElectronStorage] Failed to get latest pattern:", err);

View file

@ -2,12 +2,10 @@ import type { PesPatternData } from "../../formats/import/pesImporter";
export interface ICachedPattern { export interface ICachedPattern {
uuid: string; uuid: string;
pesData: PesPatternData; // Original unrotated pattern data pesData: PesPatternData;
uploadedPesData?: PesPatternData; // Pattern with rotation applied (what was uploaded to machine)
fileName: string; fileName: string;
timestamp: number; timestamp: number;
patternOffset?: { x: number; y: number }; patternOffset?: { x: number; y: number };
patternRotation?: number; // Rotation angle in degrees
} }
export interface IStorageService { export interface IStorageService {
@ -16,8 +14,6 @@ export interface IStorageService {
pesData: PesPatternData, pesData: PesPatternData,
fileName: string, fileName: string,
patternOffset?: { x: number; y: number }, patternOffset?: { x: number; y: number },
patternRotation?: number,
uploadedPesData?: PesPatternData,
): Promise<void>; ): Promise<void>;
getPatternByUUID(uuid: string): Promise<ICachedPattern | null>; getPatternByUUID(uuid: string): Promise<ICachedPattern | null>;

View file

@ -2,12 +2,10 @@ import type { PesPatternData } from "../formats/import/pesImporter";
interface CachedPattern { interface CachedPattern {
uuid: string; uuid: string;
pesData: PesPatternData; // Original unrotated pattern data pesData: PesPatternData;
uploadedPesData?: PesPatternData; // Pattern with rotation applied (what was uploaded to machine)
fileName: string; fileName: string;
timestamp: number; timestamp: number;
patternOffset?: { x: number; y: number }; patternOffset?: { x: number; y: number };
patternRotation?: number; // Rotation angle in degrees
} }
const CACHE_KEY = "brother_pattern_cache"; const CACHE_KEY = "brother_pattern_cache";
@ -41,8 +39,6 @@ export class PatternCacheService {
pesData: PesPatternData, pesData: PesPatternData,
fileName: string, fileName: string,
patternOffset?: { x: number; y: number }, patternOffset?: { x: number; y: number },
patternRotation?: number,
uploadedPesData?: PesPatternData,
): void { ): void {
try { try {
// Convert penData Uint8Array to array for JSON serialization // Convert penData Uint8Array to array for JSON serialization
@ -51,24 +47,12 @@ export class PatternCacheService {
penData: Array.from(pesData.penData) as unknown as Uint8Array, penData: Array.from(pesData.penData) as unknown as Uint8Array,
}; };
// Also convert uploadedPesData if present
const uploadedPesDataWithArrayPenData = uploadedPesData
? {
...uploadedPesData,
penData: Array.from(
uploadedPesData.penData,
) as unknown as Uint8Array,
}
: undefined;
const cached: CachedPattern = { const cached: CachedPattern = {
uuid, uuid,
pesData: pesDataWithArrayPenData, pesData: pesDataWithArrayPenData,
uploadedPesData: uploadedPesDataWithArrayPenData,
fileName, fileName,
timestamp: Date.now(), timestamp: Date.now(),
patternOffset, patternOffset,
patternRotation,
}; };
localStorage.setItem(CACHE_KEY, JSON.stringify(cached)); localStorage.setItem(CACHE_KEY, JSON.stringify(cached));
@ -79,10 +63,6 @@ export class PatternCacheService {
uuid, uuid,
"Offset:", "Offset:",
patternOffset, patternOffset,
"Rotation:",
patternRotation,
"Has uploaded data:",
!!uploadedPesData,
); );
} catch (err) { } catch (err) {
console.error("[PatternCache] Failed to save pattern:", err); console.error("[PatternCache] Failed to save pattern:", err);
@ -121,23 +101,11 @@ export class PatternCacheService {
pattern.pesData.penData = new Uint8Array(pattern.pesData.penData); pattern.pesData.penData = new Uint8Array(pattern.pesData.penData);
} }
// Restore Uint8Array from array inside uploadedPesData if present
if (
pattern.uploadedPesData &&
Array.isArray(pattern.uploadedPesData.penData)
) {
pattern.uploadedPesData.penData = new Uint8Array(
pattern.uploadedPesData.penData,
);
}
console.log( console.log(
"[PatternCache] Found cached pattern:", "[PatternCache] Found cached pattern:",
pattern.fileName, pattern.fileName,
"UUID:", "UUID:",
uuid, uuid,
"Has uploaded data:",
!!pattern.uploadedPesData,
); );
return pattern; return pattern;
} catch (err) { } catch (err) {
@ -163,16 +131,6 @@ export class PatternCacheService {
pattern.pesData.penData = new Uint8Array(pattern.pesData.penData); pattern.pesData.penData = new Uint8Array(pattern.pesData.penData);
} }
// Restore Uint8Array from array inside uploadedPesData if present
if (
pattern.uploadedPesData &&
Array.isArray(pattern.uploadedPesData.penData)
) {
pattern.uploadedPesData.penData = new Uint8Array(
pattern.uploadedPesData.penData,
);
}
return pattern; return pattern;
} catch (err) { } catch (err) {
console.error("[PatternCache] Failed to retrieve pattern:", err); console.error("[PatternCache] Failed to retrieve pattern:", err);

View file

@ -1,194 +0,0 @@
import { create } from "zustand";
import type { PesPatternData } from "../formats/import/pesImporter";
import { uuidToString } from "../services/PatternCacheService";
/**
* Machine Cache Store
*
* Manages pattern caching and resume functionality.
* Handles checking for cached patterns on the machine and loading them.
* Extracted from useMachineStore for better separation of concerns.
*/
interface MachineCacheState {
// Resume state
resumeAvailable: boolean;
resumeFileName: string | null;
resumedPattern: {
pesData: PesPatternData;
uploadedPesData?: PesPatternData;
patternOffset?: { x: number; y: number };
patternRotation?: number;
} | null;
// Actions
checkResume: () => Promise<PesPatternData | null>;
loadCachedPattern: () => Promise<{
pesData: PesPatternData;
uploadedPesData?: PesPatternData;
patternOffset?: { x: number; y: number };
patternRotation?: number;
} | null>;
// Helper methods for inter-store communication
setResumeAvailable: (available: boolean, fileName: string | null) => void;
clearResumeState: () => void;
}
export const useMachineCacheStore = create<MachineCacheState>((set, get) => ({
// Initial state
resumeAvailable: false,
resumeFileName: null,
resumedPattern: null,
/**
* Check for resumable pattern on the machine
* Queries the machine for its current pattern UUID and checks if we have it cached
*/
checkResume: async (): Promise<PesPatternData | null> => {
try {
// Import here to avoid circular dependency
const { useMachineStore } = await import("./useMachineStore");
const { service, storageService } = useMachineStore.getState();
console.log("[Resume] Checking for cached pattern...");
const machineUuid = await service.getPatternUUID();
console.log(
"[Resume] Machine UUID:",
machineUuid ? uuidToString(machineUuid) : "none",
);
if (!machineUuid) {
console.log("[Resume] No pattern loaded on machine");
set({ resumeAvailable: false, resumeFileName: null });
return null;
}
const uuidStr = uuidToString(machineUuid);
const cached = await storageService.getPatternByUUID(uuidStr);
if (cached) {
console.log(
"[Resume] Pattern found in cache:",
cached.fileName,
"Offset:",
cached.patternOffset,
"Rotation:",
cached.patternRotation,
"Has uploaded data:",
!!cached.uploadedPesData,
);
console.log("[Resume] Auto-loading cached pattern...");
set({
resumeAvailable: true,
resumeFileName: cached.fileName,
resumedPattern: {
pesData: cached.pesData,
uploadedPesData: cached.uploadedPesData,
patternOffset: cached.patternOffset,
patternRotation: cached.patternRotation,
},
});
// Fetch pattern info from machine
try {
const info = await service.getPatternInfo();
useMachineStore.setState({ patternInfo: info });
console.log("[Resume] Pattern info loaded from machine");
} catch (err) {
console.error("[Resume] Failed to load pattern info:", err);
}
return cached.pesData;
} else {
console.log("[Resume] Pattern on machine not found in cache");
set({ resumeAvailable: false, resumeFileName: null });
return null;
}
} catch (err) {
console.error("[Resume] Failed to check resume:", err);
set({ resumeAvailable: false, resumeFileName: null });
return null;
}
},
/**
* Load cached pattern data
* Used when the user wants to restore a previously uploaded pattern
*/
loadCachedPattern: async (): Promise<{
pesData: PesPatternData;
uploadedPesData?: PesPatternData;
patternOffset?: { x: number; y: number };
patternRotation?: number;
} | null> => {
const { resumeAvailable } = get();
if (!resumeAvailable) return null;
try {
// Import here to avoid circular dependency
const { useMachineStore } = await import("./useMachineStore");
const { service, storageService, refreshPatternInfo } =
useMachineStore.getState();
const machineUuid = await service.getPatternUUID();
if (!machineUuid) return null;
const uuidStr = uuidToString(machineUuid);
const cached = await storageService.getPatternByUUID(uuidStr);
if (cached) {
console.log(
"[Resume] Loading cached pattern:",
cached.fileName,
"Offset:",
cached.patternOffset,
"Rotation:",
cached.patternRotation,
"Has uploaded data:",
!!cached.uploadedPesData,
);
await refreshPatternInfo();
return {
pesData: cached.pesData,
uploadedPesData: cached.uploadedPesData,
patternOffset: cached.patternOffset,
patternRotation: cached.patternRotation,
};
}
return null;
} catch (err) {
console.error(
"[Resume] Failed to load cached pattern:",
err instanceof Error ? err.message : "Unknown error",
);
return null;
}
},
/**
* Set resume availability
* Used by other stores to update resume state
*/
setResumeAvailable: (available: boolean, fileName: string | null) => {
set({
resumeAvailable: available,
resumeFileName: fileName,
...(available === false && { resumedPattern: null }),
});
},
/**
* Clear resume state
* Called when pattern is deleted from machine
*/
clearResumeState: () => {
set({
resumeAvailable: false,
resumeFileName: null,
resumedPattern: null,
});
},
}));

View file

@ -13,6 +13,7 @@ import { SewingMachineError } from "../utils/errorCodeHelpers";
import { uuidToString } from "../services/PatternCacheService"; import { uuidToString } from "../services/PatternCacheService";
import { createStorageService } from "../platform"; import { createStorageService } from "../platform";
import type { IStorageService } from "../platform/interfaces/IStorageService"; import type { IStorageService } from "../platform/interfaces/IStorageService";
import type { PesPatternData } from "../formats/import/pesImporter";
import { usePatternStore } from "./usePatternStore"; import { usePatternStore } from "./usePatternStore";
interface MachineState { interface MachineState {
@ -33,6 +34,18 @@ interface MachineState {
patternInfo: PatternInfo | null; patternInfo: PatternInfo | null;
sewingProgress: SewingProgress | null; sewingProgress: SewingProgress | null;
// Upload state
uploadProgress: number;
isUploading: boolean;
// Resume state
resumeAvailable: boolean;
resumeFileName: string | null;
resumedPattern: {
pesData: PesPatternData;
patternOffset?: { x: number; y: number };
} | null;
// Error state // Error state
error: string | null; error: string | null;
isPairingError: boolean; isPairingError: boolean;
@ -52,13 +65,21 @@ interface MachineState {
refreshPatternInfo: () => Promise<void>; refreshPatternInfo: () => Promise<void>;
refreshProgress: () => Promise<void>; refreshProgress: () => Promise<void>;
refreshServiceCount: () => Promise<void>; refreshServiceCount: () => Promise<void>;
uploadPattern: (
penData: Uint8Array,
pesData: PesPatternData,
fileName: string,
patternOffset?: { x: number; y: number },
) => Promise<void>;
startMaskTrace: () => Promise<void>; startMaskTrace: () => Promise<void>;
startSewing: () => Promise<void>; startSewing: () => Promise<void>;
resumeSewing: () => Promise<void>; resumeSewing: () => Promise<void>;
deletePattern: () => Promise<void>; deletePattern: () => Promise<void>;
checkResume: () => Promise<PesPatternData | null>;
// Initialization loadCachedPattern: () => Promise<{
initialize: () => void; pesData: PesPatternData;
patternOffset?: { x: number; y: number };
} | null>;
// Internal methods // Internal methods
_setupSubscriptions: () => void; _setupSubscriptions: () => void;
@ -77,6 +98,11 @@ export const useMachineStore = create<MachineState>((set, get) => ({
machineError: SewingMachineError.None, machineError: SewingMachineError.None,
patternInfo: null, patternInfo: null,
sewingProgress: null, sewingProgress: null,
uploadProgress: 0,
isUploading: false,
resumeAvailable: false,
resumeFileName: null,
resumedPattern: null,
error: null, error: null,
isPairingError: false, isPairingError: false,
isCommunicating: false, isCommunicating: false,
@ -84,10 +110,70 @@ export const useMachineStore = create<MachineState>((set, get) => ({
pollIntervalId: null, pollIntervalId: null,
serviceCountIntervalId: null, serviceCountIntervalId: null,
// Check for resumable pattern
checkResume: async (): Promise<PesPatternData | null> => {
try {
const { service, storageService } = get();
console.log("[Resume] Checking for cached pattern...");
const machineUuid = await service.getPatternUUID();
console.log(
"[Resume] Machine UUID:",
machineUuid ? uuidToString(machineUuid) : "none",
);
if (!machineUuid) {
console.log("[Resume] No pattern loaded on machine");
set({ resumeAvailable: false, resumeFileName: null });
return null;
}
const uuidStr = uuidToString(machineUuid);
const cached = await storageService.getPatternByUUID(uuidStr);
if (cached) {
console.log(
"[Resume] Pattern found in cache:",
cached.fileName,
"Offset:",
cached.patternOffset,
);
console.log("[Resume] Auto-loading cached pattern...");
set({
resumeAvailable: true,
resumeFileName: cached.fileName,
resumedPattern: {
pesData: cached.pesData,
patternOffset: cached.patternOffset,
},
});
// Fetch pattern info from machine
try {
const info = await service.getPatternInfo();
set({ patternInfo: info });
console.log("[Resume] Pattern info loaded from machine");
} catch (err) {
console.error("[Resume] Failed to load pattern info:", err);
}
return cached.pesData;
} else {
console.log("[Resume] Pattern on machine not found in cache");
set({ resumeAvailable: false, resumeFileName: null });
return null;
}
} catch (err) {
console.error("[Resume] Failed to check resume:", err);
set({ resumeAvailable: false, resumeFileName: null });
return null;
}
},
// Connect to machine // Connect to machine
connect: async () => { connect: async () => {
try { try {
const { service } = get(); const { service, checkResume } = get();
set({ error: null, isPairingError: false }); set({ error: null, isPairingError: false });
await service.connect(); await service.connect();
@ -104,9 +190,8 @@ export const useMachineStore = create<MachineState>((set, get) => ({
machineError: state.error, machineError: state.error,
}); });
// Check for resume possibility using cache store // Check for resume possibility
const { useMachineCacheStore } = await import("./useMachineCacheStore"); await checkResume();
await useMachineCacheStore.getState().checkResume();
// Start polling // Start polling
get()._startPolling(); get()._startPolling();
@ -214,6 +299,69 @@ export const useMachineStore = create<MachineState>((set, get) => ({
} }
}, },
// Upload pattern to machine
uploadPattern: async (
penData: Uint8Array,
pesData: PesPatternData,
fileName: string,
patternOffset?: { x: number; y: number },
) => {
const {
isConnected,
service,
storageService,
refreshStatus,
refreshPatternInfo,
} = get();
if (!isConnected) {
set({ error: "Not connected to machine" });
return;
}
try {
set({ error: null, uploadProgress: 0, isUploading: true });
const uuid = await service.uploadPattern(
penData,
(progress) => {
set({ uploadProgress: progress });
},
pesData.bounds,
patternOffset,
);
set({ uploadProgress: 100 });
// Cache the pattern with its UUID and offset
const uuidStr = uuidToString(uuid);
storageService.savePattern(uuidStr, pesData, fileName, patternOffset);
console.log(
"[Cache] Saved pattern:",
fileName,
"with UUID:",
uuidStr,
"Offset:",
patternOffset,
);
// Clear resume state since we just uploaded
set({
resumeAvailable: false,
resumeFileName: null,
});
// Refresh status and pattern info after upload
await refreshStatus();
await refreshPatternInfo();
} catch (err) {
set({
error: err instanceof Error ? err.message : "Failed to upload pattern",
});
} finally {
set({ isUploading: false });
}
},
// Start mask trace // Start mask trace
startMaskTrace: async () => { startMaskTrace: async () => {
const { isConnected, service, refreshStatus } = get(); const { isConnected, service, refreshStatus } = get();
@ -289,19 +437,14 @@ export const useMachineStore = create<MachineState>((set, get) => ({
set({ set({
patternInfo: null, patternInfo: null,
sewingProgress: null, sewingProgress: null,
uploadProgress: 0,
resumeAvailable: false,
resumeFileName: null,
}); });
// Clear uploaded pattern data in pattern store // Clear uploaded pattern data in pattern store
usePatternStore.getState().clearUploadedPattern(); usePatternStore.getState().clearUploadedPattern();
// Clear upload state in upload store
const { useMachineUploadStore } = await import("./useMachineUploadStore");
useMachineUploadStore.getState().reset();
// Clear resume state in cache store
const { useMachineCacheStore } = await import("./useMachineCacheStore");
useMachineCacheStore.getState().clearResumeState();
await refreshStatus(); await refreshStatus();
} catch (err) { } catch (err) {
set({ set({
@ -312,9 +455,41 @@ export const useMachineStore = create<MachineState>((set, get) => ({
} }
}, },
// Initialize the store (call once from App component) // Load cached pattern
initialize: () => { loadCachedPattern: async (): Promise<{
get()._setupSubscriptions(); pesData: PesPatternData;
patternOffset?: { x: number; y: number };
} | null> => {
const { resumeAvailable, service, storageService, refreshPatternInfo } =
get();
if (!resumeAvailable) return null;
try {
const machineUuid = await service.getPatternUUID();
if (!machineUuid) return null;
const uuidStr = uuidToString(machineUuid);
const cached = await storageService.getPatternByUUID(uuidStr);
if (cached) {
console.log(
"[Resume] Loading cached pattern:",
cached.fileName,
"Offset:",
cached.patternOffset,
);
await refreshPatternInfo();
return { pesData: cached.pesData, patternOffset: cached.patternOffset };
}
return null;
} catch (err) {
set({
error:
err instanceof Error ? err.message : "Failed to load cached pattern",
});
return null;
}
}, },
// Setup service subscriptions // Setup service subscriptions
@ -388,12 +563,7 @@ export const useMachineStore = create<MachineState>((set, get) => ({
} }
// follows the apps logic: // follows the apps logic:
// Check if we have a cached pattern and pattern info needs refreshing if (get().resumeAvailable && get().patternInfo?.totalStitches == 0) {
const { useMachineCacheStore } = await import("./useMachineCacheStore");
if (
useMachineCacheStore.getState().resumeAvailable &&
get().patternInfo?.totalStitches == 0
) {
await refreshPatternInfo(); await refreshPatternInfo();
} }
@ -429,6 +599,9 @@ export const useMachineStore = create<MachineState>((set, get) => ({
}, },
})); }));
// Initialize subscriptions when store is created
useMachineStore.getState()._setupSubscriptions();
// Selector hooks for common use cases // Selector hooks for common use cases
export const useIsConnected = () => export const useIsConnected = () =>
useMachineStore((state) => state.isConnected); useMachineStore((state) => state.isConnected);

View file

@ -1,128 +0,0 @@
import { create } from "zustand";
import type { PesPatternData } from "../formats/import/pesImporter";
import { uuidToString } from "../services/PatternCacheService";
/**
* Machine Upload Store
*
* Manages the state and logic for uploading patterns to the machine.
* Extracted from useMachineStore for better separation of concerns.
*/
interface MachineUploadState {
// Upload state
uploadProgress: number;
isUploading: boolean;
// Actions
uploadPattern: (
penData: Uint8Array,
uploadedPesData: PesPatternData, // Pattern with rotation applied (for machine upload)
fileName: string,
patternOffset?: { x: number; y: number },
patternRotation?: number,
originalPesData?: PesPatternData, // Original unrotated pattern (for caching)
) => Promise<void>;
reset: () => void;
}
export const useMachineUploadStore = create<MachineUploadState>((set) => ({
// Initial state
uploadProgress: 0,
isUploading: false,
/**
* Upload a pattern to the machine
*
* @param penData - The PEN-formatted pattern data to upload
* @param uploadedPesData - Pattern with rotation applied (for machine)
* @param fileName - Name of the pattern file
* @param patternOffset - Pattern position offset
* @param patternRotation - Rotation angle in degrees
* @param originalPesData - Original unrotated pattern (for caching)
*/
uploadPattern: async (
penData: Uint8Array,
uploadedPesData: PesPatternData,
fileName: string,
patternOffset?: { x: number; y: number },
patternRotation?: number,
originalPesData?: PesPatternData,
) => {
// Import here to avoid circular dependency
const { useMachineStore } = await import("./useMachineStore");
const {
isConnected,
service,
storageService,
refreshStatus,
refreshPatternInfo,
} = useMachineStore.getState();
if (!isConnected) {
throw new Error("Not connected to machine");
}
try {
set({ uploadProgress: 0, isUploading: true });
// Upload to machine using the rotated bounds
const uuid = await service.uploadPattern(
penData,
(progress) => {
set({ uploadProgress: progress });
},
uploadedPesData.bounds,
patternOffset,
);
set({ uploadProgress: 100 });
// Cache the ORIGINAL unrotated pattern with rotation angle AND the uploaded data
// This allows us to restore the editable state correctly and ensures the exact
// uploaded data is used on resume (prevents inconsistencies from version updates)
const pesDataToCache = originalPesData || uploadedPesData;
const uuidStr = uuidToString(uuid);
storageService.savePattern(
uuidStr,
pesDataToCache,
fileName,
patternOffset,
patternRotation,
uploadedPesData, // Cache the exact uploaded data
);
console.log(
"[MachineUpload] Saved pattern:",
fileName,
"with UUID:",
uuidStr,
"Offset:",
patternOffset,
"Rotation:",
patternRotation,
"(cached original unrotated data + uploaded data)",
);
// Clear resume state in cache store since we just uploaded
const { useMachineCacheStore } = await import("./useMachineCacheStore");
useMachineCacheStore.getState().setResumeAvailable(false, null);
// Refresh status and pattern info after upload
await refreshStatus();
await refreshPatternInfo();
} catch (err) {
throw err instanceof Error ? err : new Error("Failed to upload pattern");
} finally {
set({ isUploading: false });
}
},
/**
* Reset upload state
* Called when pattern is deleted from machine
*/
reset: () => {
set({ uploadProgress: 0, isUploading: false });
},
}));

View file

@ -21,7 +21,6 @@ interface PatternState {
setUploadedPattern: ( setUploadedPattern: (
uploadedData: PesPatternData, uploadedData: PesPatternData,
uploadedOffset: { x: number; y: number }, uploadedOffset: { x: number; y: number },
fileName?: string,
) => void; ) => void;
clearUploadedPattern: () => void; clearUploadedPattern: () => void;
resetPatternOffset: () => void; resetPatternOffset: () => void;
@ -70,32 +69,23 @@ export const usePatternStore = create<PatternState>((set) => ({
setUploadedPattern: ( setUploadedPattern: (
uploadedData: PesPatternData, uploadedData: PesPatternData,
uploadedOffset: { x: number; y: number }, uploadedOffset: { x: number; y: number },
fileName?: string,
) => { ) => {
set({ set({
uploadedPesData: uploadedData, uploadedPesData: uploadedData,
uploadedPatternOffset: uploadedOffset, uploadedPatternOffset: uploadedOffset,
patternUploaded: true, patternUploaded: true,
// Optionally set filename if provided (for resume/reconnect scenarios)
...(fileName && { currentFileName: fileName }),
}); });
console.log("[PatternStore] Uploaded pattern set"); console.log("[PatternStore] Uploaded pattern set");
}, },
// Clear uploaded pattern (called when deleting from machine) // Clear uploaded pattern (called when deleting from machine)
// This reverts to pre-upload state, keeping pesData so user can re-adjust and re-upload
clearUploadedPattern: () => { clearUploadedPattern: () => {
console.log("[PatternStore] CLEARING uploaded pattern...");
set({ set({
uploadedPesData: null, uploadedPesData: null,
uploadedPatternOffset: { x: 0, y: 0 }, uploadedPatternOffset: { x: 0, y: 0 },
patternUploaded: false, patternUploaded: false,
// Keep pesData, currentFileName, patternOffset, patternRotation
// so user can adjust and re-upload
}); });
console.log( console.log("[PatternStore] Uploaded pattern cleared");
"[PatternStore] Uploaded pattern cleared - back to editable mode",
);
}, },
// Reset pattern offset to default // Reset pattern offset to default