@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,67 @@
1
+ // Gate: the model matches its spec sheet. Each `real.measures` entry (driver diameter, gauge,
2
+ // wheelbase, turret width…) is measured on the LOD0 model and must land within tolerance (2%
3
+ // default — tighter than the overall box, because these are the numbers players notice).
4
+
5
+ import {
6
+ type Bounds,
7
+ boundsOf,
8
+ type CompiledAsset,
9
+ type Measure,
10
+ worldMeshes,
11
+ } from '../core/index.ts';
12
+ import type { Finding, Gate } from './types.ts';
13
+
14
+ const AXIS = { x: 0, y: 1, z: 2 } as const;
15
+
16
+ function union(bounds: readonly Bounds[]): Bounds | null {
17
+ if (bounds.length === 0) return null;
18
+ const k = [0, 1, 2] as const;
19
+ return {
20
+ min: k.map((i) => Math.min(...bounds.map((b) => b.min[i]))) as unknown as Bounds['min'],
21
+ max: k.map((i) => Math.max(...bounds.map((b) => b.max[i]))) as unknown as Bounds['max'],
22
+ };
23
+ }
24
+
25
+ export function measure(asset: CompiledAsset, m: Measure): number | null {
26
+ const meshes = worldMeshes(asset.parts);
27
+ const of = (prefix: string): Bounds | null =>
28
+ union(
29
+ [...meshes]
30
+ .filter(([n]) => n === prefix || n.startsWith(`${prefix}_`))
31
+ .map(([, mesh]) => boundsOf(mesh)),
32
+ );
33
+ const k = AXIS[m.axis];
34
+ if (m.between) {
35
+ const [a, b] = [of(m.between[0]), of(m.between[1])];
36
+ if (!a || !b) return null;
37
+ return Math.abs((a.min[k] + a.max[k]) / 2 - (b.min[k] + b.max[k]) / 2);
38
+ }
39
+ const u = union((m.parts ?? []).map(of).filter((b): b is Bounds => b !== null));
40
+ return u ? u.max[k] - u.min[k] : null;
41
+ }
42
+
43
+ export const measures: Gate = (asset): Finding[] => {
44
+ if (asset.lod !== 0 || !asset.real?.measures) return [];
45
+ return asset.real.measures.flatMap((m): Finding[] => {
46
+ const got = measure(asset, m);
47
+ const tol = m.tolerance ?? 0.02;
48
+ if (got === null) {
49
+ return [
50
+ {
51
+ gate: 'measures',
52
+ message: `'${m.label}': no part matches ${JSON.stringify(m.parts ?? m.between)}`,
53
+ fix: 'name the parts the measure refers to, or fix the names in the measure',
54
+ },
55
+ ];
56
+ }
57
+ return Math.abs(got - m.value) / m.value > tol
58
+ ? [
59
+ {
60
+ gate: 'measures',
61
+ message: `${m.label}: ${got.toFixed(3)} m on ${m.axis.toUpperCase()}, spec ${m.value} m (±${tol * 100}%)${m.source ? ` — ${m.source}` : ''}`,
62
+ fix: 'rebuild the part from the spec number; never edit the spec to match the model',
63
+ },
64
+ ]
65
+ : [];
66
+ });
67
+ };
@@ -0,0 +1,166 @@
1
+ // Gates: surfaces a viewer either can't see or can't see cleanly. `zFighting` — a face lying in
2
+ // another part's face, both facing the same way, flickers in every engine. `hiddenParts` — a part
3
+ // entirely inside another closed solid is budget nobody ever sees. Both need a closed partner
4
+ // (point-in-solid); open sheets are skipped, never guessed.
5
+
6
+ import {
7
+ type Bounds,
8
+ boundsOf,
9
+ type CompiledAsset,
10
+ type Mesh,
11
+ type Vec3,
12
+ worldMeshes,
13
+ } from '../core/index.ts';
14
+ import { TriangleGrid } from './geometry.ts';
15
+ import { SolidIndex } from './inside.ts';
16
+ import type { Finding, Gate } from './types.ts';
17
+
18
+ const overlaps = (a: Bounds, b: Bounds, eps: number): boolean =>
19
+ [0, 1, 2].every(
20
+ (k) => (a.min[k] ?? 0) <= (b.max[k] ?? 0) + eps && (b.min[k] ?? 0) <= (a.max[k] ?? 0) + eps,
21
+ );
22
+ const contains = (outer: Bounds, inner: Bounds): boolean =>
23
+ [0, 1, 2].every(
24
+ (k) => (outer.min[k] ?? 0) <= (inner.min[k] ?? 0) && (inner.max[k] ?? 0) <= (outer.max[k] ?? 0),
25
+ );
26
+
27
+ /** Up to `cap` triangle centroids with unit face normals, evenly strided (deterministic). */
28
+ function faceSamples(m: Mesh, cap: number): Array<{ p: Vec3; n: Vec3 }> {
29
+ const tris = m.indices.length / 3;
30
+ const step = Math.max(1, Math.floor(tris / cap));
31
+ const out: Array<{ p: Vec3; n: Vec3 }> = [];
32
+ const P = m.positions;
33
+ const v = (i: number): Vec3 => [P[i * 3] ?? 0, P[i * 3 + 1] ?? 0, P[i * 3 + 2] ?? 0];
34
+ for (let t = 0; t < tris; t += step) {
35
+ const a = v(m.indices[t * 3] ?? 0);
36
+ const b = v(m.indices[t * 3 + 1] ?? 0);
37
+ const c = v(m.indices[t * 3 + 2] ?? 0);
38
+ const u: Vec3 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
39
+ const w: Vec3 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
40
+ const g: Vec3 = [
41
+ u[1] * w[2] - u[2] * w[1],
42
+ u[2] * w[0] - u[0] * w[2],
43
+ u[0] * w[1] - u[1] * w[0],
44
+ ];
45
+ const len = Math.hypot(...g);
46
+ if (len < 1e-10) continue;
47
+ out.push({
48
+ p: [(a[0] + b[0] + c[0]) / 3, (a[1] + b[1] + c[1]) / 3, (a[2] + b[2] + c[2]) / 3],
49
+ n: [g[0] / len, g[1] / len, g[2] / len],
50
+ });
51
+ }
52
+ return out;
53
+ }
54
+
55
+ interface Scene {
56
+ readonly names: string[];
57
+ readonly mesh: Map<string, Mesh>;
58
+ readonly bounds: Map<string, Bounds>;
59
+ grid(name: string): TriangleGrid;
60
+ solid(name: string): SolidIndex;
61
+ }
62
+
63
+ function scene(asset: CompiledAsset): Scene {
64
+ const mesh = worldMeshes(asset.parts);
65
+ const bounds = new Map([...mesh].map(([n, m]) => [n, boundsOf(m)]));
66
+ const grids = new Map<string, TriangleGrid>();
67
+ const solids = new Map<string, SolidIndex>();
68
+ const lazy =
69
+ <T>(cache: Map<string, T>, make: (m: Mesh) => T) =>
70
+ (name: string): T => {
71
+ const hit = cache.get(name);
72
+ if (hit) return hit;
73
+ const made = make(
74
+ mesh.get(name) ?? {
75
+ positions: new Float32Array(),
76
+ normals: new Float32Array(),
77
+ uvs: new Float32Array(),
78
+ colors: new Float32Array(),
79
+ indices: new Uint32Array(),
80
+ },
81
+ );
82
+ cache.set(name, made);
83
+ return made;
84
+ };
85
+ return {
86
+ names: [...mesh.keys()],
87
+ mesh,
88
+ bounds,
89
+ grid: lazy(grids, (m) => new TriangleGrid(m)),
90
+ solid: lazy(solids, (m) => new SolidIndex(m)),
91
+ };
92
+ }
93
+
94
+ const ON = 2e-4;
95
+ const PROBE = 2e-3;
96
+
97
+ export const zFighting: Gate = (asset) => {
98
+ const s = scene(asset);
99
+ const out: Finding[] = [];
100
+ for (const a of s.names) {
101
+ const ba = s.bounds.get(a);
102
+ const ma = s.mesh.get(a);
103
+ if (!ba || !ma) continue;
104
+ const samples = faceSamples(ma, 256);
105
+ for (const b of s.names) {
106
+ const bb = s.bounds.get(b);
107
+ if (a === b || !bb || !overlaps(ba, bb, ON)) continue;
108
+ let hits = 0;
109
+ for (const { p, n } of samples) {
110
+ if (!overlaps({ min: p, max: p }, bb, ON) || s.grid(b).nearest(p, ON) > ON) continue;
111
+ const solid = s.solid(b);
112
+ const behind = solid.inside([
113
+ p[0] - n[0] * PROBE,
114
+ p[1] - n[1] * PROBE,
115
+ p[2] - n[2] * PROBE,
116
+ ]);
117
+ const front = solid.inside([p[0] + n[0] * PROBE, p[1] + n[1] * PROBE, p[2] + n[2] * PROBE]);
118
+ if (behind && !front) hits++;
119
+ }
120
+ if (hits >= 2)
121
+ out.push({
122
+ gate: 'z-fighting',
123
+ part: a,
124
+ message: `'${a}' has ${hits} face sample(s) lying in '${b}''s surface, facing the same way — it flickers`,
125
+ fix: `stand '${a}' proud of '${b}' by ≥ 2 mm, or inset it, or cut it from '${b}'; coplanar faces never render cleanly`,
126
+ });
127
+ }
128
+ }
129
+ return out;
130
+ };
131
+
132
+ export const hiddenParts: Gate = (asset) => {
133
+ const s = scene(asset);
134
+ return s.names.flatMap((a): Finding[] => {
135
+ const ba = s.bounds.get(a);
136
+ const ma = s.mesh.get(a);
137
+ if (!ba || !ma) return [];
138
+ const pts = faceSamples(ma, 64).map((x) => x.p);
139
+ const stride = Math.max(1, Math.floor(ma.positions.length / 192));
140
+ for (let i = 0; i < ma.positions.length / 3; i += stride)
141
+ pts.push([
142
+ ma.positions[i * 3] ?? 0,
143
+ ma.positions[i * 3 + 1] ?? 0,
144
+ ma.positions[i * 3 + 2] ?? 0,
145
+ ]);
146
+ const host = s.names.find((b) => {
147
+ const bb = s.bounds.get(b);
148
+ if (b === a || !bb || !contains(bb, ba)) return false;
149
+ // Inside, and not merely flush with the host's skin (that is seating, not hiding).
150
+ return (
151
+ pts.every((p) => s.solid(b).inside(p)) &&
152
+ pts.every((p) => s.grid(b).nearest(p, PROBE) > PROBE)
153
+ );
154
+ });
155
+ return host
156
+ ? [
157
+ {
158
+ gate: 'hidden-parts',
159
+ part: a,
160
+ message: `'${a}' (${ma.indices.length / 3} tris) is entirely inside '${host}' — nobody can see it`,
161
+ fix: `delete it, gate it behind ctx.lod, or move it out of '${host}'; hidden triangles are wasted budget`,
162
+ },
163
+ ]
164
+ : [];
165
+ });
166
+ };
@@ -0,0 +1,137 @@
1
+ // Gates: per-part mesh sanity a reviewer would spot at once — non-finite numbers (NaN from a
2
+ // zero-length normalize), vertex colours outside [0,1], shading normals that point against the
3
+ // face winding (black or inside-out lighting), and wasted zero-area triangles.
4
+
5
+ import type { CompiledAsset, Mesh, Part } from '../core/index.ts';
6
+ import type { Finding, Gate } from './types.ts';
7
+
8
+ const parts = (asset: CompiledAsset): Array<[Part, Mesh]> =>
9
+ asset.parts.flatMap((p): Array<[Part, Mesh]> => (p.mesh ? [[p, p.mesh]] : []));
10
+
11
+ const finite = (a: Float32Array): boolean => {
12
+ for (let i = 0; i < a.length; i++) if (!Number.isFinite(a[i] ?? 0)) return false;
13
+ return true;
14
+ };
15
+
16
+ export const nonFinite: Gate = (asset) =>
17
+ parts(asset).flatMap(([p, m]): Finding[] => {
18
+ const bad = (
19
+ [
20
+ ['positions', m.positions],
21
+ ['normals', m.normals],
22
+ ['uvs', m.uvs],
23
+ ['colors', m.colors],
24
+ ] as const
25
+ )
26
+ .filter(([, a]) => !finite(a))
27
+ .map(([n]) => n);
28
+ const maxIndex = m.positions.length / 3;
29
+ const outOfRange = m.indices.some((i) => i >= maxIndex);
30
+ if (outOfRange) bad.push('indices' as never);
31
+ return bad.length
32
+ ? [
33
+ {
34
+ gate: 'non-finite',
35
+ part: p.name,
36
+ message: `'${p.name}' has NaN/Infinity or out-of-range data in ${bad.join(', ')}`,
37
+ fix: 'look for a zero-length normalize, a divide by a zero count or segment, or an index past the vertex count',
38
+ },
39
+ ]
40
+ : [];
41
+ });
42
+
43
+ export const vertexColors: Gate = (asset) =>
44
+ parts(asset).flatMap(([p, m]): Finding[] => {
45
+ let lo = Infinity;
46
+ let hi = -Infinity;
47
+ for (const c of m.colors) {
48
+ if (c < lo) lo = c;
49
+ if (c > hi) hi = c;
50
+ }
51
+ return lo < -1e-6 || hi > 1 + 1e-6
52
+ ? [
53
+ {
54
+ gate: 'vertex-colors',
55
+ part: p.name,
56
+ message: `'${p.name}' vertex colour spans ${lo.toFixed(3)}…${hi.toFixed(3)}, outside linear [0,1]`,
57
+ fix: 'colours are linear 0–1 (use the palette / material helpers); clamp after grime or livery maths',
58
+ },
59
+ ]
60
+ : [];
61
+ });
62
+
63
+ /** Per triangle: geometric normal (from winding) vs the mean of its three vertex normals. */
64
+ function windingStats(m: Mesh): {
65
+ flipped: number;
66
+ area: number;
67
+ flippedArea: number;
68
+ counted: number;
69
+ } {
70
+ const P = m.positions;
71
+ const N = m.normals;
72
+ let flipped = 0;
73
+ let area = 0;
74
+ let flippedArea = 0;
75
+ let counted = 0;
76
+ for (let t = 0; t < m.indices.length; t += 3) {
77
+ const a = (m.indices[t] ?? 0) * 3;
78
+ const b = (m.indices[t + 1] ?? 0) * 3;
79
+ const c = (m.indices[t + 2] ?? 0) * 3;
80
+ const ux = (P[b] ?? 0) - (P[a] ?? 0);
81
+ const uy = (P[b + 1] ?? 0) - (P[a + 1] ?? 0);
82
+ const uz = (P[b + 2] ?? 0) - (P[a + 2] ?? 0);
83
+ const vx = (P[c] ?? 0) - (P[a] ?? 0);
84
+ const vy = (P[c + 1] ?? 0) - (P[a + 1] ?? 0);
85
+ const vz = (P[c + 2] ?? 0) - (P[a + 2] ?? 0);
86
+ const gx = uy * vz - uz * vy;
87
+ const gy = uz * vx - ux * vz;
88
+ const gz = ux * vy - uy * vx;
89
+ const len = Math.hypot(gx, gy, gz);
90
+ if (len < 1e-12) continue;
91
+ counted++;
92
+ const nx = (N[a] ?? 0) + (N[b] ?? 0) + (N[c] ?? 0);
93
+ const ny = (N[a + 1] ?? 0) + (N[b + 1] ?? 0) + (N[c + 1] ?? 0);
94
+ const nz = (N[a + 2] ?? 0) + (N[b + 2] ?? 0) + (N[c + 2] ?? 0);
95
+ area += len;
96
+ // Against the face by more than ~95°: the lighting is inside out, not just smoothed.
97
+ if ((gx * nx + gy * ny + gz * nz) / (len * (Math.hypot(nx, ny, nz) || 1)) < -0.1) {
98
+ flipped++;
99
+ flippedArea += len;
100
+ }
101
+ }
102
+ return { flipped, area, flippedArea, counted };
103
+ }
104
+
105
+ /** Shading normals must agree with the winding (the side a renderer culls/lights). */
106
+ export const normalsMatchWinding: Gate = (asset) =>
107
+ parts(asset).flatMap(([p, m]): Finding[] => {
108
+ const s = windingStats(m);
109
+ // 1% of a part's area tolerates a few pinched poles on smooth caps.
110
+ return s.flipped > 0 && s.flippedArea > s.area * 0.01
111
+ ? [
112
+ {
113
+ gate: 'normals-winding',
114
+ part: p.name,
115
+ message: `'${p.name}': ${s.flipped} triangle(s) (${((100 * s.flippedArea) / s.area).toFixed(1)}% of its area) have vertex normals opposing their face — inverted winding, or normals smoothed across a hard crease`,
116
+ fix: 'split normals at creases (lathe crease angle / weightedNormals hardAngle), or fix the mirrored piece: a negative-scale place() flips winding but not normals',
117
+ },
118
+ ]
119
+ : [];
120
+ });
121
+
122
+ /** Zero-area triangles: invisible, but they cost index bandwidth and skew QA sampling. */
123
+ export const degenerateTriangles: Gate = (asset) =>
124
+ parts(asset).flatMap(([p, m]): Finding[] => {
125
+ const tris = m.indices.length / 3;
126
+ const zero = tris - windingStats(m).counted;
127
+ return zero > tris * 0.05
128
+ ? [
129
+ {
130
+ gate: 'degenerate-triangles',
131
+ part: p.name,
132
+ message: `'${p.name}': ${zero} of ${tris} triangles have zero area`,
133
+ fix: 'a ring/lathe with a zero radius emits fans of slivers: cap it with a point instead, or skip the segment',
134
+ },
135
+ ]
136
+ : [];
137
+ });
@@ -0,0 +1,17 @@
1
+ // QA vocabulary shared by every gate.
2
+
3
+ import type { CompiledAsset } from '../core/index.ts';
4
+
5
+ export interface Finding {
6
+ readonly gate: string;
7
+ readonly part?: string;
8
+ readonly message: string;
9
+ readonly fix: string;
10
+ }
11
+
12
+ export type Gate = (asset: CompiledAsset) => Finding[];
13
+
14
+ /** Contact tolerance: surfaces closer than this touch (metres). */
15
+ export const CONTACT_EPS = 0.02;
16
+ /** Clash tolerance: penetration deeper than this is a clash (metres). */
17
+ export const CLASH_EPS = 0.01;
@@ -0,0 +1,115 @@
1
+ // Beauty render: the zero-GPU product shot for READMEs, gallery cards and the review pack.
2
+ // Perspective 3/4 hero camera, trim-sheet textures × vertex colour with normal + ORM maps, a key
3
+ // sun with soft PCF shadows, sky fill with multi-direction ambient occlusion, a seamless studio
4
+ // floor that only shows the shadows, 2× supersampling. Deterministic: same asset → same bytes.
5
+
6
+ import { type CompiledAsset, encodePng } from '../core/index.ts';
7
+ import type { PaintedSheet } from '../material/index.ts';
8
+ import { DEFAULT_SHEET, paintTrimSheet } from '../material/index.ts';
9
+ import { frameOrbit, type PerspectiveCamera, scaled, zoomWindow } from './camera.ts';
10
+ import { depthMap } from './depth-map.ts';
11
+ import { floorGrid } from './ground.ts';
12
+ import { pcfTaps, studioRig } from './lighting.ts';
13
+ import { checkBeautyOptions } from './options.ts';
14
+ import { resolve } from './resolve.ts';
15
+ import { sceneOf } from './scene.ts';
16
+ import type { ShadeContext } from './shade.ts';
17
+ import { sample, sheetTextures } from './texture.ts';
18
+ import { rasteriseVis } from './vis-buffer.ts';
19
+
20
+ export interface BeautyOptions {
21
+ /** Output size in pixels. Default 1600 × 900. */
22
+ readonly width?: number;
23
+ readonly height?: number;
24
+ /** Camera orbit, degrees: azimuth about Z from the front (+X), −35 = front-left 3/4. */
25
+ readonly azDeg?: number;
26
+ /** Elevation above the horizon, degrees (−10…89). Default 18. */
27
+ readonly elDeg?: number;
28
+ /** Vertical field of view, degrees. Default 30. */
29
+ readonly fovDeg?: number;
30
+ /** Samples per pixel edge (1–4). Default 2 (4 samples per pixel). */
31
+ readonly supersample?: number;
32
+ /** The painted trim sheet; default paints `DEFAULT_SHEET` at 1024² once per process. */
33
+ readonly sheet?: PaintedSheet;
34
+ /** Zoom into the default framing: window centre (0–1, 0–1) of the full shot and a zoom ≥ 1. */
35
+ readonly window?: { readonly x: number; readonly y: number; readonly zoom: number };
36
+ /** Frame with this asset instead (e.g. LOD0's framing for a LOD2 render, so they line up). */
37
+ readonly frameFrom?: CompiledAsset;
38
+ }
39
+
40
+ export interface BeautyRender {
41
+ readonly width: number;
42
+ readonly height: number;
43
+ /** RGBA8, sRGB, opaque. */
44
+ readonly rgba: Uint8Array;
45
+ /** Asset coverage per pixel, 0–255 (the silhouette, antialiased). */
46
+ readonly mask: Uint8Array;
47
+ /** Distinct triangles visible per cell of a 16 × 9 grid — where the detail is. */
48
+ readonly detail: Uint32Array;
49
+ }
50
+
51
+ let defaultSheet: PaintedSheet | undefined;
52
+ const sheetOf = (o: BeautyOptions): PaintedSheet => {
53
+ if (o.sheet) return o.sheet;
54
+ defaultSheet ??= paintTrimSheet({ ...DEFAULT_SHEET, size: 1024 });
55
+ return defaultSheet;
56
+ };
57
+
58
+ export function cameraFor(asset: CompiledAsset, o: BeautyOptions = {}): PerspectiveCamera {
59
+ const v = checkBeautyOptions(o);
60
+ const cam = frameOrbit(
61
+ sceneOf(o.frameFrom ?? asset).positions,
62
+ { azDeg: v.azDeg, elDeg: v.elDeg, fovDeg: v.fovDeg, fill: 0.8 },
63
+ v.width,
64
+ v.height,
65
+ );
66
+ return o.window ? zoomWindow(cam, o.window.x, o.window.y, o.window.zoom) : cam;
67
+ }
68
+
69
+ export function renderBeauty(asset: CompiledAsset, o: BeautyOptions = {}): BeautyRender {
70
+ const v = checkBeautyOptions(o);
71
+ const ss = v.supersample;
72
+ const scene = sceneOf(asset);
73
+ const tex = sheetTextures(sheetOf(o));
74
+ const rig = studioRig(v.azDeg);
75
+ const texel = new Float32Array(4);
76
+ const alpha = (t: number, b1: number, b2: number, mip = 2): boolean => {
77
+ const I = scene.indices;
78
+ const U = scene.uvs;
79
+ const b0 = 1 - b1 - b2;
80
+ const a = (I[t * 3] ?? 0) * 2;
81
+ const b = (I[t * 3 + 1] ?? 0) * 2;
82
+ const c = (I[t * 3 + 2] ?? 0) * 2;
83
+ const u = (U[a] ?? 0) * b0 + (U[b] ?? 0) * b1 + (U[c] ?? 0) * b2;
84
+ const w = (U[a + 1] ?? 0) * b0 + (U[b + 1] ?? 0) * b1 + (U[c + 1] ?? 0) * b2;
85
+ sample(tex.albedo, mip, u, w, texel);
86
+ return (texel[3] ?? 1) >= 0.5;
87
+ };
88
+ const cam = scaled(cameraFor(asset, o), ss);
89
+ const vis = rasteriseVis(scene, cam, tex.size, alpha);
90
+ const extent = Math.max(...scene.max.map((m, k) => m - (scene.min[k] ?? 0)));
91
+ // Map resolutions follow the output: full quality at hero sizes, cheap for thumbnails/tests.
92
+ const px = Math.max(v.width, v.height) * ss;
93
+ const res = (lo: number, hi: number, f: number): number =>
94
+ Math.max(lo, Math.min(hi, Math.round(px * f)));
95
+ const sunMap = depthMap(scene, rig.sun, res(256, 2048, 1), undefined, alpha);
96
+ const ctx: ShadeContext = {
97
+ scene,
98
+ cam,
99
+ tex,
100
+ rig,
101
+ sunMap,
102
+ aoMaps: rig.skyDirs.map((d) => depthMap(scene, d, res(96, 384, 0.25), undefined, alpha)),
103
+ // Penumbra ~0.4% of the asset's size: soft enough to read as a studio key, never mushy.
104
+ pcf: pcfTaps(Math.max(1.2, 0.004 * extent * sunMap.k)),
105
+ reach: 0.12 * extent,
106
+ };
107
+ const floor = floorGrid(ctx, res(96, 512, 0.25));
108
+ return resolve(ctx, vis, floor, v.width, v.height, ss);
109
+ }
110
+
111
+ /** The beauty render as PNG bytes. */
112
+ export function beauty(asset: CompiledAsset, o: BeautyOptions = {}): Uint8Array {
113
+ const r = renderBeauty(asset, o);
114
+ return encodePng(r.width, r.height, r.rgba);
115
+ }
@@ -0,0 +1,142 @@
1
+ // Perspective camera for beauty renders: orbit (azimuth/elevation about the asset), auto-framed so
2
+ // the asset fills the frame, with an optional zoomed sub-window (a true high-res crop of the same
3
+ // shot, used for detail close-ups). Z up, +X front; azimuth 0 looks at the front, −35° is a 3/4.
4
+
5
+ export interface PerspectiveCamera {
6
+ readonly eye: readonly [number, number, number];
7
+ /** Unit right, up, forward (view direction). */
8
+ readonly r: readonly [number, number, number];
9
+ readonly u: readonly [number, number, number];
10
+ readonly f: readonly [number, number, number];
11
+ /** Pixels per unit of tan(angle) — the focal length in pixels. */
12
+ readonly focal: number;
13
+ readonly cx: number;
14
+ readonly cy: number;
15
+ readonly width: number;
16
+ readonly height: number;
17
+ }
18
+
19
+ export interface Orbit {
20
+ readonly azDeg: number;
21
+ readonly elDeg: number;
22
+ readonly fovDeg: number;
23
+ /** Fraction of the frame the asset may fill (0–1). */
24
+ readonly fill: number;
25
+ }
26
+
27
+ type V3 = [number, number, number];
28
+
29
+ function basis(azDeg: number, elDeg: number): { dir: V3; r: V3; u: V3; f: V3 } {
30
+ const az = (azDeg * Math.PI) / 180;
31
+ const el = (elDeg * Math.PI) / 180;
32
+ const dir: V3 = [Math.cos(el) * Math.cos(az), Math.cos(el) * Math.sin(az), Math.sin(el)];
33
+ const f: V3 = [-dir[0], -dir[1], -dir[2]];
34
+ const rl = Math.hypot(f[1], f[0]) || 1;
35
+ const r: V3 = [f[1] / rl, -f[0] / rl, 0];
36
+ const u: V3 = [r[1] * f[2] - r[2] * f[1], r[2] * f[0] - r[0] * f[2], r[0] * f[1] - r[1] * f[0]];
37
+ return { dir, r, u, f };
38
+ }
39
+
40
+ /** Project world xyz → [screen x, screen y, view depth]. Depth ≤ 0 means behind the eye. */
41
+ export function project(c: PerspectiveCamera, x: number, y: number, z: number): V3 {
42
+ const dx = x - c.eye[0];
43
+ const dy = y - c.eye[1];
44
+ const dz = z - c.eye[2];
45
+ const w = dx * c.f[0] + dy * c.f[1] + dz * c.f[2];
46
+ const sx = dx * c.r[0] + dy * c.r[1] + dz * c.r[2];
47
+ const sy = dx * c.u[0] + dy * c.u[1] + dz * c.u[2];
48
+ return [c.cx + (sx / w) * c.focal, c.cy - (sy / w) * c.focal, w];
49
+ }
50
+
51
+ /**
52
+ * Orbit camera framing `points` (xyz triples, world) in a width × height image. Iterates a few
53
+ * times: re-centre on the projected extent, then dolly so the larger axis fills `fill`.
54
+ */
55
+ export function frameOrbit(
56
+ points: Float32Array,
57
+ o: Orbit,
58
+ width: number,
59
+ height: number,
60
+ ): PerspectiveCamera {
61
+ const { dir, r, u, f } = basis(o.azDeg, o.elDeg);
62
+ const stride = Math.max(1, Math.floor(points.length / 3 / 20000)) * 3;
63
+ let lo: V3 = [Infinity, Infinity, Infinity];
64
+ let hi: V3 = [-Infinity, -Infinity, -Infinity];
65
+ for (let i = 0; i < points.length; i += 3)
66
+ for (let k = 0; k < 3; k++) {
67
+ const v = points[i + k] ?? 0;
68
+ lo[k] = Math.min(lo[k] ?? 0, v);
69
+ hi[k] = Math.max(hi[k] ?? 0, v);
70
+ }
71
+ if (points.length === 0) {
72
+ lo = [-0.5, -0.5, -0.5];
73
+ hi = [0.5, 0.5, 0.5];
74
+ }
75
+ const target: V3 = [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2];
76
+ const radius = Math.max(Math.hypot(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]) / 2, 0.05);
77
+ const tanY = Math.tan((o.fovDeg * Math.PI) / 360);
78
+ const focal = height / 2 / tanY;
79
+ let dist = radius / Math.sin(Math.atan(tanY)) + radius;
80
+ const at = (d: number): PerspectiveCamera => ({
81
+ eye: [target[0] + dir[0] * d, target[1] + dir[1] * d, target[2] + dir[2] * d],
82
+ r,
83
+ u,
84
+ f,
85
+ focal,
86
+ cx: width / 2,
87
+ cy: height / 2,
88
+ width,
89
+ height,
90
+ });
91
+ for (let iter = 0; iter < 6; iter++) {
92
+ const cam = at(dist);
93
+ let x0 = Infinity;
94
+ let x1 = -Infinity;
95
+ let y0 = Infinity;
96
+ let y1 = -Infinity;
97
+ for (let i = 0; i < points.length; i += stride) {
98
+ const p = project(cam, points[i] ?? 0, points[i + 1] ?? 0, points[i + 2] ?? 0);
99
+ x0 = Math.min(x0, p[0]);
100
+ x1 = Math.max(x1, p[0]);
101
+ y0 = Math.min(y0, p[1]);
102
+ y1 = Math.max(y1, p[1]);
103
+ }
104
+ if (!Number.isFinite(x0)) break;
105
+ // Shift the target by the screen-space offset of the extent's centre, at the target's depth.
106
+ const k = dist / focal;
107
+ const ox = ((x0 + x1) / 2 - width / 2) * k;
108
+ const oy = -((y0 + y1) / 2 - height / 2) * k;
109
+ for (let a = 0; a < 3; a++) target[a] = (target[a] ?? 0) + (r[a] ?? 0) * ox + (u[a] ?? 0) * oy;
110
+ const need = Math.max((x1 - x0) / (width * o.fill), (y1 - y0) / (height * o.fill));
111
+ // Perspective: dolly on the part of the distance in front of the target, not through it.
112
+ dist = Math.max(dist * need ** 0.85, radius * 1.05);
113
+ }
114
+ return at(dist);
115
+ }
116
+
117
+ /** The same shot through a zoomed window: (nx, ny) ∈ [0,1]² is the window centre, k the zoom. */
118
+ export function zoomWindow(
119
+ c: PerspectiveCamera,
120
+ nx: number,
121
+ ny: number,
122
+ k: number,
123
+ ): PerspectiveCamera {
124
+ return {
125
+ ...c,
126
+ focal: c.focal * k,
127
+ cx: c.width / 2 - (nx * c.width - c.cx) * k,
128
+ cy: c.height / 2 - (ny * c.height - c.cy) * k,
129
+ };
130
+ }
131
+
132
+ /** Same camera at another resolution (supersampling): every pixel quantity scales by s. */
133
+ export function scaled(c: PerspectiveCamera, s: number): PerspectiveCamera {
134
+ return {
135
+ ...c,
136
+ focal: c.focal * s,
137
+ cx: c.cx * s,
138
+ cy: c.cy * s,
139
+ width: c.width * s,
140
+ height: c.height * s,
141
+ };
142
+ }