@developerz.ai/ts23d 0.0.0-stage → 0.1.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 (524) 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 +79 -0
  13. package/CLAUDE.md +85 -0
  14. package/LICENSE +21 -0
  15. package/README.md +380 -2
  16. package/dist/chunk-11v28977.js +1575 -0
  17. package/dist/chunk-11v28977.js.map +30 -0
  18. package/dist/chunk-1ypfs7ga.js +3571 -0
  19. package/dist/chunk-1ypfs7ga.js.map +42 -0
  20. package/dist/chunk-4ddzsqtc.js +464 -0
  21. package/dist/chunk-4ddzsqtc.js.map +19 -0
  22. package/dist/chunk-a8fxakcj.js +19 -0
  23. package/dist/chunk-a8fxakcj.js.map +11 -0
  24. package/dist/chunk-gcyp9b4d.js +755 -0
  25. package/dist/chunk-gcyp9b4d.js.map +22 -0
  26. package/dist/chunk-kgvbpqqw.js +44 -0
  27. package/dist/chunk-kgvbpqqw.js.map +10 -0
  28. package/dist/chunk-kvcep7ft.js +1919 -0
  29. package/dist/chunk-kvcep7ft.js.map +43 -0
  30. package/dist/chunk-ravbb06q.js +89 -0
  31. package/dist/chunk-ravbb06q.js.map +11 -0
  32. package/dist/chunk-w1zyhj40.js +1031 -0
  33. package/dist/chunk-w1zyhj40.js.map +21 -0
  34. package/dist/chunk-wtqhq529.js +2532 -0
  35. package/dist/chunk-wtqhq529.js.map +43 -0
  36. package/dist/chunk-xnt2sdhf.js +836 -0
  37. package/dist/chunk-xnt2sdhf.js.map +17 -0
  38. package/dist/cli/args.d.ts +24 -0
  39. package/dist/cli/bin.d.ts +2 -0
  40. package/dist/cli/bin.js +307 -0
  41. package/dist/cli/bin.js.map +14 -0
  42. package/dist/cli/load.d.ts +2 -0
  43. package/dist/cli/manifest.d.ts +49 -0
  44. package/dist/cli/render-cmds.d.ts +12 -0
  45. package/dist/cli/run.d.ts +1 -0
  46. package/dist/core/asset.d.ts +10 -0
  47. package/dist/core/definition.d.ts +148 -0
  48. package/dist/core/errors.d.ts +17 -0
  49. package/dist/core/family.d.ts +23 -0
  50. package/dist/core/index.d.ts +9 -0
  51. package/dist/core/index.js +69 -0
  52. package/dist/core/index.js.map +9 -0
  53. package/dist/core/mesh.d.ts +35 -0
  54. package/dist/core/part.d.ts +114 -0
  55. package/dist/core/png.d.ts +1 -0
  56. package/dist/core/rng.d.ts +12 -0
  57. package/dist/core/transform.d.ts +20 -0
  58. package/dist/core/vec.d.ts +11 -0
  59. package/dist/core/world.d.ts +16 -0
  60. package/dist/export/bin-writer.d.ts +16 -0
  61. package/dist/export/container.d.ts +3 -0
  62. package/dist/export/glb.d.ts +38 -0
  63. package/dist/export/index.d.ts +4 -0
  64. package/dist/export/index.js +26 -0
  65. package/dist/export/index.js.map +9 -0
  66. package/dist/export/lights.d.ts +8 -0
  67. package/dist/export/materials.d.ts +23 -0
  68. package/dist/export/merge-pivots.d.ts +6 -0
  69. package/dist/export/metadata.d.ts +19 -0
  70. package/dist/export/motion.d.ts +9 -0
  71. package/dist/export/optimize-parts.d.ts +26 -0
  72. package/dist/export/primitive.d.ts +3 -0
  73. package/dist/export/tangents.d.ts +2 -0
  74. package/dist/export/textures.d.ts +22 -0
  75. package/dist/export/version.d.ts +2 -0
  76. package/dist/grammar/index.d.ts +2 -0
  77. package/dist/grammar/index.js +16 -0
  78. package/dist/grammar/index.js.map +9 -0
  79. package/dist/grammar/split.d.ts +21 -0
  80. package/dist/grammar/whyte.d.ts +5 -0
  81. package/dist/index.d.ts +11 -0
  82. package/dist/index.js +411 -0
  83. package/dist/index.js.map +9 -0
  84. package/dist/kinds/building.d.ts +44 -0
  85. package/dist/kinds/faces.d.ts +27 -0
  86. package/dist/kinds/head-armour.d.ts +9 -0
  87. package/dist/kinds/head-beard.d.ts +6 -0
  88. package/dist/kinds/head-bust.d.ts +4 -0
  89. package/dist/kinds/head-extras.d.ts +15 -0
  90. package/dist/kinds/head-face.d.ts +6 -0
  91. package/dist/kinds/head-geom.d.ts +7 -0
  92. package/dist/kinds/head-hair.d.ts +4 -0
  93. package/dist/kinds/head-layout.d.ts +29 -0
  94. package/dist/kinds/head-mesh.d.ts +27 -0
  95. package/dist/kinds/head-params.d.ts +108 -0
  96. package/dist/kinds/head-presets.d.ts +19 -0
  97. package/dist/kinds/head-rim.d.ts +10 -0
  98. package/dist/kinds/head.d.ts +39 -0
  99. package/dist/kinds/hull-lines.d.ts +65 -0
  100. package/dist/kinds/hull-skin.d.ts +6 -0
  101. package/dist/kinds/hull.d.ts +14 -0
  102. package/dist/kinds/index.d.ts +16 -0
  103. package/dist/kinds/index.js +78 -0
  104. package/dist/kinds/index.js.map +9 -0
  105. package/dist/kinds/mass.d.ts +15 -0
  106. package/dist/kinds/openings.d.ts +30 -0
  107. package/dist/kinds/planned.d.ts +6 -0
  108. package/dist/kinds/roof-chimney.d.ts +16 -0
  109. package/dist/kinds/roof-shape.d.ts +64 -0
  110. package/dist/kinds/roof.d.ts +31 -0
  111. package/dist/kinds/vegetation-fronds.d.ts +15 -0
  112. package/dist/kinds/vegetation-impostor.d.ts +9 -0
  113. package/dist/kinds/vegetation-leaves.d.ts +15 -0
  114. package/dist/kinds/vegetation-mesh.d.ts +21 -0
  115. package/dist/kinds/vegetation-params.d.ts +90 -0
  116. package/dist/kinds/vegetation-presets.d.ts +20 -0
  117. package/dist/kinds/vegetation-skeleton.d.ts +20 -0
  118. package/dist/kinds/vegetation.d.ts +42 -0
  119. package/dist/kinds/vehicle.d.ts +42 -0
  120. package/dist/material/generator.d.ts +4 -0
  121. package/dist/material/index.d.ts +4 -0
  122. package/dist/material/index.js +38 -0
  123. package/dist/material/index.js.map +9 -0
  124. package/dist/material/paint/bond.d.ts +3 -0
  125. package/dist/material/paint/canvas.d.ts +32 -0
  126. package/dist/material/paint/cell.d.ts +4 -0
  127. package/dist/material/paint/cells-round.d.ts +5 -0
  128. package/dist/material/paint/cells.d.ts +7 -0
  129. package/dist/material/paint/compose.d.ts +5 -0
  130. package/dist/material/paint/concrete.d.ts +3 -0
  131. package/dist/material/paint/courses.d.ts +31 -0
  132. package/dist/material/paint/decode-png.d.ts +6 -0
  133. package/dist/material/paint/default-sheet.d.ts +2 -0
  134. package/dist/material/paint/foliage-broad.d.ts +5 -0
  135. package/dist/material/paint/foliage-common.d.ts +35 -0
  136. package/dist/material/paint/foliage-compound.d.ts +4 -0
  137. package/dist/material/paint/foliage-conifer.d.ts +5 -0
  138. package/dist/material/paint/foliage-raster.d.ts +26 -0
  139. package/dist/material/paint/foliage.d.ts +3 -0
  140. package/dist/material/paint/masks.d.ts +3 -0
  141. package/dist/material/paint/noise.d.ts +9 -0
  142. package/dist/material/paint/normal.d.ts +8 -0
  143. package/dist/material/paint/orm.d.ts +8 -0
  144. package/dist/material/paint/planks.d.ts +3 -0
  145. package/dist/material/paint/plates.d.ts +3 -0
  146. package/dist/material/paint/shapes.d.ts +8 -0
  147. package/dist/material/paint/slate.d.ts +3 -0
  148. package/dist/material/paint/stamp.d.ts +3 -0
  149. package/dist/material/paint/strip.d.ts +3 -0
  150. package/dist/material/paint/surface.d.ts +5 -0
  151. package/dist/material/paint/types.d.ts +35 -0
  152. package/dist/material/paint/validate.d.ts +3 -0
  153. package/dist/material/palette.d.ts +12 -0
  154. package/dist/material/shader-godot.d.ts +2 -0
  155. package/dist/material/shader-three.d.ts +2 -0
  156. package/dist/material/shader.d.ts +46 -0
  157. package/dist/material/trim-sheet.d.ts +22 -0
  158. package/dist/neural/generator.d.ts +25 -0
  159. package/dist/neural/images.d.ts +17 -0
  160. package/dist/neural/index.d.ts +2 -0
  161. package/dist/neural/index.js +14 -0
  162. package/dist/neural/index.js.map +9 -0
  163. package/dist/passes/bake-ao.d.ts +17 -0
  164. package/dist/passes/grime.d.ts +15 -0
  165. package/dist/passes/index.d.ts +6 -0
  166. package/dist/passes/index.js +22 -0
  167. package/dist/passes/index.js.map +9 -0
  168. package/dist/passes/livery.d.ts +19 -0
  169. package/dist/passes/optimize-cache.d.ts +15 -0
  170. package/dist/passes/optimize-weld.d.ts +15 -0
  171. package/dist/passes/optimize.d.ts +28 -0
  172. package/dist/passes/ray-grid.d.ts +23 -0
  173. package/dist/passes/weighted-normals.d.ts +8 -0
  174. package/dist/primitives/beam.d.ts +17 -0
  175. package/dist/primitives/bevel-box.d.ts +13 -0
  176. package/dist/primitives/box.d.ts +16 -0
  177. package/dist/primitives/cell.d.ts +20 -0
  178. package/dist/primitives/check.d.ts +12 -0
  179. package/dist/primitives/closed.d.ts +11 -0
  180. package/dist/primitives/erosion.d.ts +22 -0
  181. package/dist/primitives/font-5x7.d.ts +3 -0
  182. package/dist/primitives/heightfield.d.ts +16 -0
  183. package/dist/primitives/index.d.ts +23 -0
  184. package/dist/primitives/index.js +81 -0
  185. package/dist/primitives/index.js.map +9 -0
  186. package/dist/primitives/lathe-facets.d.ts +8 -0
  187. package/dist/primitives/lathe.d.ts +26 -0
  188. package/dist/primitives/lattice.d.ts +30 -0
  189. package/dist/primitives/loft.d.ts +15 -0
  190. package/dist/primitives/mc-table.d.ts +2 -0
  191. package/dist/primitives/member.d.ts +19 -0
  192. package/dist/primitives/noise.d.ts +34 -0
  193. package/dist/primitives/planar.d.ts +10 -0
  194. package/dist/primitives/prism.d.ts +13 -0
  195. package/dist/primitives/sculpt-edit.d.ts +56 -0
  196. package/dist/primitives/sculpt-tree.d.ts +23 -0
  197. package/dist/primitives/sculpt.d.ts +27 -0
  198. package/dist/primitives/sdf-grid.d.ts +17 -0
  199. package/dist/primitives/sdf-ops.d.ts +37 -0
  200. package/dist/primitives/sdf.d.ts +21 -0
  201. package/dist/primitives/strip-uv.d.ts +11 -0
  202. package/dist/primitives/surface-nets.d.ts +7 -0
  203. package/dist/primitives/sweep.d.ts +19 -0
  204. package/dist/primitives/terrain-color.d.ts +30 -0
  205. package/dist/primitives/terrain-grid.d.ts +36 -0
  206. package/dist/primitives/text-layout.d.ts +22 -0
  207. package/dist/primitives/text.d.ts +27 -0
  208. package/dist/primitives/tiled.d.ts +22 -0
  209. package/dist/primitives/transport.d.ts +17 -0
  210. package/dist/primitives/triangulate.d.ts +2 -0
  211. package/dist/primitives/value-noise.d.ts +10 -0
  212. package/dist/qa/checks.d.ts +11 -0
  213. package/dist/qa/clash.d.ts +2 -0
  214. package/dist/qa/floating.d.ts +5 -0
  215. package/dist/qa/gates.d.ts +12 -0
  216. package/dist/qa/geometry.d.ts +21 -0
  217. package/dist/qa/index.d.ts +12 -0
  218. package/dist/qa/index.js +78 -0
  219. package/dist/qa/index.js.map +9 -0
  220. package/dist/qa/inside.d.ts +12 -0
  221. package/dist/qa/inspect.d.ts +18 -0
  222. package/dist/qa/lods.d.ts +12 -0
  223. package/dist/qa/measures.d.ts +4 -0
  224. package/dist/qa/occlusion.d.ts +3 -0
  225. package/dist/qa/sanity.d.ts +7 -0
  226. package/dist/qa/types.d.ts +12 -0
  227. package/dist/render/beauty.d.ts +40 -0
  228. package/dist/render/camera.d.ts +33 -0
  229. package/dist/render/depth-map.d.ts +38 -0
  230. package/dist/render/font.d.ts +1 -0
  231. package/dist/render/ground.d.ts +13 -0
  232. package/dist/render/index.d.ts +10 -0
  233. package/dist/render/index.js +47 -0
  234. package/dist/render/index.js.map +9 -0
  235. package/dist/render/lighting.d.ts +20 -0
  236. package/dist/render/lod-strip.d.ts +5 -0
  237. package/dist/render/metrics.d.ts +37 -0
  238. package/dist/render/options.d.ts +28 -0
  239. package/dist/render/photo-match.d.ts +19 -0
  240. package/dist/render/raster.d.ts +25 -0
  241. package/dist/render/resolve.d.ts +11 -0
  242. package/dist/render/review-prompt.d.ts +2 -0
  243. package/dist/render/review-warnings.d.ts +34 -0
  244. package/dist/render/review.d.ts +24 -0
  245. package/dist/render/scene.d.ts +18 -0
  246. package/dist/render/shade.d.ts +22 -0
  247. package/dist/render/sheet.d.ts +15 -0
  248. package/dist/render/texture.d.ts +15 -0
  249. package/dist/render/thumbnail.d.ts +8 -0
  250. package/dist/render/vis-buffer.d.ts +18 -0
  251. package/dist/testing/assert.d.ts +21 -0
  252. package/dist/testing/compiled.d.ts +15 -0
  253. package/dist/testing/hash.d.ts +1 -0
  254. package/dist/testing/index.d.ts +4 -0
  255. package/dist/testing/index.js +134 -0
  256. package/dist/testing/index.js.map +13 -0
  257. package/dist/testing/snapshot.d.ts +13 -0
  258. package/docs/README.md +52 -0
  259. package/docs/architecture/README.md +68 -0
  260. package/docs/architecture/adding-a-kind.md +44 -0
  261. package/docs/architecture/adding-a-primitive.md +49 -0
  262. package/docs/architecture/errors.md +47 -0
  263. package/docs/architecture/glb-production.md +214 -0
  264. package/docs/architecture/gltf-capabilities.md +80 -0
  265. package/docs/decisions/001-recipe-compiler-over-ai-meshes.md +27 -0
  266. package/docs/decisions/002-typescript-not-json.md +22 -0
  267. package/docs/decisions/003-shared-trim-sheet.md +21 -0
  268. package/docs/decisions/004-z-up-metres.md +21 -0
  269. package/docs/decisions/005-zero-runtime-deps.md +21 -0
  270. package/docs/decisions/006-single-package-subpath-exports.md +20 -0
  271. package/docs/decisions/007-cpu-contact-sheets.md +21 -0
  272. package/docs/decisions/008-surface-materials.md +21 -0
  273. package/docs/decisions/009-gltf-front-axis.md +21 -0
  274. package/docs/decisions/README.md +15 -0
  275. package/docs/idea/01-vision.md +117 -0
  276. package/docs/idea/02-dsl.md +343 -0
  277. package/docs/idea/03-ir.md +97 -0
  278. package/docs/idea/04-materials.md +114 -0
  279. package/docs/idea/05-agent-loop.md +141 -0
  280. package/docs/idea/06-qa.md +101 -0
  281. package/docs/idea/07-what-fits.md +45 -0
  282. package/docs/idea/08-neural-adapters.md +78 -0
  283. package/docs/idea/09-kinds.md +105 -0
  284. package/docs/landscape.md +49 -0
  285. package/docs/plans/roadmap.md +127 -0
  286. package/examples/build-with-bun.ts +41 -0
  287. package/llms.txt +47 -0
  288. package/package.json +99 -4
  289. package/recipes/README.md +38 -0
  290. package/recipes/asteroid/README.md +46 -0
  291. package/recipes/asteroid/asteroid.ts +336 -0
  292. package/recipes/boulders/README.md +28 -0
  293. package/recipes/boulders/boulders.ts +117 -0
  294. package/recipes/boxcar/README.md +46 -0
  295. package/recipes/boxcar/boxcar.ts +916 -0
  296. package/recipes/cargo_container_scifi/README.md +52 -0
  297. package/recipes/cargo_container_scifi/cargo_container_scifi.ts +461 -0
  298. package/recipes/dropship/README.md +84 -0
  299. package/recipes/dropship/dropship.ts +1144 -0
  300. package/recipes/eiffel_tower/README.md +110 -0
  301. package/recipes/eiffel_tower/eiffel_tower.ts +781 -0
  302. package/recipes/goblin_head/README.md +62 -0
  303. package/recipes/goblin_head/goblin_head.ts +36 -0
  304. package/recipes/human_head/README.md +64 -0
  305. package/recipes/human_head/human_head.ts +37 -0
  306. package/recipes/loco_440/README.md +32 -0
  307. package/recipes/loco_440/loco_440.ts +1404 -0
  308. package/recipes/material_swatches/README.md +21 -0
  309. package/recipes/material_swatches/material_swatches.ts +183 -0
  310. package/recipes/mountain_range/README.md +45 -0
  311. package/recipes/mountain_range/mountain_range.ts +128 -0
  312. package/recipes/oak/README.md +30 -0
  313. package/recipes/oak/oak.ts +19 -0
  314. package/recipes/orc_head/README.md +66 -0
  315. package/recipes/orc_head/orc_head.ts +39 -0
  316. package/recipes/palm/README.md +30 -0
  317. package/recipes/palm/palm.ts +19 -0
  318. package/recipes/pine/README.md +29 -0
  319. package/recipes/pine/pine.ts +19 -0
  320. package/recipes/signal_gantry/README.md +36 -0
  321. package/recipes/signal_gantry/signal_gantry.ts +522 -0
  322. package/recipes/station_halt/README.md +39 -0
  323. package/recipes/station_halt/station_halt.ts +722 -0
  324. package/recipes/t34/README.md +44 -0
  325. package/recipes/t34/t34.ts +1068 -0
  326. package/recipes/tiger_1/README.md +75 -0
  327. package/recipes/tiger_1/tiger_1.ts +1370 -0
  328. package/recipes/water_tower/README.md +40 -0
  329. package/recipes/water_tower/water_tower.ts +542 -0
  330. package/src/cli/args.ts +112 -0
  331. package/src/cli/bin.ts +7 -0
  332. package/src/cli/load.ts +17 -0
  333. package/src/cli/manifest.ts +89 -0
  334. package/src/cli/render-cmds.ts +66 -0
  335. package/src/cli/run.ts +168 -0
  336. package/src/core/asset.ts +128 -0
  337. package/src/core/definition.ts +149 -0
  338. package/src/core/errors.ts +45 -0
  339. package/src/core/family.ts +50 -0
  340. package/src/core/index.ts +66 -0
  341. package/src/core/mesh.ts +114 -0
  342. package/src/core/part.ts +130 -0
  343. package/src/core/png.ts +49 -0
  344. package/src/core/rng.ts +49 -0
  345. package/src/core/transform.ts +90 -0
  346. package/src/core/vec.ts +26 -0
  347. package/src/core/world.ts +80 -0
  348. package/src/export/bin-writer.ts +79 -0
  349. package/src/export/container.ts +35 -0
  350. package/src/export/glb.ts +200 -0
  351. package/src/export/index.ts +18 -0
  352. package/src/export/lights.ts +49 -0
  353. package/src/export/materials.ts +126 -0
  354. package/src/export/merge-pivots.ts +146 -0
  355. package/src/export/metadata.ts +59 -0
  356. package/src/export/motion.ts +70 -0
  357. package/src/export/optimize-parts.ts +80 -0
  358. package/src/export/primitive.ts +35 -0
  359. package/src/export/tangents.ts +75 -0
  360. package/src/export/textures.ts +88 -0
  361. package/src/export/version.ts +6 -0
  362. package/src/grammar/index.ts +4 -0
  363. package/src/grammar/split.ts +87 -0
  364. package/src/grammar/whyte.ts +20 -0
  365. package/src/index.ts +14 -0
  366. package/src/kinds/building.ts +153 -0
  367. package/src/kinds/faces.ts +138 -0
  368. package/src/kinds/head-armour.ts +151 -0
  369. package/src/kinds/head-beard.ts +133 -0
  370. package/src/kinds/head-bust.ts +221 -0
  371. package/src/kinds/head-extras.ts +150 -0
  372. package/src/kinds/head-face.ts +222 -0
  373. package/src/kinds/head-geom.ts +31 -0
  374. package/src/kinds/head-hair.ts +152 -0
  375. package/src/kinds/head-layout.ts +63 -0
  376. package/src/kinds/head-mesh.ts +157 -0
  377. package/src/kinds/head-params.ts +153 -0
  378. package/src/kinds/head-presets.ts +152 -0
  379. package/src/kinds/head-rim.ts +128 -0
  380. package/src/kinds/head.ts +171 -0
  381. package/src/kinds/hull-lines.ts +160 -0
  382. package/src/kinds/hull-skin.ts +83 -0
  383. package/src/kinds/hull.ts +209 -0
  384. package/src/kinds/index.ts +63 -0
  385. package/src/kinds/mass.ts +110 -0
  386. package/src/kinds/openings.ts +141 -0
  387. package/src/kinds/planned.ts +12 -0
  388. package/src/kinds/roof-chimney.ts +100 -0
  389. package/src/kinds/roof-shape.ts +230 -0
  390. package/src/kinds/roof.ts +250 -0
  391. package/src/kinds/vegetation-fronds.ts +126 -0
  392. package/src/kinds/vegetation-impostor.ts +118 -0
  393. package/src/kinds/vegetation-leaves.ts +168 -0
  394. package/src/kinds/vegetation-mesh.ts +174 -0
  395. package/src/kinds/vegetation-params.ts +147 -0
  396. package/src/kinds/vegetation-presets.ts +184 -0
  397. package/src/kinds/vegetation-skeleton.ts +168 -0
  398. package/src/kinds/vegetation.ts +199 -0
  399. package/src/kinds/vehicle.ts +144 -0
  400. package/src/material/generator.ts +56 -0
  401. package/src/material/index.ts +29 -0
  402. package/src/material/paint/bond.ts +60 -0
  403. package/src/material/paint/canvas.ts +91 -0
  404. package/src/material/paint/cell.ts +39 -0
  405. package/src/material/paint/cells-round.ts +119 -0
  406. package/src/material/paint/cells.ts +111 -0
  407. package/src/material/paint/compose.ts +53 -0
  408. package/src/material/paint/concrete.ts +62 -0
  409. package/src/material/paint/courses.ts +85 -0
  410. package/src/material/paint/decode-png.ts +100 -0
  411. package/src/material/paint/default-sheet.ts +61 -0
  412. package/src/material/paint/foliage-broad.ts +126 -0
  413. package/src/material/paint/foliage-common.ts +93 -0
  414. package/src/material/paint/foliage-compound.ts +135 -0
  415. package/src/material/paint/foliage-conifer.ts +143 -0
  416. package/src/material/paint/foliage-raster.ts +196 -0
  417. package/src/material/paint/foliage.ts +23 -0
  418. package/src/material/paint/masks.ts +44 -0
  419. package/src/material/paint/noise.ts +52 -0
  420. package/src/material/paint/normal.ts +52 -0
  421. package/src/material/paint/orm.ts +95 -0
  422. package/src/material/paint/planks.ts +56 -0
  423. package/src/material/paint/plates.ts +72 -0
  424. package/src/material/paint/shapes.ts +28 -0
  425. package/src/material/paint/slate.ts +52 -0
  426. package/src/material/paint/stamp.ts +40 -0
  427. package/src/material/paint/strip.ts +42 -0
  428. package/src/material/paint/surface.ts +60 -0
  429. package/src/material/paint/types.ts +51 -0
  430. package/src/material/paint/validate.ts +58 -0
  431. package/src/material/palette.ts +43 -0
  432. package/src/material/shader-godot.ts +77 -0
  433. package/src/material/shader-three.ts +103 -0
  434. package/src/material/shader.ts +100 -0
  435. package/src/material/trim-sheet.ts +100 -0
  436. package/src/neural/generator.ts +37 -0
  437. package/src/neural/images.ts +27 -0
  438. package/src/neural/index.ts +10 -0
  439. package/src/passes/bake-ao.ts +183 -0
  440. package/src/passes/grime.ts +37 -0
  441. package/src/passes/index.ts +8 -0
  442. package/src/passes/livery.ts +66 -0
  443. package/src/passes/optimize-cache.ts +175 -0
  444. package/src/passes/optimize-weld.ts +147 -0
  445. package/src/passes/optimize.ts +85 -0
  446. package/src/passes/ray-grid.ts +237 -0
  447. package/src/passes/weighted-normals.ts +125 -0
  448. package/src/primitives/beam.ts +60 -0
  449. package/src/primitives/bevel-box.ts +140 -0
  450. package/src/primitives/box.ts +90 -0
  451. package/src/primitives/cell.ts +115 -0
  452. package/src/primitives/check.ts +49 -0
  453. package/src/primitives/closed.ts +74 -0
  454. package/src/primitives/erosion.ts +183 -0
  455. package/src/primitives/font-5x7.ts +117 -0
  456. package/src/primitives/heightfield.ts +101 -0
  457. package/src/primitives/index.ts +56 -0
  458. package/src/primitives/lathe-facets.ts +45 -0
  459. package/src/primitives/lathe.ts +174 -0
  460. package/src/primitives/lattice.ts +131 -0
  461. package/src/primitives/loft.ts +174 -0
  462. package/src/primitives/mc-table.ts +41 -0
  463. package/src/primitives/member.ts +130 -0
  464. package/src/primitives/noise.ts +157 -0
  465. package/src/primitives/planar.ts +55 -0
  466. package/src/primitives/prism.ts +85 -0
  467. package/src/primitives/sculpt-edit.ts +212 -0
  468. package/src/primitives/sculpt-tree.ts +226 -0
  469. package/src/primitives/sculpt.ts +80 -0
  470. package/src/primitives/sdf-grid.ts +131 -0
  471. package/src/primitives/sdf-ops.ts +145 -0
  472. package/src/primitives/sdf.ts +83 -0
  473. package/src/primitives/strip-uv.ts +26 -0
  474. package/src/primitives/surface-nets.ts +161 -0
  475. package/src/primitives/sweep.ts +162 -0
  476. package/src/primitives/terrain-color.ts +85 -0
  477. package/src/primitives/terrain-grid.ts +137 -0
  478. package/src/primitives/text-layout.ts +85 -0
  479. package/src/primitives/text.ts +197 -0
  480. package/src/primitives/tiled.ts +93 -0
  481. package/src/primitives/transport.ts +85 -0
  482. package/src/primitives/triangulate.ts +42 -0
  483. package/src/primitives/value-noise.ts +62 -0
  484. package/src/qa/checks.ts +110 -0
  485. package/src/qa/clash.ts +177 -0
  486. package/src/qa/floating.ts +63 -0
  487. package/src/qa/gates.ts +126 -0
  488. package/src/qa/geometry.ts +187 -0
  489. package/src/qa/index.ts +28 -0
  490. package/src/qa/inside.ts +114 -0
  491. package/src/qa/inspect.ts +107 -0
  492. package/src/qa/lods.ts +97 -0
  493. package/src/qa/measures.ts +67 -0
  494. package/src/qa/occlusion.ts +166 -0
  495. package/src/qa/sanity.ts +137 -0
  496. package/src/qa/types.ts +17 -0
  497. package/src/render/beauty.ts +115 -0
  498. package/src/render/camera.ts +142 -0
  499. package/src/render/depth-map.ts +191 -0
  500. package/src/render/font.ts +76 -0
  501. package/src/render/ground.ts +87 -0
  502. package/src/render/index.ts +34 -0
  503. package/src/render/lighting.ts +95 -0
  504. package/src/render/lod-strip.ts +34 -0
  505. package/src/render/metrics.ts +147 -0
  506. package/src/render/options.ts +81 -0
  507. package/src/render/photo-match.ts +28 -0
  508. package/src/render/raster.ts +143 -0
  509. package/src/render/resolve.ts +109 -0
  510. package/src/render/review-prompt.ts +59 -0
  511. package/src/render/review-warnings.ts +92 -0
  512. package/src/render/review.ts +103 -0
  513. package/src/render/scene.ts +119 -0
  514. package/src/render/shade.ts +199 -0
  515. package/src/render/sheet.ts +88 -0
  516. package/src/render/texture.ts +97 -0
  517. package/src/render/thumbnail.ts +75 -0
  518. package/src/render/vis-buffer.ts +119 -0
  519. package/src/testing/assert.ts +115 -0
  520. package/src/testing/compiled.ts +31 -0
  521. package/src/testing/hash.ts +14 -0
  522. package/src/testing/index.ts +14 -0
  523. package/src/testing/snapshot.ts +37 -0
  524. package/src/types/gltf-validator.d.ts +13 -0
@@ -0,0 +1,79 @@
1
+ // Packs typed arrays into one 4-byte-aligned glTF buffer and records bufferViews + accessors.
2
+
3
+ export const ARRAY_BUFFER = 34962;
4
+ export const ELEMENT_ARRAY_BUFFER = 34963;
5
+ const FLOAT = 5126;
6
+ const UINT32 = 5125;
7
+ const UINT16 = 5123;
8
+
9
+ export interface Json {
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ export class BinWriter {
14
+ private readonly chunks: Uint8Array[] = [];
15
+ length = 0;
16
+ readonly views: Json[] = [];
17
+ readonly accessors: Json[] = [];
18
+
19
+ private push(bytes: Uint8Array, target?: number): number {
20
+ const pad = (4 - (this.length % 4)) % 4;
21
+ if (pad) {
22
+ this.chunks.push(new Uint8Array(pad));
23
+ this.length += pad;
24
+ }
25
+ this.views.push({
26
+ buffer: 0,
27
+ byteOffset: this.length,
28
+ byteLength: bytes.byteLength,
29
+ ...(target ? { target } : {}),
30
+ });
31
+ this.chunks.push(bytes);
32
+ this.length += bytes.byteLength;
33
+ return this.views.length - 1;
34
+ }
35
+
36
+ accessor(
37
+ data: Float32Array | Uint32Array | Uint16Array,
38
+ type: 'SCALAR' | 'VEC2' | 'VEC3' | 'VEC4',
39
+ target?: number,
40
+ bounds = false,
41
+ /** Integer data read as 0..1 (vertex colour as normalised unsigned short). */
42
+ normalized = false,
43
+ ): number {
44
+ const view = this.push(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), target);
45
+ const width = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 }[type];
46
+ const acc: Json = {
47
+ bufferView: view,
48
+ componentType:
49
+ data instanceof Uint32Array ? UINT32 : data instanceof Uint16Array ? UINT16 : FLOAT,
50
+ count: data.length / width,
51
+ type,
52
+ ...(normalized ? { normalized: true } : {}),
53
+ };
54
+ if (bounds) {
55
+ const min = Array<number>(width).fill(Infinity);
56
+ const max = Array<number>(width).fill(-Infinity);
57
+ for (let i = 0; i < data.length; i++) {
58
+ const k = i % width;
59
+ const v = data[i] ?? 0;
60
+ if (v < (min[k] ?? 0)) min[k] = v;
61
+ if (v > (max[k] ?? 0)) max[k] = v;
62
+ }
63
+ acc['min'] = min;
64
+ acc['max'] = max;
65
+ }
66
+ this.accessors.push(acc);
67
+ return this.accessors.length - 1;
68
+ }
69
+
70
+ bytes(): Uint8Array {
71
+ const out = new Uint8Array(Math.ceil(this.length / 4) * 4);
72
+ let o = 0;
73
+ for (const c of this.chunks) {
74
+ out.set(c, o);
75
+ o += c.byteLength;
76
+ }
77
+ return out;
78
+ }
79
+ }
@@ -0,0 +1,35 @@
1
+ // The GLB container: 12-byte header, JSON chunk (space-padded), BIN chunk (zero-padded), every
2
+ // length a multiple of 4 [glTF 2.0 §glb-file-format-specification].
3
+
4
+ import type { Json } from './bin-writer.ts';
5
+
6
+ export function packGlb(gltf: Json, body: Uint8Array): Uint8Array {
7
+ let json = new TextEncoder().encode(JSON.stringify(gltf));
8
+ const jsonPad = (4 - (json.byteLength % 4)) % 4;
9
+ if (jsonPad) {
10
+ const padded = new Uint8Array(json.byteLength + jsonPad).fill(0x20);
11
+ padded.set(json);
12
+ json = padded;
13
+ }
14
+ const total = 12 + 8 + json.byteLength + 8 + body.byteLength;
15
+ const out = new Uint8Array(total);
16
+ const dv = new DataView(out.buffer);
17
+ dv.setUint32(0, 0x46546c67, true);
18
+ dv.setUint32(4, 2, true);
19
+ dv.setUint32(8, total, true);
20
+ dv.setUint32(12, json.byteLength, true);
21
+ dv.setUint32(16, 0x4e4f534a, true);
22
+ out.set(json, 20);
23
+ const binAt = 20 + json.byteLength;
24
+ dv.setUint32(binAt, body.byteLength, true);
25
+ dv.setUint32(binAt + 4, 0x004e4942, true);
26
+ out.set(body, binAt + 8);
27
+ return out;
28
+ }
29
+
30
+ export function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
31
+ const out = new Uint8Array(a.byteLength + b.byteLength);
32
+ out.set(a);
33
+ out.set(b, a.byteLength);
34
+ return out;
35
+ }
@@ -0,0 +1,200 @@
1
+ // glTF 2.0 binary writer. One material (the trim sheet) for the whole asset, one node per part
2
+ // with its pivot as translation, and each part's motion baked as a looping animation clip.
3
+ // Extras a stock viewer understands: light sockets → KHR_lights_punctual, glowing parts →
4
+ // emissive (+ KHR_materials_emissive_strength), provenance → asset.copyright + KHR_xmp_json_ld.
5
+ // Zero dependencies and byte-deterministic: same CompiledAsset in, same bytes out.
6
+
7
+ import type { CompiledAsset, Part } from '../core/index.ts';
8
+ import { quatFromAxisAngle, Ts23dError } from '../core/index.ts';
9
+ import type { OptimizeOptions } from '../passes/index.ts';
10
+ import { BinWriter, type Json } from './bin-writer.ts';
11
+ import { concat, packGlb } from './container.ts';
12
+ import { LIGHTS, lightOf } from './lights.ts';
13
+ import { EMISSIVE_STRENGTH, type GlowRule, glowMaterial, materialsFor } from './materials.ts';
14
+ import { globRe, glowLayer, mergeToPivots } from './merge-pivots.ts';
15
+ import { type GlbMeta, metadata } from './metadata.ts';
16
+ import { motionChannel } from './motion.ts';
17
+ import { type ExportReport, optimizeParts } from './optimize-parts.ts';
18
+ import { writePrimitive } from './primitive.ts';
19
+ import { type SheetBinding, sheetGltf } from './textures.ts';
20
+
21
+ // Viewers auto-play the FIRST clip and glTF has no "paused" flag, so the first clip is `rest`: two
22
+ // keys holding the root's rest rotation. The file opens still; pick `run` to play it.
23
+ // Authoring frame: Z up, +X front. glTF: Y up, +Z front, +X = the asset's left. This root
24
+ // rotation maps +X→+Z, +Z→+Y, +Y→+X (verified numerically), so assets face forward in engines.
25
+ const Z_UP_TO_Y_UP = [-0.5, -0.5, -0.5, 0.5];
26
+ function restClip(bin: BinWriter): Json {
27
+ const input = bin.accessor(new Float32Array([0, 1]), 'SCALAR', undefined, true);
28
+ const output = bin.accessor(new Float32Array([...Z_UP_TO_Y_UP, ...Z_UP_TO_Y_UP]), 'VEC4');
29
+ return {
30
+ name: 'rest',
31
+ samplers: [{ input, output, interpolation: 'STEP' }],
32
+ channels: [{ sampler: 0, target: { node: 0, path: 'rotation' } }],
33
+ };
34
+ }
35
+
36
+ export interface GlbOptions {
37
+ /** Keep every part as its own node (debugging, per-part picking). Default merges to pivots. */
38
+ readonly keepParts?: boolean;
39
+ /**
40
+ * Bake each part's `motion` into a looping `run` clip. Off by default: glTF has no autoplay
41
+ * flag and most viewers play the first clip, while engines usually drive the named pivots
42
+ * from game code. Turn it on for previews, cutscenes and ambient props.
43
+ */
44
+ readonly animations?: boolean;
45
+ /**
46
+ * Bind the painted trim sheet to every material: a `PaintedSheet` embeds the three PNGs (one
47
+ * self-contained file); `{ external: true, prefix? }` references shared `trim_sheet_*.png` by
48
+ * URI. Omitted: no textures, byte-identical to the untextured output.
49
+ */
50
+ readonly sheet?: SheetBinding;
51
+ /**
52
+ * Weld + vertex-cache + vertex-fetch optimisation per primitive (passes/optimize). On by
53
+ * default; `false` writes the meshes exactly as compiled (debugging the IR).
54
+ */
55
+ readonly optimize?: boolean | OptimizeOptions;
56
+ /** Parts that emit light (lamp lenses, lit windows): one emissive material per rule. */
57
+ readonly glow?: readonly GlowRule[];
58
+ /** Provenance: asset.copyright + an XMP packet (title, creators, licence, sources). */
59
+ readonly meta?: GlbMeta;
60
+ }
61
+
62
+ /** The GLB bytes plus what the optimiser did (vertex counts, ACMR before/after, per part). */
63
+ export interface GlbResult {
64
+ readonly glb: Uint8Array;
65
+ readonly report: ExportReport;
66
+ }
67
+
68
+ export function toGlb(input: CompiledAsset, o: GlbOptions = {}): Uint8Array {
69
+ return exportGlb(input, o).glb;
70
+ }
71
+
72
+ function checkGlow(rules: readonly GlowRule[]): void {
73
+ for (const r of rules)
74
+ if ((r.strength ?? 4) < 0 || (r.color ?? [0, 0, 0]).some((c) => c < 0 || c > 1))
75
+ throw new Ts23dError('T3D_RECIPE_INVALID', {
76
+ cause: `glow rule for ${JSON.stringify(r.parts)} has a colour outside 0..1 or a negative strength`,
77
+ fix: 'colour is linear 0..1 (hex() converts sRGB); brightness goes in `strength`',
78
+ });
79
+ }
80
+
81
+ export function exportGlb(input: CompiledAsset, o: GlbOptions = {}): GlbResult {
82
+ const glow = o.glow ?? input.glow ?? [];
83
+ checkGlow(glow);
84
+ const opt = o.optimize === false ? false : o.optimize === true ? {} : (o.optimize ?? {});
85
+ const merged = o.keepParts
86
+ ? input
87
+ : mergeToPivots(
88
+ input,
89
+ glow.map((g) => g.parts),
90
+ );
91
+ const { asset, report } = optimizeParts(merged, opt);
92
+ // optimizeParts copies parts: carry the glow tag across by position.
93
+ const glowRes = glow.map((g) => g.parts.map(globRe));
94
+ const glowOf = (p: Part, i: number): number =>
95
+ glowLayer.get(merged.parts[i] ?? p) ??
96
+ (o.keepParts ? glowRes.findIndex((rs) => rs.some((r) => r.test(p.name))) : -1);
97
+ const mats = materialsFor(
98
+ new Set(asset.parts.flatMap((p) => (p.surface ? [p.surface] : []))),
99
+ o.sheet !== undefined,
100
+ );
101
+ const glowAt = mats.materials.length;
102
+ const materials = [
103
+ ...mats.materials,
104
+ ...glow.map((g, i) => glowMaterial(g, i, o.sheet !== undefined)),
105
+ ];
106
+ const bin = new BinWriter();
107
+ const meshes: Json[] = [];
108
+ const nodes: Json[] = [{ name: asset.name, children: [] as number[] }];
109
+ const index = new Map<string, number>();
110
+ const samplers: Json[] = [];
111
+ const channels: Json[] = [];
112
+ const lights: Json[] = [];
113
+ const attach = (parentId: number, id: number): void => {
114
+ const parent = nodes[parentId];
115
+ if (!parent) return;
116
+ parent['children'] = [...((parent['children'] as number[] | undefined) ?? []), id];
117
+ };
118
+
119
+ asset.parts.forEach((part: Part, i) => {
120
+ const node: Json = { name: part.name, translation: [...part.at] };
121
+ if (part.yaw) node['rotation'] = [...quatFromAxisAngle([0, 0, 1], part.yaw)];
122
+ if (part.emitter || part.socket) {
123
+ node['extras'] = {
124
+ ts23d: {
125
+ ...(part.emitter ? { emitter: part.emitter } : {}),
126
+ ...(part.socket ? { socket: part.socket } : {}),
127
+ },
128
+ };
129
+ }
130
+ if (part.mesh && part.mesh.indices.length > 0) {
131
+ const g = glowOf(part, i);
132
+ const material = g >= 0 ? glowAt + g : (mats.index.get(part.surface ?? 'opaque') ?? 0);
133
+ meshes.push({
134
+ name: part.name,
135
+ primitives: [writePrimitive(bin, part.mesh, material, o.sheet !== undefined)],
136
+ });
137
+ node['mesh'] = meshes.length - 1;
138
+ }
139
+ nodes.push(node);
140
+ const id = nodes.length - 1;
141
+ index.set(part.name, id);
142
+ attach(part.parent === null ? 0 : (index.get(part.parent) ?? 0), id);
143
+ const lit = lightOf(part);
144
+ if (lit) {
145
+ lights.push(lit.light);
146
+ nodes.push({
147
+ name: `${part.name}_light`,
148
+ rotation: [...lit.rotation],
149
+ extensions: { [LIGHTS]: { light: lights.length - 1 } },
150
+ });
151
+ attach(id, nodes.length - 1);
152
+ }
153
+ if (part.motion && o.animations) {
154
+ const { sampler, channel } = motionChannel(bin, part.motion, id, part.at);
155
+ samplers.push(sampler);
156
+ channels.push({ ...channel, sampler: samplers.length - 1 });
157
+ }
158
+ });
159
+
160
+ // Every accessor — the rest clip's included — must be written BEFORE the buffer is sealed.
161
+ const animations = channels.length ? [restClip(bin), { name: 'run', samplers, channels }] : [];
162
+ const geometry = bin.bytes();
163
+ const tex = o.sheet ? sheetGltf(o.sheet, bin.views, geometry.byteLength) : null;
164
+ const body = tex?.tail.byteLength ? concat(geometry, tex.tail) : geometry;
165
+ const meta = metadata(asset.name, o.meta ?? input.meta);
166
+ const used = [
167
+ ...mats.extensionsUsed,
168
+ ...(glow.some((g) => (g.strength ?? 4) !== 1) ? [EMISSIVE_STRENGTH] : []),
169
+ ...(lights.length ? [LIGHTS] : []),
170
+ ...meta.used,
171
+ ].sort();
172
+ const extensions: Json = {
173
+ ...(lights.length ? { [LIGHTS]: { lights } } : {}),
174
+ ...(meta.extensions ?? {}),
175
+ };
176
+ const root = nodes[0];
177
+ if (root) root['rotation'] = Z_UP_TO_Y_UP;
178
+ const gltf: Json = {
179
+ asset: meta.asset,
180
+ extras: {
181
+ kind: asset.kind,
182
+ lod: asset.lod,
183
+ units: 'metres',
184
+ up: 'Z (rotated to glTF Y-up at the root)',
185
+ },
186
+ ...(used.length ? { extensionsUsed: used } : {}),
187
+ ...(Object.keys(extensions).length ? { extensions } : {}),
188
+ scene: 0,
189
+ scenes: [{ name: asset.name, nodes: [0] }],
190
+ nodes,
191
+ meshes,
192
+ materials,
193
+ ...(tex ? tex.json : {}),
194
+ buffers: [{ byteLength: body.byteLength }],
195
+ bufferViews: bin.views,
196
+ accessors: bin.accessors,
197
+ ...(animations.length ? { animations } : {}),
198
+ };
199
+ return { glb: packGlb(gltf, body), report };
200
+ }
@@ -0,0 +1,18 @@
1
+ // Public API of /export: back ends that turn a CompiledAsset into a file.
2
+
3
+ export { exportGlb, type GlbOptions, type GlbResult, toGlb } from './glb.ts';
4
+ export { mergeToPivots } from './merge-pivots.ts';
5
+ export {
6
+ type ExportReport,
7
+ optimizeParts,
8
+ type PartOptimizeReport,
9
+ summarize,
10
+ } from './optimize-parts.ts';
11
+ export {
12
+ type ExternalSheet,
13
+ SHEET_MAPS,
14
+ type SheetBinding,
15
+ type SheetMap,
16
+ sheetFileName,
17
+ sheetPngs,
18
+ } from './textures.ts';
@@ -0,0 +1,49 @@
1
+ // KHR_lights_punctual from light sockets: `ctx.socket('headlamp_l', { kind: 'light', dir,
2
+ // data: { light: 'spot', color: '#ffe8c0', intensity: 200, range: 30, cone: 25 } })` becomes a
3
+ // real light every engine and viewer can switch on. The socket node stays as authored; the light
4
+ // rides a child node `<socket>_light` rotated so its −Z (the glTF light direction) is `dir`.
5
+
6
+ import type { Part, Quat, Vec3 } from '../core/index.ts';
7
+ import { hex } from '../material/index.ts';
8
+ import type { Json } from './bin-writer.ts';
9
+
10
+ export const LIGHTS = 'KHR_lights_punctual';
11
+
12
+ /** Shortest rotation taking unit `a` onto unit `b`. */
13
+ function between(a: Vec3, b: Vec3): Quat {
14
+ const d = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
15
+ if (d < -0.999999) return [1, 0, 0, 0]; // opposite: half turn about X (⊥ to −Z)
16
+ const c: Vec3 = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
17
+ const w = 1 + d;
18
+ const n = Math.hypot(c[0], c[1], c[2], w);
19
+ return [c[0] / n, c[1] / n, c[2] / n, w / n];
20
+ }
21
+
22
+ const num = (v: unknown, fallback: number): number =>
23
+ typeof v === 'number' && Number.isFinite(v) ? v : fallback;
24
+
25
+ /** The glTF light for a light socket, or null for any other part. */
26
+ export function lightOf(part: Part): { light: Json; rotation: Quat } | null {
27
+ const s = part.socket;
28
+ if (s?.kind !== 'light') return null;
29
+ const d = s.data ?? {};
30
+ const type = d['light'] === 'point' || d['light'] === 'directional' ? d['light'] : 'spot';
31
+ const color = typeof d['color'] === 'string' ? hex(d['color']) : [1, 1, 1];
32
+ const dir = s.dir ?? [1, 0, 0];
33
+ const len = Math.hypot(...dir) || 1;
34
+ const outer = (Math.min(89, num(d['cone'], 30)) * Math.PI) / 180;
35
+ const inner = Math.min(outer * 0.999, (num(d['inner'], 0) * Math.PI) / 180);
36
+ const range = num(d['range'], 0);
37
+ return {
38
+ light: {
39
+ name: part.name,
40
+ type,
41
+ color,
42
+ // Spot/point: candela; directional: lux. Defaults suit a vehicle lamp / a sun.
43
+ intensity: num(d['intensity'], type === 'directional' ? 3 : type === 'point' ? 20 : 100),
44
+ ...(range > 0 && type !== 'directional' ? { range } : {}),
45
+ ...(type === 'spot' ? { spot: { innerConeAngle: inner, outerConeAngle: outer } } : {}),
46
+ },
47
+ rotation: between([0, 0, -1], [dir[0] / len, dir[1] / len, dir[2] / len]),
48
+ };
49
+ }
@@ -0,0 +1,126 @@
1
+ // The (at most) four shared materials, one per Surface in use. All sample the same trim sheet and
2
+ // take tint from COLOR_0, so batching holds; only the shading model differs. Glass/translucent use
3
+ // KHR_materials_transmission + _volume + _ior (three, Babylon, glTFast); engines without them fall
4
+ // back to alpha BLEND via baseColorFactor.a. With a painted sheet bound (textures.ts), every
5
+ // material samples it: baseColor (sRGB, alpha = cutout mask), normal, and ORM in both the
6
+ // occlusion and metallicRoughness slots. Opaque/cutout factors go to 1 so the ORM texture rules;
7
+ // glass/translucent keep theirs as multipliers (clear stays clear whatever strip it maps).
8
+
9
+ import type { Surface } from '../core/index.ts';
10
+ import type { Json } from './bin-writer.ts';
11
+
12
+ const MATERIALS: Readonly<Record<Surface, Json>> = {
13
+ opaque: {
14
+ name: 'ts23d_trim',
15
+ pbrMetallicRoughness: {
16
+ baseColorFactor: [1, 1, 1, 1],
17
+ metallicFactor: 0,
18
+ roughnessFactor: 0.8,
19
+ },
20
+ },
21
+ cutout: {
22
+ name: 'ts23d_trim_cutout',
23
+ alphaMode: 'MASK',
24
+ alphaCutoff: 0.5,
25
+ doubleSided: true,
26
+ pbrMetallicRoughness: {
27
+ baseColorFactor: [1, 1, 1, 1],
28
+ metallicFactor: 0,
29
+ roughnessFactor: 0.9,
30
+ },
31
+ },
32
+ glass: {
33
+ name: 'ts23d_glass',
34
+ alphaMode: 'BLEND',
35
+ pbrMetallicRoughness: {
36
+ baseColorFactor: [1, 1, 1, 0.25],
37
+ metallicFactor: 0,
38
+ roughnessFactor: 0.05,
39
+ },
40
+ extensions: {
41
+ KHR_materials_transmission: { transmissionFactor: 1 },
42
+ KHR_materials_volume: { thicknessFactor: 0.01 },
43
+ KHR_materials_ior: { ior: 1.5 },
44
+ },
45
+ },
46
+ translucent: {
47
+ name: 'ts23d_translucent',
48
+ alphaMode: 'BLEND',
49
+ pbrMetallicRoughness: {
50
+ baseColorFactor: [1, 1, 1, 0.6],
51
+ metallicFactor: 0,
52
+ roughnessFactor: 0.35,
53
+ },
54
+ extensions: {
55
+ KHR_materials_transmission: { transmissionFactor: 0.6 },
56
+ KHR_materials_volume: { thicknessFactor: 0.02 },
57
+ KHR_materials_ior: { ior: 1.45 },
58
+ },
59
+ },
60
+ };
61
+
62
+ const ORDER: readonly Surface[] = ['opaque', 'cutout', 'glass', 'translucent'];
63
+
64
+ /** The same material sampling the sheet (texture indices fixed by textures.ts). */
65
+ function textured(m: Json, surface: Surface): Json {
66
+ const pbr = m['pbrMetallicRoughness'] as Json;
67
+ const own = surface === 'opaque' || surface === 'cutout';
68
+ return {
69
+ ...m,
70
+ pbrMetallicRoughness: {
71
+ ...pbr,
72
+ baseColorTexture: { index: 0 },
73
+ metallicRoughnessTexture: { index: 2 },
74
+ ...(own ? { metallicFactor: 1, roughnessFactor: 1 } : {}),
75
+ },
76
+ normalTexture: { index: 1 },
77
+ occlusionTexture: { index: 2 },
78
+ };
79
+ }
80
+
81
+ /** Materials for the surfaces present, plus each surface's index and the extensions used. */
82
+ export function materialsFor(
83
+ used: ReadonlySet<Surface>,
84
+ withSheet = false,
85
+ ): {
86
+ materials: Json[];
87
+ index: Map<Surface, number>;
88
+ extensionsUsed: string[];
89
+ } {
90
+ const present = ORDER.filter((s) => s === 'opaque' || used.has(s));
91
+ const index = new Map(present.map((s, i) => [s, i] as const));
92
+ const ext = new Set<string>();
93
+ for (const s of present)
94
+ for (const e of Object.keys((MATERIALS[s]['extensions'] as Json | undefined) ?? {})) ext.add(e);
95
+ const materials = present.map((s) => (withSheet ? textured(MATERIALS[s], s) : MATERIALS[s]));
96
+ return { materials, index, extensionsUsed: [...ext].sort() };
97
+ }
98
+
99
+ export const EMISSIVE_STRENGTH = 'KHR_materials_emissive_strength';
100
+
101
+ /** A lit surface: lamp lenses, windows at night, panel glow. */
102
+ export interface GlowRule {
103
+ /** Part names or globs: 'headlamp_*', 'window_*'. */
104
+ readonly parts: readonly string[];
105
+ /** Linear RGB 0..1 (use `hex('#ffd9a0')` from /material). Default warm white. */
106
+ readonly color?: readonly [number, number, number];
107
+ /** Brightness multiplier (KHR_materials_emissive_strength, Ratified); 1 = plain glTF. */
108
+ readonly strength?: number;
109
+ }
110
+
111
+ /**
112
+ * The opaque trim material plus emission. emissiveFactor caps at 1 in core glTF; anything
113
+ * brighter rides KHR_materials_emissive_strength (engines without it see the clamped glow).
114
+ */
115
+ export function glowMaterial(rule: GlowRule, i: number, withSheet: boolean): Json {
116
+ const base = withSheet ? textured(MATERIALS.opaque, 'opaque') : MATERIALS.opaque;
117
+ const strength = rule.strength ?? 4;
118
+ return {
119
+ ...base,
120
+ name: `ts23d_glow_${i}`,
121
+ emissiveFactor: [...(rule.color ?? [1, 0.85, 0.6])],
122
+ ...(strength !== 1
123
+ ? { extensions: { [EMISSIVE_STRENGTH]: { emissiveStrength: strength } } }
124
+ : {}),
125
+ };
126
+ }
@@ -0,0 +1,146 @@
1
+ // Batching for engines: every part that is NOT a pivot is merged into its nearest pivot ancestor,
2
+ // so draw calls = pivots with geometry, not parts. A pivot is anything the engine may move: a
3
+ // group (`ctx.group`, a named handle like `turret`, `gun`, `door_l`) or a part with `motion`.
4
+ // Static top-level geometry merges into one `body` part. Names the engine binds to survive.
5
+ // Non-opaque surfaces split into `<pivot>_<surface>` children (one draw call per surface), and
6
+ // glowing parts (GlbOptions.glow) into `<pivot>_glow_<i>` children with their emissive material.
7
+
8
+ import {
9
+ type CompiledAsset,
10
+ type Mesh,
11
+ merge,
12
+ type Part,
13
+ rotateZ,
14
+ type Surface,
15
+ toWorld,
16
+ type WorldTransform,
17
+ worldTransforms,
18
+ } from '../core/index.ts';
19
+
20
+ const isPivot = (p: Part): boolean => p.mesh === null || p.motion !== null;
21
+
22
+ /** Merged layer parts that glow → index of the glow rule (read by the GLB writer). */
23
+ export const glowLayer = new WeakMap<Part, number>();
24
+
25
+ /** `*`/`?` glob → anchored RegExp (export cannot import /qa: same tier). */
26
+ export const globRe = (pattern: string): RegExp =>
27
+ new RegExp(
28
+ `^${pattern
29
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
30
+ .replaceAll('*', '.*')
31
+ .replaceAll('?', '.')}$`,
32
+ );
33
+ const SURFACES: readonly Surface[] = ['cutout', 'glass', 'translucent'];
34
+
35
+ /** Re-express a part's mesh in its pivot's local space. */
36
+ function intoPivot(mesh: Mesh, part: WorldTransform, pivot: WorldTransform): Mesh {
37
+ const positions = new Float32Array(mesh.positions.length);
38
+ const normals = new Float32Array(mesh.normals.length);
39
+ for (let i = 0; i < positions.length; i += 3) {
40
+ const w = toWorld(part, [
41
+ mesh.positions[i] ?? 0,
42
+ mesh.positions[i + 1] ?? 0,
43
+ mesh.positions[i + 2] ?? 0,
44
+ ]);
45
+ positions.set(
46
+ rotateZ([w[0] - pivot.origin[0], w[1] - pivot.origin[1], w[2] - pivot.origin[2]], -pivot.yaw),
47
+ i,
48
+ );
49
+ normals.set(
50
+ rotateZ(
51
+ rotateZ(
52
+ [mesh.normals[i] ?? 0, mesh.normals[i + 1] ?? 0, mesh.normals[i + 2] ?? 0],
53
+ part.yaw,
54
+ ),
55
+ -pivot.yaw,
56
+ ),
57
+ i,
58
+ );
59
+ }
60
+ return { ...mesh, positions, normals };
61
+ }
62
+
63
+ export function mergeToPivots(
64
+ asset: CompiledAsset,
65
+ glow: readonly (readonly string[])[] = [],
66
+ ): CompiledAsset {
67
+ const glowRes = glow.map((globs) => globs.map(globRe));
68
+ const glowOf = (name: string): number => glowRes.findIndex((rs) => rs.some((r) => r.test(name)));
69
+ const byName = new Map(asset.parts.map((p) => [p.name, p]));
70
+ const xf = worldTransforms(asset.parts);
71
+ const ROOT = 'body';
72
+ const identity: WorldTransform = { origin: [0, 0, 0], yaw: 0 };
73
+ const pivotOf = (p: Part): string => {
74
+ let cur = p.parent ? byName.get(p.parent) : undefined;
75
+ while (cur && !isPivot(cur)) cur = cur.parent ? byName.get(cur.parent) : undefined;
76
+ return cur?.name ?? ROOT;
77
+ };
78
+ // Buckets keyed by pivot + surface: opaque merges into the pivot's own mesh; each other
79
+ // surface in use becomes one child `<pivot>_<surface>` so materials never mix in a primitive.
80
+ const extra = new Map<string, Mesh[]>();
81
+ const bucket = (pivot: string, surface: Surface | undefined, mesh: Mesh, g = -1): void => {
82
+ const k = `${pivot}|${g >= 0 ? `glow_${g}` : (surface ?? 'opaque')}`;
83
+ extra.set(k, [...(extra.get(k) ?? []), mesh]);
84
+ };
85
+ for (const p of asset.parts) {
86
+ if (!p.mesh) continue;
87
+ const g = glowOf(p.name);
88
+ if (isPivot(p)) {
89
+ bucket(p.name, p.surface, p.mesh, g);
90
+ continue;
91
+ }
92
+ const pivot = pivotOf(p);
93
+ const t = xf.get(p.name) ?? identity;
94
+ const pt = pivot === ROOT ? identity : (xf.get(pivot) ?? identity);
95
+ bucket(pivot, p.surface, intoPivot(p.mesh, t, pt), g);
96
+ }
97
+ const parts: Part[] = [];
98
+ const layers = (pivot: string, parent: string | null): void => {
99
+ for (const surface of SURFACES) {
100
+ const meshes = extra.get(`${pivot}|${surface}`);
101
+ if (!meshes) continue;
102
+ parts.push({
103
+ name: `${pivot}_${surface}`,
104
+ mesh: merge(meshes),
105
+ at: [0, 0, 0],
106
+ parent,
107
+ yaw: 0,
108
+ motion: null,
109
+ surface,
110
+ });
111
+ }
112
+ glow.forEach((_, g) => {
113
+ const meshes = extra.get(`${pivot}|glow_${g}`);
114
+ if (!meshes) return;
115
+ const part: Part = {
116
+ name: `${pivot}_glow_${g}`,
117
+ mesh: merge(meshes),
118
+ at: [0, 0, 0],
119
+ parent,
120
+ yaw: 0,
121
+ motion: null,
122
+ };
123
+ glowLayer.set(part, g);
124
+ parts.push(part);
125
+ });
126
+ };
127
+ const body = extra.get(`${ROOT}|opaque`);
128
+ if (body)
129
+ parts.push({
130
+ name: ROOT,
131
+ mesh: merge(body),
132
+ at: [0, 0, 0],
133
+ parent: null,
134
+ yaw: 0,
135
+ motion: null,
136
+ });
137
+ layers(ROOT, null);
138
+ for (const p of asset.parts) {
139
+ if (!isPivot(p)) continue;
140
+ const own = extra.get(`${p.name}|opaque`);
141
+ const { surface: _drop, ...rest } = p;
142
+ parts.push({ ...rest, mesh: own ? merge(own) : null });
143
+ layers(p.name, p.name);
144
+ }
145
+ return { ...asset, parts };
146
+ }