@developerz.ai/ts23d 0.0.0-stage → 0.2.0

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