mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Merge pull request #8 from jhbruhn/fix/pattern-rendering-and-coordinate-bugs
Fix: pattern rendering, coordinate bugs, rotated lock stitches, fixed color changes
This commit is contained in:
commit
a1b009065c
8 changed files with 394 additions and 46 deletions
|
|
@ -154,6 +154,9 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgr
|
||||||
const groups: StitchGroup[] = [];
|
const groups: StitchGroup[] = [];
|
||||||
let currentGroup: StitchGroup | null = null;
|
let currentGroup: StitchGroup | null = null;
|
||||||
|
|
||||||
|
let prevX = 0;
|
||||||
|
let prevY = 0;
|
||||||
|
|
||||||
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, y, cmd, colorIndex] = stitch;
|
const [x, y, cmd, colorIndex] = stitch;
|
||||||
|
|
@ -168,16 +171,30 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgr
|
||||||
currentGroup.completed !== isCompleted ||
|
currentGroup.completed !== isCompleted ||
|
||||||
currentGroup.isJump !== isJump
|
currentGroup.isJump !== isJump
|
||||||
) {
|
) {
|
||||||
currentGroup = {
|
// For jump stitches, we need to create a line from previous position to current position
|
||||||
color,
|
// So we include both the previous point and current point
|
||||||
points: [x, y],
|
if (isJump && i > 0) {
|
||||||
completed: isCompleted,
|
currentGroup = {
|
||||||
isJump,
|
color,
|
||||||
};
|
points: [prevX, prevY, x, y],
|
||||||
|
completed: isCompleted,
|
||||||
|
isJump,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentGroup = {
|
||||||
|
color,
|
||||||
|
points: [x, y],
|
||||||
|
completed: isCompleted,
|
||||||
|
isJump,
|
||||||
|
};
|
||||||
|
}
|
||||||
groups.push(currentGroup);
|
groups.push(currentGroup);
|
||||||
} else {
|
} else {
|
||||||
currentGroup.points.push(x, y);
|
currentGroup.points.push(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevX = x;
|
||||||
|
prevY = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
|
|
@ -189,12 +206,12 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgr
|
||||||
<Line
|
<Line
|
||||||
key={i}
|
key={i}
|
||||||
points={group.points}
|
points={group.points}
|
||||||
stroke={group.isJump ? (group.completed ? '#cccccc' : '#e8e8e8') : group.color}
|
stroke={group.color}
|
||||||
strokeWidth={1.5}
|
strokeWidth={group.isJump ? 1.5 : 1.5}
|
||||||
lineCap="round"
|
lineCap="round"
|
||||||
lineJoin="round"
|
lineJoin="round"
|
||||||
dash={group.isJump ? [3, 3] : undefined}
|
dash={group.isJump ? [8, 4] : undefined}
|
||||||
opacity={group.isJump ? 1 : (showProgress && !group.completed ? 0.75 : 1.0)}
|
opacity={group.isJump ? (group.completed ? 0.8 : 0.5) : (showProgress && !group.completed ? 0.3 : 1.0)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,14 @@ export function PatternCanvas() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stitches
|
<Stitches
|
||||||
stitches={pesData.stitches}
|
stitches={pesData.penStitches.stitches.map((s, i): [number, number, number, number] => {
|
||||||
|
// 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}
|
pesData={pesData}
|
||||||
currentStitchIndex={sewingProgress?.currentStitch || 0}
|
currentStitchIndex={sewingProgress?.currentStitch || 0}
|
||||||
showProgress={patternUploaded || isUploading}
|
showProgress={patternUploaded || isUploading}
|
||||||
|
|
@ -304,11 +311,17 @@ export function PatternCanvas() {
|
||||||
|
|
||||||
{/* Current position layer */}
|
{/* Current position layer */}
|
||||||
<Layer>
|
<Layer>
|
||||||
{pesData && sewingProgress && sewingProgress.currentStitch > 0 && (
|
{pesData && pesData.penStitches && sewingProgress && sewingProgress.currentStitch > 0 && (
|
||||||
<Group x={localPatternOffset.x} y={localPatternOffset.y}>
|
<Group x={localPatternOffset.x} y={localPatternOffset.y}>
|
||||||
<CurrentPosition
|
<CurrentPosition
|
||||||
currentStitchIndex={sewingProgress.currentStitch}
|
currentStitchIndex={sewingProgress.currentStitch}
|
||||||
stitches={pesData.stitches}
|
stitches={pesData.penStitches.stitches.map((s, i): [number, number, number, number] => {
|
||||||
|
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>
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -267,9 +267,11 @@ export function useBrotherMachine() {
|
||||||
setResumeAvailable(false);
|
setResumeAvailable(false);
|
||||||
setResumeFileName(null);
|
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 refreshStatus();
|
||||||
await refreshPatternInfo();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
err instanceof Error ? err.message : "Failed to upload pattern",
|
err instanceof Error ? err.message : "Failed to upload pattern",
|
||||||
|
|
@ -278,7 +280,7 @@ export function useBrotherMachine() {
|
||||||
setIsUploading(false); // Clear loading state
|
setIsUploading(false); // Clear loading state
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[service, storageService, isConnected, refreshStatus, refreshPatternInfo],
|
[service, storageService, isConnected, refreshStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startMaskTrace = useCallback(async () => {
|
const startMaskTrace = useCallback(async () => {
|
||||||
|
|
@ -287,13 +289,24 @@ export function useBrotherMachine() {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
await service.startMaskTrace();
|
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) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
err instanceof Error ? err.message : "Failed to start mask trace",
|
err instanceof Error ? err.message : "Failed to start mask trace",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [service, isConnected, refreshStatus]);
|
}, [service, isConnected, refreshStatus, refreshPatternInfo]);
|
||||||
|
|
||||||
const startSewing = useCallback(async () => {
|
const startSewing = useCallback(async () => {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,7 @@ export class BrotherPP1Service {
|
||||||
const readUInt16LE = (offset: number) =>
|
const readUInt16LE = (offset: number) =>
|
||||||
data[offset] | (data[offset + 1] << 8);
|
data[offset] | (data[offset + 1] << 8);
|
||||||
|
|
||||||
return {
|
const patternInfo = {
|
||||||
boundLeft: readInt16LE(0),
|
boundLeft: readInt16LE(0),
|
||||||
boundTop: readInt16LE(2),
|
boundTop: readInt16LE(2),
|
||||||
boundRight: readInt16LE(4),
|
boundRight: readInt16LE(4),
|
||||||
|
|
@ -403,6 +403,13 @@ export class BrotherPP1Service {
|
||||||
totalStitches: readUInt16LE(10),
|
totalStitches: readUInt16LE(10),
|
||||||
speed: readUInt16LE(12),
|
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> {
|
async getSewingProgress(): Promise<SewingProgress> {
|
||||||
|
|
|
||||||
|
|
@ -159,14 +159,15 @@ export function renderStitches(
|
||||||
// Create Konva.Line for each group
|
// Create Konva.Line for each group
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
if (group.isJump) {
|
if (group.isJump) {
|
||||||
// Jump stitches - dashed gray lines
|
// Jump stitches - dashed lines in thread color
|
||||||
const line = new Konva.Line({
|
const line = new Konva.Line({
|
||||||
points: group.points,
|
points: group.points,
|
||||||
stroke: group.completed ? '#cccccc' : '#e8e8e8',
|
stroke: group.color,
|
||||||
strokeWidth: 1.5,
|
strokeWidth: 1.0,
|
||||||
lineCap: 'round',
|
lineCap: 'round',
|
||||||
lineJoin: 'round',
|
lineJoin: 'round',
|
||||||
dash: [3, 3],
|
dash: [5, 5],
|
||||||
|
opacity: group.completed ? 0.6 : 0.25,
|
||||||
});
|
});
|
||||||
stitchesGroup.add(line);
|
stitchesGroup.add(line);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import type { WorkerMessage, WorkerResponse } from '../workers/patternConverter.worker';
|
import type { WorkerMessage, WorkerResponse } from '../workers/patternConverter.worker';
|
||||||
import PatternConverterWorker from '../workers/patternConverter.worker?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 type PyodideState = 'not_loaded' | 'loading' | 'ready' | 'error';
|
||||||
|
|
||||||
export interface PesPatternData {
|
export interface PesPatternData {
|
||||||
stitches: number[][];
|
stitches: number[][]; // Original PES stitches (for reference)
|
||||||
threads: Array<{
|
threads: Array<{
|
||||||
color: number;
|
color: number;
|
||||||
hex: string;
|
hex: string;
|
||||||
|
|
@ -22,7 +24,8 @@ export interface PesPatternData {
|
||||||
chart: string | null;
|
chart: string | null;
|
||||||
threadIndices: number[];
|
threadIndices: number[];
|
||||||
}>;
|
}>;
|
||||||
penData: Uint8Array;
|
penData: Uint8Array; // Raw PEN bytes sent to machine
|
||||||
|
penStitches: PenData; // Parsed PEN stitches (for rendering)
|
||||||
colorCount: number;
|
colorCount: number;
|
||||||
stitchCount: number;
|
stitchCount: number;
|
||||||
bounds: {
|
bounds: {
|
||||||
|
|
@ -175,9 +178,16 @@ class PatternConverterClient {
|
||||||
case 'CONVERT_COMPLETE': {
|
case 'CONVERT_COMPLETE': {
|
||||||
worker.removeEventListener('message', handleMessage);
|
worker.removeEventListener('message', handleMessage);
|
||||||
// Convert penData array back to Uint8Array
|
// 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 = {
|
const result: PesPatternData = {
|
||||||
...message.data,
|
...message.data,
|
||||||
penData: new Uint8Array(message.data.penData),
|
penData,
|
||||||
|
penStitches,
|
||||||
};
|
};
|
||||||
resolve(result);
|
resolve(result);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,20 @@ export function parsePenData(data: Uint8Array): PenData {
|
||||||
const yFlags = data[offset + 2] & 0x07;
|
const yFlags = data[offset + 2] & 0x07;
|
||||||
|
|
||||||
// Decode coordinates (shift right by 3 to get actual position)
|
// Decode coordinates (shift right by 3 to get actual position)
|
||||||
// Using signed 16-bit interpretation
|
// The coordinates are stored as signed 16-bit values, left-shifted by 3
|
||||||
let x = (xRaw >> 3);
|
// Step 1: Clear the flag bits (low 3 bits) from the raw values
|
||||||
let y = (yRaw >> 3);
|
const xRawClean = xRaw & 0xFFF8;
|
||||||
|
const yRawClean = yRaw & 0xFFF8;
|
||||||
|
|
||||||
// Convert to signed if needed
|
// Step 2: Convert from unsigned 16-bit to signed 16-bit
|
||||||
if (x > 0x7FF) x = x - 0x2000;
|
let xSigned = xRawClean;
|
||||||
if (y > 0x7FF) y = y - 0x2000;
|
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 = {
|
const stitch: PenStitch = {
|
||||||
x,
|
x,
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Convert PES file to PEN format
|
||||||
*/
|
*/
|
||||||
|
|
@ -210,6 +337,10 @@ def map_cmd(pystitch_cmd):
|
||||||
# Each stitch in pattern.stitches is [x, y, cmd]
|
# Each stitch in pattern.stitches is [x, y, cmd]
|
||||||
# We need to assign color indices based on COLOR_CHANGE commands
|
# We need to assign color indices based on COLOR_CHANGE commands
|
||||||
# and filter out COLOR_CHANGE and STOP commands (they're not actual stitches)
|
# 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 = []
|
stitches_with_colors = []
|
||||||
current_color = 0
|
current_color = 0
|
||||||
|
|
@ -217,7 +348,7 @@ current_color = 0
|
||||||
for i, stitch in enumerate(pattern.stitches):
|
for i, stitch in enumerate(pattern.stitches):
|
||||||
x, y, cmd = stitch
|
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:
|
if cmd == COLOR_CHANGE:
|
||||||
current_color += 1
|
current_color += 1
|
||||||
continue
|
continue
|
||||||
|
|
@ -230,8 +361,17 @@ for i, stitch in enumerate(pattern.stitches):
|
||||||
if cmd == END:
|
if cmd == END:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Add actual stitch with color index and mapped command
|
# PyStitch inserts duplicate stitches at the same coordinates during color changes
|
||||||
# Map PyStitch cmd values to our known JavaScript constant values
|
# 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)
|
mapped_cmd = map_cmd(cmd)
|
||||||
stitches_with_colors.append([x, y, mapped_cmd, current_color])
|
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)
|
// PEN format uses absolute coordinates, shifted left by 3 bits (as per official app line 780)
|
||||||
const penStitches: number[] = [];
|
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++) {
|
for (let i = 0; i < stitches.length; i++) {
|
||||||
const stitch = stitches[i];
|
const stitch = stitches[i];
|
||||||
const absX = Math.round(stitch[0]);
|
const absX = Math.round(stitch[0]);
|
||||||
|
|
@ -334,6 +481,43 @@ for i, stitch in enumerate(pattern.stitches):
|
||||||
maxY = Math.max(maxY, absY);
|
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
|
// 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
|
||||||
// As per official app line 780: buffer[index64] = (byte) ((int) numArray4[index64 / 4, 0] << 3 & (int) byte.MaxValue);
|
// 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;
|
const isLastStitch = i === stitches.length - 1 || (cmd & END) !== 0;
|
||||||
|
|
||||||
// Check for color change by comparing stitch color index
|
// 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 nextStitch = stitches[i + 1];
|
||||||
const nextStitchColor = nextStitch?.[3];
|
const nextStitchColor = nextStitch?.[3];
|
||||||
|
const isColorChange = !isLastStitch && nextStitchColor !== undefined && nextStitchColor !== stitchColor;
|
||||||
|
|
||||||
if (
|
// Mark the very last stitch of the pattern with DATA_END
|
||||||
!isLastStitch &&
|
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
|
|
||||||
xEncoded = (xEncoded & 0xfff8) | PEN_DATA_END;
|
xEncoded = (xEncoded & 0xfff8) | PEN_DATA_END;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,6 +555,101 @@ for i, stitch in enumerate(pattern.stitches):
|
||||||
(yEncoded >> 8) & 0xff
|
(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
|
// Check for end command
|
||||||
if ((cmd & END) !== 0) {
|
if ((cmd & END) !== 0) {
|
||||||
break;
|
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
|
// Post result back to main thread
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'CONVERT_COMPLETE',
|
type: 'CONVERT_COMPLETE',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue