diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 2206350..3b29cfe 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
- "Bash(npm run build:*)"
+ "Bash(npm run build:*)",
+ "Bash(npm run lint)"
],
"deny": [],
"ask": []
diff --git a/src/App.tsx b/src/App.tsx
index 532b1b4..9f0a0e0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -8,7 +8,6 @@ import { WorkflowStepper } from './components/WorkflowStepper';
import { NextStepGuide } from './components/NextStepGuide';
import type { PesPatternData } from './utils/pystitchConverter';
import { pyodideLoader } from './utils/pyodideLoader';
-import { MachineStatus } from './types/machine';
import { hasError } from './utils/errorCodeHelpers';
import './App.css';
@@ -36,20 +35,21 @@ function App() {
}, []);
// Auto-load cached pattern when available
- useEffect(() => {
- if (machine.resumedPattern && !pesData) {
- console.log('[App] Loading resumed pattern:', machine.resumeFileName, 'Offset:', machine.resumedPattern.patternOffset);
- setPesData(machine.resumedPattern.pesData);
- // Restore the cached pattern offset
- if (machine.resumedPattern.patternOffset) {
- setPatternOffset(machine.resumedPattern.patternOffset);
- }
- // Preserve the filename from cache
- if (machine.resumeFileName) {
- setCurrentFileName(machine.resumeFileName);
- }
+ const resumedPattern = machine.resumedPattern;
+ const resumeFileName = machine.resumeFileName;
+
+ if (resumedPattern && !pesData) {
+ console.log('[App] Loading resumed pattern:', resumeFileName, 'Offset:', resumedPattern.patternOffset);
+ setPesData(resumedPattern.pesData);
+ // Restore the cached pattern offset
+ if (resumedPattern.patternOffset) {
+ setPatternOffset(resumedPattern.patternOffset);
}
- }, [machine.resumedPattern, pesData, machine.resumeFileName]);
+ // Preserve the filename from cache
+ if (resumeFileName) {
+ setCurrentFileName(resumeFileName);
+ }
+ }
const handlePatternLoaded = useCallback((data: PesPatternData, fileName: string) => {
setPesData(data);
@@ -77,20 +77,20 @@ function App() {
}, [machine]);
// Track pattern uploaded state based on machine status
- useEffect(() => {
- if (!machine.isConnected) {
- setPatternUploaded(false);
- return;
- }
+ const isConnected = machine.isConnected;
+ const patternInfo = machine.patternInfo;
- // Pattern is uploaded if machine has pattern info
- if (machine.patternInfo !== null) {
- setPatternUploaded(true);
- } else {
- // No pattern info means no pattern on machine
+ if (!isConnected) {
+ if (patternUploaded) {
setPatternUploaded(false);
}
- }, [machine.machineStatus, machine.patternInfo, machine.isConnected]);
+ } else {
+ // Pattern is uploaded if machine has pattern info
+ const shouldBeUploaded = patternInfo !== null;
+ if (patternUploaded !== shouldBeUploaded) {
+ setPatternUploaded(shouldBeUploaded);
+ }
+ }
return (
diff --git a/src/components/KonvaComponents.tsx b/src/components/KonvaComponents.tsx
index 48fdb32..5d674ad 100644
--- a/src/components/KonvaComponents.tsx
+++ b/src/components/KonvaComponents.tsx
@@ -3,8 +3,7 @@ import { Group, Line, Rect, Text, Circle } from 'react-konva';
import type { PesPatternData } from '../utils/pystitchConverter';
import { getThreadColor } from '../utils/pystitchConverter';
import type { MachineInfo } from '../types/machine';
-
-const MOVE = 0x10;
+import { MOVE } from '../utils/embroideryConstants';
interface GridProps {
gridSize: number;
@@ -182,7 +181,7 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgr
}
return groups;
- }, [stitches, pesData, currentStitchIndex, showProgress]);
+ }, [stitches, pesData, currentStitchIndex]);
return (
diff --git a/src/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx
index f2ad68d..dc80d2d 100644
--- a/src/components/PatternCanvas.tsx
+++ b/src/components/PatternCanvas.tsx
@@ -26,14 +26,16 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
const [patternOffset, setPatternOffset] = useState(initialPatternOffset || { x: 0, y: 0 });
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
const initialScaleRef = useRef(1);
+ const prevPesDataRef = useRef(null);
// Update pattern offset when initialPatternOffset changes
- useEffect(() => {
- if (initialPatternOffset) {
- setPatternOffset(initialPatternOffset);
- console.log('[PatternCanvas] Restored pattern offset:', initialPatternOffset);
- }
- }, [initialPatternOffset]);
+ if (initialPatternOffset && (
+ patternOffset.x !== initialPatternOffset.x ||
+ patternOffset.y !== initialPatternOffset.y
+ )) {
+ setPatternOffset(initialPatternOffset);
+ console.log('[PatternCanvas] Restored pattern offset:', initialPatternOffset);
+ }
// Track container size
useEffect(() => {
@@ -57,20 +59,29 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
return () => resizeObserver.disconnect();
}, []);
- // Calculate initial scale when pattern or hoop changes
+ // Calculate and store initial scale when pattern or hoop changes
useEffect(() => {
- if (!pesData || containerSize.width === 0) return;
+ if (!pesData || containerSize.width === 0) {
+ prevPesDataRef.current = null;
+ return;
+ }
- const { bounds } = pesData;
- const viewWidth = machineInfo ? machineInfo.maxWidth : bounds.maxX - bounds.minX;
- const viewHeight = machineInfo ? machineInfo.maxHeight : bounds.maxY - bounds.minY;
+ // Only recalculate if pattern changed
+ if (prevPesDataRef.current !== pesData) {
+ prevPesDataRef.current = pesData;
- const initialScale = calculateInitialScale(containerSize.width, containerSize.height, viewWidth, viewHeight);
- initialScaleRef.current = initialScale;
+ const { bounds } = pesData;
+ const viewWidth = machineInfo ? machineInfo.maxWidth : bounds.maxX - bounds.minX;
+ const viewHeight = machineInfo ? machineInfo.maxHeight : bounds.maxY - bounds.minY;
- // Set initial scale and center position when pattern loads
- setStageScale(initialScale);
- setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
+ const initialScale = calculateInitialScale(containerSize.width, containerSize.height, viewWidth, viewHeight);
+ initialScaleRef.current = initialScale;
+
+ // Reset view when pattern changes
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setStageScale(initialScale);
+ setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
+ }
}, [pesData, machineInfo, containerSize]);
// Wheel zoom handler
diff --git a/src/components/ProgressMonitor.tsx b/src/components/ProgressMonitor.tsx
index 661dad8..d10e596 100644
--- a/src/components/ProgressMonitor.tsx
+++ b/src/components/ProgressMonitor.tsx
@@ -6,7 +6,6 @@ import {
CheckBadgeIcon,
ClockIcon,
PauseCircleIcon,
- XCircleIcon,
ExclamationCircleIcon
} from '@heroicons/react/24/solid';
import type { PatternInfo, SewingProgress } from '../types/machine';
@@ -44,12 +43,7 @@ export function ProgressMonitor({
isDeleting = false,
}: ProgressMonitorProps) {
// State indicators
- const isSewing = machineStatus === MachineStatus.SEWING;
- const isComplete = machineStatus === MachineStatus.SEWING_COMPLETE;
- const isColorChange = machineStatus === MachineStatus.COLOR_CHANGE_WAIT;
- const isMaskTracing = machineStatus === MachineStatus.MASK_TRACING;
const isMaskTraceComplete = machineStatus === MachineStatus.MASK_TRACE_COMPLETE;
- const isMaskTraceWait = machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT;
const stateVisual = getStateVisualInfo(machineStatus);
diff --git a/src/services/PatternCacheService.ts b/src/services/PatternCacheService.ts
index 98b3237..456c58b 100644
--- a/src/services/PatternCacheService.ts
+++ b/src/services/PatternCacheService.ts
@@ -42,7 +42,7 @@ export class PatternCacheService {
// Convert penData Uint8Array to array for JSON serialization
const pesDataWithArrayPenData = {
...pesData,
- penData: Array.from(pesData.penData) as any,
+ penData: Array.from(pesData.penData) as unknown as Uint8Array,
};
const cached: CachedPattern = {
diff --git a/src/utils/embroideryConstants.ts b/src/utils/embroideryConstants.ts
new file mode 100644
index 0000000..4f1f006
--- /dev/null
+++ b/src/utils/embroideryConstants.ts
@@ -0,0 +1,23 @@
+/**
+ * Embroidery command constants
+ * These are bitmask flags used to identify stitch types in parsed embroidery files
+ *
+ * Note: PyStitch may use sequential values (0, 1, 2, etc.) internally,
+ * but pyembroidery (which PyStitch is based on) uses these bitmask values
+ * for compatibility with the embroidery format specifications.
+ */
+
+// Stitch type flags (bitmasks - can be combined)
+export const STITCH = 0x00; // Regular stitch (no flags)
+export const MOVE = 0x10; // Jump/move stitch (move without stitching)
+export const JUMP = MOVE; // Alias: JUMP is the same as MOVE
+export const TRIM = 0x20; // Trim thread command
+export const COLOR_CHANGE = 0x40; // Color change command
+export const STOP = 0x80; // Stop command
+export const END = 0x100; // End of pattern
+
+// PEN format flags for Brother machines
+export const PEN_FEED_DATA = 0x01; // Bit 0: Jump stitch (move without stitching)
+export const PEN_CUT_DATA = 0x02; // Bit 1: Trim/cut thread command
+export const PEN_COLOR_END = 0x03; // Last stitch before color change
+export const PEN_DATA_END = 0x05; // Last stitch of entire pattern
diff --git a/src/utils/konvaRenderers.ts b/src/utils/konvaRenderers.ts
index 74aa028..5484b2a 100644
--- a/src/utils/konvaRenderers.ts
+++ b/src/utils/konvaRenderers.ts
@@ -2,8 +2,7 @@ import Konva from 'konva';
import type { PesPatternData } from './pystitchConverter';
import { getThreadColor } from './pystitchConverter';
import type { MachineInfo } from '../types/machine';
-
-const MOVE = 0x10;
+import { MOVE } from './embroideryConstants';
/**
* Renders a grid with specified spacing
@@ -270,9 +269,7 @@ export function renderCurrentPosition(
*/
export function renderLegend(
layer: Konva.Layer,
- pesData: PesPatternData,
- _stageWidth: number,
- _stageHeight: number
+ pesData: PesPatternData
): void {
const legendGroup = new Konva.Group({ name: 'legend' });
diff --git a/src/utils/pystitchConverter.ts b/src/utils/pystitchConverter.ts
index 8079060..a5d8996 100644
--- a/src/utils/pystitchConverter.ts
+++ b/src/utils/pystitchConverter.ts
@@ -1,19 +1,22 @@
import { pyodideLoader } from './pyodideLoader';
+import {
+ STITCH,
+ MOVE,
+ TRIM,
+ END,
+ PEN_FEED_DATA,
+ PEN_CUT_DATA,
+ PEN_COLOR_END,
+ PEN_DATA_END,
+} from './embroideryConstants';
-// PEN format flags
-// Y-coordinate low byte flags (can be combined)
-const PEN_FEED_DATA = 0x01; // Bit 0: Jump stitch (move without stitching)
-const PEN_CUT_DATA = 0x02; // Bit 1: Trim/cut thread command
-
-// X-coordinate low byte flags (bits 0-2, mutually exclusive)
-const PEN_COLOR_END = 0x03; // Last stitch before color change
-const PEN_DATA_END = 0x05; // Last stitch of entire pattern
-
-// Embroidery command constants (from pyembroidery)
-const MOVE = 0x10;
-const COLOR_CHANGE = 0x40;
-const STOP = 0x80;
-const END = 0x100;
+// JavaScript constants module to expose to Python
+const jsEmbConstants = {
+ STITCH,
+ MOVE,
+ TRIM,
+ END,
+};
export interface PesPatternData {
stitches: number[][];
@@ -39,6 +42,9 @@ export async function convertPesToPen(file: File): Promise {
// Ensure Pyodide is initialized
const pyodide = await pyodideLoader.initialize();
+ // Register our JavaScript constants module for Python to import
+ pyodide.registerJsModule('js_emb_constants', jsEmbConstants);
+
// Read the PES file
const buffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(buffer);
@@ -51,10 +57,37 @@ export async function convertPesToPen(file: File): Promise {
const result = await pyodide.runPythonAsync(`
import pystitch
from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE
+from js_emb_constants import STITCH as JS_STITCH, MOVE as JS_MOVE, TRIM as JS_TRIM, END as JS_END
# Read the PES file
pattern = pystitch.read('${filename}')
+def map_cmd(pystitch_cmd):
+ """Map PyStitch command to our JavaScript constant values
+
+ This ensures we have known, consistent values regardless of PyStitch's internal values.
+ Our JS constants use pyembroidery-style bitmask values:
+ STITCH = 0x00, MOVE/JUMP = 0x10, TRIM = 0x20, END = 0x100
+ """
+ if pystitch_cmd == STITCH:
+ return JS_STITCH
+ elif pystitch_cmd == JUMP:
+ return JS_MOVE # PyStitch JUMP maps to our MOVE constant
+ elif pystitch_cmd == TRIM:
+ return JS_TRIM
+ elif pystitch_cmd == END:
+ return JS_END
+ else:
+ # For any other commands, preserve as bitmask
+ result = JS_STITCH
+ if pystitch_cmd & JUMP:
+ result |= JS_MOVE
+ if pystitch_cmd & TRIM:
+ result |= JS_TRIM
+ if pystitch_cmd & END:
+ result |= JS_END
+ return result
+
# Use the raw stitches list which preserves command flags
# Each stitch in pattern.stitches is [x, y, cmd]
# We need to assign color indices based on COLOR_CHANGE commands
@@ -79,9 +112,10 @@ for i, stitch in enumerate(pattern.stitches):
if cmd == END:
continue
- # Add actual stitch with color index and command
- # Keep JUMP/TRIM flags as they indicate jump stitches
- stitches_with_colors.append([x, y, cmd, current_color])
+ # Add actual stitch with color index and mapped command
+ # Map PyStitch cmd values to our known JavaScript constant values
+ mapped_cmd = map_cmd(cmd)
+ stitches_with_colors.append([x, y, mapped_cmd, current_color])
# Convert to JSON-serializable format
{
@@ -106,13 +140,13 @@ for i, stitch in enumerate(pattern.stitches):
// Clean up virtual file
try {
pyodide.FS.unlink(filename);
- } catch (e) {
+ } catch {
// Ignore errors
}
// Extract stitches and validate
- const stitches: number[][] = Array.from(data.stitches).map((stitch: any) =>
- Array.from(stitch) as number[]
+ const stitches: number[][] = Array.from(data.stitches as ArrayLike>).map((stitch) =>
+ Array.from(stitch)
);
if (!stitches || stitches.length === 0) {
@@ -120,7 +154,7 @@ for i, stitch in enumerate(pattern.stitches):
}
// Extract thread data
- const threads = data.threads.map((thread: any) => ({
+ const threads = (data.threads as Array<{ color?: number; hex?: string }>).map((thread) => ({
color: thread.color || 0,
hex: thread.hex || '#000000',
}));
@@ -134,7 +168,6 @@ for i, stitch in enumerate(pattern.stitches):
// PyStitch returns ABSOLUTE coordinates
// PEN format uses absolute coordinates, shifted left by 3 bits (as per official app line 780)
const penStitches: number[] = [];
- let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index
for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i];
@@ -143,8 +176,8 @@ for i, stitch in enumerate(pattern.stitches):
const cmd = stitch[2];
const stitchColor = stitch[3]; // Color index from PyStitch
- // Track bounds for non-jump stitches (cmd=0 is STITCH)
- if (cmd === 0) {
+ // Track bounds for non-jump stitches
+ if (cmd === STITCH) {
minX = Math.min(minX, absX);
maxX = Math.max(maxX, absX);
minY = Math.min(minY, absY);
@@ -158,11 +191,11 @@ for i, stitch in enumerate(pattern.stitches):
let yEncoded = (absY << 3) & 0xFFFF;
// Add command flags to Y-coordinate based on stitch type
- // PyStitch constants: STITCH=0, JUMP=1, TRIM=2
- if (cmd === 1) {
- // JUMP: Set bit 0 (FEED_DATA) - move without stitching
+ if (cmd & MOVE) {
+ // MOVE/JUMP: Set bit 0 (FEED_DATA) - move without stitching
yEncoded |= PEN_FEED_DATA;
- } else if (cmd === 2) {
+ }
+ if (cmd & TRIM) {
// TRIM: Set bit 1 (CUT_DATA) - cut thread command
yEncoded |= PEN_CUT_DATA;
}
@@ -179,7 +212,6 @@ for i, stitch in enumerate(pattern.stitches):
if (!isLastStitch && nextStitchColor !== undefined && nextStitchColor !== stitchColor) {
// This is the last stitch before a color change (but not the last stitch overall)
xEncoded = (xEncoded & 0xFFF8) | PEN_COLOR_END;
- currentColor = nextStitchColor;
} else if (isLastStitch) {
// This is the very last stitch of the pattern
xEncoded = (xEncoded & 0xFFF8) | PEN_DATA_END;