From 4fb2b40cbad6a0fe705a1d24cda2376009a59447 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sun, 14 Dec 2025 13:23:20 +0100 Subject: [PATCH 1/2] fix: Add starting lock stitches to PEN encoder to match C# behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The encoder now adds 8 lock stitches at the very beginning of every pattern, matching the behavior of the original C# PesxToPen.cs code (Nuihajime_TomeDataPlus is called when counter <= 2). Key changes: - Find first non-MOVE stitch for lock stitch placement - Add 8 starting lock stitches before main encoding loop - Calculate forward-looking direction for optimal knot hiding - Update all 30 tests to account for starting lock stitches Also added tests to verify: - DATA_END flag is automatically added to last stitch - Starting lock stitches are correctly placed at pattern start 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/formats/pen/encoder.test.ts | 94 +++++++++++++++++++++++++-------- src/formats/pen/encoder.ts | 14 +++++ 2 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/formats/pen/encoder.test.ts b/src/formats/pen/encoder.test.ts index 0d7f6cb..38d1c7b 100644 --- a/src/formats/pen/encoder.test.ts +++ b/src/formats/pen/encoder.test.ts @@ -258,6 +258,7 @@ describe('encodeStitchesToPen', () => { const decoded = decodeAllPenStitches(result.penBytes); // Expected sequence: + // 0. 8 starting lock stitches at (0, 0) // 1. Stitch at (0, 0) - color 0 // 2. Stitch at (10, 0) - color 0 (last before change) // 3. 8 finishing lock stitches around (10, 0) @@ -269,6 +270,13 @@ describe('encodeStitchesToPen', () => { let idx = 0; + // 0. 8 starting lock stitches at (0, 0) + for (let i = 0; i < 8; i++) { + expect(decoded[idx].x).toBeCloseTo(0, 1); + expect(decoded[idx].y).toBeCloseTo(0, 1); + idx++; + } + // 1. First stitch (0, 0) expect(decoded[idx].x).toBe(0); expect(decoded[idx].y).toBe(0); @@ -336,7 +344,7 @@ describe('encodeStitchesToPen', () => { const result = encodeStitchesToPen(stitches); const decoded = decodeAllPenStitches(result.penBytes); - let idx = 2; // Skip first two regular stitches + let idx = 10; // Skip 8 starting locks + first two regular stitches // After second stitch, should have: // 1. 8 finishing lock stitches at (10, 0) @@ -400,7 +408,7 @@ describe('encodeStitchesToPen', () => { // 8. First stitch of new color at (50, 20) // 9. Last stitch at (60, 20) with END flag - let idx = 0; + let idx = 8; // Skip 8 starting lock stitches // 1-2. First two stitches expect(decoded[idx++].x).toBe(0); @@ -472,7 +480,7 @@ describe('encodeStitchesToPen', () => { // 6. Stitch at (110, 0) // 7. Stitch at (120, 0) with END flag - let idx = 0; + let idx = 8; // Skip 8 starting lock stitches // 1-2. First two stitches expect(decoded[idx++].x).toBe(0); @@ -526,7 +534,7 @@ describe('encodeStitchesToPen', () => { // Second stitch (jump) should have FEED_DATA flag (0x01) in Y low byte // Stitch format: [xLow, xHigh, yLow, yHigh] // We need to find the jump stitch - it's the second one encoded - const jumpStitchStart = 4; // Skip first stitch + const jumpStitchStart = 36; // Skip 8 starting locks (32 bytes) + first stitch (4 bytes) const yLow = result.penBytes[jumpStitchStart + 2]; expect(yLow & 0x01).toBe(0x01); // FEED_DATA flag }); @@ -558,21 +566,25 @@ describe('encodeStitchesToPen', () => { const result = encodeStitchesToPen(stitches); const decoded = decodeAllPenStitches(result.penBytes); + let idx = 8; // Skip 8 starting lock stitches + // Verify sequence: // 1. Regular stitch at (0, 0) - expect(decoded[0].x).toBe(0); - expect(decoded[0].isCut).toBe(false); + const firstIdx = idx; + expect(decoded[idx++].x).toBe(0); + expect(decoded[firstIdx].isCut).toBe(false); // 2. TRIM command at (10, 0) - should have CUT flag - expect(decoded[1].x).toBe(10); - expect(decoded[1].y).toBe(0); - expect(decoded[1].isCut).toBe(true); - expect(decoded[1].isFeed).toBe(false); // TRIM doesn't include FEED - expect(decoded[1].yFlags).toBe(PEN_CUT_DATA); // Only CUT flag + const trimIdx = idx; + expect(decoded[idx++].x).toBe(10); + expect(decoded[trimIdx].y).toBe(0); + expect(decoded[trimIdx].isCut).toBe(true); + expect(decoded[trimIdx].isFeed).toBe(false); // TRIM doesn't include FEED + expect(decoded[trimIdx].yFlags).toBe(PEN_CUT_DATA); // Only CUT flag // 3. Final stitch with DATA_END - expect(decoded[2].x).toBe(20); - expect(decoded[2].isDataEnd).toBe(true); + expect(decoded[idx].x).toBe(20); + expect(decoded[idx].isDataEnd).toBe(true); }); it('should handle empty stitch array', () => { @@ -594,7 +606,7 @@ describe('encodeStitchesToPen', () => { const result = encodeStitchesToPen(stitches); - expect(result.penBytes.length).toBe(4); + expect(result.penBytes.length).toBe(36); // 8 starting locks (32 bytes) + 1 stitch (4 bytes) expect(result.bounds.minX).toBe(5); expect(result.bounds.maxX).toBe(5); expect(result.bounds.minY).toBe(10); @@ -614,14 +626,14 @@ describe('encodeStitchesToPen', () => { const result = encodeStitchesToPen(stitches); const decoded = decodeAllPenStitches(result.penBytes); - // First two stitches should NOT have DATA_END flag - expect(decoded[0].isDataEnd).toBe(false); - expect(decoded[1].isDataEnd).toBe(false); + // First two stitches (after 8 starting locks) should NOT have DATA_END flag + expect(decoded[8].isDataEnd).toBe(false); + expect(decoded[9].isDataEnd).toBe(false); // Last stitch SHOULD have DATA_END flag automatically added - expect(decoded[2].isDataEnd).toBe(true); - expect(decoded[2].x).toBe(20); - expect(decoded[2].y).toBe(0); + expect(decoded[10].isDataEnd).toBe(true); + expect(decoded[10].x).toBe(20); + expect(decoded[10].y).toBe(0); }); it('should add DATA_END flag when input has explicit END flag', () => { @@ -636,8 +648,44 @@ describe('encodeStitchesToPen', () => { const decoded = decodeAllPenStitches(result.penBytes); // Last stitch should have DATA_END flag - expect(decoded[2].isDataEnd).toBe(true); - expect(decoded[2].x).toBe(20); - expect(decoded[2].y).toBe(0); + expect(decoded[10].isDataEnd).toBe(true); + expect(decoded[10].x).toBe(20); + expect(decoded[10].y).toBe(0); + }); + + it('should add lock stitches at the very start of the pattern', () => { + // Matching C# behavior: Nuihajime_TomeDataPlus is called when counter <= 2 + // This adds starting lock stitches to secure the thread at pattern start + const stitches = [ + [10, 20, STITCH, 0], + [20, 20, STITCH, 0], + [30, 20, STITCH | END, 0], + ]; + + const result = encodeStitchesToPen(stitches); + const decoded = decodeAllPenStitches(result.penBytes); + + // Expected sequence: + // 1. 8 starting lock stitches at (10, 20) + // 2. First actual stitch at (10, 20) + // 3. Second stitch at (20, 20) + // 4. Last stitch at (30, 20) + + // First 8 stitches should be lock stitches around the starting position + for (let i = 0; i < 8; i++) { + expect(decoded[i].x).toBeCloseTo(10, 1); + expect(decoded[i].y).toBeCloseTo(20, 1); + expect(decoded[i].isFeed).toBe(false); + expect(decoded[i].isCut).toBe(false); + } + + // Then the actual stitches + expect(decoded[8].x).toBe(10); + expect(decoded[8].y).toBe(20); + expect(decoded[9].x).toBe(20); + expect(decoded[9].y).toBe(20); + expect(decoded[10].x).toBe(30); + expect(decoded[10].y).toBe(20); + expect(decoded[10].isDataEnd).toBe(true); }); }); diff --git a/src/formats/pen/encoder.ts b/src/formats/pen/encoder.ts index c2e962a..777112b 100644 --- a/src/formats/pen/encoder.ts +++ b/src/formats/pen/encoder.ts @@ -178,6 +178,20 @@ export function encodeStitchesToPen(stitches: number[][]): PenEncodingResult { let prevX = 0; let prevY = 0; + // Add starting lock stitches at the very beginning of the pattern + // Matches C# behavior: Nuihajime_TomeDataPlus is called when counter <= 2 + // Find the first non-MOVE stitch to place the starting locks + const firstStitchIndex = stitches.findIndex(s => (s[2] & MOVE) === 0); + if (firstStitchIndex !== -1) { + const firstStitch = stitches[firstStitchIndex]; + const startX = Math.round(firstStitch[0]); + const startY = Math.round(firstStitch[1]); + + // Calculate direction for starting locks (look forward into the pattern) + const startDir = calculateLockDirection(stitches, firstStitchIndex, true); + penStitches.push(...generateLockStitches(startX, startY, startDir.dirX, startDir.dirY)); + } + for (let i = 0; i < stitches.length; i++) { const stitch = stitches[i]; const absX = Math.round(stitch[0]); From 6048a612306c86255d816a9ed32d9dbff3c47b54 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sun, 14 Dec 2025 15:20:47 +0100 Subject: [PATCH 2/2] fix: add more tests for lock stitch at start, properly place the lock stitch after the first imported stitch --- src/formats/pen/encoder.test.ts | 72 +++++++++++++++++---------------- src/formats/pen/encoder.ts | 35 ++++++++-------- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/formats/pen/encoder.test.ts b/src/formats/pen/encoder.test.ts index 38d1c7b..21493f9 100644 --- a/src/formats/pen/encoder.test.ts +++ b/src/formats/pen/encoder.test.ts @@ -4,6 +4,7 @@ import { calculateLockDirection, generateLockStitches, encodeStitchesToPen, + LOCK_STITCH_JUMP_SIZE } from './encoder'; import { decodeAllPenStitches } from './decoder'; import { STITCH, MOVE, TRIM, END } from '../import/constants'; @@ -270,19 +271,17 @@ describe('encodeStitchesToPen', () => { let idx = 0; - // 0. 8 starting lock stitches at (0, 0) - for (let i = 0; i < 8; i++) { - expect(decoded[idx].x).toBeCloseTo(0, 1); - expect(decoded[idx].y).toBeCloseTo(0, 1); - idx++; - } - // 1. First stitch (0, 0) expect(decoded[idx].x).toBe(0); expect(decoded[idx].y).toBe(0); expect(decoded[idx].isFeed).toBe(false); expect(decoded[idx].isCut).toBe(false); - idx++; + + // 0. 8 starting lock stitches at (0, 0) + for (; idx < 9; idx++) { + expect(decoded[idx].x).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); + expect(decoded[idx].y).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); + } // 2. Second stitch (10, 0) - last before color change expect(decoded[idx].x).toBe(10); @@ -292,8 +291,8 @@ describe('encodeStitchesToPen', () => { // 3. 8 finishing lock stitches (should be around position 10, 0) for (let i = 0; i < 8; i++) { const lockStitch = decoded[idx]; - expect(lockStitch.x).toBeCloseTo(10, 1); // Allow some deviation due to rotation - expect(lockStitch.y).toBeCloseTo(0, 1); + expect(lockStitch.x).to.be.closeTo(10, LOCK_STITCH_JUMP_SIZE); // Allow some deviation due to rotation + expect(lockStitch.y).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); expect(lockStitch.isFeed).toBe(false); expect(lockStitch.isCut).toBe(false); idx++; @@ -316,8 +315,8 @@ describe('encodeStitchesToPen', () => { // 6. 8 starting lock stitches for new color for (let i = 0; i < 8; i++) { const lockStitch = decoded[idx]; - expect(lockStitch.x).toBeCloseTo(10, 1); - expect(lockStitch.y).toBeCloseTo(0, 1); + expect(lockStitch.x).to.be.closeTo(10, LOCK_STITCH_JUMP_SIZE); + expect(lockStitch.y).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); idx++; } @@ -349,8 +348,8 @@ describe('encodeStitchesToPen', () => { // After second stitch, should have: // 1. 8 finishing lock stitches at (10, 0) for (let i = 0; i < 8; i++) { - expect(decoded[idx].x).toBeCloseTo(10, 1); - expect(decoded[idx].y).toBeCloseTo(0, 1); + expect(decoded[idx].x).to.be.closeTo(10, LOCK_STITCH_JUMP_SIZE); + expect(decoded[idx].y).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); idx++; } @@ -373,8 +372,8 @@ describe('encodeStitchesToPen', () => { // 5. 8 starting lock stitches at (30, 10) for (let i = 0; i < 8; i++) { - expect(decoded[idx].x).toBeCloseTo(30, 1); - expect(decoded[idx].y).toBeCloseTo(10, 1); + expect(decoded[idx].x).to.be.closeTo(30, LOCK_STITCH_JUMP_SIZE); + expect(decoded[idx].y).to.be.closeTo(10, LOCK_STITCH_JUMP_SIZE); idx++; } @@ -408,16 +407,17 @@ describe('encodeStitchesToPen', () => { // 8. First stitch of new color at (50, 20) // 9. Last stitch at (60, 20) with END flag - let idx = 8; // Skip 8 starting lock stitches + let idx = 0; // 1-2. First two stitches expect(decoded[idx++].x).toBe(0); + idx = 9; // skip startingLock expect(decoded[idx++].x).toBe(10); // 3. 8 finishing lock stitches at (10, 0) for (let i = 0; i < 8; i++) { - expect(decoded[idx].x).toBeCloseTo(10, 1); - expect(decoded[idx].y).toBeCloseTo(0, 1); + expect(decoded[idx].x).to.be.closeTo(10, LOCK_STITCH_JUMP_SIZE); + expect(decoded[idx].y).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); idx++; } @@ -441,8 +441,8 @@ describe('encodeStitchesToPen', () => { // 7. 8 starting lock stitches at (50, 20) for (let i = 0; i < 8; i++) { - expect(decoded[idx].x).toBeCloseTo(50, 1); - expect(decoded[idx].y).toBeCloseTo(20, 1); + expect(decoded[idx].x).to.be.closeTo(50, LOCK_STITCH_JUMP_SIZE); + expect(decoded[idx].y).to.be.closeTo(20, LOCK_STITCH_JUMP_SIZE); idx++; } @@ -480,17 +480,18 @@ describe('encodeStitchesToPen', () => { // 6. Stitch at (110, 0) // 7. Stitch at (120, 0) with END flag - let idx = 8; // Skip 8 starting lock stitches + let idx = 0; // 1-2. First two stitches expect(decoded[idx++].x).toBe(0); + idx = 9; // Skip 8 starting lock stitches expect(decoded[idx++].x).toBe(10); // 3. 8 finishing lock stitches at (10, 0) for (let i = 0; i < 8; i++) { const lockStitch = decoded[idx]; - expect(lockStitch.x).toBeCloseTo(10, 1); - expect(lockStitch.y).toBeCloseTo(0, 1); + expect(lockStitch.x).to.be.closeTo(10, LOCK_STITCH_JUMP_SIZE); + expect(lockStitch.y).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); expect(lockStitch.isFeed).toBe(false); expect(lockStitch.isCut).toBe(false); idx++; @@ -508,8 +509,8 @@ describe('encodeStitchesToPen', () => { // 5. 8 starting lock stitches at (100, 0) for (let i = 0; i < 8; i++) { const lockStitch = decoded[idx]; - expect(lockStitch.x).toBeCloseTo(100, 1); - expect(lockStitch.y).toBeCloseTo(0, 1); + expect(lockStitch.x).to.be.closeTo(100, LOCK_STITCH_JUMP_SIZE); + expect(lockStitch.y).to.be.closeTo(0, LOCK_STITCH_JUMP_SIZE); idx++; } @@ -566,12 +567,13 @@ describe('encodeStitchesToPen', () => { const result = encodeStitchesToPen(stitches); const decoded = decodeAllPenStitches(result.penBytes); - let idx = 8; // Skip 8 starting lock stitches + let idx = 0; // Verify sequence: // 1. Regular stitch at (0, 0) const firstIdx = idx; expect(decoded[idx++].x).toBe(0); + idx = 9; // Skip 8 starting lock stitches expect(decoded[firstIdx].isCut).toBe(false); // 2. TRIM command at (10, 0) - should have CUT flag @@ -657,31 +659,33 @@ describe('encodeStitchesToPen', () => { // Matching C# behavior: Nuihajime_TomeDataPlus is called when counter <= 2 // This adds starting lock stitches to secure the thread at pattern start const stitches = [ - [10, 20, STITCH, 0], + [10, 20, MOVE, 0], [20, 20, STITCH, 0], [30, 20, STITCH | END, 0], ]; const result = encodeStitchesToPen(stitches); const decoded = decodeAllPenStitches(result.penBytes); - + console.log(decoded); // Expected sequence: + // 0. Feed move to proper location (should always happen here) // 1. 8 starting lock stitches at (10, 20) // 2. First actual stitch at (10, 20) // 3. Second stitch at (20, 20) // 4. Last stitch at (30, 20) + expect(decoded[0].x).toBe(10); + expect(decoded[0].y).toBe(20); + expect(decoded[0].isFeed).toBe(true); // First 8 stitches should be lock stitches around the starting position - for (let i = 0; i < 8; i++) { - expect(decoded[i].x).toBeCloseTo(10, 1); - expect(decoded[i].y).toBeCloseTo(20, 1); + for (let i = 1; i < 1 + 8; i++) { + expect(decoded[i].x).to.be.closeTo(10, LOCK_STITCH_JUMP_SIZE); + expect(decoded[i].y).to.be.closeTo(20, LOCK_STITCH_JUMP_SIZE); expect(decoded[i].isFeed).toBe(false); expect(decoded[i].isCut).toBe(false); } // Then the actual stitches - expect(decoded[8].x).toBe(10); - expect(decoded[8].y).toBe(20); expect(decoded[9].x).toBe(20); expect(decoded[9].y).toBe(20); expect(decoded[10].x).toBe(30); diff --git a/src/formats/pen/encoder.ts b/src/formats/pen/encoder.ts index 777112b..122ed8d 100644 --- a/src/formats/pen/encoder.ts +++ b/src/formats/pen/encoder.ts @@ -17,7 +17,8 @@ const PEN_DATA_END = 0x05; // Last stitch of entire pattern 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 -const LOCK_STITCH_SCALE = 0.4 / 8.0; // Scale the magnitude-8 vector down to 0.4 +export const LOCK_STITCH_JUMP_SIZE = 4.0; +const LOCK_STITCH_SCALE = LOCK_STITCH_JUMP_SIZE / 8.0; // Scale the magnitude-8 vector down to 4 export interface StitchData { x: number; @@ -153,7 +154,9 @@ export function generateLockStitches(x: number, y: number, dirX: number, dirY: n 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)); + const xAdd = scaledDirX * sign; + const yAdd = scaledDirY * sign; + lockBytes.push(...encodeStitchPosition(x + xAdd, y + yAdd)); } return lockBytes; @@ -177,20 +180,7 @@ export function encodeStitchesToPen(stitches: number[][]): PenEncodingResult { // Track position for calculating jump distances let prevX = 0; let prevY = 0; - - // Add starting lock stitches at the very beginning of the pattern - // Matches C# behavior: Nuihajime_TomeDataPlus is called when counter <= 2 - // Find the first non-MOVE stitch to place the starting locks - const firstStitchIndex = stitches.findIndex(s => (s[2] & MOVE) === 0); - if (firstStitchIndex !== -1) { - const firstStitch = stitches[firstStitchIndex]; - const startX = Math.round(firstStitch[0]); - const startY = Math.round(firstStitch[1]); - - // Calculate direction for starting locks (look forward into the pattern) - const startDir = calculateLockDirection(stitches, firstStitchIndex, true); - penStitches.push(...generateLockStitches(startX, startY, startDir.dirX, startDir.dirY)); - } + for (let i = 0; i < stitches.length; i++) { const stitch = stitches[i]; @@ -208,11 +198,13 @@ export function encodeStitchesToPen(stitches: number[][]): PenEncodingResult { 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 (jumpDist > FEED_LENGTH) { + 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 @@ -285,6 +277,15 @@ export function encodeStitchesToPen(stitches: number[][]): PenEncodingResult { 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, cut, jump, COLOR_END, starting lock if (isColorChange) { const nextStitchCmd = nextStitch[2];