@developerz.ai/ts23d 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (567) hide show
  1. package/.claude/agents/bug-hunter.md +21 -0
  2. package/.claude/agents/docs-author.md +13 -0
  3. package/.claude/agents/dx-auditor.md +14 -0
  4. package/.claude/agents/primitive-author.md +22 -0
  5. package/.claude/agents/recipe-author.md +36 -0
  6. package/.claude/agents/visual-reviewer.md +23 -0
  7. package/.claude/commands/feature.md +28 -0
  8. package/.claude/commands/new-recipe.md +19 -0
  9. package/.claude/commands/release.md +14 -0
  10. package/.claude/commands/verify.md +9 -0
  11. package/.claude/skills/author-recipe/SKILL.md +93 -0
  12. package/CHANGELOG.md +90 -0
  13. package/CLAUDE.md +85 -0
  14. package/LICENSE +21 -0
  15. package/README.md +385 -2
  16. package/dist/chunk-6b05mnmx.js +19 -0
  17. package/dist/chunk-6b05mnmx.js.map +11 -0
  18. package/dist/chunk-7rtfjh9f.js +45 -0
  19. package/dist/chunk-7rtfjh9f.js.map +10 -0
  20. package/dist/chunk-8j4c28jt.js +4085 -0
  21. package/dist/chunk-8j4c28jt.js.map +46 -0
  22. package/dist/chunk-d1fv6hrm.js +2532 -0
  23. package/dist/chunk-d1fv6hrm.js.map +43 -0
  24. package/dist/chunk-dbq4qf52.js +902 -0
  25. package/dist/chunk-dbq4qf52.js.map +25 -0
  26. package/dist/chunk-drna2rny.js +836 -0
  27. package/dist/chunk-drna2rny.js.map +17 -0
  28. package/dist/chunk-dxvypr7y.js +89 -0
  29. package/dist/chunk-dxvypr7y.js.map +11 -0
  30. package/dist/chunk-f6yme003.js +1589 -0
  31. package/dist/chunk-f6yme003.js.map +30 -0
  32. package/dist/chunk-hg7pf34j.js +465 -0
  33. package/dist/chunk-hg7pf34j.js.map +19 -0
  34. package/dist/chunk-jq4e17tj.js +1031 -0
  35. package/dist/chunk-jq4e17tj.js.map +21 -0
  36. package/dist/chunk-y63c6zcv.js +2668 -0
  37. package/dist/chunk-y63c6zcv.js.map +53 -0
  38. package/dist/cli/args.d.ts +30 -0
  39. package/dist/cli/bin.d.ts +2 -0
  40. package/dist/cli/bin.js +348 -0
  41. package/dist/cli/bin.js.map +15 -0
  42. package/dist/cli/load.d.ts +2 -0
  43. package/dist/cli/manifest.d.ts +53 -0
  44. package/dist/cli/render-cmds.d.ts +12 -0
  45. package/dist/cli/run.d.ts +1 -0
  46. package/dist/cli/texture-cache.d.ts +4 -0
  47. package/dist/core/asset.d.ts +10 -0
  48. package/dist/core/definition.d.ts +155 -0
  49. package/dist/core/errors.d.ts +17 -0
  50. package/dist/core/family.d.ts +23 -0
  51. package/dist/core/index.d.ts +9 -0
  52. package/dist/core/index.js +69 -0
  53. package/dist/core/index.js.map +9 -0
  54. package/dist/core/mesh.d.ts +35 -0
  55. package/dist/core/part.d.ts +114 -0
  56. package/dist/core/png.d.ts +1 -0
  57. package/dist/core/rng.d.ts +12 -0
  58. package/dist/core/transform.d.ts +20 -0
  59. package/dist/core/vec.d.ts +11 -0
  60. package/dist/core/world.d.ts +16 -0
  61. package/dist/export/bin-writer.d.ts +16 -0
  62. package/dist/export/container.d.ts +3 -0
  63. package/dist/export/credits.d.ts +4 -0
  64. package/dist/export/glb-options.d.ts +48 -0
  65. package/dist/export/glb.d.ts +8 -0
  66. package/dist/export/hierarchy.d.ts +23 -0
  67. package/dist/export/index.d.ts +5 -0
  68. package/dist/export/index.js +40 -0
  69. package/dist/export/index.js.map +9 -0
  70. package/dist/export/lights.d.ts +8 -0
  71. package/dist/export/lod-coverage.d.ts +12 -0
  72. package/dist/export/materials.d.ts +23 -0
  73. package/dist/export/merge-pivots.d.ts +6 -0
  74. package/dist/export/metadata.d.ts +21 -0
  75. package/dist/export/motion.d.ts +9 -0
  76. package/dist/export/optimize-parts.d.ts +26 -0
  77. package/dist/export/primitive.d.ts +3 -0
  78. package/dist/export/tangents.d.ts +2 -0
  79. package/dist/export/textures.d.ts +22 -0
  80. package/dist/export/version.d.ts +2 -0
  81. package/dist/grammar/index.d.ts +2 -0
  82. package/dist/grammar/index.js +16 -0
  83. package/dist/grammar/index.js.map +9 -0
  84. package/dist/grammar/split.d.ts +21 -0
  85. package/dist/grammar/whyte.d.ts +5 -0
  86. package/dist/index.d.ts +11 -0
  87. package/dist/index.js +457 -0
  88. package/dist/index.js.map +9 -0
  89. package/dist/kinds/building.d.ts +44 -0
  90. package/dist/kinds/faces.d.ts +27 -0
  91. package/dist/kinds/head-armour.d.ts +5 -0
  92. package/dist/kinds/head-beard.d.ts +6 -0
  93. package/dist/kinds/head-bust.d.ts +5 -0
  94. package/dist/kinds/head-extras.d.ts +15 -0
  95. package/dist/kinds/head-face.d.ts +8 -0
  96. package/dist/kinds/head-garb.d.ts +4 -0
  97. package/dist/kinds/head-geom.d.ts +7 -0
  98. package/dist/kinds/head-hair.d.ts +4 -0
  99. package/dist/kinds/head-layout.d.ts +29 -0
  100. package/dist/kinds/head-mesh.d.ts +31 -0
  101. package/dist/kinds/head-mouth.d.ts +4 -0
  102. package/dist/kinds/head-params.d.ts +116 -0
  103. package/dist/kinds/head-presets.d.ts +19 -0
  104. package/dist/kinds/head-rim.d.ts +10 -0
  105. package/dist/kinds/head-tone.d.ts +10 -0
  106. package/dist/kinds/head-torso.d.ts +35 -0
  107. package/dist/kinds/head.d.ts +39 -0
  108. package/dist/kinds/hull-lines.d.ts +65 -0
  109. package/dist/kinds/hull-skin.d.ts +6 -0
  110. package/dist/kinds/hull.d.ts +14 -0
  111. package/dist/kinds/index.d.ts +16 -0
  112. package/dist/kinds/index.js +78 -0
  113. package/dist/kinds/index.js.map +9 -0
  114. package/dist/kinds/mass.d.ts +15 -0
  115. package/dist/kinds/openings.d.ts +30 -0
  116. package/dist/kinds/planned.d.ts +6 -0
  117. package/dist/kinds/roof-chimney.d.ts +16 -0
  118. package/dist/kinds/roof-shape.d.ts +64 -0
  119. package/dist/kinds/roof.d.ts +31 -0
  120. package/dist/kinds/vegetation-fronds.d.ts +15 -0
  121. package/dist/kinds/vegetation-impostor.d.ts +9 -0
  122. package/dist/kinds/vegetation-leaves.d.ts +15 -0
  123. package/dist/kinds/vegetation-mesh.d.ts +21 -0
  124. package/dist/kinds/vegetation-params.d.ts +90 -0
  125. package/dist/kinds/vegetation-presets.d.ts +20 -0
  126. package/dist/kinds/vegetation-skeleton.d.ts +20 -0
  127. package/dist/kinds/vegetation.d.ts +42 -0
  128. package/dist/kinds/vehicle.d.ts +42 -0
  129. package/dist/material/cc0.d.ts +4 -0
  130. package/dist/material/define.d.ts +34 -0
  131. package/dist/material/generator.d.ts +4 -0
  132. package/dist/material/index.d.ts +9 -0
  133. package/dist/material/index.js +70 -0
  134. package/dist/material/index.js.map +9 -0
  135. package/dist/material/library.d.ts +7 -0
  136. package/dist/material/material-def.d.ts +30 -0
  137. package/dist/material/paint/bond.d.ts +3 -0
  138. package/dist/material/paint/canvas.d.ts +39 -0
  139. package/dist/material/paint/cell.d.ts +4 -0
  140. package/dist/material/paint/cells-round.d.ts +5 -0
  141. package/dist/material/paint/cells.d.ts +7 -0
  142. package/dist/material/paint/compose.d.ts +5 -0
  143. package/dist/material/paint/concrete.d.ts +3 -0
  144. package/dist/material/paint/courses.d.ts +31 -0
  145. package/dist/material/paint/decode-png.d.ts +6 -0
  146. package/dist/material/paint/default-sheet.d.ts +4 -0
  147. package/dist/material/paint/fabric.d.ts +4 -0
  148. package/dist/material/paint/foliage-broad.d.ts +5 -0
  149. package/dist/material/paint/foliage-common.d.ts +35 -0
  150. package/dist/material/paint/foliage-compound.d.ts +4 -0
  151. package/dist/material/paint/foliage-conifer.d.ts +5 -0
  152. package/dist/material/paint/foliage-raster.d.ts +26 -0
  153. package/dist/material/paint/foliage.d.ts +3 -0
  154. package/dist/material/paint/image-strip.d.ts +4 -0
  155. package/dist/material/paint/kit.d.ts +5 -0
  156. package/dist/material/paint/masks.d.ts +3 -0
  157. package/dist/material/paint/metal.d.ts +3 -0
  158. package/dist/material/paint/noise.d.ts +9 -0
  159. package/dist/material/paint/normal.d.ts +8 -0
  160. package/dist/material/paint/orm.d.ts +8 -0
  161. package/dist/material/paint/planks.d.ts +3 -0
  162. package/dist/material/paint/plates.d.ts +3 -0
  163. package/dist/material/paint/shapes.d.ts +8 -0
  164. package/dist/material/paint/slate.d.ts +3 -0
  165. package/dist/material/paint/stamp.d.ts +3 -0
  166. package/dist/material/paint/strip.d.ts +3 -0
  167. package/dist/material/paint/surface.d.ts +5 -0
  168. package/dist/material/paint/types.d.ts +65 -0
  169. package/dist/material/paint/validate.d.ts +3 -0
  170. package/dist/material/palette.d.ts +14 -0
  171. package/dist/material/shader-godot.d.ts +2 -0
  172. package/dist/material/shader-three.d.ts +2 -0
  173. package/dist/material/shader.d.ts +46 -0
  174. package/dist/material/slots.d.ts +5 -0
  175. package/dist/material/texture-source.d.ts +34 -0
  176. package/dist/material/trim-sheet.d.ts +38 -0
  177. package/dist/neural/generator.d.ts +25 -0
  178. package/dist/neural/images.d.ts +17 -0
  179. package/dist/neural/index.d.ts +2 -0
  180. package/dist/neural/index.js +14 -0
  181. package/dist/neural/index.js.map +9 -0
  182. package/dist/passes/bake-ao.d.ts +17 -0
  183. package/dist/passes/grime.d.ts +15 -0
  184. package/dist/passes/index.d.ts +6 -0
  185. package/dist/passes/index.js +22 -0
  186. package/dist/passes/index.js.map +9 -0
  187. package/dist/passes/livery.d.ts +19 -0
  188. package/dist/passes/optimize-cache.d.ts +15 -0
  189. package/dist/passes/optimize-weld.d.ts +15 -0
  190. package/dist/passes/optimize.d.ts +28 -0
  191. package/dist/passes/ray-grid.d.ts +23 -0
  192. package/dist/passes/weighted-normals.d.ts +8 -0
  193. package/dist/primitives/beam.d.ts +17 -0
  194. package/dist/primitives/bevel-box.d.ts +13 -0
  195. package/dist/primitives/box.d.ts +16 -0
  196. package/dist/primitives/cell.d.ts +20 -0
  197. package/dist/primitives/check.d.ts +12 -0
  198. package/dist/primitives/closed.d.ts +11 -0
  199. package/dist/primitives/erosion.d.ts +22 -0
  200. package/dist/primitives/font-5x7.d.ts +3 -0
  201. package/dist/primitives/heightfield.d.ts +16 -0
  202. package/dist/primitives/index.d.ts +23 -0
  203. package/dist/primitives/index.js +81 -0
  204. package/dist/primitives/index.js.map +9 -0
  205. package/dist/primitives/lathe-facets.d.ts +8 -0
  206. package/dist/primitives/lathe.d.ts +26 -0
  207. package/dist/primitives/lattice.d.ts +30 -0
  208. package/dist/primitives/loft.d.ts +15 -0
  209. package/dist/primitives/mc-table.d.ts +2 -0
  210. package/dist/primitives/member.d.ts +19 -0
  211. package/dist/primitives/noise.d.ts +34 -0
  212. package/dist/primitives/planar.d.ts +10 -0
  213. package/dist/primitives/prism.d.ts +13 -0
  214. package/dist/primitives/sculpt-edit.d.ts +56 -0
  215. package/dist/primitives/sculpt-tree.d.ts +23 -0
  216. package/dist/primitives/sculpt.d.ts +27 -0
  217. package/dist/primitives/sdf-grid.d.ts +17 -0
  218. package/dist/primitives/sdf-ops.d.ts +37 -0
  219. package/dist/primitives/sdf.d.ts +21 -0
  220. package/dist/primitives/strip-uv.d.ts +11 -0
  221. package/dist/primitives/surface-nets.d.ts +7 -0
  222. package/dist/primitives/sweep.d.ts +19 -0
  223. package/dist/primitives/terrain-color.d.ts +30 -0
  224. package/dist/primitives/terrain-grid.d.ts +36 -0
  225. package/dist/primitives/text-layout.d.ts +22 -0
  226. package/dist/primitives/text.d.ts +27 -0
  227. package/dist/primitives/tiled.d.ts +22 -0
  228. package/dist/primitives/transport.d.ts +17 -0
  229. package/dist/primitives/triangulate.d.ts +2 -0
  230. package/dist/primitives/value-noise.d.ts +10 -0
  231. package/dist/qa/checks.d.ts +11 -0
  232. package/dist/qa/clash.d.ts +2 -0
  233. package/dist/qa/floating.d.ts +5 -0
  234. package/dist/qa/gates.d.ts +12 -0
  235. package/dist/qa/geometry.d.ts +21 -0
  236. package/dist/qa/index.d.ts +12 -0
  237. package/dist/qa/index.js +78 -0
  238. package/dist/qa/index.js.map +9 -0
  239. package/dist/qa/inside.d.ts +12 -0
  240. package/dist/qa/inspect.d.ts +18 -0
  241. package/dist/qa/lods.d.ts +12 -0
  242. package/dist/qa/measures.d.ts +4 -0
  243. package/dist/qa/occlusion.d.ts +3 -0
  244. package/dist/qa/sanity.d.ts +7 -0
  245. package/dist/qa/types.d.ts +12 -0
  246. package/dist/render/beauty.d.ts +40 -0
  247. package/dist/render/camera.d.ts +33 -0
  248. package/dist/render/depth-map.d.ts +38 -0
  249. package/dist/render/font.d.ts +1 -0
  250. package/dist/render/ground.d.ts +13 -0
  251. package/dist/render/index.d.ts +10 -0
  252. package/dist/render/index.js +47 -0
  253. package/dist/render/index.js.map +9 -0
  254. package/dist/render/lighting.d.ts +27 -0
  255. package/dist/render/lod-strip.d.ts +5 -0
  256. package/dist/render/metrics.d.ts +37 -0
  257. package/dist/render/options.d.ts +28 -0
  258. package/dist/render/photo-match.d.ts +19 -0
  259. package/dist/render/raster.d.ts +25 -0
  260. package/dist/render/resolve.d.ts +11 -0
  261. package/dist/render/review-prompt.d.ts +2 -0
  262. package/dist/render/review-warnings.d.ts +34 -0
  263. package/dist/render/review.d.ts +24 -0
  264. package/dist/render/scene.d.ts +18 -0
  265. package/dist/render/shade.d.ts +22 -0
  266. package/dist/render/sheet.d.ts +15 -0
  267. package/dist/render/texture.d.ts +15 -0
  268. package/dist/render/thumbnail.d.ts +8 -0
  269. package/dist/render/vis-buffer.d.ts +18 -0
  270. package/dist/testing/assert.d.ts +21 -0
  271. package/dist/testing/compiled.d.ts +15 -0
  272. package/dist/testing/hash.d.ts +1 -0
  273. package/dist/testing/index.d.ts +4 -0
  274. package/dist/testing/index.js +134 -0
  275. package/dist/testing/index.js.map +13 -0
  276. package/dist/testing/snapshot.d.ts +13 -0
  277. package/docs/README.md +56 -0
  278. package/docs/architecture/README.md +68 -0
  279. package/docs/architecture/adding-a-kind.md +44 -0
  280. package/docs/architecture/adding-a-primitive.md +49 -0
  281. package/docs/architecture/errors.md +47 -0
  282. package/docs/architecture/glb-production.md +214 -0
  283. package/docs/architecture/gltf-capabilities.md +117 -0
  284. package/docs/architecture/materials.md +151 -0
  285. package/docs/decisions/001-recipe-compiler-over-ai-meshes.md +27 -0
  286. package/docs/decisions/002-typescript-not-json.md +22 -0
  287. package/docs/decisions/003-shared-trim-sheet.md +21 -0
  288. package/docs/decisions/004-z-up-metres.md +21 -0
  289. package/docs/decisions/005-zero-runtime-deps.md +21 -0
  290. package/docs/decisions/006-single-package-subpath-exports.md +20 -0
  291. package/docs/decisions/007-cpu-contact-sheets.md +21 -0
  292. package/docs/decisions/008-surface-materials.md +21 -0
  293. package/docs/decisions/009-gltf-front-axis.md +21 -0
  294. package/docs/decisions/README.md +15 -0
  295. package/docs/idea/01-vision.md +117 -0
  296. package/docs/idea/02-dsl.md +343 -0
  297. package/docs/idea/03-ir.md +97 -0
  298. package/docs/idea/04-materials.md +114 -0
  299. package/docs/idea/05-agent-loop.md +141 -0
  300. package/docs/idea/06-qa.md +101 -0
  301. package/docs/idea/07-what-fits.md +45 -0
  302. package/docs/idea/08-neural-adapters.md +78 -0
  303. package/docs/idea/09-kinds.md +105 -0
  304. package/docs/idea/10-sculpting.md +166 -0
  305. package/docs/idea/11-materials-authoring.md +132 -0
  306. package/docs/idea/12-blender-bridge.md +136 -0
  307. package/docs/idea/13-asset-pipeline-research.md +89 -0
  308. package/docs/landscape.md +49 -0
  309. package/docs/plans/roadmap.md +139 -0
  310. package/examples/build-with-bun.ts +41 -0
  311. package/llms.txt +47 -0
  312. package/package.json +100 -4
  313. package/recipes/README.md +38 -0
  314. package/recipes/asteroid/README.md +46 -0
  315. package/recipes/asteroid/asteroid.ts +336 -0
  316. package/recipes/boulders/README.md +28 -0
  317. package/recipes/boulders/boulders.ts +117 -0
  318. package/recipes/boxcar/README.md +53 -0
  319. package/recipes/boxcar/boxcar.ts +1000 -0
  320. package/recipes/cargo_container_scifi/README.md +52 -0
  321. package/recipes/cargo_container_scifi/cargo_container_scifi.ts +461 -0
  322. package/recipes/dropship/README.md +84 -0
  323. package/recipes/dropship/dropship.ts +1144 -0
  324. package/recipes/eiffel_tower/README.md +110 -0
  325. package/recipes/eiffel_tower/eiffel_tower.ts +781 -0
  326. package/recipes/goblin_head/README.md +75 -0
  327. package/recipes/goblin_head/goblin_head.ts +38 -0
  328. package/recipes/human_head/README.md +68 -0
  329. package/recipes/human_head/human_head.ts +38 -0
  330. package/recipes/loco_440/README.md +32 -0
  331. package/recipes/loco_440/loco_440.ts +1404 -0
  332. package/recipes/material_swatches/README.md +21 -0
  333. package/recipes/material_swatches/material_swatches.ts +189 -0
  334. package/recipes/mountain_range/README.md +45 -0
  335. package/recipes/mountain_range/mountain_range.ts +128 -0
  336. package/recipes/oak/README.md +30 -0
  337. package/recipes/oak/oak.ts +19 -0
  338. package/recipes/orc_head/README.md +78 -0
  339. package/recipes/orc_head/orc_head.ts +41 -0
  340. package/recipes/palm/README.md +30 -0
  341. package/recipes/palm/palm.ts +19 -0
  342. package/recipes/pine/README.md +29 -0
  343. package/recipes/pine/pine.ts +19 -0
  344. package/recipes/signal_gantry/README.md +36 -0
  345. package/recipes/signal_gantry/signal_gantry.ts +522 -0
  346. package/recipes/station_halt/README.md +39 -0
  347. package/recipes/station_halt/station_halt.ts +722 -0
  348. package/recipes/t34/README.md +44 -0
  349. package/recipes/t34/t34.ts +1068 -0
  350. package/recipes/tiger_1/README.md +75 -0
  351. package/recipes/tiger_1/tiger_1.ts +1370 -0
  352. package/recipes/water_tower/README.md +40 -0
  353. package/recipes/water_tower/water_tower.ts +542 -0
  354. package/src/cli/args.ts +125 -0
  355. package/src/cli/bin.ts +7 -0
  356. package/src/cli/load.ts +17 -0
  357. package/src/cli/manifest.ts +95 -0
  358. package/src/cli/render-cmds.ts +66 -0
  359. package/src/cli/run.ts +192 -0
  360. package/src/cli/texture-cache.ts +24 -0
  361. package/src/core/asset.ts +129 -0
  362. package/src/core/definition.ts +156 -0
  363. package/src/core/errors.ts +50 -0
  364. package/src/core/family.ts +50 -0
  365. package/src/core/index.ts +66 -0
  366. package/src/core/mesh.ts +114 -0
  367. package/src/core/part.ts +130 -0
  368. package/src/core/png.ts +49 -0
  369. package/src/core/rng.ts +49 -0
  370. package/src/core/transform.ts +90 -0
  371. package/src/core/vec.ts +26 -0
  372. package/src/core/world.ts +80 -0
  373. package/src/export/bin-writer.ts +79 -0
  374. package/src/export/container.ts +35 -0
  375. package/src/export/credits.ts +25 -0
  376. package/src/export/glb-options.ts +53 -0
  377. package/src/export/glb.ts +187 -0
  378. package/src/export/hierarchy.ts +122 -0
  379. package/src/export/index.ts +34 -0
  380. package/src/export/lights.ts +49 -0
  381. package/src/export/lod-coverage.ts +71 -0
  382. package/src/export/materials.ts +126 -0
  383. package/src/export/merge-pivots.ts +146 -0
  384. package/src/export/metadata.ts +62 -0
  385. package/src/export/motion.ts +70 -0
  386. package/src/export/optimize-parts.ts +80 -0
  387. package/src/export/primitive.ts +35 -0
  388. package/src/export/tangents.ts +75 -0
  389. package/src/export/textures.ts +88 -0
  390. package/src/export/version.ts +6 -0
  391. package/src/grammar/index.ts +4 -0
  392. package/src/grammar/split.ts +87 -0
  393. package/src/grammar/whyte.ts +20 -0
  394. package/src/index.ts +14 -0
  395. package/src/kinds/building.ts +153 -0
  396. package/src/kinds/faces.ts +138 -0
  397. package/src/kinds/head-armour.ts +181 -0
  398. package/src/kinds/head-beard.ts +133 -0
  399. package/src/kinds/head-bust.ts +184 -0
  400. package/src/kinds/head-extras.ts +150 -0
  401. package/src/kinds/head-face.ts +242 -0
  402. package/src/kinds/head-garb.ts +181 -0
  403. package/src/kinds/head-geom.ts +31 -0
  404. package/src/kinds/head-hair.ts +193 -0
  405. package/src/kinds/head-layout.ts +63 -0
  406. package/src/kinds/head-mesh.ts +195 -0
  407. package/src/kinds/head-mouth.ts +111 -0
  408. package/src/kinds/head-params.ts +162 -0
  409. package/src/kinds/head-presets.ts +155 -0
  410. package/src/kinds/head-rim.ts +128 -0
  411. package/src/kinds/head-tone.ts +52 -0
  412. package/src/kinds/head-torso.ts +230 -0
  413. package/src/kinds/head.ts +176 -0
  414. package/src/kinds/hull-lines.ts +160 -0
  415. package/src/kinds/hull-skin.ts +83 -0
  416. package/src/kinds/hull.ts +209 -0
  417. package/src/kinds/index.ts +63 -0
  418. package/src/kinds/mass.ts +110 -0
  419. package/src/kinds/openings.ts +141 -0
  420. package/src/kinds/planned.ts +12 -0
  421. package/src/kinds/roof-chimney.ts +100 -0
  422. package/src/kinds/roof-shape.ts +230 -0
  423. package/src/kinds/roof.ts +250 -0
  424. package/src/kinds/vegetation-fronds.ts +126 -0
  425. package/src/kinds/vegetation-impostor.ts +118 -0
  426. package/src/kinds/vegetation-leaves.ts +168 -0
  427. package/src/kinds/vegetation-mesh.ts +174 -0
  428. package/src/kinds/vegetation-params.ts +147 -0
  429. package/src/kinds/vegetation-presets.ts +184 -0
  430. package/src/kinds/vegetation-skeleton.ts +168 -0
  431. package/src/kinds/vegetation.ts +199 -0
  432. package/src/kinds/vehicle.ts +144 -0
  433. package/src/material/cc0.ts +41 -0
  434. package/src/material/define.ts +130 -0
  435. package/src/material/generator.ts +63 -0
  436. package/src/material/index.ts +60 -0
  437. package/src/material/library.ts +196 -0
  438. package/src/material/material-def.ts +84 -0
  439. package/src/material/paint/bond.ts +60 -0
  440. package/src/material/paint/canvas.ts +101 -0
  441. package/src/material/paint/cell.ts +39 -0
  442. package/src/material/paint/cells-round.ts +119 -0
  443. package/src/material/paint/cells.ts +111 -0
  444. package/src/material/paint/compose.ts +53 -0
  445. package/src/material/paint/concrete.ts +62 -0
  446. package/src/material/paint/courses.ts +85 -0
  447. package/src/material/paint/decode-png.ts +106 -0
  448. package/src/material/paint/default-sheet.ts +73 -0
  449. package/src/material/paint/fabric.ts +63 -0
  450. package/src/material/paint/foliage-broad.ts +126 -0
  451. package/src/material/paint/foliage-common.ts +93 -0
  452. package/src/material/paint/foliage-compound.ts +135 -0
  453. package/src/material/paint/foliage-conifer.ts +143 -0
  454. package/src/material/paint/foliage-raster.ts +196 -0
  455. package/src/material/paint/foliage.ts +23 -0
  456. package/src/material/paint/image-strip.ts +88 -0
  457. package/src/material/paint/kit.ts +37 -0
  458. package/src/material/paint/masks.ts +46 -0
  459. package/src/material/paint/metal.ts +109 -0
  460. package/src/material/paint/noise.ts +52 -0
  461. package/src/material/paint/normal.ts +52 -0
  462. package/src/material/paint/orm.ts +108 -0
  463. package/src/material/paint/planks.ts +101 -0
  464. package/src/material/paint/plates.ts +72 -0
  465. package/src/material/paint/shapes.ts +28 -0
  466. package/src/material/paint/slate.ts +52 -0
  467. package/src/material/paint/stamp.ts +40 -0
  468. package/src/material/paint/strip.ts +48 -0
  469. package/src/material/paint/surface.ts +60 -0
  470. package/src/material/paint/types.ts +94 -0
  471. package/src/material/paint/validate.ts +74 -0
  472. package/src/material/palette.ts +46 -0
  473. package/src/material/shader-godot.ts +77 -0
  474. package/src/material/shader-three.ts +103 -0
  475. package/src/material/shader.ts +100 -0
  476. package/src/material/slots.ts +18 -0
  477. package/src/material/texture-source.ts +86 -0
  478. package/src/material/trim-sheet.ts +166 -0
  479. package/src/neural/generator.ts +37 -0
  480. package/src/neural/images.ts +27 -0
  481. package/src/neural/index.ts +10 -0
  482. package/src/passes/bake-ao.ts +183 -0
  483. package/src/passes/grime.ts +37 -0
  484. package/src/passes/index.ts +8 -0
  485. package/src/passes/livery.ts +66 -0
  486. package/src/passes/optimize-cache.ts +175 -0
  487. package/src/passes/optimize-weld.ts +147 -0
  488. package/src/passes/optimize.ts +85 -0
  489. package/src/passes/ray-grid.ts +237 -0
  490. package/src/passes/weighted-normals.ts +125 -0
  491. package/src/primitives/beam.ts +60 -0
  492. package/src/primitives/bevel-box.ts +140 -0
  493. package/src/primitives/box.ts +90 -0
  494. package/src/primitives/cell.ts +115 -0
  495. package/src/primitives/check.ts +49 -0
  496. package/src/primitives/closed.ts +74 -0
  497. package/src/primitives/erosion.ts +183 -0
  498. package/src/primitives/font-5x7.ts +117 -0
  499. package/src/primitives/heightfield.ts +101 -0
  500. package/src/primitives/index.ts +56 -0
  501. package/src/primitives/lathe-facets.ts +45 -0
  502. package/src/primitives/lathe.ts +174 -0
  503. package/src/primitives/lattice.ts +131 -0
  504. package/src/primitives/loft.ts +174 -0
  505. package/src/primitives/mc-table.ts +41 -0
  506. package/src/primitives/member.ts +130 -0
  507. package/src/primitives/noise.ts +157 -0
  508. package/src/primitives/planar.ts +55 -0
  509. package/src/primitives/prism.ts +85 -0
  510. package/src/primitives/sculpt-edit.ts +212 -0
  511. package/src/primitives/sculpt-tree.ts +226 -0
  512. package/src/primitives/sculpt.ts +80 -0
  513. package/src/primitives/sdf-grid.ts +131 -0
  514. package/src/primitives/sdf-ops.ts +145 -0
  515. package/src/primitives/sdf.ts +83 -0
  516. package/src/primitives/strip-uv.ts +26 -0
  517. package/src/primitives/surface-nets.ts +161 -0
  518. package/src/primitives/sweep.ts +162 -0
  519. package/src/primitives/terrain-color.ts +85 -0
  520. package/src/primitives/terrain-grid.ts +137 -0
  521. package/src/primitives/text-layout.ts +85 -0
  522. package/src/primitives/text.ts +197 -0
  523. package/src/primitives/tiled.ts +93 -0
  524. package/src/primitives/transport.ts +85 -0
  525. package/src/primitives/triangulate.ts +42 -0
  526. package/src/primitives/value-noise.ts +62 -0
  527. package/src/qa/checks.ts +110 -0
  528. package/src/qa/clash.ts +177 -0
  529. package/src/qa/floating.ts +63 -0
  530. package/src/qa/gates.ts +126 -0
  531. package/src/qa/geometry.ts +187 -0
  532. package/src/qa/index.ts +28 -0
  533. package/src/qa/inside.ts +114 -0
  534. package/src/qa/inspect.ts +107 -0
  535. package/src/qa/lods.ts +97 -0
  536. package/src/qa/measures.ts +67 -0
  537. package/src/qa/occlusion.ts +166 -0
  538. package/src/qa/sanity.ts +137 -0
  539. package/src/qa/types.ts +17 -0
  540. package/src/render/beauty.ts +115 -0
  541. package/src/render/camera.ts +142 -0
  542. package/src/render/depth-map.ts +191 -0
  543. package/src/render/font.ts +76 -0
  544. package/src/render/ground.ts +87 -0
  545. package/src/render/index.ts +34 -0
  546. package/src/render/lighting.ts +121 -0
  547. package/src/render/lod-strip.ts +34 -0
  548. package/src/render/metrics.ts +147 -0
  549. package/src/render/options.ts +81 -0
  550. package/src/render/photo-match.ts +28 -0
  551. package/src/render/raster.ts +143 -0
  552. package/src/render/resolve.ts +109 -0
  553. package/src/render/review-prompt.ts +59 -0
  554. package/src/render/review-warnings.ts +92 -0
  555. package/src/render/review.ts +103 -0
  556. package/src/render/scene.ts +119 -0
  557. package/src/render/shade.ts +204 -0
  558. package/src/render/sheet.ts +88 -0
  559. package/src/render/texture.ts +97 -0
  560. package/src/render/thumbnail.ts +75 -0
  561. package/src/render/vis-buffer.ts +119 -0
  562. package/src/testing/assert.ts +115 -0
  563. package/src/testing/compiled.ts +31 -0
  564. package/src/testing/hash.ts +14 -0
  565. package/src/testing/index.ts +14 -0
  566. package/src/testing/snapshot.ts +37 -0
  567. package/src/types/gltf-validator.d.ts +13 -0
@@ -0,0 +1,93 @@
1
+ // A flat quad tiling one trim strip at TRUE SCALE: walls, roofs, decks, floors. Flat normal u × v
2
+ // (front = the side u × v points to, CCW). U runs along `u` in metres (the sampler repeats it); V
3
+ // runs up `v` and is cut into one band per `vMetres`, so brick courses keep their real height on a
4
+ // 3 m wall. `uOffset`/`vOffset` phase the pattern so adjacent faces line up.
5
+ // triangles = 2 · segments · bands · (doubleSided ? 2 : 1), bands = ⌈|v| / vMetres⌉ at offset 0
6
+
7
+ import { type Mesh, MeshBuilder, type Vec3 } from '../core/index.ts';
8
+ import { type StripId, stripRect } from '../material/index.ts';
9
+ import { count, positive, spanOf } from './check.ts';
10
+ import { stripU, stripV, stripVMetres } from './strip-uv.ts';
11
+
12
+ export interface TiledOptions {
13
+ /** One corner of the face. */
14
+ readonly origin: Vec3;
15
+ /** Edge along the strip (texture U), metres. */
16
+ readonly u: Vec3;
17
+ /** The other edge, texture "up" (V), metres. */
18
+ readonly v: Vec3;
19
+ readonly color: Vec3;
20
+ readonly strip?: StripId;
21
+ /** Columns along `u` (vertex density for grime/bending); the LOD knob. Default 1. */
22
+ readonly segments?: number;
23
+ /** Metres the pattern is shifted along U / V (continue a course round a corner). */
24
+ readonly uOffset?: number;
25
+ readonly vOffset?: number;
26
+ /** World metres one strip band spans along V. Default: the house sheet's `scaleMetres`.
27
+ * Raise it to stretch the strip (fewer bands, fewer triangles on a far LOD). */
28
+ readonly vMetres?: number;
29
+ readonly doubleSided?: boolean;
30
+ }
31
+
32
+ export function tiled(o: TiledOptions): Mesh {
33
+ const { lu, lv, n } = spanOf('tiled', o.origin, o.u, o.v);
34
+ const strip = o.strip ?? 'plain';
35
+ const segs = count('tiled segments', o.segments ?? 1, 1, 4096);
36
+ const vm = positive('tiled vMetres', o.vMetres ?? stripVMetres(strip), 'e.g. 0.45 for brick');
37
+ const rect = stripRect(strip);
38
+ const uOff = o.uOffset ?? 0;
39
+ const b = new MeshBuilder();
40
+ const sides = o.doubleSided ? [1, -1] : [1];
41
+ let t = 0;
42
+ while (t < lv - 1e-9) {
43
+ let local = (o.vOffset ?? 0) + t;
44
+ local -= Math.floor(local / vm) * vm;
45
+ if (vm - local < 1e-9) local = 0;
46
+ const step = Math.min(lv - t, vm - local);
47
+ const fv0 = local / vm;
48
+ const fv1 = (local + step) / vm;
49
+ for (const side of sides) {
50
+ const nn: Vec3 = [n[0] * side, n[1] * side, n[2] * side];
51
+ const first = row(b, o, nn, rect, uOff, lu, segs, t / lv, stripV(rect, fv0));
52
+ row(b, o, nn, rect, uOff, lu, segs, (t + step) / lv, stripV(rect, fv1));
53
+ for (let j = 0; j < segs; j++) {
54
+ const a = first + j;
55
+ const c = first + segs + 1 + j;
56
+ if (side > 0) b.quad(a, a + 1, c + 1, c);
57
+ else b.quad(a, c, c + 1, a + 1);
58
+ }
59
+ }
60
+ t += step;
61
+ }
62
+ return b.build();
63
+ }
64
+
65
+ /** One row of segs + 1 vertices at fraction `fv` up the face; returns the first index. */
66
+ function row(
67
+ b: MeshBuilder,
68
+ o: TiledOptions,
69
+ n: Vec3,
70
+ rect: ReturnType<typeof stripRect>,
71
+ uOff: number,
72
+ lu: number,
73
+ segs: number,
74
+ fv: number,
75
+ v: number,
76
+ ): number {
77
+ let first = -1;
78
+ for (let j = 0; j <= segs; j++) {
79
+ const fu = j / segs;
80
+ const id = b.vertex({
81
+ p: [
82
+ o.origin[0] + o.u[0] * fu + o.v[0] * fv,
83
+ o.origin[1] + o.u[1] * fu + o.v[1] * fv,
84
+ o.origin[2] + o.u[2] * fu + o.v[2] * fv,
85
+ ],
86
+ n,
87
+ uv: [stripU(rect, uOff + lu * fu), v],
88
+ color: o.color,
89
+ });
90
+ if (first < 0) first = id;
91
+ }
92
+ return first;
93
+ }
@@ -0,0 +1,85 @@
1
+ // Parallel-transport frames along a polyline (double reflection, Wang et al. 2008): the section
2
+ // never twist-flips at inflections the way Frenet frames do. Shared by `sweep` and by kinds that
3
+ // must place things exactly on a swept surface (leaves on twigs, children on branches).
4
+
5
+ import { Ts23dError, type Vec3 } from '../core/index.ts';
6
+
7
+ export interface SweepFrame {
8
+ /** Unit tangent. */
9
+ readonly t: Vec3;
10
+ /** Unit normal: section X. */
11
+ readonly n: Vec3;
12
+ /** Unit binormal t × n: section Y. */
13
+ readonly b: Vec3;
14
+ }
15
+
16
+ const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
17
+ const dot = (a: Vec3, b: Vec3): number => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
18
+ const unit = (a: Vec3): Vec3 => {
19
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
20
+ return [a[0] / l, a[1] / l, a[2] / l];
21
+ };
22
+ const cross = (a: Vec3, b: Vec3): Vec3 => [
23
+ a[1] * b[2] - a[2] * b[1],
24
+ a[2] * b[0] - a[0] * b[2],
25
+ a[0] * b[1] - a[1] * b[0],
26
+ ];
27
+ const reflect = (v: Vec3, axis: Vec3, c: number): Vec3 => {
28
+ const k = (2 / c) * dot(axis, v);
29
+ return [v[0] - k * axis[0], v[1] - k * axis[1], v[2] - k * axis[2]];
30
+ };
31
+
32
+ /** Throws unless the path has ≥ 2 points and no zero-length segment. Returns cumulative lengths. */
33
+ export function arcLengths(path: readonly Vec3[]): number[] {
34
+ if (path.length < 2) {
35
+ throw new Ts23dError('T3D_RECIPE_INVALID', {
36
+ cause: `sweep path has ${path.length} point(s)`,
37
+ fix: 'pass at least 2 path points (from → to); a single point has no direction to sweep along',
38
+ });
39
+ }
40
+ const s = [0];
41
+ for (let i = 1; i < path.length; i++) {
42
+ const d = Math.hypot(...sub(path[i] ?? [0, 0, 0], path[i - 1] ?? [0, 0, 0]));
43
+ if (d < 1e-9) {
44
+ throw new Ts23dError('T3D_RECIPE_INVALID', {
45
+ cause: `sweep path points ${i - 1} and ${i} coincide (zero-length segment)`,
46
+ fix: 'make consecutive path points distinct — drop duplicates before sweeping',
47
+ });
48
+ }
49
+ s.push((s[i - 1] ?? 0) + d);
50
+ }
51
+ return s;
52
+ }
53
+
54
+ /**
55
+ * One frame per path point. The first normal comes from world X when the path starts near
56
+ * vertical, else world Z, so a path in the XZ plane keeps b = ±Y all the way (trunk widths
57
+ * measured across Y stay exact).
58
+ */
59
+ export function transportFrames(path: readonly Vec3[]): SweepFrame[] {
60
+ arcLengths(path);
61
+ const last = path.length - 1;
62
+ const tangents = path.map((_, i) =>
63
+ unit(sub(path[Math.min(last, i + 1)] ?? [0, 0, 0], path[Math.max(0, i - 1)] ?? [0, 0, 0])),
64
+ );
65
+ const t0 = tangents[0] ?? [0, 0, 1];
66
+ const ref: Vec3 = Math.abs(t0[2]) > 0.9 ? [1, 0, 0] : [0, 0, 1];
67
+ const k = dot(ref, t0);
68
+ let n = unit([ref[0] - k * t0[0], ref[1] - k * t0[1], ref[2] - k * t0[2]]);
69
+ const out: SweepFrame[] = [{ t: t0, n, b: cross(t0, n) }];
70
+ for (let i = 1; i <= last; i++) {
71
+ const p0 = path[i - 1] ?? [0, 0, 0];
72
+ const p1 = path[i] ?? [0, 0, 0];
73
+ const ti = tangents[i - 1] ?? t0;
74
+ const tj = tangents[i] ?? t0;
75
+ const v1 = sub(p1, p0);
76
+ const c1 = dot(v1, v1);
77
+ const nL = reflect(n, v1, c1);
78
+ const tL = reflect(ti, v1, c1);
79
+ const v2 = sub(tj, tL);
80
+ const c2 = dot(v2, v2);
81
+ n = unit(c2 < 1e-12 ? nL : reflect(nL, v2, c2));
82
+ out.push({ t: tj, n, b: cross(tj, n) });
83
+ }
84
+ return out;
85
+ }
@@ -0,0 +1,42 @@
1
+ // Ear-clipping triangulation of a simple 2D polygon (convex or concave, either winding).
2
+ // Returns index triples into the input, counter-clockwise.
3
+
4
+ import type { Vec2 } from '../core/index.ts';
5
+
6
+ const area = (p: readonly Vec2[]): number =>
7
+ p.reduce((s, a, i) => {
8
+ const b = p[(i + 1) % p.length] ?? a;
9
+ return s + a[0] * b[1] - b[0] * a[1];
10
+ }, 0) / 2;
11
+
12
+ const cross = (a: Vec2, b: Vec2, c: Vec2): number =>
13
+ (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
14
+
15
+ function inside(p: Vec2, a: Vec2, b: Vec2, c: Vec2): boolean {
16
+ return cross(a, b, p) >= 0 && cross(b, c, p) >= 0 && cross(c, a, p) >= 0;
17
+ }
18
+
19
+ export function triangulate(poly: readonly Vec2[]): [number, number, number][] {
20
+ const idx = poly.map((_, i) => i);
21
+ if (area(poly) < 0) idx.reverse();
22
+ const out: [number, number, number][] = [];
23
+ let guard = idx.length * idx.length;
24
+ while (idx.length > 3 && guard-- > 0) {
25
+ for (let i = 0; i < idx.length; i++) {
26
+ const ia = idx[(i + idx.length - 1) % idx.length] ?? 0;
27
+ const ib = idx[i] ?? 0;
28
+ const ic = idx[(i + 1) % idx.length] ?? 0;
29
+ const a = poly[ia] ?? [0, 0];
30
+ const b = poly[ib] ?? [0, 0];
31
+ const c = poly[ic] ?? [0, 0];
32
+ if (cross(a, b, c) <= 0) continue;
33
+ if (idx.some((j) => j !== ia && j !== ib && j !== ic && inside(poly[j] ?? [0, 0], a, b, c)))
34
+ continue;
35
+ out.push([ia, ib, ic]);
36
+ idx.splice(i, 1);
37
+ break;
38
+ }
39
+ }
40
+ if (idx.length === 3) out.push([idx[0] ?? 0, idx[1] ?? 0, idx[2] ?? 0]);
41
+ return out;
42
+ }
@@ -0,0 +1,62 @@
1
+ // Seeded 3D value noise for SDF displacement (rock pitting, skin wrinkles): a shuffled lattice
2
+ // from the recipe's Rng, quintic-faded trilinear interpolation, fractal octaves. Deterministic.
3
+
4
+ import type { Rng } from '../core/index.ts';
5
+ import type { Field } from './sdf-ops.ts';
6
+
7
+ /**
8
+ * Seeded fractal value noise in [−1, 1]: `scale` = metres per feature, `octaves` halve the scale
9
+ * and amplitude each time. Same Rng state → same field, bit for bit.
10
+ */
11
+ export function valueNoise(
12
+ rng: Rng,
13
+ o: { readonly scale?: number; readonly octaves?: number } = {},
14
+ ): Field {
15
+ const perm = new Uint8Array(512);
16
+ const vals = new Float64Array(256);
17
+ for (let i = 0; i < 256; i++) {
18
+ perm[i] = i;
19
+ vals[i] = rng.next() * 2 - 1;
20
+ }
21
+ for (let i = 255; i > 0; i--) {
22
+ const j = rng.int(0, i);
23
+ const t = perm[i] ?? 0;
24
+ perm[i] = perm[j] ?? 0;
25
+ perm[j] = t;
26
+ }
27
+ for (let i = 0; i < 256; i++) perm[i + 256] = perm[i] ?? 0;
28
+ const at = (x: number, y: number, z: number): number =>
29
+ vals[perm[(perm[(perm[x & 255] ?? 0) + (y & 255)] ?? 0) + (z & 255)] ?? 0] ?? 0;
30
+ const fade = (t: number): number => t * t * t * (t * (t * 6 - 15) + 10);
31
+ const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;
32
+ const one = (x: number, y: number, z: number): number => {
33
+ const xi = Math.floor(x);
34
+ const yi = Math.floor(y);
35
+ const zi = Math.floor(z);
36
+ const u = fade(x - xi);
37
+ const v = fade(y - yi);
38
+ const w = fade(z - zi);
39
+ const plane = (k: number): number =>
40
+ lerp(
41
+ lerp(at(xi, yi, k), at(xi + 1, yi, k), u),
42
+ lerp(at(xi, yi + 1, k), at(xi + 1, yi + 1, k), u),
43
+ v,
44
+ );
45
+ return lerp(plane(zi), plane(zi + 1), w);
46
+ };
47
+ const s = 1 / (o.scale ?? 1);
48
+ const octaves = o.octaves ?? 3;
49
+ return (p) => {
50
+ let sum = 0;
51
+ let amp = 1;
52
+ let norm = 0;
53
+ let f = s;
54
+ for (let i = 0; i < octaves; i++) {
55
+ sum += amp * one(p[0] * f, p[1] * f, p[2] * f);
56
+ norm += amp;
57
+ amp *= 0.5;
58
+ f *= 2;
59
+ }
60
+ return sum / norm;
61
+ };
62
+ }
@@ -0,0 +1,110 @@
1
+ // Check builders: a recipe states its physical intent in code and QA enforces it at every LOD.
2
+ // checks: [clear('track_*', 'hull_*'), noOverlap('road_wheel_l_*'), touches('turret_body', 'hull_top'),
3
+ // symmetric('_l', '_r', 'y')]
4
+ // Each returns a core `Check`; write your own the same way: (asset) => CheckFinding[].
5
+
6
+ import type { Check, CheckFinding } from '../core/index.ts';
7
+ import { type Inspector, inspect } from './inspect.ts';
8
+
9
+ // One inspector per compiled asset, shared by every check (its caches are the speed).
10
+ const inspectors = new WeakMap<object, Inspector>();
11
+ const inspectorFor = (asset: Parameters<Check>[0]): Inspector => {
12
+ const hit = inspectors.get(asset);
13
+ if (hit) return hit;
14
+ const made = inspect(asset);
15
+ inspectors.set(asset, made);
16
+ return made;
17
+ };
18
+
19
+ const CLASH = 0.01;
20
+
21
+ /** Nothing matching `a` sinks into anything matching `b` (closed solids), with optional min gap. */
22
+ export const clear =
23
+ (a: string, b: string, minGap = 0): Check =>
24
+ (asset) => {
25
+ const q = inspectorFor(asset);
26
+ const out: CheckFinding[] = [];
27
+ for (const pa of q.parts(a)) {
28
+ for (const pb of q.parts(b)) {
29
+ if (pa === pb) continue;
30
+ const pen = q.penetration(pa, pb);
31
+ if (pen.depth > CLASH) {
32
+ out.push({
33
+ part: pa,
34
+ message: `'${pa}' sinks ${(pen.depth * 100).toFixed(1)} cm into '${pb}'`,
35
+ fix: `move '${pa}' out of '${pb}' (or shrink one); they are declared clear of each other`,
36
+ });
37
+ } else if (minGap > 0) {
38
+ const d = q.distance(pa, pb, minGap);
39
+ if (d < minGap)
40
+ out.push({
41
+ part: pa,
42
+ message: `'${pa}' is ${(d * 100).toFixed(1)} cm from '${pb}', needs ≥ ${minGap * 100} cm`,
43
+ fix: 'open the gap — running clearance, mud, suspension travel',
44
+ });
45
+ }
46
+ }
47
+ }
48
+ return out;
49
+ };
50
+
51
+ /** No two parts matching the selector interpenetrate (interleaved wheels, stacked crates). */
52
+ export const noOverlap =
53
+ (selector: string): Check =>
54
+ (asset) => {
55
+ const names = inspectorFor(asset).parts(selector);
56
+ return names.flatMap((a, i) => names.slice(i + 1).flatMap((b) => clear(a, b)(asset)));
57
+ };
58
+
59
+ /** `a` must touch `b` (surface within 2 cm): a turret sits on its ring, a lamp on its post. */
60
+ export const touches =
61
+ (a: string, b: string, eps = 0.02): Check =>
62
+ (asset) => {
63
+ const q = inspectorFor(asset);
64
+ // A part dropped at this LOD (detail gating) has nothing to touch: the check doesn't apply.
65
+ if (q.parts(a).length === 0 || q.parts(b).length === 0) return [];
66
+ const d = q.distance(a, b, 1);
67
+ return d <= eps
68
+ ? []
69
+ : [
70
+ {
71
+ part: a,
72
+ message: `'${a}' should touch '${b}' but is ${(d * 100).toFixed(1)} cm away`,
73
+ fix: `seat '${a}' on '${b}'`,
74
+ },
75
+ ];
76
+ };
77
+
78
+ /** Every part containing `left` has a mirror part (with `right`) reflected across the axis plane. */
79
+ export const symmetric =
80
+ (left: string, right: string, axis: 'x' | 'y' | 'z' = 'y', tol = 0.02): Check =>
81
+ (asset) => {
82
+ const q = inspectorFor(asset);
83
+ const k = ({ x: 0, y: 1, z: 2 } as const)[axis];
84
+ // Side tokens are whole name segments: `_l` matches `wheel_l_3` and `rod_l`, never `axle_lead`.
85
+ const token = new RegExp(`${left.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=_|$)`);
86
+ return q
87
+ .parts('*')
88
+ .filter((n) => token.test(n))
89
+ .flatMap((l): CheckFinding[] => {
90
+ const r = l.replace(token, right);
91
+ const bl = q.bounds(l);
92
+ const br = q.bounds(r);
93
+ if (!bl || !br)
94
+ return [{ part: l, message: `'${l}' has no mirror '${r}'`, fix: `add '${r}' or rename` }];
95
+ const off = Math.abs(bl.min[k] + br.max[k]) + Math.abs(bl.max[k] + br.min[k]);
96
+ return off > tol * 2
97
+ ? [
98
+ {
99
+ part: l,
100
+ message: `'${l}' and '${r}' are not mirrored across ${axis}=0 (off by ${(off * 50).toFixed(1)} cm)`,
101
+ fix: 'build one side and mirror it — place(mesh, { scale: [1, -1, 1] }) flips winding; prefer reusing the same builder with a sign',
102
+ },
103
+ ]
104
+ : [];
105
+ });
106
+ };
107
+
108
+ /** Run a recipe's checks; the QA gate calls this. */
109
+ export const runChecks = (asset: Parameters<Check>[0]): CheckFinding[] =>
110
+ asset.checks.flatMap((c) => [...c(asset)]);
@@ -0,0 +1,177 @@
1
+ // Gate: moving parts never cut through anything. Each motion root (a spinning wheel/axle group, a
2
+ // rotor, a sliding crosshead) is swept through its whole motion; any other part whose surface goes
3
+ // deeper than CLASH_EPS into that swept volume fails — wheel vs fender, prop vs cowling, rod vs frame.
4
+ // Spins sweep a solid of revolution (radius profile per axial slice); sways sweep only their
5
+ // sector (the same profile, kept per 5° angular bin and widened by the swing); slides use the
6
+ // moving mesh's bounds extended along the stroke (one side only for `oneWay`).
7
+
8
+ import {
9
+ boundsOf,
10
+ type CompiledAsset,
11
+ type Mesh,
12
+ merge,
13
+ type Part,
14
+ type Vec3,
15
+ worldMeshes,
16
+ worldTransforms,
17
+ } from '../core/index.ts';
18
+ import { surfaceSamples } from './geometry.ts';
19
+ import { CLASH_EPS, type Finding, type Gate } from './types.ts';
20
+
21
+ const SLICES = 24;
22
+
23
+ function subtree(parts: readonly Part[], root: string): Set<string> {
24
+ const out = new Set([root]);
25
+ for (const p of parts) if (p.parent && out.has(p.parent)) out.add(p.name);
26
+ return out;
27
+ }
28
+
29
+ type Inside = (p: Vec3) => boolean;
30
+
31
+ /** A swept volume plus its conservative world AABB, so far-away parts and samples skip the test. */
32
+ interface Volume {
33
+ readonly inside: Inside;
34
+ readonly min: Vec3;
35
+ readonly max: Vec3;
36
+ }
37
+
38
+ const BINS = 72;
39
+ const TAU = Math.PI * 2;
40
+
41
+ /**
42
+ * Swept volume of `mesh` rotating about `axis` through `pivot`. `range` (radians, rest = 0)
43
+ * limits it to a sector; omitted = a full revolution.
44
+ */
45
+ function spinVolume(
46
+ mesh: Mesh,
47
+ pivot: Vec3,
48
+ axis: Vec3,
49
+ range?: readonly [number, number],
50
+ ): Volume {
51
+ const len = Math.hypot(...axis) || 1;
52
+ const u: Vec3 = [axis[0] / len, axis[1] / len, axis[2] / len];
53
+ // A frame around the axis, for the angle of each point.
54
+ const ref: Vec3 = Math.abs(u[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
55
+ const dotRef = ref[0] * u[0] + ref[1] * u[1] + ref[2] * u[2];
56
+ const e1raw: Vec3 = [ref[0] - dotRef * u[0], ref[1] - dotRef * u[1], ref[2] - dotRef * u[2]];
57
+ const e1l = Math.hypot(...e1raw) || 1;
58
+ const e1: Vec3 = [e1raw[0] / e1l, e1raw[1] / e1l, e1raw[2] / e1l];
59
+ const e2: Vec3 = [
60
+ u[1] * e1[2] - u[2] * e1[1],
61
+ u[2] * e1[0] - u[0] * e1[2],
62
+ u[0] * e1[1] - u[1] * e1[0],
63
+ ];
64
+ const coords = (p: Vec3): [number, number, number] => {
65
+ const d: Vec3 = [p[0] - pivot[0], p[1] - pivot[1], p[2] - pivot[2]];
66
+ const a = d[0] * u[0] + d[1] * u[1] + d[2] * u[2];
67
+ const x = d[0] * e1[0] + d[1] * e1[1] + d[2] * e1[2];
68
+ const y = d[0] * e2[0] + d[1] * e2[1] + d[2] * e2[2];
69
+ return [a, Math.hypot(x, y), Math.atan2(y, x)];
70
+ };
71
+ const bin = (phi: number): number =>
72
+ Math.floor((((phi % TAU) + TAU) % TAU) / (TAU / BINS)) % BINS;
73
+ const pts = surfaceSamples(mesh, 0.05, 8).map(coords);
74
+ const lo = Math.min(...pts.map((p) => p[0]));
75
+ const hi = Math.max(...pts.map((p) => p[0]));
76
+ const span = Math.max(hi - lo, 1e-6);
77
+ const slice = (a: number): number => Math.min(SLICES - 1, Math.floor(((a - lo) / span) * SLICES));
78
+ // radius[slice * BINS + bin]: farthest body sample in that slice and angular bin (rest pose).
79
+ const radius = new Float64Array(SLICES * BINS).fill(-1);
80
+ for (const [a, r, phi] of pts) {
81
+ const k = slice(a) * BINS + bin(phi);
82
+ radius[k] = Math.max(radius[k] ?? -1, r);
83
+ }
84
+ // Bins a rest-pose bin sweeps into: the swing plus one bin of slack either side.
85
+ const [from, to] = range ?? [0, TAU];
86
+ const steps = range ? Math.ceil((to - from) / (TAU / BINS)) + 2 : BINS;
87
+ const reach = Math.max(...radius) + Math.max(Math.abs(lo), Math.abs(hi));
88
+ return {
89
+ min: [pivot[0] - reach, pivot[1] - reach, pivot[2] - reach],
90
+ max: [pivot[0] + reach, pivot[1] + reach, pivot[2] + reach],
91
+ inside: (p) => {
92
+ const [a, r, phi] = coords(p);
93
+ if (a < lo + CLASH_EPS || a > hi - CLASH_EPS) return false;
94
+ const base = slice(a) * BINS;
95
+ // p is swept if some rest-pose bin, rotated by θ ∈ [from, to], lands on p's angle.
96
+ for (let i = 0; i < Math.min(steps, BINS); i++) {
97
+ const theta = range ? from - TAU / BINS + i * (TAU / BINS) : i * (TAU / BINS);
98
+ if (r < (radius[base + bin(phi - theta)] ?? -1) - CLASH_EPS) return true;
99
+ }
100
+ return false;
101
+ },
102
+ };
103
+ }
104
+
105
+ function slideVolume(mesh: Mesh, offset: Vec3, oneWay = false): Volume {
106
+ const b = boundsOf(mesh);
107
+ // Two-sided strokes reach ±offset; one-way strokes reach rest..rest + offset.
108
+ const neg = (k: number): number =>
109
+ oneWay ? Math.min(0, offset[k] ?? 0) : -Math.abs(offset[k] ?? 0);
110
+ const pos = (k: number): number =>
111
+ oneWay ? Math.max(0, offset[k] ?? 0) : Math.abs(offset[k] ?? 0);
112
+ const lo = [0, 1, 2].map((k) => (b.min[k] ?? 0) + neg(k) + CLASH_EPS);
113
+ const hi = [0, 1, 2].map((k) => (b.max[k] ?? 0) + pos(k) - CLASH_EPS);
114
+ return {
115
+ min: [lo[0] ?? 0, lo[1] ?? 0, lo[2] ?? 0],
116
+ max: [hi[0] ?? 0, hi[1] ?? 0, hi[2] ?? 0],
117
+ inside: (p) => [0, 1, 2].every((k) => (p[k] ?? 0) > (lo[k] ?? 0) && (p[k] ?? 0) < (hi[k] ?? 0)),
118
+ };
119
+ }
120
+
121
+ const inBox = (p: Vec3, v: Volume): boolean =>
122
+ p[0] >= v.min[0] &&
123
+ p[0] <= v.max[0] &&
124
+ p[1] >= v.min[1] &&
125
+ p[1] <= v.max[1] &&
126
+ p[2] >= v.min[2] &&
127
+ p[2] <= v.max[2];
128
+
129
+ export const clashes: Gate = (asset: CompiledAsset): Finding[] => {
130
+ const meshes = worldMeshes(asset.parts);
131
+ const xf = worldTransforms(asset.parts);
132
+ const findings: Finding[] = [];
133
+ for (const root of asset.parts.filter((p) => p.motion)) {
134
+ const moving = subtree(asset.parts, root.name);
135
+ const body = merge([...moving].flatMap((n) => meshes.get(n) ?? []));
136
+ const t = xf.get(root.name);
137
+ if (!t || body.indices.length === 0 || !root.motion) continue;
138
+ const c = Math.cos(t.yaw);
139
+ const s = Math.sin(t.yaw);
140
+ const rot = (v: Vec3): Vec3 => [v[0] * c - v[1] * s, v[0] * s + v[1] * c, v[2]];
141
+ const m = root.motion;
142
+ const peak = m.kind === 'sway' ? (m.degrees * Math.PI) / 180 : 0;
143
+ const vol =
144
+ m.kind === 'slide'
145
+ ? slideVolume(body, rot(m.offset), m.oneWay)
146
+ : m.kind === 'sway'
147
+ ? spinVolume(body, t.origin, rot(m.axis), m.oneWay ? [0, peak] : [-peak, peak])
148
+ : spinVolume(body, t.origin, rot(m.axis));
149
+ for (const [name, mesh] of meshes) {
150
+ if (moving.has(name) || root.clearOf?.includes(name)) continue;
151
+ const b = boundsOf(mesh);
152
+ if (
153
+ !inBox(b.min, {
154
+ ...vol,
155
+ min: [
156
+ vol.min[0] - (b.max[0] - b.min[0]),
157
+ vol.min[1] - (b.max[1] - b.min[1]),
158
+ vol.min[2] - (b.max[2] - b.min[2]),
159
+ ],
160
+ })
161
+ )
162
+ continue;
163
+ const hits = surfaceSamples(mesh, 0.1, 4).filter(
164
+ (p) => inBox(p, vol) && vol.inside(p),
165
+ ).length;
166
+ if (hits > 0) {
167
+ findings.push({
168
+ gate: 'clashes',
169
+ part: name,
170
+ message: `'${name}' cuts into the swept ${root.motion.kind} of '${root.name}' (${hits} sample${hits === 1 ? '' : 's'} deeper than ${CLASH_EPS * 100} cm)`,
171
+ fix: `move/trim '${name}' or '${root.name}' for running clearance; if they are meant to interlock (an axle through its bearing), list it: clearOf: ['${name}'] on '${root.name}'`,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ return findings;
177
+ };
@@ -0,0 +1,63 @@
1
+ // Gate: everything is attached. A part is grounded if it reaches z≈0 or its SURFACE comes within
2
+ // CONTACT_EPS of a grounded part's surface — transitively. Bounding boxes are only the prefilter.
3
+
4
+ import {
5
+ type Bounds,
6
+ boundsOf,
7
+ type CompiledAsset,
8
+ type Mesh,
9
+ worldMeshes,
10
+ } from '../core/index.ts';
11
+ import { surfaceSamples, TriangleGrid } from './geometry.ts';
12
+ import { CONTACT_EPS, type Finding, type Gate } from './types.ts';
13
+
14
+ const near = (a: Bounds, b: Bounds, eps: number): boolean =>
15
+ [0, 1, 2].every(
16
+ (k) => (a.min[k] ?? 0) <= (b.max[k] ?? 0) + eps && (b.min[k] ?? 0) <= (a.max[k] ?? 0) + eps,
17
+ );
18
+
19
+ export function touching(a: Mesh, b: Mesh, grid: TriangleGrid, eps = CONTACT_EPS): boolean {
20
+ const bb = boundsOf(b);
21
+ for (const p of surfaceSamples(a, 0.1, 4)) {
22
+ if (!near({ min: p, max: p }, bb, eps)) continue;
23
+ if (grid.nearest(p, eps) <= eps) return true;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ export const floatingParts: Gate = (asset: CompiledAsset): Finding[] => {
29
+ const meshes = [...worldMeshes(asset.parts)];
30
+ const bounds = new Map(meshes.map(([n, m]) => [n, boundsOf(m)]));
31
+ const grids = new Map<string, TriangleGrid>();
32
+ const grid = (n: string, m: Mesh): TriangleGrid => {
33
+ const g = grids.get(n) ?? new TriangleGrid(m);
34
+ grids.set(n, g);
35
+ return g;
36
+ };
37
+ const grounded = new Set(
38
+ meshes.filter(([n]) => (bounds.get(n)?.min[2] ?? 1) <= CONTACT_EPS).map(([n]) => n),
39
+ );
40
+ const frontier = [...grounded];
41
+ while (frontier.length > 0) {
42
+ const g = frontier.pop() ?? '';
43
+ const gm = meshes.find(([n]) => n === g)?.[1];
44
+ const gb = bounds.get(g);
45
+ if (!gm || !gb) continue;
46
+ for (const [n, m] of meshes) {
47
+ const b = bounds.get(n);
48
+ if (grounded.has(n) || !b || !near(b, gb, CONTACT_EPS)) continue;
49
+ if (touching(m, gm, grid(g, gm)) || touching(gm, m, grid(n, m))) {
50
+ grounded.add(n);
51
+ frontier.push(n);
52
+ }
53
+ }
54
+ }
55
+ return meshes
56
+ .filter(([n]) => !grounded.has(n))
57
+ .map(([n]) => ({
58
+ gate: 'floating-parts',
59
+ part: n,
60
+ message: `'${n}' has no surface within ${CONTACT_EPS * 100} cm of anything attached to the ground`,
61
+ fix: 'move it onto what holds it (check `at`, parent and the LOD-gated detail around it) — bounding boxes touching is not enough',
62
+ }));
63
+ };