mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Merge pull request #10 from jhbruhn/feature/add-pen-conversion-tests
Feature: add PEN conversion tests, refactoring around PEN conversion
This commit is contained in:
commit
e7fb02a163
28 changed files with 2119 additions and 927 deletions
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: Build and Lint
|
name: Build, Test, and Lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
@ -8,8 +8,8 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-lint:
|
build-test-lint:
|
||||||
name: Build and Lint
|
name: Build, Test, and Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -28,5 +28,8 @@ jobs:
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test:run
|
||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist', '.vite']),
|
globalIgnores(['dist', 'dist-electron', '.vite']),
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
extends: [
|
extends: [
|
||||||
|
|
|
||||||
412
package-lock.json
generated
412
package-lock.json
generated
|
|
@ -39,6 +39,7 @@
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"@vitest/ui": "^4.0.15",
|
||||||
"electron": "^39.2.6",
|
"electron": "^39.2.6",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
|
@ -48,7 +49,8 @@
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.46.4",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.2.4",
|
||||||
"vite-plugin-static-copy": "^3.1.4"
|
"vite-plugin-static-copy": "^3.1.4",
|
||||||
|
"vitest": "^4.0.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
|
|
@ -4179,6 +4181,13 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@polka/url": {
|
||||||
|
"version": "1.0.0-next.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
|
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@reforged/maker-appimage": {
|
"node_modules/@reforged/maker-appimage": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@reforged/maker-appimage/-/maker-appimage-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@reforged/maker-appimage/-/maker-appimage-5.1.1.tgz",
|
||||||
|
|
@ -4532,6 +4541,13 @@
|
||||||
"integrity": "sha512-aywhxHNb6l7COooF3m439eT/6QN8E/RSl5IVboSKthMHcp0GlZYMSoS7546rqDLmFRxTD8f1tu/NIS9vtDwYAg==",
|
"integrity": "sha512-aywhxHNb6l7COooF3m439eT/6QN8E/RSl5IVboSKthMHcp0GlZYMSoS7546rqDLmFRxTD8f1tu/NIS9vtDwYAg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@szmarczak/http-timer": {
|
"node_modules/@szmarczak/http-timer": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||||
|
|
@ -4888,6 +4904,24 @@
|
||||||
"@types/responselike": "^1.0.0"
|
"@types/responselike": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/electron-squirrel-startup": {
|
"node_modules/@types/electron-squirrel-startup": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/electron-squirrel-startup/-/electron-squirrel-startup-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/electron-squirrel-startup/-/electron-squirrel-startup-1.0.2.tgz",
|
||||||
|
|
@ -5331,6 +5365,139 @@
|
||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "4.0.15",
|
||||||
|
"@vitest/utils": "4.0.15",
|
||||||
|
"chai": "^6.2.1",
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "4.0.15",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.21"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0-0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.0.15",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.0.15",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/ui": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-sxSyJMaKp45zI0u+lHrPuZM1ZJQ8FaVD35k+UxVrha1yyvQ+TZuUYllUixwvQXlB7ixoDc7skf3lQPopZIvaQw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.0.15",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"flatted": "^3.3.3",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"sirv": "^3.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vitest": "4.0.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.0.15",
|
||||||
|
"tinyrainbow": "^3.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vscode/sudo-prompt": {
|
"node_modules/@vscode/sudo-prompt": {
|
||||||
"version": "9.3.1",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz",
|
||||||
|
|
@ -5844,6 +6011,16 @@
|
||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||||
|
|
@ -6334,6 +6511,16 @@
|
||||||
"follow-redirects": "^1.15.6"
|
"follow-redirects": "^1.15.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
|
|
@ -8394,6 +8581,16 @@
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
|
|
@ -8542,6 +8739,16 @@
|
||||||
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==",
|
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exponential-backoff": {
|
"node_modules/exponential-backoff": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
||||||
|
|
@ -8705,6 +8912,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
|
|
@ -11423,6 +11637,16 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mrmime": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
@ -11702,6 +11926,17 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/omggif": {
|
"node_modules/omggif": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
||||||
|
|
@ -12042,6 +12277,13 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pe-library": {
|
"node_modules/pe-library": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-1.0.1.tgz",
|
||||||
|
|
@ -13375,6 +13617,13 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
|
|
@ -13382,6 +13631,21 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sirv": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@polka/url": "^1.0.0-next.24",
|
||||||
|
"mrmime": "^2.0.0",
|
||||||
|
"totalist": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
|
|
@ -13576,6 +13840,20 @@
|
||||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stream-buffers": {
|
"node_modules/stream-buffers": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
|
||||||
|
|
@ -14208,6 +14486,13 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinycolor2": {
|
"node_modules/tinycolor2": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||||
|
|
@ -14215,6 +14500,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
|
@ -14231,6 +14526,16 @@
|
||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.0.33",
|
"version": "0.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||||
|
|
@ -14319,6 +14624,16 @@
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/totalist": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tough-cookie": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||||
|
|
@ -14732,6 +15047,84 @@
|
||||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "4.0.15",
|
||||||
|
"@vitest/mocker": "4.0.15",
|
||||||
|
"@vitest/pretty-format": "4.0.15",
|
||||||
|
"@vitest/runner": "4.0.15",
|
||||||
|
"@vitest/snapshot": "4.0.15",
|
||||||
|
"@vitest/spy": "4.0.15",
|
||||||
|
"@vitest/utils": "4.0.15",
|
||||||
|
"es-module-lexer": "^1.7.0",
|
||||||
|
"expect-type": "^1.2.2",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^3.10.0",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^1.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.0.3",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "4.0.15",
|
||||||
|
"@vitest/browser-preview": "4.0.15",
|
||||||
|
"@vitest/browser-webdriverio": "4.0.15",
|
||||||
|
"@vitest/ui": "4.0.15",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.4",
|
"version": "2.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||||
|
|
@ -14886,6 +15279,23 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:run": "vitest run",
|
||||||
"start": "electron-forge start",
|
"start": "electron-forge start",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"make": "electron-forge make",
|
"make": "electron-forge make",
|
||||||
|
|
@ -49,6 +52,7 @@
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"@vitest/ui": "^4.0.15",
|
||||||
"electron": "^39.2.6",
|
"electron": "^39.2.6",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
|
@ -58,6 +62,7 @@
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.46.4",
|
||||||
"vite": "^7.2.4",
|
"vite": "^7.2.4",
|
||||||
"vite-plugin-static-copy": "^3.1.4"
|
"vite-plugin-static-copy": "^3.1.4",
|
||||||
|
"vitest": "^4.0.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useShallow } from 'zustand/react/shallow';
|
||||||
import { useMachineStore } from '../stores/useMachineStore';
|
import { useMachineStore } from '../stores/useMachineStore';
|
||||||
import { usePatternStore } from '../stores/usePatternStore';
|
import { usePatternStore } from '../stores/usePatternStore';
|
||||||
import { useUIStore } from '../stores/useUIStore';
|
import { useUIStore } from '../stores/useUIStore';
|
||||||
import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter';
|
import { convertPesToPen, type PesPatternData } from '../formats/import/pesImporter';
|
||||||
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
||||||
import { PatternInfoSkeleton } from './SkeletonLoader';
|
import { PatternInfoSkeleton } from './SkeletonLoader';
|
||||||
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
|
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { Group, Line, Rect, Text, Circle } from 'react-konva';
|
import { Group, Line, Rect, Text, Circle } from 'react-konva';
|
||||||
import type { PesPatternData } from '../utils/pystitchConverter';
|
import type { PesPatternData } from '../formats/import/pesImporter';
|
||||||
import { getThreadColor } from '../utils/pystitchConverter';
|
import { getThreadColor } from '../formats/import/pesImporter';
|
||||||
import type { MachineInfo } from '../types/machine';
|
import type { MachineInfo } from '../types/machine';
|
||||||
import { MOVE } from '../utils/embroideryConstants';
|
import { MOVE } from '../formats/import/constants';
|
||||||
|
|
||||||
interface GridProps {
|
interface GridProps {
|
||||||
gridSize: number;
|
gridSize: number;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { usePatternStore } from '../stores/usePatternStore';
|
||||||
import { Stage, Layer, Group } from 'react-konva';
|
import { Stage, Layer, Group } from 'react-konva';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { PlusIcon, MinusIcon, ArrowPathIcon, LockClosedIcon, PhotoIcon, ArrowsPointingInIcon } from '@heroicons/react/24/solid';
|
import { PlusIcon, MinusIcon, ArrowPathIcon, LockClosedIcon, PhotoIcon, ArrowsPointingInIcon } from '@heroicons/react/24/solid';
|
||||||
import type { PesPatternData } from '../utils/pystitchConverter';
|
import type { PesPatternData } from '../formats/import/pesImporter';
|
||||||
import { calculateInitialScale } from '../utils/konvaRenderers';
|
import { calculateInitialScale } from '../utils/konvaRenderers';
|
||||||
import { Grid, Origin, Hoop, Stitches, PatternBounds, CurrentPosition } from './KonvaComponents';
|
import { Grid, Origin, Hoop, Stitches, PatternBounds, CurrentPosition } from './KonvaComponents';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { WorkerMessage, WorkerResponse } from '../workers/patternConverter.worker';
|
import type { WorkerMessage, WorkerResponse } from './worker';
|
||||||
import PatternConverterWorker from '../workers/patternConverter.worker?worker';
|
import PatternConverterWorker from './worker?worker';
|
||||||
import { parsePenData } from './penParser';
|
import { decodePenData } from '../pen/decoder';
|
||||||
import type { PenData } from '../types/machine';
|
import type { DecodedPenData } from '../pen/types';
|
||||||
|
|
||||||
export type PyodideState = 'not_loaded' | 'loading' | 'ready' | 'error';
|
export type PyodideState = 'not_loaded' | 'loading' | 'ready' | 'error';
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ export interface PesPatternData {
|
||||||
threadIndices: number[];
|
threadIndices: number[];
|
||||||
}>;
|
}>;
|
||||||
penData: Uint8Array; // Raw PEN bytes sent to machine
|
penData: Uint8Array; // Raw PEN bytes sent to machine
|
||||||
penStitches: PenData; // Parsed PEN stitches (for rendering)
|
penStitches: DecodedPenData; // Decoded PEN stitches (for rendering)
|
||||||
colorCount: number;
|
colorCount: number;
|
||||||
stitchCount: number;
|
stitchCount: number;
|
||||||
bounds: {
|
bounds: {
|
||||||
|
|
@ -180,9 +180,9 @@ class PatternConverterClient {
|
||||||
// Convert penData array back to Uint8Array
|
// Convert penData array back to Uint8Array
|
||||||
const penData = new Uint8Array(message.data.penData);
|
const penData = new Uint8Array(message.data.penData);
|
||||||
|
|
||||||
// Parse the PEN data to get stitches for rendering
|
// Decode the PEN data to get stitches for rendering
|
||||||
const penStitches = parsePenData(penData);
|
const penStitches = decodePenData(penData);
|
||||||
console.log('[PatternConverter] Parsed PEN data:', penStitches.totalStitches, 'stitches,', penStitches.colorCount, 'colors');
|
console.log('[PatternConverter] Decoded PEN data:', penStitches.stitches.length, 'stitches,', penStitches.colorBlocks.length, 'colors');
|
||||||
|
|
||||||
const result: PesPatternData = {
|
const result: PesPatternData = {
|
||||||
...message.data,
|
...message.data,
|
||||||
|
|
@ -15,9 +15,3 @@ export const TRIM = 0x20; // Trim thread command
|
||||||
export const COLOR_CHANGE = 0x40; // Color change command
|
export const COLOR_CHANGE = 0x40; // Color change command
|
||||||
export const STOP = 0x80; // Stop command
|
export const STOP = 0x80; // Stop command
|
||||||
export const END = 0x100; // End of pattern
|
export const END = 0x100; // End of pattern
|
||||||
|
|
||||||
// PEN format flags for Brother machines
|
|
||||||
export const PEN_FEED_DATA = 0x01; // Bit 0: Jump stitch (move without stitching)
|
|
||||||
export const PEN_CUT_DATA = 0x02; // Bit 1: Trim/cut thread command
|
|
||||||
export const PEN_COLOR_END = 0x03; // Last stitch before color change
|
|
||||||
export const PEN_DATA_END = 0x05; // Last stitch of entire pattern
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { patternConverterClient, type PesPatternData } from "./patternConverterClient";
|
import { patternConverterClient, type PesPatternData } from "./client";
|
||||||
|
|
||||||
// Re-export the type for backwards compatibility
|
// Re-export the type for backwards compatibility
|
||||||
export type { PesPatternData };
|
export type { PesPatternData };
|
||||||
423
src/formats/import/worker.ts
Normal file
423
src/formats/import/worker.ts
Normal file
|
|
@ -0,0 +1,423 @@
|
||||||
|
import { loadPyodide, type PyodideInterface } from 'pyodide';
|
||||||
|
import {
|
||||||
|
STITCH,
|
||||||
|
MOVE,
|
||||||
|
TRIM,
|
||||||
|
END,
|
||||||
|
} from './constants';
|
||||||
|
import { encodeStitchesToPen } from '../pen/encoder';
|
||||||
|
|
||||||
|
// Message types from main thread
|
||||||
|
export type WorkerMessage =
|
||||||
|
| { type: 'INITIALIZE'; pyodideIndexURL?: string; pystitchWheelURL?: string }
|
||||||
|
| { type: 'CONVERT_PES'; fileData: ArrayBuffer; fileName: string };
|
||||||
|
|
||||||
|
// Response types to main thread
|
||||||
|
export type WorkerResponse =
|
||||||
|
| { type: 'INIT_PROGRESS'; progress: number; step: string }
|
||||||
|
| { type: 'INIT_COMPLETE' }
|
||||||
|
| { type: 'INIT_ERROR'; error: string }
|
||||||
|
| {
|
||||||
|
type: 'CONVERT_COMPLETE';
|
||||||
|
data: {
|
||||||
|
stitches: number[][];
|
||||||
|
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: number[]; // Serialized as array
|
||||||
|
colorCount: number;
|
||||||
|
stitchCount: number;
|
||||||
|
bounds: {
|
||||||
|
minX: number;
|
||||||
|
maxX: number;
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| { type: 'CONVERT_ERROR'; error: string };
|
||||||
|
|
||||||
|
console.log('[PatternConverterWorker] Worker script loaded');
|
||||||
|
|
||||||
|
let pyodide: PyodideInterface | null = null;
|
||||||
|
let isInitializing = false;
|
||||||
|
|
||||||
|
// JavaScript constants module to expose to Python
|
||||||
|
const jsEmbConstants = {
|
||||||
|
STITCH,
|
||||||
|
MOVE,
|
||||||
|
TRIM,
|
||||||
|
END,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Pyodide with progress tracking
|
||||||
|
*/
|
||||||
|
async function initializePyodide(pyodideIndexURL?: string, pystitchWheelURL?: string) {
|
||||||
|
if (pyodide) {
|
||||||
|
return; // Already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitializing) {
|
||||||
|
throw new Error('Initialization already in progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitializing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'INIT_PROGRESS',
|
||||||
|
progress: 0,
|
||||||
|
step: 'Starting initialization...',
|
||||||
|
} as WorkerResponse);
|
||||||
|
|
||||||
|
console.log('[PyodideWorker] Loading Pyodide runtime...');
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: 'INIT_PROGRESS',
|
||||||
|
progress: 10,
|
||||||
|
step: 'Loading Python runtime...',
|
||||||
|
} as WorkerResponse);
|
||||||
|
|
||||||
|
// Load Pyodide runtime
|
||||||
|
// Use provided URL or default to /assets/
|
||||||
|
const indexURL = pyodideIndexURL || '/assets/';
|
||||||
|
console.log('[PyodideWorker] Pyodide index URL:', indexURL);
|
||||||
|
|
||||||
|
pyodide = await loadPyodide({
|
||||||
|
indexURL: indexURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[PyodideWorker] Pyodide runtime loaded');
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: 'INIT_PROGRESS',
|
||||||
|
progress: 70,
|
||||||
|
step: 'Python runtime loaded',
|
||||||
|
} as WorkerResponse);
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: 'INIT_PROGRESS',
|
||||||
|
progress: 75,
|
||||||
|
step: 'Loading pystitch library...',
|
||||||
|
} as WorkerResponse);
|
||||||
|
|
||||||
|
// Load pystitch wheel
|
||||||
|
// Use provided URL or default
|
||||||
|
const wheelURL = pystitchWheelURL || '/pystitch-1.0.0-py3-none-any.whl';
|
||||||
|
console.log('[PyodideWorker] Pystitch wheel URL:', wheelURL);
|
||||||
|
|
||||||
|
await pyodide.loadPackage(wheelURL);
|
||||||
|
|
||||||
|
console.log('[PyodideWorker] pystitch library loaded');
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: 'INIT_PROGRESS',
|
||||||
|
progress: 100,
|
||||||
|
step: 'Ready!',
|
||||||
|
} as WorkerResponse);
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: 'INIT_COMPLETE',
|
||||||
|
} as WorkerResponse);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
console.error('[PyodideWorker] Initialization error:', err);
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: 'INIT_ERROR',
|
||||||
|
error: errorMsg,
|
||||||
|
} as WorkerResponse);
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
isInitializing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert PES file to PEN format
|
||||||
|
*/
|
||||||
|
async function convertPesToPen(fileData: ArrayBuffer) {
|
||||||
|
if (!pyodide) {
|
||||||
|
throw new Error('Pyodide not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Register our JavaScript constants module for Python to import
|
||||||
|
pyodide.registerJsModule('js_emb_constants', jsEmbConstants);
|
||||||
|
|
||||||
|
// Convert to Uint8Array
|
||||||
|
const uint8Array = new Uint8Array(fileData);
|
||||||
|
|
||||||
|
// Write file to Pyodide virtual filesystem
|
||||||
|
const tempFileName = '/tmp/pattern.pes';
|
||||||
|
pyodide.FS.writeFile(tempFileName, uint8Array);
|
||||||
|
|
||||||
|
// Read the pattern using PyStitch (same logic as original converter)
|
||||||
|
const result = await pyodide.runPythonAsync(`
|
||||||
|
import pystitch
|
||||||
|
from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE
|
||||||
|
from js_emb_constants import STITCH as JS_STITCH, MOVE as JS_MOVE, TRIM as JS_TRIM, END as JS_END
|
||||||
|
|
||||||
|
# Read the PES file
|
||||||
|
pattern = pystitch.read('${tempFileName}')
|
||||||
|
|
||||||
|
def map_cmd(pystitch_cmd):
|
||||||
|
"""Map PyStitch command to our JavaScript constant values
|
||||||
|
|
||||||
|
This ensures we have known, consistent values regardless of PyStitch's internal values.
|
||||||
|
Our JS constants use pyembroidery-style bitmask values:
|
||||||
|
STITCH = 0x00, MOVE/JUMP = 0x10, TRIM = 0x20, END = 0x100
|
||||||
|
"""
|
||||||
|
if pystitch_cmd == STITCH:
|
||||||
|
return JS_STITCH
|
||||||
|
elif pystitch_cmd == JUMP:
|
||||||
|
return JS_MOVE # PyStitch JUMP maps to our MOVE constant
|
||||||
|
elif pystitch_cmd == TRIM:
|
||||||
|
return JS_TRIM
|
||||||
|
elif pystitch_cmd == END:
|
||||||
|
return JS_END
|
||||||
|
else:
|
||||||
|
# For any other commands, preserve as bitmask
|
||||||
|
result = JS_STITCH
|
||||||
|
if pystitch_cmd & JUMP:
|
||||||
|
result |= JS_MOVE
|
||||||
|
if pystitch_cmd & TRIM:
|
||||||
|
result |= JS_TRIM
|
||||||
|
if pystitch_cmd & END:
|
||||||
|
result |= JS_END
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
#
|
||||||
|
# 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 = []
|
||||||
|
current_color = 0
|
||||||
|
|
||||||
|
for i, stitch in enumerate(pattern.stitches):
|
||||||
|
x, y, cmd = stitch
|
||||||
|
|
||||||
|
# Check for color change command
|
||||||
|
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
|
||||||
|
|
||||||
|
# PyStitch inserts duplicate stitches at the same coordinates during color changes
|
||||||
|
# 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)
|
||||||
|
stitches_with_colors.append([x, y, mapped_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',
|
||||||
|
'catalog_number': thread.catalog_number if hasattr(thread, 'catalog_number') else -1,
|
||||||
|
'brand': thread.brand if hasattr(thread, 'brand') else "",
|
||||||
|
'description': thread.description if hasattr(thread, 'description') else "",
|
||||||
|
'chart': thread.chart if hasattr(thread, 'chart') else ""
|
||||||
|
}
|
||||||
|
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(tempFileName);
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stitches and validate
|
||||||
|
const stitches: number[][] = Array.from(
|
||||||
|
data.stitches as ArrayLike<ArrayLike<number>>
|
||||||
|
).map((stitch) => Array.from(stitch));
|
||||||
|
|
||||||
|
if (!stitches || stitches.length === 0) {
|
||||||
|
throw new Error('Invalid PES file or no stitches found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract thread data - preserve null values for unavailable metadata
|
||||||
|
const threads = (
|
||||||
|
data.threads as Array<{
|
||||||
|
color?: number;
|
||||||
|
hex?: string;
|
||||||
|
catalog_number?: number | string;
|
||||||
|
brand?: string;
|
||||||
|
description?: string;
|
||||||
|
chart?: string;
|
||||||
|
}>
|
||||||
|
).map((thread) => {
|
||||||
|
// Normalize catalog_number - can be string or number from PyStitch
|
||||||
|
const catalogNum = thread.catalog_number;
|
||||||
|
const normalizedCatalog =
|
||||||
|
catalogNum !== undefined &&
|
||||||
|
catalogNum !== null &&
|
||||||
|
catalogNum !== -1 &&
|
||||||
|
catalogNum !== '-1' &&
|
||||||
|
catalogNum !== ''
|
||||||
|
? String(catalogNum)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: thread.color ?? 0,
|
||||||
|
hex: thread.hex || '#000000',
|
||||||
|
catalogNumber: normalizedCatalog,
|
||||||
|
brand: thread.brand && thread.brand !== '' ? thread.brand : null,
|
||||||
|
description:
|
||||||
|
thread.description && thread.description !== ''
|
||||||
|
? thread.description
|
||||||
|
: null,
|
||||||
|
chart: thread.chart && thread.chart !== '' ? thread.chart : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encode stitches to PEN format using the extracted encoder
|
||||||
|
console.log('[patternConverter] Encoding stitches to PEN format...');
|
||||||
|
console.log(' - Input stitches:', stitches);
|
||||||
|
const { penBytes: penStitches, bounds } = encodeStitchesToPen(stitches);
|
||||||
|
const { minX, maxX, minY, maxY } = bounds;
|
||||||
|
|
||||||
|
// Calculate unique colors from threads (threads represent color blocks, not unique colors)
|
||||||
|
const uniqueColors = threads.reduce(
|
||||||
|
(acc, thread, idx) => {
|
||||||
|
const existing = acc.find((c) => c.hex === thread.hex);
|
||||||
|
if (existing) {
|
||||||
|
existing.threadIndices.push(idx);
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
color: thread.color,
|
||||||
|
hex: thread.hex,
|
||||||
|
brand: thread.brand,
|
||||||
|
catalogNumber: thread.catalogNumber,
|
||||||
|
description: thread.description,
|
||||||
|
chart: thread.chart,
|
||||||
|
threadIndices: [idx],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as Array<{
|
||||||
|
color: number;
|
||||||
|
hex: string;
|
||||||
|
brand: string | null;
|
||||||
|
catalogNumber: string | null;
|
||||||
|
description: string | null;
|
||||||
|
chart: string | null;
|
||||||
|
threadIndices: number[];
|
||||||
|
}>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
self.postMessage({
|
||||||
|
type: 'CONVERT_COMPLETE',
|
||||||
|
data: {
|
||||||
|
stitches,
|
||||||
|
threads,
|
||||||
|
uniqueColors,
|
||||||
|
penData: penStitches, // Send as array (will be converted to Uint8Array in main thread)
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as WorkerResponse);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
console.error('[PyodideWorker] Conversion error:', err);
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: 'CONVERT_ERROR',
|
||||||
|
error: errorMsg,
|
||||||
|
} as WorkerResponse);
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle messages from main thread
|
||||||
|
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
||||||
|
const message = event.data;
|
||||||
|
console.log('[PatternConverterWorker] Received message:', message.type);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (message.type) {
|
||||||
|
case 'INITIALIZE':
|
||||||
|
console.log('[PatternConverterWorker] Starting initialization...');
|
||||||
|
await initializePyodide(message.pyodideIndexURL, message.pystitchWheelURL);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'CONVERT_PES':
|
||||||
|
console.log('[PatternConverterWorker] Starting PES conversion...');
|
||||||
|
await convertPesToPen(message.fileData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error('[PatternConverterWorker] Unknown message type:', message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PatternConverterWorker] Error handling message:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[PatternConverterWorker] Message handler registered');
|
||||||
179
src/formats/pen/decoder.ts
Normal file
179
src/formats/pen/decoder.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
/**
|
||||||
|
* PEN Format Decoder
|
||||||
|
*
|
||||||
|
* This module contains the logic for decoding Brother PP1 PEN format embroidery files.
|
||||||
|
* The PEN format uses absolute coordinates shifted left by 3 bits, with flags in the low 3 bits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DecodedPenStitch, DecodedPenData, PenColorBlock } from './types';
|
||||||
|
|
||||||
|
// PEN format flags
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a single PEN stitch (4 bytes) into coordinates and flags
|
||||||
|
*
|
||||||
|
* @param bytes The byte array containing PEN data
|
||||||
|
* @param offset The offset in bytes to start reading from
|
||||||
|
* @returns Decoded stitch with coordinates and flag information
|
||||||
|
*/
|
||||||
|
export function decodePenStitch(
|
||||||
|
bytes: Uint8Array | number[],
|
||||||
|
offset: number
|
||||||
|
): DecodedPenStitch {
|
||||||
|
const xLow = bytes[offset];
|
||||||
|
const xHigh = bytes[offset + 1];
|
||||||
|
const yLow = bytes[offset + 2];
|
||||||
|
const yHigh = bytes[offset + 3];
|
||||||
|
|
||||||
|
const xRaw = xLow | (xHigh << 8);
|
||||||
|
const yRaw = yLow | (yHigh << 8);
|
||||||
|
|
||||||
|
// Extract flags from low 3 bits
|
||||||
|
const xFlags = xRaw & 0x07;
|
||||||
|
const yFlags = yRaw & 0x07;
|
||||||
|
|
||||||
|
// Clear flags and shift right to get actual coordinates
|
||||||
|
const xClean = xRaw & 0xFFF8;
|
||||||
|
const yClean = yRaw & 0xFFF8;
|
||||||
|
|
||||||
|
// Convert to signed 16-bit
|
||||||
|
let xSigned = xClean;
|
||||||
|
let ySigned = yClean;
|
||||||
|
if (xSigned > 0x7FFF) xSigned = xSigned - 0x10000;
|
||||||
|
if (ySigned > 0x7FFF) ySigned = ySigned - 0x10000;
|
||||||
|
|
||||||
|
// Shift right by 3 to get actual coordinates
|
||||||
|
const x = xSigned >> 3;
|
||||||
|
const y = ySigned >> 3;
|
||||||
|
|
||||||
|
const isFeed = (yFlags & PEN_FEED_DATA) !== 0;
|
||||||
|
const isCut = (yFlags & PEN_CUT_DATA) !== 0;
|
||||||
|
const isColorEnd = xFlags === PEN_COLOR_END;
|
||||||
|
const isDataEnd = xFlags === PEN_DATA_END;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
xFlags,
|
||||||
|
yFlags,
|
||||||
|
isFeed,
|
||||||
|
isCut,
|
||||||
|
isColorEnd,
|
||||||
|
isDataEnd,
|
||||||
|
// Compatibility aliases
|
||||||
|
isJump: isFeed,
|
||||||
|
flags: (xFlags & 0x07) | (yFlags & 0x07),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode all stitches from PEN format bytes
|
||||||
|
*
|
||||||
|
* @param bytes PEN format byte array
|
||||||
|
* @returns Array of decoded stitches
|
||||||
|
*/
|
||||||
|
export function decodeAllPenStitches(bytes: Uint8Array | number[]): DecodedPenStitch[] {
|
||||||
|
if (bytes.length < 4 || bytes.length % 4 !== 0) {
|
||||||
|
throw new Error(`Invalid PEN data size: ${bytes.length} bytes (must be multiple of 4)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stitches: DecodedPenStitch[] = [];
|
||||||
|
for (let i = 0; i < bytes.length; i += 4) {
|
||||||
|
stitches.push(decodePenStitch(bytes, i));
|
||||||
|
}
|
||||||
|
return stitches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode PEN format data into a complete pattern structure
|
||||||
|
*
|
||||||
|
* This function parses PEN bytes and extracts:
|
||||||
|
* - Individual stitches with coordinates and flags
|
||||||
|
* - Color blocks (groups of stitches using the same thread color)
|
||||||
|
* - Pattern bounds
|
||||||
|
*
|
||||||
|
* @param data PEN format byte array
|
||||||
|
* @returns Complete decoded pattern data
|
||||||
|
*/
|
||||||
|
export function decodePenData(data: Uint8Array): DecodedPenData {
|
||||||
|
const stitches = decodeAllPenStitches(data);
|
||||||
|
const colorBlocks: PenColorBlock[] = [];
|
||||||
|
|
||||||
|
let minX = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
let currentColorStart = 0;
|
||||||
|
let currentColor = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < stitches.length; i++) {
|
||||||
|
const stitch = stitches[i];
|
||||||
|
|
||||||
|
// Track bounds (exclude jump stitches)
|
||||||
|
if (!stitch.isFeed) {
|
||||||
|
minX = Math.min(minX, stitch.x);
|
||||||
|
maxX = Math.max(maxX, stitch.x);
|
||||||
|
minY = Math.min(minY, stitch.y);
|
||||||
|
maxY = Math.max(maxY, stitch.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for color change or data end
|
||||||
|
if (stitch.isColorEnd) {
|
||||||
|
colorBlocks.push({
|
||||||
|
startStitchIndex: currentColorStart,
|
||||||
|
endStitchIndex: i,
|
||||||
|
colorIndex: currentColor,
|
||||||
|
// Compatibility aliases
|
||||||
|
startStitch: currentColorStart,
|
||||||
|
endStitch: i,
|
||||||
|
});
|
||||||
|
currentColor++;
|
||||||
|
currentColorStart = i + 1;
|
||||||
|
} else if (stitch.isDataEnd) {
|
||||||
|
// Final color block
|
||||||
|
if (currentColorStart <= i) {
|
||||||
|
colorBlocks.push({
|
||||||
|
startStitchIndex: currentColorStart,
|
||||||
|
endStitchIndex: i,
|
||||||
|
colorIndex: currentColor,
|
||||||
|
// Compatibility aliases
|
||||||
|
startStitch: currentColorStart,
|
||||||
|
endStitch: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stitches,
|
||||||
|
colorBlocks,
|
||||||
|
bounds: {
|
||||||
|
minX: minX === Infinity ? 0 : minX,
|
||||||
|
maxX: maxX === -Infinity ? 0 : maxX,
|
||||||
|
minY: minY === Infinity ? 0 : minY,
|
||||||
|
maxY: maxY === -Infinity ? 0 : maxY,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color index for a stitch at the given index
|
||||||
|
*
|
||||||
|
* @param penData Decoded PEN pattern data
|
||||||
|
* @param stitchIndex Index of the stitch
|
||||||
|
* @returns Color index, or -1 if not found
|
||||||
|
*/
|
||||||
|
export function getStitchColor(penData: DecodedPenData, stitchIndex: number): number {
|
||||||
|
for (const block of penData.colorBlocks) {
|
||||||
|
if (stitchIndex >= block.startStitchIndex && stitchIndex <= block.endStitchIndex) {
|
||||||
|
return block.colorIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
643
src/formats/pen/encoder.test.ts
Normal file
643
src/formats/pen/encoder.test.ts
Normal file
|
|
@ -0,0 +1,643 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
encodeStitchPosition,
|
||||||
|
calculateLockDirection,
|
||||||
|
generateLockStitches,
|
||||||
|
encodeStitchesToPen,
|
||||||
|
} from './encoder';
|
||||||
|
import { decodeAllPenStitches } from './decoder';
|
||||||
|
import { STITCH, MOVE, TRIM, END } from '../import/constants';
|
||||||
|
|
||||||
|
// PEN format flag constants for testing
|
||||||
|
const PEN_FEED_DATA = 0x01;
|
||||||
|
const PEN_CUT_DATA = 0x02;
|
||||||
|
|
||||||
|
describe('encodeStitchPosition', () => {
|
||||||
|
it('should encode position (0, 0) correctly', () => {
|
||||||
|
const result = encodeStitchPosition(0, 0);
|
||||||
|
expect(result).toEqual([0x00, 0x00, 0x00, 0x00]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should shift coordinates left by 3 bits', () => {
|
||||||
|
// Position (1, 1) should become (8, 8) after shifting
|
||||||
|
const result = encodeStitchPosition(1, 1);
|
||||||
|
expect(result).toEqual([0x08, 0x00, 0x08, 0x00]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle negative coordinates', () => {
|
||||||
|
// -1 in 16-bit signed = 0xFFFF, shifted left 3 = 0xFFF8
|
||||||
|
const result = encodeStitchPosition(-1, -1);
|
||||||
|
expect(result).toEqual([0xF8, 0xFF, 0xF8, 0xFF]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode multi-byte coordinates correctly', () => {
|
||||||
|
// Position (128, 0) -> shifted = 1024 = 0x0400
|
||||||
|
const result = encodeStitchPosition(128, 0);
|
||||||
|
expect(result).toEqual([0x00, 0x04, 0x00, 0x00]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should round fractional coordinates', () => {
|
||||||
|
const result = encodeStitchPosition(1.5, 2.4);
|
||||||
|
// 2 << 3 = 16, 2 << 3 = 16
|
||||||
|
expect(result).toEqual([0x10, 0x00, 0x10, 0x00]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calculateLockDirection', () => {
|
||||||
|
it('should look ahead for forward direction', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[20, 0, STITCH, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = calculateLockDirection(stitches, 0, true);
|
||||||
|
|
||||||
|
// Should accumulate forward stitches
|
||||||
|
expect(result.dirX).toBeGreaterThan(0);
|
||||||
|
expect(result.dirY).toBe(0);
|
||||||
|
// Result should have magnitude ~8.0
|
||||||
|
const magnitude = Math.sqrt(result.dirX ** 2 + result.dirY ** 2);
|
||||||
|
expect(magnitude).toBeCloseTo(8.0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should look backward for backward direction', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[20, 0, STITCH, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = calculateLockDirection(stitches, 2, false);
|
||||||
|
|
||||||
|
// Should accumulate backward stitches
|
||||||
|
expect(result.dirX).toBeLessThan(0);
|
||||||
|
expect(result.dirY).toBe(0);
|
||||||
|
// Result should have magnitude ~8.0
|
||||||
|
const magnitude = Math.sqrt(result.dirX ** 2 + result.dirY ** 2);
|
||||||
|
expect(magnitude).toBeCloseTo(8.0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip MOVE stitches when accumulating', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[5, 0, MOVE, 0], // Should be skipped
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[15, 0, STITCH, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = calculateLockDirection(stitches, 0, true);
|
||||||
|
|
||||||
|
// Should skip the MOVE stitch and only count actual stitches
|
||||||
|
expect(result.dirX).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return fallback diagonal for empty or short stitch sequences', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = calculateLockDirection(stitches, 0, true);
|
||||||
|
|
||||||
|
// Should return diagonal fallback
|
||||||
|
const expectedMag = 8.0 / Math.sqrt(2);
|
||||||
|
expect(result.dirX).toBeCloseTo(expectedMag, 1);
|
||||||
|
expect(result.dirY).toBeCloseTo(expectedMag, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize accumulated vector to magnitude 8.0', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[3, 4, STITCH, 0], // Distance = 5
|
||||||
|
[6, 8, STITCH, 0], // Accumulated: (6, 8), length = 10
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = calculateLockDirection(stitches, 0, true);
|
||||||
|
|
||||||
|
// Should normalize (6, 8) to magnitude 8.0
|
||||||
|
// Expected: (6 * 8 / 10, 8 * 8 / 10) = (4.8, 6.4)
|
||||||
|
expect(result.dirX).toBeCloseTo(4.8, 1);
|
||||||
|
expect(result.dirY).toBeCloseTo(6.4, 1);
|
||||||
|
|
||||||
|
const magnitude = Math.sqrt(result.dirX ** 2 + result.dirY ** 2);
|
||||||
|
expect(magnitude).toBeCloseTo(8.0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop accumulating after reaching target length', () => {
|
||||||
|
// Create a long chain of stitches
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[2, 0, STITCH, 0],
|
||||||
|
[4, 0, STITCH, 0],
|
||||||
|
[6, 0, STITCH, 0],
|
||||||
|
[8, 0, STITCH, 0],
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[100, 0, STITCH, 0], // This should not be reached
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = calculateLockDirection(stitches, 0, true);
|
||||||
|
|
||||||
|
// Should stop once accumulated length >= 8.0
|
||||||
|
const magnitude = Math.sqrt(result.dirX ** 2 + result.dirY ** 2);
|
||||||
|
expect(magnitude).toBeCloseTo(8.0, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateLockStitches', () => {
|
||||||
|
it('should generate 8 lock stitches (32 bytes)', () => {
|
||||||
|
const result = generateLockStitches(0, 0, 8.0, 0);
|
||||||
|
expect(result.length).toBe(32); // 8 stitches * 4 bytes each
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should alternate between +dir and -dir', () => {
|
||||||
|
const result = generateLockStitches(0, 0, 8.0, 0);
|
||||||
|
expect(result.length).toBe(32); // 8 stitches * 4 bytes
|
||||||
|
|
||||||
|
// With a larger base position, verify the pattern still generates correctly
|
||||||
|
const result2 = generateLockStitches(100, 100, 8.0, 0);
|
||||||
|
expect(result2.length).toBe(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rotate stitches in the given direction', () => {
|
||||||
|
// Direction pointing right (8, 0)
|
||||||
|
const result = generateLockStitches(0, 0, 8.0, 0);
|
||||||
|
|
||||||
|
// Scale: 0.4 / 8.0 = 0.05
|
||||||
|
// Scaled direction: (0.4, 0)
|
||||||
|
// Positions should alternate between (+0.4, 0) and (-0.4, 0)
|
||||||
|
|
||||||
|
expect(result.length).toBe(32);
|
||||||
|
|
||||||
|
// With diagonal direction (8/√2, 8/√2)
|
||||||
|
const diag = 8.0 / Math.sqrt(2);
|
||||||
|
const result2 = generateLockStitches(0, 0, diag, diag);
|
||||||
|
expect(result2.length).toBe(32);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encodeStitchesToPen', () => {
|
||||||
|
it('should encode a simple stitch sequence', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[20, 0, STITCH | END, 0], // Last stitch with both STITCH and END flags
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
expect(result.penBytes.length).toBeGreaterThan(0);
|
||||||
|
expect(result.penBytes.length % 4).toBe(0); // Should be multiple of 4 (4 bytes per stitch)
|
||||||
|
expect(result.bounds.minX).toBe(0);
|
||||||
|
expect(result.bounds.maxX).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track bounds correctly', () => {
|
||||||
|
const stitches = [
|
||||||
|
[10, 20, STITCH, 0],
|
||||||
|
[-5, 30, STITCH, 0],
|
||||||
|
[15, -10, STITCH, 0],
|
||||||
|
[0, 0, END, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
expect(result.bounds.minX).toBe(-5);
|
||||||
|
expect(result.bounds.maxX).toBe(15);
|
||||||
|
expect(result.bounds.minY).toBe(-10);
|
||||||
|
expect(result.bounds.maxY).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark the last stitch with DATA_END flag', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, END, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
// Last stitch should have DATA_END (0x05) in low 3 bits of X coordinate
|
||||||
|
const lastStitchStart = result.penBytes.length - 4;
|
||||||
|
const xLow = result.penBytes[lastStitchStart];
|
||||||
|
expect(xLow & 0x07).toBe(0x05); // DATA_END flag
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle color changes with lock stitches', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0], // Color 0
|
||||||
|
[10, 0, STITCH, 0], // Color 0
|
||||||
|
[20, 0, STITCH, 0], // Color 0 - last stitch before color change
|
||||||
|
[20, 0, STITCH, 1], // Color 1 - first stitch of new color
|
||||||
|
[30, 0, STITCH, 1], // Color 1
|
||||||
|
[40, 0, END, 1], // Color 1 - last stitch
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
// Should include:
|
||||||
|
// - Regular stitches for color 0 (3 stitches = 12 bytes)
|
||||||
|
// - Finishing lock stitches (32 bytes)
|
||||||
|
// - Cut command (4 bytes)
|
||||||
|
// - COLOR_END marker (4 bytes)
|
||||||
|
// - Starting lock stitches (32 bytes)
|
||||||
|
// - Regular stitches for color 1 (3 stitches = 12 bytes)
|
||||||
|
// Total: 96+ bytes
|
||||||
|
|
||||||
|
expect(result.penBytes.length).toBeGreaterThan(90); // Should have many bytes from lock stitches
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode color change sequence in correct order', () => {
|
||||||
|
// Test the exact sequence of operations for a color change
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0], // Color 0
|
||||||
|
[10, 0, STITCH, 0], // Color 0 - last stitch before color change
|
||||||
|
[10, 0, STITCH, 1], // Color 1 - first stitch (same position)
|
||||||
|
[20, 0, STITCH | END, 1], // Color 1 - last stitch
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
const decoded = decodeAllPenStitches(result.penBytes);
|
||||||
|
|
||||||
|
// Expected sequence:
|
||||||
|
// 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)
|
||||||
|
// 4. Cut command at (10, 0)
|
||||||
|
// 5. COLOR_END marker at (10, 0) - no jump needed since next color is at same position
|
||||||
|
// 6. 8 starting lock stitches around (10, 0)
|
||||||
|
// 7. Stitch at (10, 0) - color 1
|
||||||
|
// 8. Stitch at (20, 0) - color 1 (last, with END flag)
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
// 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++;
|
||||||
|
|
||||||
|
// 2. Second stitch (10, 0) - last before color change
|
||||||
|
expect(decoded[idx].x).toBe(10);
|
||||||
|
expect(decoded[idx].y).toBe(0);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 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.isFeed).toBe(false);
|
||||||
|
expect(lockStitch.isCut).toBe(false);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cut command
|
||||||
|
const cutStitch = decoded[idx];
|
||||||
|
expect(cutStitch.x).toBe(10);
|
||||||
|
expect(cutStitch.y).toBe(0);
|
||||||
|
expect(cutStitch.isCut).toBe(true);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 5. COLOR_END marker (no jump needed since same position)
|
||||||
|
const colorEndStitch = decoded[idx];
|
||||||
|
expect(colorEndStitch.x).toBe(10);
|
||||||
|
expect(colorEndStitch.y).toBe(0);
|
||||||
|
expect(colorEndStitch.isColorEnd).toBe(true);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. First stitch of new color
|
||||||
|
expect(decoded[idx].x).toBe(10);
|
||||||
|
expect(decoded[idx].y).toBe(0);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 8. Last stitch with DATA_END flag
|
||||||
|
expect(decoded[idx].x).toBe(20);
|
||||||
|
expect(decoded[idx].y).toBe(0);
|
||||||
|
expect(decoded[idx].isDataEnd).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode color change with jump in correct order', () => {
|
||||||
|
// Test color change when next color is at a different position
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0], // Color 0
|
||||||
|
[10, 0, STITCH, 0], // Color 0 - last before change
|
||||||
|
[30, 10, STITCH, 1], // Color 1 - different position, requires jump
|
||||||
|
[40, 10, STITCH | END, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
const decoded = decodeAllPenStitches(result.penBytes);
|
||||||
|
|
||||||
|
let idx = 2; // Skip first two regular stitches
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Cut command at (10, 0)
|
||||||
|
expect(decoded[idx].isCut).toBe(true);
|
||||||
|
expect(decoded[idx].x).toBe(10);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 3. Jump to new position (30, 10)
|
||||||
|
expect(decoded[idx].x).toBe(30);
|
||||||
|
expect(decoded[idx].y).toBe(10);
|
||||||
|
expect(decoded[idx].isFeed).toBe(true);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 4. COLOR_END marker at (30, 10)
|
||||||
|
expect(decoded[idx].x).toBe(30);
|
||||||
|
expect(decoded[idx].y).toBe(10);
|
||||||
|
expect(decoded[idx].isColorEnd).toBe(true);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Continue with new color stitches
|
||||||
|
expect(decoded[idx].x).toBe(30);
|
||||||
|
expect(decoded[idx].y).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode color change followed by explicit JUMP in correct order', () => {
|
||||||
|
// Test when PES data has a JUMP stitch immediately after color change
|
||||||
|
// This is a common pattern: color change, then jump to new location
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0], // Color 0
|
||||||
|
[10, 0, STITCH, 0], // Color 0 - last before change
|
||||||
|
[50, 20, MOVE, 1], // Color 1 - JUMP to new location (50, 20)
|
||||||
|
[50, 20, STITCH, 1], // Color 1 - first actual stitch at new location
|
||||||
|
[60, 20, STITCH | END, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
const decoded = decodeAllPenStitches(result.penBytes);
|
||||||
|
|
||||||
|
// Expected sequence:
|
||||||
|
// 1. Stitch at (0, 0) - color 0
|
||||||
|
// 2. Stitch at (10, 0) - color 0 (last before change)
|
||||||
|
// 3. 8 finishing lock stitches at (10, 0)
|
||||||
|
// 4. Cut command at (10, 0)
|
||||||
|
// 5. Jump to (50, 20) - from MOVE stitch (the encoder skips this MOVE in the loop with i++)
|
||||||
|
// 6. COLOR_END marker at (50, 20)
|
||||||
|
// 7. 8 starting lock stitches at (50, 20)
|
||||||
|
// 8. First stitch of new color at (50, 20)
|
||||||
|
// 9. Last stitch at (60, 20) with END flag
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
// 1-2. First two stitches
|
||||||
|
expect(decoded[idx++].x).toBe(0);
|
||||||
|
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);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cut command at (10, 0)
|
||||||
|
expect(decoded[idx].x).toBe(10);
|
||||||
|
expect(decoded[idx].y).toBe(0);
|
||||||
|
expect(decoded[idx].isCut).toBe(true);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 5. Jump to new location (50, 20) - extracted from the MOVE stitch
|
||||||
|
expect(decoded[idx].x).toBe(50);
|
||||||
|
expect(decoded[idx].y).toBe(20);
|
||||||
|
expect(decoded[idx].isFeed).toBe(true);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 6. COLOR_END marker at (50, 20)
|
||||||
|
expect(decoded[idx].x).toBe(50);
|
||||||
|
expect(decoded[idx].y).toBe(20);
|
||||||
|
expect(decoded[idx].isColorEnd).toBe(true);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. First actual stitch of new color at (50, 20)
|
||||||
|
expect(decoded[idx].x).toBe(50);
|
||||||
|
expect(decoded[idx].y).toBe(20);
|
||||||
|
expect(decoded[idx].isFeed).toBe(false);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 9. Last stitch with DATA_END
|
||||||
|
expect(decoded[idx].x).toBe(60);
|
||||||
|
expect(decoded[idx].y).toBe(20);
|
||||||
|
expect(decoded[idx].isDataEnd).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle long jumps with lock stitches and cut in correct order', () => {
|
||||||
|
// Test the exact sequence for a long jump (distance > 50)
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[100, 0, MOVE, 0], // Long jump (distance = 90 > 50)
|
||||||
|
[110, 0, STITCH, 0],
|
||||||
|
[120, 0, STITCH | END, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
const decoded = decodeAllPenStitches(result.penBytes);
|
||||||
|
|
||||||
|
// Expected sequence:
|
||||||
|
// 1. Stitch at (0, 0)
|
||||||
|
// 2. Stitch at (10, 0)
|
||||||
|
// 3. 8 finishing lock stitches at (10, 0)
|
||||||
|
// 4. Jump to (100, 0) with FEED and CUT flags
|
||||||
|
// 5. 8 starting lock stitches at (100, 0)
|
||||||
|
// 6. Stitch at (110, 0)
|
||||||
|
// 7. Stitch at (120, 0) with END flag
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
// 1-2. First two stitches
|
||||||
|
expect(decoded[idx++].x).toBe(0);
|
||||||
|
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.isFeed).toBe(false);
|
||||||
|
expect(lockStitch.isCut).toBe(false);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Jump to (100, 0) with BOTH FEED and CUT flags
|
||||||
|
const jumpStitch = decoded[idx];
|
||||||
|
expect(jumpStitch.x).toBe(100);
|
||||||
|
expect(jumpStitch.y).toBe(0);
|
||||||
|
expect(jumpStitch.isFeed).toBe(true);
|
||||||
|
expect(jumpStitch.isCut).toBe(true);
|
||||||
|
expect(jumpStitch.yFlags).toBe(PEN_FEED_DATA | PEN_CUT_DATA); // 0x03
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6-7. Final two stitches
|
||||||
|
expect(decoded[idx].x).toBe(110);
|
||||||
|
expect(decoded[idx].y).toBe(0);
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
expect(decoded[idx].x).toBe(120);
|
||||||
|
expect(decoded[idx].isDataEnd).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode MOVE flag for jump stitches', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, MOVE, 0], // Short jump (no lock stitches)
|
||||||
|
[20, 0, END, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
// 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 yLow = result.penBytes[jumpStitchStart + 2];
|
||||||
|
expect(yLow & 0x01).toBe(0x01); // FEED_DATA flag
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include MOVE stitches in bounds calculation', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[100, 100, MOVE, 0], // Jump - should not affect bounds
|
||||||
|
[10, 10, STITCH, 0],
|
||||||
|
[20, 20, STITCH | END, 0], // Last stitch with both STITCH and END flags
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
// Bounds should only include STITCH positions, not MOVE
|
||||||
|
expect(result.bounds.minX).toBe(0);
|
||||||
|
expect(result.bounds.maxX).toBe(20);
|
||||||
|
expect(result.bounds.minY).toBe(0);
|
||||||
|
expect(result.bounds.maxY).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle TRIM flag correctly', () => {
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, TRIM, 0],
|
||||||
|
[20, 0, STITCH | END, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
const decoded = decodeAllPenStitches(result.penBytes);
|
||||||
|
|
||||||
|
// Verify sequence:
|
||||||
|
// 1. Regular stitch at (0, 0)
|
||||||
|
expect(decoded[0].x).toBe(0);
|
||||||
|
expect(decoded[0].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
|
||||||
|
|
||||||
|
// 3. Final stitch with DATA_END
|
||||||
|
expect(decoded[2].x).toBe(20);
|
||||||
|
expect(decoded[2].isDataEnd).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty stitch array', () => {
|
||||||
|
const stitches: number[][] = [];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
expect(result.penBytes.length).toBe(0);
|
||||||
|
expect(result.bounds.minX).toBe(0);
|
||||||
|
expect(result.bounds.maxX).toBe(0);
|
||||||
|
expect(result.bounds.minY).toBe(0);
|
||||||
|
expect(result.bounds.maxY).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single stitch', () => {
|
||||||
|
const stitches = [
|
||||||
|
[5, 10, END, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
|
||||||
|
expect(result.penBytes.length).toBe(4);
|
||||||
|
expect(result.bounds.minX).toBe(5);
|
||||||
|
expect(result.bounds.maxX).toBe(5);
|
||||||
|
expect(result.bounds.minY).toBe(10);
|
||||||
|
expect(result.bounds.maxY).toBe(10);
|
||||||
|
// END stitches update bounds (they're not MOVE stitches)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add DATA_END flag to last stitch even without END flag in input', () => {
|
||||||
|
// Test that the encoder automatically marks the last stitch with DATA_END
|
||||||
|
// even if the input stitches don't have an END flag
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[20, 0, STITCH, 0], // Last stitch - NO END flag
|
||||||
|
];
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add DATA_END flag when input has explicit END flag', () => {
|
||||||
|
// Verify that END flag in input also results in DATA_END flag in output
|
||||||
|
const stitches = [
|
||||||
|
[0, 0, STITCH, 0],
|
||||||
|
[10, 0, STITCH, 0],
|
||||||
|
[20, 0, STITCH | END, 0], // Explicit END flag
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = encodeStitchesToPen(stitches);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
364
src/formats/pen/encoder.ts
Normal file
364
src/formats/pen/encoder.ts
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
const LOCK_STITCH_SCALE = 0.4 / 8.0; // Scale the magnitude-8 vector down to 0.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;
|
||||||
|
lockBytes.push(...encodeStitchPosition(x + scaledDirX * sign, y + scaledDirY * sign));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
51
src/formats/pen/types.ts
Normal file
51
src/formats/pen/types.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* PEN Format Types
|
||||||
|
*
|
||||||
|
* Type definitions for decoded PEN format data.
|
||||||
|
* These types represent the parsed structure of Brother PP1 PEN embroidery files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single decoded PEN stitch with coordinates and flags
|
||||||
|
*/
|
||||||
|
export interface DecodedPenStitch {
|
||||||
|
x: number; // X coordinate (already shifted right by 3)
|
||||||
|
y: number; // Y coordinate (already shifted right by 3)
|
||||||
|
xFlags: number; // Flags from X coordinate low 3 bits
|
||||||
|
yFlags: number; // Flags from Y coordinate low 3 bits
|
||||||
|
isFeed: boolean; // Jump/move without stitching (Y-bit 0)
|
||||||
|
isCut: boolean; // Trim/cut thread (Y-bit 1)
|
||||||
|
isColorEnd: boolean; // Color change marker (X-bits 0-2 = 0x03)
|
||||||
|
isDataEnd: boolean; // Pattern end marker (X-bits 0-2 = 0x05)
|
||||||
|
|
||||||
|
// Compatibility aliases
|
||||||
|
isJump: boolean; // Alias for isFeed (backward compatibility)
|
||||||
|
flags: number; // Combined flags (backward compatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A color block representing stitches of the same thread color
|
||||||
|
*/
|
||||||
|
export interface PenColorBlock {
|
||||||
|
startStitchIndex: number; // Index of first stitch in this color
|
||||||
|
endStitchIndex: number; // Index of last stitch in this color
|
||||||
|
colorIndex: number; // Color number (0-based)
|
||||||
|
|
||||||
|
// Compatibility aliases
|
||||||
|
startStitch: number; // Alias for startStitchIndex (backward compatibility)
|
||||||
|
endStitch: number; // Alias for endStitchIndex (backward compatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete decoded PEN pattern data
|
||||||
|
*/
|
||||||
|
export interface DecodedPenData {
|
||||||
|
stitches: DecodedPenStitch[];
|
||||||
|
colorBlocks: PenColorBlock[];
|
||||||
|
bounds: {
|
||||||
|
minX: number;
|
||||||
|
maxX: number;
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "../services/PatternCacheService";
|
} from "../services/PatternCacheService";
|
||||||
import type { IStorageService } from "../platform/interfaces/IStorageService";
|
import type { IStorageService } from "../platform/interfaces/IStorageService";
|
||||||
import { createStorageService } from "../platform";
|
import { createStorageService } from "../platform";
|
||||||
import type { PesPatternData } from "../utils/pystitchConverter";
|
import type { PesPatternData } from "../formats/import/pesImporter";
|
||||||
import { SewingMachineError } from "../utils/errorCodeHelpers";
|
import { SewingMachineError } from "../utils/errorCodeHelpers";
|
||||||
|
|
||||||
export function useBrotherMachine() {
|
export function useBrotherMachine() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { PatternCacheService } from '../../services/PatternCacheService';
|
import { PatternCacheService } from '../../services/PatternCacheService';
|
||||||
import type { IStorageService, ICachedPattern } from '../interfaces/IStorageService';
|
import type { IStorageService, ICachedPattern } from '../interfaces/IStorageService';
|
||||||
import type { PesPatternData } from '../../utils/pystitchConverter';
|
import type { PesPatternData } from '../../formats/import/pesImporter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browser implementation of storage service using localStorage
|
* Browser implementation of storage service using localStorage
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { IStorageService, ICachedPattern } from '../interfaces/IStorageService';
|
import type { IStorageService, ICachedPattern } from '../interfaces/IStorageService';
|
||||||
import type { PesPatternData } from '../../utils/pystitchConverter';
|
import type { PesPatternData } from '../../formats/import/pesImporter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Electron implementation of storage service using electron-store via IPC
|
* Electron implementation of storage service using electron-store via IPC
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { PesPatternData } from '../../utils/pystitchConverter';
|
import type { PesPatternData } from '../../formats/import/pesImporter';
|
||||||
|
|
||||||
export interface ICachedPattern {
|
export interface ICachedPattern {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { PesPatternData } from '../utils/pystitchConverter';
|
import type { PesPatternData } from '../formats/import/pesImporter';
|
||||||
|
|
||||||
interface CachedPattern {
|
interface CachedPattern {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { SewingMachineError } from '../utils/errorCodeHelpers';
|
||||||
import { uuidToString } from '../services/PatternCacheService';
|
import { uuidToString } from '../services/PatternCacheService';
|
||||||
import { createStorageService } from '../platform';
|
import { createStorageService } from '../platform';
|
||||||
import type { IStorageService } from '../platform/interfaces/IStorageService';
|
import type { IStorageService } from '../platform/interfaces/IStorageService';
|
||||||
import type { PesPatternData } from '../utils/pystitchConverter';
|
import type { PesPatternData } from '../formats/import/pesImporter';
|
||||||
|
|
||||||
interface MachineState {
|
interface MachineState {
|
||||||
// Service instances
|
// Service instances
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { PesPatternData } from '../utils/pystitchConverter';
|
import type { PesPatternData } from '../formats/import/pesImporter';
|
||||||
|
|
||||||
interface PatternState {
|
interface PatternState {
|
||||||
// Pattern data
|
// Pattern data
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { patternConverterClient } from '../utils/patternConverterClient';
|
import { patternConverterClient } from '../formats/import/client';
|
||||||
|
|
||||||
interface UIState {
|
interface UIState {
|
||||||
// Pyodide state
|
// Pyodide state
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { PesPatternData } from './pystitchConverter';
|
import type { PesPatternData } from '../formats/import/pesImporter';
|
||||||
import { getThreadColor } from './pystitchConverter';
|
import { getThreadColor } from '../formats/import/pesImporter';
|
||||||
import type { MachineInfo } from '../types/machine';
|
import type { MachineInfo } from '../types/machine';
|
||||||
import { MOVE } from './embroideryConstants';
|
import { MOVE } from '../formats/import/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a grid with specified spacing
|
* Renders a grid with specified spacing
|
||||||
|
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import type { PenData, PenStitch, PenColorBlock } from '../types/machine';
|
|
||||||
|
|
||||||
// PEN format flags
|
|
||||||
const PEN_FEED_DATA = 0x01; // Y-coordinate low byte, bit 0
|
|
||||||
const PEN_COLOR_END = 0x03; // X-coordinate low byte, bits 0-2
|
|
||||||
const PEN_DATA_END = 0x05; // X-coordinate low byte, bits 0-2
|
|
||||||
|
|
||||||
export function parsePenData(data: Uint8Array): PenData {
|
|
||||||
if (data.length < 4 || data.length % 4 !== 0) {
|
|
||||||
throw new Error(`Invalid PEN data size: ${data.length} bytes`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stitches: PenStitch[] = [];
|
|
||||||
const colorBlocks: PenColorBlock[] = [];
|
|
||||||
const stitchCount = data.length / 4;
|
|
||||||
|
|
||||||
let currentColorStart = 0;
|
|
||||||
let currentColor = 0;
|
|
||||||
let minX = Infinity, maxX = -Infinity;
|
|
||||||
let minY = Infinity, maxY = -Infinity;
|
|
||||||
|
|
||||||
console.log(`Parsing PEN data: ${data.length} bytes, ${stitchCount} stitches`);
|
|
||||||
|
|
||||||
for (let i = 0; i < stitchCount; i++) {
|
|
||||||
const offset = i * 4;
|
|
||||||
|
|
||||||
// Extract coordinates (shifted left by 3 bits in PEN format)
|
|
||||||
const xRaw = data[offset] | (data[offset + 1] << 8);
|
|
||||||
const yRaw = data[offset + 2] | (data[offset + 3] << 8);
|
|
||||||
|
|
||||||
// Extract flags from low 3 bits
|
|
||||||
const xFlags = data[offset] & 0x07;
|
|
||||||
const yFlags = data[offset + 2] & 0x07;
|
|
||||||
|
|
||||||
// Decode coordinates (shift right by 3 to get actual position)
|
|
||||||
// The coordinates are stored as signed 16-bit values, left-shifted by 3
|
|
||||||
// Step 1: Clear the flag bits (low 3 bits) from the raw values
|
|
||||||
const xRawClean = xRaw & 0xFFF8;
|
|
||||||
const yRawClean = yRaw & 0xFFF8;
|
|
||||||
|
|
||||||
// Step 2: Convert from unsigned 16-bit to signed 16-bit
|
|
||||||
let xSigned = xRawClean;
|
|
||||||
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 = {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
flags: (xFlags & 0x07) | (yFlags & 0x07),
|
|
||||||
isJump: (yFlags & PEN_FEED_DATA) !== 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
stitches.push(stitch);
|
|
||||||
|
|
||||||
// Track bounds
|
|
||||||
if (!stitch.isJump) {
|
|
||||||
minX = Math.min(minX, x);
|
|
||||||
maxX = Math.max(maxX, x);
|
|
||||||
minY = Math.min(minY, y);
|
|
||||||
maxY = Math.max(maxY, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for color change or data end
|
|
||||||
if (xFlags === PEN_COLOR_END) {
|
|
||||||
const block: PenColorBlock = {
|
|
||||||
startStitch: currentColorStart,
|
|
||||||
endStitch: i,
|
|
||||||
colorIndex: currentColor,
|
|
||||||
};
|
|
||||||
colorBlocks.push(block);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Color ${currentColor}: stitches ${currentColorStart}-${i} (${
|
|
||||||
i - currentColorStart + 1
|
|
||||||
} stitches)`
|
|
||||||
);
|
|
||||||
|
|
||||||
currentColor++;
|
|
||||||
currentColorStart = i + 1;
|
|
||||||
} else if (xFlags === PEN_DATA_END) {
|
|
||||||
if (currentColorStart < i) {
|
|
||||||
const block: PenColorBlock = {
|
|
||||||
startStitch: currentColorStart,
|
|
||||||
endStitch: i,
|
|
||||||
colorIndex: currentColor,
|
|
||||||
};
|
|
||||||
colorBlocks.push(block);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Color ${currentColor} (final): stitches ${currentColorStart}-${i} (${
|
|
||||||
i - currentColorStart + 1
|
|
||||||
} stitches)`
|
|
||||||
);
|
|
||||||
|
|
||||||
currentColor++;
|
|
||||||
}
|
|
||||||
console.log(`Data end marker at stitch ${i}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: PenData = {
|
|
||||||
stitches,
|
|
||||||
colorBlocks,
|
|
||||||
totalStitches: stitches.length,
|
|
||||||
colorCount: colorBlocks.length,
|
|
||||||
bounds: {
|
|
||||||
minX: minX === Infinity ? 0 : minX,
|
|
||||||
maxX: maxX === -Infinity ? 0 : maxX,
|
|
||||||
minY: minY === Infinity ? 0 : minY,
|
|
||||||
maxY: maxY === -Infinity ? 0 : maxY,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Parsed: ${result.totalStitches} stitches, ${result.colorCount} colors`
|
|
||||||
);
|
|
||||||
console.log(`Bounds: (${result.bounds.minX}, ${result.bounds.minY}) to (${result.bounds.maxX}, ${result.bounds.maxY})`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStitchColor(penData: PenData, stitchIndex: number): number {
|
|
||||||
for (const block of penData.colorBlocks) {
|
|
||||||
if (stitchIndex >= block.startStitch && stitchIndex <= block.endStitch) {
|
|
||||||
return block.colorIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,754 +0,0 @@
|
||||||
import { loadPyodide, type PyodideInterface } from 'pyodide';
|
|
||||||
import {
|
|
||||||
STITCH,
|
|
||||||
MOVE,
|
|
||||||
TRIM,
|
|
||||||
END,
|
|
||||||
PEN_FEED_DATA,
|
|
||||||
PEN_CUT_DATA,
|
|
||||||
PEN_COLOR_END,
|
|
||||||
PEN_DATA_END,
|
|
||||||
} from '../utils/embroideryConstants';
|
|
||||||
|
|
||||||
// Message types from main thread
|
|
||||||
export type WorkerMessage =
|
|
||||||
| { type: 'INITIALIZE'; pyodideIndexURL?: string; pystitchWheelURL?: string }
|
|
||||||
| { type: 'CONVERT_PES'; fileData: ArrayBuffer; fileName: string };
|
|
||||||
|
|
||||||
// Response types to main thread
|
|
||||||
export type WorkerResponse =
|
|
||||||
| { type: 'INIT_PROGRESS'; progress: number; step: string }
|
|
||||||
| { type: 'INIT_COMPLETE' }
|
|
||||||
| { type: 'INIT_ERROR'; error: string }
|
|
||||||
| {
|
|
||||||
type: 'CONVERT_COMPLETE';
|
|
||||||
data: {
|
|
||||||
stitches: number[][];
|
|
||||||
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: number[]; // Serialized as array
|
|
||||||
colorCount: number;
|
|
||||||
stitchCount: number;
|
|
||||||
bounds: {
|
|
||||||
minX: number;
|
|
||||||
maxX: number;
|
|
||||||
minY: number;
|
|
||||||
maxY: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| { type: 'CONVERT_ERROR'; error: string };
|
|
||||||
|
|
||||||
console.log('[PatternConverterWorker] Worker script loaded');
|
|
||||||
|
|
||||||
let pyodide: PyodideInterface | null = null;
|
|
||||||
let isInitializing = false;
|
|
||||||
|
|
||||||
// JavaScript constants module to expose to Python
|
|
||||||
const jsEmbConstants = {
|
|
||||||
STITCH,
|
|
||||||
MOVE,
|
|
||||||
TRIM,
|
|
||||||
END,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize Pyodide with progress tracking
|
|
||||||
*/
|
|
||||||
async function initializePyodide(pyodideIndexURL?: string, pystitchWheelURL?: string) {
|
|
||||||
if (pyodide) {
|
|
||||||
return; // Already initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInitializing) {
|
|
||||||
throw new Error('Initialization already in progress');
|
|
||||||
}
|
|
||||||
|
|
||||||
isInitializing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
self.postMessage({
|
|
||||||
type: 'INIT_PROGRESS',
|
|
||||||
progress: 0,
|
|
||||||
step: 'Starting initialization...',
|
|
||||||
} as WorkerResponse);
|
|
||||||
|
|
||||||
console.log('[PyodideWorker] Loading Pyodide runtime...');
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'INIT_PROGRESS',
|
|
||||||
progress: 10,
|
|
||||||
step: 'Loading Python runtime...',
|
|
||||||
} as WorkerResponse);
|
|
||||||
|
|
||||||
// Load Pyodide runtime
|
|
||||||
// Use provided URL or default to /assets/
|
|
||||||
const indexURL = pyodideIndexURL || '/assets/';
|
|
||||||
console.log('[PyodideWorker] Pyodide index URL:', indexURL);
|
|
||||||
|
|
||||||
pyodide = await loadPyodide({
|
|
||||||
indexURL: indexURL,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[PyodideWorker] Pyodide runtime loaded');
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'INIT_PROGRESS',
|
|
||||||
progress: 70,
|
|
||||||
step: 'Python runtime loaded',
|
|
||||||
} as WorkerResponse);
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'INIT_PROGRESS',
|
|
||||||
progress: 75,
|
|
||||||
step: 'Loading pystitch library...',
|
|
||||||
} as WorkerResponse);
|
|
||||||
|
|
||||||
// Load pystitch wheel
|
|
||||||
// Use provided URL or default
|
|
||||||
const wheelURL = pystitchWheelURL || '/pystitch-1.0.0-py3-none-any.whl';
|
|
||||||
console.log('[PyodideWorker] Pystitch wheel URL:', wheelURL);
|
|
||||||
|
|
||||||
await pyodide.loadPackage(wheelURL);
|
|
||||||
|
|
||||||
console.log('[PyodideWorker] pystitch library loaded');
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'INIT_PROGRESS',
|
|
||||||
progress: 100,
|
|
||||||
step: 'Ready!',
|
|
||||||
} as WorkerResponse);
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'INIT_COMPLETE',
|
|
||||||
} as WorkerResponse);
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
||||||
console.error('[PyodideWorker] Initialization error:', err);
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'INIT_ERROR',
|
|
||||||
error: errorMsg,
|
|
||||||
} as WorkerResponse);
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
isInitializing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
async function convertPesToPen(fileData: ArrayBuffer) {
|
|
||||||
if (!pyodide) {
|
|
||||||
throw new Error('Pyodide not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Register our JavaScript constants module for Python to import
|
|
||||||
pyodide.registerJsModule('js_emb_constants', jsEmbConstants);
|
|
||||||
|
|
||||||
// Convert to Uint8Array
|
|
||||||
const uint8Array = new Uint8Array(fileData);
|
|
||||||
|
|
||||||
// Write file to Pyodide virtual filesystem
|
|
||||||
const tempFileName = '/tmp/pattern.pes';
|
|
||||||
pyodide.FS.writeFile(tempFileName, uint8Array);
|
|
||||||
|
|
||||||
// Read the pattern using PyStitch (same logic as original converter)
|
|
||||||
const result = await pyodide.runPythonAsync(`
|
|
||||||
import pystitch
|
|
||||||
from pystitch.EmbConstant import STITCH, JUMP, TRIM, STOP, END, COLOR_CHANGE
|
|
||||||
from js_emb_constants import STITCH as JS_STITCH, MOVE as JS_MOVE, TRIM as JS_TRIM, END as JS_END
|
|
||||||
|
|
||||||
# Read the PES file
|
|
||||||
pattern = pystitch.read('${tempFileName}')
|
|
||||||
|
|
||||||
def map_cmd(pystitch_cmd):
|
|
||||||
"""Map PyStitch command to our JavaScript constant values
|
|
||||||
|
|
||||||
This ensures we have known, consistent values regardless of PyStitch's internal values.
|
|
||||||
Our JS constants use pyembroidery-style bitmask values:
|
|
||||||
STITCH = 0x00, MOVE/JUMP = 0x10, TRIM = 0x20, END = 0x100
|
|
||||||
"""
|
|
||||||
if pystitch_cmd == STITCH:
|
|
||||||
return JS_STITCH
|
|
||||||
elif pystitch_cmd == JUMP:
|
|
||||||
return JS_MOVE # PyStitch JUMP maps to our MOVE constant
|
|
||||||
elif pystitch_cmd == TRIM:
|
|
||||||
return JS_TRIM
|
|
||||||
elif pystitch_cmd == END:
|
|
||||||
return JS_END
|
|
||||||
else:
|
|
||||||
# For any other commands, preserve as bitmask
|
|
||||||
result = JS_STITCH
|
|
||||||
if pystitch_cmd & JUMP:
|
|
||||||
result |= JS_MOVE
|
|
||||||
if pystitch_cmd & TRIM:
|
|
||||||
result |= JS_TRIM
|
|
||||||
if pystitch_cmd & END:
|
|
||||||
result |= JS_END
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
#
|
|
||||||
# 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 = []
|
|
||||||
current_color = 0
|
|
||||||
|
|
||||||
for i, stitch in enumerate(pattern.stitches):
|
|
||||||
x, y, cmd = stitch
|
|
||||||
|
|
||||||
# Check for color change command
|
|
||||||
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
|
|
||||||
|
|
||||||
# PyStitch inserts duplicate stitches at the same coordinates during color changes
|
|
||||||
# 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)
|
|
||||||
stitches_with_colors.append([x, y, mapped_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',
|
|
||||||
'catalog_number': thread.catalog_number if hasattr(thread, 'catalog_number') else -1,
|
|
||||||
'brand': thread.brand if hasattr(thread, 'brand') else "",
|
|
||||||
'description': thread.description if hasattr(thread, 'description') else "",
|
|
||||||
'chart': thread.chart if hasattr(thread, 'chart') else ""
|
|
||||||
}
|
|
||||||
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(tempFileName);
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract stitches and validate
|
|
||||||
const stitches: number[][] = Array.from(
|
|
||||||
data.stitches as ArrayLike<ArrayLike<number>>
|
|
||||||
).map((stitch) => Array.from(stitch));
|
|
||||||
|
|
||||||
if (!stitches || stitches.length === 0) {
|
|
||||||
throw new Error('Invalid PES file or no stitches found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract thread data - preserve null values for unavailable metadata
|
|
||||||
const threads = (
|
|
||||||
data.threads as Array<{
|
|
||||||
color?: number;
|
|
||||||
hex?: string;
|
|
||||||
catalog_number?: number | string;
|
|
||||||
brand?: string;
|
|
||||||
description?: string;
|
|
||||||
chart?: string;
|
|
||||||
}>
|
|
||||||
).map((thread) => {
|
|
||||||
// Normalize catalog_number - can be string or number from PyStitch
|
|
||||||
const catalogNum = thread.catalog_number;
|
|
||||||
const normalizedCatalog =
|
|
||||||
catalogNum !== undefined &&
|
|
||||||
catalogNum !== null &&
|
|
||||||
catalogNum !== -1 &&
|
|
||||||
catalogNum !== '-1' &&
|
|
||||||
catalogNum !== ''
|
|
||||||
? String(catalogNum)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
color: thread.color ?? 0,
|
|
||||||
hex: thread.hex || '#000000',
|
|
||||||
catalogNumber: normalizedCatalog,
|
|
||||||
brand: thread.brand && thread.brand !== '' ? thread.brand : null,
|
|
||||||
description:
|
|
||||||
thread.description && thread.description !== ''
|
|
||||||
? thread.description
|
|
||||||
: null,
|
|
||||||
chart: thread.chart && thread.chart !== '' ? thread.chart : null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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[] = [];
|
|
||||||
|
|
||||||
// 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++) {
|
|
||||||
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
|
|
||||||
if (cmd === STITCH) {
|
|
||||||
minX = Math.min(minX, absX);
|
|
||||||
maxX = Math.max(maxX, absX);
|
|
||||||
minY = Math.min(minY, 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
|
|
||||||
// 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 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 stitch as 4 bytes: [X_low, X_high, Y_low, Y_high]
|
|
||||||
penStitches.push(
|
|
||||||
xEncoded & 0xff,
|
|
||||||
(xEncoded >> 8) & 0xff,
|
|
||||||
yEncoded & 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
|
|
||||||
if ((cmd & END) !== 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate unique colors from threads (threads represent color blocks, not unique colors)
|
|
||||||
const uniqueColors = threads.reduce(
|
|
||||||
(acc, thread, idx) => {
|
|
||||||
const existing = acc.find((c) => c.hex === thread.hex);
|
|
||||||
if (existing) {
|
|
||||||
existing.threadIndices.push(idx);
|
|
||||||
} else {
|
|
||||||
acc.push({
|
|
||||||
color: thread.color,
|
|
||||||
hex: thread.hex,
|
|
||||||
brand: thread.brand,
|
|
||||||
catalogNumber: thread.catalogNumber,
|
|
||||||
description: thread.description,
|
|
||||||
chart: thread.chart,
|
|
||||||
threadIndices: [idx],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[] as Array<{
|
|
||||||
color: number;
|
|
||||||
hex: string;
|
|
||||||
brand: string | null;
|
|
||||||
catalogNumber: string | null;
|
|
||||||
description: string | null;
|
|
||||||
chart: string | null;
|
|
||||||
threadIndices: number[];
|
|
||||||
}>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
self.postMessage({
|
|
||||||
type: 'CONVERT_COMPLETE',
|
|
||||||
data: {
|
|
||||||
stitches,
|
|
||||||
threads,
|
|
||||||
uniqueColors,
|
|
||||||
penData: penStitches, // Send as array (will be converted to Uint8Array in main thread)
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as WorkerResponse);
|
|
||||||
} catch (err) {
|
|
||||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
||||||
console.error('[PyodideWorker] Conversion error:', err);
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'CONVERT_ERROR',
|
|
||||||
error: errorMsg,
|
|
||||||
} as WorkerResponse);
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle messages from main thread
|
|
||||||
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
|
||||||
const message = event.data;
|
|
||||||
console.log('[PatternConverterWorker] Received message:', message.type);
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (message.type) {
|
|
||||||
case 'INITIALIZE':
|
|
||||||
console.log('[PatternConverterWorker] Starting initialization...');
|
|
||||||
await initializePyodide(message.pyodideIndexURL, message.pystitchWheelURL);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'CONVERT_PES':
|
|
||||||
console.log('[PatternConverterWorker] Starting PES conversion...');
|
|
||||||
await convertPesToPen(message.fileData);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.error('[PatternConverterWorker] Unknown message type:', message);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[PatternConverterWorker] Error handling message:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[PatternConverterWorker] Message handler registered');
|
|
||||||
9
vitest.config.ts
Normal file
9
vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue