@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,3571 @@
1
+ import {
2
+ Ts23dError2,
3
+ notImplemented2,
4
+ BUDGETS2,
5
+ defineFamily2,
6
+ MeshBuilder2,
7
+ triangleCount2,
8
+ boundsOf2,
9
+ translate2,
10
+ merge2,
11
+ place2,
12
+ sub2,
13
+ dot2,
14
+ cross2,
15
+ normalize2
16
+ } from "./chunk-4ddzsqtc.js";
17
+ import {
18
+ split2,
19
+ parseWhyte2
20
+ } from "./chunk-ravbb06q.js";
21
+ import {
22
+ stripRect2,
23
+ DEFAULT_SHEET2,
24
+ hex2
25
+ } from "./chunk-kvcep7ft.js";
26
+ import {
27
+ box2,
28
+ lathe2,
29
+ cylinder2,
30
+ beam2,
31
+ triangulate2,
32
+ ringXY2,
33
+ loft2,
34
+ planar2,
35
+ sd2,
36
+ sdf2,
37
+ sculptField2,
38
+ arcLengths2,
39
+ transportFrames2,
40
+ sweep2
41
+ } from "./chunk-wtqhq529.js";
42
+ import {
43
+ bakeAo2
44
+ } from "./chunk-xnt2sdhf.js";
45
+
46
+ // src/kinds/faces.ts
47
+ var EPS = 0.000000001;
48
+ function clipPoly(poly, f) {
49
+ const out = [];
50
+ const n = poly.length;
51
+ for (let i = 0;i < n; i++) {
52
+ const a = poly[i];
53
+ const b = poly[(i + 1) % n];
54
+ if (a === undefined || b === undefined)
55
+ continue;
56
+ const fa = f(a);
57
+ const fb = f(b);
58
+ if (fa <= EPS)
59
+ out.push(a);
60
+ if (fa <= EPS !== fb <= EPS) {
61
+ const t = fa / (fa - fb);
62
+ out.push(a.map((v, k) => v + ((b[k] ?? v) - v) * t));
63
+ }
64
+ }
65
+ return dedupe(out);
66
+ }
67
+ function dedupe(poly) {
68
+ const out = [];
69
+ for (const p of poly) {
70
+ const q = out.at(-1);
71
+ if (!q || p.some((v, k) => Math.abs(v - (q[k] ?? 0)) > 0.0000001))
72
+ out.push(p);
73
+ }
74
+ const first = out[0];
75
+ const last = out.at(-1);
76
+ if (out.length > 1 && first && last && first.every((v, k) => Math.abs(v - (last[k] ?? 0)) < 0.0000001))
77
+ out.pop();
78
+ return out;
79
+ }
80
+ var area2 = (poly) => poly.reduce((s, a, i) => {
81
+ const b = poly[(i + 1) % poly.length] ?? a;
82
+ return s + a[0] * b[1] - b[0] * a[1];
83
+ }, 0) / 2;
84
+ var stripScale = (strip) => DEFAULT_SHEET2.strips[strip]?.scaleMetres ?? 1;
85
+ function emitFace(b, o) {
86
+ if (o.points.length < 3)
87
+ return 0;
88
+ const n = normalize2(o.normal);
89
+ const points = dot2(newell(o.points), n) < 0 ? [...o.points].reverse() : o.points;
90
+ const vDir = normalize2(sub2(o.up, scaleV(n, dot2(o.up, n))));
91
+ const uDir = cross2(vDir, n);
92
+ const rect = stripRect2(o.strip);
93
+ const scale = stripScale(o.strip);
94
+ const vOf = (p) => dot2(sub2(p, o.vOrigin), vDir);
95
+ const vs = points.map(vOf);
96
+ const lo = Math.min(...vs);
97
+ const hi = Math.max(...vs);
98
+ if (hi - lo < 0.000001)
99
+ return 0;
100
+ const colour = (p) => typeof o.color === "function" ? o.color(p) : o.color;
101
+ const bands = [];
102
+ if (o.banded === false)
103
+ bands.push([lo, hi]);
104
+ else {
105
+ for (let k = Math.floor(lo / scale + 0.000000001);k * scale < hi - 0.000000001; k++) {
106
+ bands.push([k * scale, (k + 1) * scale]);
107
+ }
108
+ }
109
+ let tris = 0;
110
+ for (const [v0, v1] of bands) {
111
+ const span = v1 - v0;
112
+ const poly = clipPoly(clipPoly(points, (p) => v0 - vOf(p)), (p) => vOf(p) - v1);
113
+ if (poly.length < 3)
114
+ continue;
115
+ const ids = poly.map((p) => {
116
+ const f = Math.min(1, Math.max(0, (vOf(p) - v0) / span));
117
+ return b.vertex({
118
+ p,
119
+ n,
120
+ uv: [
121
+ rect.u0 + dot2(p, uDir) / rect.metres * (rect.u1 - rect.u0),
122
+ rect.v0 + f * (rect.v1 - rect.v0)
123
+ ],
124
+ color: colour(p)
125
+ });
126
+ });
127
+ const first = ids[0] ?? 0;
128
+ for (let i = 1;i + 1 < ids.length; i++)
129
+ b.tri(first, ids[i] ?? 0, ids[i + 1] ?? 0);
130
+ tris += ids.length - 2;
131
+ }
132
+ return tris;
133
+ }
134
+ function newell(poly) {
135
+ const n = [0, 0, 0];
136
+ for (let i = 0;i < poly.length; i++) {
137
+ const a = poly[i] ?? [0, 0, 0];
138
+ const b = poly[(i + 1) % poly.length] ?? a;
139
+ n[0] += (a[1] - b[1]) * (a[2] + b[2]);
140
+ n[1] += (a[2] - b[2]) * (a[0] + b[0]);
141
+ n[2] += (a[0] - b[0]) * (a[1] + b[1]);
142
+ }
143
+ return n;
144
+ }
145
+ var scaleV = (v, s) => [v[0] * s, v[1] * s, v[2] * s];
146
+
147
+ // src/kinds/mass.ts
148
+ function mass2(o, lod = 0) {
149
+ const [w, d, h] = o.size;
150
+ if (!(w > 0 && d > 0 && h > 0))
151
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
152
+ cause: `mass size is [${o.size.join(", ")}]`,
153
+ fix: "give width (X), depth (Y) and height (Z) in metres, all > 0"
154
+ });
155
+ const [cx, cy, z0] = o.at ?? [0, 0, 0];
156
+ const [x0, x1, y0, y1, z1] = [cx - w / 2, cx + w / 2, cy - d / 2, cy + d / 2, z0 + h];
157
+ const b = new MeshBuilder2;
158
+ const strip = o.strip ?? "brick";
159
+ const origin = [0, 0, o.courseOrigin ?? 0];
160
+ const sides = [
161
+ [
162
+ "front",
163
+ [1, 0, 0],
164
+ [
165
+ [x1, y0, z0],
166
+ [x1, y1, z0],
167
+ [x1, y1, z1],
168
+ [x1, y0, z1]
169
+ ]
170
+ ],
171
+ [
172
+ "back",
173
+ [-1, 0, 0],
174
+ [
175
+ [x0, y1, z0],
176
+ [x0, y0, z0],
177
+ [x0, y0, z1],
178
+ [x0, y1, z1]
179
+ ]
180
+ ],
181
+ [
182
+ "left",
183
+ [0, 1, 0],
184
+ [
185
+ [x1, y1, z0],
186
+ [x0, y1, z0],
187
+ [x0, y1, z1],
188
+ [x1, y1, z1]
189
+ ]
190
+ ],
191
+ [
192
+ "right",
193
+ [0, -1, 0],
194
+ [
195
+ [x0, y0, z0],
196
+ [x1, y0, z0],
197
+ [x1, y0, z1],
198
+ [x0, y0, z1]
199
+ ]
200
+ ]
201
+ ];
202
+ for (const [name, normal, points] of sides) {
203
+ if (o.omit?.includes(name))
204
+ continue;
205
+ emitFace(b, {
206
+ points,
207
+ normal,
208
+ up: [0, 0, 1],
209
+ vOrigin: origin,
210
+ color: o.color,
211
+ strip,
212
+ banded: lod < 2
213
+ });
214
+ }
215
+ for (const [name, z, s] of [
216
+ ["top", z1, 1],
217
+ ["bottom", z0, -1]
218
+ ]) {
219
+ if (o.omit?.includes(name))
220
+ continue;
221
+ emitFace(b, {
222
+ points: [
223
+ [x0, y0, z],
224
+ [x1, y0, z],
225
+ [x1, y1, z],
226
+ [x0, y1, z]
227
+ ],
228
+ normal: [0, 0, s],
229
+ up: [0, 1, 0],
230
+ vOrigin: [cx, y0, z],
231
+ color: o.color,
232
+ strip: "plain",
233
+ banded: false
234
+ });
235
+ }
236
+ return b.build();
237
+ }
238
+
239
+ // src/kinds/openings.ts
240
+ var YAW = {
241
+ front: 0,
242
+ left: Math.PI / 2,
243
+ back: Math.PI,
244
+ right: -Math.PI / 2
245
+ };
246
+ var onWall = (size, y, z, color, strip) => box2({ size, at: [size[0] / 2, y, z], color, omit: ["back"], ...strip ? { strip } : {} });
247
+ function opening2(o, lod = 0) {
248
+ const { width: w, height: h } = o;
249
+ if (!(w > 0.2 && h > 0.2 && Number.isFinite(w) && Number.isFinite(h)))
250
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
251
+ cause: `${o.kind} opening is ${w} × ${h} m`,
252
+ fix: "give a clear width and height > 0.2 m (sash ≈ 0.9 × 1.6, door ≈ 0.9 × 2.1)"
253
+ });
254
+ const r = o.depth ?? 0.12;
255
+ const s = o.surround ?? 0.1;
256
+ const trim = o.trim ?? [0.78, 0.75, 0.68];
257
+ const door = o.kind === "door";
258
+ const parts = [];
259
+ parts.push(door ? onWall([0.05, w, h], 0, 0, o.frame, "wood") : onWall([0.02, w, h], 0, 0, o.glass, "glass"));
260
+ if (lod < 2) {
261
+ const lintel = o.lintel ?? true;
262
+ const reveal = (size, y, z) => mass2({
263
+ size,
264
+ at: [size[0] / 2, y, z],
265
+ color: o.wall,
266
+ strip: o.wallStrip ?? "plain",
267
+ courseOrigin: -o.at[2],
268
+ omit: ["back"]
269
+ }, lod);
270
+ for (const side of [-1, 1])
271
+ parts.push(reveal([r, s, h], side * (w / 2 + s / 2), 0));
272
+ if (lintel)
273
+ parts.push(onWall([r + 0.03, w + 2 * s + 0.2, 0.22], 0, h, trim, "stone"));
274
+ else
275
+ parts.push(reveal([r, w + 2 * s, s], 0, h));
276
+ if (!door && (o.sill ?? true))
277
+ parts.push(onWall([r + 0.07, w + 2 * s + 0.12, 0.07], 0, -0.07, trim, "stone"));
278
+ if (!door)
279
+ parts.push(...sash(o, w, h, lod));
280
+ else if (lod === 0)
281
+ parts.push(...panels(o, w, h));
282
+ }
283
+ return place2(merge2(parts), { yaw: YAW[o.facing], at: o.at });
284
+ }
285
+ function sash(o, w, h, lod) {
286
+ const f = 0.06;
287
+ const d = 0.05;
288
+ const out = [
289
+ onWall([d, f, h], -(w / 2 - f / 2), 0, o.frame),
290
+ onWall([d, f, h], w / 2 - f / 2, 0, o.frame),
291
+ onWall([d, w - 2 * f, f], 0, 0, o.frame),
292
+ onWall([d, w - 2 * f, f], 0, h - f, o.frame)
293
+ ];
294
+ if (lod > 0)
295
+ return out;
296
+ const [across, up] = o.panes ?? [2, 2];
297
+ const bar = 0.03;
298
+ for (let i = 1;i < across; i++)
299
+ out.push(onWall([d * 0.8, bar, h - 2 * f], -w / 2 + f + (w - 2 * f) * i / across, f, o.frame));
300
+ for (let j = 1;j < up; j++)
301
+ out.push(onWall([d * 0.8, w - 2 * f, bar], 0, f + (h - 2 * f) * j / up - bar / 2, o.frame));
302
+ return out;
303
+ }
304
+ function panels(o, w, h) {
305
+ const out = [];
306
+ const pw = (w - 0.36) / 2;
307
+ const tone = [o.frame[0] * 0.85, o.frame[1] * 0.85, o.frame[2] * 0.85];
308
+ for (const y of [-1, 1])
309
+ for (const [z0, ph] of [
310
+ [0.15, h * 0.38],
311
+ [0.15 + h * 0.38 + 0.15, h - 0.45 - h * 0.38]
312
+ ])
313
+ out.push(box2({
314
+ size: [0.02, pw, ph],
315
+ at: [0.06, y * (pw / 2 + 0.06), z0],
316
+ color: tone,
317
+ omit: ["back"]
318
+ }));
319
+ out.push(box2({ size: [0.06, 0.06, 0.06], at: [0.08, w / 2 - 0.12, h * 0.48], color: [0.7, 0.6, 0.3] }));
320
+ return out;
321
+ }
322
+
323
+ // src/kinds/building.ts
324
+ var OPENINGS = new Set(["win", "door", "arch", "shop"]);
325
+ function facade2(ctx, o) {
326
+ const [w, d] = o.footprint;
327
+ const ratio = o.windowRatio ?? 0.6;
328
+ const depth = 0.08;
329
+ for (const side of o.sides) {
330
+ const along = side === "front" || side === "back" ? d : w;
331
+ for (let f = 0;f < o.floors; f++) {
332
+ const line = f === 0 && o.ground ? o.ground : o.grammar;
333
+ const z = o.at[2] + f * o.floorHeight;
334
+ split2(line, along).forEach((slot, i) => {
335
+ if (!OPENINGS.has(slot.symbol) || slot.size < 0.2)
336
+ return;
337
+ const isDoor = slot.symbol === "door" || slot.symbol === "shop";
338
+ const h = isDoor ? o.floorHeight * 0.85 : o.floorHeight * ratio;
339
+ const sill = isDoor ? 0 : (o.floorHeight - h) / 2;
340
+ const c = slot.start + slot.size / 2 - along / 2;
341
+ const inset = slot.size * 0.85;
342
+ const pos = side === "front" ? [o.at[0] + w / 2, o.at[1] + c, z + sill] : side === "back" ? [o.at[0] - w / 2, o.at[1] - c, z + sill] : side === "left" ? [o.at[0] - c, o.at[1] + d / 2, z + sill] : [o.at[0] + c, o.at[1] - d / 2, z + sill];
343
+ const name = `${o.prefix}_${side}_${f}_${i}`;
344
+ if (o.kit) {
345
+ const k = o.kit;
346
+ ctx.part(name, opening2({
347
+ kind: isDoor ? "door" : "window",
348
+ width: Math.max(0.3, inset - 0.2),
349
+ height: h - (isDoor ? 0.25 : 0.2),
350
+ at: pos,
351
+ facing: side,
352
+ glass: o.glass,
353
+ frame: o.frame ?? [0.92, 0.9, 0.85],
354
+ wall: k.wall,
355
+ ...k.trim ? { trim: k.trim } : {},
356
+ ...k.depth !== undefined ? { depth: k.depth } : {},
357
+ ...k.panes ? { panes: k.panes } : {},
358
+ ...k.strip ? { wallStrip: k.strip } : {}
359
+ }, ctx.lod));
360
+ return;
361
+ }
362
+ const size = side === "front" || side === "back" ? [depth, inset, h] : [inset, depth, h];
363
+ ctx.part(name, box2({
364
+ size,
365
+ at: pos,
366
+ color: isDoor ? o.frame ?? o.glass : o.glass,
367
+ strip: "glass",
368
+ omit: ["bottom"]
369
+ }));
370
+ });
371
+ }
372
+ }
373
+ }
374
+ function setbacks2(ctx, o) {
375
+ let z = 0;
376
+ o.tiers.forEach((t, i) => {
377
+ const h = t.floors * o.floorHeight;
378
+ ctx.part(`${o.prefix}_${i}`, box2({
379
+ size: [t.footprint[0], t.footprint[1], h],
380
+ at: [0, 0, z],
381
+ color: o.color,
382
+ strip: "stone",
383
+ omit: i === 0 ? ["bottom"] : []
384
+ }));
385
+ if (o.detail)
386
+ facade2(ctx, {
387
+ prefix: `${o.prefix}_${i}_win`,
388
+ at: [0, 0, z],
389
+ footprint: t.footprint,
390
+ floors: t.floors,
391
+ floorHeight: o.floorHeight,
392
+ grammar: o.grammar,
393
+ sides: ["front", "back", "left", "right"],
394
+ glass: o.glass
395
+ });
396
+ z += h;
397
+ });
398
+ return z;
399
+ }
400
+ // src/kinds/head-geom.ts
401
+ var DEG = 180 / Math.PI;
402
+ function alongX(d, roll = 0) {
403
+ const yaw = Math.atan2(d[1], d[0]);
404
+ const pitch = -Math.atan2(d[2], Math.hypot(d[0], d[1]));
405
+ return [roll, pitch * DEG, yaw * DEG];
406
+ }
407
+ function alongZ(d) {
408
+ const len = Math.hypot(d[0], d[1], d[2]) || 1;
409
+ const tilt = Math.acos(Math.max(-1, Math.min(1, d[2] / len)));
410
+ return [0, tilt * DEG, Math.atan2(d[1], d[0]) * DEG];
411
+ }
412
+ var unit = (d) => {
413
+ const l = Math.hypot(d[0], d[1], d[2]) || 1;
414
+ return [d[0] / l, d[1] / l, d[2] / l];
415
+ };
416
+ var along = (a, d, t) => [
417
+ a[0] + d[0] * t,
418
+ a[1] + d[1] * t,
419
+ a[2] + d[2] * t
420
+ ];
421
+
422
+ // src/kinds/head-beard.ts
423
+ var RAD = Math.PI / 180;
424
+ function browEdits(p, l) {
425
+ const h = p.brow.hair ?? 0;
426
+ if (h <= 0)
427
+ return [];
428
+ const [ex, ey] = l.eye;
429
+ const r = l.eyeR;
430
+ const lift = [p.brow.ridge * 0.55 + 0.012, 0, p.brow.ridge * 0.8 + 0.018];
431
+ const inner = [ex + r * 0.75 + lift[0], 0.035, l.browZ - p.brow.scowl + lift[2]];
432
+ const outer = [
433
+ ex + r * 0.1 + lift[0] * 0.6,
434
+ ey + r * 1.25,
435
+ l.browZ + p.brow.scowl * 0.5 + lift[2] * 0.7
436
+ ];
437
+ const mid = [
438
+ (inner[0] + outer[0]) / 2,
439
+ inner[1] + (outer[1] - inner[1]) * 0.55,
440
+ (inner[2] + outer[2]) / 2 + 0.014
441
+ ];
442
+ return [
443
+ {
444
+ group: [
445
+ { shape: "capsule", at: inner, to: mid, size: h },
446
+ { shape: "capsule", at: mid, to: outer, size: h * 0.65, blend: 0.006 }
447
+ ],
448
+ color: p.colors.hair,
449
+ blend: 0.006
450
+ }
451
+ ];
452
+ }
453
+ var below = (p, deg) => {
454
+ const n = [-Math.sin(deg * RAD), 0, Math.cos(deg * RAD)];
455
+ return {
456
+ shape: "roundBox",
457
+ at: [p[0] - n[0], p[1], p[2] - n[2]],
458
+ size: [4, 4, 2],
459
+ rotate: [0, -deg, 0],
460
+ op: "intersect",
461
+ blend: 0.02
462
+ };
463
+ };
464
+ function beardEdits(p, l) {
465
+ if (p.beard === "none")
466
+ return [];
467
+ const h = p.colors.hair;
468
+ const light = [h[0] * 1.35, h[1] * 1.35, h[2] * 1.35];
469
+ const [ax, ay, az] = l.angle;
470
+ const ch = l.chin;
471
+ const mw = p.mouth.width;
472
+ const lipT = p.mouth.lips;
473
+ const full = p.beard !== "goatee";
474
+ const mass = full ? {
475
+ shape: "ellipsoid",
476
+ at: [(ax + ch[0]) / 2 + 0.03, 0, (az + ch[2]) / 2 + 0.01],
477
+ size: [(ch[0] - ax) / 2 + 0.08, ay + 0.065, 0.17]
478
+ } : { shape: "ellipsoid", at: [ch[0] + 0.01, 0, ch[2] + 0.02], size: [0.09, mw * 0.9, 0.11] };
479
+ const out = [
480
+ {
481
+ group: [
482
+ mass,
483
+ {
484
+ shape: "ellipsoid",
485
+ at: [ch[0] + 0.025, 0, ch[2] - 0.03],
486
+ size: [0.08, full ? 0.13 : 0.08, 0.1],
487
+ blend: 0.04
488
+ },
489
+ below([l.mouthX, 0, l.mouthZ + 0.03], full ? 28 : 0),
490
+ {
491
+ shape: "ellipsoid",
492
+ at: [l.mouthX + 0.02, 0, l.mouthZ - lipT * 0.3],
493
+ size: [0.07, mw * 1.05, lipT * 2.3],
494
+ op: "subtract",
495
+ blend: 0.012
496
+ },
497
+ ...[-0.05, 0.05].map((y) => ({
498
+ shape: "capsule",
499
+ at: [ch[0] + 0.12, y, l.mouthZ - 0.06],
500
+ to: [ch[0] + 0.06, y * 1.3, ch[2] - 0.14],
501
+ size: 0.008,
502
+ op: "subtract",
503
+ blend: 0.012,
504
+ mirror: false
505
+ }))
506
+ ],
507
+ color: h,
508
+ blend: 0.012
509
+ }
510
+ ];
511
+ if (p.beard === "braided")
512
+ out.push({
513
+ shape: "capsule",
514
+ at: [ch[0] + 0.03, 0.035, ch[2] - 0.08],
515
+ to: [ch[0] + 0.01, 0.045, ch[2] - 0.34],
516
+ size: 0.032,
517
+ color: light,
518
+ blend: 0.02
519
+ });
520
+ const root = [l.mouthX + 0.02, 0.012, l.mouthZ + lipT * 1.9];
521
+ const bend = [l.mouthX - 0.005, mw * 0.85, l.mouthZ + lipT * 0.6];
522
+ const tip = along(bend, unit([-0.4, 0.5, -1]), 0.05);
523
+ out.push({
524
+ group: [
525
+ { shape: "capsule", at: root, to: bend, size: 0.024 },
526
+ { shape: "capsule", at: bend, to: tip, size: 0.016, blend: 0.01 }
527
+ ],
528
+ color: light,
529
+ blend: 0.008
530
+ });
531
+ return out;
532
+ }
533
+
534
+ // src/kinds/head-armour.ts
535
+ var RAD2 = Math.PI / 180;
536
+ var shade = (c, k) => [c[0] * k, c[1] * k, c[2] * k];
537
+ var above = (p, deg, op = "intersect") => {
538
+ const n = [0, -Math.sin(deg * RAD2), Math.cos(deg * RAD2)];
539
+ return {
540
+ shape: "roundBox",
541
+ at: [p[0] + n[0], p[1] + n[1], p[2] + n[2]],
542
+ size: [4, 4, 2],
543
+ rotate: [deg, 0, 0],
544
+ op
545
+ };
546
+ };
547
+ var slab = (p, deg, t) => ({
548
+ shape: "roundBox",
549
+ at: p,
550
+ size: [4, 4, t],
551
+ rotate: [deg, 0, 0],
552
+ op: "intersect"
553
+ });
554
+ var chestOf = (cx, s, b) => ({
555
+ at: [cx - 0.04, 0, b + 0.2],
556
+ r: [0.36, s * 0.75, 0.42]
557
+ });
558
+ function armour(p, cx, s, b, nz) {
559
+ const c = p.colors;
560
+ const metal = c.metal;
561
+ const out = [];
562
+ for (let k = 0;k < 3; k++) {
563
+ const at = [cx - 0.03, s * (0.78 + k * 0.1), b + 0.53 - k * 0.12];
564
+ const r = [0.31 - k * 0.02, 0.27 - k * 0.03, 0.19 - k * 0.02];
565
+ const tilt = -(28 + k * 14);
566
+ const cut = [at[0], at[1], at[2] - r[2] * 0.35];
567
+ out.push({
568
+ group: [{ shape: "ellipsoid", at, size: r }, above(cut, tilt)],
569
+ color: shade(metal, 1 - k * 0.12),
570
+ blend: 0.008
571
+ }, {
572
+ group: [
573
+ { shape: "ellipsoid", at, size: [r[0] + 0.018, r[1] + 0.018, r[2] + 0.018] },
574
+ slab(cut, tilt, 0.035)
575
+ ],
576
+ color: shade(metal, 0.55),
577
+ blend: 0.004
578
+ });
579
+ for (let j = 0;j < 4; j++) {
580
+ const t = (j - 1.5) * 0.11;
581
+ out.push({
582
+ shape: "sphere",
583
+ at: [at[0] + t, at[1] + Math.sin(-tilt * RAD2) * r[1] * 0.55, cut[2] + 0.03],
584
+ size: 0.016,
585
+ color: shade(metal, 1.4),
586
+ blend: 0.004
587
+ });
588
+ }
589
+ }
590
+ for (let k = 0;k < 3; k++)
591
+ out.push({
592
+ shape: "cone",
593
+ at: [cx - 0.17 + k * 0.14, s * 0.84, b + 0.68 - Math.abs(k - 1) * 0.03],
594
+ size: [0.065, 0.006, 0.26 - Math.abs(k - 1) * 0.08],
595
+ rotate: alongZ(unit([(k - 1) * 0.35, 0.45, 1])),
596
+ color: c.teeth,
597
+ blend: 0.01
598
+ });
599
+ const chest = chestOf(cx, s, b);
600
+ const grow = (g) => [chest.r[0] + g, chest.r[1] + g, chest.r[2] + g];
601
+ const band = [cx, 0, b + 0.28];
602
+ out.push({
603
+ group: [{ shape: "ellipsoid", at: chest.at, size: grow(0.03) }, slab(band, 52, 0.17)],
604
+ color: c.leather,
605
+ blend: 0.006,
606
+ mirror: false
607
+ }, {
608
+ group: [
609
+ { shape: "ellipsoid", at: chest.at, size: grow(0.055) },
610
+ {
611
+ shape: "roundBox",
612
+ at: [cx + 0.3, 0.02, b + 0.3],
613
+ size: [0.3, 0.15, 0.15],
614
+ rotate: [52, 0, 0],
615
+ op: "intersect"
616
+ },
617
+ {
618
+ shape: "ellipsoid",
619
+ at: [cx + 0.42, 0.02, b + 0.3],
620
+ size: [0.1, 0.045, 0.045],
621
+ op: "subtract"
622
+ }
623
+ ],
624
+ color: shade(metal, 0.8),
625
+ blend: 0.004,
626
+ mirror: false
627
+ });
628
+ const fur = c.fur ?? c.leather;
629
+ const R = p.neck.radius + 0.1;
630
+ out.push({
631
+ shape: "torus",
632
+ at: [cx - 0.08, 0, nz + 0.02],
633
+ size: [R, 0.07],
634
+ color: shade(fur, 0.75),
635
+ blend: 0.04
636
+ });
637
+ for (let row = 0;row < 2; row++)
638
+ for (let i = 0;i < 10; i++) {
639
+ const a = (-85 + i * 20 + row * 10) * RAD2;
640
+ const rr = R + row * 0.05;
641
+ const root = [
642
+ cx - 0.08 + Math.cos(a) * rr,
643
+ Math.abs(Math.sin(a)) * rr + 0.015,
644
+ nz + 0.07 - row * 0.06
645
+ ];
646
+ const dir = unit([Math.cos(a), Math.abs(Math.sin(a)), -0.5 - (i + row) % 2 * 0.35]);
647
+ out.push({
648
+ shape: "cone",
649
+ at: root,
650
+ size: [0.07 - row * 0.01, 0.012, 0.13 + (i + row) % 3 * 0.03],
651
+ rotate: alongZ(dir),
652
+ color: (i + row) % 2 ? fur : shade(fur, 0.7),
653
+ blend: 0.03
654
+ });
655
+ }
656
+ return out;
657
+ }
658
+
659
+ // src/kinds/head-bust.ts
660
+ function torso(p, cx, b) {
661
+ const c = p.colors;
662
+ return [
663
+ {
664
+ shape: "ellipsoid",
665
+ at: [cx + 0.17, 0.16, b + 0.32],
666
+ size: [0.16, 0.19, 0.12],
667
+ rotate: [0, 0, 12],
668
+ color: c.skin,
669
+ blend: 0.07
670
+ },
671
+ {
672
+ shape: "capsule",
673
+ at: [cx + 0.33, 0, b + 0.42],
674
+ to: [cx + 0.34, 0, b + 0.1],
675
+ size: 0.012,
676
+ op: "subtract",
677
+ blend: 0.04,
678
+ color: c.shade,
679
+ mirror: false
680
+ }
681
+ ];
682
+ }
683
+ function hood(p, cx, nz) {
684
+ const c = p.colors;
685
+ return [
686
+ {
687
+ shape: "torus",
688
+ at: [cx - 0.08, 0, nz + 0.04],
689
+ size: [p.neck.radius + 0.13, 0.11],
690
+ color: c.cloth,
691
+ blend: 0.05
692
+ },
693
+ {
694
+ shape: "ellipsoid",
695
+ at: [cx - 0.36, 0, nz + 0.14],
696
+ size: [0.16, 0.3, 0.2],
697
+ color: c.cloth,
698
+ blend: 0.08
699
+ },
700
+ {
701
+ shape: "cone",
702
+ at: [cx - 0.42, 0, nz + 0.1],
703
+ size: [0.12, 0.02, 0.32],
704
+ rotate: [0, -165, 0],
705
+ color: c.cloth,
706
+ blend: 0.05
707
+ },
708
+ {
709
+ shape: "roundBox",
710
+ at: [cx + 0.1, 0.32, nz - 0.02],
711
+ size: [0.04, 0.1, 0.08],
712
+ round: 0.01,
713
+ rotate: [20, 0, 0],
714
+ color: c.leather,
715
+ blend: 0.005,
716
+ mirror: false
717
+ },
718
+ {
719
+ shape: "capsule",
720
+ at: [cx + 0.18, 0.12, nz - 0.02],
721
+ to: [cx + 0.26, 0.05, nz - 0.32],
722
+ size: 0.018,
723
+ color: c.leather,
724
+ blend: 0.01
725
+ }
726
+ ];
727
+ }
728
+ function tunic(p, cx, s, b, nz) {
729
+ const c = p.colors;
730
+ const chest = chestOf(cx, s, b);
731
+ const shell = (g) => ({
732
+ shape: "ellipsoid",
733
+ at: chest.at,
734
+ size: [chest.r[0] + g, chest.r[1] + g, chest.r[2] + g]
735
+ });
736
+ const n = p.neck.radius;
737
+ const axis = (z) => [cx - 0.08, 0, z];
738
+ const mantle = (g) => ({
739
+ shape: "ellipsoid",
740
+ at: [cx - 0.1, 0, b + 0.45],
741
+ size: [0.42 + g, s + 0.06 + g, 0.3 + g]
742
+ });
743
+ const opening = {
744
+ shape: "ellipsoid",
745
+ at: [cx + 0.38, 0, nz - 0.1],
746
+ size: [0.3, s * 0.45, 0.5],
747
+ op: "subtract",
748
+ blend: 0.02
749
+ };
750
+ const brooch = (y) => ({
751
+ group: [
752
+ mantle(0.03),
753
+ { shape: "sphere", at: [cx + 0.17, y, b + 0.5], size: 0.065, op: "intersect" }
754
+ ],
755
+ color: c.metal,
756
+ blend: 0.004
757
+ });
758
+ return [
759
+ {
760
+ group: [
761
+ mantle(0),
762
+ { shape: "roundBox", at: [0, 0, b + 0.3 + 1], size: [4, 4, 2], op: "intersect" },
763
+ opening
764
+ ],
765
+ color: c.cape ?? c.leather,
766
+ blend: 0.01
767
+ },
768
+ brooch(s * 0.4),
769
+ {
770
+ group: [
771
+ { shape: "capsule", at: axis(nz - 0.06), to: axis(nz + 0.1), size: n + 0.08 },
772
+ {
773
+ shape: "capsule",
774
+ at: axis(nz - 0.2),
775
+ to: axis(nz + 0.3),
776
+ size: n + 0.02,
777
+ op: "subtract"
778
+ },
779
+ {
780
+ shape: "ellipsoid",
781
+ at: [cx + 0.2, 0, nz + 0.12],
782
+ size: [0.12, 0.08, 0.14],
783
+ op: "subtract",
784
+ blend: 0.02
785
+ }
786
+ ],
787
+ color: c.leather,
788
+ blend: 0.015
789
+ },
790
+ {
791
+ group: [
792
+ shell(0.012),
793
+ { shape: "roundBox", at: [cx + 0.3, 0, b + 0.2], size: [0.4, 0.07, 0.6], op: "intersect" }
794
+ ],
795
+ color: c.leather,
796
+ blend: 0.004,
797
+ mirror: false
798
+ },
799
+ ...[0.42, 0.3, 0.18].map((z) => ({
800
+ group: [
801
+ shell(0.03),
802
+ { shape: "sphere", at: [cx + 0.3, 0, b + z], size: 0.03, op: "intersect" }
803
+ ],
804
+ color: c.metal,
805
+ blend: 0.003,
806
+ mirror: false
807
+ }))
808
+ ];
809
+ }
810
+ function bustEdits(p, l) {
811
+ const c = p.colors;
812
+ const cx = l.cranium.at[0] + p.jaw.muzzle * 0.3;
813
+ const s = p.bust.shoulders;
814
+ const b = l.base;
815
+ const nz = b + 0.6;
816
+ const body = p.bust.style === "pauldrons" ? c.skin : c.cloth;
817
+ const e = [
818
+ { shape: "ellipsoid", at: chestOf(cx, s, b).at, size: chestOf(cx, s, b).r, color: body },
819
+ {
820
+ shape: "capsule",
821
+ at: [cx - 0.16, 0.1, nz + 0.16],
822
+ to: [cx - 0.1, s * 0.78, b + 0.44],
823
+ size: 0.15,
824
+ color: body,
825
+ blend: 0.15
826
+ },
827
+ { shape: "sphere", at: [cx - 0.04, s * 0.84, b + 0.3], size: 0.2, color: body, blend: 0.1 },
828
+ {
829
+ shape: "capsule",
830
+ at: [cx - 0.2, 0.08, nz + 0.3],
831
+ to: [cx - 0.1, s * 0.62, b + 0.5],
832
+ size: 0.1 + p.neck.radius * 0.2,
833
+ color: body,
834
+ blend: 0.12
835
+ },
836
+ {
837
+ shape: "capsule",
838
+ at: [cx - 0.08, 0, nz - 0.05],
839
+ to: [cx - 0.1, 0, nz + 0.12],
840
+ size: p.neck.radius * 1.05,
841
+ color: c.skin,
842
+ blend: 0.03
843
+ }
844
+ ];
845
+ e.push({
846
+ shape: "roundBox",
847
+ at: [0, s + 1.02, b + 0.5],
848
+ size: [4, 2, 4],
849
+ op: "subtract",
850
+ color: body
851
+ });
852
+ if (p.bust.style === "pauldrons")
853
+ e.push(...torso(p, cx, b), ...armour(p, cx, s, b, nz));
854
+ else if (p.bust.style === "hood")
855
+ e.push(...hood(p, cx, nz));
856
+ else
857
+ e.push(...tunic(p, cx, s, b, nz));
858
+ e.push({ shape: "roundBox", at: [0, 0, b - 1], size: [4, 4, 2], op: "subtract" });
859
+ return e;
860
+ }
861
+
862
+ // src/kinds/head-extras.ts
863
+ var RAD3 = Math.PI / 180;
864
+ function earEdits(p, l, minThick = 0) {
865
+ const { length, width, sweep, lift, shape } = p.ears;
866
+ const cx = l.cranium.at[0];
867
+ const ry = l.cranium.r[1];
868
+ const root = [cx + 0.02, ry * 0.9, l.eye[2] - 0.05];
869
+ const d = unit([-Math.sin(sweep * RAD3), Math.cos(sweep * RAD3), Math.tan(lift * RAD3)]);
870
+ const rot = alongX(d, shape === "pointed" ? -10 : 0);
871
+ const thick = Math.max(0.022 + width * 0.06, minThick);
872
+ const front = [Math.cos(sweep * RAD3), Math.sin(sweep * RAD3), 0];
873
+ const shade = p.colors.shade;
874
+ if (shape === "round") {
875
+ const at = along(root, d, 0.03);
876
+ const rot2 = [0, -lift, 90 + sweep];
877
+ return [
878
+ { shape: "ellipsoid", at, size: [width / 2, thick, length / 2], rotate: rot2, blend: 0.02 },
879
+ {
880
+ shape: "ellipsoid",
881
+ at: along(along(at, d, thick * 0.6), front, 0.004),
882
+ size: [width * 0.3, thick * 0.55, length * 0.3],
883
+ rotate: rot2,
884
+ op: "subtract",
885
+ blend: 0.012,
886
+ color: shade
887
+ },
888
+ {
889
+ shape: "ellipsoid",
890
+ at: [at[0], at[1] + thick * 0.4, at[2] - length * 0.33],
891
+ size: [width * 0.24, thick * 0.9, length * 0.16],
892
+ blend: 0.015
893
+ }
894
+ ];
895
+ }
896
+ return [
897
+ {
898
+ shape: "ellipsoid",
899
+ at: along(root, d, length * 0.3),
900
+ size: [length * 0.36, thick, width * 0.5],
901
+ rotate: rot,
902
+ blend: 0.03
903
+ },
904
+ {
905
+ shape: "ellipsoid",
906
+ at: along(root, d, length * 0.68),
907
+ size: [length * 0.34, thick * 0.75, width * 0.22],
908
+ rotate: rot,
909
+ blend: 0.04
910
+ },
911
+ {
912
+ shape: "ellipsoid",
913
+ at: along(along(root, d, length * 0.42), front, thick * 0.85),
914
+ size: [length * 0.36, thick * 0.6, width * 0.3],
915
+ rotate: rot,
916
+ op: "subtract",
917
+ blend: 0.015,
918
+ color: shade
919
+ },
920
+ { shape: "sphere", at: root, size: 0.04, blend: 0.03 }
921
+ ];
922
+ }
923
+ function tuskEdits(p, l, minR = 0) {
924
+ const t = p.tusks;
925
+ if (!t)
926
+ return [];
927
+ const out = [];
928
+ let at = [l.mouthX - 0.03 + p.jaw.underbite * 0.5, t.spread, l.mouthZ - 0.02];
929
+ let dir = unit([0.25, t.flare * 0.5, 1]);
930
+ const seg = t.length / 3;
931
+ for (let i = 0;i < 3; i++) {
932
+ const r0 = Math.max(t.radius * (1 - i / 3.4), minR);
933
+ const r1 = Math.max(t.radius * (1 - (i + 1) / 3.4), minR * 0.6);
934
+ out.push({
935
+ shape: "cone",
936
+ at,
937
+ size: [r0, Math.max(r1, 0.004), seg * 1.05],
938
+ rotate: alongZ(dir),
939
+ color: p.colors.teeth,
940
+ blend: i === 0 ? 0.02 : 0.004
941
+ });
942
+ at = along(at, dir, seg);
943
+ dir = unit([dir[0] - 0.22, dir[1] + t.flare * 0.15, dir[2]]);
944
+ }
945
+ return out;
946
+ }
947
+ function paintEdits(p, l) {
948
+ const c = p.colors.paint;
949
+ if (!c)
950
+ return [];
951
+ const [ex, ey, ez] = l.eye;
952
+ const band = (at, r, mirror = true, tilt = 0) => ({
953
+ shape: "ellipsoid",
954
+ at,
955
+ size: r,
956
+ rotate: [tilt, 0, 0],
957
+ op: "paint",
958
+ color: c,
959
+ blend: 0.006,
960
+ mirror
961
+ });
962
+ const chinZ = (l.mouthZ + l.chin[2]) / 2 - 0.02;
963
+ return [
964
+ band([ex + 0.1, ey - 0.035, ez - 0.13], [0.45, 0.016, 0.09], true, -8),
965
+ band([ex + 0.1, ey + 0.035, ez - 0.12], [0.45, 0.016, 0.085], true, -8),
966
+ band([l.cranium.at[0], 0, l.browZ + 0.14], [0.6, 0.028, 0.13], false),
967
+ band([l.chin[0], 0, chinZ], [0.4, 0.012, 0.06], false),
968
+ band([l.chin[0], 0.055, chinZ], [0.4, 0.012, 0.05])
969
+ ];
970
+ }
971
+ function wartEdits(p, l, rng) {
972
+ const cx = l.cranium.at[0];
973
+ const [rx, ry, rz] = l.cranium.r;
974
+ return Array.from({ length: p.warts }, () => {
975
+ const yaw = rng.range(-1.2, 1.2);
976
+ const el = rng.range(-0.1, 0.9);
977
+ const at = [
978
+ cx + rx * Math.cos(el) * Math.cos(yaw),
979
+ ry * Math.cos(el) * Math.sin(yaw),
980
+ l.cranium.at[2] + rz * Math.sin(el)
981
+ ];
982
+ return {
983
+ shape: "sphere",
984
+ at,
985
+ size: rng.range(0.008, 0.016),
986
+ color: p.colors.shade,
987
+ blend: 0.01,
988
+ mirror: false
989
+ };
990
+ });
991
+ }
992
+
993
+ // src/kinds/head-face.ts
994
+ function primaryEdits(l) {
995
+ const [cx, , cz] = l.cranium.at;
996
+ const [rx, ry, rz] = l.cranium.r;
997
+ return [
998
+ { shape: "ellipsoid", at: [cx, 0, cz], size: [rx, ry, rz] },
999
+ {
1000
+ shape: "ellipsoid",
1001
+ at: [l.faceX - 0.19, 0, l.eye[2] - 0.03],
1002
+ size: [0.2, ry * 0.86, 0.24],
1003
+ blend: 0.08
1004
+ }
1005
+ ];
1006
+ }
1007
+ function skullEdits(p, l) {
1008
+ const c = p.colors;
1009
+ const cx = l.cranium.at[0];
1010
+ const [ex, ey, ez] = l.eye;
1011
+ const r = l.eyeR;
1012
+ const lipT = p.mouth.lips;
1013
+ const mw = p.mouth.width;
1014
+ const [ax, ay, az] = l.angle;
1015
+ const [hx, hz] = l.hinge;
1016
+ const chin = l.chin;
1017
+ const tipX = l.faceX + 0.06 + p.nose.length * 0.25 + p.nose.bridge + p.jaw.muzzle * 0.5;
1018
+ const tipZ = l.noseZ + 0.035 - p.nose.hook;
1019
+ const e = [
1020
+ ...primaryEdits(l),
1021
+ {
1022
+ shape: "ellipsoid",
1023
+ at: [ex - 0.03, (ey + 0.06) * p.cheeks.width, ez - r - 0.065 * p.cheeks.size],
1024
+ size: [0.09 * p.cheeks.size, 0.08 * p.cheeks.size, 0.065 * p.cheeks.size],
1025
+ rotate: [0, 0, 25],
1026
+ blend: 0.09
1027
+ },
1028
+ {
1029
+ shape: "ellipsoid",
1030
+ at: [l.faceX - 0.13 + p.jaw.muzzle * 0.6, 0, l.noseZ + 0.01],
1031
+ size: [0.15, 0.2 * p.cheeks.width, 0.13],
1032
+ blend: 0.08
1033
+ },
1034
+ {
1035
+ shape: "ellipsoid",
1036
+ at: [l.mouthX - 0.13, 0, l.mouthZ + 0.02],
1037
+ size: [0.14, Math.max(mw * 1.15, 0.13), 0.12 + p.jaw.drop * 0.2],
1038
+ blend: 0.07
1039
+ },
1040
+ {
1041
+ shape: "roundBox",
1042
+ at: [(ax + chin[0]) / 2 + 0.01, 0, (az + chin[2]) / 2 + 0.03],
1043
+ size: [chin[0] - ax + 0.04, ay * 1.75, 0.16 + p.jaw.drop * 0.5],
1044
+ round: 0.07,
1045
+ rotate: [0, Math.atan2(az - chin[2], chin[0] - ax) * (180 / Math.PI) * 0.6, 0],
1046
+ blend: 0.08
1047
+ },
1048
+ {
1049
+ shape: "capsule",
1050
+ at: [hx - 0.04, ay * 0.85, hz],
1051
+ to: [ax, ay, az],
1052
+ size: 0.055,
1053
+ blend: 0.07
1054
+ },
1055
+ { shape: "sphere", at: [ax + 0.02, ay * 0.95, az], size: 0.065, blend: 0.07 },
1056
+ {
1057
+ shape: "ellipsoid",
1058
+ at: chin,
1059
+ size: [0.065 * p.jaw.chin, 0.09 * p.jaw.chin, 0.07 * p.jaw.chin],
1060
+ blend: 0.06
1061
+ },
1062
+ {
1063
+ shape: "capsule",
1064
+ at: [cx + 0.02, 0, ez - 0.22],
1065
+ to: [cx - 0.05, 0, l.base + 0.45],
1066
+ size: p.neck.radius,
1067
+ blend: 0.1
1068
+ },
1069
+ {
1070
+ shape: "capsule",
1071
+ at: [ex + r * 0.75, 0.025, l.browZ - p.brow.scowl],
1072
+ to: [ex + r * 0.1, ey + r * 1.25, l.browZ + p.brow.scowl * 0.5],
1073
+ size: p.brow.ridge,
1074
+ blend: 0.06
1075
+ },
1076
+ { shape: "sphere", at: l.eye, size: r * 1.12, op: "subtract", blend: r * 0.5, color: c.shade }
1077
+ ];
1078
+ const lid = (deg, upper) => {
1079
+ const t = deg * Math.PI / 180;
1080
+ const n = upper ? [-Math.sin(t), 0, Math.cos(t)] : [Math.sin(t), 0, Math.cos(t)];
1081
+ const k = upper ? 1 : -1;
1082
+ return {
1083
+ group: [
1084
+ { shape: "sphere", at: l.eye, size: r * (upper ? 1.14 : 1.1) },
1085
+ {
1086
+ shape: "roundBox",
1087
+ at: [ex + n[0] * k, ey, ez + n[2] * k],
1088
+ size: [4, 4, 2],
1089
+ rotate: [0, upper ? -deg : deg, 0],
1090
+ op: "intersect"
1091
+ }
1092
+ ],
1093
+ blend: r * 0.22
1094
+ };
1095
+ };
1096
+ e.push(lid(p.eyes.lid, true), lid(p.eyes.lowerLid, false));
1097
+ const noseRoot = [ex + r * 0.4, 0, ez - r * 0.4];
1098
+ const noseDir = [tipX - noseRoot[0], 0, tipZ + 0.01 - noseRoot[2]];
1099
+ const midNose = [
1100
+ (ex + r * 0.6 + tipX) / 2,
1101
+ 0,
1102
+ (ez - 0.02 + tipZ) / 2 + p.nose.hook * 0.6 + 0.01
1103
+ ];
1104
+ e.push({
1105
+ shape: "cone",
1106
+ at: noseRoot,
1107
+ size: [0.02 + p.nose.width * 0.6, p.nose.tip * 0.8, Math.hypot(...noseDir)],
1108
+ rotate: alongZ(noseDir),
1109
+ blend: 0.04
1110
+ }, { shape: "sphere", at: midNose, size: 0.02 + p.nose.width * 0.2, blend: 0.04 }, { shape: "sphere", at: [tipX, 0, tipZ], size: p.nose.tip, blend: 0.03 }, {
1111
+ shape: "ellipsoid",
1112
+ at: [tipX - p.nose.tip * 1.2, p.nose.width, l.noseZ + 0.02],
1113
+ size: [0.045, 0.035, 0.032],
1114
+ blend: 0.025
1115
+ }, {
1116
+ shape: "ellipsoid",
1117
+ at: [tipX - p.nose.tip * 0.6, p.nose.width * 0.55, l.noseZ + 0.005],
1118
+ size: [0.022, 0.014, 0.012],
1119
+ op: "subtract",
1120
+ blend: 0.008,
1121
+ color: c.mouth
1122
+ });
1123
+ const g = p.mouth.grin;
1124
+ e.push({
1125
+ shape: "ellipsoid",
1126
+ at: [l.mouthX - 0.01, 0, l.mouthZ + lipT * 0.7],
1127
+ size: [0.032, mw, lipT],
1128
+ color: c.lips,
1129
+ blend: 0.03
1130
+ }, {
1131
+ shape: "ellipsoid",
1132
+ at: [l.mouthX - 0.015 + p.jaw.underbite * 0.7, 0, l.mouthZ - lipT * 0.8],
1133
+ size: [0.034, mw * 0.85, lipT * 1.1],
1134
+ color: c.lips,
1135
+ blend: 0.03
1136
+ }, {
1137
+ shape: "capsule",
1138
+ at: [l.mouthX + 0.05, 0, l.mouthZ],
1139
+ to: [l.mouthX - 0.03 - 0.02 * g, mw * 0.98, l.mouthZ + 0.012 + g * 0.05],
1140
+ size: 0.006 + g * 0.022,
1141
+ op: "subtract",
1142
+ blend: 0.012,
1143
+ color: c.mouth
1144
+ });
1145
+ if (g > 0.15)
1146
+ e.push(...teethRows(p, l));
1147
+ const fold = unit([-0.02, mw - p.nose.width, l.mouthZ - l.noseZ]);
1148
+ e.push({
1149
+ shape: "ellipsoid",
1150
+ at: [tipX - p.nose.tip * 1.5 - 0.035, p.nose.width + 0.05, l.noseZ - 0.035],
1151
+ size: [0.08, 0.006, 0.006],
1152
+ rotate: alongX(fold),
1153
+ op: "subtract",
1154
+ blend: 0.03,
1155
+ color: c.shade
1156
+ });
1157
+ return e;
1158
+ }
1159
+ function teethRows(p, l) {
1160
+ const out = [];
1161
+ const mw = p.mouth.width * 0.8;
1162
+ for (let i = 0;i < 4; i++) {
1163
+ const a = (i + 0.5) / 4 * 0.9;
1164
+ const y = Math.sin(a) * mw;
1165
+ const x = l.mouthX - 0.025 - (1 - Math.cos(a)) * mw * 0.9;
1166
+ const z = l.mouthZ + 0.004 + p.mouth.grin * 0.03 * (i / 4);
1167
+ for (const [dz, h] of [
1168
+ [0.016, 0.02],
1169
+ [-0.016, 0.018]
1170
+ ])
1171
+ out.push({
1172
+ shape: "roundBox",
1173
+ at: [x, y, z + dz * (1 + p.mouth.grin)],
1174
+ size: [0.02, 0.028, h * (1 + p.mouth.grin * 0.6)],
1175
+ round: 0.006,
1176
+ rotate: [0, 0, a * 180 / Math.PI],
1177
+ color: p.colors.teeth,
1178
+ blend: 0.003
1179
+ });
1180
+ }
1181
+ return out;
1182
+ }
1183
+
1184
+ // src/kinds/head-hair.ts
1185
+ var RAD4 = Math.PI / 180;
1186
+ function hairEdits(p, l) {
1187
+ const h = p.colors.hair;
1188
+ const [cx, , cz] = l.cranium.at;
1189
+ const [rx, ry, rz] = l.cranium.r;
1190
+ const top = l.crown;
1191
+ switch (p.hair) {
1192
+ case "topknot":
1193
+ return [
1194
+ { shape: "ellipsoid", at: [cx - 0.12, 0, top - 0.02], size: [0.11, 0.08, 0.07], color: h },
1195
+ {
1196
+ shape: "sphere",
1197
+ at: [cx - 0.16, 0, top + 0.07],
1198
+ size: 0.075,
1199
+ color: h,
1200
+ blend: 0.03
1201
+ },
1202
+ {
1203
+ shape: "capsule",
1204
+ at: [cx - 0.2, 0, top + 0.08],
1205
+ to: [cx - 0.42, 0, top - 0.25],
1206
+ size: 0.04,
1207
+ color: h,
1208
+ blend: 0.04
1209
+ },
1210
+ {
1211
+ shape: "torus",
1212
+ at: [cx - 0.17, 0, top + 0.02],
1213
+ size: [0.06, 0.018],
1214
+ color: p.colors.metal
1215
+ }
1216
+ ];
1217
+ case "mohawk":
1218
+ return Array.from({ length: 6 }, (_, i) => {
1219
+ const a = (-20 + i * 26) * RAD4;
1220
+ return {
1221
+ shape: "ellipsoid",
1222
+ at: [cx + Math.sin(a) * rx * 0.95, 0, cz + Math.cos(a) * rz * 0.97],
1223
+ size: [0.06, 0.035, 0.09],
1224
+ rotate: [0, a * 180 / Math.PI - 15, 0],
1225
+ color: h,
1226
+ blend: 0.03
1227
+ };
1228
+ });
1229
+ case "swept": {
1230
+ const C = [cx - 0.04, 0, cz + 0.04];
1231
+ const R = [rx + 0.035, ry + 0.04, rz + 0.02];
1232
+ const on = (az, el, lift = 0) => [
1233
+ C[0] + (R[0] + lift) * Math.cos(el * RAD4) * Math.cos(az * RAD4),
1234
+ C[1] + (R[1] + lift) * Math.cos(el * RAD4) * Math.sin(az * RAD4),
1235
+ C[2] + (R[2] + lift) * Math.sin(el * RAD4)
1236
+ ];
1237
+ const light = [h[0] * 1.45, h[1] * 1.45, h[2] * 1.45];
1238
+ const cone = (a, b, r0, r1) => ({
1239
+ shape: "cone",
1240
+ at: a,
1241
+ size: [r0, r1, Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])],
1242
+ rotate: alongZ([b[0] - a[0], b[1] - a[1], b[2] - a[2]])
1243
+ });
1244
+ const lock = (az, el, len, r, i) => {
1245
+ const a = on(az, el, -0.005);
1246
+ const m = on(az + len * 0.3, el - len * 0.45, -0.012);
1247
+ const b = on(az + len * 0.6, el - len, -0.045);
1248
+ return {
1249
+ group: [cone(a, m, r, r * 0.75), cone(m, b, r * 0.75, r * 0.15)],
1250
+ color: i % 2 ? h : light,
1251
+ blend: 0.035,
1252
+ ...az === 0 ? { mirror: false } : {}
1253
+ };
1254
+ };
1255
+ return [
1256
+ {
1257
+ group: [
1258
+ { shape: "ellipsoid", at: C, size: R },
1259
+ {
1260
+ shape: "ellipsoid",
1261
+ at: [l.faceX + 0.12, 0, l.eye[2] - 0.08],
1262
+ size: [0.34, ry * 0.92, 0.36],
1263
+ op: "subtract",
1264
+ blend: 0.03
1265
+ },
1266
+ {
1267
+ shape: "ellipsoid",
1268
+ at: [cx + 0.02, ry, l.eye[2] - 0.08],
1269
+ size: [0.13, 0.25, 0.16],
1270
+ op: "subtract",
1271
+ blend: 0.03
1272
+ }
1273
+ ],
1274
+ color: h,
1275
+ blend: 0.012
1276
+ },
1277
+ lock(0, 44, 50, 0.07, 1),
1278
+ lock(22, 46, 50, 0.07, 0),
1279
+ lock(45, 40, 45, 0.068, 1),
1280
+ lock(70, 34, 40, 0.06, 0),
1281
+ lock(95, 30, 35, 0.055, 1),
1282
+ lock(120, 30, 30, 0.05, 0)
1283
+ ];
1284
+ }
1285
+ case "tufts":
1286
+ return Array.from({ length: 5 }, (_, i) => {
1287
+ const a = (i / 4 - 0.5) * 1.6;
1288
+ const base = [cx - 0.1 + Math.cos(a) * 0.05, Math.sin(a) * 0.12, top - 0.04];
1289
+ return {
1290
+ shape: "cone",
1291
+ at: base,
1292
+ size: [0.025, 0.004, 0.12 + i % 2 * 0.05],
1293
+ rotate: alongZ([-0.6, Math.sin(a) * 0.8, 1]),
1294
+ color: h,
1295
+ blend: 0.01,
1296
+ mirror: false
1297
+ };
1298
+ });
1299
+ case "braids":
1300
+ return [
1301
+ { ...capOf(l, h) },
1302
+ {
1303
+ shape: "capsule",
1304
+ at: [cx - 0.2, ry * 0.7, cz - 0.1],
1305
+ to: [cx - 0.1, ry * 0.8, cz - 0.6],
1306
+ size: 0.035,
1307
+ color: h,
1308
+ blend: 0.02
1309
+ }
1310
+ ];
1311
+ default:
1312
+ return [];
1313
+ }
1314
+ }
1315
+ var capOf = (l, h) => ({
1316
+ shape: "ellipsoid",
1317
+ at: [l.cranium.at[0] - 0.07, 0, l.cranium.at[2] + 0.09],
1318
+ size: [l.cranium.r[0] + 0.02, l.cranium.r[1] + 0.025, l.cranium.r[2]],
1319
+ color: h,
1320
+ blend: 0.02
1321
+ });
1322
+
1323
+ // src/kinds/head-layout.ts
1324
+ function headLayout(p) {
1325
+ const L = p.eyes.line;
1326
+ const crH = p.cranium.height;
1327
+ const rx = p.cranium.depth / 2;
1328
+ const ry = p.cranium.width / 2;
1329
+ const rz = crH * 0.8;
1330
+ const cx = -0.02 - p.jaw.muzzle * 0.4;
1331
+ const cz = L + crH * 0.2;
1332
+ const faceX = cx + rx * 0.97;
1333
+ const jut = p.jaw.muzzle;
1334
+ const eyeR = p.eyes.radius;
1335
+ const eye = [faceX - eyeR - p.brow.overhang, p.eyes.spacing, L];
1336
+ const noseZ = L - p.nose.length;
1337
+ const mouthZ = noseZ - 0.1 - p.jaw.drop * 0.3;
1338
+ const chinZ = -p.jaw.drop;
1339
+ const mouthX = faceX - 0.01 + jut + p.jaw.underbite * 0.3;
1340
+ return {
1341
+ cranium: { at: [cx, 0, cz], r: [rx, ry, rz] },
1342
+ faceX,
1343
+ eye,
1344
+ eyeR,
1345
+ browZ: L + eyeR * 0.9 + p.brow.ridge * 0.6,
1346
+ noseZ,
1347
+ mouthZ,
1348
+ mouthX,
1349
+ chin: [faceX - 0.07 + jut + p.jaw.underbite, 0, chinZ + 0.07 * p.jaw.chin],
1350
+ hinge: [cx + 0.04, L - 0.09],
1351
+ angle: [cx - 0.02, p.jaw.width, 0.15 - p.jaw.drop * 0.6],
1352
+ crown: cz + rz,
1353
+ base: -0.85 - p.jaw.drop * 0.5
1354
+ };
1355
+ }
1356
+
1357
+ // src/kinds/head-rim.ts
1358
+ function grad(f, p, e, out) {
1359
+ const q = [p[0], p[1], p[2]];
1360
+ const at = (dx, dy, dz) => {
1361
+ q[0] = p[0] + dx;
1362
+ q[1] = p[1] + dy;
1363
+ q[2] = p[2] + dz;
1364
+ return f(q);
1365
+ };
1366
+ out[0] = (at(e, 0, 0) - at(-e, 0, 0)) / (2 * e);
1367
+ out[1] = (at(0, e, 0) - at(0, -e, 0)) / (2 * e);
1368
+ out[2] = (at(0, 0, e) - at(0, 0, -e)) / (2 * e);
1369
+ return f(p);
1370
+ }
1371
+ function project(f, p, e, onlyOutside) {
1372
+ const g = [0, 0, 0];
1373
+ const v = grad(f, p, e, g);
1374
+ if (onlyOutside && v <= 0)
1375
+ return;
1376
+ const gg = g[0] * g[0] + g[1] * g[1] + g[2] * g[2];
1377
+ if (gg < 0.000000000001)
1378
+ return;
1379
+ for (let k = 0;k < 3; k++)
1380
+ p[k] = (p[k] ?? 0) - v * (g[k] ?? 0) / gg;
1381
+ }
1382
+ function sharpenRim(m, skull, side, part, res) {
1383
+ const positions = new Float32Array(m.positions);
1384
+ const normals = new Float32Array(m.normals);
1385
+ const e = res * 0.25;
1386
+ const gs = [0, 0, 0];
1387
+ const gc = [0, 0, 0];
1388
+ for (let i = 0;i < positions.length; i += 3) {
1389
+ const p = [positions[i] ?? 0, positions[i + 1] ?? 0, positions[i + 2] ?? 0];
1390
+ if (Math.abs(side(p)) > res * 2)
1391
+ continue;
1392
+ const c = grad(side, p, e, gc);
1393
+ const s = grad(skull, p, e, gs);
1394
+ if (Math.abs(s) > res * 2)
1395
+ continue;
1396
+ if (part(p) < Math.max(s, c) - res * 0.5)
1397
+ continue;
1398
+ const n = [normals[i] ?? 0, normals[i + 1] ?? 0, normals[i + 2] ?? 0];
1399
+ const ls = Math.hypot(...gs) || 1;
1400
+ const lc = Math.hypot(...gc) || 1;
1401
+ const onSkin = (n[0] * gs[0] + n[1] * gs[1] + n[2] * gs[2]) / ls >= (n[0] * gc[0] + n[1] * gc[1] + n[2] * gc[2]) / lc;
1402
+ for (let k = 0;k < 4; k++) {
1403
+ if (onSkin) {
1404
+ project(skull, p, e, false);
1405
+ project(side, p, e, true);
1406
+ } else {
1407
+ project(side, p, e, false);
1408
+ project(skull, p, e, true);
1409
+ }
1410
+ }
1411
+ const other = onSkin ? side(p) : skull(p);
1412
+ if (other > -res * 0.6)
1413
+ for (let k = 0;k < 6; k++) {
1414
+ project(skull, p, e, false);
1415
+ project(side, p, e, false);
1416
+ }
1417
+ positions[i] = p[0];
1418
+ positions[i + 1] = p[1];
1419
+ positions[i + 2] = p[2];
1420
+ if (onSkin) {
1421
+ grad(skull, p, e, gs);
1422
+ const l = Math.hypot(...gs) || 1;
1423
+ normals[i] = gs[0] / l;
1424
+ normals[i + 1] = gs[1] / l;
1425
+ normals[i + 2] = gs[2] / l;
1426
+ }
1427
+ }
1428
+ return { ...m, positions, normals };
1429
+ }
1430
+ function creaseNormals(m, minDot = 0.2) {
1431
+ const P = m.positions;
1432
+ const acc = new Float64Array(P.length);
1433
+ for (let t = 0;t < m.indices.length; t += 3) {
1434
+ const a = (m.indices[t] ?? 0) * 3;
1435
+ const b = (m.indices[t + 1] ?? 0) * 3;
1436
+ const c = (m.indices[t + 2] ?? 0) * 3;
1437
+ const ux = (P[b] ?? 0) - (P[a] ?? 0);
1438
+ const uy = (P[b + 1] ?? 0) - (P[a + 1] ?? 0);
1439
+ const uz = (P[b + 2] ?? 0) - (P[a + 2] ?? 0);
1440
+ const vx = (P[c] ?? 0) - (P[a] ?? 0);
1441
+ const vy = (P[c + 1] ?? 0) - (P[a + 1] ?? 0);
1442
+ const vz = (P[c + 2] ?? 0) - (P[a + 2] ?? 0);
1443
+ const n = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
1444
+ for (const v of [a, b, c])
1445
+ for (let k = 0;k < 3; k++)
1446
+ acc[v + k] = (acc[v + k] ?? 0) + (n[k] ?? 0);
1447
+ }
1448
+ const normals = new Float32Array(m.normals);
1449
+ for (let i = 0;i < normals.length; i += 3) {
1450
+ const l = Math.hypot(acc[i] ?? 0, acc[i + 1] ?? 0, acc[i + 2] ?? 0);
1451
+ if (l < 0.00000000000000000001)
1452
+ continue;
1453
+ const f = [(acc[i] ?? 0) / l, (acc[i + 1] ?? 0) / l, (acc[i + 2] ?? 0) / l];
1454
+ const d = (normals[i] ?? 0) * (f[0] ?? 0) + (normals[i + 1] ?? 0) * (f[1] ?? 0) + (normals[i + 2] ?? 0) * (f[2] ?? 0);
1455
+ if (d >= minDot)
1456
+ continue;
1457
+ normals[i] = f[0] ?? 0;
1458
+ normals[i + 1] = f[1] ?? 0;
1459
+ normals[i + 2] = f[2] ?? 0;
1460
+ }
1461
+ return { ...m, normals };
1462
+ }
1463
+
1464
+ // src/kinds/head-mesh.ts
1465
+ function jawRegion(p, l) {
1466
+ const chin = l.chin;
1467
+ const reach = Math.max(p.mouth.width * 1.25, (p.tusks?.spread ?? 0) + p.mouth.width * 0.5);
1468
+ const centre = [l.mouthX - 0.15, 0, l.mouthZ];
1469
+ const radii = [Math.max(0.27, l.mouthX - chin[0] + 0.2), reach, l.mouthZ - chin[2] + 0.1];
1470
+ const e = sd2.ellipsoid(centre, radii);
1471
+ return { field: (q) => Math.max(q[2] - l.mouthZ, e(q)), centre, radii };
1472
+ }
1473
+ var unionBounds = (a, b) => b ? [
1474
+ [Math.min(a[0][0], b[0][0]), Math.min(a[0][1], b[0][1]), Math.min(a[0][2], b[0][2])],
1475
+ [Math.max(a[1][0], b[1][0]), Math.max(a[1][1], b[1][1]), Math.max(a[1][2], b[1][2])]
1476
+ ] : [a[0], a[1]];
1477
+ function regionGroup(l, r, grow) {
1478
+ const cx = r.centre[0];
1479
+ const [rx, ry, rz] = r.radii;
1480
+ return {
1481
+ group: [
1482
+ { shape: "ellipsoid", at: r.centre, size: [rx + grow, ry + grow, rz + grow] },
1483
+ {
1484
+ shape: "roundBox",
1485
+ at: [cx, 0, l.mouthZ + grow - 1],
1486
+ size: [4, 4, 2],
1487
+ op: "intersect"
1488
+ }
1489
+ ],
1490
+ mirror: false,
1491
+ blend: 0
1492
+ };
1493
+ }
1494
+ function headAndJaw(p, l, set, res) {
1495
+ const opts = { color: p.colors.skin, mirror: "y" };
1496
+ const region = jawRegion(p, l);
1497
+ const cut = region.field;
1498
+ const lap = res * 0.75;
1499
+ const proud = res * 0.15;
1500
+ const ears = set.ears.length > 0 ? [{ group: set.ears, blend: 0.02 }] : [];
1501
+ const tusks = set.tusks.length > 0 ? [{ group: set.tusks, blend: 0.01 }] : [];
1502
+ const h = sculptField2([...set.skull, ...ears, { ...regionGroup(l, region, 0), op: "subtract" }], opts);
1503
+ const jRaw = sculptField2([...set.skull, { ...regionGroup(l, region, lap), op: "intersect" }, ...tusks], opts);
1504
+ const jField = (q) => jRaw.field(q) - proud;
1505
+ const mouth = p.colors.mouth;
1506
+ const skull = sculptField2(set.skull, opts).field;
1507
+ const deep = -Math.max(res * 4, 0.03);
1508
+ const paint = (colorAt) => (q) => Math.abs(cut(q)) < res * 1.5 && skull(q) < deep ? mouth : colorAt(q);
1509
+ const [ec, er] = [region.centre, region.radii];
1510
+ const jb = [
1511
+ [ec[0] - er[0] - lap, -er[1] - lap, ec[2] - er[2] - lap],
1512
+ [ec[0] + er[0] + lap, er[1] + lap, ec[2] + lap + 0.01]
1513
+ ];
1514
+ const tb = set.tusks.length > 0 ? sculptField2(set.tusks, opts).bounds : undefined;
1515
+ const head = sdf2({
1516
+ field: h.field,
1517
+ bounds: h.bounds,
1518
+ resolution: res,
1519
+ color: paint(h.colorAt),
1520
+ symmetry: "y"
1521
+ });
1522
+ const jaw = sdf2({
1523
+ field: jField,
1524
+ bounds: unionBounds(jb, tb),
1525
+ resolution: res,
1526
+ color: paint(jRaw.colorAt),
1527
+ symmetry: "y"
1528
+ });
1529
+ if (res > 0.03)
1530
+ return { head: creaseNormals(head), jaw: creaseNormals(jaw) };
1531
+ const jawSkull = (q) => skull(q) - proud;
1532
+ return {
1533
+ head: creaseNormals(head),
1534
+ jaw: creaseNormals(sharpenRim(jaw, jawSkull, (q) => cut(q) - lap, jField, res))
1535
+ };
1536
+ }
1537
+ function bustMesh(p, set, res) {
1538
+ const s = sculptField2(set.bust, { color: p.colors.skin, mirror: "y" });
1539
+ return sdf2({
1540
+ field: s.field,
1541
+ bounds: s.bounds,
1542
+ resolution: res,
1543
+ color: (q) => s.colorAt(q),
1544
+ symmetry: "y"
1545
+ });
1546
+ }
1547
+ function eyeMesh(p, r, res) {
1548
+ const c = p.colors;
1549
+ const iris = Math.cos(0.62);
1550
+ const limbus = Math.cos(0.7);
1551
+ const pupil = Math.cos(0.3);
1552
+ return sdf2({
1553
+ field: (q) => Math.hypot(q[0], q[1], q[2]) - r,
1554
+ bounds: [
1555
+ [-r * 1.3, -r * 1.3, -r * 1.3],
1556
+ [r * 1.3, r * 1.3, r * 1.3]
1557
+ ],
1558
+ resolution: res,
1559
+ color: (q) => {
1560
+ const cos = q[0] / (Math.hypot(q[0], q[1], q[2]) || 1);
1561
+ if (cos > pupil)
1562
+ return c.pupil;
1563
+ if (cos > iris)
1564
+ return c.iris;
1565
+ if (cos > limbus)
1566
+ return [c.iris[0] * 0.45, c.iris[1] * 0.45, c.iris[2] * 0.45];
1567
+ return c.sclera;
1568
+ }
1569
+ });
1570
+ }
1571
+
1572
+ // src/kinds/head-params.ts
1573
+ var bad = (cause, fix) => {
1574
+ throw new Ts23dError2("T3D_RECIPE_INVALID", { cause, fix });
1575
+ };
1576
+ function numbers(o, path, out) {
1577
+ if (typeof o === "number")
1578
+ out.push([path, o]);
1579
+ else if (o && typeof o === "object" && !Array.isArray(o))
1580
+ for (const [k, v] of Object.entries(o))
1581
+ numbers(v, path ? `${path}.${k}` : k, out);
1582
+ }
1583
+ var SIGNED = new Set([
1584
+ "brow.scowl",
1585
+ "jaw.underbite",
1586
+ "nose.hook",
1587
+ "ears.lift",
1588
+ "ears.sweep",
1589
+ "nose.bridge"
1590
+ ]);
1591
+ var ZERO_OK = new Set([
1592
+ "mouth.grin",
1593
+ "warts",
1594
+ "jaw.drop",
1595
+ "jaw.muzzle",
1596
+ "brow.overhang",
1597
+ "brow.hair"
1598
+ ]);
1599
+ function checkHead2(p) {
1600
+ if (!(p.height >= 0.05 && p.height <= 3))
1601
+ bad(`head height ${p.height} m`, "give height in metres, chin to crown: human 0.23, orc ≈ 0.3");
1602
+ const flat = [];
1603
+ numbers({ ...p, colors: undefined, height: undefined }, "", flat);
1604
+ for (const [k, v] of flat) {
1605
+ if (!Number.isFinite(v))
1606
+ bad(`head param ${k} is ${v}`, "use finite numbers (head units)");
1607
+ if (SIGNED.has(k))
1608
+ continue;
1609
+ if (v < 0 || v === 0 && !ZERO_OK.has(k))
1610
+ bad(`head param ${k} is ${v}`, `make ${k} > 0 (head units: 1 = chin to crown)`);
1611
+ }
1612
+ const e = p.eyes;
1613
+ if (e.line < 0.3 || e.line > 0.7)
1614
+ bad(`eyes.line ${e.line}`, "keep the eye line between 0.3 and 0.7 of the head height");
1615
+ if (e.spacing < e.radius * 1.1)
1616
+ bad(`eyes.spacing ${e.spacing} < eyes.radius ${e.radius} × 1.1: the eyeballs overlap`, "raise eyes.spacing (centre to midline) or shrink eyes.radius");
1617
+ if (e.lid <= e.lowerLid * -1 || e.lid > 80)
1618
+ bad(`eye lids ${e.lid}°/${e.lowerLid}° leave no opening`, "use lid in (−lowerLid, 80] degrees");
1619
+ if (p.mouth.grin > 1)
1620
+ bad(`mouth.grin ${p.mouth.grin}`, "grin runs 0 (closed) … 1 (wide)");
1621
+ if (p.tusks && p.tusks.length > 0.6)
1622
+ bad(`tusks.length ${p.tusks.length}`, "tusks longer than 0.6 head units read as horns");
1623
+ return p;
1624
+ }
1625
+
1626
+ // src/kinds/head-presets.ts
1627
+ var human = {
1628
+ height: 0.23,
1629
+ cranium: { width: 0.66, depth: 0.86, height: 0.5 },
1630
+ brow: { ridge: 0.035, overhang: 0.03, scowl: 0.012, hair: 0.018 },
1631
+ cheeks: { size: 1, width: 1 },
1632
+ jaw: { muzzle: 0, width: 0.22, chin: 1.05, underbite: 0, drop: 0 },
1633
+ nose: { length: 0.18, width: 0.06, tip: 0.036, hook: 0, bridge: 0 },
1634
+ ears: { shape: "round", length: 0.27, width: 0.15, sweep: 12, lift: 0 },
1635
+ eyes: { radius: 0.052, spacing: 0.135, line: 0.5, lid: 20, lowerLid: 30 },
1636
+ mouth: { width: 0.1, lips: 0.018, grin: 0 },
1637
+ tusks: null,
1638
+ neck: { radius: 0.27 },
1639
+ bust: { shoulders: 0.95, style: "tunic" },
1640
+ hair: "swept",
1641
+ beard: "short",
1642
+ warts: 0,
1643
+ colors: {
1644
+ skin: hex2("#d9a07a"),
1645
+ shade: hex2("#a8644a"),
1646
+ lips: hex2("#b86b5c"),
1647
+ sclera: hex2("#ece6dc"),
1648
+ iris: hex2("#3d6e9e"),
1649
+ pupil: hex2("#141414"),
1650
+ teeth: hex2("#ece2c8"),
1651
+ mouth: hex2("#4a1a18"),
1652
+ hair: hex2("#5a3a22"),
1653
+ cloth: hex2("#2e4f8a"),
1654
+ cape: hex2("#7a2420"),
1655
+ leather: hex2("#6b4428"),
1656
+ metal: hex2("#c9a24a")
1657
+ }
1658
+ };
1659
+ var orc = {
1660
+ height: 0.3,
1661
+ cranium: { width: 0.7, depth: 0.8, height: 0.34 },
1662
+ brow: { ridge: 0.065, overhang: 0.075, scowl: 0.035 },
1663
+ cheeks: { size: 1.35, width: 1.15 },
1664
+ jaw: { muzzle: 0.1, width: 0.37, chin: 1.6, underbite: 0.07, drop: 0.1 },
1665
+ nose: { length: 0.15, width: 0.09, tip: 0.04, hook: -0.01, bridge: -0.045 },
1666
+ ears: { shape: "pointed", length: 0.3, width: 0.17, sweep: 25, lift: 8 },
1667
+ eyes: { radius: 0.05, spacing: 0.155, line: 0.56, lid: 16, lowerLid: 26 },
1668
+ mouth: { width: 0.17, lips: 0.03, grin: 0 },
1669
+ tusks: { length: 0.27, radius: 0.05, spread: 0.14, flare: 0.35 },
1670
+ neck: { radius: 0.32 },
1671
+ bust: { shoulders: 1.05, style: "pauldrons" },
1672
+ hair: "topknot",
1673
+ beard: "none",
1674
+ warts: 6,
1675
+ colors: {
1676
+ skin: hex2("#5f8b3a"),
1677
+ shade: hex2("#3c5c24"),
1678
+ lips: hex2("#4f6f30"),
1679
+ sclera: hex2("#8a5a1a"),
1680
+ iris: hex2("#ffb030"),
1681
+ pupil: hex2("#140a06"),
1682
+ teeth: hex2("#e2d6b0"),
1683
+ mouth: hex2("#2a0e0c"),
1684
+ hair: hex2("#1c1714"),
1685
+ cloth: hex2("#6b2a20"),
1686
+ leather: hex2("#5a3a22"),
1687
+ metal: hex2("#6e6a62"),
1688
+ fur: hex2("#5e4a34"),
1689
+ paint: hex2("#8a1c12")
1690
+ }
1691
+ };
1692
+ var goblin = {
1693
+ height: 0.19,
1694
+ cranium: { width: 0.66, depth: 0.84, height: 0.5 },
1695
+ brow: { ridge: 0.03, overhang: 0.015, scowl: 0.025 },
1696
+ cheeks: { size: 1.15, width: 1.1 },
1697
+ jaw: { muzzle: 0.06, width: 0.2, chin: 0.85, underbite: 0, drop: 0 },
1698
+ nose: { length: 0.2, width: 0.07, tip: 0.05, hook: 0.07, bridge: 0.12 },
1699
+ ears: { shape: "pointed", length: 0.62, width: 0.3, sweep: 22, lift: 18 },
1700
+ eyes: { radius: 0.072, spacing: 0.15, line: 0.5, lid: 16, lowerLid: 26 },
1701
+ mouth: { width: 0.2, lips: 0.016, grin: 0.75 },
1702
+ tusks: null,
1703
+ neck: { radius: 0.2 },
1704
+ bust: { shoulders: 0.75, style: "hood" },
1705
+ hair: "tufts",
1706
+ beard: "none",
1707
+ warts: 4,
1708
+ colors: {
1709
+ skin: hex2("#9cb84a"),
1710
+ shade: hex2("#6a8a2c"),
1711
+ lips: hex2("#7a8e36"),
1712
+ sclera: hex2("#f0e6a0"),
1713
+ iris: hex2("#e0a020"),
1714
+ pupil: hex2("#120c04"),
1715
+ teeth: hex2("#e8dcae"),
1716
+ mouth: hex2("#3a1010"),
1717
+ hair: hex2("#3a3024"),
1718
+ cloth: hex2("#6a4a7a"),
1719
+ leather: hex2("#5a3a22"),
1720
+ metal: hex2("#a88a4a")
1721
+ }
1722
+ };
1723
+ var dwarf = {
1724
+ ...human,
1725
+ height: 0.24,
1726
+ cranium: { width: 0.74, depth: 0.86, height: 0.48 },
1727
+ brow: { ridge: 0.05, overhang: 0.05, scowl: 0.02, hair: 0.024 },
1728
+ cheeks: { size: 1.25, width: 1.1 },
1729
+ jaw: { muzzle: 0.02, width: 0.27, chin: 1.2, underbite: 0, drop: 0.02 },
1730
+ nose: { length: 0.2, width: 0.08, tip: 0.06, hook: 0.02, bridge: 0.02 },
1731
+ eyes: { radius: 0.045, spacing: 0.14, line: 0.52, lid: 10, lowerLid: 24 },
1732
+ neck: { radius: 0.34 },
1733
+ bust: { shoulders: 1.1, style: "tunic" },
1734
+ hair: "braids",
1735
+ beard: "braided",
1736
+ colors: { ...human.colors, skin: hex2("#d49470"), hair: hex2("#8a3f1c"), cloth: hex2("#4a5a2a") }
1737
+ };
1738
+ var elf = {
1739
+ ...human,
1740
+ height: 0.235,
1741
+ cranium: { width: 0.62, depth: 0.86, height: 0.52 },
1742
+ brow: { ridge: 0.025, overhang: 0.02, scowl: -0.01, hair: 0.012 },
1743
+ cheeks: { size: 1.1, width: 1.05 },
1744
+ jaw: { muzzle: 0, width: 0.19, chin: 0.85, underbite: 0, drop: 0 },
1745
+ nose: { length: 0.18, width: 0.045, tip: 0.028, hook: 0, bridge: 0.01 },
1746
+ ears: { shape: "pointed", length: 0.42, width: 0.16, sweep: 40, lift: 24 },
1747
+ eyes: { radius: 0.05, spacing: 0.13, line: 0.5, lid: 14, lowerLid: 22 },
1748
+ bust: { shoulders: 0.85, style: "tunic" },
1749
+ hair: "swept",
1750
+ beard: "none",
1751
+ colors: {
1752
+ ...human.colors,
1753
+ skin: hex2("#e8c0a0"),
1754
+ hair: hex2("#e0d090"),
1755
+ iris: hex2("#3a8a5a"),
1756
+ cloth: hex2("#2a6a4a")
1757
+ }
1758
+ };
1759
+ var HEADS2 = { human, orc, goblin, dwarf, elf };
1760
+
1761
+ // src/kinds/head.ts
1762
+ function headParams2(o = {}) {
1763
+ const r = o.race ?? "human";
1764
+ const base = typeof r === "string" ? HEADS2[r] : r;
1765
+ if (!base)
1766
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
1767
+ cause: `unknown head race '${String(r)}'`,
1768
+ fix: `use one of ${Object.keys(HEADS2).join(", ")} or pass a HeadParams object`
1769
+ });
1770
+ const out = { ...base };
1771
+ for (const [k, v] of Object.entries(o.params ?? {})) {
1772
+ const was = out[k];
1773
+ out[k] = v && typeof v === "object" && !Array.isArray(v) && was && typeof was === "object" ? { ...was, ...v } : v;
1774
+ }
1775
+ return checkHead2(out);
1776
+ }
1777
+ function seatEyes(p, l) {
1778
+ const f = sculptField2(primaryEdits(l), { color: p.colors.skin }).field;
1779
+ const [, ey, ez] = l.eye;
1780
+ let x = l.faceX + 0.4;
1781
+ for (let i = 0;i < 400 && f([x, ey, ez]) > 0; i++)
1782
+ x -= 0.002;
1783
+ const r = l.eyeR;
1784
+ return { ...l, eye: [x - r * 0.7 - p.brow.overhang * 0.3, ey, ez] };
1785
+ }
1786
+ var HEAD_RES2 = [0.02, 0.045, 0.14];
1787
+ function headEdits2(p, rng, lod = 0) {
1788
+ const l = seatEyes(p, headLayout(p));
1789
+ const res = HEAD_RES2[lod];
1790
+ const warts = lod < 2 ? wartEdits(p, l, rng) : [];
1791
+ const hair = lod < 2 || p.hair !== "tufts" ? hairEdits(p, l) : [];
1792
+ return {
1793
+ layout: l,
1794
+ skull: [
1795
+ ...skullEdits(p, l),
1796
+ ...hair,
1797
+ ...browEdits(p, l),
1798
+ ...beardEdits(p, l),
1799
+ ...warts,
1800
+ ...paintEdits(p, l)
1801
+ ],
1802
+ ears: earEdits(p, l, res * 0.7),
1803
+ tusks: tuskEdits(p, l, res * 0.45),
1804
+ bust: bustEdits(p, l)
1805
+ };
1806
+ }
1807
+ function head2(ctx, o = {}) {
1808
+ const p = headParams2(o);
1809
+ const set = headEdits2(p, ctx.rng.fork("head"), ctx.lod);
1810
+ const l = set.layout;
1811
+ const H = p.height;
1812
+ const res = HEAD_RES2[ctx.lod];
1813
+ const { head: hm, jaw: jm } = headAndJaw(p, l, set, res);
1814
+ const bm = bustMesh(p, set, res * 1.6);
1815
+ const eye = eyeMesh(p, l.eyeR, l.eyeR / ctx.pick([7, 3.5, 2.2]));
1816
+ const [ox, oy, oz] = o.at ?? [0, 0, 0];
1817
+ const toWorld = (m, at = [0, 0, 0]) => place2(m, { scale: H, at: [ox + at[0] * H, oy + at[1] * H, oz + (at[2] - l.base) * H] });
1818
+ const ex = l.eye[0] + (ctx.lod > 0 ? res * 0.4 : 0);
1819
+ const [, ey, ez] = l.eye;
1820
+ const world = {
1821
+ bust: toWorld(bm),
1822
+ head: toWorld(hm),
1823
+ jaw: toWorld(jm),
1824
+ eye_l: toWorld(eye, [ex, ey, ez]),
1825
+ eye_r: toWorld(eye, [ex, -ey, ez])
1826
+ };
1827
+ const occ = (skip) => merge2(Object.values(world).filter((m) => m !== skip));
1828
+ const occHead = occ(world.jaw);
1829
+ const occJaw = occ(world.head);
1830
+ const ao = (m, by = occHead) => ctx.lod < 2 ? bakeAo2(m, { samples: ctx.pick([16, 10, 8]), radius: 0.12 * H / 0.23, strength: 0.55 }, by) : m;
1831
+ const pre = o.prefix ? `${o.prefix}_` : "";
1832
+ const pivot = (x, y, z) => [
1833
+ ox + x * H,
1834
+ oy + y * H,
1835
+ oz + (z - l.base) * H
1836
+ ];
1837
+ const local = (m, at, by) => place2(ao(m, by), { at: [-at[0], -at[1], -at[2]] });
1838
+ const hinge = pivot(l.hinge[0], 0, l.hinge[1]);
1839
+ const eyeL = pivot(ex, ey, ez);
1840
+ const eyeR = pivot(ex, -ey, ez);
1841
+ const names = {
1842
+ bust: `${pre}bust`,
1843
+ head: `${pre}head`,
1844
+ jaw: `${pre}jaw`,
1845
+ eyeL: `${pre}eye_l`,
1846
+ eyeR: `${pre}eye_r`
1847
+ };
1848
+ ctx.part(names.bust, ao(world.bust));
1849
+ ctx.part(names.head, ao(world.head));
1850
+ const sway = o.jawSway ?? 0;
1851
+ ctx.part(names.jaw, local(world.jaw, hinge, occJaw), {
1852
+ at: hinge,
1853
+ ...sway > 0 ? { motion: { kind: "sway", axis: [0, 1, 0], degrees: sway, hz: 0.5 }, clearOf: [names.head] } : {}
1854
+ });
1855
+ ctx.part(names.eyeL, local(world.eye_l, eyeL), { at: eyeL });
1856
+ ctx.part(names.eyeR, local(world.eye_r, eyeR), { at: eyeR });
1857
+ return { parts: names, height: (l.crown - l.base) * H, params: p };
1858
+ }
1859
+ // src/kinds/hull-lines.ts
1860
+ var bad2 = (cause, fix) => new Ts23dError2("T3D_RECIPE_INVALID", { cause, fix });
1861
+ function validateHull(o) {
1862
+ const dims = [o.length, o.beam, o.draft, o.freeboard];
1863
+ if (!dims.every((v) => v > 0 && Number.isFinite(v)))
1864
+ throw bad2(`hull length/beam/draft/freeboard = ${dims.join(" / ")}`, "give all four in metres, > 0 (a harbour tug: 30 / 9 / 4 / 1.2)");
1865
+ if (o.beam > o.length * 0.8)
1866
+ throw bad2(`hull beam ${o.beam} is over 0.8 × length ${o.length}`, "check the numbers: length/beam is 2.5–4 for tugs, 6–9 for ships");
1867
+ const n = o.fullness ?? 3;
1868
+ if (!(n >= 1.2 && n <= 8))
1869
+ throw bad2(`hull fullness is ${n}`, "use 1.5 (soft V) … 5 (barge)");
1870
+ const tw = o.transomWidth ?? 0.75;
1871
+ if (!(tw > 0 && tw <= 1))
1872
+ throw bad2(`hull transomWidth is ${tw}`, "use a share 0.3–1 of the half-beam");
1873
+ }
1874
+ var smooth = (t) => t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t);
1875
+ function hullLines2(o) {
1876
+ validateHull(o);
1877
+ const L = o.length;
1878
+ const half = L / 2;
1879
+ const [sb, ss] = o.sheer ?? [0.035 * L, 0.012 * L];
1880
+ const round = (o.stern ?? "round") === "round";
1881
+ const tw = o.transomWidth ?? 0.75;
1882
+ const pe = o.entrance ?? 2;
1883
+ const n = o.fullness ?? 3;
1884
+ const flare = o.flare ?? 0.06;
1885
+ const dead = Math.tan((o.deadriseDeg ?? 5) * Math.PI / 180);
1886
+ const xf = 0.1 * L;
1887
+ const xa = -0.15 * L;
1888
+ const kf = 0.22 * L;
1889
+ const ka = -0.25 * L;
1890
+ const deck = (x) => {
1891
+ const u = x / half;
1892
+ return o.freeboard + (u > 0 ? sb : ss) * u * u;
1893
+ };
1894
+ const plan = (x) => {
1895
+ if (x > xf)
1896
+ return 1 - ((x - xf) / (half - xf)) ** pe;
1897
+ if (x < xa) {
1898
+ const v = Math.min(1, (xa - x) / (xa + half));
1899
+ return round ? Math.sqrt(Math.max(0, 1 - v * v)) : 1 - (1 - tw) * v * v;
1900
+ }
1901
+ return 1;
1902
+ };
1903
+ const halfBreadth = (x) => o.beam / 2 * Math.max(0, plan(x));
1904
+ const counter = round ? 0.35 * o.freeboard : 0.15 * o.freeboard;
1905
+ const keel = (x) => {
1906
+ if (x > kf) {
1907
+ const u = Math.min(1, (x - kf) / (half - kf));
1908
+ return -o.draft + (deck(half) + o.draft) * u ** 2;
1909
+ }
1910
+ if (x < ka) {
1911
+ const v = Math.min(1, (ka - x) / (ka + half));
1912
+ return -o.draft + (counter + o.draft) * v ** 1.6;
1913
+ }
1914
+ return -o.draft;
1915
+ };
1916
+ const section = (x, theta) => {
1917
+ const f = plan(x);
1918
+ const ns = 1.2 + (n - 1.2) * smooth(f / 0.8);
1919
+ const s = Math.sin(theta) ** (2 / ns);
1920
+ const c = Math.cos(theta) ** (2 / ns);
1921
+ const zk = keel(x);
1922
+ const zd = deck(x);
1923
+ const zf = 1 - c;
1924
+ const y = halfBreadth(x) * (1 - flare + flare * zf * zf) * s;
1925
+ return [y, zk + (zd - zk) * zf + dead * y * c];
1926
+ };
1927
+ return { L, halfBreadth, deck, keel, section };
1928
+ }
1929
+ function thetaAt(lines, x, z) {
1930
+ let lo = 0;
1931
+ let hi = Math.PI / 2;
1932
+ if (lines.section(x, lo)[1] >= z)
1933
+ return lo;
1934
+ if (lines.section(x, hi)[1] <= z)
1935
+ return hi;
1936
+ for (let i = 0;i < 40; i++) {
1937
+ const mid = (lo + hi) / 2;
1938
+ if (lines.section(x, mid)[1] < z)
1939
+ lo = mid;
1940
+ else
1941
+ hi = mid;
1942
+ }
1943
+ return (lo + hi) / 2;
1944
+ }
1945
+
1946
+ // src/kinds/hull-skin.ts
1947
+ var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1948
+ var cross3 = (a, b) => [
1949
+ a[1] * b[2] - a[2] * b[1],
1950
+ a[2] * b[0] - a[0] * b[2],
1951
+ a[0] * b[1] - a[1] * b[0]
1952
+ ];
1953
+ function normalsOf(g) {
1954
+ const R = g.length;
1955
+ const out = g.map((row, i) => row.map((p, j) => {
1956
+ const r = (k) => g[Math.max(0, Math.min(R - 1, k))] ?? row;
1957
+ const ti = sub3(r(i + 1)[j] ?? p, r(i - 1)[j] ?? p);
1958
+ const tj = sub3(row[Math.min(row.length - 1, j + 1)] ?? p, row[Math.max(0, j - 1)] ?? p);
1959
+ const n = cross3(tj, ti);
1960
+ const l = Math.hypot(...n);
1961
+ return l < 0.000000001 ? [0, 0, 0] : [n[0] / l, n[1] / l, n[2] / l];
1962
+ }));
1963
+ for (let i = 0;i < R; i++) {
1964
+ const row = out[i] ?? [];
1965
+ row.forEach((n, j) => {
1966
+ if (n[0] || n[1] || n[2])
1967
+ return;
1968
+ for (let d = 1;d < R; d++) {
1969
+ const m = out[i - d]?.[j] ?? out[i + d]?.[j];
1970
+ if (m && (m[0] || m[1] || m[2])) {
1971
+ row[j] = m;
1972
+ return;
1973
+ }
1974
+ }
1975
+ });
1976
+ }
1977
+ return out;
1978
+ }
1979
+ function emitBand(b, g, nrm, j0, j1, color, mirror) {
1980
+ const rect = stripRect2("plain");
1981
+ for (const side of mirror ? [1, -1] : [1]) {
1982
+ const ids = g.map((row, i) => row.slice(j0, j1 + 1).map((p, k) => {
1983
+ const n = nrm[i]?.[j0 + k] ?? [0, 0, 1];
1984
+ return b.vertex({
1985
+ p: [p[0], p[1] * side, p[2]],
1986
+ n: [n[0], n[1] * side, n[2]],
1987
+ uv: [
1988
+ rect.u0 + p[0] / rect.metres,
1989
+ rect.v0 + k / Math.max(1, j1 - j0) * (rect.v1 - rect.v0)
1990
+ ],
1991
+ color
1992
+ });
1993
+ }));
1994
+ for (let i = 0;i + 1 < ids.length; i++) {
1995
+ for (let k = 0;k < j1 - j0; k++) {
1996
+ const a = ids[i]?.[k] ?? 0;
1997
+ const c = ids[i]?.[k + 1] ?? 0;
1998
+ const e = ids[i + 1]?.[k + 1] ?? 0;
1999
+ const d = ids[i + 1]?.[k] ?? 0;
2000
+ if (side > 0)
2001
+ b.quad(a, c, e, d);
2002
+ else
2003
+ b.quad(a, d, e, c);
2004
+ }
2005
+ }
2006
+ }
2007
+ }
2008
+
2009
+ // src/kinds/hull.ts
2010
+ var deckBand = (o, lod) => lod === 2 ? o.beam : stripScale("wood");
2011
+ function deckMesh(o, ln, xs, inset, lod) {
2012
+ const b = new MeshBuilder2;
2013
+ const rect = stripRect2("wood");
2014
+ const w = deckBand(o, lod);
2015
+ const camber = o.camber ?? o.beam / 50;
2016
+ const yd = (x) => Math.max(0, ln.halfBreadth(x) - inset);
2017
+ for (let y0 = -Math.ceil(o.beam / 2 / w) * w;y0 < o.beam / 2; y0 += w) {
2018
+ const at = (x, y) => {
2019
+ const h = yd(x);
2020
+ const c = Math.max(-h, Math.min(h, y));
2021
+ return [x, c, ln.deck(x) + (h > 0 ? camber * (1 - (c / h) ** 2) : 0)];
2022
+ };
2023
+ for (let i = 0;i + 1 < xs.length; i++) {
2024
+ const x0 = xs[i] ?? 0;
2025
+ const x1 = xs[i + 1] ?? 0;
2026
+ const q = [at(x0, y0), at(x0, y0 + w), at(x1, y0 + w), at(x1, y0)];
2027
+ if (Math.abs((q[1]?.[1] ?? 0) - (q[0]?.[1] ?? 0)) + Math.abs((q[2]?.[1] ?? 0) - (q[3]?.[1] ?? 0)) < 0.000001)
2028
+ continue;
2029
+ const ids = q.map((p) => b.vertex({
2030
+ p,
2031
+ n: [0, 0, 1],
2032
+ uv: [rect.u0 + p[0] / rect.metres, rect.v0 + (p[1] - y0) / w * (rect.v1 - rect.v0)],
2033
+ color: o.colors.deck
2034
+ }));
2035
+ b.quad(ids[0] ?? 0, ids[3] ?? 0, ids[2] ?? 0, ids[1] ?? 0);
2036
+ }
2037
+ }
2038
+ return b.build();
2039
+ }
2040
+ function hullMesh2(o, lod = 0) {
2041
+ const ln = hullLines2(o);
2042
+ const half = o.length / 2;
2043
+ const stations = [36, 18, 8][lod] ?? 8;
2044
+ const na = [6, 3, 2][lod] ?? 2;
2045
+ const nc = [4, 2, 1][lod] ?? 1;
2046
+ const xs = Array.from({ length: stations + 1 }, (_, i) => -half + o.length * (1 - Math.cos(Math.PI * i / stations)) / 2);
2047
+ const boot = o.bootTop ?? 0.3;
2048
+ const hb = o.bulwark ?? 0;
2049
+ const tb = 0.12;
2050
+ const grid = xs.map((x) => {
2051
+ const tw = thetaAt(ln, x, 0);
2052
+ const tbt = thetaAt(ln, x, boot);
2053
+ const row = [];
2054
+ const seg = (t0, t1, k, first) => {
2055
+ for (let i = first ? 0 : 1;i <= k; i++) {
2056
+ const [y, z] = ln.section(x, t0 + (t1 - t0) * i / k);
2057
+ row.push([x, y, z]);
2058
+ }
2059
+ };
2060
+ seg(0, tw, na, true);
2061
+ seg(tw, tbt, 1, false);
2062
+ seg(tbt, Math.PI / 2, nc, false);
2063
+ return row;
2064
+ });
2065
+ const nrm = normalsOf(grid);
2066
+ for (const row of nrm) {
2067
+ const n = row[0];
2068
+ if (n) {
2069
+ const l = Math.hypot(n[0], n[2]) || 1;
2070
+ row[0] = [n[0] / l, 0, n[2] / l];
2071
+ }
2072
+ }
2073
+ const b = new MeshBuilder2;
2074
+ const c = o.colors;
2075
+ emitBand(b, grid, nrm, 0, na, c.bottom, true);
2076
+ emitBand(b, grid, nrm, na, na + 1, c.boot ?? [0.06, 0.06, 0.06], true);
2077
+ emitBand(b, grid, nrm, na + 1, na + 1 + nc, c.topsides, true);
2078
+ const parts = [b.build()];
2079
+ if (hb > 0)
2080
+ parts.push(bulwarks(o, ln, xs, hb, tb));
2081
+ if (o.fender && lod < 2)
2082
+ parts.push(...fenders(o.fender, ln, xs, lod));
2083
+ if ((o.stern ?? "round") === "transom") {
2084
+ const x = xs[0] ?? -half;
2085
+ const star = (grid[0] ?? []).map((p) => p);
2086
+ if (hb > 0)
2087
+ star.push([x, ln.halfBreadth(x), ln.deck(x) + hb]);
2088
+ const port = [...star].reverse().map((p) => [p[0], -p[1], p[2]]);
2089
+ const yd = ln.halfBreadth(x);
2090
+ const arc = [];
2091
+ const w = deckBand(o, lod);
2092
+ for (let y = Math.ceil(o.beam / 2 / w) * w;y > -o.beam / 2; y -= w) {
2093
+ if (hb === 0 && Math.abs(y) < yd - 0.000000001)
2094
+ arc.push([x, y, ln.deck(x) + (o.camber ?? o.beam / 50) * (1 - (y / yd) ** 2)]);
2095
+ }
2096
+ parts.push(transom(dedupe([...port.slice(0, -1), ...star, ...arc]), c.topsides));
2097
+ }
2098
+ const at = o.at ?? [0, 0, 0];
2099
+ return {
2100
+ shell: translate2(merge2(parts), at),
2101
+ deck: translate2(deckMesh(o, ln, xs, hb > 0 ? tb : 0, lod), at),
2102
+ lines: ln
2103
+ };
2104
+ }
2105
+ function bulwarks(o, ln, xs, hb, tb) {
2106
+ const g = xs.map((x) => {
2107
+ const y = ln.halfBreadth(x);
2108
+ const z = ln.deck(x);
2109
+ const yi = Math.max(0, y - tb);
2110
+ return [
2111
+ [x, y, z],
2112
+ [x, y, z + hb],
2113
+ [x, yi, z + hb],
2114
+ [x, yi, z]
2115
+ ];
2116
+ });
2117
+ const b = new MeshBuilder2;
2118
+ const top = o.colors.bulwark ?? o.colors.topsides;
2119
+ for (const [j0, color] of [
2120
+ [0, o.colors.topsides],
2121
+ [1, top],
2122
+ [2, top]
2123
+ ]) {
2124
+ const sliced = g.map((row) => row.slice(j0, j0 + 2));
2125
+ emitBand(b, sliced, normalsOf(sliced), 0, 1, color, true);
2126
+ }
2127
+ return b.build();
2128
+ }
2129
+ function fenders(f, ln, xs, lod) {
2130
+ const r = f.radius ?? 0.15;
2131
+ return [1, -1].map((side) => sweep2({
2132
+ section: { radius: r, segments: lod === 0 ? 8 : 5 },
2133
+ path: xs.map((x) => [x, side * (ln.halfBreadth(x) + r * 0.4), ln.deck(x) - r * 1.2]),
2134
+ color: f.color,
2135
+ caps: true
2136
+ }));
2137
+ }
2138
+ function transom(points, color) {
2139
+ const b = new MeshBuilder2;
2140
+ const rect = stripRect2("plain");
2141
+ const ids = points.map((p) => b.vertex({ p, n: [-1, 0, 0], uv: [rect.u0 + p[1] / rect.metres, rect.v0], color }));
2142
+ for (const [i, j, k] of triangulate2(points.map((p) => [p[1], p[2]])))
2143
+ b.tri(ids[i] ?? 0, ids[k] ?? 0, ids[j] ?? 0);
2144
+ return b.build();
2145
+ }
2146
+ function hull2(ctx, o) {
2147
+ const name = o.name ?? "hull";
2148
+ const m = hullMesh2(o, ctx.lod);
2149
+ ctx.part(name, m.shell);
2150
+ ctx.part(`${name}_deck`, m.deck);
2151
+ return m.lines;
2152
+ }
2153
+ // src/kinds/planned.ts
2154
+ function creature2(_ctx, _o) {
2155
+ notImplemented2("creature (SDF-skinned skeleton)", "docs/idea/07-what-fits.md");
2156
+ }
2157
+ // src/kinds/roof-chimney.ts
2158
+ function chimney(c, heightAt, at, lod) {
2159
+ const [w, d] = c.size ?? [0.9, 0.6];
2160
+ const cx = at[0] + c.at[0];
2161
+ const cy = at[1] + c.at[1];
2162
+ const corners = [
2163
+ [cx - w / 2, cy - d / 2],
2164
+ [cx + w / 2, cy - d / 2],
2165
+ [cx + w / 2, cy + d / 2],
2166
+ [cx - w / 2, cy + d / 2]
2167
+ ];
2168
+ const exit = Math.max(...corners.map(([x, y]) => heightAt(x, y)));
2169
+ const top = exit + (c.height ?? 0.9);
2170
+ const z0 = at[2];
2171
+ const strip = c.strip ?? "brick";
2172
+ const b = new MeshBuilder2;
2173
+ corners.forEach(([x0, y0], i) => {
2174
+ const [x1, y1] = corners[(i + 1) % 4] ?? [x0, y0];
2175
+ emitFace(b, {
2176
+ points: [
2177
+ [x0, y0, z0],
2178
+ [x1, y1, z0],
2179
+ [x1, y1, top],
2180
+ [x0, y0, top]
2181
+ ],
2182
+ normal: [y1 - y0, -(x1 - x0), 0],
2183
+ up: [0, 0, 1],
2184
+ vOrigin: [0, 0, z0],
2185
+ color: c.color,
2186
+ strip,
2187
+ banded: lod < 2
2188
+ });
2189
+ });
2190
+ emitFace(b, {
2191
+ points: corners.map(([x, y]) => [x, y, top]),
2192
+ normal: [0, 0, 1],
2193
+ up: [0, 1, 0],
2194
+ vOrigin: [cx, cy, top],
2195
+ color: c.color,
2196
+ strip: "plain",
2197
+ banded: false
2198
+ });
2199
+ const parts = [b.build()];
2200
+ if (lod < 2) {
2201
+ const k = 0.06;
2202
+ const cap = [c.color[0] * 0.85, c.color[1] * 0.85, c.color[2] * 0.85];
2203
+ parts.push(box2({ size: [w + 2 * k, d + 2 * k, 0.1], at: [cx, cy, top - 0.22], color: cap }));
2204
+ parts.push(box2({ size: [w + k, d + k, 0.07], at: [cx, cy, top - 0.07], color: cap }));
2205
+ }
2206
+ if (lod === 0) {
2207
+ const n = c.pots ?? Math.max(1, Math.round(w / 0.4));
2208
+ const pot = c.potColor ?? [0.62, 0.32, 0.2];
2209
+ for (let i = 0;i < n; i++) {
2210
+ const x = cx - w / 2 + w * (i + 0.5) / n;
2211
+ parts.push(lathe2({
2212
+ profile: [
2213
+ [0.13, 0],
2214
+ [0.1, 0.36],
2215
+ [0.125, 0.4],
2216
+ [0.125, 0.46]
2217
+ ],
2218
+ segments: 8,
2219
+ caps: true,
2220
+ at: [x, cy, top - 0.02],
2221
+ color: pot
2222
+ }));
2223
+ }
2224
+ }
2225
+ return merge2(parts);
2226
+ }
2227
+
2228
+ // src/kinds/roof-shape.ts
2229
+ var RAD5 = Math.PI / 180;
2230
+ var FRAMES = {
2231
+ front: [
2232
+ [0, 1],
2233
+ [-1, 0]
2234
+ ],
2235
+ back: [
2236
+ [0, -1],
2237
+ [1, 0]
2238
+ ],
2239
+ left: [
2240
+ [-1, 0],
2241
+ [0, -1]
2242
+ ],
2243
+ right: [
2244
+ [1, 0],
2245
+ [0, 1]
2246
+ ]
2247
+ };
2248
+ var bad3 = (cause, fix) => new Ts23dError2("T3D_RECIPE_INVALID", { cause, fix });
2249
+ function validateRoof(o) {
2250
+ const [w, d] = o.footprint;
2251
+ if (!(w > 0 && d > 0 && Number.isFinite(w) && Number.isFinite(d)))
2252
+ throw bad3(`roof footprint is [${w}, ${d}]`, "pass the wall-line [width X, depth Y] in metres, both > 0");
2253
+ const maxPitch = o.type === "mansard" || o.type === "gambrel" ? 85 : 75;
2254
+ if (!(o.pitchDeg >= 3 && o.pitchDeg <= maxPitch))
2255
+ throw bad3(`${o.type} roof pitchDeg is ${o.pitchDeg}`, `use 3–${maxPitch}° (slate ≥ 20°, tile 30–45°, sheet metal ≥ 3°); for a flat roof use a box`);
2256
+ const t = o.thickness ?? 0.18;
2257
+ if (!(t > 0 && t < 1))
2258
+ throw bad3(`roof thickness is ${t}`, "use 0.1–0.3 m (rafter + battens + slate)");
2259
+ const ov = o.overhang ?? 0.35;
2260
+ if (!(ov >= 0 && ov < 3) || !((o.rake ?? ov) >= 0))
2261
+ throw bad3(`roof overhang is ${ov}`, "use 0–1.5 m past the wall line (0 for a party wall)");
2262
+ if (o.type === "mansard" || o.type === "gambrel") {
2263
+ const up = o.upperPitchDeg ?? (o.type === "mansard" ? 20 : 30);
2264
+ if (!(up > 0 && up < o.pitchDeg))
2265
+ throw bad3(`${o.type} upperPitchDeg ${up} is not below pitchDeg ${o.pitchDeg}`, "the upper slope must be shallower than the lower one (mansard 70°/20°, gambrel 60°/30°)");
2266
+ const k = o.knee ?? 0.3;
2267
+ if (!(k > 0 && k < 1))
2268
+ throw bad3(`${o.type} knee is ${k}`, "use a fraction 0.15–0.6 of the half-span");
2269
+ }
2270
+ }
2271
+ function underPlanes(o, L, D) {
2272
+ const tp = Math.tan(o.pitchDeg * RAD5);
2273
+ const side = (s, c = 0, k = s) => [
2274
+ { c: c + D * k, ga: 0, gb: -s },
2275
+ { c: c + D * k, ga: 0, gb: s }
2276
+ ];
2277
+ const ends = (s, c = 0, k = s) => [
2278
+ { c: c + L * k, ga: -s, gb: 0 },
2279
+ { c: c + L * k, ga: s, gb: 0 }
2280
+ ];
2281
+ switch (o.type) {
2282
+ case "gable":
2283
+ return side(tp);
2284
+ case "shed":
2285
+ return [{ c: D * tp, ga: 0, gb: tp }];
2286
+ case "hip":
2287
+ return [...side(tp), ...ends(tp)];
2288
+ case "pyramid": {
2289
+ const h = Math.min(L, D) * tp;
2290
+ return [...side(h / D), ...ends(h / L)];
2291
+ }
2292
+ case "gambrel":
2293
+ case "mansard": {
2294
+ const t2 = Math.tan((o.upperPitchDeg ?? (o.type === "mansard" ? 20 : 30)) * RAD5);
2295
+ const r = (o.knee ?? (o.type === "mansard" ? 0.3 : 0.45)) * Math.min(L, D);
2296
+ const upper = (half) => r * tp + (half - r) * t2 - half * t2;
2297
+ const lower = [...side(tp), ...o.type === "mansard" ? ends(tp) : []];
2298
+ const top = [
2299
+ ...side(t2, upper(D), t2),
2300
+ ...o.type === "mansard" ? ends(t2, upper(L), t2) : []
2301
+ ];
2302
+ return [...lower, ...top];
2303
+ }
2304
+ }
2305
+ }
2306
+ function roofShape(o) {
2307
+ validateRoof(o);
2308
+ const [w, d] = o.footprint;
2309
+ const ridge = o.ridge ?? (w >= d ? "x" : "y");
2310
+ const low = o.type === "shed" ? o.low ?? "front" : ridge === "x" ? "right" : "front";
2311
+ const [ex, ey] = FRAMES[low];
2312
+ const L = (Math.abs(ex[0]) > 0.5 ? w : d) / 2;
2313
+ const D = (Math.abs(ex[0]) > 0.5 ? d : w) / 2;
2314
+ const ov = o.overhang ?? 0.35;
2315
+ const t = o.thickness ?? 0.18;
2316
+ const under = underPlanes(o, L, D);
2317
+ const top = under.map((p) => ({ ...p, c: p.c + t * Math.hypot(1, p.ga, p.gb) }));
2318
+ const hipped = o.type === "hip" || o.type === "pyramid" || o.type === "mansard";
2319
+ const A = hipped ? 4 * (L + D + ov) : L + (o.rake ?? ov);
2320
+ const B = D + ov;
2321
+ const g0 = under[0] ?? { c: 0, ga: 0, gb: 1 };
2322
+ const zEave = -ov * Math.hypot(g0.ga, g0.gb);
2323
+ let outline = [
2324
+ [-A, -B],
2325
+ [A, -B],
2326
+ [A, B],
2327
+ [-A, B]
2328
+ ];
2329
+ for (const p of under)
2330
+ outline = clipPoly(outline, (q) => zEave - zAt(p, q));
2331
+ return { L, D, under, top, outline, ex, ey, thickness: t };
2332
+ }
2333
+ var zAt = (p, q) => p.c + p.ga * q[0] + p.gb * q[1];
2334
+ var envelope = (planes, q) => Math.min(...planes.map((p) => zAt(p, q)));
2335
+ function facets(planes, outline) {
2336
+ const out = [];
2337
+ planes.forEach((p, i) => {
2338
+ let cell = [...outline];
2339
+ planes.forEach((q, j) => {
2340
+ if (j !== i && cell.length >= 3)
2341
+ cell = clipPoly(cell, (r) => zAt(p, r) - zAt(q, r));
2342
+ });
2343
+ if (cell.length >= 3 && Math.abs(area2(cell)) > 0.000001)
2344
+ out.push([p, cell]);
2345
+ });
2346
+ return out;
2347
+ }
2348
+ var toWorld = (s, q, z, at) => [
2349
+ at[0] + s.ex[0] * q[0] + s.ey[0] * q[1],
2350
+ at[1] + s.ex[1] * q[0] + s.ey[1] * q[1],
2351
+ at[2] + z
2352
+ ];
2353
+ var toLocal = (s, x, y, at) => {
2354
+ const dx = x - at[0];
2355
+ const dy = y - at[1];
2356
+ return [dx * s.ex[0] + dy * s.ex[1], dx * s.ey[0] + dy * s.ey[1]];
2357
+ };
2358
+ function roofHeightAt2(o, x, y) {
2359
+ const s = roofShape(o);
2360
+ return o.at[2] + envelope(s.top, toLocal(s, x, y, o.at));
2361
+ }
2362
+ function ridgeHeight2(o) {
2363
+ const s = roofShape(o);
2364
+ let z = -Infinity;
2365
+ for (const [p, cell] of facets(s.top, s.outline))
2366
+ for (const q of cell)
2367
+ z = Math.max(z, zAt(p, q));
2368
+ return o.at[2] + z;
2369
+ }
2370
+
2371
+ // src/kinds/roof.ts
2372
+ var W = (s, q, z, at) => toWorld(s, q, z, at);
2373
+ var dirW = (s, a, b, z) => [
2374
+ s.ex[0] * a + s.ey[0] * b,
2375
+ s.ex[1] * a + s.ey[1] * b,
2376
+ z
2377
+ ];
2378
+ var onOutline = (outline, q) => outline.some((a, i) => {
2379
+ const b = outline[(i + 1) % outline.length] ?? a;
2380
+ const ex = b[0] - a[0];
2381
+ const ey = b[1] - a[1];
2382
+ const l2 = ex * ex + ey * ey || 1;
2383
+ const t = Math.max(0, Math.min(1, ((q[0] - a[0]) * ex + (q[1] - a[1]) * ey) / l2));
2384
+ return Math.hypot(a[0] + ex * t - q[0], a[1] + ey * t - q[1]) < 0.000001;
2385
+ });
2386
+ function slopes(b, s, planes, o, face) {
2387
+ const cells = facets(planes, s.outline);
2388
+ for (const [p, cell] of cells) {
2389
+ const pts = cell.map((q) => W(s, q, zAt(p, q), o.at));
2390
+ const low = pts.reduce((m, q) => q[2] < m[2] ? q : m, pts[0] ?? o.at);
2391
+ const k = face.down ? -1 : 1;
2392
+ emitFace(b, {
2393
+ points: pts,
2394
+ normal: dirW(s, -p.ga * k, -p.gb * k, k),
2395
+ up: [0, 0, 1],
2396
+ vOrigin: low,
2397
+ color: face.color,
2398
+ strip: face.strip,
2399
+ banded: face.banded
2400
+ });
2401
+ }
2402
+ return cells;
2403
+ }
2404
+ function fascia(b, s, o, trim, verts) {
2405
+ const n = s.outline.length;
2406
+ for (let i = 0;i < n; i++) {
2407
+ const a = s.outline[i] ?? [0, 0];
2408
+ const c = s.outline[(i + 1) % n] ?? a;
2409
+ const ex = c[0] - a[0];
2410
+ const ey = c[1] - a[1];
2411
+ const len2 = ex * ex + ey * ey;
2412
+ if (len2 < 0.000000000001)
2413
+ continue;
2414
+ const ts = [0, 1];
2415
+ for (const q of verts) {
2416
+ const t = ((q[0] - a[0]) * ex + (q[1] - a[1]) * ey) / len2;
2417
+ const off = Math.abs((q[0] - a[0]) * ey - (q[1] - a[1]) * ex) / Math.sqrt(len2);
2418
+ if (t > 0.000001 && t < 1 - 0.000001 && off < 0.000001)
2419
+ ts.push(t);
2420
+ }
2421
+ ts.sort((x, y) => x - y);
2422
+ const out = dirW(s, ey, -ex, 0);
2423
+ for (let k = 0;k + 1 < ts.length; k++) {
2424
+ const q0 = [a[0] + ex * (ts[k] ?? 0), a[1] + ey * (ts[k] ?? 0)];
2425
+ const q1 = [a[0] + ex * (ts[k + 1] ?? 1), a[1] + ey * (ts[k + 1] ?? 1)];
2426
+ if (Math.hypot(q1[0] - q0[0], q1[1] - q0[1]) < 0.000001)
2427
+ continue;
2428
+ emitFace(b, {
2429
+ points: [
2430
+ W(s, q0, envelope(s.under, q0), o.at),
2431
+ W(s, q1, envelope(s.under, q1), o.at),
2432
+ W(s, q1, envelope(s.top, q1), o.at),
2433
+ W(s, q0, envelope(s.top, q0), o.at)
2434
+ ],
2435
+ normal: out,
2436
+ up: [0, 0, 1],
2437
+ vOrigin: o.at,
2438
+ color: trim,
2439
+ strip: "plain"
2440
+ });
2441
+ }
2442
+ }
2443
+ }
2444
+ function gableWalls(b, s, o, banded) {
2445
+ const wall = o.wall ?? { color: [0.8, 0.76, 0.68] };
2446
+ const lines = [
2447
+ [[s.L, -s.D], [s.L, s.D], dirW(s, 1, 0, 0)],
2448
+ [[-s.L, s.D], [-s.L, -s.D], dirW(s, -1, 0, 0)],
2449
+ [[s.L, s.D], [-s.L, s.D], dirW(s, 0, 1, 0)],
2450
+ [[-s.L, -s.D], [s.L, -s.D], dirW(s, 0, -1, 0)]
2451
+ ];
2452
+ for (const [q0, q1, out] of lines) {
2453
+ const at = (t) => [q0[0] + (q1[0] - q0[0]) * t, q0[1] + (q1[1] - q0[1]) * t];
2454
+ const ts = new Set([0, 1]);
2455
+ s.under.forEach((p, i) => {
2456
+ s.under.forEach((q, j) => {
2457
+ if (j <= i)
2458
+ return;
2459
+ const d0 = zAt(p, q0) - zAt(q, q0);
2460
+ const d1 = zAt(p, q1) - zAt(q, q1);
2461
+ if (Math.abs(d0 - d1) < 0.000000000001)
2462
+ return;
2463
+ const t = d0 / (d0 - d1);
2464
+ if (t > 0.000001 && t < 1 - 0.000001 && Math.abs(envelope(s.under, at(t)) - zAt(p, at(t))) < 0.0000001)
2465
+ ts.add(t);
2466
+ });
2467
+ });
2468
+ const prof = [...ts].sort((x, y) => x - y).map((t) => [at(t), envelope(s.under, at(t))]);
2469
+ if (Math.max(...prof.map(([, z]) => z)) < 0.01)
2470
+ continue;
2471
+ const pts = [W(s, q0, 0, o.at)];
2472
+ for (const [q, z] of prof)
2473
+ pts.push(W(s, q, Math.max(0, z), o.at));
2474
+ pts.push(W(s, q1, 0, o.at));
2475
+ emitFace(b, {
2476
+ points: pts,
2477
+ normal: out,
2478
+ up: [0, 0, 1],
2479
+ vOrigin: o.at,
2480
+ color: wall.color,
2481
+ strip: wall.strip ?? "plain",
2482
+ banded
2483
+ });
2484
+ }
2485
+ }
2486
+ function caps(s, o, cells, trim, lod) {
2487
+ const seen = new Set;
2488
+ const out = [];
2489
+ const r = Math.max(0.06, s.thickness * 0.5);
2490
+ for (const [p, cell] of cells) {
2491
+ cell.forEach((q0, i) => {
2492
+ const q1 = cell[(i + 1) % cell.length] ?? q0;
2493
+ const mid = [(q0[0] + q1[0]) / 2, (q0[1] + q1[1]) / 2];
2494
+ if (onOutline(s.outline, mid) || Math.hypot(q1[0] - q0[0], q1[1] - q0[1]) < 0.001)
2495
+ return;
2496
+ const key = [q0, q1].map((q) => `${q[0].toFixed(4)},${q[1].toFixed(4)}`).sort().join("|");
2497
+ if (seen.has(key))
2498
+ return;
2499
+ seen.add(key);
2500
+ const lift = r * 0.3;
2501
+ out.push(beam2({
2502
+ from: W(s, q0, zAt(p, q0) + lift, o.at),
2503
+ to: W(s, q1, zAt(p, q1) + lift, o.at),
2504
+ width: r * 2,
2505
+ segments: lod === 0 ? 8 : 5,
2506
+ color: trim,
2507
+ strip: "plain"
2508
+ }));
2509
+ });
2510
+ }
2511
+ return out;
2512
+ }
2513
+ function roofMesh2(o, lod = 0) {
2514
+ const s = roofShape(o);
2515
+ const trim = o.trim ?? [o.color[0] * 0.55, o.color[1] * 0.55, o.color[2] * 0.55];
2516
+ const strip = o.strip ?? "slate";
2517
+ const b = new MeshBuilder2;
2518
+ const extra = [];
2519
+ if (lod === 2) {
2520
+ slopes(b, s, s.under, o, { down: false, color: o.color, strip, banded: false });
2521
+ } else {
2522
+ const top = slopes(b, s, s.top, o, { down: false, color: o.color, strip, banded: true });
2523
+ const under = slopes(b, s, s.under, o, {
2524
+ down: true,
2525
+ color: trim,
2526
+ strip: "wood",
2527
+ banded: lod === 0
2528
+ });
2529
+ const verts = [...top, ...under].flatMap(([, cell]) => cell);
2530
+ fascia(b, s, o, trim, verts);
2531
+ if (o.caps !== false)
2532
+ extra.push(...caps(s, o, top, trim, lod));
2533
+ }
2534
+ gableWalls(b, s, o, lod < 2);
2535
+ for (const c of o.chimneys ?? []) {
2536
+ extra.push(chimney(c, (x, y) => roofHeightAt2(o, x, y), o.at, lod));
2537
+ }
2538
+ return merge2([b.build(), ...extra]);
2539
+ }
2540
+ function roof2(ctx, o) {
2541
+ ctx.part(o.name ?? "roof", roofMesh2(o, ctx.lod));
2542
+ return roofInfo2(o);
2543
+ }
2544
+ function roofInfo2(o) {
2545
+ const s = roofShape(o);
2546
+ const eave = Math.min(...s.outline.map((q) => envelope(s.under, q)));
2547
+ return {
2548
+ ridge: ridgeHeight2(o),
2549
+ eave: o.at[2] + eave,
2550
+ heightAt: (x, y) => roofHeightAt2(o, x, y)
2551
+ };
2552
+ }
2553
+ // src/kinds/vegetation-fronds.ts
2554
+ function tri(mb, a, b, c, col, tipCol) {
2555
+ const e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
2556
+ const e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
2557
+ const n = [
2558
+ e1[1] * e2[2] - e1[2] * e2[1],
2559
+ e1[2] * e2[0] - e1[0] * e2[2],
2560
+ e1[0] * e2[1] - e1[1] * e2[0]
2561
+ ];
2562
+ const l = Math.hypot(n[0], n[1], n[2]) || 1;
2563
+ for (const s of [1, -1]) {
2564
+ const nn = [n[0] / l * s, n[1] / l * s, n[2] / l * s];
2565
+ const i = mb.vertex({ p: a, n: nn, uv: [0, 0], color: col });
2566
+ const j = mb.vertex({ p: b, n: nn, uv: [0, 0], color: col });
2567
+ const k = mb.vertex({ p: c, n: nn, uv: [0, 0], color: tipCol });
2568
+ if (s > 0)
2569
+ mb.tri(i, j, k);
2570
+ else
2571
+ mb.tri(i, k, j);
2572
+ }
2573
+ }
2574
+ function frondLeaflets(stems, level, leaf, perSide, shade, rng) {
2575
+ const mb = new MeshBuilder2;
2576
+ for (const st of stems) {
2577
+ if (st.level !== level)
2578
+ continue;
2579
+ const arc = arcLengths2(st.pts);
2580
+ const frames = transportFrames2(st.pts);
2581
+ const k = 1 + rng.range(-leaf.colorVar, leaf.colorVar);
2582
+ const step = st.length * 0.9 / perSide;
2583
+ for (let i = 0;i < perSide; i++) {
2584
+ const u = 0.1 + 0.9 * (i + 0.5) / perSide;
2585
+ const s = u * st.length;
2586
+ let j = 0;
2587
+ while (j < arc.length - 2 && (arc[j + 1] ?? 0) < s)
2588
+ j++;
2589
+ const a = st.pts[j] ?? [0, 0, 0];
2590
+ const b = st.pts[j + 1] ?? a;
2591
+ const f = (s - (arc[j] ?? 0)) / ((arc[j + 1] ?? 1) - (arc[j] ?? 0) || 1);
2592
+ const fr = frames[j] ?? { t: [1, 0, 0], n: [0, 0, 1], b: [0, 1, 0] };
2593
+ const r = (st.radii[j] ?? 0) * (1 - f) + (st.radii[j + 1] ?? 0) * f;
2594
+ const c = [
2595
+ a[0] + (b[0] - a[0]) * f,
2596
+ a[1] + (b[1] - a[1]) * f,
2597
+ a[2] + (b[2] - a[2]) * f
2598
+ ];
2599
+ const len = st.length * leaf.width * Math.sin(Math.PI * (0.12 + 0.88 * u)) ** 0.6;
2600
+ const ao = 0.6 + 0.4 * Math.min(1, Math.hypot(c[0] - shade.centre[0], c[1] - shade.centre[1]) / shade.radius);
2601
+ const col = [
2602
+ leaf.color[0] * k * ao * 0.75,
2603
+ leaf.color[1] * k * ao * 0.75,
2604
+ leaf.color[2] * k * ao * 0.75
2605
+ ];
2606
+ const tipCol = [col[0] * 1.45, col[1] * 1.35, col[2] * 1.2];
2607
+ for (const side of [1, -1]) {
2608
+ const o = [fr.b[0] * side, fr.b[1] * side, fr.b[2] * side];
2609
+ const root = (d) => [
2610
+ c[0] + o[0] * r + fr.t[0] * d,
2611
+ c[1] + o[1] * r + fr.t[1] * d,
2612
+ c[2] + o[2] * r + fr.t[2] * d
2613
+ ];
2614
+ const tip = [
2615
+ c[0] + (o[0] * 0.8 + fr.t[0] * 0.45 - fr.n[0] * 0.35) * len,
2616
+ c[1] + (o[1] * 0.8 + fr.t[1] * 0.45 - fr.n[1] * 0.35) * len,
2617
+ c[2] + (o[2] * 0.8 + fr.t[2] * 0.45 - fr.n[2] * 0.35) * len - 0.2 * len
2618
+ ];
2619
+ tri(mb, root(-step * 0.35), root(step * 0.35), tip, col, tipCol);
2620
+ }
2621
+ }
2622
+ }
2623
+ return mb.build();
2624
+ }
2625
+ function fruit(trunk, at, o, rng) {
2626
+ const j = Math.max(0, Math.min(trunk.pts.length - 1, Math.round(at * (trunk.pts.length - 1))));
2627
+ const c = trunk.pts[j] ?? [0, 0, 0];
2628
+ const rt = trunk.radii[j] ?? 0.2;
2629
+ const profile = [0, 1, 2, 3, 4].map((i) => {
2630
+ const a = -Math.PI / 2 + i / 4 * Math.PI;
2631
+ return [Math.max(0.0001, Math.cos(a)) * o.radius, Math.sin(a) * o.radius];
2632
+ });
2633
+ const out = [];
2634
+ for (let i = 0;i < o.count; i++) {
2635
+ const a = i / o.count * Math.PI * 2 + rng.range(-0.3, 0.3);
2636
+ const d = rt + o.radius * 0.6;
2637
+ const dz = rng.range(-1, 0.5) * o.radius;
2638
+ out.push(lathe2({
2639
+ profile,
2640
+ segments: 6,
2641
+ caps: false,
2642
+ at: [c[0] + Math.cos(a) * d, c[1] + Math.sin(a) * d, c[2] + dz],
2643
+ color: o.color
2644
+ }));
2645
+ }
2646
+ return merge2(out);
2647
+ }
2648
+
2649
+ // src/kinds/vegetation-impostor.ts
2650
+ function coarseTrunk(trunk, p, apex) {
2651
+ const n = trunk.pts.length - 1;
2652
+ const idx = [...new Set([0, Math.round(n / 3), Math.round(2 * n / 3), n])];
2653
+ const pts = idx.map((i) => trunk.pts[i] ?? [0, 0, 0]);
2654
+ const radii = idx.map((i) => trunk.radii[i] ?? 0.1);
2655
+ const last = pts.length - 1;
2656
+ const tip = pts[last] ?? [0, 0, 1];
2657
+ if (apex !== null && apex > tip[2] + 0.000001) {
2658
+ pts.push([tip[0], tip[1], apex]);
2659
+ radii.push(0);
2660
+ } else
2661
+ radii[last] = 0;
2662
+ return sweep2({
2663
+ section: { radius: 1, segments: 5 },
2664
+ path: pts,
2665
+ scale: radii,
2666
+ color: p.bark,
2667
+ strip: "wood",
2668
+ caps: apex === null ? "end" : false
2669
+ });
2670
+ }
2671
+ function blob(stems, p, rng) {
2672
+ const trunk = stems[0];
2673
+ if (!trunk)
2674
+ return { trunk: new MeshBuilder2().build(), leaves: new MeshBuilder2().build() };
2675
+ const tip = trunk.pts[trunk.pts.length - 1] ?? [0, 0, 1];
2676
+ const margin = p.leaves.size * 0.6;
2677
+ let z0 = Infinity;
2678
+ let z1 = tip[2];
2679
+ for (const s of stems)
2680
+ for (const q of s.level > 0 ? s.pts : []) {
2681
+ z0 = Math.min(z0, q[2] - margin * (1 + (p.leaves.hang ?? 0) * 2));
2682
+ z1 = Math.max(z1, q[2] + margin);
2683
+ }
2684
+ if (!Number.isFinite(z0))
2685
+ z0 = tip[2] * 0.5;
2686
+ const BINS = 6;
2687
+ const r = new Array(BINS).fill(0.2);
2688
+ for (const s of stems)
2689
+ for (const q of s.level > 0 ? s.pts : []) {
2690
+ const b = Math.min(BINS - 1, Math.max(0, Math.floor((q[2] - z0) / (z1 - z0) * BINS)));
2691
+ r[b] = Math.max(r[b] ?? 0, Math.hypot(q[0] - tip[0], q[1] - tip[1]) + margin);
2692
+ }
2693
+ const profile = [[0, z0]];
2694
+ for (let b = 0;b < BINS; b++)
2695
+ profile.push([r[b] ?? 0.2, z0 + (b + 0.5) / BINS * (z1 - z0)]);
2696
+ profile.push([0, z1]);
2697
+ const leaves = lathe2({ profile, segments: 10, color: p.leaves.color, at: [tip[0], tip[1], 0] });
2698
+ const [cr, cg, cb] = p.leaves.color;
2699
+ for (let i = 0;i < leaves.colors.length / 3; i++) {
2700
+ const t = ((leaves.positions[i * 3 + 2] ?? 0) - z0) / (z1 - z0);
2701
+ const k = (0.55 + 0.5 * t) * (1 + rng.range(-0.08, 0.08));
2702
+ leaves.colors.set([cr * k, cg * k, cb * k], i * 3);
2703
+ }
2704
+ return { trunk: coarseTrunk(trunk, p, z1), leaves };
2705
+ }
2706
+ function frondCards(stems, p, level) {
2707
+ const trunk = stems[0];
2708
+ const mb = new MeshBuilder2;
2709
+ if (!trunk)
2710
+ return { trunk: mb.build(), leaves: mb.build() };
2711
+ const top = trunk.pts[trunk.pts.length - 1] ?? [0, 0, 1];
2712
+ const fronds = stems.filter((s) => s.level === level);
2713
+ const every = Math.max(1, Math.ceil(fronds.length / 12));
2714
+ const [cr, cg, cb] = p.leaves.color;
2715
+ for (const [fi, st] of fronds.entries()) {
2716
+ if (fi % every !== 0)
2717
+ continue;
2718
+ const n = st.pts.length - 1;
2719
+ const pts = [top, st.pts[Math.round(n / 2)] ?? top, st.pts[n] ?? top];
2720
+ const fr = transportFrames2(pts);
2721
+ const w = [0.25, 1, 0.35].map((k) => k * st.length * p.leaves.width);
2722
+ for (const side of [1, -1]) {
2723
+ const edge = pts.map((c, j) => {
2724
+ const f = fr[j];
2725
+ const ww = (w[j] ?? 0) * side;
2726
+ if (!f)
2727
+ return c;
2728
+ return [
2729
+ c[0] + (f.b[0] * 0.85 - f.n[0] * 0.45 * side) * ww,
2730
+ c[1] + (f.b[1] * 0.85 - f.n[1] * 0.45 * side) * ww,
2731
+ c[2] + (f.b[2] * 0.85 - f.n[2] * 0.45 * side) * ww
2732
+ ];
2733
+ });
2734
+ for (let j = 0;j < 2; j++) {
2735
+ const q = [pts[j], pts[j + 1], edge[j + 1], edge[j]];
2736
+ const [a, b, c] = [q[0] ?? top, q[1] ?? top, q[2] ?? top];
2737
+ const e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
2738
+ const e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
2739
+ const nx = (e1[1] ?? 0) * (e2[2] ?? 0) - (e1[2] ?? 0) * (e2[1] ?? 0);
2740
+ const ny = (e1[2] ?? 0) * (e2[0] ?? 0) - (e1[0] ?? 0) * (e2[2] ?? 0);
2741
+ const nz = (e1[0] ?? 0) * (e2[1] ?? 0) - (e1[1] ?? 0) * (e2[0] ?? 0);
2742
+ const l = Math.hypot(nx, ny, nz) || 1;
2743
+ for (const s of [1, -1]) {
2744
+ const nn = [nx / l * s, ny / l * s, nz / l * s];
2745
+ const ids = q.map((v, k) => {
2746
+ const sh = k < 2 ? 0.7 : 1;
2747
+ return mb.vertex({ p: v, n: nn, uv: [0, 0], color: [cr * sh, cg * sh, cb * sh] });
2748
+ });
2749
+ const [i0 = 0, i1 = 0, i2 = 0, i3 = 0] = s > 0 ? ids : [...ids].reverse();
2750
+ mb.quad(i0, i1, i2, i3);
2751
+ }
2752
+ }
2753
+ }
2754
+ }
2755
+ return { trunk: coarseTrunk(trunk, p, null), leaves: mb.build() };
2756
+ }
2757
+
2758
+ // src/kinds/vegetation-params.ts
2759
+ var OUTLINES = {
2760
+ oval: [
2761
+ [0, 0],
2762
+ [0.25, -0.4],
2763
+ [0.65, -0.5],
2764
+ [1, 0],
2765
+ [0.65, 0.5],
2766
+ [0.25, 0.4]
2767
+ ],
2768
+ lobed: [
2769
+ [0, 0],
2770
+ [0.3, -0.45],
2771
+ [0.5, -0.3],
2772
+ [0.75, -0.5],
2773
+ [1, 0],
2774
+ [0.75, 0.5],
2775
+ [0.5, 0.3],
2776
+ [0.3, 0.45]
2777
+ ],
2778
+ tuft: [
2779
+ [0, 0],
2780
+ [0.35, -0.5],
2781
+ [0.45, -0.15],
2782
+ [0.75, -0.4],
2783
+ [1, 0],
2784
+ [0.75, 0.4],
2785
+ [0.45, 0.15],
2786
+ [0.35, 0.5]
2787
+ ],
2788
+ strand: [
2789
+ [0, 0],
2790
+ [0.1, -0.5],
2791
+ [1, 0],
2792
+ [0.1, 0.5]
2793
+ ]
2794
+ };
2795
+ function shapeRatio(shape, r) {
2796
+ const x = Math.min(1, Math.max(0, r));
2797
+ switch (shape) {
2798
+ case "conical":
2799
+ return 0.2 + 0.8 * x;
2800
+ case "spherical":
2801
+ return 0.2 + 0.8 * Math.sin(Math.PI * x);
2802
+ case "hemispherical":
2803
+ return 0.2 + 0.8 * Math.sin(Math.PI / 2 * x);
2804
+ case "cylindrical":
2805
+ return 1;
2806
+ case "tapered":
2807
+ return 0.5 + 0.5 * x;
2808
+ case "flame":
2809
+ return x <= 0.7 ? 0.15 + 0.85 * x / 0.7 : 0.15 + 0.85 * (1 - x) / 0.3;
2810
+ }
2811
+ }
2812
+
2813
+ // src/kinds/vegetation-leaves.ts
2814
+ var unit2 = (a) => {
2815
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
2816
+ return [a[0] / l, a[1] / l, a[2] / l];
2817
+ };
2818
+ function crownShade(stems, level) {
2819
+ let r = 0.1;
2820
+ let z0 = Infinity;
2821
+ let z1 = -Infinity;
2822
+ const t = stems[0]?.pts;
2823
+ const c = t?.[t.length - 1] ?? [0, 0, 0];
2824
+ for (const s of stems) {
2825
+ if (s.level < level)
2826
+ continue;
2827
+ for (const p of s.pts) {
2828
+ r = Math.max(r, Math.hypot(p[0] - c[0], p[1] - c[1]));
2829
+ z0 = Math.min(z0, p[2]);
2830
+ z1 = Math.max(z1, p[2]);
2831
+ }
2832
+ }
2833
+ return { centre: c, radius: r, zMin: z0, zMax: Math.max(z1, z0 + 0.1) };
2834
+ }
2835
+ function card(leaf, base, dir, side, len, shade, rng) {
2836
+ const outline = OUTLINES[leaf.shape === "frond" ? "oval" : leaf.shape];
2837
+ const w = len * leaf.width;
2838
+ const points = outline.map(([u, v]) => [
2839
+ base[0] + dir[0] * u * len + side[0] * v * w,
2840
+ base[1] + dir[1] * u * len + side[1] * v * w,
2841
+ base[2] + dir[2] * u * len + side[2] * v * w
2842
+ ]);
2843
+ const m = planar2({ points, color: leaf.color, doubleSided: true });
2844
+ const k = 1 + rng.range(-leaf.colorVar, leaf.colorVar);
2845
+ const hue = rng.range(-leaf.colorVar, leaf.colorVar) * 0.6;
2846
+ const radial = Math.hypot(base[0] - shade.centre[0], base[1] - shade.centre[1]) / shade.radius;
2847
+ const up = (base[2] - shade.zMin) / (shade.zMax - shade.zMin);
2848
+ const ao = 0.5 + 0.35 * Math.min(1, radial) ** 0.8 + 0.2 * Math.max(0, Math.min(1, up));
2849
+ const [r, g, b] = leaf.color;
2850
+ for (let i = 0;i < m.colors.length / 3; i++) {
2851
+ const u = outline[i % outline.length]?.[0] ?? 0;
2852
+ const s = ao * k * (0.7 + 0.4 * u);
2853
+ m.colors[i * 3] = r * s * (1 + hue);
2854
+ m.colors[i * 3 + 1] = g * s;
2855
+ m.colors[i * 3 + 2] = b * s * (1 - hue);
2856
+ }
2857
+ return m;
2858
+ }
2859
+ function aim(radial, along, leaf, rng) {
2860
+ const hang = leaf.hang ?? 0;
2861
+ const flat = leaf.flat ?? 0;
2862
+ const lift = rng.range(-0.3, 0.5);
2863
+ const out = unit2([
2864
+ radial[0] * 0.9 + along[0] * 0.7,
2865
+ radial[1] * 0.9 + along[1] * 0.7,
2866
+ radial[2] * 0.9 + along[2] * 0.7 + lift
2867
+ ]);
2868
+ const d0 = unit2([out[0] * (1 - hang), out[1] * (1 - hang), out[2] * (1 - hang) - hang]);
2869
+ const dir = unit2([d0[0], d0[1], d0[2] * (1 - flat)]);
2870
+ const level = unit2([-dir[1], dir[0], 0]);
2871
+ const s1 = unit2([
2872
+ dir[1] * radial[2] - dir[2] * radial[1],
2873
+ dir[2] * radial[0] - dir[0] * radial[2],
2874
+ dir[0] * radial[1] - dir[1] * radial[0]
2875
+ ]);
2876
+ const s0 = Math.hypot(...s1) < 0.5 ? level : s1;
2877
+ const side = unit2([
2878
+ s0[0] + (level[0] - s0[0]) * flat,
2879
+ s0[1] + (level[1] - s0[1]) * flat,
2880
+ s0[2] + (level[2] - s0[2]) * flat
2881
+ ]);
2882
+ const roll = rng.range(-0.7, 0.7) * (1 - flat);
2883
+ const t = [
2884
+ dir[1] * side[2] - dir[2] * side[1],
2885
+ dir[2] * side[0] - dir[0] * side[2],
2886
+ dir[0] * side[1] - dir[1] * side[0]
2887
+ ];
2888
+ const c = Math.cos(roll);
2889
+ const s = Math.sin(roll);
2890
+ return { dir, side: [side[0] * c + t[0] * s, side[1] * c + t[1] * s, side[2] * c + t[2] * s] };
2891
+ }
2892
+ function leafCards(stems, level, ringSegments, leaf, shade, rng) {
2893
+ const out = [];
2894
+ for (const st of stems) {
2895
+ if (st.level !== level)
2896
+ continue;
2897
+ const frames = transportFrames2(st.pts);
2898
+ const res = st.pts.length - 1;
2899
+ for (let i = 0;i < leaf.perStem; i++) {
2900
+ const from = leaf.from ?? 0.3;
2901
+ const t = from + (1 - from) * (i + 1) / leaf.perStem;
2902
+ const j = Math.max(1, Math.min(res, Math.round(t * res)));
2903
+ const f = frames[j];
2904
+ const c = st.pts[j];
2905
+ if (!f || !c)
2906
+ continue;
2907
+ const a = rng.int(0, ringSegments - 1) / ringSegments * Math.PI * 2;
2908
+ const radial = [
2909
+ Math.cos(a) * f.n[0] + Math.sin(a) * f.b[0],
2910
+ Math.cos(a) * f.n[1] + Math.sin(a) * f.b[1],
2911
+ Math.cos(a) * f.n[2] + Math.sin(a) * f.b[2]
2912
+ ];
2913
+ const r = st.radii[j] ?? 0;
2914
+ const base = [c[0] + radial[0] * r, c[1] + radial[1] * r, c[2] + radial[2] * r];
2915
+ const { dir, side } = aim(radial, f.t, leaf, rng);
2916
+ out.push(card(leaf, base, dir, side, leaf.size * rng.range(0.8, 1.2), shade, rng));
2917
+ }
2918
+ }
2919
+ return merge2(out);
2920
+ }
2921
+ function clusterCards(stems, level, leaf, shade, rng) {
2922
+ const out = [];
2923
+ for (const st of stems) {
2924
+ if (st.level !== level)
2925
+ continue;
2926
+ const tip = st.pts[st.pts.length - 1] ?? st.surface;
2927
+ const p0 = st.pts[0] ?? st.surface;
2928
+ const along = unit2([tip[0] - p0[0], tip[1] - p0[1], tip[2] - p0[2]]);
2929
+ const radial = unit2([st.surface[0] - p0[0], st.surface[1] - p0[1], st.surface[2] - p0[2]]);
2930
+ const { dir, side } = aim(radial, along, leaf, rng);
2931
+ const len = st.length * 1.1 + leaf.size;
2932
+ out.push(card(leaf, st.surface, dir, side, len, shade, rng));
2933
+ }
2934
+ return merge2(out);
2935
+ }
2936
+
2937
+ // src/kinds/vegetation-mesh.ts
2938
+ var RADIAL = [
2939
+ [8, 6, 5],
2940
+ [6, 4, 4],
2941
+ [4, 3, 3],
2942
+ [3, 3, 3]
2943
+ ];
2944
+ var radialSegments = (level, lod) => RADIAL[Math.min(3, level)]?.[lod] ?? 3;
2945
+ function coarsen(st, step) {
2946
+ const pts = [];
2947
+ const radii = [];
2948
+ const last = st.pts.length - 1;
2949
+ for (let i = 0;i <= last; i += step) {
2950
+ pts.push(st.pts[i] ?? [0, 0, 0]);
2951
+ radii.push(st.radii[i] ?? 0);
2952
+ }
2953
+ if (last % step !== 0) {
2954
+ pts.push(st.pts[last] ?? [0, 0, 0]);
2955
+ radii.push(st.radii[last] ?? 0);
2956
+ }
2957
+ return { pts, radii };
2958
+ }
2959
+ function stemsMesh(stems, lo, hi, lod, color) {
2960
+ const out = [];
2961
+ for (const st of stems) {
2962
+ if (st.level < lo || st.level > hi)
2963
+ continue;
2964
+ const { pts, radii } = coarsen(st, lod === 0 ? 1 : 2);
2965
+ out.push(sweep2({
2966
+ section: { radius: 1, segments: radialSegments(st.level, lod) },
2967
+ path: pts,
2968
+ scale: radii,
2969
+ color,
2970
+ strip: "wood"
2971
+ }));
2972
+ }
2973
+ return merge2(out);
2974
+ }
2975
+ var mix = (a, b, t) => [
2976
+ a[0] + (b[0] - a[0]) * t,
2977
+ a[1] + (b[1] - a[1]) * t,
2978
+ a[2] + (b[2] - a[2]) * t
2979
+ ];
2980
+ function barkTint(m, p, top, rng) {
2981
+ const hi = p.barkTop ?? p.bark;
2982
+ for (let i = 0;i < m.colors.length / 3; i++) {
2983
+ const z = m.positions[i * 3 + 2] ?? 0;
2984
+ const t = Math.min(1, Math.max(0, (z / Math.max(0.1, top) - 0.3) / 0.4));
2985
+ let c = mix(p.bark, hi, t * t * (3 - 2 * t));
2986
+ if (p.barkMarks && rng.next() < p.barkMarks.amount)
2987
+ c = mix(c, p.barkMarks.color, 0.85);
2988
+ const k = 1 + rng.range(-0.06, 0.06);
2989
+ m.colors.set([c[0] * k, c[1] * k, c[2] * k], i * 3);
2990
+ }
2991
+ return m;
2992
+ }
2993
+ function trunkMesh(trunk, p, lod, radiusScale, rng) {
2994
+ const { pts, radii } = coarsen(trunk, lod === 0 ? 1 : 2);
2995
+ const m = sweep2({
2996
+ section: { radius: 1, segments: radialSegments(0, lod) },
2997
+ path: pts,
2998
+ scale: radii.map((r) => r * radiusScale),
2999
+ color: p.bark,
3000
+ strip: "wood",
3001
+ caps: p.impostor === "cards" ? "end" : false
3002
+ });
3003
+ return barkTint(m, p, pts[pts.length - 1]?.[2] ?? 1, rng);
3004
+ }
3005
+ function axisAt(trunk, z) {
3006
+ for (let i = 1;i < trunk.pts.length; i++) {
3007
+ const a = trunk.pts[i - 1] ?? [0, 0, 0];
3008
+ const b = trunk.pts[i] ?? a;
3009
+ if (b[2] >= z || i === trunk.pts.length - 1) {
3010
+ const f = Math.min(1, Math.max(0, (z - a[2]) / (b[2] - a[2] || 1)));
3011
+ return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, z];
3012
+ }
3013
+ }
3014
+ return [0, 0, z];
3015
+ }
3016
+ function rootsMesh(trunk, p, lod, radiusScale, rng) {
3017
+ const r0 = (trunk.radii[0] ?? 0.2) * radiusScale;
3018
+ const lobes = p.roots ?? 0;
3019
+ const phase = rng.range(0, Math.PI * 2);
3020
+ const levels = [
3021
+ { z: -0.1, k: 1.7, amp: 0.7 },
3022
+ { z: 0.5 * r0, k: 1.3, amp: 0.4 },
3023
+ { z: 1.3 * r0, k: 1.04, amp: 0.15 },
3024
+ { z: 2.4 * r0, k: 0.9, amp: 0 }
3025
+ ];
3026
+ const segs = lod === 0 ? 16 : 10;
3027
+ const rings = levels.map(({ z, k, amp }) => {
3028
+ const c = axisAt(trunk, z);
3029
+ const lobe = (a) => 1 + amp * Math.max(0, Math.cos(lobes * (a - phase))) ** 2;
3030
+ return ringXY2(c, r0 * k, r0 * k, segs, lobe);
3031
+ });
3032
+ const m = loft2({ rings, color: p.bark, strip: "wood" });
3033
+ return barkTint(m, p, trunk.pts[trunk.pts.length - 1]?.[2] ?? 1, rng);
3034
+ }
3035
+ function fitFactors(meshes, target) {
3036
+ const lo = [Infinity, Infinity, Infinity];
3037
+ const hi = [-Infinity, -Infinity, -Infinity];
3038
+ for (const m of meshes) {
3039
+ if (m.indices.length === 0)
3040
+ continue;
3041
+ const b = boundsOf2(m);
3042
+ for (let k = 0;k < 3; k++) {
3043
+ lo[k] = Math.min(lo[k] ?? 0, b.min[k] ?? 0);
3044
+ hi[k] = Math.max(hi[k] ?? 0, b.max[k] ?? 0);
3045
+ }
3046
+ }
3047
+ const f = (k) => target[k] / Math.max(0.000001, (hi[k] ?? 1) - (lo[k] ?? 0));
3048
+ return [f(0), f(1), f(2)];
3049
+ }
3050
+ function scaleMesh(m, s) {
3051
+ const positions = new Float32Array(m.positions.length);
3052
+ const normals = new Float32Array(m.normals.length);
3053
+ for (let i = 0;i < positions.length; i += 3) {
3054
+ positions[i] = (m.positions[i] ?? 0) * s[0];
3055
+ positions[i + 1] = (m.positions[i + 1] ?? 0) * s[1];
3056
+ positions[i + 2] = (m.positions[i + 2] ?? 0) * s[2];
3057
+ const nx = (m.normals[i] ?? 0) / s[0];
3058
+ const ny = (m.normals[i + 1] ?? 0) / s[1];
3059
+ const nz = (m.normals[i + 2] ?? 0) / s[2];
3060
+ const l = Math.hypot(nx, ny, nz) || 1;
3061
+ normals[i] = nx / l;
3062
+ normals[i + 1] = ny / l;
3063
+ normals[i + 2] = nz / l;
3064
+ }
3065
+ return { ...m, positions, normals };
3066
+ }
3067
+
3068
+ // src/kinds/vegetation-presets.ts
3069
+ var oak2 = {
3070
+ shape: "hemispherical",
3071
+ baseSize: 0.3,
3072
+ ratioPower: 1.15,
3073
+ taper: 0.3,
3074
+ levels: [
3075
+ { branches: 0, length: 0.55, downAngle: 0, curveVar: 5, segments: 8 },
3076
+ {
3077
+ branches: 8,
3078
+ length: 0.95,
3079
+ downAngle: 58,
3080
+ downAngleV: -18,
3081
+ curve: -8,
3082
+ curveVar: 16,
3083
+ segments: 7
3084
+ },
3085
+ { branches: 6, length: 0.55, downAngle: 50, curve: 12, curveVar: 18, start: 0.2, segments: 4 },
3086
+ { branches: 5, length: 0.5, downAngle: 45, curveVar: 14, start: 0.15, segments: 2 }
3087
+ ],
3088
+ bark: hex2("#4a4036"),
3089
+ barkTop: hex2("#5a5045"),
3090
+ leaves: {
3091
+ shape: "lobed",
3092
+ size: 1.7,
3093
+ width: 0.8,
3094
+ perStem: 5,
3095
+ color: hex2("#3d6022"),
3096
+ colorVar: 0.16
3097
+ },
3098
+ roots: 5,
3099
+ impostor: "blob",
3100
+ typical: { height: 22, spread: 22, dbh: 1.3 }
3101
+ };
3102
+ var pine2 = {
3103
+ shape: "cylindrical",
3104
+ baseSize: 0.6,
3105
+ ratioPower: 1.2,
3106
+ taper: 0.12,
3107
+ levels: [
3108
+ { branches: 0, length: 0.94, downAngle: 0, curveVar: 3, segments: 10 },
3109
+ {
3110
+ branches: 13,
3111
+ length: 0.34,
3112
+ downAngle: 94,
3113
+ downAngleV: -14,
3114
+ curve: 16,
3115
+ curveVar: 14,
3116
+ segments: 5
3117
+ },
3118
+ { branches: 5, length: 0.42, downAngle: 60, curve: 18, curveVar: 14, start: 0.3, segments: 3 }
3119
+ ],
3120
+ bark: hex2("#4a3a30"),
3121
+ barkTop: hex2("#c0652c"),
3122
+ leaves: {
3123
+ shape: "tuft",
3124
+ size: 2.1,
3125
+ width: 0.95,
3126
+ perStem: 5,
3127
+ from: 0.5,
3128
+ flat: 0.8,
3129
+ color: hex2("#24422e"),
3130
+ colorVar: 0.1
3131
+ },
3132
+ roots: 4,
3133
+ impostor: "blob",
3134
+ typical: { height: 25, spread: 9, dbh: 0.6 }
3135
+ };
3136
+ var palm2 = {
3137
+ shape: "cylindrical",
3138
+ baseSize: 0.95,
3139
+ ratioPower: 1.1,
3140
+ taper: 0.7,
3141
+ lean: 12,
3142
+ levels: [
3143
+ { branches: 0, length: 1, downAngle: 0, curve: 24, curveVar: 1.5, segments: 12 },
3144
+ {
3145
+ branches: 22,
3146
+ length: 0.26,
3147
+ downAngle: 115,
3148
+ downAngleV: -85,
3149
+ rotate: 137.5,
3150
+ curve: -55,
3151
+ curveVar: 4,
3152
+ segments: 8
3153
+ }
3154
+ ],
3155
+ bark: hex2("#6b5d4a"),
3156
+ barkTop: hex2("#8a7a62"),
3157
+ leaves: {
3158
+ shape: "frond",
3159
+ size: 0,
3160
+ width: 0.2,
3161
+ perStem: 26,
3162
+ color: hex2("#4f7a2a"),
3163
+ colorVar: 0.12
3164
+ },
3165
+ roots: 9,
3166
+ fruit: { count: 7, radius: 0.13, color: hex2("#5f6a22") },
3167
+ impostor: "cards",
3168
+ typical: { height: 20, spread: 9, dbh: 0.35 }
3169
+ };
3170
+ var birch2 = {
3171
+ shape: "flame",
3172
+ baseSize: 0.3,
3173
+ ratioPower: 1.4,
3174
+ taper: 0.1,
3175
+ levels: [
3176
+ { branches: 0, length: 1, downAngle: 0, curveVar: 3, segments: 10 },
3177
+ {
3178
+ branches: 22,
3179
+ length: 0.32,
3180
+ downAngle: 45,
3181
+ downAngleV: 10,
3182
+ curve: -25,
3183
+ curveVar: 8,
3184
+ segments: 4
3185
+ },
3186
+ { branches: 5, length: 0.55, downAngle: 30, curve: -45, curveVar: 10, start: 0.2, segments: 3 }
3187
+ ],
3188
+ bark: hex2("#d9d6cc"),
3189
+ barkMarks: { color: hex2("#24221f"), amount: 0.12 },
3190
+ leaves: {
3191
+ shape: "oval",
3192
+ size: 0.8,
3193
+ width: 0.7,
3194
+ perStem: 4,
3195
+ color: hex2("#6f9a32"),
3196
+ colorVar: 0.15
3197
+ },
3198
+ impostor: "blob",
3199
+ typical: { height: 18, spread: 8, dbh: 0.4 }
3200
+ };
3201
+ var willow2 = {
3202
+ shape: "hemispherical",
3203
+ baseSize: 0.25,
3204
+ ratioPower: 1.7,
3205
+ taper: 0.35,
3206
+ levels: [
3207
+ { branches: 0, length: 0.55, downAngle: 0, curveVar: 5, segments: 6 },
3208
+ {
3209
+ branches: 8,
3210
+ length: 0.95,
3211
+ downAngle: 42,
3212
+ curve: 40,
3213
+ curveBack: -70,
3214
+ curveVar: 8,
3215
+ segments: 6
3216
+ },
3217
+ { branches: 7, length: 0.55, downAngle: 35, curve: -80, curveVar: 8, start: 0.3, segments: 4 }
3218
+ ],
3219
+ bark: hex2("#4f463a"),
3220
+ leaves: {
3221
+ shape: "strand",
3222
+ size: 2.6,
3223
+ width: 0.22,
3224
+ perStem: 4,
3225
+ color: hex2("#8aa83e"),
3226
+ colorVar: 0.12,
3227
+ hang: 0.92
3228
+ },
3229
+ roots: 6,
3230
+ impostor: "blob",
3231
+ typical: { height: 12, spread: 14, dbh: 0.8 }
3232
+ };
3233
+ var SPECIES2 = { oak: oak2, pine: pine2, palm: palm2, birch: birch2, willow: willow2 };
3234
+
3235
+ // src/kinds/vegetation-skeleton.ts
3236
+ var deg = Math.PI / 180;
3237
+ var unit3 = (a) => {
3238
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
3239
+ return [a[0] / l, a[1] / l, a[2] / l];
3240
+ };
3241
+ function rotate(v, k, a) {
3242
+ const c = Math.cos(a);
3243
+ const s = Math.sin(a);
3244
+ const d = (k[0] * v[0] + k[1] * v[1] + k[2] * v[2]) * (1 - c);
3245
+ return [
3246
+ v[0] * c + (k[1] * v[2] - k[2] * v[1]) * s + k[0] * d,
3247
+ v[1] * c + (k[2] * v[0] - k[0] * v[2]) * s + k[1] * d,
3248
+ v[2] * c + (k[0] * v[1] - k[1] * v[0]) * s + k[2] * d
3249
+ ];
3250
+ }
3251
+ function upAxis(dir) {
3252
+ const k = [dir[1], -dir[0], 0];
3253
+ return Math.hypot(k[0], k[1]) < 0.0001 ? [0, 1, 0] : unit3(k);
3254
+ }
3255
+ function path(start, dir0, len, lv, trunk, rng) {
3256
+ const res = Math.max(1, lv.segments);
3257
+ const step = len / res;
3258
+ const pts = [start];
3259
+ let dir = dir0;
3260
+ let p = start;
3261
+ for (let i = 0;i < res; i++) {
3262
+ const half = lv.curveBack !== undefined && i >= res / 2;
3263
+ const bend = half ? (lv.curveBack ?? 0) / Math.ceil(res / 2) : (lv.curve ?? 0) / (lv.curveBack !== undefined ? Math.max(1, Math.floor(res / 2)) : res);
3264
+ const wob = lv.curveVar ?? 0;
3265
+ if (trunk)
3266
+ dir = rotate(dir, [0, 1, 0], -(bend + rng.range(-wob, wob)) * deg);
3267
+ else {
3268
+ dir = rotate(dir, upAxis(dir), (bend + rng.range(-wob, wob)) * deg);
3269
+ dir = rotate(dir, [0, 0, 1], rng.range(-wob, wob) * deg);
3270
+ }
3271
+ dir = unit3(dir);
3272
+ p = [p[0] + dir[0] * step, p[1] + dir[1] * step, p[2] + dir[2] * step];
3273
+ pts.push(p);
3274
+ }
3275
+ return pts;
3276
+ }
3277
+ function grow(p, size, rng) {
3278
+ const t0 = p.levels[0];
3279
+ if (!t0)
3280
+ return [];
3281
+ const L0 = size.height * t0.length;
3282
+ const r0 = size.dbh / 2;
3283
+ const lean = (p.lean ?? 0) * deg;
3284
+ const trunkPts = path([0, 0, -0.05], [Math.sin(lean), 0, Math.cos(lean)], L0, t0, true, rng);
3285
+ const arc0 = arcLengths2(trunkPts);
3286
+ const bh = 1.3;
3287
+ const trunkR = arc0.map((s) => {
3288
+ const f = Math.max(0, (s - bh) / Math.max(0.1, L0 - bh));
3289
+ return r0 * (1 - (1 - p.taper) * f);
3290
+ });
3291
+ const stems = [
3292
+ {
3293
+ level: 0,
3294
+ pts: trunkPts,
3295
+ radii: trunkR,
3296
+ length: L0,
3297
+ parent: -1,
3298
+ surface: [0, 0, 0],
3299
+ maxLength: L0
3300
+ }
3301
+ ];
3302
+ for (let si = 0;si < stems.length; si++) {
3303
+ const par = stems[si];
3304
+ const lv = par ? p.levels[par.level + 1] : undefined;
3305
+ if (!par || !lv)
3306
+ continue;
3307
+ const arc = arcLengths2(par.pts);
3308
+ const frames = transportFrames2(par.pts);
3309
+ const s0 = (par.level === 0 ? p.baseSize : lv.start ?? 0.1) * par.length;
3310
+ const s1 = par.length * 0.97;
3311
+ const fill = par.level === 0 ? 1 : Math.min(1, par.length / par.maxLength);
3312
+ const n = par.level === 0 ? lv.branches : Math.round(lv.branches * (0.25 + 0.75 * fill));
3313
+ let phi = rng.range(0, 360);
3314
+ for (let i = 0;i < n; i++) {
3315
+ const s = s0 + (s1 - s0) * (i + rng.range(0.15, 0.85)) / Math.max(1, n);
3316
+ let j = 0;
3317
+ while (j < arc.length - 2 && (arc[j + 1] ?? 0) < s)
3318
+ j++;
3319
+ const a = par.pts[j] ?? [0, 0, 0];
3320
+ const b = par.pts[j + 1] ?? a;
3321
+ const f = (s - (arc[j] ?? 0)) / ((arc[j + 1] ?? 1) - (arc[j] ?? 0) || 1);
3322
+ const c = [
3323
+ a[0] + (b[0] - a[0]) * f,
3324
+ a[1] + (b[1] - a[1]) * f,
3325
+ a[2] + (b[2] - a[2]) * f
3326
+ ];
3327
+ const rp = (par.radii[j] ?? 0) * (1 - f) + (par.radii[j + 1] ?? 0) * f;
3328
+ const T = unit3([b[0] - a[0], b[1] - a[1], b[2] - a[2]]);
3329
+ const fr = frames[j] ?? { n: [1, 0, 0], b: [0, 1, 0] };
3330
+ phi += (lv.rotate ?? 137.5) + rng.range(-10, 10);
3331
+ const cp = Math.cos(phi * deg);
3332
+ const sp = Math.sin(phi * deg);
3333
+ const raw = [
3334
+ cp * fr.n[0] + sp * fr.b[0],
3335
+ cp * fr.n[1] + sp * fr.b[1],
3336
+ cp * fr.n[2] + sp * fr.b[2]
3337
+ ];
3338
+ const tdot = raw[0] * T[0] + raw[1] * T[1] + raw[2] * T[2];
3339
+ const np = unit3([raw[0] - tdot * T[0], raw[1] - tdot * T[1], raw[2] - tdot * T[2]]);
3340
+ const zone = (s - s0) / Math.max(0.000001, s1 - s0);
3341
+ const down = (lv.downAngle + (lv.downAngleV ?? 0) * zone + rng.range(-5, 5)) * deg;
3342
+ const dir = unit3([
3343
+ T[0] * Math.cos(down) + np[0] * Math.sin(down),
3344
+ T[1] * Math.cos(down) + np[1] * Math.sin(down),
3345
+ T[2] * Math.cos(down) + np[2] * Math.sin(down)
3346
+ ]);
3347
+ const vary = 1 + rng.range(-(lv.lengthVar ?? 0.15), lv.lengthVar ?? 0.15);
3348
+ const maxLength = lv.length * par.length;
3349
+ const base = par.level === 0 ? maxLength * shapeRatio(p.shape, (par.length - s) / Math.max(0.000001, par.length - s0)) : lv.length * (par.length - 0.6 * s);
3350
+ const len = Math.max(0.15, base * vary);
3351
+ const r0c = par.radii[0] ?? rp;
3352
+ const rc = Math.max(0.006, Math.min(0.8 * rp, r0c * (len / par.length) ** p.ratioPower));
3353
+ const start = [
3354
+ c[0] + np[0] * (rp - rc),
3355
+ c[1] + np[1] * (rp - rc),
3356
+ c[2] + np[2] * (rp - rc)
3357
+ ];
3358
+ const pts = path(start, dir, len, lv, false, rng);
3359
+ stems.push({
3360
+ level: par.level + 1,
3361
+ pts,
3362
+ radii: pts.map((_, k) => rc * (1 - 0.75 * k / Math.max(1, pts.length - 1))),
3363
+ length: len,
3364
+ parent: si,
3365
+ surface: [c[0] + np[0] * rp, c[1] + np[1] * rp, c[2] + np[2] * rp],
3366
+ maxLength
3367
+ });
3368
+ }
3369
+ }
3370
+ return stems;
3371
+ }
3372
+
3373
+ // src/kinds/vegetation.ts
3374
+ function treeParams2(o) {
3375
+ const s = o.species ?? "oak";
3376
+ const base = typeof s === "string" ? SPECIES2[s] : s;
3377
+ if (!base) {
3378
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
3379
+ cause: `unknown tree species '${String(s)}'`,
3380
+ fix: `use one of ${Object.keys(SPECIES2).join(", ")} or pass a TreeParams object (spread a preset and override)`
3381
+ });
3382
+ }
3383
+ return { ...base, ...o.params };
3384
+ }
3385
+ function tree2(ctx, o) {
3386
+ const p = treeParams2(o);
3387
+ if (!(o.height > 0) || p.levels.length < 2 || p.levels.length > 4) {
3388
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
3389
+ cause: `tree height ${o.height} m with ${p.levels.length} branch levels`,
3390
+ fix: "height must be > 0 and levels must hold the trunk plus 1–3 branch levels"
3391
+ });
3392
+ }
3393
+ const k = o.height / p.typical.height;
3394
+ const spread = o.spread ?? p.typical.spread * k;
3395
+ const dbh = o.dbh ?? p.typical.dbh * k;
3396
+ const pre = o.prefix ? `${o.prefix}_` : "";
3397
+ const parts = {
3398
+ trunk: `${pre}trunk`,
3399
+ crown: `${pre}crown`,
3400
+ branches: `${pre}branches`,
3401
+ leaves: `${pre}leaves`,
3402
+ roots: `${pre}roots`
3403
+ };
3404
+ const rng = ctx.rng.fork(`tree:${o.prefix ?? ""}:${o.seed ?? 0}`);
3405
+ const stems = grow(p, { height: o.height, dbh }, rng.fork("skeleton"));
3406
+ const trunkStem = stems[0];
3407
+ if (!trunkStem)
3408
+ return { parts, height: o.height, spread, dbh };
3409
+ const deepest = p.levels.length - 1;
3410
+ const lod = ctx.lod;
3411
+ const top = trunkStem.pts[trunkStem.pts.length - 1]?.[2] ?? o.height;
3412
+ const shade = crownShade(stems, 1);
3413
+ let trunk;
3414
+ let roots = null;
3415
+ let branches = null;
3416
+ let leaves;
3417
+ if (lod === 2) {
3418
+ const imp = p.impostor === "cards" ? frondCards(stems, p, 1) : blob(stems, p, rng.fork("impostor"));
3419
+ trunk = () => barkTint(imp.trunk, p, top, rng.fork("bark"));
3420
+ leaves = imp.leaves;
3421
+ } else {
3422
+ const hi = lod === 0 || deepest < 2 ? deepest : deepest - 1;
3423
+ trunk = (r) => trunkMesh(trunkStem, p, lod, r, rng.fork("bark"));
3424
+ if (p.roots)
3425
+ roots = (r) => rootsMesh(trunkStem, p, lod, r, rng.fork("roots"));
3426
+ const wood = [
3427
+ barkTint(stemsMesh(stems, 1, hi, lod, p.bark), p, top, rng.fork("bark:branches"))
3428
+ ];
3429
+ if (p.fruit && lod === 0)
3430
+ wood.push(fruit(trunkStem, p.baseSize - 0.01, p.fruit, rng.fork("fruit")));
3431
+ branches = merge2(wood);
3432
+ const lr = rng.fork("leaves");
3433
+ leaves = p.leaves.shape === "frond" ? frondLeaflets(stems, deepest, p.leaves, lod === 0 ? p.leaves.perStem : Math.ceil(p.leaves.perStem / 2), shade, lr) : hi === deepest ? leafCards(stems, deepest, radialSegments(deepest, lod), p.leaves, shade, lr) : clusterCards(stems, deepest, p.leaves, shade, lr);
3434
+ }
3435
+ const s = fitFactors([trunk(1), branches, leaves, roots?.(1) ?? null].filter((m) => m !== null), [spread, spread, o.height]);
3436
+ const fit = (m) => scaleMesh(m, s);
3437
+ const base = trunkStem.pts[Math.round(p.baseSize * (trunkStem.pts.length - 1))] ?? [0, 0, 0];
3438
+ const pivot = [base[0] * s[0], base[1] * s[1], base[2] * s[2]];
3439
+ const back = [-pivot[0], -pivot[1], -pivot[2]];
3440
+ ctx.part(parts.trunk, fit(trunk(1 / s[1])));
3441
+ if (roots)
3442
+ ctx.part(parts.roots, fit(roots(1 / s[1])));
3443
+ ctx.group(parts.crown, {
3444
+ at: pivot,
3445
+ motion: { kind: "sway", axis: [1, 0, 0], degrees: 2, hz: 0.4 },
3446
+ clearOf: roots ? [parts.trunk, parts.roots] : [parts.trunk]
3447
+ });
3448
+ if (branches && triangleCount2(branches) > 0)
3449
+ ctx.part(parts.branches, translate2(fit(branches), back), { parent: parts.crown });
3450
+ ctx.part(parts.leaves, translate2(fit(leaves), back), { parent: parts.crown });
3451
+ return { parts, height: o.height, spread, dbh };
3452
+ }
3453
+ function treeReal2(o) {
3454
+ const pre = o.prefix ? `${o.prefix}_` : "";
3455
+ const src = o.source ? { source: o.source } : {};
3456
+ return {
3457
+ length: o.spread,
3458
+ width: o.spread,
3459
+ height: o.height,
3460
+ measures: [
3461
+ {
3462
+ label: "trunk diameter at breast height",
3463
+ value: o.dbh,
3464
+ axis: "y",
3465
+ parts: [`${pre}trunk`],
3466
+ ...src
3467
+ },
3468
+ {
3469
+ label: "total height",
3470
+ value: o.height,
3471
+ axis: "z",
3472
+ parts: [`${pre}trunk`, `${pre}crown`, `${pre}branches`, `${pre}leaves`, `${pre}roots`],
3473
+ ...src
3474
+ }
3475
+ ]
3476
+ };
3477
+ }
3478
+ var trees2 = defineFamily2({
3479
+ kind: "vegetation",
3480
+ tags: ["vegetation", "tree"],
3481
+ budget: BUDGETS2.tree,
3482
+ defaults: { species: "oak", height: 22 },
3483
+ build: (ctx, p) => {
3484
+ tree2(ctx, p);
3485
+ }
3486
+ });
3487
+ // src/kinds/vehicle.ts
3488
+ var segs = (ctx, hi) => [hi, Math.max(8, hi / 2), 6][ctx.lod] ?? 6;
3489
+ function axle2(ctx, o) {
3490
+ ctx.group(o.name, { at: o.at, motion: { kind: "spin", axis: [0, 1, 0], rps: o.rps } });
3491
+ for (const [s, y] of [
3492
+ ["l", o.gauge / 2],
3493
+ ["r", -o.gauge / 2]
3494
+ ]) {
3495
+ ctx.part(`${o.name}_wheel_${s}`, cylinder2({
3496
+ radius: o.radius,
3497
+ length: o.width,
3498
+ segments: segs(ctx, 24),
3499
+ axis: "y",
3500
+ at: [0, y, 0],
3501
+ color: o.color,
3502
+ strip: "iron"
3503
+ }), { parent: o.name });
3504
+ }
3505
+ }
3506
+ function whyteAxles2(arrangement, o) {
3507
+ const groups = parseWhyte2(arrangement);
3508
+ const out = [];
3509
+ let x = 0;
3510
+ for (const g of groups) {
3511
+ const r = g.role === "driving" ? o.driving : g.role === "leading" ? o.leading : o.trailing;
3512
+ for (let i = 0;i < g.count / 2; i++) {
3513
+ out.push({ role: g.role, x: x + r, radius: r });
3514
+ x += r * 2 + o.spacing;
3515
+ }
3516
+ }
3517
+ const mid = x / 2;
3518
+ return out.map((a) => ({ ...a, x: mid - a.x }));
3519
+ }
3520
+ function trackRun2(ctx, o) {
3521
+ const pitch = o.length / o.wheels;
3522
+ for (let i = 0;i < o.wheels; i++) {
3523
+ const x = -o.length / 2 + pitch * (i + 0.5);
3524
+ ctx.part(`road_wheel_${o.side}_${i}`, cylinder2({
3525
+ radius: o.radius,
3526
+ length: o.width * 0.8,
3527
+ segments: segs(ctx, 20),
3528
+ axis: "y",
3529
+ at: [0, 0, 0],
3530
+ color: o.color,
3531
+ strip: "iron"
3532
+ }), { at: [x, o.y, o.radius + 0.06], motion: { kind: "spin", axis: [0, 1, 0], rps: -0.6 } });
3533
+ }
3534
+ const t = 0.06;
3535
+ ctx.part(`track_${o.side}_bottom`, box2({
3536
+ size: [o.length + o.radius, o.width, t],
3537
+ at: [0, o.y, 0],
3538
+ color: o.track,
3539
+ strip: "iron"
3540
+ }));
3541
+ ctx.part(`track_${o.side}_top`, box2({
3542
+ size: [o.length, o.width, t],
3543
+ at: [0, o.y, o.radius * 2 + 0.06],
3544
+ color: o.track,
3545
+ strip: "iron"
3546
+ }));
3547
+ }
3548
+ function propeller2(ctx, o) {
3549
+ ctx.group(o.name, {
3550
+ at: o.at,
3551
+ ...o.parent ? { parent: o.parent } : {},
3552
+ motion: { kind: "spin", axis: [1, 0, 0], rps: 6 }
3553
+ });
3554
+ ctx.part(`${o.name}_hub`, cylinder2({
3555
+ radius: o.radius * 0.12,
3556
+ length: o.radius * 0.3,
3557
+ segments: segs(ctx, 12),
3558
+ axis: "x",
3559
+ color: o.color
3560
+ }), { parent: o.name });
3561
+ const blade = box2({ size: [0.03, o.radius * 0.12, o.radius], color: o.color });
3562
+ for (let i = 0;i < o.blades; i++) {
3563
+ ctx.part(`${o.name}_blade_${i}`, place2(blade, { roll: i / o.blades * Math.PI * 2 }), {
3564
+ parent: o.name
3565
+ });
3566
+ }
3567
+ }
3568
+ export { mass2, opening2, facade2, setbacks2, checkHead2, HEADS2, headParams2, HEAD_RES2, headEdits2, head2, hullLines2, hullMesh2, hull2, creature2, roofHeightAt2, ridgeHeight2, roofMesh2, roof2, roofInfo2, oak2, pine2, palm2, birch2, willow2, SPECIES2, treeParams2, tree2, treeReal2, trees2, axle2, whyteAxles2, trackRun2, propeller2 };
3569
+
3570
+ //# debugId=5C8F10A02B6E9AB264756E2164756E21
3571
+ //# sourceMappingURL=chunk-1ypfs7ga.js.map