respira/src/utils/pystitchConverter.ts
Jan-Henrik Bruhn d0a8273fee Fix PEN encoding and improve UI layout
Major fixes:
- Fix PEN data encoding to properly mark color changes and end markers
  - COLOR_END (0x03) for intermediate color blocks
  - DATA_END (0x05) for the final stitch only
  - Machine now correctly reads total stitch count across all color blocks
- Reset uploadProgress when pattern is deleted to re-enable upload button
- Allow pattern deletion during WAITING states
- Allow pattern upload in COMPLETE states
- Fix pattern state tracking to reset when patternInfo is null

UI improvements:
- Integrate workflow stepper into compact header
- Change app title to "SKiTCH Controller"
- Reduce header size from ~200px to ~70px
- Make Sewing Progress section more compact with two-column layout
- Replace emojis with Heroicons throughout
- Reorganize action buttons with better visual hierarchy
- Add cursor-pointer to all buttons for better UX
- Fix cached pattern not showing info in Pattern File box
- Remove duplicate status messages (keep only state visual indicator)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 12:56:27 +01:00

226 lines
6.8 KiB
TypeScript

import { pyodideLoader } from './pyodideLoader';
// PEN format flags
const PEN_FEED_DATA = 0x01; // Y-coordinate low byte, bit 0 (jump)
const PEN_COLOR_END = 0x03; // X-coordinate low byte, bits 0-2
const PEN_DATA_END = 0x05; // X-coordinate low byte, bits 0-2
// Embroidery command constants (from pyembroidery)
const MOVE = 0x10;
const COLOR_CHANGE = 0x40;
const STOP = 0x80;
const END = 0x100;
export interface PesPatternData {
stitches: number[][];
threads: Array<{
color: number;
hex: string;
}>;
penData: Uint8Array;
colorCount: number;
stitchCount: number;
bounds: {
minX: number;
maxX: number;
minY: number;
maxY: number;
};
}
/**
* Reads a PES file using PyStitch and converts it to PEN format
*/
export async function convertPesToPen(file: File): Promise<PesPatternData> {
// Ensure Pyodide is initialized
const pyodide = await pyodideLoader.initialize();
// Read the PES file
const buffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(buffer);
// Write file to Pyodide virtual filesystem
const filename = '/tmp/pattern.pes';
pyodide.FS.writeFile(filename, uint8Array);
// Read the pattern using PyStitch
const result = await pyodide.runPythonAsync(`
import pystitch
from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE
# Read the PES file
pattern = pystitch.read('${filename}')
# Use the raw stitches list which preserves command flags
# 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 = []
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
if cmd == COLOR_CHANGE:
current_color += 1
continue
# Check for stop command - skip it
if cmd == STOP:
continue
# Check for standalone END command (no stitch data)
if cmd == END:
continue
# 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
{
'stitches': stitches_with_colors,
'threads': [
{
'color': thread.color if hasattr(thread, 'color') else 0,
'hex': thread.hex_color() if hasattr(thread, 'hex_color') else '#000000'
}
for thread in pattern.threadlist
],
'thread_count': len(pattern.threadlist),
'stitch_count': len(stitches_with_colors),
'color_changes': current_color
}
`);
// Convert Python result to JavaScript
const data = result.toJs({ dict_converter: Object.fromEntries });
// Clean up virtual file
try {
pyodide.FS.unlink(filename);
} catch (e) {
// Ignore errors
}
// Extract stitches and validate
const stitches: number[][] = Array.from(data.stitches).map((stitch: any) =>
Array.from(stitch) as number[]
);
if (!stitches || stitches.length === 0) {
throw new Error('Invalid PES file or no stitches found');
}
// Extract thread data
const threads = data.threads.map((thread: any) => ({
color: thread.color || 0,
hex: thread.hex || '#000000',
}));
// Track bounds
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
// PyStitch returns ABSOLUTE coordinates
// PEN format uses absolute coordinates, shifted left by 3 bits (as per official app line 780)
const penStitches: number[] = [];
let currentColor = stitches[0]?.[3] ?? 0; // Track current color using stitch color index
for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i];
const absX = Math.round(stitch[0]);
const absY = Math.round(stitch[1]);
const cmd = stitch[2];
const stitchColor = stitch[3]; // Color index from PyStitch
// Track bounds for non-jump stitches (cmd=0 is STITCH)
if (cmd === 0) {
minX = Math.min(minX, absX);
maxX = Math.max(maxX, absX);
minY = Math.min(minY, absY);
maxY = Math.max(maxY, absY);
}
// 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);
let xEncoded = (absX << 3) & 0xFFFF;
let yEncoded = (absY << 3) & 0xFFFF;
// Add jump flag if this is a JUMP (1) or TRIM (2) command
// PyStitch constants: STITCH=0, JUMP=1, TRIM=2
if (cmd === 1 || cmd === 2) {
yEncoded |= PEN_FEED_DATA;
}
// Check if this is the last stitch
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];
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;
currentColor = nextStitchColor;
} else if (isLastStitch) {
// This is the very last stitch of the pattern
xEncoded = (xEncoded & 0xFFF8) | PEN_DATA_END;
}
// Add stitch as 4 bytes: [X_low, X_high, Y_low, Y_high]
penStitches.push(
xEncoded & 0xFF,
(xEncoded >> 8) & 0xFF,
yEncoded & 0xFF,
(yEncoded >> 8) & 0xFF
);
// Check for end command
if ((cmd & END) !== 0) {
break;
}
}
const penData = new Uint8Array(penStitches);
return {
stitches,
threads,
penData,
colorCount: data.thread_count,
stitchCount: data.stitch_count,
bounds: {
minX: minX === Infinity ? 0 : minX,
maxX: maxX === -Infinity ? 0 : maxX,
minY: minY === Infinity ? 0 : minY,
maxY: maxY === -Infinity ? 0 : maxY,
},
};
}
/**
* Get thread color from pattern data
*/
export function getThreadColor(data: PesPatternData, colorIndex: number): string {
if (!data.threads || colorIndex < 0 || colorIndex >= data.threads.length) {
// Default colors if not specified or index out of bounds
const defaultColors = [
'#FF0000', '#00FF00', '#0000FF', '#FFFF00',
'#FF00FF', '#00FFFF', '#FFA500', '#800080',
];
const safeIndex = Math.max(0, colorIndex) % defaultColors.length;
return defaultColors[safeIndex];
}
return data.threads[colorIndex]?.hex || '#000000';
}