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/components/PatternCanvas.tsx b/src/components/PatternCanvas.tsx
index e47b035..c7cdbdb 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..e775f31 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",
@@ -278,7 +280,7 @@ export function useBrotherMachine() {
setIsUploading(false); // Clear loading state
}
},
- [service, storageService, isConnected, refreshStatus, refreshPatternInfo],
+ [service, storageService, isConnected, refreshStatus],
);
const startMaskTrace = useCallback(async () => {
@@ -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/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/patternConverterClient.ts b/src/utils/patternConverterClient.ts
index aaa0d61..2ac46ec 100644
--- a/src/utils/patternConverterClient.ts
+++ b/src/utils/patternConverterClient.ts
@@ -1,10 +1,12 @@
import type { WorkerMessage, WorkerResponse } from '../workers/patternConverter.worker';
import PatternConverterWorker from '../workers/patternConverter.worker?worker';
+import { parsePenData } from './penParser';
+import type { PenData } from '../types/machine';
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 +24,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 +178,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 9c112e4..edee9b6 100644
--- a/src/utils/penParser.ts
+++ b/src/utils/penParser.ts
@@ -33,13 +33,20 @@ 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
+ // Step 1: Clear the flag bits (low 3 bits) from the raw values
+ const xRawClean = xRaw & 0xFFF8;
+ const yRawClean = yRaw & 0xFFF8;
- // Convert to signed if needed
- if (x > 0x7FF) x = x - 0x2000;
- if (y > 0x7FF) y = y - 0x2000;
+ // 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;
+
+ // Step 3: Shift right by 3 (arithmetic shift, preserves sign)
+ 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 0878053..88d56a1 100644
--- a/src/workers/patternConverter.worker.ts
+++ b/src/workers/patternConverter.worker.ts
@@ -152,6 +152,133 @@ 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
+ * @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
*/
@@ -210,6 +337,10 @@ 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
@@ -217,7 +348,7 @@ current_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:
current_color += 1
continue
@@ -230,8 +361,17 @@ 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
+ # 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 x == last_x and y == last_y:
+ # Duplicate position - skip it
+ continue
+
+ # Add actual stitch with current color index and mapped command
mapped_cmd = map_cmd(cmd)
stitches_with_colors.append([x, y, mapped_cmd, current_color])
@@ -319,6 +459,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]);
@@ -334,6 +481,43 @@ 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
+ // 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));
+
+ // Encode jump with both FEED and CUT flags
+ 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
+
+ penStitches.push(
+ xEncoded & 0xff,
+ (xEncoded >> 8) & 0xff,
+ yEncoded & 0xff,
+ (yEncoded >> 8) & 0xff
+ );
+
+ // 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));
+
+ // 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);
@@ -354,20 +538,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;
}
@@ -379,6 +555,101 @@ 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
+ // 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)})`);
+
+ // 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
+ 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
+ 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) {
+ const 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;
+ const 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
+ // 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));
+ 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;
@@ -415,6 +686,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',