fix: Implement rotated lock stitches and improve PEN format handling

- 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 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik 2025-12-13 23:25:48 +01:00
parent 0bd037b98a
commit 8a32d5184e
6 changed files with 335 additions and 64 deletions

View file

@ -292,7 +292,14 @@ export function PatternCanvas() {
}}
>
<Stitches
stitches={pesData.stitches}
stitches={pesData.penStitches.stitches.map((s, i) => {
// 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 */}
<Layer>
{pesData && sewingProgress && sewingProgress.currentStitch > 0 && (
{pesData && pesData.penStitches && sewingProgress && sewingProgress.currentStitch > 0 && (
<Group x={localPatternOffset.x} y={localPatternOffset.y}>
<CurrentPosition
currentStitchIndex={sewingProgress.currentStitch}
stitches={pesData.stitches}
stitches={pesData.penStitches.stitches.map((s, i) => {
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];
})}
/>
</Group>
)}

View file

@ -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;

View file

@ -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<SewingProgress> {

View file

@ -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;

View file

@ -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;

View file

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