respira/src/utils/patternConverterClient.ts
Jan-Henrik Bruhn abf7b9a67f fix: Resolve TypeScript build errors in PatternCanvas and imports
- Fix PenData import to use correct source (types/machine instead of penParser)
- Add explicit return type annotations for map callbacks in PatternCanvas
- Add parentheses around arrow function parameters to satisfy linter

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 23:38:13 +01:00

258 lines
7.6 KiB
TypeScript

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[][]; // Original PES stitches (for reference)
threads: Array<{
color: number;
hex: string;
brand: string | null;
catalogNumber: string | null;
description: string | null;
chart: string | null;
}>;
uniqueColors: Array<{
color: number;
hex: string;
brand: string | null;
catalogNumber: string | null;
description: string | null;
chart: string | null;
threadIndices: number[];
}>;
penData: Uint8Array; // Raw PEN bytes sent to machine
penStitches: PenData; // Parsed PEN stitches (for rendering)
colorCount: number;
stitchCount: number;
bounds: {
minX: number;
maxX: number;
minY: number;
maxY: number;
};
}
export type ProgressCallback = (progress: number, step: string) => void;
class PatternConverterClient {
private worker: Worker | null = null;
private state: PyodideState = 'not_loaded';
private error: string | null = null;
private initPromise: Promise<void> | null = null;
private progressCallbacks: Set<ProgressCallback> = new Set();
/**
* Get the current Pyodide state
*/
getState(): PyodideState {
return this.state;
}
/**
* Get the error message if state is 'error'
*/
getError(): string | null {
return this.error;
}
/**
* Initialize the worker and load Pyodide
*/
async initialize(onProgress?: ProgressCallback): Promise<void> {
// If already ready, return immediately
if (this.state === 'ready') {
return;
}
// If currently loading, add progress callback and wait for the existing promise
if (this.initPromise) {
if (onProgress) {
this.progressCallbacks.add(onProgress);
}
return this.initPromise;
}
// Create worker if it doesn't exist
if (!this.worker) {
console.log('[PatternConverterClient] Creating worker...');
try {
this.worker = new PatternConverterWorker();
console.log('[PatternConverterClient] Worker created successfully');
this.setupWorkerListeners();
} catch (err) {
console.error('[PatternConverterClient] Failed to create worker:', err);
throw err;
}
}
// Add progress callback if provided
if (onProgress) {
this.progressCallbacks.add(onProgress);
}
// Start initialization
this.state = 'loading';
this.error = null;
this.initPromise = new Promise<void>((resolve, reject) => {
const handleMessage = (event: MessageEvent<WorkerResponse>) => {
const message = event.data;
switch (message.type) {
case 'INIT_PROGRESS':
// Notify all progress callbacks
this.progressCallbacks.forEach((callback) => {
callback(message.progress, message.step);
});
break;
case 'INIT_COMPLETE':
this.state = 'ready';
this.progressCallbacks.clear();
this.worker?.removeEventListener('message', handleMessage);
resolve();
break;
case 'INIT_ERROR':
this.state = 'error';
this.error = message.error;
this.progressCallbacks.clear();
this.worker?.removeEventListener('message', handleMessage);
reject(new Error(message.error));
break;
}
};
this.worker?.addEventListener('message', handleMessage);
// Send initialization message with asset URLs
// Resolve URLs relative to the current page location
const baseURL = window.location.origin + window.location.pathname.replace(/\/[^/]*$/, '/');
const pyodideIndexURL = new URL('assets/', baseURL).href;
const pystitchWheelURL = new URL('pystitch-1.0.0-py3-none-any.whl', baseURL).href;
console.log('[PatternConverterClient] Base URL:', baseURL);
console.log('[PatternConverterClient] Pyodide index URL:', pyodideIndexURL);
console.log('[PatternConverterClient] Pystitch wheel URL:', pystitchWheelURL);
const initMessage: WorkerMessage = {
type: 'INITIALIZE',
pyodideIndexURL,
pystitchWheelURL,
};
this.worker?.postMessage(initMessage);
});
return this.initPromise;
}
/**
* Convert PES file to PEN format using the worker
*/
async convertPesToPen(file: File): Promise<PesPatternData> {
// Ensure worker is initialized
if (this.state !== 'ready') {
throw new Error('Pyodide worker not initialized. Call initialize() first.');
}
if (!this.worker) {
throw new Error('Worker not available');
}
return new Promise<PesPatternData>((resolve, reject) => {
// Store reference to worker for TypeScript null checking
const worker = this.worker;
if (!worker) {
reject(new Error('Worker not available'));
return;
}
const handleMessage = (event: MessageEvent<WorkerResponse>) => {
const message = event.data;
switch (message.type) {
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,
penStitches,
};
resolve(result);
break;
}
case 'CONVERT_ERROR':
worker.removeEventListener('message', handleMessage);
reject(new Error(message.error));
break;
}
};
worker.addEventListener('message', handleMessage);
// Read file as ArrayBuffer and send to worker
const reader = new FileReader();
reader.onload = () => {
const convertMessage: WorkerMessage = {
type: 'CONVERT_PES',
fileData: reader.result as ArrayBuffer,
fileName: file.name,
};
worker.postMessage(convertMessage);
};
reader.onerror = () => {
worker.removeEventListener('message', handleMessage);
reject(new Error('Failed to read file'));
};
reader.readAsArrayBuffer(file);
});
}
/**
* Setup worker event listeners
*/
private setupWorkerListeners() {
if (!this.worker) return;
this.worker.addEventListener('error', (event) => {
console.error('[PyodideWorkerClient] Worker error:', event);
this.state = 'error';
this.error = event.message || 'Worker error';
});
this.worker.addEventListener('messageerror', (event) => {
console.error('[PyodideWorkerClient] Worker message error:', event);
this.state = 'error';
this.error = 'Failed to deserialize worker message';
});
}
/**
* Terminate the worker (cleanup)
*/
terminate() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
this.state = 'not_loaded';
this.error = null;
this.initPromise = null;
this.progressCallbacks.clear();
}
}
// Export singleton instance
export const patternConverterClient = new PatternConverterClient();