第 6 组:源码链接、构建和文档约束闭环完成

This commit is contained in:
cnc
2026-06-01 06:11:08 +08:00
parent 1498d9830d
commit 4c55ab3435
43 changed files with 8414 additions and 636 deletions

View File

@@ -12,6 +12,7 @@
<div class="brand">CNC SIM</div>
<div class="status-strip">
<span id="wasmState" class="status alarm">WASM OFFLINE</span>
<span id="storageState" class="status">OPFS INIT</span>
<span id="modeState" class="status">MEM</span>
<span id="unitState" class="status">MM</span>
<span id="programState" class="status">RESET</span>
@@ -65,7 +66,6 @@
<span>BACKEND</span>
<select id="backendSelect">
<option value="linuxcnc-rs274">LINUXCNC</option>
<option value="smoke">SMOKE</option>
</select>
</label>
<span id="lineCount">0 LINES</span>

994
web/package-lock.json generated Normal file
View File

@@ -0,0 +1,994 @@
{
"name": "cnc-wasm-simulator-web",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cnc-wasm-simulator-web",
"dependencies": {
"three": "^0.164.0"
},
"devDependencies": {
"@types/three": "^0.164.0",
"typescript": "^5.4.5",
"vite": "^5.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
"integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
"integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
"integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
"integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
"integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
"integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
"integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
"integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
"integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
"integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
"integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
"integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
"integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
"integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
"integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
"integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
"integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
"integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
"integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
"integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
"integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
"integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
"integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
"integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
"integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true
},
"node_modules/@types/three": {
"version": "0.164.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.164.1.tgz",
"integrity": "sha512-dR/trWDhyaNqJV38rl1TonlCA9DpnX7OPYDWD81bmBGn/+uEc3+zNalFxQcV4FlPTeDBhCY3SFWKvK6EJwL88g==",
"dev": true,
"dependencies": {
"@tweenjs/tween.js": "~23.1.1",
"@types/stats.js": "*",
"@types/webxr": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
"integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.60.4",
"@rollup/rollup-android-arm64": "4.60.4",
"@rollup/rollup-darwin-arm64": "4.60.4",
"@rollup/rollup-darwin-x64": "4.60.4",
"@rollup/rollup-freebsd-arm64": "4.60.4",
"@rollup/rollup-freebsd-x64": "4.60.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
"@rollup/rollup-linux-arm-musleabihf": "4.60.4",
"@rollup/rollup-linux-arm64-gnu": "4.60.4",
"@rollup/rollup-linux-arm64-musl": "4.60.4",
"@rollup/rollup-linux-loong64-gnu": "4.60.4",
"@rollup/rollup-linux-loong64-musl": "4.60.4",
"@rollup/rollup-linux-ppc64-gnu": "4.60.4",
"@rollup/rollup-linux-ppc64-musl": "4.60.4",
"@rollup/rollup-linux-riscv64-gnu": "4.60.4",
"@rollup/rollup-linux-riscv64-musl": "4.60.4",
"@rollup/rollup-linux-s390x-gnu": "4.60.4",
"@rollup/rollup-linux-x64-gnu": "4.60.4",
"@rollup/rollup-linux-x64-musl": "4.60.4",
"@rollup/rollup-openbsd-x64": "4.60.4",
"@rollup/rollup-openharmony-arm64": "4.60.4",
"@rollup/rollup-win32-arm64-msvc": "4.60.4",
"@rollup/rollup-win32-ia32-msvc": "4.60.4",
"@rollup/rollup-win32-x64-gnu": "4.60.4",
"@rollup/rollup-win32-x64-msvc": "4.60.4",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/three": {
"version": "0.164.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.164.1.tgz",
"integrity": "sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w=="
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +1,383 @@
[
{"field":"config","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"Sim-5Axis Bridge Mill (xyzbcw)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"5axiskins coordinates=xyzbcwy","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"bridgemill","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"5axis","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"Sim-5Axis Bridge Mill (xyzbcw)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"Sim-5Axis Bridge Mill","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"5axiskins coordinates=xyzbcwy","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"5axiskins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/bridgemill","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"bridgemill","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"machine","value":"sim-xyzab-tdr-kins (switchkins)","m428":1,"m429":0,"m430":-1},
{"field":"kinematics","value":"xyzab_tdr_kins","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"table-dual-rotary","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab-tdr","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"sim-xyzab-tdr-kins (switchkins)","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"sim-xyzab-tdr-kins","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab_tdr_kins","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab_tdr","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab-tdr-kins","m428":1,"m429":0,"m430":-1},
{"field":"postguiHalFile","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"postguiHalFile","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"postgui_halfile","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"postgui_halfile","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"postgui_hal_file","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"postgui_hal_file","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/table-dual-rotary","m428":1,"m429":0,"m430":-1},
{"field":"remap","value":"table-dual-rotary","m428":1,"m429":0,"m430":-1},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"machine","value":"sim-xyzac-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"kinematics","value":"xyzac-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"table-rotary-tilting","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzac-trt","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzac-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzac-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzac-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzac-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"postguiHalFile","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"postguiHalFile","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"postgui_halfile","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"postgui_halfile","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"postgui_hal_file","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"postgui_hal_file","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"POSTGUI_HALFILE","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"POSTGUI_HALFILE","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting","m428":1,"m429":0,"m430":2},
{"field":"remap","value":"table-rotary-tilting","m428":1,"m429":0,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"machine","value":"sim-xyzbc-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"kinematics","value":"xyzbc-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzbc-trt","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzbc-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzbc-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzbc-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzbc-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"xyzacb-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"xyzacb_trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb-trsrn_twp","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb-trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb_trsrn","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"table-rotary_spindle-rotary-nutating","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"xyzbca-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"xyzbca_trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca-trsrn_twp","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca-trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca_trsrn","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"hexapod (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"genhexkins","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"hexapod-sim","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"hexapod","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"hexapod (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"genhexkins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/hexapod-sim","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"hexapod-sim","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"melfa (mm)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"genserkins","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"melfa-sim","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"melfa","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"melfa (mm)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"genserkins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/melfa-sim","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"melfa-sim","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"configPath","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"config_path","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"ini","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"iniFile","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"iniFileName","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"ini_file","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"machine","value":"millturn (mm)","m428":0,"m429":1,"m430":-1},
{"field":"kinematics","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"switchkins","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"switchkins","value":"millturn (mm)","m428":0,"m429":1,"m430":-1},
{"field":"halFile","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"halFile","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"halfile","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"halfile","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"hal_file","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"hal_file","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"HALFILE","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"HALFILE","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"postguiHalFile","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"postguiHalFile","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"postgui_halfile","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"postgui_halfile","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"postgui_hal_file","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"postgui_hal_file","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"remap","value":"configs/sim/axis/vismach/millturn","m428":0,"m429":1,"m430":-1},
{"field":"remap","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"PUMA (pumakins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"pumakins","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"PUMA (pumakins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"PUMA","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"pumakins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/puma","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"puma","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"puma_cube.ini (pumakins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma_cube","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma_cube.ini (pumakins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"puma560 (switchkins) (inch)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma560","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma560 (switchkins) (inch)","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma560_uvw","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"SCARA (genserkins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"scarakins coordinates=xyzcab","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"scara","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"SCARA (genserkins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"SCARA","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"scarakins coordinates=xyzcab","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"scarakins","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/scara","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"scara","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}
{"field":"configPath","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/qtaxis/non-trivial/scara","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/qtvcp_screens/non-trivial/scara","m428":0,"m429":1,"m430":2}
]

View File

@@ -2,6 +2,7 @@ import { createWasmSimulator } from "./wasm-core.js";
const elements = {
wasmState: document.querySelector("#wasmState"),
storageState: document.querySelector("#storageState"),
modeState: document.querySelector("#modeState"),
unitState: document.querySelector("#unitState"),
programState: document.querySelector("#programState"),
@@ -26,12 +27,82 @@ const elements = {
let simulatorPromise = null;
let lastEvents = [];
let isParsing = false;
let hasUserEditedProgram = false;
let saveProgramTimer = 0;
let saveProgramSequence = 0;
let saveProgramPromise = Promise.resolve();
let startupPromise = Promise.resolve();
const programPath = "programs/current.ngc";
// LinuxCNC source basis: interp_internal.hh defines RS274NGC_PARAMETER_FILE_NAME_DEFAULT.
const linuxCncParameterFileName = "rs274ngc.var";
const parameterPath = `parameters/${linuxCncParameterFileName}`;
const linuxCncBackend = "linuxcnc-rs274";
const opfsOptions = { required: true, mountPoint: "/cnc", workspacePath: "cnc-simulator" };
function setStatus(node, text, alarm = false) {
node.textContent = text;
node.classList.toggle("alarm", alarm);
}
function setRuntimeReady(simulator) {
setStatus(elements.wasmState, "WASM ONLINE");
if (simulator.fs?.opfs) {
const storageState = elements.storageState.textContent;
if (storageState === "OPFS SAVING" || storageState.startsWith("OPFS SAVED")) {
setStatus(elements.storageState, storageState);
} else {
setStatus(elements.storageState, "OPFS READY");
}
} else {
setStatus(elements.storageState, "OPFS OFF", true);
}
}
function setRuntimeUnavailable() {
setStatus(elements.wasmState, "WASM OFFLINE", true);
setStatus(elements.storageState, "OPFS OFF", true);
}
function setControlsEnabled(enabled) {
elements.parseBtn.disabled = !enabled;
elements.resetBtn.disabled = !enabled;
elements.holdBtn.disabled = !enabled;
elements.stopBtn.disabled = !enabled;
elements.backendSelect.disabled = !enabled;
}
function setStorageSaved(size) {
setStatus(elements.storageState, Number.isFinite(size) ? `OPFS SAVED ${size}B` : "OPFS SAVED");
}
function setProgramReady() {
if (elements.programState.textContent !== "ALARM") {
setStatus(elements.programState, "READY");
}
}
async function statIfExists(opfs, path) {
if (!(await opfs.exists(path))) {
return null;
}
return opfs.stat(path);
}
async function logWorkspaceState(opfs) {
const programStat = await statIfExists(opfs, programPath);
if (programStat?.kind === "file") {
log(`OPFS PROGRAM ${programStat.size}B`);
}
const parameterStat = await statIfExists(opfs, parameterPath);
if (parameterStat?.kind === "file") {
log(`OPFS PARAMETERS ${parameterStat.size}B`);
} else {
log("OPFS PARAMETERS PENDING");
}
}
function log(message, error = false) {
const line = document.createElement("div");
line.className = `alarm-line${error ? " error" : ""}`;
@@ -163,21 +234,63 @@ function updateLineCount() {
elements.lineCount.textContent = `${count} LINES`;
}
async function getOpfsWorkspace() {
const simulator = await getSimulator();
setRuntimeReady(simulator);
if (!simulator.fs.opfs) {
throw new Error("OPFS workspace is required for browser program parsing");
}
return simulator.fs.opfs;
}
async function getSimulator() {
if (!simulatorPromise) {
simulatorPromise = createWasmSimulator();
simulatorPromise = createWasmSimulator({
opfs: opfsOptions,
}).then((simulator) => {
setRuntimeReady(simulator);
return simulator;
}).catch((error) => {
simulatorPromise = null;
globalThis.__cncSimulatorForTest = null;
setRuntimeUnavailable();
throw error;
});
globalThis.__cncSimulatorForTest = simulatorPromise;
}
return simulatorPromise;
}
async function parseProgram() {
if (isParsing) {
return;
}
isParsing = true;
setControlsEnabled(false);
setStatus(elements.programState, "RUN");
let runtimeReady = false;
let savedProgramSize = null;
try {
await startupPromise.catch(() => {});
const simulator = await getSimulator();
setStatus(elements.wasmState, "WASM ONLINE");
const events = simulator.parse(elements.input.value, "linuxcnc", {
backend: elements.backendSelect.value,
});
runtimeReady = true;
setRuntimeReady(simulator);
const parseOptions = {
backend: linuxCncBackend,
};
if (!simulator.fs.opfs) {
throw new Error("OPFS workspace is required for browser program parsing");
}
elements.backendSelect.value = linuxCncBackend;
await saveProgramPromise.catch(() => {});
await simulator.fs.opfs.writeFile(programPath, elements.input.value);
const programStat = await simulator.fs.opfs.stat(programPath);
savedProgramSize = programStat.kind === "file" ? programStat.size : null;
setStorageSaved(savedProgramSize);
const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);
const parameterStat = await simulator.fs.opfs.stat(parameterPath);
setStorageSaved(parameterStat.size);
log(`OPFS PARAMETERS ${parameterStat.size}B`);
lastEvents = events;
drawToolpath(events);
@@ -193,9 +306,82 @@ async function parseProgram() {
setStatus(elements.programState, "END");
log(`OK ${events.length} EVENTS`);
} catch (error) {
setStatus(elements.wasmState, "WASM OFFLINE", true);
if (!runtimeReady) {
setRuntimeUnavailable();
}
if (savedProgramSize !== null) {
log(`OPFS PROGRAM SAVED ${savedProgramSize}B`);
}
setStatus(elements.programState, "ALARM", true);
log(error.message, true);
} finally {
isParsing = false;
setControlsEnabled(true);
}
}
async function saveProgramDraft() {
const sequence = ++saveProgramSequence;
saveProgramPromise = saveProgramPromise.catch(() => {}).then(async () => {
const opfs = await getOpfsWorkspace();
const program = elements.input.value;
setStatus(elements.storageState, "OPFS SAVING");
await opfs.writeFile(programPath, program);
if (sequence === saveProgramSequence) {
const stat = await opfs.stat(programPath);
setStorageSaved(stat.size);
setProgramReady();
}
});
try {
await saveProgramPromise;
} catch (error) {
setStatus(elements.storageState, "OPFS ALARM", true);
log(error.message, true);
}
}
function scheduleProgramSave() {
window.clearTimeout(saveProgramTimer);
saveProgramTimer = window.setTimeout(() => {
void saveProgramDraft();
}, 350);
}
function flushProgramSave() {
window.clearTimeout(saveProgramTimer);
if (hasUserEditedProgram) {
void saveProgramDraft();
}
}
async function restoreProgram() {
try {
const opfs = await getOpfsWorkspace();
if (!(await opfs.exists(programPath))) {
await opfs.writeFile(programPath, elements.input.value);
const stat = await opfs.stat(programPath);
setStorageSaved(stat.size);
setProgramReady();
log(`OPFS PROGRAM CREATED ${stat.size}B`);
await logWorkspaceState(opfs);
return;
}
const program = new TextDecoder().decode(await opfs.readFile(programPath));
if (hasUserEditedProgram) {
return;
}
elements.input.value = program;
updateLineCount();
const stat = await opfs.stat(programPath);
setStorageSaved(stat.size);
setProgramReady();
log("OPFS PROGRAM RESTORED");
await logWorkspaceState(opfs);
} catch (error) {
if (error?.name !== "NotFoundError") {
log(error.message, true);
}
}
}
@@ -205,6 +391,7 @@ document.querySelectorAll("[data-mode]").forEach((button) => {
});
});
elements.backendSelect.value = linuxCncBackend;
elements.parseBtn.addEventListener("click", parseProgram);
elements.resetBtn.addEventListener("click", () => {
lastEvents = [];
@@ -214,8 +401,32 @@ elements.resetBtn.addEventListener("click", () => {
});
elements.holdBtn.addEventListener("click", () => setStatus(elements.programState, "HOLD"));
elements.stopBtn.addEventListener("click", () => setStatus(elements.programState, "STOP", true));
elements.input.addEventListener("input", updateLineCount);
elements.input.addEventListener("input", () => {
updateLineCount();
hasUserEditedProgram = true;
setStatus(elements.programState, "EDIT");
scheduleProgramSave();
});
document.addEventListener("keydown", (event) => {
if (!(event.ctrlKey || event.metaKey) || event.altKey) {
return;
}
if (event.key === "s") {
event.preventDefault();
void saveProgramDraft();
} else if (event.key === "Enter") {
event.preventDefault();
void parseProgram();
}
});
window.addEventListener("resize", resizeCanvas);
window.addEventListener("pagehide", flushProgramSave);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
flushProgramSave();
}
});
updateLineCount();
resizeCanvas();
startupPromise = restoreProgram();

View File

@@ -84,8 +84,19 @@ export type CncParseOptions = {
config?: string;
configPath?: string;
config_path?: string;
ini?: string;
iniFile?: string;
iniFileName?: string;
ini_file?: string;
ini_file_name?: string;
INI_FILE_NAME?: string;
halFile?: string;
halfile?: string;
hal_file?: string;
postguiHalFile?: string;
postgui_halfile?: string;
postgui_hal_file?: string;
POSTGUI_HALFILE?: string;
kinematics?: string;
trt?: "xyzbc" | "xyzac" | string;
pivotLength?: number;

View File

@@ -2,13 +2,65 @@ import type { CncDialect, CncEvent, CncParseOptions } from "./index";
export type WasmSimulator = {
parse(program: string, dialect?: CncDialect, options?: CncParseOptions): CncEvent[];
parseFile(path: string, dialect?: CncDialect, options?: CncParseOptions): Promise<CncEvent[]>;
parseWithParameterFile(
program: string,
parameterPath: string,
dialect?: CncDialect,
options?: CncParseOptions,
): Promise<CncEvent[]>;
parseFileWithParameterFile(
path: string,
parameterPath: string,
dialect?: CncDialect,
options?: CncParseOptions,
): Promise<CncEvent[]>;
dispose(): void;
fs: {
opfs: WasmOpfsWorkspace | null;
module: unknown;
};
};
export type WasmOpfsWorkspace = {
mountPoint: string;
workspacePath: string;
resolvePath(path: string): string;
readFile(path: string): Promise<Uint8Array>;
readDirectory(path: string): Promise<string[]>;
persistFile(path: string): Promise<Uint8Array>;
persistDirectory(path: string): Promise<string[]>;
writeFile(path: string, data: Uint8Array | string): Promise<void>;
copyFile(fromPath: string, toPath: string): Promise<Uint8Array>;
moveFile(fromPath: string, toPath: string): Promise<void>;
exists(path: string): Promise<boolean>;
stat(path: string): Promise<WasmOpfsEntryStat>;
loadParameterFile(path: string): Promise<Uint8Array | null>;
persistParameterFile(path: string): Promise<Uint8Array>;
removeFile(path: string): Promise<void>;
removeDirectory(path: string): Promise<void>;
clear(): Promise<void>;
};
export type WasmOpfsEntryStat =
| {
kind: "file";
size: number;
}
| {
kind: "directory";
size: null;
};
export type WasmModuleOptions = {
locateFile?: (file: string) => string;
print?: (text: string) => void;
printErr?: (text: string) => void;
opfs?: false | {
required?: boolean;
mountPoint?: string;
workspacePath?: string;
};
};
export function createWasmSimulator(moduleOptions?: WasmModuleOptions): Promise<WasmSimulator>;

View File

@@ -41,6 +41,486 @@ const EVENT_OFFSETS = {
reserved: 268,
};
const DEFAULT_OPFS_MOUNT_POINT = "/cnc";
const LINUXCNC_DEFAULT_PARAMETER_FILE = "rs274ngc.var";
const LINUXCNC_PARAMETER_SCRATCH_FILES = [
LINUXCNC_DEFAULT_PARAMETER_FILE,
`${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`,
`${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`,
];
function isOpfsAvailable() {
return Boolean(globalThis.navigator?.storage?.getDirectory);
}
function assertRelativePath(path) {
if (!path || typeof path !== "string") {
throw new Error("OPFS path must be a non-empty relative path");
}
if (
path.startsWith("/") ||
path.includes("\\") ||
path.includes("\0") ||
path.split("/").some((part) => part === "" || part === "." || part === "..")
) {
throw new Error(`OPFS path must stay inside the CNC workspace: ${path}`);
}
}
function assertWorkspacePath(path) {
assertRelativePath(path);
}
function assertMountPoint(path) {
if (!path || typeof path !== "string") {
throw new Error("OPFS mount point must be a non-empty absolute path");
}
const parts = path.split("/").slice(1);
if (
!path.startsWith("/") ||
path.includes("\\") ||
path.includes("\0") ||
parts.some((part) => part === "" || part === "." || part === "..")
) {
throw new Error(`OPFS mount point must stay inside the Emscripten filesystem: ${path}`);
}
}
async function getOpfsDirectoryHandle(path, create) {
if (!isOpfsAvailable()) {
throw new Error("OPFS is not available in this browser context");
}
let directory = await globalThis.navigator.storage.getDirectory();
for (const part of path.split("/").filter(Boolean)) {
directory = await directory.getDirectoryHandle(part, { create });
}
return directory;
}
async function readOpfsFile(workspacePath, relativePath) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const fileName = parts.pop();
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
const file = await directory.getFileHandle(fileName, { create: false });
return new Uint8Array(await (await file.getFile()).arrayBuffer());
}
async function writeOpfsFile(workspacePath, relativePath, data) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const fileName = parts.pop();
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), true);
const file = await directory.getFileHandle(fileName, { create: true });
const writable = await file.createWritable();
try {
await writable.truncate(0);
await writable.write(data);
await writable.close();
} catch (error) {
try {
await writable.abort();
} catch (_abortError) {
// Preserve the original OPFS write failure.
}
throw error;
}
}
async function removeOpfsEntry(workspacePath, relativePath, recursive) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const entryName = parts.pop();
try {
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
await directory.removeEntry(entryName, { recursive });
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
}
}
async function opfsEntryExists(workspacePath, relativePath) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const entryName = parts.pop();
try {
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
try {
await directory.getFileHandle(entryName, { create: false });
return true;
} catch (fileError) {
if (fileError?.name !== "NotFoundError" && fileError?.name !== "TypeMismatchError") {
throw fileError;
}
}
try {
await directory.getDirectoryHandle(entryName, { create: false });
return true;
} catch (directoryError) {
if (directoryError?.name !== "NotFoundError" && directoryError?.name !== "TypeMismatchError") {
throw directoryError;
}
}
return false;
} catch (error) {
if (error?.name === "NotFoundError") {
return false;
}
throw error;
}
}
async function statOpfsEntry(workspacePath, relativePath) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const entryName = parts.pop();
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
try {
const file = await directory.getFileHandle(entryName, { create: false });
return { kind: "file", size: (await file.getFile()).size };
} catch (fileError) {
if (fileError?.name !== "TypeMismatchError") {
throw fileError;
}
}
await directory.getDirectoryHandle(entryName, { create: false });
return { kind: "directory", size: null };
}
async function clearOpfsWorkspace(workspacePath) {
const parts = workspacePath.split("/").filter(Boolean);
if (parts.length === 0) {
return;
}
const entryName = parts.pop();
const directory = await getOpfsDirectoryHandle(parts.join("/"), true);
try {
await directory.removeEntry(entryName, { recursive: true });
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
}
await getOpfsDirectoryHandle(workspacePath, true);
}
async function replaceOpfsDirectory(workspacePath, relativePath) {
assertRelativePath(relativePath);
await removeOpfsEntry(workspacePath, relativePath, true);
const directory = await getOpfsDirectoryHandle([workspacePath, relativePath].filter(Boolean).join("/"), true);
for await (const [name] of directory.entries()) {
await directory.removeEntry(name, { recursive: true });
}
}
async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "") {
assertRelativePath(relativePath);
const directory = await getOpfsDirectoryHandle([workspacePath, relativePath, relativeRoot].filter(Boolean).join("/"), false);
const files = [];
const directories = [];
for await (const [name, handle] of directory.entries()) {
const path = relativeRoot ? `${relativeRoot}/${name}` : name;
if (handle.kind === "directory") {
directories.push(path);
const child = await readOpfsDirectoryTree(workspacePath, relativePath, path);
directories.push(...child.directories);
files.push(...child.files);
} else if (handle.kind === "file") {
files.push(path);
}
}
return {
directories: directories.sort(),
files: files.sort(),
};
}
function ensureWasmDirectory(module, path) {
const parts = path.split("/").filter(Boolean);
let current = "";
for (const part of parts) {
current += `/${part}`;
try {
module.FS.mkdir(current);
} catch (error) {
if (error?.errno !== 20) {
throw error;
}
}
}
}
function readWasmDirectoryTree(module, rootPath, relativeRoot = "") {
const files = [];
const directories = [];
for (const entry of module.FS.readdir(rootPath)) {
if (entry === "." || entry === "..") {
continue;
}
const path = `${rootPath}/${entry}`;
const relativePath = relativeRoot ? `${relativeRoot}/${entry}` : entry;
const mode = module.FS.stat(path).mode;
if (module.FS.isDir(mode)) {
directories.push(relativePath);
const child = readWasmDirectoryTree(module, path, relativePath);
directories.push(...child.directories);
files.push(...child.files);
} else if (module.FS.isFile(mode)) {
files.push(relativePath);
}
}
return {
directories: directories.sort(),
files: files.sort(),
};
}
function removeWasmPath(module, path, recursive) {
let mode;
try {
mode = module.FS.stat(path).mode;
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
return;
}
if (module.FS.isDir(mode)) {
if (!recursive) {
module.FS.rmdir(path);
return;
}
for (const entry of module.FS.readdir(path)) {
if (entry !== "." && entry !== "..") {
removeWasmPath(module, `${path}/${entry}`, true);
}
}
module.FS.rmdir(path);
} else {
module.FS.unlink(path);
}
}
function wasmPathExists(module, path) {
try {
module.FS.stat(path);
return true;
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
return false;
}
}
function readFirstExistingWasmFile(module, paths) {
for (const path of paths) {
try {
return module.FS.readFile(path);
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
}
}
return null;
}
function writeWasmFileReplacingPath(module, path, data) {
removeWasmPath(module, path, true);
const parentIndex = path.lastIndexOf("/");
if (parentIndex > 0) {
ensureWasmDirectory(module, path.slice(0, parentIndex));
}
module.FS.writeFile(path, data);
}
function cleanupLinuxCncParameterFiles(module) {
for (const path of LINUXCNC_PARAMETER_SCRATCH_FILES) {
removeWasmPath(module, path, true);
removeWasmPath(module, `/${path}`, true);
}
}
function installOpfsWorkspace(module, options) {
const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false);
if (opfsOptions === false) {
if (isOpfsAvailable()) {
throw new Error("OPFS cannot be disabled in browser contexts with OPFS support");
}
return null;
}
if (!module.FS) {
throw new Error("cnc_sim.js was not built with Emscripten FS export");
}
if (!isOpfsAvailable()) {
if (opfsOptions.required) {
throw new Error("OPFS is only available in browser contexts");
}
return null;
}
const mountPoint = opfsOptions.mountPoint ?? DEFAULT_OPFS_MOUNT_POINT;
const workspacePath = opfsOptions.workspacePath ?? "cnc-simulator";
assertMountPoint(mountPoint);
assertWorkspacePath(workspacePath);
ensureWasmDirectory(module, mountPoint);
return {
mountPoint,
workspacePath,
resolvePath(path) {
assertRelativePath(path);
return `${mountPoint}/${path}`;
},
async readFile(path) {
const data = await readOpfsFile(workspacePath, path);
const wasmPath = this.resolvePath(path);
writeWasmFileReplacingPath(module, wasmPath, data);
return data;
},
async readDirectory(path) {
assertRelativePath(path);
let tree;
try {
tree = await readOpfsDirectoryTree(workspacePath, path);
} catch (error) {
if (error?.name === "NotFoundError") {
removeWasmPath(module, this.resolvePath(path), true);
}
throw error;
}
const wasmPath = this.resolvePath(path);
removeWasmPath(module, wasmPath, true);
ensureWasmDirectory(module, wasmPath);
for (const directory of tree.directories) {
ensureWasmDirectory(module, `${wasmPath}/${directory}`);
}
for (const file of tree.files) {
await this.readFile(`${path}/${file}`);
}
return tree.files;
},
async persistFile(path) {
const wasmPath = this.resolvePath(path);
const data = module.FS.readFile(wasmPath);
await writeOpfsFile(workspacePath, path, data);
return data;
},
async persistDirectory(path) {
assertRelativePath(path);
const wasmPath = this.resolvePath(path);
const tree = readWasmDirectoryTree(module, wasmPath);
await replaceOpfsDirectory(workspacePath, path);
for (const directory of tree.directories) {
await getOpfsDirectoryHandle([workspacePath, path, directory].filter(Boolean).join("/"), true);
}
for (const file of tree.files) {
await this.persistFile(`${path}/${file}`);
}
return tree.files;
},
async writeFile(path, data) {
await writeOpfsFile(workspacePath, path, data);
const wasmPath = this.resolvePath(path);
writeWasmFileReplacingPath(module, wasmPath, data);
},
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() links the
// current parameter file to filename + ".bak" before replacing it.
async copyFile(fromPath, toPath) {
assertRelativePath(fromPath);
assertRelativePath(toPath);
const data = await readOpfsFile(workspacePath, fromPath);
await writeOpfsFile(workspacePath, toPath, data);
const fromWasmPath = this.resolvePath(fromPath);
const toWasmPath = this.resolvePath(toPath);
writeWasmFileReplacingPath(module, fromWasmPath, data);
writeWasmFileReplacingPath(module, toWasmPath, data);
return data;
},
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes a
// temporary filename + ".new" and renames it over the parameter file.
async moveFile(fromPath, toPath) {
assertRelativePath(fromPath);
assertRelativePath(toPath);
if (fromPath === toPath) {
return;
}
const data = await readOpfsFile(workspacePath, fromPath);
await writeOpfsFile(workspacePath, toPath, data);
await removeOpfsEntry(workspacePath, fromPath, false);
const fromWasmPath = this.resolvePath(fromPath);
const toWasmPath = this.resolvePath(toPath);
writeWasmFileReplacingPath(module, toWasmPath, data);
removeWasmPath(module, fromWasmPath, false);
},
async exists(path) {
return opfsEntryExists(workspacePath, path);
},
async stat(path) {
return statOpfsEntry(workspacePath, path);
},
// LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the
// configured parameter file, and save_parameters() writes filename + ".new"
// before replacing the main file and managing filename + ".bak".
async loadParameterFile(path) {
assertRelativePath(path);
try {
const data = await readOpfsFile(workspacePath, path);
writeWasmFileReplacingPath(module, LINUXCNC_DEFAULT_PARAMETER_FILE, data);
writeWasmFileReplacingPath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}`, data);
return data;
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
cleanupLinuxCncParameterFiles(module);
return null;
}
},
async persistParameterFile(path) {
assertRelativePath(path);
const data = readFirstExistingWasmFile(module, [
LINUXCNC_DEFAULT_PARAMETER_FILE,
`/${LINUXCNC_DEFAULT_PARAMETER_FILE}`,
]);
if (data === null) {
throw new Error("LinuxCNC parameter file was not produced by the interpreter");
}
await writeOpfsFile(workspacePath, path, data);
await removeOpfsEntry(workspacePath, `${path}.new`, false);
removeWasmPath(module, `${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, false);
removeWasmPath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, false);
const backupPath = `${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`;
const backupData = readFirstExistingWasmFile(module, [backupPath, `/${backupPath}`]);
if (backupData !== null) {
await writeOpfsFile(workspacePath, `${path}.bak`, backupData);
} else {
await removeOpfsEntry(workspacePath, `${path}.bak`, false);
}
return data;
},
async removeFile(path) {
await removeOpfsEntry(workspacePath, path, false);
removeWasmPath(module, this.resolvePath(path), false);
},
async removeDirectory(path) {
await removeOpfsEntry(workspacePath, path, true);
removeWasmPath(module, this.resolvePath(path), true);
},
async clear() {
await clearOpfsWorkspace(workspacePath);
for (const entry of module.FS.readdir(mountPoint)) {
if (entry !== "." && entry !== "..") {
removeWasmPath(module, `${mountPoint}/${entry}`, true);
}
}
},
};
}
function readPose(view, offset) {
return {
x: view.getFloat64(offset + 0, true),
@@ -82,12 +562,21 @@ async function loadModuleFactory() {
}
export async function createWasmSimulator(moduleOptions = {}) {
if (moduleOptions.opfs === false && isOpfsAvailable()) {
throw new Error("OPFS cannot be disabled in browser contexts with OPFS support");
}
const hasOpfsOptions = Object.prototype.hasOwnProperty.call(moduleOptions, "opfs");
const emscriptenModuleOptions = hasOpfsOptions
? (({ opfs: _opfs, ...rest }) => rest)(moduleOptions)
: moduleOptions;
const createModule = await loadModuleFactory();
if (!createModule) {
throw new Error("cnc_sim.js did not export createCncSimModule");
}
const module = await createModule(moduleOptions);
const module = hasOpfsOptions ? await createModule(emscriptenModuleOptions) : await createModule(moduleOptions);
const opfs = installOpfsWorkspace(module, moduleOptions);
const create = module.cwrap("cnc_sim_create", "number", []);
const destroy = module.cwrap("cnc_sim_destroy", null, ["number"]);
const reset = module.cwrap("cnc_sim_reset", null, ["number"]);
@@ -100,9 +589,9 @@ export async function createWasmSimulator(moduleOptions = {}) {
const handle = create();
let callbackPtr = 0;
return {
parse(program, dialect = "linuxcnc", options = {}) {
const events = [];
const parseText = (program, dialect = "linuxcnc", options = {}, parseOptions = {}) => {
const events = [];
try {
reset(handle);
setDialect(handle, DIALECT[dialect] ?? DIALECT.linuxcnc);
@@ -117,8 +606,19 @@ export async function createWasmSimulator(moduleOptions = {}) {
...(options.config ? { config: options.config } : {}),
...(options.configPath ? { configPath: options.configPath } : {}),
...(options.config_path ? { config_path: options.config_path } : {}),
...(options.ini ? { ini: options.ini } : {}),
...(options.iniFile ? { iniFile: options.iniFile } : {}),
...(options.iniFileName ? { iniFileName: options.iniFileName } : {}),
...(options.ini_file ? { ini_file: options.ini_file } : {}),
...(options.ini_file_name ? { ini_file_name: options.ini_file_name } : {}),
...(options.INI_FILE_NAME ? { INI_FILE_NAME: options.INI_FILE_NAME } : {}),
...(options.halFile ? { halFile: options.halFile } : {}),
...(options.halfile ? { halfile: options.halfile } : {}),
...(options.hal_file ? { hal_file: options.hal_file } : {}),
...(options.postguiHalFile ? { postguiHalFile: options.postguiHalFile } : {}),
...(options.postgui_halfile ? { postgui_halfile: options.postgui_halfile } : {}),
...(options.postgui_hal_file ? { postgui_hal_file: options.postgui_hal_file } : {}),
...(options.POSTGUI_HALFILE ? { POSTGUI_HALFILE: options.POSTGUI_HALFILE } : {}),
...(options.kinematics ? { kinematics: options.kinematics } : {}),
...(options.trt ? { trt: options.trt } : {}),
...(options.pivotLength !== undefined ? { pivotLength: options.pivotLength } : {}),
@@ -135,9 +635,13 @@ export async function createWasmSimulator(moduleOptions = {}) {
});
const configBytes = module.lengthBytesUTF8(config) + 1;
const configPtr = module._malloc(configBytes);
module.stringToUTF8(config, configPtr, configBytes);
const configRc = loadConfig(handle, configPtr, configBytes - 1);
module._free(configPtr);
let configRc;
try {
module.stringToUTF8(config, configPtr, configBytes);
configRc = loadConfig(handle, configPtr, configBytes - 1);
} finally {
module._free(configPtr);
}
if (configRc !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
@@ -153,14 +657,59 @@ export async function createWasmSimulator(moduleOptions = {}) {
const bytes = module.lengthBytesUTF8(program) + 1;
const ptr = module._malloc(bytes);
module.stringToUTF8(program, ptr, bytes);
const rc = parseProgram(handle, ptr, bytes - 1);
module._free(ptr);
let rc;
try {
module.stringToUTF8(program, ptr, bytes);
rc = parseProgram(handle, ptr, bytes - 1);
} finally {
module._free(ptr);
}
if (rc !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
return events;
} finally {
if (opfs && !parseOptions.keepLinuxCncParameterFiles) {
cleanupLinuxCncParameterFiles(module);
}
}
};
return {
parse(program, dialect = "linuxcnc", options = {}) {
return parseText(program, dialect, options);
},
async parseFile(path, dialect = "linuxcnc", options = {}) {
if (!opfs) {
throw new Error("OPFS workspace is not available for program file parsing");
}
const program = new TextDecoder().decode(await opfs.readFile(path));
return parseText(program, dialect, options);
},
async parseWithParameterFile(program, parameterPath, dialect = "linuxcnc", options = {}) {
if (!opfs) {
throw new Error("OPFS workspace is not available for parameter file persistence");
}
cleanupLinuxCncParameterFiles(module);
await opfs.loadParameterFile(parameterPath);
try {
const events = parseText(program, dialect, options, { keepLinuxCncParameterFiles: true });
await opfs.persistParameterFile(parameterPath);
return events;
} finally {
cleanupLinuxCncParameterFiles(module);
}
},
async parseFileWithParameterFile(path, parameterPath, dialect = "linuxcnc", options = {}) {
if (!opfs) {
throw new Error("OPFS workspace is not available for program and parameter file parsing");
}
const program = new TextDecoder().decode(await opfs.readFile(path));
return this.parseWithParameterFile(program, parameterPath, dialect, options);
},
dispose() {
@@ -170,5 +719,10 @@ export async function createWasmSimulator(moduleOptions = {}) {
}
destroy(handle);
},
fs: {
opfs,
module,
},
};
}

View File

@@ -0,0 +1,66 @@
export async function runBrowserAppOpfsSections(context) {
const {
browserHasOpfs,
createHiddenAppFrame,
disposeAppFrame,
expectIncludes,
expectText,
getAppFrameSimulator,
parseAppFrameProgram,
runSection,
waitFor,
waitForAppAlarm,
waitForAppControls,
} = context;
if (!browserHasOpfs()) {
return;
}
await runSection("browser app opfs save", async () => {
const appFrame = await createHiddenAppFrame();
try {
await waitForAppControls(appFrame, "app frame did not load CNC controls");
parseAppFrameProgram(appFrame, "G21 G90\nG0 X1\nG28.1\nM30\n");
await waitForAppAlarm(appFrame, "OK ", "app frame did not parse program");
const appSimulator = await getAppFrameSimulator(appFrame, "app frame did not create OPFS simulator workspace");
const savedProgram = new TextDecoder().decode(await appSimulator.fs.opfs.readFile("programs/current.ngc"));
expectText(savedProgram, "G21 G90\nG0 X1\nG28.1\nM30\n",
"app frame did not persist current program into OPFS");
const savedParameterFile = new TextDecoder().decode(await appSimulator.fs.opfs.readFile("parameters/rs274ngc.var"));
expectIncludes(savedParameterFile, "5161\t1.000000",
"app frame did not persist LinuxCNC parameter state into OPFS");
} finally {
await disposeAppFrame(appFrame);
}
});
await runSection("browser app opfs restore", async () => {
const restoredAppFrame = await createHiddenAppFrame();
try {
await waitForAppControls(restoredAppFrame, "restored app frame did not load CNC controls");
await waitFor(() => {
const restoredInput = restoredAppFrame.contentDocument?.querySelector("#programInput");
return restoredInput?.value === "G21 G90\nG0 X1\nG28.1\nM30\n";
}, "app frame did not restore current program from OPFS after reload");
await waitForAppAlarm(
restoredAppFrame,
"OPFS PROGRAM RESTORED",
"app frame did not report OPFS program restore after reload",
);
await getAppFrameSimulator(restoredAppFrame, "restored app frame did not create OPFS simulator workspace");
parseAppFrameProgram(
restoredAppFrame,
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X12\nO10 endif\nM30\n",
);
await waitForAppAlarm(
restoredAppFrame,
"OK ",
"restored app frame did not parse program with reloaded parameter state",
);
await waitFor(
() => restoredAppFrame.contentDocument?.querySelector("#axisX")?.value === "12.000",
"app frame did not reload LinuxCNC parameter state from OPFS after reload",
);
} finally {
await disposeAppFrame(restoredAppFrame);
}
});
}

View File

@@ -0,0 +1,176 @@
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const linuxCncParameterScratchFiles = ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"];
export function createBrowserSmokeHarness(result) {
const sectionResults = [];
const near = (actual, expected) => Math.abs(actual - expected) < 1e-6;
const waitFor = async (predicate, message) => {
for (let i = 0; i < 50; i += 1) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(message);
};
const runSection = async (name, action) => {
const sectionNumber = sectionResults.length + 1;
result.textContent = `running: ${sectionNumber}: ${name}`;
try {
await action();
sectionResults.push(name);
} catch (error) {
const detail = error && error.stack ? error.stack : error;
throw new Error(`section "${name}" failed: ${detail}`);
}
};
const completedMessage = () => `browser wasm smoke passed (${sectionResults.length} sections)`;
const browserHasOpfs = () => navigator.storage && navigator.storage.getDirectory;
const expectEvent = (events, predicate, message) => {
if (!events.some(predicate)) {
throw new Error(message);
}
};
const expectNoEvent = (events, predicate, message) => {
if (events.some(predicate)) {
throw new Error(message);
}
};
const expectText = (actual, expected, message) => {
if (actual !== expected) {
throw new Error(message);
}
};
const expectIncludes = (actual, expected, message) => {
if (!actual.includes(expected)) {
throw new Error(message);
}
};
const expectFileList = (actual, expected, message) => {
if (
actual.length !== expected.length ||
expected.some((entry) => !actual.includes(entry))
) {
throw new Error(`${message}: ${actual.join(",")}`);
}
};
const expectErrorContaining = async (action, expectedText, message) => {
try {
await action();
throw new Error(message);
} catch (error) {
if (!String(error?.message ?? error).includes(expectedText)) {
throw error;
}
}
};
const expectMissingOpfsFile = async (action, message) => {
try {
await action();
throw new Error(message);
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
}
};
const expectMissingWasmPath = (action, message) => {
try {
action();
throw new Error(message);
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
}
};
const expectWasmFilesMissing = (fs, paths, messageForPath) => {
for (const wasmPath of paths) {
expectMissingWasmPath(
() => fs.readFile(wasmPath),
messageForPath(wasmPath),
);
}
};
const decodeBytes = (bytes) => textDecoder.decode(bytes);
const encodeText = (text) => textEncoder.encode(text);
const readOpfsText = async (simulator, path) => decodeBytes(await simulator.fs.opfs.readFile(path));
const readWasmText = (simulator, path) => decodeBytes(simulator.fs.module.FS.readFile(path));
const writeWasmText = (simulator, path, text) => simulator.fs.module.FS.writeFile(path, encodeText(text));
const removeWasmPathIfExists = (simulator, path) => {
try {
simulator.fs.module.FS.unlink(path);
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
}
};
const expectOpfsText = async (simulator, path, expected, message) => {
expectText(await readOpfsText(simulator, path), expected, message);
};
const expectWasmText = (simulator, path, expected, message) => {
expectText(readWasmText(simulator, path), expected, message);
};
const expectOpfsExists = async (simulator, path, message) => {
if (!(await simulator.fs.opfs.exists(path))) {
throw new Error(message);
}
};
const expectOpfsMissing = async (simulator, path, message) => {
if (await simulator.fs.opfs.exists(path)) {
throw new Error(message);
}
};
const expectOpfsFileStat = async (simulator, path, expectedSize, message) => {
const stat = await simulator.fs.opfs.stat(path);
if (stat.kind !== "file" || stat.size !== expectedSize) {
throw new Error(message);
}
};
const expectOpfsDirectoryStat = async (simulator, path, message) => {
const stat = await simulator.fs.opfs.stat(path);
if (stat.kind !== "directory" || stat.size !== null) {
throw new Error(message);
}
};
const expectLinuxCncParameterScratchMissing = (simulator, messageForPath) => {
for (const parameterPath of linuxCncParameterScratchFiles) {
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(parameterPath),
messageForPath(parameterPath),
);
}
};
return {
browserHasOpfs,
completedMessage,
decodeBytes,
encodeText,
expectErrorContaining,
expectEvent,
expectFileList,
expectIncludes,
expectLinuxCncParameterScratchMissing,
expectMissingOpfsFile,
expectMissingWasmPath,
expectNoEvent,
expectOpfsDirectoryStat,
expectOpfsExists,
expectOpfsFileStat,
expectOpfsMissing,
expectOpfsText,
expectText,
expectWasmFilesMissing,
expectWasmText,
near,
readOpfsText,
readWasmText,
removeWasmPathIfExists,
runSection,
waitFor,
writeWasmText,
};
}

View File

@@ -0,0 +1,72 @@
export async function runLinuxCncBrowserSections(context, simulator) {
const {
expectEvent,
expectMissingWasmPath,
near,
runSection,
} = context;
await runSection("linuxcnc browser basic parse", async () => {
const events = simulator.parse("G21 G90\nG0 X0\nG1 X5 F100\nM30\n", "linuxcnc", {
backend: "linuxcnc-rs274",
});
expectEvent(events, (event) => event.type === "linear-feed" && near(event.end.x, 5),
"missing expected LinuxCNC WASM browser linear-feed event");
expectEvent(events, (event) => event.type === "program-end",
"missing expected LinuxCNC WASM browser program-end event");
});
let configCases = [];
await runSection("linuxcnc generated switchkins cases load", async () => {
const configCaseResponse = await fetch("/public/linuxcnc_switchkins_remap_config_cases.json");
if (!configCaseResponse.ok) {
throw new Error(`failed to load LinuxCNC switchkins config cases: ${configCaseResponse.status}`);
}
configCases = await configCaseResponse.json();
if (!Array.isArray(configCases) || configCases.length === 0) {
throw new Error("missing LinuxCNC switchkins config cases");
}
});
for (const configCase of configCases) {
await runSection(`linuxcnc generated switchkins ${configCase.field}=${configCase.value}`, async () => {
const options = {
backend: "linuxcnc-rs274",
[configCase.field]: configCase.value,
};
const program = configCase.m430 >= 0 ? "M428\nM429\nM430\n" : "M428\nM429\n";
const configEvents = simulator.parse(program, "linuxcnc", options);
for (const [line, kinstype] of [
[1, configCase.m428],
[2, configCase.m429],
...(configCase.m430 >= 0 ? [[3, configCase.m430]] : []),
]) {
expectEvent(configEvents,
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
`missing LinuxCNC WASM browser generated ${configCase.field}=${configCase.value} line ${line} switchkins type ${kinstype}`,
);
}
});
}
await runSection("linuxcnc browser explicit scara switchkins", async () => {
const switchEvents = simulator.parse("M428\nM429\nM430\n", "linuxcnc", {
backend: "linuxcnc-rs274",
switchkins: "scara",
});
for (const [line, kinstype] of [
[1, 0],
[2, 1],
[3, 2],
]) {
expectEvent(switchEvents,
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
`missing LinuxCNC WASM browser M${427 + line} switchkins type ${kinstype}`,
);
}
});
await runSection("linuxcnc browser parse parameter scratch cleanup", async () => {
for (const parameterPath of ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]) {
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(parameterPath),
`browser parse left LinuxCNC parameter state outside OPFS: ${parameterPath}`,
);
}
});
}

View File

@@ -0,0 +1,114 @@
export async function runBrowserOpfsBasicSections(context, simulator) {
const {
expectErrorContaining,
expectEvent,
expectMissingWasmPath,
expectText,
near,
runSection,
withSmokeSimulator,
} = context;
await runSection("opfs workspace clear and healthcheck", async () => {
if (!simulator.fs.opfs) {
throw new Error("expected OPFS workspace to be available in browser smoke");
}
await simulator.fs.opfs.clear();
if (simulator.fs.opfs.workspacePath !== "cnc-simulator-smoke") {
throw new Error("unexpected OPFS workspace path");
}
await simulator.fs.opfs.writeFile("healthcheck.ngc", "G21 G90\n");
if (!(await simulator.fs.opfs.exists("healthcheck.ngc"))) {
throw new Error("OPFS exists did not find a persisted file");
}
if (await simulator.fs.opfs.exists("missing-healthcheck.ngc")) {
throw new Error("OPFS exists reported a missing file");
}
const healthcheckStat = await simulator.fs.opfs.stat("healthcheck.ngc");
if (healthcheckStat.kind !== "file" || healthcheckStat.size !== 8) {
throw new Error("OPFS stat did not report the persisted healthcheck file size");
}
const clearCheck = new TextDecoder().decode(await simulator.fs.opfs.readFile("healthcheck.ngc"));
expectText(clearCheck, "G21 G90\n", "OPFS workspace did not survive clear/write round trip");
});
await runSection("opfs default mount check", async () => {
await withSmokeSimulator((defaultMountSimulator) => {
if (!defaultMountSimulator.fs.opfs || defaultMountSimulator.fs.opfs.mountPoint !== "/cnc") {
throw new Error("default OPFS mount point was not preserved");
}
});
});
await runSection("opfs program write and read", async () => {
const encoded = new TextEncoder().encode("G21 G90\nG0 X1\n");
await simulator.fs.opfs.writeFile("programs/browser-smoke.ngc", encoded);
const decoded = new TextDecoder().decode(
await simulator.fs.opfs.readFile("programs/browser-smoke.ngc"),
);
expectText(decoded, "G21 G90\nG0 X1\n", "OPFS round trip did not preserve browser smoke program");
});
await runSection("opfs unsafe path rejection", async () => {
for (const unsafePath of ["../escape.ngc", "programs/../escape.ngc", "/escape.ngc", "programs//escape.ngc"]) {
await expectErrorContaining(
() => simulator.fs.opfs.writeFile(unsafePath, "G21\n"),
"inside the CNC workspace",
`OPFS accepted unsafe path ${unsafePath}`,
);
}
for (const [operation, action] of [
["readFile", () => simulator.fs.opfs.readFile("../escape.ngc")],
["readDirectory", () => simulator.fs.opfs.readDirectory("../escape")],
["persistFile", () => simulator.fs.opfs.persistFile("../escape.ngc")],
["persistDirectory", () => simulator.fs.opfs.persistDirectory("../escape")],
["copyFile", () => simulator.fs.opfs.copyFile("../escape.ngc", "programs/escape.ngc")],
["copyFileTarget", () => simulator.fs.opfs.copyFile("healthcheck.ngc", "../escape.ngc")],
["exists", () => simulator.fs.opfs.exists("../escape.ngc")],
["stat", () => simulator.fs.opfs.stat("../escape.ngc")],
["moveFile", () => simulator.fs.opfs.moveFile("../escape.ngc", "programs/escape.ngc")],
["moveFileTarget", () => simulator.fs.opfs.moveFile("healthcheck.ngc", "../escape.ngc")],
["removeFile", () => simulator.fs.opfs.removeFile("../escape.ngc")],
["removeDirectory", () => simulator.fs.opfs.removeDirectory("../escape")],
["parseFile", () => simulator.parseFile("../escape.ngc", "linuxcnc", { backend: "linuxcnc-rs274" })],
[
"parseWithParameterFile",
() =>
simulator.parseWithParameterFile("G21 G90\nM30\n", "../escape.var", "linuxcnc", {
backend: "linuxcnc-rs274",
}),
],
]) {
await expectErrorContaining(
action,
"inside the CNC workspace",
`OPFS ${operation} accepted an unsafe path`,
);
}
});
await runSection("opfs program parse and restore", async () => {
const opfsProgramEvents = await simulator.parseFile("programs/browser-smoke.ngc", "linuxcnc", {
backend: "linuxcnc-rs274",
});
expectEvent(opfsProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1),
"OPFS program file was not parsed through LinuxCNC WASM");
for (const parameterPath of ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]) {
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(parameterPath),
`browser parseFile left LinuxCNC parameter state outside OPFS: ${parameterPath}`,
);
}
await withSmokeSimulator(async (restoredProgramSimulator) => {
// LinuxCNC source basis: rs274ngc_pre.cc read_text() reads
// blocks from the program stream after the OPFS mirror is loaded.
const restoredProgramEvents = await restoredProgramSimulator.parseFile(
"programs/browser-smoke.ngc",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(restoredProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1),
"LinuxCNC OPFS program file was not restored across WASM simulator instances");
});
});
}

View File

@@ -0,0 +1,174 @@
export async function runBrowserOpfsDirectorySections(context, simulator) {
const {
expectFileList,
expectMissingOpfsFile,
expectMissingWasmPath,
expectText,
runSection,
withSmokeSimulator,
} = context;
await runSection("opfs directory persist and load", async () => {
const fixturePath = simulator.fs.opfs.resolvePath("fixtures");
simulator.fs.module.FS.mkdirTree(`${fixturePath}/nested`);
simulator.fs.module.FS.mkdirTree(`${fixturePath}/empty/nested`);
simulator.fs.module.FS.writeFile(`${fixturePath}/nested/fixture.ngc`, "G21 G90\nG0 X3\n");
simulator.fs.module.FS.writeFile(`${fixturePath}/root.ngc`, "G21 G90\nG0 X4\n");
const persistedFiles = await simulator.fs.opfs.persistDirectory("fixtures");
const fixtureStat = await simulator.fs.opfs.stat("fixtures");
if (fixtureStat.kind !== "directory" || fixtureStat.size !== null) {
throw new Error("OPFS stat did not report a persisted directory");
}
expectFileList(persistedFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected OPFS directory persist file list");
const fixtureDecoded = new TextDecoder().decode(
await simulator.fs.opfs.readFile("fixtures/nested/fixture.ngc"),
);
expectText(fixtureDecoded, "G21 G90\nG0 X3\n",
"persisted WASM filesystem directory file was not readable from OPFS");
const emptyFixtureStat = await simulator.fs.opfs.stat("fixtures/empty/nested");
if (emptyFixtureStat.kind !== "directory" || emptyFixtureStat.size !== null) {
throw new Error("OPFS persistDirectory did not preserve an empty directory");
}
await withSmokeSimulator(async (staleDirectorySimulator) => {
await staleDirectorySimulator.fs.opfs.writeFile("fixtures/stale.ngc", "G21 G90\nG0 X6\n");
});
simulator.fs.module.FS.unlink(`${fixturePath}/nested/fixture.ngc`);
const refreshedPersistedFiles = await simulator.fs.opfs.persistDirectory("fixtures");
expectFileList(refreshedPersistedFiles, ["root.ngc"],
"unexpected refreshed OPFS directory persist file list");
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("fixtures/stale.ngc"),
"stale OPFS directory file remained after directory persist",
);
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("fixtures/nested/fixture.ngc"),
"removed WASM directory file remained in OPFS after directory persist",
);
simulator.fs.module.FS.writeFile(`${fixturePath}/nested/fixture.ngc`, "G21 G90\nG0 X3\n");
const restoredPersistedFiles = await simulator.fs.opfs.persistDirectory("fixtures");
expectFileList(restoredPersistedFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected restored OPFS directory persist file list");
const loadedFiles = await simulator.fs.opfs.readDirectory("fixtures");
expectFileList(loadedFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected OPFS directory load file list");
const loadedRootPath = simulator.fs.opfs.resolvePath("fixtures/root.ngc");
const loadedRootDecoded = new TextDecoder().decode(simulator.fs.module.FS.readFile(loadedRootPath));
expectText(loadedRootDecoded, "G21 G90\nG0 X4\n",
"OPFS directory file was not mirrored into the WASM filesystem");
const loadedEmptyPath = simulator.fs.opfs.resolvePath("fixtures/empty/nested");
const loadedEmptyMode = simulator.fs.module.FS.stat(loadedEmptyPath).mode;
if (!simulator.fs.module.FS.isDir(loadedEmptyMode)) {
throw new Error("OPFS readDirectory did not mirror an empty directory into WASM FS");
}
});
await runSection("opfs directory refresh and type conflicts", async () => {
const loadedRootPath = simulator.fs.opfs.resolvePath("fixtures/root.ngc");
await withSmokeSimulator(async (updateDirectorySimulator) => {
await updateDirectorySimulator.fs.opfs.removeFile("fixtures/root.ngc");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/nested/fixture.ngc", "G21 G90\nG0 X5\n");
const refreshedFiles = await simulator.fs.opfs.readDirectory("fixtures");
expectFileList(refreshedFiles, ["nested/fixture.ngc"],
"unexpected refreshed OPFS directory file list");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(loadedRootPath),
"stale OPFS directory mirror file remained in WASM FS",
);
const refreshedFixturePath = simulator.fs.opfs.resolvePath("fixtures/nested/fixture.ngc");
const refreshedFixture = new TextDecoder().decode(
simulator.fs.module.FS.readFile(refreshedFixturePath),
);
expectText(refreshedFixture, "G21 G90\nG0 X5\n",
"OPFS directory refresh did not replace the WASM mirror file");
await simulator.fs.opfs.writeFile("fixtures/type-conflict/original.ngc", "G21 G90\n");
await updateDirectorySimulator.fs.opfs.removeDirectory("fixtures/type-conflict");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/type-conflict", "G21 G90\nG0 X11\n");
const replacedConflictFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("fixtures/type-conflict"),
);
expectText(replacedConflictFile, "G21 G90\nG0 X11\n",
"OPFS readFile did not load a file that replaced a directory");
const conflictMirrorPath = simulator.fs.opfs.resolvePath("fixtures/type-conflict");
const conflictMirrorMode = simulator.fs.module.FS.stat(conflictMirrorPath).mode;
if (!simulator.fs.module.FS.isFile(conflictMirrorMode)) {
throw new Error("OPFS readFile did not replace the WASM mirror directory with a file");
}
await updateDirectorySimulator.fs.opfs.removeFile("fixtures/type-conflict");
await updateDirectorySimulator.fs.opfs.writeFile(
"fixtures/type-conflict/original.ngc",
"G21 G90\n",
);
await updateDirectorySimulator.fs.opfs.removeDirectory("fixtures/type-conflict");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/root.ngc", "G21 G90\nG0 X4\n");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/nested/fixture.ngc", "G21 G90\nG0 X3\n");
const restoredFiles = await simulator.fs.opfs.readDirectory("fixtures");
expectFileList(restoredFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected reloaded OPFS directory file list");
});
});
await runSection("opfs directory restore in new instance", async () => {
await withSmokeSimulator(async (restoredDirectorySimulator) => {
const restoredDirectoryFiles = await restoredDirectorySimulator.fs.opfs.readDirectory("fixtures");
expectFileList(restoredDirectoryFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected restored OPFS directory file list");
const restoredFixtureRootPath = restoredDirectorySimulator.fs.opfs.resolvePath("fixtures/root.ngc");
const restoredFixtureRoot = new TextDecoder().decode(
restoredDirectorySimulator.fs.module.FS.readFile(restoredFixtureRootPath),
);
expectText(restoredFixtureRoot, "G21 G90\nG0 X4\n",
"OPFS directory was not restored into a new WASM filesystem instance");
const restoredEmptyFixturePath = restoredDirectorySimulator.fs.opfs.resolvePath("fixtures/empty/nested");
const restoredEmptyFixtureMode =
restoredDirectorySimulator.fs.module.FS.stat(restoredEmptyFixturePath).mode;
if (!restoredDirectorySimulator.fs.module.FS.isDir(restoredEmptyFixtureMode)) {
throw new Error("OPFS empty directory was not restored into a new WASM filesystem instance");
}
});
});
await runSection("opfs metadata queries avoid wasm mirror pollution", async () => {
await withSmokeSimulator(async (existsOnlySimulator) => {
await existsOnlySimulator.fs.opfs.writeFile("programs/exists-only.ngc", "G21 G90\nG0 X10\n");
const existsOnlyPath = simulator.fs.opfs.resolvePath("programs/exists-only.ngc");
if (!(await simulator.fs.opfs.exists("programs/exists-only.ngc"))) {
throw new Error("OPFS exists did not see a file written by another simulator instance");
}
const existsOnlyStat = await simulator.fs.opfs.stat("programs/exists-only.ngc");
if (existsOnlyStat.kind !== "file" || existsOnlyStat.size !== 15) {
throw new Error("OPFS stat did not see a file written by another simulator instance");
}
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(existsOnlyPath),
"OPFS metadata queries polluted the WASM filesystem mirror",
);
});
});
await runSection("opfs workspace clear and mirror removal", async () => {
await simulator.fs.opfs.clear();
await withSmokeSimulator(async (clearedWorkspaceSimulator) => {
await expectMissingOpfsFile(
() => clearedWorkspaceSimulator.fs.opfs.readDirectory("fixtures"),
"cleared OPFS workspace still exposed a persisted directory",
);
});
await simulator.fs.opfs.removeFile("programs/browser-smoke.ngc");
if (await simulator.fs.opfs.exists("programs/browser-smoke.ngc")) {
throw new Error("OPFS exists reported a removed file");
}
const removedProgramPath = simulator.fs.opfs.resolvePath("programs/browser-smoke.ngc");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(removedProgramPath),
"OPFS removeFile did not remove the WASM mirror",
);
const fixturePath = simulator.fs.opfs.resolvePath("fixtures");
await simulator.fs.opfs.removeDirectory("fixtures");
await simulator.fs.opfs.removeDirectory("fixtures");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(fixturePath),
"OPFS removeDirectory did not remove the WASM mirror",
);
});
}

View File

@@ -0,0 +1,76 @@
export async function runBrowserOpfsMirrorSections(context, simulator) {
const {
expectMissingWasmPath,
expectText,
runSection,
} = context;
await runSection("opfs wasm mirror persist file", async () => {
const wasmPath = simulator.fs.opfs.resolvePath("programs/browser-smoke.ngc");
const wasmDecoded = new TextDecoder().decode(simulator.fs.module.FS.readFile(wasmPath));
expectText(wasmDecoded, "G21 G90\nG0 X1\n", "OPFS file was not mirrored into the WASM filesystem");
const persistedPath = simulator.fs.opfs.resolvePath("programs/browser-persisted.ngc");
simulator.fs.module.FS.writeFile(persistedPath, new TextEncoder().encode("G21 G90\nG0 X2\n"));
const persisted = new TextDecoder().decode(await simulator.fs.opfs.persistFile("programs/browser-persisted.ngc"));
expectText(persisted, "G21 G90\nG0 X2\n", "WASM filesystem file was not persisted into OPFS");
const persistedDecoded = new TextDecoder().decode(
await simulator.fs.opfs.readFile("programs/browser-persisted.ngc"),
);
expectText(persistedDecoded, "G21 G90\nG0 X2\n",
"persisted WASM filesystem file was not readable from OPFS");
});
await runSection("opfs copy and move mirror files", async () => {
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters()
// links the current parameter file to filename + ".bak", then
// writes filename + ".new" and renames it over the parameter file.
await simulator.fs.opfs.writeFile("parameters/move-main.var", "5161\t1.000000\n");
const copiedParameterBackup = new TextDecoder().decode(
await simulator.fs.opfs.copyFile("parameters/move-main.var", "parameters/move-main.var.bak"),
);
expectText(copiedParameterBackup, "5161\t1.000000\n",
"OPFS copyFile did not return the copied file contents");
const copiedParameterBackupFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/move-main.var.bak"),
);
expectText(copiedParameterBackupFile, "5161\t1.000000\n",
"OPFS copyFile did not create the backup file");
const copiedParameterBackupWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var.bak");
const copiedParameterBackupWasm = new TextDecoder().decode(
simulator.fs.module.FS.readFile(copiedParameterBackupWasmPath),
);
expectText(copiedParameterBackupWasm, "5161\t1.000000\n",
"OPFS copyFile did not update the WASM mirror destination");
await simulator.fs.opfs.writeFile("parameters/move-main.var.new", "5161\t2.000000\n");
await simulator.fs.opfs.moveFile("parameters/move-main.var.new", "parameters/move-main.var");
const movedParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/move-main.var"),
);
expectText(movedParameterFile, "5161\t2.000000\n",
"OPFS moveFile did not replace the destination file");
if (await simulator.fs.opfs.exists("parameters/move-main.var.new")) {
throw new Error("OPFS moveFile left the source file behind");
}
const movedParameterWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var");
const movedParameterWasm = new TextDecoder().decode(
simulator.fs.module.FS.readFile(movedParameterWasmPath),
);
expectText(movedParameterWasm, "5161\t2.000000\n",
"OPFS moveFile did not update the WASM mirror destination");
const movedTemporaryWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var.new");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(movedTemporaryWasmPath),
"OPFS moveFile left the WASM mirror source behind",
);
});
await runSection("opfs write truncates wasm mirror", async () => {
await simulator.fs.opfs.writeFile("programs/truncate.ngc", "G21 G90\nG0 X12345\nM30\n");
await simulator.fs.opfs.writeFile("programs/truncate.ngc", "G21\n");
const truncatedProgram = new TextDecoder().decode(await simulator.fs.opfs.readFile("programs/truncate.ngc"));
expectText(truncatedProgram, "G21\n", "OPFS writeFile did not truncate stale program bytes");
const truncatedWasmPath = simulator.fs.opfs.resolvePath("programs/truncate.ngc");
const truncatedWasmProgram = new TextDecoder().decode(simulator.fs.module.FS.readFile(truncatedWasmPath));
expectText(truncatedWasmProgram, "G21\n", "OPFS writeFile did not truncate the WASM mirror");
});
}

View File

@@ -0,0 +1,255 @@
export async function runBrowserOpfsParameterSections(context, simulator) {
const {
expectErrorContaining,
expectEvent,
expectIncludes,
expectMissingOpfsFile,
expectMissingWasmPath,
expectNoEvent,
expectText,
expectWasmFilesMissing,
near,
removeWasmPathIfExists,
runSection,
withSmokeSimulator,
} = context;
await runSection("opfs parameter save", async () => {
await simulator.parseWithParameterFile(
"G21 G90\nG0 X1\nG28.1\nM30\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
const opfsParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/rs274ngc.var"),
);
expectIncludes(opfsParameterFile, "5161\t1.000000", "LinuxCNC parameter file was not persisted into OPFS");
});
await runSection("opfs parameter restore in new instance", async () => {
await withSmokeSimulator(async (restoredWorkspaceSimulator) => {
// LinuxCNC source basis: rs274ngc_pre.cc restore_parameters()
// loads persisted numbered parameters before interpreting blocks.
const restoredWorkspaceEvents = await restoredWorkspaceSimulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X8\nO10 endif\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(restoredWorkspaceEvents, (event) => event.type === "linear-feed" && near(event.end.x, 8),
"LinuxCNC OPFS parameter file was not restored across WASM simulator instances");
});
});
await runSection("opfs parameter temporary cleanup", async () => {
await simulator.fs.opfs.writeFile("parameters/rs274ngc.var.new", "stale temporary parameter file\n");
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes
// filename + ".new", then renames it over the parameter file.
simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary parameter file\n");
const restoredParameterEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(restoredParameterEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9),
"LinuxCNC parameter file was not restored from OPFS");
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/rs274ngc.var.new"),
"stale LinuxCNC temporary parameter file remained in OPFS",
);
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var.new"),
"stale LinuxCNC temporary parameter file remained in WASM FS",
);
});
await runSection("opfs parameter backup bridge", async () => {
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters()
// attempts link(filename, filename + ".bak") but treats link
// failure as non-fatal. The browser bridge must persist a backup
// when the WASM filesystem exposes one.
simulator.fs.module.FS.writeFile("rs274ngc.var", "5161\t2.000000\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "5161\t1.000000\n");
await simulator.fs.opfs.persistParameterFile("parameters/backup-bridge.var");
const opfsParameterBackup = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/backup-bridge.var.bak"),
);
expectIncludes(opfsParameterBackup, "5161\t1.000000",
"LinuxCNC parameter backup file was not bridged into OPFS");
});
await runSection("opfs parameter stale backup cleanup", async () => {
removeWasmPathIfExists(simulator, "rs274ngc.var.bak");
removeWasmPathIfExists(simulator, "/rs274ngc.var.bak");
await simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/rs274ngc.var.bak"),
"stale LinuxCNC parameter backup file remained in OPFS",
);
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters()
// unlinks stale backup files before replacing the parameter file.
await simulator.fs.opfs.removeFile("parameters/missing-cleanup/rs274ngc.var.bak");
});
await runSection("opfs program file with parameter persistence", async () => {
await simulator.fs.opfs.writeFile("programs/parameter-file-smoke.ngc", "G21 G90\nF100\nG0 X1\nG28.1\nM30\n");
const fileParameterEvents = await simulator.parseFileWithParameterFile(
"programs/parameter-file-smoke.ngc",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(fileParameterEvents, (event) => event.type === "program-end",
"OPFS program file with LinuxCNC parameter persistence did not parse");
});
await runSection("opfs missing parameter file creation", async () => {
await simulator.fs.opfs.writeFile("parameters/missing-main.var.bak", "stale backup without main file\n");
await simulator.fs.opfs.writeFile("parameters/missing-main.var.new", "stale temporary without main file\n");
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() unlinks
// filename + RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX before linking,
// then renames filename + ".new" over the parameter file.
simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "stale wasm backup without main file\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary without main file\n");
await simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/missing-main.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
const missingMainParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/missing-main.var"),
);
expectIncludes(missingMainParameterFile, "5220\t1.000000",
"LinuxCNC missing parameter file was not created through OPFS");
expectIncludes(missingMainParameterFile, "5161\t0.000000",
"LinuxCNC missing parameter file did not include required G28 parameter");
const missingMainRestoreEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5220 EQ 1]\nG1 X6\nO10 endif\n",
"parameters/missing-main.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(missingMainRestoreEvents, (event) => event.type === "linear-feed" && near(event.end.x, 6),
"LinuxCNC-created missing OPFS parameter file was not restored on the next parse");
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/missing-main.var.bak"),
"stale LinuxCNC missing-parameter backup remained in OPFS",
);
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var.bak"),
"stale LinuxCNC missing-parameter backup remained in WASM FS",
);
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/missing-main.var.new"),
"stale LinuxCNC missing-parameter temporary file remained in OPFS",
);
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var.new"),
"stale LinuxCNC missing-parameter temporary file remained in WASM FS",
);
});
await runSection("opfs invalid parameter restore failures", async () => {
await simulator.fs.opfs.writeFile("parameters/out-of-order.var", "5162\t1.000000\n5161\t1.000000\n");
await expectErrorContaining(
() => simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/out-of-order.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
),
"Parameter file out of order",
"LinuxCNC accepted an out-of-order OPFS parameter file",
);
const outOfOrderParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/out-of-order.var"),
);
expectText(outOfOrderParameterFile, "5162\t1.000000\n5161\t1.000000\n",
"LinuxCNC persisted an out-of-order OPFS parameter file after restore failure");
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var"),
"LinuxCNC out-of-order parameter file remained in WASM FS after restore failure",
);
await simulator.fs.opfs.writeFile("parameters/out-of-range.var", "0\t1.000000\n");
await expectErrorContaining(
() => simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/out-of-range.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
),
"Parameter number out of range",
"LinuxCNC accepted an out-of-range OPFS parameter file",
);
const outOfRangeParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/out-of-range.var"),
);
expectText(outOfRangeParameterFile, "0\t1.000000\n",
"LinuxCNC persisted an out-of-range OPFS parameter file after restore failure");
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var"),
"LinuxCNC out-of-range parameter file remained in WASM FS after restore failure",
);
await simulator.fs.opfs.writeFile("parameters/failed-parse.var", "1\t0.000000\n");
await expectErrorContaining(
() => simulator.parseWithParameterFile(
"G21 G90\nG28.1\n#0 = 1\n",
"parameters/failed-parse.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
),
"",
"LinuxCNC accepted a failing parse that should not persist parameters",
);
const failedParseParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/failed-parse.var"),
);
if (failedParseParameterFile.includes("5161\t1.000000")) {
throw new Error("LinuxCNC persisted failed parse parameter updates into OPFS");
}
});
await runSection("opfs stale parameter state cleanup", async () => {
await simulator.fs.opfs.writeFile("parameters/stale-state.var", "5161\t1.000000\n");
const staleStateEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n",
"parameters/stale-state.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(staleStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9),
"LinuxCNC did not honor the loaded OPFS parameter state");
await simulator.fs.opfs.removeFile("parameters/stale-state.var");
const missingStateEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n",
"parameters/stale-state.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectNoEvent(missingStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9),
"LinuxCNC stale OPFS parameter state was not cleared when the file was missing");
// LinuxCNC source basis: rs274ngc_pre.cc restore_parameters()
// treats a missing parameter file as OK; the browser OPFS bridge
// must not leave stale default parameter files in the WASM mirror.
simulator.fs.module.FS.writeFile("rs274ngc.var", "5161\t1.000000\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale temporary parameter file\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "stale backup parameter file\n");
const missingLoad = await simulator.fs.opfs.loadParameterFile("parameters/load-missing.var");
if (missingLoad !== null) {
throw new Error("missing OPFS parameter file did not return null");
}
expectWasmFilesMissing(
simulator.fs.module.FS,
["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"],
(parameterPath) => `missing OPFS parameter load left stale WASM file ${parameterPath}`,
);
});
}

View File

@@ -0,0 +1,49 @@
export async function runBrowserOpfsPolicySections(context) {
const {
browserHasOpfs,
createDefaultBrowserSimulator,
createSmokeSimulator,
expectErrorContaining,
runSection,
} = context;
if (!browserHasOpfs()) {
return;
}
await runSection("browser opfs policy", async () => {
const defaultOpfsSimulator = await createDefaultBrowserSimulator();
try {
if (!defaultOpfsSimulator.fs.opfs) {
throw new Error("default browser simulator did not create an OPFS workspace");
}
if (defaultOpfsSimulator.fs.opfs.workspacePath !== "cnc-simulator") {
throw new Error("default browser simulator used an unexpected OPFS workspace path");
}
} finally {
defaultOpfsSimulator.dispose();
}
await expectErrorContaining(
() => createSmokeSimulator(false),
"OPFS cannot be disabled",
"browser simulator allowed OPFS to be disabled",
);
for (const unsafeWorkspacePath of ["../bad-workspace", "/bad-workspace", "bad//workspace"]) {
await expectErrorContaining(
() => createSmokeSimulator({ workspacePath: unsafeWorkspacePath }),
"inside the CNC workspace",
`browser simulator accepted unsafe OPFS workspace ${unsafeWorkspacePath}`,
);
}
for (const unsafeMountPoint of ["cnc", "/cnc/../escape", "/cnc//escape"]) {
await expectErrorContaining(
() => createSmokeSimulator({ mountPoint: unsafeMountPoint, workspacePath: "cnc-simulator-smoke" }),
"Emscripten filesystem",
`browser simulator accepted unsafe OPFS mount ${unsafeMountPoint}`,
);
}
await expectErrorContaining(
() => createSmokeSimulator({ mountPoint: "", workspacePath: "cnc-simulator-smoke" }),
"non-empty absolute path",
"browser simulator accepted empty OPFS mount",
);
});
}

View File

@@ -0,0 +1,14 @@
import { runBrowserOpfsBasicSections } from "/test-browser-wasm-smoke-opfs-basic-sections.js";
import { runBrowserOpfsDirectorySections } from "/test-browser-wasm-smoke-opfs-directory-sections.js";
import { runBrowserOpfsMirrorSections } from "/test-browser-wasm-smoke-opfs-mirror-sections.js";
import { runBrowserOpfsParameterSections } from "/test-browser-wasm-smoke-opfs-parameter-sections.js";
export async function runBrowserOpfsWorkspaceSections(context, simulator) {
if (!context.browserHasOpfs()) {
return;
}
await runBrowserOpfsBasicSections(context, simulator);
await runBrowserOpfsParameterSections(context, simulator);
await runBrowserOpfsMirrorSections(context, simulator);
await runBrowserOpfsDirectorySections(context, simulator);
}

View File

@@ -0,0 +1,99 @@
import { createBrowserSmokeHarness } from "/test-browser-wasm-smoke-helpers.js";
import { runBrowserAppOpfsSections } from "/test-browser-wasm-smoke-app-sections.js";
import { runLinuxCncBrowserSections } from "/test-browser-wasm-smoke-linuxcnc-sections.js";
import { runBrowserOpfsPolicySections } from "/test-browser-wasm-smoke-opfs-policy-sections.js";
import { runBrowserOpfsWorkspaceSections } from "/test-browser-wasm-smoke-opfs-workspace-sections.js";
export async function runBrowserWasmSmoke({ createWasmSimulator, result }) {
const harness = createBrowserSmokeHarness(result);
const createSmokeSimulator = (opfs = { mountPoint: "/cnc", workspacePath: "cnc-simulator-smoke" }) =>
createWasmSimulator({
locateFile: (file) => `/public/${file}`,
print: () => {},
printErr: () => {},
...(opfs === undefined ? {} : { opfs }),
});
const createDefaultBrowserSimulator = () =>
createWasmSimulator({
locateFile: (file) => `/public/${file}`,
print: () => {},
printErr: () => {},
});
const withSmokeSimulator = async (action, opfs) => {
const nestedSimulator = await createSmokeSimulator(opfs);
try {
return await action(nestedSimulator);
} finally {
nestedSimulator.dispose();
}
};
const createHiddenAppFrame = async () => {
const appFrame = document.createElement("iframe");
const appHtml = await (await fetch("/")).text();
appFrame.srcdoc = appHtml.replace(
"</head>",
'<script src="/public/cnc_sim.js"><\/script></head>',
);
appFrame.style.display = "none";
document.body.appendChild(appFrame);
await new Promise((resolve) => {
appFrame.addEventListener("load", resolve, { once: true });
});
return appFrame;
};
const waitForAppControls = (appFrame, message) =>
harness.waitFor(
() => appFrame.contentDocument?.querySelector("#parseBtn"),
message,
);
const parseAppFrameProgram = (appFrame, program) => {
const appInput = appFrame.contentDocument.querySelector("#programInput");
appInput.value = program;
appInput.dispatchEvent(new Event("input", { bubbles: true }));
appFrame.contentDocument.querySelector("#parseBtn").click();
};
const waitForAppAlarm = (appFrame, text, message) =>
harness.waitFor(
() => appFrame.contentDocument?.querySelector("#alarmList")?.textContent.includes(text),
message,
);
const getAppFrameSimulator = async (appFrame, message) => {
const appSimulator = await appFrame.contentWindow.__cncSimulatorForTest;
if (!appSimulator?.fs?.opfs) {
throw new Error(message);
}
return appSimulator;
};
const disposeAppFrame = async (appFrame) => {
const appSimulator = await appFrame.contentWindow?.__cncSimulatorForTest;
appSimulator?.dispose?.();
appFrame.remove();
};
const context = {
...harness,
createDefaultBrowserSimulator,
createHiddenAppFrame,
createSmokeSimulator,
disposeAppFrame,
getAppFrameSimulator,
parseAppFrameProgram,
waitForAppAlarm,
waitForAppControls,
withSmokeSimulator,
};
try {
const simulator = await createSmokeSimulator();
try {
await runLinuxCncBrowserSections(context, simulator);
await runBrowserOpfsWorkspaceSections(context, simulator);
await runBrowserOpfsPolicySections(context);
await runBrowserAppOpfsSections(context);
result.textContent = harness.completedMessage();
} finally {
simulator.dispose();
}
} catch (error) {
result.textContent = `browser wasm smoke failed: ${error && error.stack ? error.stack : error}`;
}
}

View File

@@ -6,82 +6,16 @@
</head>
<body>
<pre id="result">pending</pre>
<!-- smoke marker: browser wasm smoke passed -->
<script src="/public/cnc_sim.js"></script>
<script type="module">
import { createWasmSimulator } from "/src/wasm-core.js";
import { runBrowserWasmSmoke } from "/test-browser-wasm-smoke-sections.js";
const result = document.querySelector("#result");
const near = (actual, expected) => Math.abs(actual - expected) < 1e-6;
try {
const simulator = await createWasmSimulator({
locateFile: (file) => `/public/${file}`,
print: () => {},
printErr: () => {},
});
try {
const events = simulator.parse("G21 G90\nG0 X0\nG1 X5 F100\nM30\n", "linuxcnc", {
backend: "linuxcnc-rs274",
});
const sawFeed = events.some((event) => event.type === "linear-feed" && near(event.end.x, 5));
const sawEnd = events.some((event) => event.type === "program-end");
if (!sawFeed || !sawEnd) {
throw new Error("missing expected LinuxCNC WASM browser events");
}
const switchEvents = simulator.parse("M428\nM429\nM430\n", "linuxcnc", {
backend: "linuxcnc-rs274",
switchkins: "scara",
});
for (const [line, kinstype] of [
[1, 0],
[2, 1],
[3, 2],
]) {
const sawSwitch = switchEvents.some(
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
);
if (!sawSwitch) {
throw new Error(`missing LinuxCNC WASM browser M${427 + line} switchkins type ${kinstype}`);
}
}
const configCaseResponse = await fetch("/public/linuxcnc_switchkins_remap_config_cases.json");
if (!configCaseResponse.ok) {
throw new Error(`failed to load LinuxCNC switchkins config cases: ${configCaseResponse.status}`);
}
const configCases = await configCaseResponse.json();
if (!Array.isArray(configCases) || configCases.length === 0) {
throw new Error("missing LinuxCNC switchkins config cases");
}
for (const configCase of configCases) {
const options = {
backend: "linuxcnc-rs274",
[configCase.field]: configCase.value,
};
const program = configCase.m430 >= 0 ? "M428\nM429\nM430\n" : "M428\nM429\n";
const configEvents = simulator.parse(program, "linuxcnc", options);
for (const [line, kinstype] of [
[1, configCase.m428],
[2, configCase.m429],
...(configCase.m430 >= 0 ? [[3, configCase.m430]] : []),
]) {
const sawSwitch = configEvents.some(
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
);
if (!sawSwitch) {
throw new Error(
`missing LinuxCNC WASM browser generated ${configCase.field}=${configCase.value} line ${line} switchkins type ${kinstype}`,
);
}
}
}
result.textContent = "browser wasm smoke passed";
} finally {
simulator.dispose();
}
} catch (error) {
result.textContent = `browser wasm smoke failed: ${error && error.stack ? error.stack : error}`;
}
runBrowserWasmSmoke({
createWasmSimulator,
result: document.querySelector("#result"),
});
</script>
</body>
</html>