From 2b5e1d763b0b47d79099976dbbb35f6d2d7fac63 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Fri, 26 Dec 2025 22:48:25 +0100 Subject: [PATCH 1/2] fix: Store original and uploaded pattern data to prevent rotation inconsistencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store both original unrotated pesData and uploaded rotated pesData in cache to ensure exact consistency on resume and prevent issues from algorithm changes between versions. This fixes rotation/position reset issues after page reload. - Cache original unrotated pattern + rotation angle for editing - Cache exact uploaded pattern data sent to machine - Restore original offset after loading cached pattern - Use cached uploaded data on resume instead of recalculating 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/App.tsx | 148 ++++++++++++++++-- src/components/FileUpload.tsx | 5 + src/components/LeftSidebar.tsx | 6 +- .../PatternCanvas/PatternCanvas.tsx | 17 +- src/components/PatternSummaryCard.tsx | 8 +- src/components/ProgressMonitor.tsx | 14 +- src/platform/browser/BrowserStorageService.ts | 4 +- .../electron/ElectronStorageService.ts | 19 +++ src/platform/interfaces/IStorageService.ts | 6 +- src/services/PatternCacheService.ts | 32 +++- src/stores/useMachineStore.ts | 52 +++++- src/stores/usePatternStore.ts | 12 +- 12 files changed, 287 insertions(+), 36 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f732876..5d6689d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,13 @@ import { LeftSidebar } from "./components/LeftSidebar"; import { PatternCanvas } from "./components/PatternCanvas"; import { PatternCanvasPlaceholder } from "./components/PatternCanvasPlaceholder"; 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"; function App() { @@ -25,10 +32,20 @@ function App() { ); // Pattern store - for auto-loading cached pattern - const { pesData, setPattern, setPatternOffset } = usePatternStore( + const { + pesData, + uploadedPesData, + setPattern, + setUploadedPattern, + setPatternRotation, + setPatternOffset, + } = usePatternStore( useShallow((state) => ({ pesData: state.pesData, + uploadedPesData: state.uploadedPesData, setPattern: state.setPattern, + setUploadedPattern: state.setUploadedPattern, + setPatternRotation: state.setPatternRotation, setPatternOffset: state.setPatternOffset, })), ); @@ -47,23 +64,130 @@ function App() { // Auto-load cached pattern when available useEffect(() => { - if (resumedPattern && !pesData) { + // Only auto-load if we have a resumed pattern and haven't already loaded it + if (resumedPattern && !uploadedPesData && !pesData) { + if (!resumedPattern.pesData) { + console.error( + "[App] ERROR: resumedPattern has no pesData!", + resumedPattern, + ); + return; + } + console.log( "[App] Loading resumed pattern:", resumeFileName, "Offset:", resumedPattern.patternOffset, + "Rotation:", + resumedPattern.patternRotation, + "Has stitches:", + resumedPattern.pesData.stitches?.length || 0, + "Has cached uploaded data:", + !!resumedPattern.uploadedPesData, ); - setPattern(resumedPattern.pesData, resumeFileName || ""); - // Restore the cached pattern offset - if (resumedPattern.patternOffset) { - setPatternOffset( - resumedPattern.patternOffset.x, - resumedPattern.patternOffset.y, + + const originalPesData = resumedPattern.pesData; + const cachedUploadedPesData = resumedPattern.uploadedPesData; + const rotation = resumedPattern.patternRotation || 0; + const originalOffset = resumedPattern.patternOffset || { x: 0, y: 0 }; + + // 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 (
@@ -76,7 +200,11 @@ function App() { {/* Right Column - Pattern Preview */}
- {pesData ? : } + {pesData || uploadedPesData ? ( + + ) : ( + + )}
diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 2f7e54e..0adad6a 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -206,11 +206,14 @@ export function FileUpload() { setUploadedPattern(pesDataForUpload, adjustedOffset); // Upload the pattern with offset + // IMPORTANT: Pass original unrotated pesData for caching, rotated pesData for upload uploadPattern( penDataToUpload, pesDataForUpload, displayFileName, adjustedOffset, + patternRotation, + pesData, // Original unrotated pattern for caching ); return; // Early return to skip the upload below @@ -226,6 +229,8 @@ export function FileUpload() { pesDataForUpload, displayFileName, patternOffset, + 0, // No rotation + // No need to pass originalPesData since it's the same as pesDataForUpload ); } }, [ diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx index adddb0e..01abd0f 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -13,14 +13,16 @@ export function LeftSidebar() { })), ); - const { pesData } = usePatternStore( + const { pesData, uploadedPesData } = usePatternStore( useShallow((state) => ({ pesData: state.pesData, + uploadedPesData: state.uploadedPesData, })), ); // Derived state: pattern is uploaded if machine has pattern info const patternUploaded = usePatternUploaded(); + const hasPattern = pesData || uploadedPesData; return (
@@ -31,7 +33,7 @@ export function LeftSidebar() { {isConnected && !patternUploaded && } {/* Compact Pattern Summary - Show after upload (during sewing stages) */} - {isConnected && patternUploaded && pesData && } + {isConnected && patternUploaded && hasPattern && } {/* Progress Monitor - Show when pattern is uploaded */} {isConnected && patternUploaded && ( diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx index b44d3a9..1756186 100644 --- a/src/components/PatternCanvas/PatternCanvas.tsx +++ b/src/components/PatternCanvas/PatternCanvas.tsx @@ -190,7 +190,9 @@ export function PatternCanvas() { {/* Original pattern layer: draggable with transformer (shown before upload starts) */} - + {pesData && ( {/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */} - + {uploadedPesData && ( @@ -257,7 +261,10 @@ export function PatternCanvas() { onZoomReset={handleZoomReset} onCenterPattern={handleCenterPattern} canCenterPattern={ - !!pesData && !patternUploaded && !isUploading + !!pesData && + !patternUploaded && + !isUploading && + !uploadedPesData } /> diff --git a/src/components/PatternSummaryCard.tsx b/src/components/PatternSummaryCard.tsx index 71874c5..ec02865 100644 --- a/src/components/PatternSummaryCard.tsx +++ b/src/components/PatternSummaryCard.tsx @@ -25,14 +25,16 @@ export function PatternSummaryCard() { ); // Pattern store - const { pesData, currentFileName } = usePatternStore( + const { pesData, uploadedPesData, currentFileName } = usePatternStore( useShallow((state) => ({ pesData: state.pesData, + uploadedPesData: state.uploadedPesData, currentFileName: state.currentFileName, })), ); - if (!pesData) return null; + const displayPattern = uploadedPesData || pesData; + if (!displayPattern) return null; const canDelete = canDeletePattern(machineStatus); return ( @@ -52,7 +54,7 @@ export function PatternSummaryCard() {
- + {canDelete && (