mirror of
https://github.com/jhbruhn/respira.git
synced 2026-03-14 02:38:41 +00:00
Compare commits
No commits in common. "91bc0285e0367bb11f9dbcddf6e217d3366fcea3" and "7fd31d209cbd5784ffe5c515c8a0506b74f3eed2" have entirely different histories.
91bc0285e0
...
7fd31d209c
18 changed files with 340 additions and 867 deletions
|
|
@ -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": []
|
||||||
|
|
|
||||||
158
src/App.tsx
158
src/App.tsx
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue