From 0bd037b98a6791a0a4c245a4b399fbfca58c24fc Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 13 Dec 2025 18:37:30 +0100 Subject: [PATCH 1/5] fix: Resolve pattern rendering and coordinate handling bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses multiple critical issues in pattern rendering and coordinate handling: 1. Fixed Y-axis offset accumulation in penParser.ts - Corrected sign extension logic for 16-bit signed coordinates - Changed to interpret full 16-bit value as signed before shifting - Prevents coordinate drift and offset accumulation 2. Fixed color assignment for tack stitches in patternConverter.worker.ts - Added detection for small finishing stitches after COLOR_CHANGE commands - Assigns tack stitches to correct (previous) color instead of new color - Uses conservative pattern matching (< 1.0 unit, followed by JUMP) 3. Made jump stitches visible in pattern preview (KonvaComponents.tsx) - Render jump stitches in thread color instead of gray - Use dashed pattern [8, 4] to distinguish from regular stitches - Set appropriate opacity (0.8 completed, 0.5 not completed) - Fixed critical bug: include previous position in jump groups to create proper line segments 4. Updated konvaRenderers.ts for consistency - Applied same jump stitch rendering logic - Ensures consistent behavior across rendering methods 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/KonvaComponents.tsx | 37 ++++++++++++++------ src/utils/konvaRenderers.ts | 9 ++--- src/utils/penParser.ts | 17 +++++---- src/workers/patternConverter.worker.ts | 48 +++++++++++++++++++++++--- 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/src/components/KonvaComponents.tsx b/src/components/KonvaComponents.tsx index 5d674ad..5c0e4ad 100644 --- a/src/components/KonvaComponents.tsx +++ b/src/components/KonvaComponents.tsx @@ -154,6 +154,9 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgr const groups: StitchGroup[] = []; let currentGroup: StitchGroup | 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; @@ -168,16 +171,30 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgr currentGroup.completed !== isCompleted || currentGroup.isJump !== isJump ) { - currentGroup = { - color, - points: [x, y], - completed: isCompleted, - isJump, - }; + // For jump stitches, we need to create a line from previous position to current position + // So we include both the previous point and current point + if (isJump && i > 0) { + currentGroup = { + color, + points: [prevX, prevY, x, y], + completed: isCompleted, + isJump, + }; + } else { + currentGroup = { + color, + points: [x, y], + completed: isCompleted, + isJump, + }; + } groups.push(currentGroup); } else { currentGroup.points.push(x, y); } + + prevX = x; + prevY = y; } return groups; @@ -189,12 +206,12 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgr ))} diff --git a/src/utils/konvaRenderers.ts b/src/utils/konvaRenderers.ts index 5484b2a..21ca8bc 100644 --- a/src/utils/konvaRenderers.ts +++ b/src/utils/konvaRenderers.ts @@ -159,14 +159,15 @@ export function renderStitches( // Create Konva.Line for each group groups.forEach((group) => { if (group.isJump) { - // Jump stitches - dashed gray lines + // Jump stitches - dashed lines in thread color const line = new Konva.Line({ points: group.points, - stroke: group.completed ? '#cccccc' : '#e8e8e8', - strokeWidth: 1.5, + stroke: group.color, + strokeWidth: 1.0, lineCap: 'round', lineJoin: 'round', - dash: [3, 3], + dash: [5, 5], + opacity: group.completed ? 0.6 : 0.25, }); stitchesGroup.add(line); } else { diff --git a/src/utils/penParser.ts b/src/utils/penParser.ts index 9c112e4..1610f3f 100644 --- a/src/utils/penParser.ts +++ b/src/utils/penParser.ts @@ -33,13 +33,18 @@ export function parsePenData(data: Uint8Array): PenData { const yFlags = data[offset + 2] & 0x07; // Decode coordinates (shift right by 3 to get actual position) - // Using signed 16-bit interpretation - let x = (xRaw >> 3); - let y = (yRaw >> 3); + // The coordinates are stored as signed 16-bit values, left-shifted by 3 + // We need to interpret them as signed before shifting - // Convert to signed if needed - if (x > 0x7FF) x = x - 0x2000; - if (y > 0x7FF) y = y - 0x2000; + // Convert from unsigned 16-bit to signed 16-bit + let xSigned = xRaw; + let ySigned = yRaw; + if (xSigned > 0x7FFF) xSigned = xSigned - 0x10000; + if (ySigned > 0x7FFF) ySigned = ySigned - 0x10000; + + // Now shift right by 3 (arithmetic shift, preserves sign) + let x = xSigned >> 3; + let y = ySigned >> 3; const stitch: PenStitch = { x, diff --git a/src/workers/patternConverter.worker.ts b/src/workers/patternConverter.worker.ts index 0878053..9d00271 100644 --- a/src/workers/patternConverter.worker.ts +++ b/src/workers/patternConverter.worker.ts @@ -210,15 +210,21 @@ def map_cmd(pystitch_cmd): # Each stitch in pattern.stitches is [x, y, cmd] # We need to assign color indices based on COLOR_CHANGE commands # and filter out COLOR_CHANGE and STOP commands (they're not actual stitches) +# +# IMPORTANT: In PES files, COLOR_CHANGE commands can appear before finishing +# stitches (tack/lock stitches) that semantically belong to the PREVIOUS color. +# We need to detect this pattern and assign colors correctly. stitches_with_colors = [] current_color = 0 +prev_color = 0 for i, stitch in enumerate(pattern.stitches): x, y, cmd = stitch - # Check for color change command - increment color but don't add stitch + # Check for color change command if cmd == COLOR_CHANGE: + prev_color = current_color current_color += 1 continue @@ -230,10 +236,44 @@ for i, stitch in enumerate(pattern.stitches): if cmd == END: continue - # Add actual stitch with color index and mapped command - # Map PyStitch cmd values to our known JavaScript constant values + # Determine which color this stitch belongs to + # After a COLOR_CHANGE, check if this might be a finishing tack stitch + # belonging to the previous color rather than the new color + stitch_color = current_color + + # If this is the first stitch after a color change (color just incremented) + # and it's a very small stitch before a JUMP, it's likely a tack stitch + if current_color != prev_color and len(stitches_with_colors) > 0: + last_x, last_y = stitches_with_colors[-1][0], stitches_with_colors[-1][1] + dx, dy = x - last_x, y - last_y + dist = (dx*dx + dy*dy)**0.5 + + # Check if this is a tiny stitch (< 1.0 unit - typical tack stitch) + # and if there's a JUMP coming soon + if dist < 1.0: + # Look ahead to see if there's a JUMP within next 10 stitches + has_jump_ahead = False + for j in range(i+1, min(i+11, len(pattern.stitches))): + next_stitch = pattern.stitches[j] + next_cmd = next_stitch[2] + if next_cmd == JUMP: + has_jump_ahead = True + break + elif next_cmd == STITCH: + # If we hit a regular stitch before a JUMP, this might be the new color + next_x, next_y = next_stitch[0], next_stitch[1] + next_dist = ((next_x - last_x)**2 + (next_y - last_y)**2)**0.5 + # Only continue if following stitches are also tiny + if next_dist >= 1.0: + break + + # If we found a jump ahead, this small stitch belongs to previous color + if has_jump_ahead: + stitch_color = prev_color + + # Add actual stitch with assigned color index and mapped command mapped_cmd = map_cmd(cmd) - stitches_with_colors.append([x, y, mapped_cmd, current_color]) + stitches_with_colors.append([x, y, mapped_cmd, stitch_color]) # Convert to JSON-serializable format { From 8a32d5184e56ef0a5883e55cd633c5167e82dcc0 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 13 Dec 2025 23:25:48 +0100 Subject: [PATCH 2/5] fix: Implement rotated lock stitches and improve PEN format handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add lock stitch rotation based on movement direction (matches C# PesxToPen.cs) - Calculate direction by accumulating vectors up to 5 stitches or 8.0 units - Scale direction vectors from magnitude 8.0 down to 0.4 for proper lock stitch size - Generate 8 lock stitches (not 4) alternating between +dir and -dir - Remove PyStitch duplicate position stitches during color changes - Add long jump detection with automatic lock stitches and cut commands - Improve color change sequence: finish locks, cut, jump, COLOR_END, start locks - Parse PEN data to get actual stitches for rendering (fixes jump stitch colors) - Add encodeStitchPosition() helper function for coordinate encoding - Improve pattern info refresh timing after mask trace - Add detailed logging for PEN encoding and pattern info responses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/PatternCanvas.tsx | 19 +- src/hooks/useBrotherMachine.ts | 21 +- src/services/BrotherPP1Service.ts | 9 +- src/utils/patternConverterClient.ts | 15 +- src/utils/penParser.ts | 12 +- src/workers/patternConverter.worker.ts | 323 +++++++++++++++++++++---- 6 files changed, 335 insertions(+), 64 deletions(-) diff --git a/src/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx index e47b035..c9ab905 100644 --- a/src/components/PatternCanvas.tsx +++ b/src/components/PatternCanvas.tsx @@ -292,7 +292,14 @@ export function PatternCanvas() { }} > { + // Convert PEN stitch format {x, y, flags, isJump} to PES format [x, y, cmd, colorIndex] + const cmd = s.isJump ? 0x10 : 0; // MOVE flag if jump + const colorIndex = pesData.penStitches.colorBlocks.find( + b => i >= b.startStitch && i <= b.endStitch + )?.colorIndex ?? 0; + return [s.x, s.y, cmd, colorIndex]; + })} pesData={pesData} currentStitchIndex={sewingProgress?.currentStitch || 0} showProgress={patternUploaded || isUploading} @@ -304,11 +311,17 @@ export function PatternCanvas() { {/* Current position layer */} - {pesData && sewingProgress && sewingProgress.currentStitch > 0 && ( + {pesData && pesData.penStitches && sewingProgress && sewingProgress.currentStitch > 0 && ( { + const cmd = s.isJump ? 0x10 : 0; + const colorIndex = pesData.penStitches.colorBlocks.find( + b => i >= b.startStitch && i <= b.endStitch + )?.colorIndex ?? 0; + return [s.x, s.y, cmd, colorIndex]; + })} /> )} diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts index 096f465..5d61fef 100644 --- a/src/hooks/useBrotherMachine.ts +++ b/src/hooks/useBrotherMachine.ts @@ -267,9 +267,11 @@ export function useBrotherMachine() { setResumeAvailable(false); setResumeFileName(null); - // Refresh status and pattern info after upload + // Refresh status after upload + // NOTE: We don't call refreshPatternInfo() here because the machine hasn't + // finished processing the pattern yet. Pattern info (stitch count, time estimate) + // is only available AFTER startMaskTrace() is called. await refreshStatus(); - await refreshPatternInfo(); } catch (err) { setError( err instanceof Error ? err.message : "Failed to upload pattern", @@ -287,13 +289,24 @@ export function useBrotherMachine() { try { setError(null); await service.startMaskTrace(); - await refreshStatus(); + + // After mask trace, poll machine status a few times to ensure it's ready + // The machine needs time to process the pattern before pattern info is accurate + console.log('[MaskTrace] Polling machine status...'); + for (let i = 0; i < 3; i++) { + await new Promise(resolve => setTimeout(resolve, 200)); + await refreshStatus(); + } + + // Now the machine should have accurate pattern info + console.log('[MaskTrace] Refreshing pattern info...'); + await refreshPatternInfo(); } catch (err) { setError( err instanceof Error ? err.message : "Failed to start mask trace", ); } - }, [service, isConnected, refreshStatus]); + }, [service, isConnected, refreshStatus, refreshPatternInfo]); const startSewing = useCallback(async () => { if (!isConnected) return; diff --git a/src/services/BrotherPP1Service.ts b/src/services/BrotherPP1Service.ts index 0999f0d..c5f3b34 100644 --- a/src/services/BrotherPP1Service.ts +++ b/src/services/BrotherPP1Service.ts @@ -394,7 +394,7 @@ export class BrotherPP1Service { const readUInt16LE = (offset: number) => data[offset] | (data[offset + 1] << 8); - return { + const patternInfo = { boundLeft: readInt16LE(0), boundTop: readInt16LE(2), boundRight: readInt16LE(4), @@ -403,6 +403,13 @@ export class BrotherPP1Service { totalStitches: readUInt16LE(10), speed: readUInt16LE(12), }; + + console.log('[BrotherPP1] Pattern Info Response:', { + rawData: Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' '), + parsed: patternInfo, + }); + + return patternInfo; } async getSewingProgress(): Promise { diff --git a/src/utils/patternConverterClient.ts b/src/utils/patternConverterClient.ts index aaa0d61..30496c8 100644 --- a/src/utils/patternConverterClient.ts +++ b/src/utils/patternConverterClient.ts @@ -1,10 +1,11 @@ import type { WorkerMessage, WorkerResponse } from '../workers/patternConverter.worker'; import PatternConverterWorker from '../workers/patternConverter.worker?worker'; +import { parsePenData, type PenData } from './penParser'; export type PyodideState = 'not_loaded' | 'loading' | 'ready' | 'error'; export interface PesPatternData { - stitches: number[][]; + stitches: number[][]; // Original PES stitches (for reference) threads: Array<{ color: number; hex: string; @@ -22,7 +23,8 @@ export interface PesPatternData { chart: string | null; threadIndices: number[]; }>; - penData: Uint8Array; + penData: Uint8Array; // Raw PEN bytes sent to machine + penStitches: PenData; // Parsed PEN stitches (for rendering) colorCount: number; stitchCount: number; bounds: { @@ -175,9 +177,16 @@ class PatternConverterClient { case 'CONVERT_COMPLETE': { worker.removeEventListener('message', handleMessage); // Convert penData array back to Uint8Array + const penData = new Uint8Array(message.data.penData); + + // Parse the PEN data to get stitches for rendering + const penStitches = parsePenData(penData); + console.log('[PatternConverter] Parsed PEN data:', penStitches.totalStitches, 'stitches,', penStitches.colorCount, 'colors'); + const result: PesPatternData = { ...message.data, - penData: new Uint8Array(message.data.penData), + penData, + penStitches, }; resolve(result); break; diff --git a/src/utils/penParser.ts b/src/utils/penParser.ts index 1610f3f..8f8669b 100644 --- a/src/utils/penParser.ts +++ b/src/utils/penParser.ts @@ -34,15 +34,17 @@ export function parsePenData(data: Uint8Array): PenData { // Decode coordinates (shift right by 3 to get actual position) // The coordinates are stored as signed 16-bit values, left-shifted by 3 - // We need to interpret them as signed before shifting + // Step 1: Clear the flag bits (low 3 bits) from the raw values + const xRawClean = xRaw & 0xFFF8; + const yRawClean = yRaw & 0xFFF8; - // Convert from unsigned 16-bit to signed 16-bit - let xSigned = xRaw; - let ySigned = yRaw; + // Step 2: Convert from unsigned 16-bit to signed 16-bit + let xSigned = xRawClean; + let ySigned = yRawClean; if (xSigned > 0x7FFF) xSigned = xSigned - 0x10000; if (ySigned > 0x7FFF) ySigned = ySigned - 0x10000; - // Now shift right by 3 (arithmetic shift, preserves sign) + // Step 3: Shift right by 3 (arithmetic shift, preserves sign) let x = xSigned >> 3; let y = ySigned >> 3; diff --git a/src/workers/patternConverter.worker.ts b/src/workers/patternConverter.worker.ts index 9d00271..c5145ac 100644 --- a/src/workers/patternConverter.worker.ts +++ b/src/workers/patternConverter.worker.ts @@ -152,6 +152,127 @@ async function initializePyodide(pyodideIndexURL?: string, pystitchWheelURL?: st } } +/** + * Calculate lock stitch direction by accumulating movement vectors + * Matches the C# logic that accumulates coordinates until reaching threshold + * @param stitches Array of stitches to analyze + * @param currentIndex Current stitch index + * @param lookAhead If true, look forward; if false, look backward + * @returns Direction vector components (normalized and scaled to magnitude 8.0) + */ +function calculateLockDirection( + stitches: number[][], + currentIndex: number, + lookAhead: boolean +): { dirX: number; dirY: number } { + const TARGET_LENGTH = 8.0; // Target accumulated length (from C# code) + const MAX_POINTS = 5; // Maximum points to accumulate (from C# code) + + let accumulatedX = 0; + let accumulatedY = 0; + let maxLength = 0; + let bestX = 0; + let bestY = 0; + + const step = lookAhead ? 1 : -1; + const maxIterations = lookAhead + ? Math.min(MAX_POINTS, stitches.length - currentIndex - 1) + : Math.min(MAX_POINTS, currentIndex); + + for (let i = 0; i < maxIterations; i++) { + const idx = currentIndex + (step * (i + 1)); + if (idx < 0 || idx >= stitches.length) break; + + const stitch = stitches[idx]; + const cmd = stitch[2]; + + // Skip MOVE/JUMP stitches + if ((cmd & MOVE) !== 0) continue; + + // Accumulate relative coordinates + const deltaX = Math.round(stitch[0]) - Math.round(stitches[currentIndex][0]); + const deltaY = Math.round(stitch[1]) - Math.round(stitches[currentIndex][1]); + + accumulatedX += deltaX; + accumulatedY += deltaY; + + const length = Math.sqrt(accumulatedX * accumulatedX + accumulatedY * accumulatedY); + + // Track the maximum length vector seen so far + if (length > maxLength) { + maxLength = length; + bestX = accumulatedX; + bestY = accumulatedY; + } + + // If we've accumulated enough length, use current vector + if (length >= TARGET_LENGTH) { + return { + dirX: (accumulatedX * 8.0) / length, + dirY: (accumulatedY * 8.0) / length + }; + } + } + + // If we didn't reach target length, use the best vector we found + if (maxLength > 0.1) { + return { + dirX: (bestX * 8.0) / maxLength, + dirY: (bestY * 8.0) / maxLength + }; + } + + // Fallback: diagonal direction with magnitude 8.0 + const mag = 8.0 / Math.sqrt(2); // ~5.66 for diagonal + return { dirX: mag, dirY: mag }; +} + +/** + * Generate lock/tack stitches at a position, rotated toward the direction of travel + * Matches Nuihajime_TomeDataPlus from PesxToPen.cs with vector rotation + * @param x X coordinate + * @param y Y coordinate + * @param dirX Direction X component (scaled) + * @param dirY Direction Y component (scaled) + * @returns Array of PEN bytes for lock stitches + */ +function generateLockStitches(x: number, y: number, dirX: number, dirY: number): number[] { + const lockBytes: number[] = []; + + // Generate 8 lock stitches in alternating pattern + // Pattern from C# (from Nuihajime_TomeDataPlus): [+x, +y, -x, -y] repeated + // The direction vector has magnitude ~8.0, so we need to scale it down + // to get reasonable lock stitch size (approximately 0.4 units) + const scale = 0.4 / 8.0; // Scale the magnitude-8 vector down to 0.4 + const scaledDirX = dirX * scale; + const scaledDirY = dirY * scale; + + // Generate 8 stitches alternating between forward and backward + for (let i = 0; i < 8; i++) { + // Alternate between forward (+) and backward (-) direction + const sign = (i % 2 === 0) ? 1 : -1; + lockBytes.push(...encodeStitchPosition(x + scaledDirX * sign, y + scaledDirY * sign)); + } + + return lockBytes; +} + +/** + * Encode a stitch position to PEN bytes (4 bytes: X_low, X_high, Y_low, Y_high) + * Coordinates are shifted left by 3 bits to make room for flags in low 3 bits + */ +function encodeStitchPosition(x: number, y: number): number[] { + const xEnc = (Math.round(x) << 3) & 0xffff; + const yEnc = (Math.round(y) << 3) & 0xffff; + + return [ + xEnc & 0xff, + (xEnc >> 8) & 0xff, + yEnc & 0xff, + (yEnc >> 8) & 0xff + ]; +} + /** * Convert PES file to PEN format */ @@ -217,14 +338,12 @@ def map_cmd(pystitch_cmd): stitches_with_colors = [] current_color = 0 -prev_color = 0 for i, stitch in enumerate(pattern.stitches): x, y, cmd = stitch # Check for color change command if cmd == COLOR_CHANGE: - prev_color = current_color current_color += 1 continue @@ -236,44 +355,19 @@ for i, stitch in enumerate(pattern.stitches): if cmd == END: continue - # Determine which color this stitch belongs to - # After a COLOR_CHANGE, check if this might be a finishing tack stitch - # belonging to the previous color rather than the new color - stitch_color = current_color + # PyStitch inserts duplicate stitches at the same coordinates during color changes + # Skip any stitch that has the exact same position as the previous one + if len(stitches_with_colors) > 0: + last_stitch = stitches_with_colors[-1] + last_x, last_y = last_stitch[0], last_stitch[1] - # If this is the first stitch after a color change (color just incremented) - # and it's a very small stitch before a JUMP, it's likely a tack stitch - if current_color != prev_color and len(stitches_with_colors) > 0: - last_x, last_y = stitches_with_colors[-1][0], stitches_with_colors[-1][1] - dx, dy = x - last_x, y - last_y - dist = (dx*dx + dy*dy)**0.5 + if x == last_x and y == last_y: + # Duplicate position - skip it + continue - # Check if this is a tiny stitch (< 1.0 unit - typical tack stitch) - # and if there's a JUMP coming soon - if dist < 1.0: - # Look ahead to see if there's a JUMP within next 10 stitches - has_jump_ahead = False - for j in range(i+1, min(i+11, len(pattern.stitches))): - next_stitch = pattern.stitches[j] - next_cmd = next_stitch[2] - if next_cmd == JUMP: - has_jump_ahead = True - break - elif next_cmd == STITCH: - # If we hit a regular stitch before a JUMP, this might be the new color - next_x, next_y = next_stitch[0], next_stitch[1] - next_dist = ((next_x - last_x)**2 + (next_y - last_y)**2)**0.5 - # Only continue if following stitches are also tiny - if next_dist >= 1.0: - break - - # If we found a jump ahead, this small stitch belongs to previous color - if has_jump_ahead: - stitch_color = prev_color - - # Add actual stitch with assigned color index and mapped command + # Add actual stitch with current color index and mapped command mapped_cmd = map_cmd(cmd) - stitches_with_colors.append([x, y, mapped_cmd, stitch_color]) + stitches_with_colors.append([x, y, mapped_cmd, current_color]) # Convert to JSON-serializable format { @@ -359,6 +453,13 @@ for i, stitch in enumerate(pattern.stitches): // PEN format uses absolute coordinates, shifted left by 3 bits (as per official app line 780) const penStitches: number[] = []; + // Track position for calculating jump distances + let prevX = 0; + let prevY = 0; + + // Constants from PesxToPen.cs + const FEED_LENGTH = 50; // Long jump threshold requiring lock stitches and cut + console.log(stitches); for (let i = 0; i < stitches.length; i++) { const stitch = stitches[i]; const absX = Math.round(stitch[0]); @@ -374,6 +475,39 @@ for i, stitch in enumerate(pattern.stitches): maxY = Math.max(maxY, absY); } + // Check for long jumps that need lock stitches and cuts + if (cmd & MOVE) { + const jumpDist = Math.sqrt((absX - prevX) ** 2 + (absY - prevY) ** 2); + + if (jumpDist > FEED_LENGTH) { + // Long jump - add finishing lock stitches at previous position + const finishDir = calculateLockDirection(stitches, i - 1, false); + penStitches.push(...generateLockStitches(prevX, prevY, finishDir.dirX, finishDir.dirY)); + + // Encode jump with both FEED and CUT flags + let xEncoded = (absX << 3) & 0xffff; + let yEncoded = (absY << 3) & 0xffff; + yEncoded |= PEN_FEED_DATA; // Jump flag + yEncoded |= PEN_CUT_DATA; // Cut flag for long jumps + + penStitches.push( + xEncoded & 0xff, + (xEncoded >> 8) & 0xff, + yEncoded & 0xff, + (yEncoded >> 8) & 0xff + ); + + // Add starting lock stitches at new position + const startDir = calculateLockDirection(stitches, i, true); + penStitches.push(...generateLockStitches(absX, absY, startDir.dirX, startDir.dirY)); + + // Update position and continue + prevX = absX; + prevY = absY; + continue; + } + } + // Encode absolute coordinates with flags in low 3 bits // Shift coordinates left by 3 bits to make room for flags // As per official app line 780: buffer[index64] = (byte) ((int) numArray4[index64 / 4, 0] << 3 & (int) byte.MaxValue); @@ -394,20 +528,12 @@ for i, stitch in enumerate(pattern.stitches): const isLastStitch = i === stitches.length - 1 || (cmd & END) !== 0; // Check for color change by comparing stitch color index - // Mark the LAST stitch of the previous color with PEN_COLOR_END - // BUT: if this is the last stitch of the entire pattern, use DATA_END instead const nextStitch = stitches[i + 1]; const nextStitchColor = nextStitch?.[3]; + const isColorChange = !isLastStitch && nextStitchColor !== undefined && nextStitchColor !== stitchColor; - 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; - } else if (isLastStitch) { - // This is the very last stitch of the pattern + // Mark the very last stitch of the pattern with DATA_END + if (isLastStitch) { xEncoded = (xEncoded & 0xfff8) | PEN_DATA_END; } @@ -419,6 +545,98 @@ for i, stitch in enumerate(pattern.stitches): (yEncoded >> 8) & 0xff ); + // Update position for next iteration + prevX = absX; + prevY = absY; + + // Handle color change: finishing lock, cut, jump, COLOR_END, starting lock + if (isColorChange) { + const nextStitchCmd = nextStitch[2]; + const nextStitchX = Math.round(nextStitch[0]); + const nextStitchY = Math.round(nextStitch[1]); + const nextIsJump = (nextStitchCmd & MOVE) !== 0; + + console.log(`[PEN] Color change detected at stitch ${i}: color ${stitchColor} -> ${nextStitchColor}`); + console.log(`[PEN] Current position: (${absX}, ${absY})`); + console.log(`[PEN] Next stitch: cmd=${nextStitchCmd}, isJump=${nextIsJump}, pos=(${nextStitchX}, ${nextStitchY})`); + + // Step 1: Add finishing lock stitches at end of current color + const finishDir = calculateLockDirection(stitches, i, false); + penStitches.push(...generateLockStitches(absX, absY, finishDir.dirX, finishDir.dirY)); + console.log(`[PEN] Added 8 finishing lock stitches at (${absX}, ${absY}) dir=(${finishDir.dirX.toFixed(2)}, ${finishDir.dirY.toFixed(2)})`); + + // Step 2: Add cut command at current position + const cutXEncoded = (absX << 3) & 0xffff; + const cutYEncoded = ((absY << 3) & 0xffff) | PEN_CUT_DATA; + + penStitches.push( + cutXEncoded & 0xff, + (cutXEncoded >> 8) & 0xff, + cutYEncoded & 0xff, + (cutYEncoded >> 8) & 0xff + ); + console.log(`[PEN] Added cut command at (${absX}, ${absY})`); + + // Step 3: If next stitch is a JUMP, encode it and skip it in the loop + // Otherwise, add a jump ourselves if positions differ + let jumpToX = nextStitchX; + let jumpToY = nextStitchY; + + if (nextIsJump) { + // The PES has a JUMP to the new color position, we'll add it here and skip it later + console.log(`[PEN] Next stitch is JUMP, using it to move to new color`); + i++; // Skip the JUMP stitch since we're processing it here + } else if (nextStitchX === absX && nextStitchY === absY) { + // Next color starts at same position, no jump needed + console.log(`[PEN] Next color starts at same position, no jump needed`); + } else { + // Need to add a jump ourselves + console.log(`[PEN] Adding jump to next color position`); + } + + // Add jump to new position (if position changed) + if (jumpToX !== absX || jumpToY !== absY) { + let jumpXEncoded = (jumpToX << 3) & 0xffff; + let jumpYEncoded = (jumpToY << 3) & 0xffff; + jumpYEncoded |= PEN_FEED_DATA; // Jump flag + + penStitches.push( + jumpXEncoded & 0xff, + (jumpXEncoded >> 8) & 0xff, + jumpYEncoded & 0xff, + (jumpYEncoded >> 8) & 0xff + ); + console.log(`[PEN] Added jump to (${jumpToX}, ${jumpToY})`); + } + + // Step 4: Add COLOR_END marker at NEW position + // This is where the machine pauses and waits for the user to change thread color + let colorEndXEncoded = (jumpToX << 3) & 0xffff; + let colorEndYEncoded = (jumpToY << 3) & 0xffff; + + // Add COLOR_END flag to X coordinate + colorEndXEncoded = (colorEndXEncoded & 0xfff8) | PEN_COLOR_END; + + penStitches.push( + colorEndXEncoded & 0xff, + (colorEndXEncoded >> 8) & 0xff, + colorEndYEncoded & 0xff, + (colorEndYEncoded >> 8) & 0xff + ); + console.log(`[PEN] Added COLOR_END marker at (${jumpToX}, ${jumpToY})`); + + // Step 5: Add starting lock stitches at the new position + // Look ahead from the next stitch (which might be a JUMP we skipped, so use i+1) + const nextStitchIdx = nextIsJump ? i + 2 : i + 1; + const startDir = calculateLockDirection(stitches, nextStitchIdx < stitches.length ? nextStitchIdx : i, true); + penStitches.push(...generateLockStitches(jumpToX, jumpToY, startDir.dirX, startDir.dirY)); + console.log(`[PEN] Added 8 starting lock stitches at (${jumpToX}, ${jumpToY}) dir=(${startDir.dirX.toFixed(2)}, ${startDir.dirY.toFixed(2)})`); + + // Update position + prevX = jumpToX; + prevY = jumpToY; + } + // Check for end command if ((cmd & END) !== 0) { break; @@ -455,6 +673,15 @@ for i, stitch in enumerate(pattern.stitches): }> ); + // Calculate PEN stitch count (should match what machine will count) + const penStitchCount = penStitches.length / 4; + + console.log('[patternConverter] PEN encoding complete:'); + console.log(` - PyStitch stitches: ${stitches.length}`); + console.log(` - PEN bytes: ${penStitches.length}`); + console.log(` - PEN stitches (bytes/4): ${penStitchCount}`); + console.log(` - Bounds: (${minX}, ${minY}) to (${maxX}, ${maxY})`); + // Post result back to main thread self.postMessage({ type: 'CONVERT_COMPLETE', From eb774dcb30aab508debcb5bdb76076bd486ce292 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 13 Dec 2025 23:30:56 +0100 Subject: [PATCH 3/5] fix: Resolve linter issues in pattern converter and hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change let to const for variables that are never reassigned - Fix React Hook dependency array warnings in useBrotherMachine - Remove unused refreshPatternInfo dependency from uploadPattern - Add missing refreshPatternInfo dependency to startMaskTrace 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/hooks/useBrotherMachine.ts | 2 +- src/utils/penParser.ts | 4 ++-- src/workers/patternConverter.worker.ts | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts index 5d61fef..e775f31 100644 --- a/src/hooks/useBrotherMachine.ts +++ b/src/hooks/useBrotherMachine.ts @@ -280,7 +280,7 @@ export function useBrotherMachine() { setIsUploading(false); // Clear loading state } }, - [service, storageService, isConnected, refreshStatus, refreshPatternInfo], + [service, storageService, isConnected, refreshStatus], ); const startMaskTrace = useCallback(async () => { diff --git a/src/utils/penParser.ts b/src/utils/penParser.ts index 8f8669b..edee9b6 100644 --- a/src/utils/penParser.ts +++ b/src/utils/penParser.ts @@ -45,8 +45,8 @@ export function parsePenData(data: Uint8Array): PenData { if (ySigned > 0x7FFF) ySigned = ySigned - 0x10000; // Step 3: Shift right by 3 (arithmetic shift, preserves sign) - let x = xSigned >> 3; - let y = ySigned >> 3; + const x = xSigned >> 3; + const y = ySigned >> 3; const stitch: PenStitch = { x, diff --git a/src/workers/patternConverter.worker.ts b/src/workers/patternConverter.worker.ts index c5145ac..a53ae00 100644 --- a/src/workers/patternConverter.worker.ts +++ b/src/workers/patternConverter.worker.ts @@ -485,7 +485,7 @@ for i, stitch in enumerate(pattern.stitches): penStitches.push(...generateLockStitches(prevX, prevY, finishDir.dirX, finishDir.dirY)); // Encode jump with both FEED and CUT flags - let xEncoded = (absX << 3) & 0xffff; + const xEncoded = (absX << 3) & 0xffff; let yEncoded = (absY << 3) & 0xffff; yEncoded |= PEN_FEED_DATA; // Jump flag yEncoded |= PEN_CUT_DATA; // Cut flag for long jumps @@ -579,8 +579,8 @@ for i, stitch in enumerate(pattern.stitches): // Step 3: If next stitch is a JUMP, encode it and skip it in the loop // Otherwise, add a jump ourselves if positions differ - let jumpToX = nextStitchX; - let jumpToY = nextStitchY; + const jumpToX = nextStitchX; + const jumpToY = nextStitchY; if (nextIsJump) { // The PES has a JUMP to the new color position, we'll add it here and skip it later @@ -596,7 +596,7 @@ for i, stitch in enumerate(pattern.stitches): // Add jump to new position (if position changed) if (jumpToX !== absX || jumpToY !== absY) { - let jumpXEncoded = (jumpToX << 3) & 0xffff; + const jumpXEncoded = (jumpToX << 3) & 0xffff; let jumpYEncoded = (jumpToY << 3) & 0xffff; jumpYEncoded |= PEN_FEED_DATA; // Jump flag @@ -612,7 +612,7 @@ for i, stitch in enumerate(pattern.stitches): // Step 4: Add COLOR_END marker at NEW position // This is where the machine pauses and waits for the user to change thread color let colorEndXEncoded = (jumpToX << 3) & 0xffff; - let colorEndYEncoded = (jumpToY << 3) & 0xffff; + const colorEndYEncoded = (jumpToY << 3) & 0xffff; // Add COLOR_END flag to X coordinate colorEndXEncoded = (colorEndXEncoded & 0xfff8) | PEN_COLOR_END; From 8c3e177ea61fdb2a8db26ee4fab9e7d719f78ec1 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 13 Dec 2025 23:35:23 +0100 Subject: [PATCH 4/5] fix: Update color change lock stitch direction to match C# Loop C MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Color change finish lock stitches now look FORWARD (Loop C) instead of backward, aligning the knot with the stop event data for correct tension when the machine halts for thread color changes. Lock stitch direction breakdown: - Loop A (Jump/Entry): Look forward - hides knot under upcoming stitches - Loop B (End/Cut): Look backward - hides knot inside previous stitches - Loop C (Color Change): Look forward - aligns with stop event data Added detailed comments documenting which loop corresponds to each lock stitch location in the code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/workers/patternConverter.worker.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/workers/patternConverter.worker.ts b/src/workers/patternConverter.worker.ts index a53ae00..88d56a1 100644 --- a/src/workers/patternConverter.worker.ts +++ b/src/workers/patternConverter.worker.ts @@ -155,6 +155,12 @@ async function initializePyodide(pyodideIndexURL?: string, pystitchWheelURL?: st /** * Calculate lock stitch direction by accumulating movement vectors * Matches the C# logic that accumulates coordinates until reaching threshold + * + * Three use cases from C# ConvertEmb function: + * - Loop A (Jump/Entry): lookAhead=true - Hides knot under upcoming stitches + * - Loop B (End/Cut): lookAhead=false - Hides knot inside previous stitches + * - Loop C (Color Change): lookAhead=true - Aligns knot with stop event data + * * @param stitches Array of stitches to analyze * @param currentIndex Current stitch index * @param lookAhead If true, look forward; if false, look backward @@ -481,6 +487,8 @@ for i, stitch in enumerate(pattern.stitches): if (jumpDist > FEED_LENGTH) { // Long jump - add finishing lock stitches at previous position + // Loop B: End/Cut Vector - Look BACKWARD at previous stitches + // This hides the knot inside the embroidery we just finished const finishDir = calculateLockDirection(stitches, i - 1, false); penStitches.push(...generateLockStitches(prevX, prevY, finishDir.dirX, finishDir.dirY)); @@ -498,6 +506,8 @@ for i, stitch in enumerate(pattern.stitches): ); // Add starting lock stitches at new position + // Loop A: Jump/Entry Vector - Look FORWARD at upcoming stitches + // This hides the knot under the stitches we're about to make const startDir = calculateLockDirection(stitches, i, true); penStitches.push(...generateLockStitches(absX, absY, startDir.dirX, startDir.dirY)); @@ -561,7 +571,9 @@ for i, stitch in enumerate(pattern.stitches): console.log(`[PEN] Next stitch: cmd=${nextStitchCmd}, isJump=${nextIsJump}, pos=(${nextStitchX}, ${nextStitchY})`); // Step 1: Add finishing lock stitches at end of current color - const finishDir = calculateLockDirection(stitches, i, false); + // Loop C: Color Change Vector - Look FORWARD at the stop event data + // This aligns the knot with the stop command's data block for correct tension + const finishDir = calculateLockDirection(stitches, i, true); penStitches.push(...generateLockStitches(absX, absY, finishDir.dirX, finishDir.dirY)); console.log(`[PEN] Added 8 finishing lock stitches at (${absX}, ${absY}) dir=(${finishDir.dirX.toFixed(2)}, ${finishDir.dirY.toFixed(2)})`); @@ -626,7 +638,8 @@ for i, stitch in enumerate(pattern.stitches): console.log(`[PEN] Added COLOR_END marker at (${jumpToX}, ${jumpToY})`); // Step 5: Add starting lock stitches at the new position - // Look ahead from the next stitch (which might be a JUMP we skipped, so use i+1) + // Loop A: Jump/Entry Vector - Look FORWARD at upcoming stitches in new color + // This hides the knot under the stitches we're about to make const nextStitchIdx = nextIsJump ? i + 2 : i + 1; const startDir = calculateLockDirection(stitches, nextStitchIdx < stitches.length ? nextStitchIdx : i, true); penStitches.push(...generateLockStitches(jumpToX, jumpToY, startDir.dirX, startDir.dirY)); From abf7b9a67fa22b64d14a96fff71654e2352aa4e6 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 13 Dec 2025 23:38:13 +0100 Subject: [PATCH 5/5] fix: Resolve TypeScript build errors in PatternCanvas and imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix PenData import to use correct source (types/machine instead of penParser) - Add explicit return type annotations for map callbacks in PatternCanvas - Add parentheses around arrow function parameters to satisfy linter 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/PatternCanvas.tsx | 8 ++++---- src/utils/patternConverterClient.ts | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx index c9ab905..c7cdbdb 100644 --- a/src/components/PatternCanvas.tsx +++ b/src/components/PatternCanvas.tsx @@ -292,11 +292,11 @@ export function PatternCanvas() { }} > { + stitches={pesData.penStitches.stitches.map((s, i): [number, number, number, number] => { // Convert PEN stitch format {x, y, flags, isJump} to PES format [x, y, cmd, colorIndex] const cmd = s.isJump ? 0x10 : 0; // MOVE flag if jump const colorIndex = pesData.penStitches.colorBlocks.find( - b => i >= b.startStitch && i <= b.endStitch + (b) => i >= b.startStitch && i <= b.endStitch )?.colorIndex ?? 0; return [s.x, s.y, cmd, colorIndex]; })} @@ -315,10 +315,10 @@ export function PatternCanvas() { { + stitches={pesData.penStitches.stitches.map((s, i): [number, number, number, number] => { const cmd = s.isJump ? 0x10 : 0; const colorIndex = pesData.penStitches.colorBlocks.find( - b => i >= b.startStitch && i <= b.endStitch + (b) => i >= b.startStitch && i <= b.endStitch )?.colorIndex ?? 0; return [s.x, s.y, cmd, colorIndex]; })} diff --git a/src/utils/patternConverterClient.ts b/src/utils/patternConverterClient.ts index 30496c8..2ac46ec 100644 --- a/src/utils/patternConverterClient.ts +++ b/src/utils/patternConverterClient.ts @@ -1,6 +1,7 @@ import type { WorkerMessage, WorkerResponse } from '../workers/patternConverter.worker'; import PatternConverterWorker from '../workers/patternConverter.worker?worker'; -import { parsePenData, type PenData } from './penParser'; +import { parsePenData } from './penParser'; +import type { PenData } from '../types/machine'; export type PyodideState = 'not_loaded' | 'loading' | 'ready' | 'error';