Fix PES to PEN conversion and protocol implementation

- Use PyStitch raw stitches with proper command flag handling
- Import constants from pystitch.EmbConstant (STITCH, JUMP, TRIM, etc.)
- Filter COLOR_CHANGE, STOP, and END command-only stitches
- Properly encode jump/trim stitches with PEN_FEED_DATA flag
- Add pattern centering with moveX/moveY in layout
- Fix color change detection and PEN_COLOR_END marking
- Add comprehensive debug logging for pattern analysis
- Fix machine state helpers for IDLE and MASK_TRACE_COMPLETE states
- Update ProgressMonitor UI for proper button visibility
- Add error handling for undefined error codes

Machine now successfully uploads patterns, completes mask trace,
and transitions to sewing mode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik 2025-12-05 21:08:52 +01:00
parent acdf87b237
commit e0fadf69da
6 changed files with 356 additions and 89 deletions

View file

@ -230,15 +230,17 @@ export function ProgressMonitor({
</div> </div>
)} )}
{/* Mask trace complete - waiting for confirmation */} {/* Mask trace complete - ready to sew */}
{isMaskTraceComplete && ( {isMaskTraceComplete && (
<> <>
<div className="status-message success"> <div className="status-message success">
Mask trace complete! Mask trace complete!
</div> </div>
<div className="status-message warning"> {canStartSewing(machineStatus) && (
Press button on machine to confirm (or trace again) <button onClick={onStartSewing} className="btn-primary">
</div> Start Sewing
</button>
)}
{canStartMaskTrace(machineStatus) && ( {canStartMaskTrace(machineStatus) && (
<button onClick={onStartMaskTrace} className="btn-secondary"> <button onClick={onStartMaskTrace} className="btn-secondary">
Trace Again Trace Again
@ -247,6 +249,20 @@ export function ProgressMonitor({
</> </>
)} )}
{/* Pattern uploaded, ready to trace */}
{machineStatus === MachineStatus.IDLE && (
<>
<div className="status-message info">
Pattern uploaded successfully
</div>
{canStartMaskTrace(machineStatus) && (
<button onClick={onStartMaskTrace} className="btn-secondary">
Start Mask Trace
</button>
)}
</>
)}
{/* Ready to start (pattern uploaded) */} {/* Ready to start (pattern uploaded) */}
{machineStatus === MachineStatus.SEWING_WAIT && ( {machineStatus === MachineStatus.SEWING_WAIT && (
<> <>

View file

@ -202,9 +202,13 @@ export function useBrotherMachine() {
try { try {
setError(null); setError(null);
setUploadProgress(0); setUploadProgress(0);
const uuid = await service.uploadPattern(penData, (progress) => { const uuid = await service.uploadPattern(
penData,
(progress) => {
setUploadProgress(progress); setUploadProgress(progress);
}); },
pesData.bounds,
);
setUploadProgress(100); setUploadProgress(100);
// Cache the pattern with its UUID // Cache the pattern with its UUID

View file

@ -15,6 +15,7 @@ const Commands = {
MACHINE_INFO: 0x0000, MACHINE_INFO: 0x0000,
MACHINE_STATE: 0x0001, MACHINE_STATE: 0x0001,
SERVICE_COUNT: 0x0100, SERVICE_COUNT: 0x0100,
REGULAR_INSPECTION: 0x0103,
PATTERN_UUID_REQUEST: 0x0702, PATTERN_UUID_REQUEST: 0x0702,
MASK_TRACE: 0x0704, MASK_TRACE: 0x0704,
LAYOUT_SEND: 0x0705, LAYOUT_SEND: 0x0705,
@ -25,13 +26,18 @@ const Commands = {
EMB_UUID_SEND: 0x070a, EMB_UUID_SEND: 0x070a,
RESUME_FLAG_REQUEST: 0x070b, RESUME_FLAG_REQUEST: 0x070b,
RESUME: 0x070c, RESUME: 0x070c,
HOOP_AVOIDANCE: 0x070f,
START_SEWING: 0x070e, START_SEWING: 0x070e,
MASK_TRACE_1: 0x0710, MASK_TRACE_1: 0x0710,
EMB_ORG_POINT: 0x0800, EMB_ORG_POINT: 0x0800,
FIRM_UPDATE_START: 0x0b00,
SET_SETTING_REST: 0x0c00,
SET_SETTING_SEND: 0x0c01,
MACHINE_SETTING_INFO: 0x0c02, MACHINE_SETTING_INFO: 0x0c02,
SEND_DATA_INFO: 0x1200, SEND_DATA_INFO: 0x1200,
SEND_DATA: 0x1201, SEND_DATA: 0x1201,
CLEAR_ERROR: 0x1300, CLEAR_ERROR: 0x1300,
ERROR_LOG_REPLY: 0x1301,
}; };
export class BrotherPP1Service { export class BrotherPP1Service {
@ -132,6 +138,38 @@ export class BrotherPP1Service {
}); });
} }
private getCommandName(cmdId: number): string {
const names: Record<number, string> = {
[Commands.MACHINE_INFO]: "MACHINE_INFO",
[Commands.MACHINE_STATE]: "MACHINE_STATE",
[Commands.SERVICE_COUNT]: "SERVICE_COUNT",
[Commands.REGULAR_INSPECTION]: "REGULAR_INSPECTION",
[Commands.PATTERN_UUID_REQUEST]: "PATTERN_UUID_REQUEST",
[Commands.MASK_TRACE]: "MASK_TRACE",
[Commands.LAYOUT_SEND]: "LAYOUT_SEND",
[Commands.EMB_SEWING_INFO_REQUEST]: "EMB_SEWING_INFO_REQUEST",
[Commands.PATTERN_SEWING_INFO]: "PATTERN_SEWING_INFO",
[Commands.EMB_SEWING_DATA_DELETE]: "EMB_SEWING_DATA_DELETE",
[Commands.NEEDLE_MODE_INSTRUCTIONS]: "NEEDLE_MODE_INSTRUCTIONS",
[Commands.EMB_UUID_SEND]: "EMB_UUID_SEND",
[Commands.RESUME_FLAG_REQUEST]: "RESUME_FLAG_REQUEST",
[Commands.RESUME]: "RESUME",
[Commands.HOOP_AVOIDANCE]: "HOOP_AVOIDANCE",
[Commands.START_SEWING]: "START_SEWING",
[Commands.MASK_TRACE_1]: "MASK_TRACE_1",
[Commands.EMB_ORG_POINT]: "EMB_ORG_POINT",
[Commands.FIRM_UPDATE_START]: "FIRM_UPDATE_START",
[Commands.SET_SETTING_REST]: "SET_SETTING_REST",
[Commands.SET_SETTING_SEND]: "SET_SETTING_SEND",
[Commands.MACHINE_SETTING_INFO]: "MACHINE_SETTING_INFO",
[Commands.SEND_DATA_INFO]: "SEND_DATA_INFO",
[Commands.SEND_DATA]: "SEND_DATA",
[Commands.CLEAR_ERROR]: "CLEAR_ERROR",
[Commands.ERROR_LOG_REPLY]: "ERROR_LOG_REPLY",
};
return names[cmdId] || `UNKNOWN(0x${cmdId.toString(16).padStart(4, "0")})`;
}
private async sendCommand( private async sendCommand(
cmdId: number, cmdId: number,
data: Uint8Array = new Uint8Array(), data: Uint8Array = new Uint8Array(),
@ -148,30 +186,41 @@ export class BrotherPP1Service {
command[1] = cmdId & 0xff; // Low byte command[1] = cmdId & 0xff; // Low byte
command.set(data, 2); command.set(data, 2);
console.log( const hexData = Array.from(command)
"Sending command:",
Array.from(command)
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(" "), .join(" ");
console.log(
`[TX] ${this.getCommandName(cmdId)} (0x${cmdId.toString(16).padStart(4, "0")}):`,
hexData,
); );
console.log("Sending command");
// Write command and immediately read response
await this.writeCharacteristic.writeValueWithoutResponse(command);
console.log("delay"); // Write command
// Small delay to ensure response is ready await this.writeCharacteristic.writeValueWithResponse(command);
// Longer delay to allow machine to prepare response
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
console.log("reading response");
// Read response
const responseData = await this.readCharacteristic.readValue(); const responseData = await this.readCharacteristic.readValue();
const response = new Uint8Array(responseData.buffer); const response = new Uint8Array(responseData.buffer);
console.log( const hexResponse = Array.from(response)
"Received response:",
Array.from(response)
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(" "), .join(" ");
);
// Parse response
let parsed = "";
if (response.length >= 3) {
const respCmdId = (response[0] << 8) | response[1];
const status = response[2];
parsed = ` | Status: 0x${status.toString(16).padStart(2, "0")}`;
if (respCmdId !== cmdId) {
parsed += ` | WARNING: Response cmd 0x${respCmdId.toString(16).padStart(4, "0")} != request cmd`;
}
}
console.log(`[RX] ${this.getCommandName(cmdId)}:`, hexResponse, parsed);
return response; return response;
}); });
@ -289,7 +338,7 @@ export class BrotherPP1Service {
} }
} }
async sendDataChunk(offset: number, data: Uint8Array): Promise<boolean> { async sendDataChunk(offset: number, data: Uint8Array): Promise<void> {
const checksum = data.reduce((sum, byte) => (sum + byte) & 0xff, 0); const checksum = data.reduce((sum, byte) => (sum + byte) & 0xff, 0);
const payload = new Uint8Array(4 + data.length + 1); const payload = new Uint8Array(4 + data.length + 1);
@ -303,10 +352,81 @@ export class BrotherPP1Service {
payload.set(data, 4); payload.set(data, 4);
payload[4 + data.length] = checksum; payload[4 + data.length] = checksum;
const response = await this.sendCommand(Commands.SEND_DATA, payload); // Official app approach: Send chunk without waiting for response
await this.sendCommandNoResponse(Commands.SEND_DATA, payload);
}
// 0x00 = complete, 0x02 = continue private async sendCommandNoResponse(
return response[2] === 0x00; cmdId: number,
data: Uint8Array = new Uint8Array(),
): Promise<void> {
return this.enqueueCommand(async () => {
if (!this.writeCharacteristic) {
throw new Error("Not connected");
}
// Build command with big-endian command ID
const command = new Uint8Array(2 + data.length);
command[0] = (cmdId >> 8) & 0xff; // High byte
command[1] = cmdId & 0xff; // Low byte
command.set(data, 2);
const hexData = Array.from(command)
.map((b) => b.toString(16).padStart(2, "0"))
.join(" ");
console.log(
`[TX-NoResp] ${this.getCommandName(cmdId)} (0x${cmdId.toString(16).padStart(4, "0")}):`,
hexData.substring(0, 100) + (hexData.length > 100 ? "..." : ""),
);
// Write without reading response
await this.writeCharacteristic.writeValueWithResponse(command);
// Small delay to allow BLE buffer to clear
await new Promise((resolve) => setTimeout(resolve, 10));
});
}
private async pollForTransferComplete(): Promise<void> {
if (!this.readCharacteristic) {
throw new Error("Not connected");
}
// Poll until transfer is complete
while (true) {
const responseData = await this.readCharacteristic.readValue();
const response = new Uint8Array(responseData.buffer);
console.log(
"Poll response:",
Array.from(response)
.map((b) => b.toString(16).padStart(2, "0"))
.join(" "),
);
// Check response format: [CMD_HIGH, CMD_LOW, STATUS]
if (response.length < 3) {
throw new Error("Invalid response length");
}
const status = response[2];
if (status === 0x01) {
// Error
throw new Error("Transfer failed");
} else if (status === 0x00) {
// Complete
console.log("Transfer complete");
break;
} else if (status === 0x02) {
// Continue - wait 1 second and poll again (as per official app)
console.log("Transfer in progress, waiting...");
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
throw new Error(`Unknown transfer status: 0x${status.toString(16)}`);
}
}
} }
async sendUUID(uuid: Uint8Array): Promise<void> { async sendUUID(uuid: Uint8Array): Promise<void> {
@ -325,14 +445,19 @@ export class BrotherPP1Service {
rotate: number, rotate: number,
flip: number, flip: number,
frame: number, frame: number,
boundLeft: number,
boundTop: number,
boundRight: number,
boundBottom: number,
): Promise<void> { ): Promise<void> {
const payload = new Uint8Array(12); const payload = new Uint8Array(26);
const writeInt16LE = (offset: number, value: number) => { const writeInt16LE = (offset: number, value: number) => {
payload[offset] = value & 0xff; payload[offset] = value & 0xff;
payload[offset + 1] = (value >> 8) & 0xff; payload[offset + 1] = (value >> 8) & 0xff;
}; };
// Position/transformation parameters (12 bytes)
writeInt16LE(0, moveX); writeInt16LE(0, moveX);
writeInt16LE(2, moveY); writeInt16LE(2, moveY);
writeInt16LE(4, sizeX); writeInt16LE(4, sizeX);
@ -341,11 +466,41 @@ export class BrotherPP1Service {
payload[10] = flip; payload[10] = flip;
payload[11] = frame; payload[11] = frame;
// Pattern bounds (8 bytes)
writeInt16LE(12, boundLeft);
writeInt16LE(14, boundTop);
writeInt16LE(16, boundRight);
writeInt16LE(18, boundBottom);
// Repeat moveX and moveY at the end (6 bytes)
writeInt16LE(20, moveX);
writeInt16LE(22, moveY);
payload[24] = flip;
payload[25] = frame;
console.log('[DEBUG] Layout bounds:', {
boundLeft,
boundTop,
boundRight,
boundBottom,
moveX,
moveY,
sizeX,
sizeY,
});
await this.sendCommand(Commands.LAYOUT_SEND, payload); await this.sendCommand(Commands.LAYOUT_SEND, payload);
} }
async getMachineSettings(): Promise<Uint8Array> {
return await this.sendCommand(Commands.MACHINE_SETTING_INFO);
}
async startMaskTrace(): Promise<void> { async startMaskTrace(): Promise<void> {
const payload = new Uint8Array([0x01]); // Query machine settings before starting mask trace (as per official app)
await this.getMachineSettings();
const payload = new Uint8Array([0x00]);
await this.sendCommand(Commands.MASK_TRACE, payload); await this.sendCommand(Commands.MASK_TRACE, payload);
} }
@ -362,6 +517,7 @@ export class BrotherPP1Service {
async uploadPattern( async uploadPattern(
data: Uint8Array, data: Uint8Array,
onProgress?: (progress: number) => void, onProgress?: (progress: number) => void,
bounds?: { minX: number; maxX: number; minY: number; maxY: number },
): Promise<Uint8Array> { ): Promise<Uint8Array> {
// Calculate checksum // Calculate checksum
const checksum = data.reduce((sum, byte) => sum + byte, 0) & 0xffff; const checksum = data.reduce((sum, byte) => sum + byte, 0) & 0xffff;
@ -376,34 +532,64 @@ export class BrotherPP1Service {
const chunkSize = 500; const chunkSize = 500;
let offset = 0; let offset = 0;
// Send all chunks without waiting for responses (official app approach)
while (offset < data.length) { while (offset < data.length) {
const chunk = data.slice( const chunk = data.slice(
offset, offset,
Math.min(offset + chunkSize, data.length), Math.min(offset + chunkSize, data.length),
); );
const isComplete = await this.sendDataChunk(offset, chunk);
await this.sendDataChunk(offset, chunk);
offset += chunk.length; offset += chunk.length;
if (onProgress) { if (onProgress) {
onProgress((offset / data.length) * 100); onProgress((offset / data.length) * 100);
} }
if (isComplete) {
break;
} }
// Small delay between chunks // Wait a bit for machine to finish processing chunks
await new Promise((resolve) => setTimeout(resolve, 10)); await new Promise((resolve) => setTimeout(resolve, 500));
}
// Use provided bounds or default to 0
const boundLeft = bounds?.minX ?? 0;
const boundTop = bounds?.minY ?? 0;
const boundRight = bounds?.maxX ?? 0;
const boundBottom = bounds?.maxY ?? 0;
// Calculate pattern dimensions
const patternWidth = boundRight - boundLeft;
const patternHeight = boundBottom - boundTop;
// Calculate center offset to position pattern at machine center
// Machine embroidery area center is at (0, 0)
// Pattern center should align with machine center
const patternCenterX = (boundLeft + boundRight) / 2;
const patternCenterY = (boundTop + boundBottom) / 2;
// moveX/moveY shift the pattern so its center aligns with origin
const moveX = -patternCenterX;
const moveY = -patternCenterY;
// Send layout with actual pattern bounds
// sizeX/sizeY are scaling factors (100 = 100% = no scaling)
await this.sendLayout(
Math.round(moveX), // moveX - center the pattern
Math.round(moveY), // moveY - center the pattern
100, // sizeX (100% - no scaling)
100, // sizeY (100% - no scaling)
0, // rotate
0, // flip
1, // frame
boundLeft,
boundTop,
boundRight,
boundBottom,
);
// Generate random UUID // Generate random UUID
const uuid = crypto.getRandomValues(new Uint8Array(16)); const uuid = crypto.getRandomValues(new Uint8Array(16));
await this.sendUUID(uuid); await this.sendUUID(uuid);
// Send default layout (no transformation)
await this.sendLayout(0, 0, 0, 0, 0, 0, 0);
console.log( console.log(
"Pattern uploaded successfully with UUID:", "Pattern uploaded successfully with UUID:",
Array.from(uuid) Array.from(uuid)

View file

@ -76,7 +76,12 @@ const ERROR_MESSAGES: Record<number, string> = {
/** /**
* Get human-readable error message for an error code * Get human-readable error message for an error code
*/ */
export function getErrorMessage(errorCode: number): string | null { export function getErrorMessage(errorCode: number | undefined): string | null {
// Handle undefined or null
if (errorCode === undefined || errorCode === null) {
return null;
}
// 0xDD (221) is the default "no error" value // 0xDD (221) is the default "no error" value
if (errorCode === SewingMachineError.None) { if (errorCode === SewingMachineError.None) {
return null; // No error to display return null; // No error to display
@ -95,6 +100,6 @@ export function getErrorMessage(errorCode: number): string | null {
/** /**
* Check if error code represents an actual error condition * Check if error code represents an actual error condition
*/ */
export function hasError(errorCode: number): boolean { export function hasError(errorCode: number | undefined): boolean {
return errorCode !== SewingMachineError.None; return errorCode !== undefined && errorCode !== null && errorCode !== SewingMachineError.None;
} }

View file

@ -88,6 +88,7 @@ export function canUploadPattern(status: MachineStatus): boolean {
export function canStartSewing(status: MachineStatus): boolean { export function canStartSewing(status: MachineStatus): boolean {
// Only in specific ready states // Only in specific ready states
return status === MachineStatus.SEWING_WAIT || return status === MachineStatus.SEWING_WAIT ||
status === MachineStatus.MASK_TRACE_COMPLETE ||
status === MachineStatus.PAUSE || status === MachineStatus.PAUSE ||
status === MachineStatus.STOP || status === MachineStatus.STOP ||
status === MachineStatus.SEWING_INTERRUPTION; status === MachineStatus.SEWING_INTERRUPTION;
@ -97,8 +98,9 @@ export function canStartSewing(status: MachineStatus): boolean {
* Determines if mask trace can be started in the current state. * Determines if mask trace can be started in the current state.
*/ */
export function canStartMaskTrace(status: MachineStatus): boolean { export function canStartMaskTrace(status: MachineStatus): boolean {
// Only when ready or after previous trace // Can start mask trace when IDLE (after upload), SEWING_WAIT, or after previous trace
return status === MachineStatus.SEWING_WAIT || return status === MachineStatus.IDLE ||
status === MachineStatus.SEWING_WAIT ||
status === MachineStatus.MASK_TRACE_COMPLETE; status === MachineStatus.MASK_TRACE_COMPLETE;
} }

View file

@ -46,45 +46,38 @@ export async function convertPesToPen(file: File): Promise<PesPatternData> {
// Read the pattern using PyStitch // Read the pattern using PyStitch
const result = await pyodide.runPythonAsync(` const result = await pyodide.runPythonAsync(`
import pystitch import pystitch
from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE
# Read the PES file # Read the PES file
pattern = pystitch.read('${filename}') pattern = pystitch.read('${filename}')
# PyStitch groups stitches by color blocks using get_as_stitchblock # Use the raw stitches list which preserves command flags
# This returns tuples of (thread, stitches_list) for each color block # 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)
stitches_with_colors = [] stitches_with_colors = []
block_index = 0 current_color = 0
# Iterate through stitch blocks for i, stitch in enumerate(pattern.stitches):
# Each block is a tuple containing (thread, stitch_list) x, y, cmd = stitch
for block in pattern.get_as_stitchblock():
if isinstance(block, tuple):
# Extract thread and stitch list from tuple
thread_obj = None
stitches_list = None
for elem in block: # Check for color change command - increment color but don't add stitch
# Check if this is the thread object (has color or hex_color attributes) if cmd == COLOR_CHANGE:
if hasattr(elem, 'color') or hasattr(elem, 'hex_color'): current_color += 1
thread_obj = elem continue
# Check if this is the stitch list
elif isinstance(elem, list) and len(elem) > 0 and isinstance(elem[0], list):
stitches_list = elem
if stitches_list: # Check for stop command - skip it
# Find the index of this thread in the threadlist if cmd == STOP:
thread_index = block_index continue
if thread_obj and hasattr(pattern, 'threadlist'):
for i, t in enumerate(pattern.threadlist):
if t is thread_obj:
thread_index = i
break
for stitch in stitches_list: # Check for standalone END command (no stitch data)
# stitch is [x, y, command] if cmd == END:
stitches_with_colors.append([stitch[0], stitch[1], stitch[2], thread_index]) continue
block_index += 1 # Add actual stitch with color index and command
# Keep JUMP/TRIM flags as they indicate jump stitches
stitches_with_colors.append([x, y, cmd, current_color])
# Convert to JSON-serializable format # Convert to JSON-serializable format
{ {
@ -98,13 +91,16 @@ for block in pattern.get_as_stitchblock():
], ],
'thread_count': len(pattern.threadlist), 'thread_count': len(pattern.threadlist),
'stitch_count': len(stitches_with_colors), 'stitch_count': len(stitches_with_colors),
'block_count': block_index 'color_changes': current_color
} }
`); `);
// Convert Python result to JavaScript // Convert Python result to JavaScript
const data = result.toJs({ dict_converter: Object.fromEntries }); const data = result.toJs({ dict_converter: Object.fromEntries });
console.log('[DEBUG] PyStitch stitch_count:', data.stitch_count);
console.log('[DEBUG] PyStitch color_changes:', data.color_changes);
// Clean up virtual file // Clean up virtual file
try { try {
pyodide.FS.unlink(filename); pyodide.FS.unlink(filename);
@ -117,6 +113,32 @@ for block in pattern.get_as_stitchblock():
Array.from(stitch) as number[] Array.from(stitch) as number[]
); );
console.log('[DEBUG] JavaScript stitches.length:', stitches.length);
console.log('[DEBUG] First 5 stitches:', stitches.slice(0, 5));
console.log('[DEBUG] Middle 5 stitches:', stitches.slice(Math.floor(stitches.length / 2), Math.floor(stitches.length / 2) + 5));
console.log('[DEBUG] Last 5 stitches:', stitches.slice(-5));
// Count stitch types (PyStitch constants: STITCH=0, JUMP=1, TRIM=2)
let jumpCount = 0, normalCount = 0;
for (let i = 0; i < stitches.length; i++) {
const cmd = stitches[i][2];
if (cmd === 1 || cmd === 2) jumpCount++; // JUMP or TRIM
else normalCount++; // STITCH (0)
}
console.log('[DEBUG] Stitch types: normal=' + normalCount + ', jump/trim=' + jumpCount);
// Calculate min/max of raw stitch values to understand the data
let rawMinX = Infinity, rawMaxX = -Infinity, rawMinY = Infinity, rawMaxY = -Infinity;
for (let i = 0; i < stitches.length; i++) {
const x = stitches[i][0];
const y = stitches[i][1];
rawMinX = Math.min(rawMinX, x);
rawMaxX = Math.max(rawMaxX, x);
rawMinY = Math.min(rawMinY, y);
rawMaxY = Math.max(rawMaxY, y);
}
console.log('[DEBUG] Raw stitch value ranges:', { rawMinX, rawMaxX, rawMinY, rawMaxY });
if (!stitches || stitches.length === 0) { if (!stitches || stitches.length === 0) {
throw new Error('Invalid PES file or no stitches found'); throw new Error('Invalid PES file or no stitches found');
} }
@ -133,32 +155,35 @@ for block in pattern.get_as_stitchblock():
let minY = Infinity; let minY = Infinity;
let maxY = -Infinity; let maxY = -Infinity;
// Convert to PEN format // PyStitch returns ABSOLUTE coordinates
// PEN format uses absolute coordinates, shifted left by 3 bits (as per official app line 780)
const penStitches: number[] = []; const penStitches: number[] = [];
let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index
for (let i = 0; i < stitches.length; i++) { for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i]; const stitch = stitches[i];
const x = Math.round(stitch[0]); const absX = Math.round(stitch[0]);
const y = Math.round(stitch[1]); const absY = Math.round(stitch[1]);
const cmd = stitch[2]; const cmd = stitch[2];
const stitchColor = stitch[3]; // Color index from PyStitch const stitchColor = stitch[3]; // Color index from PyStitch
// Track bounds for non-jump stitches // Track bounds for non-jump stitches (cmd=0 is STITCH)
if ((cmd & MOVE) === 0) { if (cmd === 0) {
minX = Math.min(minX, x); minX = Math.min(minX, absX);
maxX = Math.max(maxX, x); maxX = Math.max(maxX, absX);
minY = Math.min(minY, y); minY = Math.min(minY, absY);
maxY = Math.max(maxY, y); maxY = Math.max(maxY, absY);
} }
// Encode coordinates with flags in low 3 bits // Encode absolute coordinates with flags in low 3 bits
// Shift coordinates left by 3 bits to make room for flags // Shift coordinates left by 3 bits to make room for flags
let xEncoded = (x << 3) & 0xFFFF; // As per official app line 780: buffer[index64] = (byte) ((int) numArray4[index64 / 4, 0] << 3 & (int) byte.MaxValue);
let yEncoded = (y << 3) & 0xFFFF; let xEncoded = (absX << 3) & 0xFFFF;
let yEncoded = (absY << 3) & 0xFFFF;
// Add jump flag if this is a move command // Add jump flag if this is a JUMP (1) or TRIM (2) command
if ((cmd & MOVE) !== 0) { // PyStitch constants: STITCH=0, JUMP=1, TRIM=2
if (cmd === 1 || cmd === 2) {
yEncoded |= PEN_FEED_DATA; yEncoded |= PEN_FEED_DATA;
} }
@ -200,6 +225,35 @@ for block in pattern.get_as_stitchblock():
const penData = new Uint8Array(penStitches); const penData = new Uint8Array(penStitches);
console.log('[DEBUG] PEN data size:', penData.length, 'bytes');
console.log('[DEBUG] Encoded stitch count:', penData.length / 4);
console.log('[DEBUG] Expected vs Actual:', data.stitch_count, 'vs', penData.length / 4);
console.log('[DEBUG] First 20 bytes (5 stitches):',
Array.from(penData.slice(0, 20))
.map(b => b.toString(16).padStart(2, '0'))
.join(' '));
console.log('[DEBUG] Last 20 bytes (5 stitches):',
Array.from(penData.slice(-20))
.map(b => b.toString(16).padStart(2, '0'))
.join(' '));
console.log('[DEBUG] Calculated bounds from stitches:', {
minX,
maxX,
minY,
maxY,
});
// Check for color change markers and end marker
let colorChangeCount = 0;
let hasEndMarker = false;
for (let i = 0; i < penData.length; i += 4) {
const xLow = penData[i];
const yLow = penData[i + 2];
if ((xLow & 0x07) === PEN_COLOR_END) colorChangeCount++;
if ((xLow & 0x07) === PEN_DATA_END) hasEndMarker = true;
}
console.log('[DEBUG] Color changes found:', colorChangeCount, '| Has END marker:', hasEndMarker);
return { return {
stitches, stitches,
threads, threads,