/** * PEN Format Encoder * * This module contains the logic for encoding embroidery stitches into the Brother PP1 PEN format. * The PEN format uses absolute coordinates shifted left by 3 bits, with flags in the low 3 bits. */ import { MOVE, TRIM, END } from '../import/constants'; // PEN format flags for Brother machines const PEN_FEED_DATA = 0x01; // Bit 0: Jump stitch (move without stitching) const PEN_CUT_DATA = 0x02; // Bit 1: Trim/cut thread command const PEN_COLOR_END = 0x03; // Last stitch before color change const PEN_DATA_END = 0x05; // Last stitch of entire pattern // Constants from PesxToPen.cs const FEED_LENGTH = 50; // Long jump threshold requiring lock stitches and cut const TARGET_LENGTH = 8.0; // Target accumulated length for lock stitch direction const MAX_POINTS = 5; // Maximum points to accumulate for lock stitch direction export const LOCK_STITCH_JUMP_SIZE = 2.0; const LOCK_STITCH_SCALE = LOCK_STITCH_JUMP_SIZE / 8.0; // Scale the magnitude-8 vector down to 4 export interface StitchData { x: number; y: number; cmd: number; colorIndex: number; } export interface PenEncodingResult { penBytes: number[]; bounds: { minX: number; maxX: number; minY: number; maxY: number; }; } /** * 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 */ export 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 ]; } /** * 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 [x, y, cmd, colorIndex] * @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) */ export function calculateLockDirection( stitches: number[][], currentIndex: number, lookAhead: boolean ): { dirX: number; dirY: number } { 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 (magnitude ~8.0) * @param dirY Direction Y component (magnitude ~8.0) * @returns Array of PEN bytes for lock stitches (32 bytes = 8 stitches * 4 bytes) */ export 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 scaledDirX = dirX * LOCK_STITCH_SCALE; const scaledDirY = dirY * LOCK_STITCH_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; const xAdd = scaledDirX * sign; const yAdd = scaledDirY * sign; lockBytes.push(...encodeStitchPosition(x + xAdd, y + yAdd)); } return lockBytes; } /** * Encode stitches array to PEN format bytes * * @param stitches Array of stitches in format [x, y, cmd, colorIndex] * @returns PEN encoding result with bytes and bounds */ export function encodeStitchesToPen(stitches: number[][]): PenEncodingResult { // Track bounds let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; const penStitches: number[] = []; // Track position for calculating jump distances let prevX = 0; let prevY = 0; 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 (regular stitches, not MOVE/JUMP) // A stitch is trackable if it's not a MOVE command if ((cmd & MOVE) === 0) { minX = Math.min(minX, absX); maxX = Math.max(maxX, absX); minY = Math.min(minY, absY); maxY = Math.max(maxY, absY); } const isFirstStitch = i == 0; // Check for long jumps that need lock stitches and cuts if (cmd & MOVE) { const jumpDist = Math.sqrt((absX - prevX) ** 2 + (absY - prevY) ** 2); if (!isFirstStitch && 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 let xEncoded = (absX << 3) & 0xffff; let yEncoded = (absY << 3) & 0xffff; // Add command flags to Y-coordinate based on stitch type if (cmd & MOVE) { // MOVE/JUMP: Set bit 0 (FEED_DATA) - move without stitching yEncoded |= PEN_FEED_DATA; } if (cmd & TRIM) { // TRIM: Set bit 1 (CUT_DATA) - cut thread command yEncoded |= PEN_CUT_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 const nextStitch = stitches[i + 1]; const nextStitchColor = nextStitch?.[3]; const isColorChange = !isLastStitch && nextStitchColor !== undefined && nextStitchColor !== stitchColor; // Mark the very last stitch of the pattern with DATA_END if (isLastStitch) { xEncoded = (xEncoded & 0xfff8) | PEN_DATA_END; } // Add the encoded stitch penStitches.push( xEncoded & 0xff, (xEncoded >> 8) & 0xff, yEncoded & 0xff, (yEncoded >> 8) & 0xff ); // Update position for next iteration prevX = absX; prevY = absY; if (isFirstStitch) { // Add starting lock stitches at the very beginning of the pattern // Matches C# behavior: Nuihajime_TomeDataPlus is called when counter <= 2 // Calculate direction for starting locks (look forward into the pattern) const startDir = calculateLockDirection(stitches, i, true); penStitches.push(...generateLockStitches(absX, absY, startDir.dirX, startDir.dirY)); } // Handle color change: finishing lock, COLOR_END+CUT, jump, 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; // 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)); // Step 2: Add COLOR_END + CUT command at CURRENT position (same stitch!) // This is where the machine pauses and waits for the user to change thread color // IMPORTANT: COLOR_END and CUT must be on the SAME stitch, not separate stitches let colorEndCutXEncoded = (absX << 3) & 0xffff; let colorEndCutYEncoded = (absY << 3) & 0xffff; // Add COLOR_END flag to X coordinate and CUT flag to Y coordinate colorEndCutXEncoded = (colorEndCutXEncoded & 0xfff8) | PEN_COLOR_END; colorEndCutYEncoded |= PEN_CUT_DATA; penStitches.push( colorEndCutXEncoded & 0xff, (colorEndCutXEncoded >> 8) & 0xff, colorEndCutYEncoded & 0xff, (colorEndCutYEncoded >> 8) & 0xff ); // Machine pauses here for color change // After user changes color, the following stitches execute with the new color // 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 i++; // Skip the JUMP stitch since we're processing it here } // 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 ); } // Step 4: 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)); // Update position prevX = jumpToX; prevY = jumpToY; } // Check for end command if ((cmd & END) !== 0) { break; } } return { penBytes: penStitches, bounds: { minX: minX === Infinity ? 0 : minX, maxX: maxX === -Infinity ? 0 : maxX, minY: minY === Infinity ? 0 : minY, maxY: maxY === -Infinity ? 0 : maxY, }, }; }