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',