diff --git a/src/App.tsx b/src/App.tsx index 5d6689d..c75e626 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { useShallow } from "zustand/react/shallow"; -import { useMachineStore } from "./stores/useMachineStore"; +import { useMachineCacheStore } from "./stores/useMachineCacheStore"; import { usePatternStore } from "./stores/usePatternStore"; import { useUIStore } from "./stores/useUIStore"; import { AppHeader } from "./components/AppHeader"; @@ -23,8 +23,8 @@ function App() { document.title = `Respira v${__APP_VERSION__}`; }, []); - // Machine store - for auto-loading cached pattern - const { resumedPattern, resumeFileName } = useMachineStore( + // Machine cache store - for auto-loading cached pattern + const { resumedPattern, resumeFileName } = useMachineCacheStore( useShallow((state) => ({ resumedPattern: state.resumedPattern, resumeFileName: state.resumeFileName, diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 0adad6a..3bb74a7 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -1,6 +1,8 @@ import { useState, useCallback } from "react"; import { useShallow } from "zustand/react/shallow"; import { useMachineStore, usePatternUploaded } from "../stores/useMachineStore"; +import { useMachineUploadStore } from "../stores/useMachineUploadStore"; +import { useMachineCacheStore } from "../stores/useMachineCacheStore"; import { usePatternStore } from "../stores/usePatternStore"; import { useUIStore } from "../stores/useUIStore"; import { @@ -40,25 +42,28 @@ import { cn } from "@/lib/utils"; export function FileUpload() { // Machine store - const { - isConnected, - machineStatus, - uploadProgress, - isUploading, - machineInfo, - resumeAvailable, - resumeFileName, - uploadPattern, - } = useMachineStore( + const { isConnected, machineStatus, machineInfo } = useMachineStore( useShallow((state) => ({ isConnected: state.isConnected, machineStatus: state.machineStatus, + machineInfo: state.machineInfo, + })), + ); + + // Machine upload store + const { uploadProgress, isUploading, uploadPattern } = useMachineUploadStore( + useShallow((state) => ({ uploadProgress: state.uploadProgress, isUploading: state.isUploading, - machineInfo: state.machineInfo, + uploadPattern: state.uploadPattern, + })), + ); + + // Machine cache store + const { resumeAvailable, resumeFileName } = useMachineCacheStore( + useShallow((state) => ({ resumeAvailable: state.resumeAvailable, resumeFileName: state.resumeFileName, - uploadPattern: state.uploadPattern, })), ); diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx index 1756186..e7cda97 100644 --- a/src/components/PatternCanvas/PatternCanvas.tsx +++ b/src/components/PatternCanvas/PatternCanvas.tsx @@ -4,6 +4,7 @@ import { useMachineStore, usePatternUploaded, } from "../../stores/useMachineStore"; +import { useMachineUploadStore } from "../../stores/useMachineUploadStore"; import { usePatternStore } from "../../stores/usePatternStore"; import { Stage, Layer } from "react-konva"; import Konva from "konva"; @@ -25,10 +26,16 @@ import { usePatternTransform } from "../../hooks/usePatternTransform"; export function PatternCanvas() { // Machine store - const { sewingProgress, machineInfo, isUploading } = useMachineStore( + const { sewingProgress, machineInfo } = useMachineStore( useShallow((state) => ({ sewingProgress: state.sewingProgress, machineInfo: state.machineInfo, + })), + ); + + // Machine upload store + const { isUploading } = useMachineUploadStore( + useShallow((state) => ({ isUploading: state.isUploading, })), ); diff --git a/src/stores/useMachineCacheStore.ts b/src/stores/useMachineCacheStore.ts new file mode 100644 index 0000000..f13899d --- /dev/null +++ b/src/stores/useMachineCacheStore.ts @@ -0,0 +1,194 @@ +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; + 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((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 => { + 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, + }); + }, +})); diff --git a/src/stores/useMachineStore.ts b/src/stores/useMachineStore.ts index 53308ed..0e47ce7 100644 --- a/src/stores/useMachineStore.ts +++ b/src/stores/useMachineStore.ts @@ -13,7 +13,6 @@ import { SewingMachineError } from "../utils/errorCodeHelpers"; import { uuidToString } from "../services/PatternCacheService"; import { createStorageService } from "../platform"; import type { IStorageService } from "../platform/interfaces/IStorageService"; -import type { PesPatternData } from "../formats/import/pesImporter"; import { usePatternStore } from "./usePatternStore"; interface MachineState { @@ -34,20 +33,6 @@ interface MachineState { patternInfo: PatternInfo | null; sewingProgress: SewingProgress | null; - // Upload state - uploadProgress: number; - isUploading: boolean; - - // Resume state - resumeAvailable: boolean; - resumeFileName: string | null; - resumedPattern: { - pesData: PesPatternData; - uploadedPesData?: PesPatternData; - patternOffset?: { x: number; y: number }; - patternRotation?: number; - } | null; - // Error state error: string | null; isPairingError: boolean; @@ -67,25 +52,10 @@ interface MachineState { refreshPatternInfo: () => Promise; refreshProgress: () => Promise; refreshServiceCount: () => Promise; - 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; startMaskTrace: () => Promise; startSewing: () => Promise; resumeSewing: () => Promise; deletePattern: () => Promise; - checkResume: () => Promise; - loadCachedPattern: () => Promise<{ - pesData: PesPatternData; - uploadedPesData?: PesPatternData; - patternOffset?: { x: number; y: number }; - patternRotation?: number; - } | null>; // Internal methods _setupSubscriptions: () => void; @@ -104,11 +74,6 @@ export const useMachineStore = create((set, get) => ({ machineError: SewingMachineError.None, patternInfo: null, sewingProgress: null, - uploadProgress: 0, - isUploading: false, - resumeAvailable: false, - resumeFileName: null, - resumedPattern: null, error: null, isPairingError: false, isCommunicating: false, @@ -116,76 +81,10 @@ export const useMachineStore = create((set, get) => ({ pollIntervalId: null, serviceCountIntervalId: null, - // Check for resumable pattern - checkResume: async (): Promise => { - 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, - "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(); - 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: async () => { try { - const { service, checkResume } = get(); + const { service } = get(); set({ error: null, isPairingError: false }); await service.connect(); @@ -202,8 +101,9 @@ export const useMachineStore = create((set, get) => ({ machineError: state.error, }); - // Check for resume possibility - await checkResume(); + // Check for resume possibility using cache store + const { useMachineCacheStore } = await import("./useMachineCacheStore"); + await useMachineCacheStore.getState().checkResume(); // Start polling get()._startPolling(); @@ -311,85 +211,6 @@ export const useMachineStore = create((set, get) => ({ } }, - // Upload pattern to machine - uploadPattern: async ( - 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) - ) => { - const { - isConnected, - service, - storageService, - refreshStatus, - refreshPatternInfo, - } = get(); - if (!isConnected) { - set({ error: "Not connected to machine" }); - return; - } - - try { - set({ error: null, 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( - "[Cache] Saved pattern:", - fileName, - "with UUID:", - uuidStr, - "Offset:", - patternOffset, - "Rotation:", - patternRotation, - "(cached original unrotated data + uploaded data)", - ); - - // 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 startMaskTrace: async () => { const { isConnected, service, refreshStatus } = get(); @@ -465,15 +286,19 @@ export const useMachineStore = create((set, get) => ({ set({ patternInfo: null, sewingProgress: null, - uploadProgress: 0, - resumeAvailable: false, - resumeFileName: null, - resumedPattern: null, // Clear this to prevent auto-reload }); // Clear uploaded pattern data in pattern store 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(); } catch (err) { set({ @@ -484,54 +309,6 @@ export const useMachineStore = create((set, get) => ({ } }, - // Load cached pattern - loadCachedPattern: async (): Promise<{ - pesData: PesPatternData; - uploadedPesData?: PesPatternData; - patternOffset?: { x: number; y: number }; - patternRotation?: 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, - "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) { - set({ - error: - err instanceof Error ? err.message : "Failed to load cached pattern", - }); - return null; - } - }, - // Setup service subscriptions _setupSubscriptions: () => { const { service } = get(); @@ -603,7 +380,12 @@ export const useMachineStore = create((set, get) => ({ } // follows the apps logic: - if (get().resumeAvailable && get().patternInfo?.totalStitches == 0) { + // Check if we have a cached pattern and pattern info needs refreshing + const { useMachineCacheStore } = await import("./useMachineCacheStore"); + if ( + useMachineCacheStore.getState().resumeAvailable && + get().patternInfo?.totalStitches == 0 + ) { await refreshPatternInfo(); } diff --git a/src/stores/useMachineUploadStore.ts b/src/stores/useMachineUploadStore.ts new file mode 100644 index 0000000..ce4ac3a --- /dev/null +++ b/src/stores/useMachineUploadStore.ts @@ -0,0 +1,128 @@ +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; + + reset: () => void; +} + +export const useMachineUploadStore = create((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 }); + }, +}));