@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,2668 @@
1
+ import {
2
+ Ts23dError2,
3
+ createRng2
4
+ } from "./chunk-hg7pf34j.js";
5
+
6
+ // src/material/cc0.ts
7
+ var PH = "https://dl.polyhaven.org/file/ph-assets/Textures/png/1k/wood_planks_grey";
8
+ var CC0_WOOD_PLANKS_GREY2 = {
9
+ id: "ph_wood_planks_grey",
10
+ kind: "flat",
11
+ scaleMetres: 0.6,
12
+ roughness: 0.85,
13
+ description: "Photo-scanned weathered grey planks (Poly Haven, CC0).",
14
+ sources: ["https://polyhaven.com/a/wood_planks_grey"],
15
+ texture: {
16
+ source: "polyhaven",
17
+ id: "wood_planks_grey",
18
+ license: "CC0-1.0",
19
+ page: "https://polyhaven.com/a/wood_planks_grey",
20
+ author: "Rob Tuytel",
21
+ metres: [1.5, 1.5],
22
+ maps: {
23
+ albedo: {
24
+ url: `${PH}/wood_planks_grey_diff_1k.png`,
25
+ sha256: "3e6aeb842edd813759fdc96a6ffe6b695c888280f42501de589442311749ac5d"
26
+ },
27
+ normal: {
28
+ url: `${PH}/wood_planks_grey_nor_gl_1k.png`,
29
+ sha256: "a9f2ad602eca2d59e84f203b1087e819e62ae517532bb25f7eb02a13c826e08b"
30
+ },
31
+ arm: {
32
+ url: `${PH}/wood_planks_grey_arm_1k.png`,
33
+ sha256: "9b81e9a0b4f9db0a920c4c2ce8ff13e0411b3624d4de455acdabe41e3fafe20e"
34
+ }
35
+ }
36
+ }
37
+ };
38
+ var CC0_MATERIALS2 = [CC0_WOOD_PLANKS_GREY2];
39
+ // src/material/library.ts
40
+ var PB = "https://physicallybased.info";
41
+ var ROBLOX = "https://create.roblox.com/docs/art/modeling/material-reference";
42
+ var PBR_GUIDE = "https://www.adobe.com/learn/substance-3d-designer/web/the-pbr-guide-part-2";
43
+ var MATERIAL_LIBRARY2 = {
44
+ oak: {
45
+ id: "oak",
46
+ kind: "planks",
47
+ scaleMetres: 0.6,
48
+ roughness: 0.68,
49
+ albedo: "#a27b52",
50
+ albedo2: "#6a482b",
51
+ params: { width: 0.14, rings: 0.55, rays: 0.6, knots: 0.6 },
52
+ description: "Oak boards: warm brown, dark latewood rings, pale ray flecks.",
53
+ sources: [PBR_GUIDE, ROBLOX]
54
+ },
55
+ pine: {
56
+ id: "pine",
57
+ kind: "planks",
58
+ scaleMetres: 0.6,
59
+ roughness: 0.72,
60
+ albedo: "#d2b183",
61
+ albedo2: "#9b6a3c",
62
+ params: { width: 0.16, rings: 0.75, knots: 2.5 },
63
+ description: "Pine boards: pale yellow, strong orange latewood, frequent knots.",
64
+ sources: [PBR_GUIDE, ROBLOX]
65
+ },
66
+ wood_weathered: {
67
+ id: "wood_weathered",
68
+ kind: "planks",
69
+ scaleMetres: 0.6,
70
+ roughness: 0.86,
71
+ albedo: "#9b968c",
72
+ albedo2: "#4f463c",
73
+ params: { width: 0.2, rings: 0.35, checks: 1, knots: 1.5, grime: 0.15 },
74
+ description: "Barn-grey weathered boards: silvered, eroded grain, open checks.",
75
+ sources: [ROBLOX]
76
+ },
77
+ wood_painted: {
78
+ id: "wood_painted",
79
+ kind: "planks",
80
+ scaleMetres: 0.6,
81
+ roughness: 0.84,
82
+ albedo: "#ededed",
83
+ albedo2: "#a08c74",
84
+ params: { width: 0.14, paint: 1, paintRough: 0.62, chips: 0.38, knots: 0.8, grime: 0.3 },
85
+ description: "Painted boards (vertex colour = paint) worn through to grey wood at the edges.",
86
+ sources: [ROBLOX]
87
+ },
88
+ iron_rust: {
89
+ id: "iron_rust",
90
+ kind: "metal",
91
+ scaleMetres: 0.5,
92
+ roughness: 0.55,
93
+ metalness: 0.9,
94
+ albedo: "#8e8a85",
95
+ albedo2: "#764323",
96
+ params: { oxide: 0.62, cast: 0.25, streaks: 0.5 },
97
+ description: "Weathered wrought/rolled iron, rust blooming in patches and runs.",
98
+ sources: [PB, ROBLOX]
99
+ },
100
+ iron_cast: {
101
+ id: "iron_cast",
102
+ kind: "metal",
103
+ scaleMetres: 0.5,
104
+ roughness: 0.68,
105
+ metalness: 0.75,
106
+ albedo: "#6c6966",
107
+ albedo2: "#55372a",
108
+ params: { cast: 1, oxide: 0.35 },
109
+ description: "Sand-cast iron (wheels, frames): dark pitted skin, dull oxide.",
110
+ sources: [PB]
111
+ },
112
+ steel_painted: {
113
+ id: "steel_painted",
114
+ kind: "metal",
115
+ scaleMetres: 0.5,
116
+ roughness: 0.4,
117
+ metalness: 1,
118
+ albedo: "#eeeeee",
119
+ albedo2: "#5a3b2a",
120
+ params: { paint: 1, paintRough: 0.42, chips: 0.2, streaks: 0.2, grime: 0.2 },
121
+ description: "Painted steel (vertex colour = paint): chips show rusty steel beneath.",
122
+ sources: [ROBLOX]
123
+ },
124
+ steel_galvanised: {
125
+ id: "steel_galvanised",
126
+ kind: "metal",
127
+ scaleMetres: 0.5,
128
+ roughness: 0.42,
129
+ metalness: 1,
130
+ albedo: "#d5dadc",
131
+ albedo2: "#9a9c98",
132
+ params: { spangle: 1, oxide: 0.12 },
133
+ description: "Hot-dip galvanised steel: zinc spangle, white-rust bloom.",
134
+ sources: [PB]
135
+ },
136
+ brass: {
137
+ id: "brass",
138
+ kind: "metal",
139
+ scaleMetres: 0.5,
140
+ roughness: 0.28,
141
+ metalness: 1,
142
+ albedo: "#f5e5ae",
143
+ albedo2: "#6b5a3a",
144
+ params: { brush: 0.3, oxide: 0.1 },
145
+ description: "Brass fittings: warm gold reflectance, dark tarnish in patches.",
146
+ sources: [PB, ROBLOX]
147
+ },
148
+ copper: {
149
+ id: "copper",
150
+ kind: "metal",
151
+ scaleMetres: 0.5,
152
+ roughness: 0.32,
153
+ metalness: 1,
154
+ albedo: "#f2b89c",
155
+ albedo2: "#6fa58f",
156
+ params: { oxide: 0.15 },
157
+ description: "Copper: salmon reflectance with verdigris patches.",
158
+ sources: [PB]
159
+ },
160
+ aluminium: {
161
+ id: "aluminium",
162
+ kind: "metal",
163
+ scaleMetres: 0.5,
164
+ roughness: 0.34,
165
+ metalness: 1,
166
+ albedo: "#e6e8e9",
167
+ params: { brush: 1 },
168
+ description: "Brushed aluminium: bright, cool, streaked along U.",
169
+ sources: [PB]
170
+ },
171
+ chrome: {
172
+ id: "chrome",
173
+ kind: "metal",
174
+ scaleMetres: 0.5,
175
+ roughness: 0.07,
176
+ metalness: 1,
177
+ albedo: "#d3d9dc",
178
+ description: "Chrome plate: near-mirror.",
179
+ sources: [PB]
180
+ },
181
+ rubber: {
182
+ id: "rubber",
183
+ kind: "noise",
184
+ scaleMetres: 0.5,
185
+ roughness: 0.78,
186
+ albedo: "#2c2c2c",
187
+ params: { tone: 0.95, mottle: 0.06, relief: 0.0002, size: 0.05, grime: 0.2 },
188
+ description: "Vulcanised rubber (tyres, hoses, seals).",
189
+ sources: [PB, ROBLOX]
190
+ },
191
+ canvas: {
192
+ id: "canvas",
193
+ kind: "weave",
194
+ scaleMetres: 1,
195
+ roughness: 0.88,
196
+ albedo: "#cbbea2",
197
+ albedo2: "#8a7c62",
198
+ params: { pitch: 0.004, folds: 0.6, stains: 0.35 },
199
+ description: "Cotton duck canvas: natural, weave relief, folds and water stains.",
200
+ sources: [ROBLOX]
201
+ },
202
+ leather: {
203
+ id: "leather",
204
+ kind: "leather",
205
+ scaleMetres: 0.5,
206
+ roughness: 0.58,
207
+ albedo: "#7c5438",
208
+ albedo2: "#3e281a",
209
+ params: { creases: 0.5, wear: 0.4 },
210
+ description: "Worn brown leather: pebble grain, dark creases, burnished wear.",
211
+ sources: [ROBLOX]
212
+ },
213
+ plastic: {
214
+ id: "plastic",
215
+ kind: "noise",
216
+ scaleMetres: 0.5,
217
+ roughness: 0.35,
218
+ albedo: "#ececec",
219
+ params: { tone: 0.97, mottle: 0.01, relief: 0.00004, size: 0.2 },
220
+ description: "Moulded plastic (vertex colour = the plastic colour): satin, even.",
221
+ sources: [ROBLOX]
222
+ }
223
+ };
224
+
225
+ // src/material/palette.ts
226
+ function hex2(value) {
227
+ const n = Number.parseInt(value.replace("#", ""), 16);
228
+ const lin = (c) => {
229
+ const s = c / 255;
230
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
231
+ };
232
+ return [lin(n >> 16 & 255), lin(n >> 8 & 255), lin(n & 255)];
233
+ }
234
+ var NATURAL2 = [1, 1, 1];
235
+ function definePalette2(entries) {
236
+ return Object.fromEntries(Object.entries(entries).map(([k, v]) => [k, hex2(v)]));
237
+ }
238
+ var palettes2 = {
239
+ victorianRail: definePalette2({
240
+ paint: "#2f4a3a",
241
+ trim: "#b08d57",
242
+ iron: "#2b2b2e",
243
+ brass: "#c9a24a",
244
+ red: "#7a2a22"
245
+ }),
246
+ artDeco: definePalette2({
247
+ limestone: "#d8cfbd",
248
+ steel: "#9aa3ad",
249
+ glass: "#4a6a80",
250
+ bronze: "#8a6a3c"
251
+ }),
252
+ modernGlass: definePalette2({
253
+ glass: "#6f8fa8",
254
+ aluminium: "#c4c9cf",
255
+ concrete: "#a7a39b"
256
+ }),
257
+ harbour: definePalette2({ hull: "#1f2f45", deck: "#8a6f4e", white: "#e8e6df", red: "#a1302a" })
258
+ };
259
+
260
+ // src/material/texture-source.ts
261
+ var SHA = /^[0-9a-f]{64}$/;
262
+ var provided = new Map;
263
+ function provideTextures2(cache) {
264
+ for (const [k, v] of cache)
265
+ provided.set(k, v);
266
+ }
267
+ function textureBytes(file, where) {
268
+ const hit = provided.get(file.sha256);
269
+ if (hit)
270
+ return hit;
271
+ throw new Ts23dError2("T3D_TEXTURE_MISSING", {
272
+ cause: `${where}: texture ${file.url} (sha256 ${file.sha256.slice(0, 12)}…) is not in the cache`,
273
+ fix: "run `bun run textures <module defining the material>` once (downloads + verifies into node_modules/.cache/ts23d/textures), then provideTextures(await loadTextureCache()) before painting"
274
+ });
275
+ }
276
+ function validateTexture(id, t) {
277
+ const fail = (cause, fix) => {
278
+ throw new Ts23dError2("T3D_MATERIAL_INVALID", { cause: `material '${id}': ${cause}`, fix });
279
+ };
280
+ if (!["polyhaven", "ambientcg", "file"].includes(t.source))
281
+ fail(`texture.source '${t.source}'`, "use 'polyhaven', 'ambientcg' or 'file'");
282
+ if (t.license !== "CC0-1.0")
283
+ fail(`texture.license '${t.license}'`, "only CC0-1.0 sets are accepted (Poly Haven and ambientCG are CC0)");
284
+ if (!(t.metres.length === 2 && t.metres.every((m) => Number.isFinite(m) && m > 0)))
285
+ fail(`texture.metres [${t.metres}]`, "give the real size the image covers, e.g. [1.5, 1.5]");
286
+ for (const [map, f] of Object.entries(t.maps)) {
287
+ if (!f)
288
+ continue;
289
+ if (!SHA.test(f.sha256))
290
+ fail(`texture.maps.${map}.sha256 '${f.sha256}'`, "lower-case hex sha256 of the file (sha256sum)");
291
+ if (!/\.png$/i.test(f.url))
292
+ fail(`texture.maps.${map}.url '${f.url}'`, "use the PNG download (8- or 16-bit); JPEG is not decoded");
293
+ }
294
+ }
295
+ var creditOf2 = (t) => `${t.id}${t.author ? ` by ${t.author}` : ""} (${t.source}, ${t.license}) ${t.page}`;
296
+
297
+ // src/material/material-def.ts
298
+ var ID = /^[a-z][a-z0-9_]{1,31}$/;
299
+ var unit = (v) => Number.isFinite(v) && v >= 0 && v <= 1;
300
+ function fail(id, cause, fix) {
301
+ throw new Ts23dError2("T3D_MATERIAL_INVALID", { cause: `material '${id}': ${cause}`, fix });
302
+ }
303
+ function colour(id, key, v) {
304
+ if (v === undefined)
305
+ return;
306
+ if (typeof v === "string") {
307
+ if (!/^#?[0-9a-fA-F]{6}$/.test(v))
308
+ fail(id, `${key} '${v}' is not a hex colour`, "use '#rrggbb'");
309
+ return hex2(v);
310
+ }
311
+ if (!(v.length === 3 && v.every(unit)))
312
+ fail(id, `${key} [${v}] is not linear RGB 0..1`, "use '#rrggbb' or three numbers in 0..1");
313
+ return v;
314
+ }
315
+ function toStripRecipe(d) {
316
+ const id = String(d.id);
317
+ if (!ID.test(id))
318
+ fail(id, "id is not snake_case (2–32 chars, starts with a letter)", "e.g. 'teak_deck'");
319
+ if (!unit(d.roughness))
320
+ fail(id, `roughness ${d.roughness}`, "roughness is 0..1");
321
+ if (d.metalness !== undefined && !unit(d.metalness))
322
+ fail(id, `metalness ${d.metalness}`, "metalness is 0..1 (0 dielectric, 1 metal)");
323
+ const scale = d.scaleMetres ?? 1;
324
+ if (!(Number.isFinite(scale) && scale > 0))
325
+ fail(id, `scaleMetres ${scale}`, "scaleMetres is the face height in metres, > 0");
326
+ if (d.paint !== undefined && typeof d.paint !== "function")
327
+ fail(id, "paint is not a function", "paint is (surface, kit) => void");
328
+ for (const [k, v] of Object.entries(d.params ?? {}))
329
+ if (!Number.isFinite(v))
330
+ fail(id, `params.${k} is ${v}`, "params are finite numbers");
331
+ if (d.texture)
332
+ validateTexture(id, d.texture);
333
+ const albedo = colour(id, "albedo", d.albedo);
334
+ const albedo2 = colour(id, "albedo2", d.albedo2);
335
+ return {
336
+ kind: d.kind ?? "noise",
337
+ scaleMetres: scale,
338
+ roughness: d.roughness,
339
+ ...d.metalness !== undefined ? { metalness: d.metalness } : {},
340
+ ...d.params ? { params: d.params } : {},
341
+ ...albedo ? { albedo } : {},
342
+ ...albedo2 ? { albedo2 } : {},
343
+ ...d.paint ? { paint: d.paint } : {},
344
+ ...d.texture ? { texture: d.texture } : {}
345
+ };
346
+ }
347
+
348
+ // src/material/paint/default-sheet.ts
349
+ var leaf = { kind: "flat", scaleMetres: 1, roughness: 0.6 };
350
+ var library = Object.fromEntries(Object.values(MATERIAL_LIBRARY2).map((d) => [d.id, toStripRecipe(d)]));
351
+ var HOUSE_STRIPS = {
352
+ plain: { kind: "noise", scaleMetres: 1, roughness: 0.6 },
353
+ brick: { kind: "bond", scaleMetres: 0.45, roughness: 0.85, params: { grime: 0.2 } },
354
+ stone: {
355
+ kind: "bond",
356
+ scaleMetres: 0.9,
357
+ roughness: 0.9,
358
+ params: {
359
+ course: 0.22,
360
+ length: 0.4,
361
+ mortar: 0.016,
362
+ depth: 0.012,
363
+ bevel: 0.025,
364
+ irregular: 0.5,
365
+ relief: 0.004,
366
+ variation: 0.3,
367
+ grime: 0.2
368
+ }
369
+ },
370
+ slate: { kind: "slate", scaleMetres: 0.66, roughness: 0.7 },
371
+ iron: { kind: "plates", scaleMetres: 0.5, roughness: 0.55, metalness: 0 },
372
+ wood: { kind: "planks", scaleMetres: 0.6, roughness: 0.75 },
373
+ glass: { kind: "glass", scaleMetres: 1, roughness: 0.05 },
374
+ concrete: { kind: "concrete", scaleMetres: 1, roughness: 0.9, params: { grime: 0.3 } },
375
+ ...library
376
+ };
377
+ var DEFAULT_SHEET2 = {
378
+ seed: 1,
379
+ size: 2048,
380
+ strips: HOUSE_STRIPS,
381
+ cells: {
382
+ window_sash: { kind: "noise", scaleMetres: 1, roughness: 0.5 },
383
+ door_panel: { kind: "noise", scaleMetres: 1, roughness: 0.6 },
384
+ clock: { kind: "noise", scaleMetres: 1, roughness: 0.3 },
385
+ smokebox: {
386
+ kind: "plates",
387
+ scaleMetres: 1,
388
+ roughness: 0.6,
389
+ metalness: 0.2,
390
+ params: { rust: 0.3 }
391
+ },
392
+ sign: { kind: "noise", scaleMetres: 1, roughness: 0.5 },
393
+ porthole: { kind: "plates", scaleMetres: 1, roughness: 0.5 },
394
+ foliage_broadleaf: leaf,
395
+ foliage_maple: leaf,
396
+ foliage_maple_autumn: leaf,
397
+ foliage_birch: leaf,
398
+ foliage_acacia: leaf,
399
+ foliage_palm: leaf,
400
+ foliage_spruce: leaf,
401
+ foliage_spruce_snow: leaf,
402
+ foliage_willow: leaf,
403
+ foliage_reeds: leaf,
404
+ foliage_needles: leaf
405
+ }
406
+ };
407
+
408
+ // src/material/slots.ts
409
+ var custom = [];
410
+ var customIndex = (id) => custom.indexOf(id);
411
+ var customIds = () => custom;
412
+ function pushCustom(id) {
413
+ custom.push(id);
414
+ return custom.length - 1;
415
+ }
416
+ function clearCustom() {
417
+ custom.length = 0;
418
+ }
419
+
420
+ // src/material/trim-sheet.ts
421
+ var STRIP_ROWS2 = 28;
422
+ var STRIP_H = 1 / 32;
423
+ var classic = [
424
+ "plain",
425
+ "brick",
426
+ "stone",
427
+ "slate",
428
+ "iron",
429
+ "wood",
430
+ "glass",
431
+ "concrete"
432
+ ];
433
+ var material = [
434
+ "oak",
435
+ "pine",
436
+ "wood_weathered",
437
+ "wood_painted",
438
+ "iron_rust",
439
+ "iron_cast",
440
+ "steel_painted",
441
+ "steel_galvanised",
442
+ "brass",
443
+ "copper",
444
+ "aluminium",
445
+ "chrome",
446
+ "rubber",
447
+ "canvas",
448
+ "leather",
449
+ "plastic"
450
+ ];
451
+ var strips = [...classic, ...material];
452
+ var CUSTOM_STRIP_SLOTS2 = STRIP_ROWS2 - strips.length;
453
+ var decals = [
454
+ "window_sash",
455
+ "door_panel",
456
+ "clock",
457
+ "smokebox",
458
+ "sign",
459
+ "porthole"
460
+ ];
461
+ var foliage = [
462
+ "foliage_broadleaf",
463
+ "foliage_maple",
464
+ "foliage_maple_autumn",
465
+ "foliage_birch",
466
+ "foliage_acacia",
467
+ "foliage_palm",
468
+ "foliage_spruce",
469
+ "foliage_spruce_snow",
470
+ "foliage_willow",
471
+ "foliage_reeds",
472
+ "foliage_needles"
473
+ ];
474
+ var cells = [...decals, ...foliage];
475
+ var CELL = 1 / 16;
476
+ var CELL_V0 = STRIP_ROWS2 * STRIP_H;
477
+ var FOLIAGE_SLOT = 8;
478
+ var slotOf = (id) => {
479
+ const d = decals.indexOf(id);
480
+ return d >= 0 ? d : FOLIAGE_SLOT + foliage.indexOf(id);
481
+ };
482
+ var TRIM_SHEET_SIZE2 = 2048;
483
+ function stripRow2(id) {
484
+ const b = strips.indexOf(id);
485
+ if (b >= 0)
486
+ return b;
487
+ const c = customIndex(id);
488
+ return c >= 0 ? strips.length + c : -1;
489
+ }
490
+ function stripRect2(id) {
491
+ const i = stripRow2(id);
492
+ if (i < 0)
493
+ throw new Ts23dError2("T3D_MATERIAL_UNKNOWN", {
494
+ cause: `strip '${id}' is neither built in nor defined`,
495
+ fix: `use a built-in strip (${strips.join(", ")}) or register it first with defineMaterial({ id: '${id}', ... })`
496
+ });
497
+ return { u0: 0, v0: i * STRIP_H, u1: 1, v1: (i + 1) * STRIP_H, metres: 2 };
498
+ }
499
+ function cellRect2(id) {
500
+ const i = slotOf(id);
501
+ const u0 = i % 16 * CELL;
502
+ const v0 = CELL_V0 + Math.floor(i / 16) * CELL;
503
+ return { u0, v0, u1: u0 + CELL, v1: v0 + CELL, metres: 1 };
504
+ }
505
+ var STRIPS2 = strips;
506
+ var CLASSIC_STRIPS2 = classic;
507
+ var MATERIAL_STRIPS2 = material;
508
+ var CELLS2 = cells;
509
+ var DECAL_CELLS2 = decals;
510
+ var FOLIAGE_CELLS2 = foliage;
511
+ var isFoliage2 = (id) => id.startsWith("foliage_");
512
+
513
+ // src/material/define.ts
514
+ var defined = new Map;
515
+ function same(a, b) {
516
+ return JSON.stringify({ ...a, paint: 0 }) === JSON.stringify({ ...b, paint: 0 }) && a.paint === b.paint;
517
+ }
518
+ function defineMaterial2(def) {
519
+ const recipe = toStripRecipe(def);
520
+ const id = def.id;
521
+ if (STRIPS2.includes(id))
522
+ throw new Ts23dError2("T3D_MATERIAL_CONFLICT", {
523
+ cause: `material '${id}' is a built-in strip`,
524
+ fix: `pick another id (e.g. '${id}_custom'), or restyle the built-in via paintTrimSheet({ ...DEFAULT_SHEET, strips: { ...DEFAULT_SHEET.strips, ${id}: … } })`
525
+ });
526
+ const prior = defined.get(id);
527
+ if (prior) {
528
+ if (same(prior, def))
529
+ return id;
530
+ throw new Ts23dError2("T3D_MATERIAL_CONFLICT", {
531
+ cause: `material '${id}' is already defined differently`,
532
+ fix: "define each material once in a shared module and import it, or give the variant its own id"
533
+ });
534
+ }
535
+ if (customIds().length >= CUSTOM_STRIP_SLOTS2)
536
+ throw new Ts23dError2("T3D_SHEET_FULL", {
537
+ cause: `the trim sheet has ${CUSTOM_STRIP_SLOTS2} custom strip rows, all taken (${customIds().join(", ")})`,
538
+ fix: "reuse a built-in strip with a vertex colour, or restyle an unused built-in strip in your SheetRecipe"
539
+ });
540
+ defined.set(id, def);
541
+ pushCustom(id);
542
+ HOUSE_STRIPS[id] = recipe;
543
+ return id;
544
+ }
545
+ function resetMaterials2() {
546
+ for (const id of defined.keys())
547
+ delete HOUSE_STRIPS[id];
548
+ defined.clear();
549
+ clearCustom();
550
+ }
551
+ function materialOf2(id) {
552
+ const recipe = HOUSE_STRIPS[id];
553
+ const lib = MATERIAL_LIBRARY2[id];
554
+ const def = lib ?? defined.get(id);
555
+ if (!recipe || customIndex(id) < 0 && !STRIPS2.includes(id))
556
+ throw new Ts23dError2("T3D_MATERIAL_UNKNOWN", {
557
+ cause: `strip '${id}' is neither built in nor defined`,
558
+ fix: `use one of ${STRIPS2.join(", ")}, or defineMaterial({ id: '${id}', ... }) first`
559
+ });
560
+ const classic = CLASSIC_STRIPS2.includes(id);
561
+ return {
562
+ id,
563
+ origin: classic ? "classic" : lib ? "library" : "defined",
564
+ recipe,
565
+ description: def?.description ?? `classic ${recipe.kind} strip (greyscale, vertex colour tints)`,
566
+ sources: def?.sources ?? []
567
+ };
568
+ }
569
+ function textureSources2() {
570
+ return [...defined.values()].flatMap((d) => d.texture ? [d.texture] : []);
571
+ }
572
+ function stripCredits2(vs) {
573
+ const out = [];
574
+ for (const [id, d] of defined) {
575
+ if (!d.texture)
576
+ continue;
577
+ const r = stripRect2(id);
578
+ for (let i = 0;i < vs.length; i++) {
579
+ const v = vs[i] ?? -1;
580
+ if (v >= r.v0 && v <= r.v1) {
581
+ out.push(creditOf2(d.texture));
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ return out;
587
+ }
588
+ // src/material/paint/noise.ts
589
+ function hash2(ix, iy, seed) {
590
+ let h = Math.imul(ix | 0, 668265261) ^ Math.imul(iy | 0, 374761393) ^ seed;
591
+ h = Math.imul(h ^ h >>> 15, 2246822507);
592
+ h = Math.imul(h ^ h >>> 13, 3266489909);
593
+ return ((h ^ h >>> 16) >>> 0) / 4294967296;
594
+ }
595
+ function valueNoise(x, y, period, seed) {
596
+ const xi = Math.floor(x);
597
+ const yi = Math.floor(y);
598
+ const fx = x - xi;
599
+ const fy = y - yi;
600
+ const sx = fx * fx * (3 - 2 * fx);
601
+ const sy = fy * fy * (3 - 2 * fy);
602
+ const x0 = (xi % period + period) % period;
603
+ const x1 = (x0 + 1) % period;
604
+ const a = hash2(x0, yi, seed);
605
+ const b = hash2(x1, yi, seed);
606
+ const c = hash2(x0, yi + 1, seed);
607
+ const d = hash2(x1, yi + 1, seed);
608
+ return a + (b - a) * sx + (c - a + (d - c - b + a) * sx) * sy;
609
+ }
610
+ function fbm(x, y, period, seed, octaves = 4, gain = 0.5) {
611
+ let sum = 0;
612
+ let amp = 1;
613
+ let norm = 0;
614
+ let f = 1;
615
+ for (let o = 0;o < octaves; o++) {
616
+ sum += amp * valueNoise(x * f, y * f, period * f, seed + o * 1013);
617
+ norm += amp;
618
+ amp *= gain;
619
+ f *= 2;
620
+ }
621
+ return sum / norm * 2 - 1;
622
+ }
623
+
624
+ // src/material/paint/canvas.ts
625
+ function createCanvas(w, h, metresU, metresV, wrapU) {
626
+ const n = w * h;
627
+ return {
628
+ w,
629
+ h,
630
+ metresU,
631
+ metresV,
632
+ wrapU,
633
+ height: new Float32Array(n),
634
+ tone: new Float32Array(n).fill(1),
635
+ rough: new Float32Array(n).fill(0.8),
636
+ metal: new Float32Array(n),
637
+ occl: new Float32Array(n).fill(1),
638
+ mix: new Float32Array(n),
639
+ colours: null,
640
+ rgba: null,
641
+ normalRgba: null
642
+ };
643
+ }
644
+ function periodFor(c, featureMetres) {
645
+ return c.wrapU ? Math.max(1, Math.round(c.metresU / featureMetres)) : 1 << 20;
646
+ }
647
+ function noiseAt(c, xm, ym, sizeU, sizeV, seed, octaves = 4) {
648
+ const finest = 1.5 * Math.max(c.metresU / c.w, c.metresV / c.h);
649
+ const k = Math.max(1, finest / Math.min(sizeU, sizeV));
650
+ const period = periodFor(c, sizeU * k);
651
+ const cell = c.wrapU ? c.metresU / period : sizeU * k;
652
+ return fbm(xm / cell, ym / (sizeV * k), period, seed, octaves);
653
+ }
654
+ var param = (params, key, fallback) => params?.[key] ?? fallback;
655
+ var clamp01 = (v) => Math.min(1, Math.max(0, v));
656
+ var smooth = (e0, e1, v) => {
657
+ const t = clamp01((v - e0) / (e1 - e0));
658
+ return t * t * (3 - 2 * t);
659
+ };
660
+
661
+ // src/material/paint/shapes.ts
662
+ function segment(px, py, ax, ay, bx, by) {
663
+ const dx = bx - ax;
664
+ const dy = by - ay;
665
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)));
666
+ return Math.hypot(px - ax - t * dx, py - ay - t * dy);
667
+ }
668
+ function rect(px, py, x0, y0, x1, y1) {
669
+ const qx = Math.max(x0 - px, px - x1);
670
+ const qy = Math.max(y0 - py, py - y1);
671
+ return qx > 0 || qy > 0 ? Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) : Math.max(qx, qy);
672
+ }
673
+ var dome = (d, r) => d < r ? Math.sqrt(1 - (d / r) ** 2) : 0;
674
+ function boltRing(px, py, ring, n, phase = 0) {
675
+ const a = Math.atan2(py, px) - phase;
676
+ const step = Math.PI * 2 / n;
677
+ const k = Math.round(a / step);
678
+ const ba = k * step + phase;
679
+ return Math.hypot(px - ring * Math.cos(ba), py - ring * Math.sin(ba));
680
+ }
681
+
682
+ // src/material/paint/cells.ts
683
+ var PANE_TONE = 0.12;
684
+ function paintWindowSash(c, r, seed) {
685
+ const cols = param(r.params, "cols", 2);
686
+ const rows = param(r.params, "rows", 2);
687
+ const frame = param(r.params, "frame", 0.07);
688
+ const bar = param(r.params, "bar", 0.022);
689
+ const meet = 0.5;
690
+ const pu = c.metresU / c.w;
691
+ const pv = c.metresV / c.h;
692
+ for (let y = 0;y < c.h; y++) {
693
+ const ym = (y + 0.5) * pv;
694
+ const lower = ym < meet;
695
+ const y0 = lower ? frame : meet + bar;
696
+ const y1 = lower ? meet - bar : 1 - frame;
697
+ for (let x = 0;x < c.w; x++) {
698
+ const xm = (x + 0.5) * pu;
699
+ const i = y * c.w + x;
700
+ const fx = (xm - frame) / (1 - 2 * frame) * cols;
701
+ const fy = (ym - y0) / (y1 - y0) * rows;
702
+ const inX = Math.min(fx - Math.floor(fx), Math.ceil(fx) - fx) * ((1 - 2 * frame) / cols);
703
+ const inY = Math.min(fy - Math.floor(fy), Math.ceil(fy) - fy) * ((y1 - y0) / rows);
704
+ const open = -rect(xm, ym, frame, y0, 1 - frame, y1);
705
+ const pane = Math.min(open, inX - bar / 2, inY - bar / 2);
706
+ const wood = noiseAt(c, xm, ym, 0.3, 0.02, seed, 3);
707
+ if (pane > 0) {
708
+ const sky = noiseAt(c, xm, ym, 0.6, 0.6, seed + 1, 2);
709
+ c.height[i] = -0.012;
710
+ c.tone[i] = PANE_TONE + 0.05 * (ym - y0) + 0.03 * sky;
711
+ c.rough[i] = 0.05;
712
+ c.metal[i] = 0;
713
+ } else {
714
+ const bevel = smooth(0, 0.01, -pane);
715
+ c.height[i] = -0.012 * (1 - bevel) + 0.0003 * wood;
716
+ c.tone[i] = 0.9 + 0.03 * wood;
717
+ c.rough[i] = r.roughness;
718
+ }
719
+ }
720
+ }
721
+ }
722
+ function paintDoorPanel(c, r, seed) {
723
+ const stile = param(r.params, "stile", 0.12);
724
+ const muntin = stile * 0.7;
725
+ const pu = c.metresU / c.w;
726
+ const pv = c.metresV / c.h;
727
+ const midX = 0.5;
728
+ const panels = [
729
+ [stile, 0.2, midX - muntin / 2, 0.42],
730
+ [midX + muntin / 2, 0.2, 1 - stile, 0.42],
731
+ [stile, 0.58, midX - muntin / 2, 1 - stile],
732
+ [midX + muntin / 2, 0.58, 1 - stile, 1 - stile]
733
+ ];
734
+ for (let y = 0;y < c.h; y++) {
735
+ const ym = (y + 0.5) * pv;
736
+ for (let x = 0;x < c.w; x++) {
737
+ const xm = (x + 0.5) * pu;
738
+ const i = y * c.w + x;
739
+ let d = 1;
740
+ for (const [x0, y0, x1, y1] of panels)
741
+ d = Math.min(d, rect(xm, ym, x0, y0, x1, y1));
742
+ const grain = noiseAt(c, xm, ym, 0.02, 0.4, seed, 3);
743
+ const recess = d < 0 ? 1 : 1 - smooth(0, 0.006, d);
744
+ const field = smooth(0.005, 0.04, -d);
745
+ c.height[i] = -0.012 * recess + 0.008 * field + 0.0003 * grain;
746
+ c.tone[i] = (0.8 + 0.08 * grain) * (d < 0 && d > -0.008 ? 0.65 : d < 0 ? 0.92 : 1);
747
+ c.rough[i] = r.roughness + 0.05 * grain;
748
+ }
749
+ }
750
+ }
751
+ function paintSign(c, r, _seed) {
752
+ const border = param(r.params, "border", 0.05);
753
+ const pu = c.metresU / c.w;
754
+ const pv = c.metresV / c.h;
755
+ const screws = [0.07, 0.93];
756
+ for (let y = 0;y < c.h; y++) {
757
+ const ym = (y + 0.5) * pv;
758
+ for (let x = 0;x < c.w; x++) {
759
+ const xm = (x + 0.5) * pu;
760
+ const i = y * c.w + x;
761
+ const inner = -rect(xm, ym, border, border, 1 - border, 1 - border);
762
+ const edge = Math.min(xm, ym, 1 - xm, 1 - ym);
763
+ const moulding = inner < 0 ? dome(Math.abs(-inner - border / 2), border / 2) : 0;
764
+ let screw = 0;
765
+ for (const sx of screws)
766
+ for (const sy of screws)
767
+ screw = Math.max(screw, dome(Math.hypot(xm - sx, ym - sy), 0.012));
768
+ c.height[i] = 0.006 * moulding + 0.002 * screw - 0.002 * (1 - smooth(0, 0.004, edge));
769
+ c.tone[i] = inner > 0 ? 0.95 : 0.85 + 0.1 * moulding;
770
+ if (screw > 0) {
771
+ c.tone[i] = 0.6;
772
+ c.metal[i] = 1;
773
+ c.rough[i] = 0.35;
774
+ }
775
+ }
776
+ }
777
+ }
778
+
779
+ // src/material/paint/cells-round.ts
780
+ var TAU = Math.PI * 2;
781
+ function hand(px, py, hours, len) {
782
+ const a = hours / 12 * TAU;
783
+ return segment(px, py, 0, 0, Math.sin(a) * len, Math.cos(a) * len);
784
+ }
785
+ function paintClock(c, r, _seed) {
786
+ const pu = c.metresU / c.w;
787
+ const pv = c.metresV / c.h;
788
+ for (let y = 0;y < c.h; y++) {
789
+ const py = (y + 0.5) * pv - 0.5;
790
+ for (let x = 0;x < c.w; x++) {
791
+ const px = (x + 0.5) * pu - 0.5;
792
+ const i = y * c.w + x;
793
+ const d = Math.hypot(px, py);
794
+ if (d > 0.48)
795
+ continue;
796
+ const i0 = i;
797
+ if (d > 0.42) {
798
+ c.height[i0] = 0.012 * dome(Math.abs(d - 0.45), 0.03);
799
+ c.tone[i0] = 0.55;
800
+ c.metal[i0] = 1;
801
+ c.rough[i0] = 0.35;
802
+ continue;
803
+ }
804
+ const a = Math.atan2(px, py);
805
+ const minute = Math.abs((a / TAU * 60 + 60.5) % 1 - 0.5) * (TAU / 60) * d;
806
+ const hour = Math.abs((a / TAU * 12 + 12.5) % 1 - 0.5) * (TAU / 12) * d;
807
+ const tickM = d > 0.38 && d < 0.41 && minute < 0.003 ? 1 : 0;
808
+ const tickH = d > 0.32 && d < 0.41 && hour < 0.008 ? 1 : 0;
809
+ const hands = Math.min(hand(px, py, 10 + 10 / 60, 0.22) - 0.012, hand(px, py, 2, 0.34) - 0.007);
810
+ const ink = Math.max(tickM, tickH, hands < 0 ? 1 : 0, d < 0.02 ? 1 : 0);
811
+ c.height[i0] = -0.004 + 0.002 * ink;
812
+ c.tone[i0] = ink ? 0.08 : 0.96;
813
+ c.rough[i0] = ink ? 0.5 : r.roughness;
814
+ c.metal[i0] = 0;
815
+ }
816
+ }
817
+ }
818
+ function paintSmokebox(c, r, seed) {
819
+ const pu = c.metresU / c.w;
820
+ const pv = c.metresV / c.h;
821
+ const metal = r.metalness ?? 0;
822
+ for (let y = 0;y < c.h; y++) {
823
+ const py = (y + 0.5) * pv - 0.5;
824
+ for (let x = 0;x < c.w; x++) {
825
+ const px = (x + 0.5) * pu - 0.5;
826
+ const i = y * c.w + x;
827
+ const d = Math.hypot(px, py);
828
+ if (d > 0.47)
829
+ continue;
830
+ const pit = noiseAt(c, px + 0.5, py + 0.5, 0.03, 0.03, seed, 3);
831
+ let h = 0.01 * (1 - (d / 0.47) ** 2) + 0.0004 * pit;
832
+ const flange = dome(Math.abs(d - 0.45), 0.02);
833
+ h += 0.006 * flange;
834
+ const bolt = dome(boltRing(px, py, 0.415, 28), 0.011);
835
+ h += 0.006 * bolt;
836
+ const strap = Math.min(segment(px, py, -0.47, 0.17, 0.12, 0.17), segment(px, py, -0.47, -0.17, 0.12, -0.17));
837
+ const strapH = smooth(0.024, 0.018, strap);
838
+ h = Math.max(h, 0.014 * strapH + h * (1 - strapH));
839
+ const boss = dome(d, 0.05);
840
+ const handle = smooth(0.012, 0.008, segment(px, py, -0.13, 0, 0.13, 0));
841
+ h += 0.02 * boss + 0.012 * handle;
842
+ c.height[i] = h;
843
+ c.tone[i] = (0.5 + 0.05 * pit) * (1 + 0.5 * Math.max(bolt, handle, strapH * 0.6, flange, boss));
844
+ c.rough[i] = r.roughness - 0.15 * Math.max(bolt, handle, boss);
845
+ c.metal[i] = metal + (1 - metal) * Math.max(handle, boss) * 0.8;
846
+ }
847
+ }
848
+ }
849
+ function paintPorthole(c, r, seed) {
850
+ const pu = c.metresU / c.w;
851
+ const pv = c.metresV / c.h;
852
+ for (let y = 0;y < c.h; y++) {
853
+ const py = (y + 0.5) * pv - 0.5;
854
+ for (let x = 0;x < c.w; x++) {
855
+ const px = (x + 0.5) * pu - 0.5;
856
+ const i = y * c.w + x;
857
+ const d = Math.hypot(px, py);
858
+ if (d > 0.44)
859
+ continue;
860
+ if (d < 0.29) {
861
+ const sky = noiseAt(c, px + 0.5, py + 0.5, 0.5, 0.5, seed, 2);
862
+ c.height[i] = -0.012;
863
+ c.tone[i] = PANE_TONE + 0.04 * (py + 0.3) + 0.02 * sky;
864
+ c.rough[i] = 0.05;
865
+ c.metal[i] = 0;
866
+ continue;
867
+ }
868
+ const ring = dome(Math.abs(d - 0.365), 0.075);
869
+ const bolt = dome(boltRing(px, py, 0.405, 10, Math.PI / 10), 0.018);
870
+ const lip = dome(Math.abs(d - 0.3), 0.012);
871
+ c.height[i] = 0.016 * ring + 0.008 * bolt + 0.004 * lip;
872
+ c.tone[i] = 0.75 + 0.15 * ring;
873
+ c.rough[i] = Math.max(0.2, r.roughness - 0.2 * ring);
874
+ c.metal[i] = 1;
875
+ }
876
+ }
877
+ }
878
+
879
+ // src/material/paint/foliage-common.ts
880
+ function curve(x0, y0, x1, y1, x2, y2, n = 16) {
881
+ const x = [];
882
+ const y = [];
883
+ const a = [];
884
+ for (let i = 0;i <= n; i++) {
885
+ const t = i / n;
886
+ const u = 1 - t;
887
+ x.push(u * u * x0 + 2 * u * t * x1 + t * t * x2);
888
+ y.push(u * u * y0 + 2 * u * t * y1 + t * t * y2);
889
+ const dx = 2 * u * (x1 - x0) + 2 * t * (x2 - x1);
890
+ const dy = 2 * u * (y1 - y0) + 2 * t * (y2 - y1);
891
+ a.push(Math.atan2(dx, dy));
892
+ }
893
+ return { x, y, a };
894
+ }
895
+ var at = (c, t) => Math.round(t * (c.x.length - 1));
896
+ function drawStem(f, c, w0, w1, col) {
897
+ const n = c.x.length - 1;
898
+ for (let i = 0;i < n; i++) {
899
+ const t0 = i / n;
900
+ const t1 = (i + 1) / n;
901
+ f.stem(c.x[i] ?? 0, c.y[i] ?? 0, c.x[i + 1] ?? 0, c.y[i + 1] ?? 0, w0 + (w1 - w0) * t0, w0 + (w1 - w0) * t1, col);
902
+ }
903
+ }
904
+ function vary(rng, col, amt) {
905
+ const k = 1 + amt * (rng.next() * 2 - 1);
906
+ const drift = 1 + amt * 0.6 * (rng.next() * 2 - 1);
907
+ return [col[0] * k * drift, col[1] * k, col[2] * k / drift];
908
+ }
909
+ var dim = (col, k) => [col[0] * k, col[1] * k, col[2] * k];
910
+ var LEAF = {
911
+ beech: [0.09, 0.2, 0.035],
912
+ maple: [0.1, 0.24, 0.04],
913
+ birch: [0.16, 0.3, 0.05],
914
+ acacia: [0.11, 0.2, 0.06],
915
+ palm: [0.12, 0.22, 0.05],
916
+ dry: [0.3, 0.2, 0.08],
917
+ spruce: [0.035, 0.09, 0.04],
918
+ pine: [0.06, 0.13, 0.04],
919
+ willow: [0.2, 0.3, 0.08],
920
+ reed: [0.2, 0.28, 0.08],
921
+ snow: [0.85, 0.88, 0.92],
922
+ bark: [0.08, 0.055, 0.035],
923
+ twig: [0.12, 0.09, 0.05]
924
+ };
925
+ var AUTUMN = [
926
+ [0.6, 0.08, 0.02],
927
+ [0.75, 0.25, 0.02],
928
+ [0.8, 0.45, 0.03],
929
+ [0.45, 0.04, 0.03],
930
+ [0.55, 0.35, 0.05]
931
+ ];
932
+
933
+ // src/material/paint/orm.ts
934
+ var AO_DEPTH = 0.006;
935
+ var AO_STRENGTH = 0.75;
936
+ var AO_RADIUS = 0.02;
937
+ function boxBlur(src, w, h, rx, ry, wrap) {
938
+ const tmp = new Float32Array(src.length);
939
+ const out = new Float32Array(src.length);
940
+ const ix = (i) => i < 0 ? wrap ? i % w + w : 0 : i >= w ? wrap ? i % w : w - 1 : i;
941
+ for (let y = 0;y < h; y++) {
942
+ const row = y * w;
943
+ let s = 0;
944
+ for (let k = -rx;k <= rx; k++)
945
+ s += src[row + ix(k)] ?? 0;
946
+ for (let x = 0;x < w; x++) {
947
+ tmp[row + x] = s / (2 * rx + 1);
948
+ s += (src[row + ix(x + rx + 1)] ?? 0) - (src[row + ix(x - rx)] ?? 0);
949
+ }
950
+ }
951
+ const iy = (j) => j < 0 ? 0 : j >= h ? h - 1 : j;
952
+ for (let x = 0;x < w; x++) {
953
+ let s = 0;
954
+ for (let k = -ry;k <= ry; k++)
955
+ s += tmp[iy(k) * w + x] ?? 0;
956
+ for (let y = 0;y < h; y++) {
957
+ out[y * w + x] = s / (2 * ry + 1);
958
+ s += (tmp[iy(y + ry + 1) * w + x] ?? 0) - (tmp[iy(y - ry) * w + x] ?? 0);
959
+ }
960
+ }
961
+ return out;
962
+ }
963
+ function cavityOcclusion(c) {
964
+ const rx = Math.max(1, Math.round(AO_RADIUS / (c.metresU / c.w)));
965
+ const ry = Math.max(1, Math.round(AO_RADIUS / (c.metresV / c.h)));
966
+ const blur = boxBlur(boxBlur(c.height, c.w, c.h, rx, ry, c.wrapU), c.w, c.h, rx, ry, c.wrapU);
967
+ const ao = new Float32Array(c.height.length);
968
+ for (let i = 0;i < ao.length; i++) {
969
+ const cavity = (blur[i] ?? 0) - (c.height[i] ?? 0);
970
+ const t = cavity <= 0 ? 0 : cavity >= AO_DEPTH ? 1 : cavity / AO_DEPTH;
971
+ ao[i] = (1 - AO_STRENGTH * t) * (c.occl[i] ?? 1);
972
+ }
973
+ return ao;
974
+ }
975
+ var SRGB = new Uint8Array(4096).map((_, i) => {
976
+ const l = i / 4095;
977
+ const s = l <= 0.0031308 ? l * 12.92 : 1.055 * l ** (1 / 2.4) - 0.055;
978
+ return Math.round(s * 255);
979
+ });
980
+ var byte = (v) => v <= 0 ? 0 : v >= 1 ? 255 : Math.round(v * 255);
981
+ function packOrm(c, ao) {
982
+ const out = new Uint8Array(c.w * c.h * 4);
983
+ for (let i = 0, o = 0;i < ao.length; i++, o += 4) {
984
+ out[o] = byte(ao[i] ?? 1);
985
+ out[o + 1] = byte(c.rough[i] ?? 0.8);
986
+ out[o + 2] = byte(c.metal[i] ?? 0);
987
+ out[o + 3] = 255;
988
+ }
989
+ return out;
990
+ }
991
+ var enc = (t) => SRGB[t <= 0 ? 0 : t >= 1 ? 4095 : Math.round(t * 4095)] ?? 255;
992
+ function packAlbedo(c, ao) {
993
+ if (c.rgba)
994
+ return c.rgba;
995
+ const out = new Uint8Array(c.w * c.h * 4);
996
+ const pair = c.colours;
997
+ if (pair) {
998
+ const [a, b] = pair;
999
+ for (let i = 0, o = 0;i < ao.length; i++, o += 4) {
1000
+ const t = (c.tone[i] ?? 1) * (0.8 + 0.2 * (ao[i] ?? 1));
1001
+ const m = Math.min(1, Math.max(0, c.mix[i] ?? 0));
1002
+ for (let k = 0;k < 3; k++)
1003
+ out[o + k] = enc(((a[k] ?? 1) * (1 - m) + (b[k] ?? 1) * m) * t);
1004
+ out[o + 3] = 255;
1005
+ }
1006
+ return out;
1007
+ }
1008
+ for (let i = 0, o = 0;i < ao.length; i++, o += 4) {
1009
+ const t = (c.tone[i] ?? 1) * (0.8 + 0.2 * (ao[i] ?? 1));
1010
+ const g = SRGB[t <= 0 ? 0 : t >= 1 ? 4095 : Math.round(t * 4095)] ?? 255;
1011
+ out[o] = g;
1012
+ out[o + 1] = g;
1013
+ out[o + 2] = g;
1014
+ out[o + 3] = 255;
1015
+ }
1016
+ return out;
1017
+ }
1018
+
1019
+ // src/material/paint/foliage-raster.ts
1020
+ var total = (a) => a.reduce((s, v) => s + v, 0);
1021
+ var LOBE = Math.PI * 2 / 5;
1022
+
1023
+ class Foliage {
1024
+ r;
1025
+ g;
1026
+ b;
1027
+ a;
1028
+ c;
1029
+ px;
1030
+ constructor(c) {
1031
+ this.c = c;
1032
+ const n = c.w * c.h;
1033
+ this.r = new Float32Array(n);
1034
+ this.g = new Float32Array(n);
1035
+ this.b = new Float32Array(n);
1036
+ this.a = new Float32Array(n);
1037
+ this.px = c.metresU / c.w;
1038
+ c.height.fill(0);
1039
+ }
1040
+ put(i, cov, col, shade, h, rough) {
1041
+ if (cov <= 0)
1042
+ return;
1043
+ const k = 1 - cov;
1044
+ const s = shade * cov;
1045
+ this.r[i] = (this.r[i] ?? 0) * k + col[0] * s;
1046
+ this.g[i] = (this.g[i] ?? 0) * k + col[1] * s;
1047
+ this.b[i] = (this.b[i] ?? 0) * k + col[2] * s;
1048
+ this.a[i] = (this.a[i] ?? 0) * k + cov;
1049
+ this.c.height[i] = (this.c.height[i] ?? 0) * k + h * cov;
1050
+ this.c.rough[i] = (this.c.rough[i] ?? 0.8) * k + rough * cov;
1051
+ }
1052
+ span(x0, y0, x1, y1) {
1053
+ const { w, h } = this.c;
1054
+ const p = this.px;
1055
+ return [
1056
+ Math.max(0, Math.floor(x0 / p) - 1),
1057
+ Math.max(0, Math.floor(y0 / p) - 1),
1058
+ Math.min(w - 1, Math.ceil(x1 / p) + 1),
1059
+ Math.min(h - 1, Math.ceil(y1 / p) + 1)
1060
+ ];
1061
+ }
1062
+ stem(ax, ay, bx, by, w0, w1, col) {
1063
+ const m = Math.max(w0, w1);
1064
+ const [sx0 = 0, sy0 = 0, sx1 = 0, sy1 = 0] = this.span(Math.min(ax, bx) - m, Math.min(ay, by) - m, Math.max(ax, bx) + m, Math.max(ay, by) + m);
1065
+ const lx = bx - ax;
1066
+ const ly = by - ay;
1067
+ const l2 = lx * lx + ly * ly || 1;
1068
+ for (let y = sy0;y <= sy1; y++) {
1069
+ const ym = (y + 0.5) * this.px;
1070
+ for (let x = sx0;x <= sx1; x++) {
1071
+ const xm = (x + 0.5) * this.px;
1072
+ const t = clamp01(((xm - ax) * lx + (ym - ay) * ly) / l2);
1073
+ const half = (w0 + (w1 - w0) * t) / 2;
1074
+ const d = segment(xm, ym, ax, ay, bx, by);
1075
+ const cov = clamp01(0.5 - (d - half) / this.px);
1076
+ const round = half > 0 ? Math.sqrt(Math.max(0, 1 - (d / half) ** 2)) : 0;
1077
+ this.put(y * this.c.w + x, cov, col, 1, 0.0015 + half * 0.4 * round, 0.85);
1078
+ }
1079
+ }
1080
+ }
1081
+ leaf(shape, bx, by, angle, len, wid, col, serrate = 0, lift = 0) {
1082
+ const ux = Math.sin(angle);
1083
+ const uy = Math.cos(angle);
1084
+ const tx = bx + ux * len;
1085
+ const ty = by + uy * len;
1086
+ const pad = shape === "palmate" ? len : wid;
1087
+ const [sx0 = 0, sy0 = 0, sx1 = 0, sy1 = 0] = this.span(Math.min(bx, tx) - pad, Math.min(by, ty) - pad, Math.max(bx, tx) + pad, Math.max(by, ty) + pad);
1088
+ for (let y = sy0;y <= sy1; y++) {
1089
+ const ym = (y + 0.5) * this.px - by;
1090
+ for (let x = sx0;x <= sx1; x++) {
1091
+ const xm = (x + 0.5) * this.px - bx;
1092
+ const along = xm * ux + ym * uy;
1093
+ const across = xm * uy - ym * ux;
1094
+ let sd;
1095
+ let rib;
1096
+ if (shape === "palmate") {
1097
+ const rr = Math.hypot(along - len * 0.45, across);
1098
+ const th = Math.atan2(across, along - len * 0.45);
1099
+ const lobe = 1 - Math.abs(Math.sin(2.5 * th)) ** 0.6;
1100
+ const edge = len * 0.5 * (0.35 + 0.65 * lobe) * (1 + serrate * Math.abs(Math.sin(18 * th)));
1101
+ sd = rr - edge;
1102
+ const axis = Math.round(th / LOBE) * LOBE;
1103
+ rib = rr * Math.abs(Math.sin(th - axis));
1104
+ } else {
1105
+ const t = along / len;
1106
+ if (t < 0 || t > 1) {
1107
+ sd = Math.max(-along, along - len) + Math.abs(across) * 0.5;
1108
+ } else {
1109
+ const prof = shape === "ovate" ? Math.sin(Math.PI * t ** 0.8) ** 0.9 : shape === "lance" ? Math.sin(Math.PI * t ** 0.6) : shape === "blade" ? (1 - t) ** 0.4 : 1 - 0.6 * t;
1110
+ const teeth = 1 - serrate * Math.abs(Math.sin(t * 40));
1111
+ sd = Math.abs(across) - wid / 2 * prof * teeth;
1112
+ }
1113
+ rib = Math.abs(across);
1114
+ }
1115
+ const cov = clamp01(0.5 - sd / this.px);
1116
+ if (cov <= 0)
1117
+ continue;
1118
+ const inside = clamp01(-sd / (wid * 0.5 + 0.000001));
1119
+ const vein = 1 - smooth(0, this.px * 1.5, rib);
1120
+ const shade = 0.85 + 0.15 * inside;
1121
+ this.put(y * this.c.w + x, cov, col, shade, 0.002 + lift + 0.0015 * inside + 0.0006 * vein, 0.6);
1122
+ }
1123
+ }
1124
+ }
1125
+ finish(axisX = 0.5, reach = 0.35) {
1126
+ const { w, h } = this.c;
1127
+ const n = w * h;
1128
+ const sa = total(this.a);
1129
+ const mean = sa > 0 ? [total(this.r) / sa, total(this.g) / sa, total(this.b) / sa] : [0.1, 0.2, 0.05];
1130
+ const out = new Uint8Array(n * 4);
1131
+ const enc = (v) => SRGB[Math.round(clamp01(v) * 4095)] ?? 0;
1132
+ for (let y = 0;y < h; y++) {
1133
+ for (let x = 0;x < w; x++) {
1134
+ const i = y * w + x;
1135
+ const a = this.a[i] ?? 0;
1136
+ const k = a > 0.02 ? 1 / a : 0;
1137
+ const mix = clamp01(a * 4);
1138
+ out[i * 4] = enc((this.r[i] ?? 0) * k * mix + mean[0] * (1 - mix));
1139
+ out[i * 4 + 1] = enc((this.g[i] ?? 0) * k * mix + mean[1] * (1 - mix));
1140
+ out[i * 4 + 2] = enc((this.b[i] ?? 0) * k * mix + mean[2] * (1 - mix));
1141
+ out[i * 4 + 3] = Math.round(clamp01(a) * 255);
1142
+ const d = Math.abs((x + 0.5) * this.px - axisX);
1143
+ this.c.occl[i] = 0.55 + 0.45 * smooth(0, reach, d + (y + 0.5) * this.px * 0.4);
1144
+ }
1145
+ }
1146
+ this.c.rgba = out;
1147
+ this.c.metal.fill(0);
1148
+ }
1149
+ }
1150
+
1151
+ // src/material/paint/foliage-broad.ts
1152
+ function cluster(c, seed, s) {
1153
+ const rng = createRng2(seed);
1154
+ const f = new Foliage(c);
1155
+ const leafAt = (x, y, a, k, lift) => {
1156
+ const len = s.len * rng.range(0.8, 1.15) * k;
1157
+ f.leaf(s.shape, x, y, a, len, s.wid * (len / s.len), s.colour(rng), s.serrate, lift);
1158
+ };
1159
+ for (let i = 0;i < s.back; i++) {
1160
+ const x = 0.5 + rng.range(-0.33, 0.33);
1161
+ const y = rng.range(0.3, 0.85);
1162
+ const col = dim(s.colour(rng), 0.5);
1163
+ const len = s.len * rng.range(0.8, 1.1);
1164
+ f.leaf(s.shape, x, y, rng.range(-1.4, 1.4), len, s.wid * (len / s.len), col, s.serrate);
1165
+ }
1166
+ const main = curve(0.5, 0.02, 0.5 + rng.range(-0.1, 0.1), 0.45, 0.5 + rng.range(-0.08, 0.08), 0.8);
1167
+ drawStem(f, main, 0.016, 0.004, LEAF.twig);
1168
+ for (let k = 0;k < s.twigs; k++) {
1169
+ const t = 0.22 + k / Math.max(1, s.twigs - 1) * 0.6;
1170
+ const side = k % 2 === 0 ? 1 : -1;
1171
+ const i = at(main, t);
1172
+ const bx = main.x[i] ?? 0.5;
1173
+ const by = main.y[i] ?? 0.5;
1174
+ const a = (main.a[i] ?? 0) + side * rng.range(0.7, 1.1);
1175
+ const len = 0.3 * (1 - t * 0.45);
1176
+ const ex = bx + Math.sin(a) * len;
1177
+ const ey = by + Math.cos(a) * len - s.droop * len * 0.4;
1178
+ const twig = curve(bx, by, (bx + ex) / 2, (by + ey) / 2 + len * 0.15, ex, ey, 8);
1179
+ drawStem(f, twig, 0.007, 0.002, LEAF.twig);
1180
+ for (let q = 1;q <= s.perTwig; q++) {
1181
+ const j = at(twig, q / s.perTwig);
1182
+ const alt = q % 2 === 0 ? 1 : -1;
1183
+ const ta = (twig.a[j] ?? 0) + (q === s.perTwig ? 0 : alt * rng.range(0.5, 0.9));
1184
+ leafAt(twig.x[j] ?? 0, twig.y[j] ?? 0, ta, 1 - 0.15 * (1 - q / s.perTwig), 0.002);
1185
+ }
1186
+ }
1187
+ const tip = main.x.length - 1;
1188
+ leafAt(main.x[tip] ?? 0.5, main.y[tip] ?? 0.8, main.a[tip] ?? 0, 1.1, 0.003);
1189
+ f.finish(0.5, 0.35);
1190
+ }
1191
+ function paintBroadleaf(c, seed) {
1192
+ cluster(c, seed, {
1193
+ shape: "ovate",
1194
+ len: 0.15,
1195
+ wid: 0.085,
1196
+ serrate: 0.03,
1197
+ twigs: 6,
1198
+ perTwig: 3,
1199
+ back: 16,
1200
+ droop: 0.1,
1201
+ colour: (rng) => vary(rng, LEAF.beech, 0.18)
1202
+ });
1203
+ }
1204
+ function paintMaple(c, seed) {
1205
+ cluster(c, seed, {
1206
+ shape: "palmate",
1207
+ len: 0.24,
1208
+ wid: 0.24,
1209
+ serrate: 0.04,
1210
+ twigs: 4,
1211
+ perTwig: 2,
1212
+ back: 7,
1213
+ droop: 0,
1214
+ colour: (rng) => vary(rng, LEAF.maple, 0.15)
1215
+ });
1216
+ }
1217
+ function paintMapleAutumn(c, seed) {
1218
+ cluster(c, seed, {
1219
+ shape: "palmate",
1220
+ len: 0.24,
1221
+ wid: 0.24,
1222
+ serrate: 0.04,
1223
+ twigs: 4,
1224
+ perTwig: 2,
1225
+ back: 7,
1226
+ droop: 0,
1227
+ colour: (rng) => vary(rng, rng.pick(AUTUMN), 0.12)
1228
+ });
1229
+ }
1230
+ function paintBirch(c, seed) {
1231
+ cluster(c, seed, {
1232
+ shape: "ovate",
1233
+ len: 0.085,
1234
+ wid: 0.06,
1235
+ serrate: 0.12,
1236
+ twigs: 8,
1237
+ perTwig: 5,
1238
+ back: 22,
1239
+ droop: 0.6,
1240
+ colour: (rng) => vary(rng, LEAF.birch, 0.2)
1241
+ });
1242
+ }
1243
+
1244
+ // src/material/paint/foliage-compound.ts
1245
+ function paintAcacia(c, seed) {
1246
+ const rng = createRng2(seed);
1247
+ const f = new Foliage(c);
1248
+ const rachis = curve(0.5, 0.02, 0.48 + rng.range(-0.05, 0.05), 0.5, 0.5, 0.95);
1249
+ for (let layer = 0;layer < 2; layer++) {
1250
+ const k = layer === 0 ? 0.5 : 1;
1251
+ if (layer === 1)
1252
+ drawStem(f, rachis, 0.01, 0.003, LEAF.twig);
1253
+ const pairs = layer === 0 ? 6 : 8;
1254
+ for (let p = 0;p < pairs; p++) {
1255
+ const t = 0.15 + p / pairs * 0.8 + (layer === 0 ? 0.05 : 0);
1256
+ const i = at(rachis, t);
1257
+ for (const side of [-1, 1]) {
1258
+ const a = (rachis.a[i] ?? 0) + side * rng.range(0.9, 1.2);
1259
+ const len = 0.22 * (1 - t * 0.5);
1260
+ const bx = rachis.x[i] ?? 0.5;
1261
+ const by = rachis.y[i] ?? 0.5;
1262
+ const pinna = curve(bx, by, bx + Math.sin(a) * len * 0.5, by + Math.cos(a) * len * 0.5 + 0.02, bx + Math.sin(a) * len, by + Math.cos(a) * len, 6);
1263
+ drawStem(f, pinna, 0.003, 0.0015, dim(LEAF.twig, k));
1264
+ for (let q = 1;q <= 12; q++) {
1265
+ const j = at(pinna, q / 12.5);
1266
+ const col = dim(vary(rng, LEAF.acacia, 0.15), k);
1267
+ for (const s2 of [-1, 1])
1268
+ f.leaf("ovate", pinna.x[j] ?? 0, pinna.y[j] ?? 0, (pinna.a[j] ?? 0) + s2 * 1.1, 0.03, 0.011, col, 0, layer * 0.002);
1269
+ }
1270
+ }
1271
+ }
1272
+ }
1273
+ f.finish(0.5, 0.25);
1274
+ }
1275
+ function paintPalm(c, seed) {
1276
+ const rng = createRng2(seed);
1277
+ const f = new Foliage(c);
1278
+ const rachis = curve(0.5, 0.02, 0.3, 0.7, 0.78, 0.96, 24);
1279
+ for (let layer = 0;layer < 2; layer++) {
1280
+ const k = layer === 0 ? 0.55 : 1;
1281
+ const n = 26;
1282
+ for (let q = 0;q < n; q++) {
1283
+ const t = 0.1 + q / n * 0.9 + (layer === 0 ? 0.015 : 0);
1284
+ const i = at(rachis, Math.min(1, t));
1285
+ const bx = rachis.x[i] ?? 0.5;
1286
+ const by = rachis.y[i] ?? 0.5;
1287
+ for (const side of [-1, 1]) {
1288
+ const a = (rachis.a[i] ?? 0) + side * rng.range(0.75, 1);
1289
+ const len = 0.3 * Math.sin(Math.PI * (0.2 + 0.75 * t)) * rng.range(0.85, 1.05);
1290
+ const wid = 0.03;
1291
+ f.leaf("lance", bx, by, a, len, wid, dim(vary(rng, LEAF.palm, 0.15), k), 0, layer * 0.002);
1292
+ if (rng.next() < 0.3) {
1293
+ const tx = bx + Math.sin(a) * len * 0.72;
1294
+ const ty = by + Math.cos(a) * len * 0.72;
1295
+ f.leaf("blade", tx, ty, a, len * 0.28, wid * 0.55, dim(vary(rng, LEAF.dry, 0.2), k), 0, layer * 0.002 + 0.0002);
1296
+ }
1297
+ }
1298
+ }
1299
+ if (layer === 0)
1300
+ drawStem(f, rachis, 0.018, 0.004, dim(LEAF.palm, 0.8));
1301
+ }
1302
+ f.finish(0.5, 0.2);
1303
+ }
1304
+ function paintWillow(c, seed) {
1305
+ const rng = createRng2(seed);
1306
+ const f = new Foliage(c);
1307
+ const strands = 9;
1308
+ for (let s = 0;s < strands; s++) {
1309
+ const back = s < 3;
1310
+ const k = back ? 0.55 : 1;
1311
+ const ex = 0.5 + rng.range(-0.4, 0.4);
1312
+ const strand = curve(0.5 + rng.range(-0.03, 0.03), 0.02, (0.5 + ex) / 2 + rng.range(-0.1, 0.1), 0.5, ex, rng.range(0.85, 0.97), 24);
1313
+ drawStem(f, strand, 0.006, 0.0015, dim(LEAF.twig, k));
1314
+ for (let q = 2;q < 30; q++) {
1315
+ const j = at(strand, q / 30);
1316
+ const side = q % 2 === 0 ? 1 : -1;
1317
+ const a = (strand.a[j] ?? 0) + side * rng.range(0.2, 0.45);
1318
+ const col = dim(vary(rng, LEAF.willow, 0.18), k);
1319
+ f.leaf("lance", strand.x[j] ?? 0, strand.y[j] ?? 0, a, rng.range(0.07, 0.1), 0.014, col, 0, back ? 0 : 0.002);
1320
+ }
1321
+ }
1322
+ f.finish(0.5, 0.2);
1323
+ }
1324
+
1325
+ // src/material/paint/foliage-conifer.ts
1326
+ function comb(f, rng, shoot, len, k, lift) {
1327
+ const n = shoot.x.length - 1;
1328
+ for (let i = 0;i <= n; i++) {
1329
+ for (const side of [-1, 1]) {
1330
+ const a = (shoot.a[i] ?? 0) + side * rng.range(0.7, 1);
1331
+ const col = dim(vary(rng, LEAF.spruce, 0.2), k);
1332
+ f.leaf("needle", shoot.x[i] ?? 0, shoot.y[i] ?? 0, a, len * rng.range(0.8, 1.1), 0.006, col, 0, lift);
1333
+ }
1334
+ }
1335
+ }
1336
+ function spruce(c, seed, snow) {
1337
+ const rng = createRng2(seed);
1338
+ const f = new Foliage(c);
1339
+ const axis = curve(0.5, 0.02, 0.5 + rng.range(-0.04, 0.04), 0.5, 0.5, 0.95, 60);
1340
+ const shoots = [];
1341
+ for (let s = 0;s < 8; s++) {
1342
+ const t = 0.15 + s / 8 * 0.75;
1343
+ const i = at(axis, t);
1344
+ const side = s % 2 === 0 ? 1 : -1;
1345
+ const bx = axis.x[i] ?? 0.5;
1346
+ const by = axis.y[i] ?? 0.5;
1347
+ const len = 0.32 * (1 - t * 0.6);
1348
+ const a = side * rng.range(0.9, 1.2);
1349
+ shoots.push(curve(bx, by, bx + Math.sin(a) * len * 0.5, by + Math.cos(a) * len * 0.5, bx + Math.sin(a) * len, by + Math.cos(a) * len + 0.03, 24));
1350
+ }
1351
+ for (const sh of shoots)
1352
+ comb(f, rng, sh, 0.045, 0.55, 0);
1353
+ drawStem(f, axis, 0.012, 0.004, LEAF.bark);
1354
+ for (const sh of shoots)
1355
+ drawStem(f, sh, 0.005, 0.002, LEAF.bark);
1356
+ comb(f, rng, axis, 0.05, 1, 0.002);
1357
+ for (const sh of shoots)
1358
+ comb(f, rng, sh, 0.04, 1, 0.002);
1359
+ if (snow) {
1360
+ for (const sh of [axis, ...shoots]) {
1361
+ const n = sh.x.length - 1;
1362
+ for (let i = 2;i < n; i += 3) {
1363
+ const col = vary(rng, LEAF.snow, 0.05);
1364
+ f.leaf("ovate", (sh.x[i] ?? 0) - 0.02, (sh.y[i] ?? 0) + 0.008, Math.PI / 2 + rng.range(-0.3, 0.3), rng.range(0.04, 0.07), rng.range(0.025, 0.04), col, 0, 0.006);
1365
+ }
1366
+ }
1367
+ }
1368
+ f.finish(0.5, 0.25);
1369
+ }
1370
+ var paintSpruce = (c, seed) => spruce(c, seed, false);
1371
+ var paintSpruceSnow = (c, seed) => spruce(c, seed, true);
1372
+ function paintNeedles(c, seed) {
1373
+ const rng = createRng2(seed);
1374
+ const f = new Foliage(c);
1375
+ const twigs = [];
1376
+ for (let s = 0;s < 5; s++) {
1377
+ const ex = 0.5 + (s - 2) * 0.13 + rng.range(-0.03, 0.03);
1378
+ twigs.push(curve(0.5, 0.02, 0.5 + (ex - 0.5) * 0.4, 0.3, ex, rng.range(0.55, 0.7), 12));
1379
+ }
1380
+ for (const [s, tw] of twigs.entries()) {
1381
+ drawStem(f, tw, 0.012, 0.006, LEAF.bark);
1382
+ const k = s % 2 === 0 ? 0.6 : 1;
1383
+ for (let i = 6;i < tw.x.length; i++) {
1384
+ const fan = i === tw.x.length - 1 ? 14 : 4;
1385
+ for (let q = 0;q < fan; q++) {
1386
+ const a = (tw.a[i] ?? 0) + rng.range(-1.1, 1.1) * (fan > 4 ? 0.8 : 1.2);
1387
+ const col = dim(vary(rng, LEAF.pine, 0.2), k);
1388
+ f.leaf("needle", tw.x[i] ?? 0, tw.y[i] ?? 0, a, rng.range(0.12, 0.2), 0.006, col, 0, k * 0.002);
1389
+ }
1390
+ }
1391
+ }
1392
+ f.finish(0.5, 0.3);
1393
+ }
1394
+ function paintReeds(c, seed) {
1395
+ const rng = createRng2(seed);
1396
+ const f = new Foliage(c);
1397
+ for (let b = 0;b < 22; b++) {
1398
+ const back = b < 8;
1399
+ const x = 0.5 + rng.range(-0.2, 0.2);
1400
+ const tilt = (x - 0.5) * 1.2 + rng.range(-0.25, 0.25);
1401
+ const col = dim(vary(rng, rng.next() < 0.2 ? LEAF.dry : LEAF.reed, 0.2), back ? 0.55 : 1);
1402
+ f.leaf("blade", x, 0, tilt, rng.range(0.55, 0.97), rng.range(0.018, 0.032), col, 0, back ? 0 : 0.002);
1403
+ }
1404
+ f.finish(0.5, 0.15);
1405
+ }
1406
+
1407
+ // src/material/paint/foliage.ts
1408
+ var FOLIAGE_PAINTERS = {
1409
+ foliage_broadleaf: paintBroadleaf,
1410
+ foliage_maple: paintMaple,
1411
+ foliage_maple_autumn: paintMapleAutumn,
1412
+ foliage_birch: paintBirch,
1413
+ foliage_acacia: paintAcacia,
1414
+ foliage_palm: paintPalm,
1415
+ foliage_spruce: paintSpruce,
1416
+ foliage_spruce_snow: paintSpruceSnow,
1417
+ foliage_willow: paintWillow,
1418
+ foliage_reeds: paintReeds,
1419
+ foliage_needles: paintNeedles
1420
+ };
1421
+
1422
+ // src/material/paint/decode-png.ts
1423
+ import { inflateSync } from "node:zlib";
1424
+ var SIG = [137, 80, 78, 71, 13, 10, 26, 10];
1425
+ var CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
1426
+ function bad(cause) {
1427
+ throw new Ts23dError2("T3D_RECIPE_INVALID", {
1428
+ cause: `cell image: ${cause}`,
1429
+ fix: "pass PNG bytes, 8 or 16 bits per channel, non-interlaced (e.g. re-save as RGBA8 PNG)"
1430
+ });
1431
+ }
1432
+ function paeth(a, b, c) {
1433
+ const p = a + b - c;
1434
+ const pa = Math.abs(p - a);
1435
+ const pb = Math.abs(p - b);
1436
+ const pc = Math.abs(p - c);
1437
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
1438
+ }
1439
+ function decodePng(bytes) {
1440
+ if (bytes.length < 33 || SIG.some((s, i) => bytes[i] !== s))
1441
+ bad("not a PNG (bad signature)");
1442
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1443
+ let width = 0;
1444
+ let height = 0;
1445
+ let type = -1;
1446
+ let depth = 8;
1447
+ let palette = new Uint8Array;
1448
+ let trns = new Uint8Array;
1449
+ const idat = [];
1450
+ for (let o = 8;o + 8 <= bytes.length; ) {
1451
+ const len = dv.getUint32(o);
1452
+ const name = String.fromCharCode(...bytes.subarray(o + 4, o + 8));
1453
+ const body = bytes.subarray(o + 8, o + 8 + len);
1454
+ if (name === "IHDR") {
1455
+ width = dv.getUint32(o + 8);
1456
+ height = dv.getUint32(o + 12);
1457
+ type = body[9] ?? -1;
1458
+ depth = body[8] ?? 0;
1459
+ if (depth !== 8 && !(depth === 16 && type !== 3))
1460
+ bad(`bit depth ${depth} (8, or 16 for non-palette images, is supported)`);
1461
+ if (body[12] !== 0)
1462
+ bad("interlaced");
1463
+ } else if (name === "PLTE")
1464
+ palette = body;
1465
+ else if (name === "tRNS")
1466
+ trns = body;
1467
+ else if (name === "IDAT")
1468
+ idat.push(body);
1469
+ else if (name === "IEND")
1470
+ break;
1471
+ o += 12 + len;
1472
+ }
1473
+ const ch = CHANNELS[type];
1474
+ if (!ch || width === 0 || height === 0)
1475
+ bad(`colour type ${type} or empty image`);
1476
+ const raw = new Uint8Array(inflateSync(Buffer.concat(idat)));
1477
+ const B = depth / 8;
1478
+ const bpp = ch * B;
1479
+ const stride = width * bpp;
1480
+ if (raw.length < height * (stride + 1))
1481
+ bad("truncated image data");
1482
+ const px = new Uint8Array(height * stride);
1483
+ for (let y = 0;y < height; y++) {
1484
+ const f = raw[y * (stride + 1)] ?? 0;
1485
+ const src = y * (stride + 1) + 1;
1486
+ const row = y * stride;
1487
+ for (let x = 0;x < stride; x++) {
1488
+ const a = x >= bpp ? px[row + x - bpp] ?? 0 : 0;
1489
+ const b = y > 0 ? px[row - stride + x] ?? 0 : 0;
1490
+ const c = x >= bpp && y > 0 ? px[row - stride + x - bpp] ?? 0 : 0;
1491
+ const v = raw[src + x] ?? 0;
1492
+ const pred = f === 1 ? a : f === 2 ? b : f === 3 ? a + b >> 1 : f === 4 ? paeth(a, b, c) : 0;
1493
+ px[row + x] = v + pred & 255;
1494
+ }
1495
+ }
1496
+ const data = new Uint8Array(width * height * 4);
1497
+ for (let i = 0;i < width * height; i++) {
1498
+ const s = i * bpp;
1499
+ const at = (k) => px[s + k * B] ?? 0;
1500
+ const g = at(0);
1501
+ const o = i * 4;
1502
+ if (type === 3) {
1503
+ data[o] = palette[g * 3] ?? 0;
1504
+ data[o + 1] = palette[g * 3 + 1] ?? 0;
1505
+ data[o + 2] = palette[g * 3 + 2] ?? 0;
1506
+ data[o + 3] = trns[g] ?? 255;
1507
+ } else if (ch >= 3) {
1508
+ data[o] = g;
1509
+ data[o + 1] = at(1);
1510
+ data[o + 2] = at(2);
1511
+ data[o + 3] = ch === 4 ? at(3) : 255;
1512
+ } else {
1513
+ data[o] = g;
1514
+ data[o + 1] = g;
1515
+ data[o + 2] = g;
1516
+ data[o + 3] = ch === 2 ? at(1) : 255;
1517
+ }
1518
+ }
1519
+ return { width, height, data };
1520
+ }
1521
+
1522
+ // src/material/paint/stamp.ts
1523
+ var STAMP_ROUGHNESS = 0.6;
1524
+ function stampImage(c, png) {
1525
+ const img = decodePng(png);
1526
+ const out = new Uint8Array(c.w * c.h * 4);
1527
+ for (let y = 0;y < c.h; y++) {
1528
+ const sy = Math.min(img.height - 1, Math.max(0, (c.h - 1 - y + 0.5) / c.h * img.height - 0.5));
1529
+ const y0 = Math.floor(sy);
1530
+ const y1 = Math.min(img.height - 1, y0 + 1);
1531
+ const fy = sy - y0;
1532
+ for (let x = 0;x < c.w; x++) {
1533
+ const sx = Math.min(img.width - 1, Math.max(0, (x + 0.5) / c.w * img.width - 0.5));
1534
+ const x0 = Math.floor(sx);
1535
+ const x1 = Math.min(img.width - 1, x0 + 1);
1536
+ const fx = sx - x0;
1537
+ for (let k = 0;k < 4; k++) {
1538
+ const a = img.data[(y0 * img.width + x0) * 4 + k] ?? 0;
1539
+ const b = img.data[(y0 * img.width + x1) * 4 + k] ?? 0;
1540
+ const d = img.data[(y1 * img.width + x0) * 4 + k] ?? 0;
1541
+ const e = img.data[(y1 * img.width + x1) * 4 + k] ?? 0;
1542
+ const top = a + (b - a) * fx;
1543
+ out[(y * c.w + x) * 4 + k] = Math.round(top + (d + (e - d) * fx - top) * fy);
1544
+ }
1545
+ }
1546
+ }
1547
+ c.rgba = out;
1548
+ c.height.fill(0);
1549
+ c.rough.fill(STAMP_ROUGHNESS);
1550
+ c.metal.fill(0);
1551
+ }
1552
+
1553
+ // src/material/paint/courses.ts
1554
+ class Courses {
1555
+ id;
1556
+ dx;
1557
+ ux;
1558
+ index = -1;
1559
+ y0 = 0;
1560
+ y1 = 0;
1561
+ bounds = [0];
1562
+ c;
1563
+ spec;
1564
+ constructor(c, spec) {
1565
+ this.c = c;
1566
+ this.spec = spec;
1567
+ this.id = new Int32Array(c.w);
1568
+ this.dx = new Float32Array(c.w);
1569
+ this.ux = new Float32Array(c.w);
1570
+ const rng = createRng2(spec.seed).fork("rows");
1571
+ let y = 0;
1572
+ while (y < c.metresV) {
1573
+ y += spec.course * (1 + spec.irregular * (rng.next() - 0.5));
1574
+ this.bounds.push(y);
1575
+ }
1576
+ }
1577
+ at(ym) {
1578
+ if (this.index >= 0 && ym >= this.y0 && ym < this.y1)
1579
+ return this.index;
1580
+ let k = 0;
1581
+ while (k < this.bounds.length - 2 && ym >= (this.bounds[k + 1] ?? 0))
1582
+ k++;
1583
+ this.index = k;
1584
+ this.y0 = this.bounds[k] ?? 0;
1585
+ this.y1 = this.bounds[k + 1] ?? this.c.metresV;
1586
+ this.fill(k);
1587
+ return k;
1588
+ }
1589
+ fill(k) {
1590
+ const { w, metresU } = this.c;
1591
+ const s = this.spec;
1592
+ const rng = createRng2(s.seed).fork(`course${k}`);
1593
+ const n = Math.max(1, Math.round(metresU / s.length));
1594
+ const lens = [];
1595
+ for (let i = 0;i < n; i++)
1596
+ lens.push(1 + s.irregular * (rng.next() - 0.5));
1597
+ const total = lens.reduce((a, b) => a + b, 0);
1598
+ const joints = [0];
1599
+ for (const l of lens)
1600
+ joints.push((joints[joints.length - 1] ?? 0) + l / total * metresU);
1601
+ const shift = s.irregular > 0 ? rng.next() * metresU : k % 2 * s.stagger * (metresU / n);
1602
+ const px = metresU / w;
1603
+ for (let x = 0;x < w; x++) {
1604
+ let u = ((x + 0.5) * px - shift) % metresU;
1605
+ if (u < 0)
1606
+ u += metresU;
1607
+ let t = 0;
1608
+ while (t < n - 1 && u >= (joints[t + 1] ?? metresU))
1609
+ t++;
1610
+ const a = joints[t] ?? 0;
1611
+ const b = joints[t + 1] ?? metresU;
1612
+ this.id[x] = t;
1613
+ this.ux[x] = u - a;
1614
+ this.dx[x] = Math.min(u - a, b - u);
1615
+ }
1616
+ }
1617
+ }
1618
+
1619
+ // src/material/paint/bond.ts
1620
+ function paintBond(c, r, seed) {
1621
+ const p = r.params;
1622
+ const mortar = param(p, "mortar", 0.01);
1623
+ const depth = param(p, "depth", 0.006);
1624
+ const bevel = param(p, "bevel", 0.004);
1625
+ const relief = param(p, "relief", 0.0008);
1626
+ const variation = param(p, "variation", 0.3);
1627
+ const course = param(p, "course", 0.075);
1628
+ const length = param(p, "length", 0.225);
1629
+ const courses = new Courses(c, {
1630
+ course,
1631
+ length,
1632
+ irregular: param(p, "irregular", 0),
1633
+ stagger: param(p, "stagger", 0.5),
1634
+ seed
1635
+ });
1636
+ const pu = c.metresU / c.w;
1637
+ const pv = c.metresV / c.h;
1638
+ const half = mortar / 2;
1639
+ for (let y = 0;y < c.h; y++) {
1640
+ const ym = (y + 0.5) * pv;
1641
+ const k = courses.at(ym);
1642
+ const dy = Math.min(ym - courses.y0, courses.y1 - ym);
1643
+ for (let x = 0;x < c.w; x++) {
1644
+ const i = y * c.w + x;
1645
+ const xm = (x + 0.5) * pu;
1646
+ const unit = courses.id[x] ?? 0;
1647
+ const chip = noiseAt(c, xm, ym, 0.02, 0.02, seed + 7, 3) * mortar * 0.6;
1648
+ const d = Math.min(courses.dx[x] ?? 0, dy) + chip;
1649
+ const fine = noiseAt(c, xm, ym, 0.006, 0.006, seed + 11, 2);
1650
+ if (d < half) {
1651
+ c.height[i] = -depth + fine * 0.0006;
1652
+ c.tone[i] = 0.95 + 0.04 * fine;
1653
+ c.rough[i] = Math.min(1, r.roughness + 0.08);
1654
+ continue;
1655
+ }
1656
+ const h = hash2(k, unit, seed);
1657
+ const h2 = hash2(unit, k, seed + 3);
1658
+ const big = noiseAt(c, xm, ym, length * 0.6, course * 0.8, seed + 5, 3);
1659
+ const edge = smooth(0, bevel, d - half);
1660
+ const tilt = ((h2 - 0.5) * (courses.ux[x] ?? 0) * 0.02 + (h - 0.5) * 0.001) * edge;
1661
+ const pit = fine > 0.55 ? (fine - 0.55) * 0.004 : 0;
1662
+ c.height[i] = -depth * (1 - edge) + relief * big + tilt - pit;
1663
+ c.tone[i] = (0.62 - variation * 0.5 + variation * h) * (1 + 0.15 * big + 0.05 * fine) - pit * 40;
1664
+ c.rough[i] = r.roughness + 0.06 * fine;
1665
+ }
1666
+ }
1667
+ }
1668
+
1669
+ // src/material/paint/concrete.ts
1670
+ var PORE_CELL = 0.008;
1671
+ function paintConcrete(c, r, seed) {
1672
+ const p = r.params;
1673
+ const pores = param(p, "pores", 0.35);
1674
+ const ties = param(p, "ties", 0.6);
1675
+ const lift = param(p, "lift", 0.6);
1676
+ const mottle = param(p, "mottle", 0.1);
1677
+ const pu = c.metresU / c.w;
1678
+ const pv = c.metresV / c.h;
1679
+ const cellM = Math.max(PORE_CELL, 2.5 * Math.max(pu, pv));
1680
+ const pPer = periodFor(c, cellM);
1681
+ const pCell = c.wrapU ? c.metresU / pPer : cellM;
1682
+ const tPer = ties > 0 ? periodFor(c, ties) : 1;
1683
+ const tCell = c.wrapU ? c.metresU / tPer : ties;
1684
+ for (let y = 0;y < c.h; y++) {
1685
+ const ym = (y + 0.5) * pv;
1686
+ const liftD = lift > 0 ? Math.abs(ym - Math.round(ym / lift) * lift) : 1;
1687
+ const liftGroove = 1 - smooth(0, 0.003, liftD);
1688
+ const liftBand = lift > 0 ? hash2(Math.floor(ym / lift), 0, seed + 4) - 0.5 : 0;
1689
+ for (let x = 0;x < c.w; x++) {
1690
+ const i = y * c.w + x;
1691
+ const xm = (x + 0.5) * pu;
1692
+ const big = noiseAt(c, xm, ym, 0.25, 0.25, seed, 4);
1693
+ const fine = noiseAt(c, xm, ym, 0.004, 0.004, seed + 2, 2);
1694
+ const ix = Math.floor(xm / pCell) % pPer;
1695
+ const iy = Math.floor(ym / pCell);
1696
+ let pit = 0;
1697
+ if (hash2(ix, iy, seed + 5) < pores) {
1698
+ const cx = (ix + 0.2 + 0.6 * hash2(ix, iy, seed + 6)) * pCell;
1699
+ const cy = (iy + 0.2 + 0.6 * hash2(ix, iy, seed + 7)) * pCell;
1700
+ const rad = pCell * (0.12 + 0.25 * hash2(ix, iy, seed + 8));
1701
+ const d = Math.hypot(xm - cx, ym - cy);
1702
+ pit = d < rad ? 1 - d / rad : 0;
1703
+ }
1704
+ let tie = 0;
1705
+ if (ties > 0) {
1706
+ const tx = xm - (Math.floor(xm / tCell) + 0.5) * tCell;
1707
+ const ty = ym - (Math.floor(ym / ties) + 0.5) * ties;
1708
+ const d = Math.hypot(tx, ty);
1709
+ tie = d < 0.012 ? 1 : 0;
1710
+ }
1711
+ c.height[i] = 0.0005 * big + 0.00015 * fine - 0.0015 * pit - 0.006 * tie - 0.0012 * liftGroove;
1712
+ c.tone[i] = (0.78 + mottle * big + 0.05 * liftBand) * (1 + 0.04 * fine) * (1 - 0.4 * pit) * (1 - 0.5 * tie);
1713
+ c.rough[i] = r.roughness + 0.05 * fine + 0.05 * big;
1714
+ }
1715
+ }
1716
+ }
1717
+
1718
+ // src/material/paint/fabric.ts
1719
+ function paintWeave(c, r, seed) {
1720
+ const p = r.params;
1721
+ const pu = c.metresU / c.w;
1722
+ const pv = c.metresV / c.h;
1723
+ const pitchU = Math.max(param(p, "pitch", 0.004), 2 * pu);
1724
+ const pitchV = Math.max(param(p, "pitch", 0.004), 2 * pv);
1725
+ const folds = param(p, "folds", 0.6);
1726
+ const stains = param(p, "stains", 0.3);
1727
+ for (let y = 0;y < c.h; y++) {
1728
+ const ym = (y + 0.5) * pv;
1729
+ const wy = Math.sin(ym / pitchV * Math.PI * 2);
1730
+ for (let x = 0;x < c.w; x++) {
1731
+ const i = y * c.w + x;
1732
+ const xm = (x + 0.5) * pu;
1733
+ const wx = Math.sin(xm / pitchU * Math.PI * 2);
1734
+ const over = Math.floor(xm / pitchU) + Math.floor(ym / pitchV);
1735
+ const thread = over % 2 === 0 ? Math.abs(wy) : Math.abs(wx);
1736
+ const slub = noiseAt(c, xm, ym, 0.05, 0.01, seed, 2);
1737
+ const fold = noiseAt(c, xm, ym, 0.5, 0.35, seed + 1, 3);
1738
+ const stain = stains * smooth(0.1, 0.6, noiseAt(c, xm, ym, 0.2, 0.15, seed + 2, 4));
1739
+ c.height[i] = 0.0002 * thread + 0.003 * folds * fold;
1740
+ c.tone[i] = (0.9 + 0.06 * thread + 0.05 * slub) * (1 - 0.25 * stain);
1741
+ c.rough[i] = Math.min(1, r.roughness + 0.04 * slub - 0.03 * thread);
1742
+ c.metal[i] = 0;
1743
+ c.mix[i] = stain;
1744
+ }
1745
+ }
1746
+ }
1747
+ function paintLeather(c, r, seed) {
1748
+ const p = r.params;
1749
+ const pu = c.metresU / c.w;
1750
+ const pv = c.metresV / c.h;
1751
+ const pebble = param(p, "pebble", 0.004);
1752
+ const creases = param(p, "creases", 0.5);
1753
+ const wear = param(p, "wear", 0.4);
1754
+ for (let y = 0;y < c.h; y++) {
1755
+ const ym = (y + 0.5) * pv;
1756
+ for (let x = 0;x < c.w; x++) {
1757
+ const i = y * c.w + x;
1758
+ const xm = (x + 0.5) * pu;
1759
+ const grain = noiseAt(c, xm, ym, pebble, pebble, seed, 3);
1760
+ const crease = smooth(0.4, 0.6, noiseAt(c, xm, ym, 0.12, 0.012, seed + 1, 3)) * creases;
1761
+ const rub = wear * smooth(0, 0.6, noiseAt(c, xm, ym, 0.3, 0.3, seed + 2, 4));
1762
+ c.height[i] = 0.00025 * grain - 0.0008 * crease;
1763
+ c.tone[i] = (0.95 + 0.05 * grain) * (1 - 0.45 * crease) * (1 + 0.12 * rub);
1764
+ c.rough[i] = Math.min(1, Math.max(0, r.roughness + 0.06 * grain - 0.2 * rub + 0.1 * crease));
1765
+ c.metal[i] = 0;
1766
+ c.mix[i] = crease;
1767
+ }
1768
+ }
1769
+ }
1770
+
1771
+ // src/material/paint/image-strip.ts
1772
+ var decoded = new Map;
1773
+ function image(f, where) {
1774
+ const hit = decoded.get(f.sha256);
1775
+ if (hit)
1776
+ return hit;
1777
+ const img = decodePng(textureBytes(f, where));
1778
+ decoded.set(f.sha256, img);
1779
+ return img;
1780
+ }
1781
+ function resample(c, t, img, fn) {
1782
+ const reps = Math.max(1, Math.round(c.metresU / (t.metres[0] ?? 1)));
1783
+ const su = c.metresU / reps;
1784
+ const sv = t.metres[1] ?? 1;
1785
+ const W = img.width;
1786
+ const H = img.height;
1787
+ const px = new Float32Array(4);
1788
+ for (let y = 0;y < c.h; y++) {
1789
+ const v0 = y * c.metresV / c.h / sv * H;
1790
+ const v1 = (y + 1) * c.metresV / c.h / sv * H;
1791
+ for (let x = 0;x < c.w; x++) {
1792
+ const u0 = x * c.metresU / c.w / su * W;
1793
+ const u1 = (x + 1) * c.metresU / c.w / su * W;
1794
+ px.fill(0);
1795
+ let n = 0;
1796
+ for (let sy = Math.floor(v0);sy < Math.max(Math.floor(v0) + 1, Math.ceil(v1)); sy++) {
1797
+ const row = H - 1 - (sy % H + H) % H;
1798
+ for (let sx = Math.floor(u0);sx < Math.max(Math.floor(u0) + 1, Math.ceil(u1)); sx++) {
1799
+ const o = (row * W + (sx % W + W) % W) * 4;
1800
+ for (let k = 0;k < 4; k++)
1801
+ px[k] = (px[k] ?? 0) + (img.data[o + k] ?? 0);
1802
+ n++;
1803
+ }
1804
+ }
1805
+ for (let k = 0;k < 4; k++)
1806
+ px[k] = (px[k] ?? 0) / n;
1807
+ fn(y * c.w + x, px);
1808
+ }
1809
+ }
1810
+ }
1811
+ function paintImageStrip(c, r, t, where) {
1812
+ const albedo = new Uint8Array(c.w * c.h * 4);
1813
+ resample(c, t, image(t.maps.albedo, where), (i, p) => {
1814
+ albedo.set([p[0] ?? 0, p[1] ?? 0, p[2] ?? 0, 255], i * 4);
1815
+ });
1816
+ c.rgba = albedo;
1817
+ c.height.fill(0);
1818
+ if (t.maps.normal) {
1819
+ const nrm = new Uint8Array(c.w * c.h * 4);
1820
+ resample(c, t, image(t.maps.normal, where), (i, p) => {
1821
+ const x = (p[0] ?? 128) / 127.5 - 1;
1822
+ const y = -((p[1] ?? 128) / 127.5 - 1);
1823
+ const z = (p[2] ?? 255) / 127.5 - 1;
1824
+ const l = Math.hypot(x, y, z) || 1;
1825
+ nrm.set([x / l * 127.5 + 127.5, y / l * 127.5 + 127.5, z / l * 127.5 + 127.5, 255], i * 4);
1826
+ });
1827
+ c.normalRgba = nrm;
1828
+ }
1829
+ if (t.maps.arm) {
1830
+ resample(c, t, image(t.maps.arm, where), (i, p) => {
1831
+ c.occl[i] = (p[0] ?? 255) / 255;
1832
+ c.rough[i] = (p[1] ?? 204) / 255;
1833
+ c.metal[i] = (p[2] ?? 0) / 255;
1834
+ });
1835
+ } else {
1836
+ c.rough.fill(r.roughness);
1837
+ c.metal.fill(r.metalness ?? 0);
1838
+ }
1839
+ }
1840
+
1841
+ // src/material/paint/kit.ts
1842
+ function paintKit(c, seed) {
1843
+ const pu = c.metresU / c.w;
1844
+ const pv = c.metresV / c.h;
1845
+ return {
1846
+ seed,
1847
+ noise: (xm, ym, sizeU, sizeV, salt = 0, octaves = 4) => noiseAt(c, xm, ym, sizeU, sizeV, seed + salt * 7919, octaves),
1848
+ hash: (a, b, salt = 0) => hash2(a, b, seed + salt),
1849
+ smooth,
1850
+ each(fn) {
1851
+ for (let y = 0;y < c.h; y++) {
1852
+ const ym = (y + 0.5) * pv;
1853
+ for (let x = 0;x < c.w; x++)
1854
+ fn(y * c.w + x, (x + 0.5) * pu, ym);
1855
+ }
1856
+ }
1857
+ };
1858
+ }
1859
+ function checkFinite(c, where) {
1860
+ const layers = { height: c.height, tone: c.tone, rough: c.rough, metal: c.metal, mix: c.mix };
1861
+ for (const [name, a] of Object.entries(layers))
1862
+ for (let i = 0;i < a.length; i++)
1863
+ if (!Number.isFinite(a[i] ?? 0))
1864
+ throw new Ts23dError2("T3D_MATERIAL_INVALID", {
1865
+ cause: `${where}: the painter wrote ${a[i]} into '${name}' at texel ${i}`,
1866
+ fix: "write finite numbers only (guard divisions; clamp with Math.min/Math.max)"
1867
+ });
1868
+ }
1869
+
1870
+ // src/material/paint/masks.ts
1871
+ function rustStreaks(c, amount, seed) {
1872
+ if (amount <= 0)
1873
+ return;
1874
+ const pu = c.metresU / c.w;
1875
+ const pv = c.metresV / c.h;
1876
+ for (let y = 0;y < c.h; y++) {
1877
+ const ym = (y + 0.5) * pv;
1878
+ for (let x = 0;x < c.w; x++) {
1879
+ const xm = (x + 0.5) * pu;
1880
+ const run = noiseAt(c, xm, ym, 0.025, 0.6, seed, 3);
1881
+ const patch = noiseAt(c, xm, ym, 0.3, 0.3, seed + 1, 2);
1882
+ const m = amount * smooth(0.15, 0.6, run) * smooth(-0.1, 0.4, patch);
1883
+ if (m <= 0)
1884
+ continue;
1885
+ const i = y * c.w + x;
1886
+ c.tone[i] = (c.tone[i] ?? 1) * (1 - 0.4 * m);
1887
+ c.rough[i] = Math.min(1, (c.rough[i] ?? 0.8) + 0.2 * m);
1888
+ c.metal[i] = (c.metal[i] ?? 0) * (1 - m);
1889
+ c.mix[i] = Math.min(1, (c.mix[i] ?? 0) + m);
1890
+ }
1891
+ }
1892
+ }
1893
+ function grime(c, amount, seed) {
1894
+ if (amount <= 0)
1895
+ return;
1896
+ const pu = c.metresU / c.w;
1897
+ const pv = c.metresV / c.h;
1898
+ for (let y = 0;y < c.h; y++) {
1899
+ const ym = (y + 0.5) * pv;
1900
+ const low = (1 - ym / c.metresV) ** 2;
1901
+ for (let x = 0;x < c.w; x++) {
1902
+ const xm = (x + 0.5) * pu;
1903
+ const streak = smooth(0, 0.7, noiseAt(c, xm, ym, 0.05, 0.5, seed, 3));
1904
+ const blot = 0.6 + 0.4 * noiseAt(c, xm, ym, 0.2, 0.2, seed + 1, 3);
1905
+ const m = amount * Math.min(1, low * blot + 0.35 * streak);
1906
+ const i = y * c.w + x;
1907
+ c.tone[i] = (c.tone[i] ?? 1) * (1 - 0.3 * m);
1908
+ c.rough[i] = Math.min(1, (c.rough[i] ?? 0.8) + 0.1 * m);
1909
+ }
1910
+ }
1911
+ }
1912
+
1913
+ // src/material/paint/metal.ts
1914
+ function crystal(xm, ym, size, cellsU, seed) {
1915
+ const gx = Math.floor(xm / size);
1916
+ const gy = Math.floor(ym / size);
1917
+ let best = 1e9;
1918
+ let id = 0;
1919
+ for (let j = -1;j <= 1; j++)
1920
+ for (let i = -1;i <= 1; i++) {
1921
+ const cx = ((gx + i) % cellsU + cellsU) % cellsU;
1922
+ const px = (gx + i + hash2(cx, gy + j, seed)) * size;
1923
+ const py = (gy + j + hash2(cx, gy + j, seed + 1)) * size;
1924
+ const d = (xm - px) ** 2 + (ym - py) ** 2;
1925
+ if (d < best) {
1926
+ best = d;
1927
+ id = hash2(cx, gy + j, seed + 2) * 1e6;
1928
+ }
1929
+ }
1930
+ return id;
1931
+ }
1932
+ function paintMetal(c, r, seed) {
1933
+ const p = r.params;
1934
+ const brush = param(p, "brush", 0);
1935
+ const cast = param(p, "cast", 0);
1936
+ const spangle = param(p, "spangle", 0);
1937
+ const oxide = param(p, "oxide", 0);
1938
+ const paint = param(p, "paint", 0);
1939
+ const paintRough = param(p, "paintRough", 0.45);
1940
+ const chips = param(p, "chips", 0.25);
1941
+ const size = param(p, "crystal", 0.03);
1942
+ const metal = r.metalness ?? 1;
1943
+ const cellsU = Math.max(1, Math.round(c.metresU / size));
1944
+ const cs = c.metresU / cellsU;
1945
+ const pu = c.metresU / c.w;
1946
+ const pv = c.metresV / c.h;
1947
+ for (let y = 0;y < c.h; y++) {
1948
+ const ym = (y + 0.5) * pv;
1949
+ for (let x = 0;x < c.w; x++) {
1950
+ const i = y * c.w + x;
1951
+ const xm = (x + 0.5) * pu;
1952
+ const big = noiseAt(c, xm, ym, 0.25, 0.25, seed, 4);
1953
+ const fine = noiseAt(c, xm, ym, 0.01, 0.01, seed + 1, 2);
1954
+ let tone = 1 + 0.05 * big;
1955
+ let rough = r.roughness + 0.06 * big + 0.03 * fine;
1956
+ let h = 0.00015 * big;
1957
+ if (brush > 0) {
1958
+ const s = noiseAt(c, xm, ym, 0.6, 0.002, seed + 2, 3);
1959
+ tone *= 1 + 0.06 * brush * s;
1960
+ rough += 0.08 * brush * s;
1961
+ h += 0.00003 * brush * s;
1962
+ }
1963
+ if (cast > 0) {
1964
+ const pit = noiseAt(c, xm, ym, 0.012, 0.012, seed + 3, 3);
1965
+ const lump = noiseAt(c, xm, ym, 0.08, 0.08, seed + 4, 3);
1966
+ h += cast * (0.0006 * pit + 0.0008 * lump);
1967
+ rough += cast * (0.1 + 0.12 * pit);
1968
+ tone *= 1 + cast * (0.16 * pit + 0.08 * lump - 0.04);
1969
+ }
1970
+ if (spangle > 0) {
1971
+ const k = crystal(xm, ym, cs, cellsU, seed + 5);
1972
+ const v = hash2(k, 7, seed) * 2 - 1;
1973
+ tone *= 1 + 0.12 * spangle * v;
1974
+ rough += 0.12 * spangle * v;
1975
+ }
1976
+ let ox = 0;
1977
+ if (oxide > 0) {
1978
+ const blot = noiseAt(c, xm, ym, 0.18, 0.18, seed + 6, 5);
1979
+ const grain = noiseAt(c, xm, ym, 0.02, 0.02, seed + 7, 3);
1980
+ ox = smooth(0.45, 0.75, blot * 0.5 + 0.5 + oxide - 0.5 + 0.15 * grain);
1981
+ h += ox * 0.0004 * (1 + grain);
1982
+ }
1983
+ let m = metal * (1 - ox);
1984
+ let mix = ox;
1985
+ rough = rough * (1 - ox) + (0.88 + 0.06 * fine) * ox;
1986
+ if (paint > 0) {
1987
+ const n = noiseAt(c, xm, ym, 0.05, 0.05, seed + 8, 4);
1988
+ const chip = smooth(0.42, 0.5, n * 0.5 + 0.5 - (1 - chips) * 0.5 + 0.1);
1989
+ const film = paint * (1 - chip);
1990
+ m *= 1 - film;
1991
+ mix = mix * (1 - film) + chip * paint;
1992
+ rough = rough * (1 - film) + (paintRough + 0.05 * big) * film;
1993
+ tone = tone * (1 - film) + (0.96 + 0.03 * big) * film;
1994
+ h -= chip * paint * 0.00015;
1995
+ }
1996
+ c.height[i] = h;
1997
+ c.tone[i] = tone;
1998
+ c.rough[i] = Math.min(1, Math.max(0, rough));
1999
+ c.metal[i] = Math.min(1, Math.max(0, m));
2000
+ c.mix[i] = mix;
2001
+ }
2002
+ }
2003
+ rustStreaks(c, param(p, "streaks", 0), seed + 77);
2004
+ }
2005
+
2006
+ // src/material/paint/planks.ts
2007
+ function paintPlanks(c, r, seed) {
2008
+ const p = r.params;
2009
+ const width = param(p, "width", 0.15);
2010
+ const length = param(p, "length", 1);
2011
+ const gap = param(p, "gap", 0.004);
2012
+ const depth = param(p, "depth", 0.004);
2013
+ const knots = param(p, "knots", 1.5);
2014
+ const rings = param(p, "rings", 0);
2015
+ const rays = param(p, "rays", 0);
2016
+ const checks = param(p, "checks", 0);
2017
+ const paint = param(p, "paint", 0);
2018
+ const paintRough = param(p, "paintRough", 0.5);
2019
+ const chips = param(p, "chips", 0.3);
2020
+ const boards = new Courses(c, { course: width, length, irregular: 0.6, stagger: 0, seed });
2021
+ const pu = c.metresU / c.w;
2022
+ const pv = c.metresV / c.h;
2023
+ const ringsN = Math.max(1, Math.min(9, Math.floor(width / pv / 4)));
2024
+ for (let y = 0;y < c.h; y++) {
2025
+ const ym = (y + 0.5) * pv;
2026
+ const k = boards.at(ym);
2027
+ const dy = Math.min(ym - boards.y0, boards.y1 - ym);
2028
+ const vy = (ym - boards.y0) / (boards.y1 - boards.y0);
2029
+ for (let x = 0;x < c.w; x++) {
2030
+ const i = y * c.w + x;
2031
+ const xm = (x + 0.5) * pu;
2032
+ const id = boards.id[x] ?? 0;
2033
+ const h = hash2(k, id, seed);
2034
+ const d = Math.min((boards.dx[x] ?? 0) * 1.5, dy);
2035
+ const edge = smooth(gap * 0.5, gap * 1.5, d);
2036
+ const warp = noiseAt(c, xm, ym, 0.4, 0.05, seed + id * 31 + k * 7, 3);
2037
+ const ring = Math.sin((vy * 9 + warp * 3 + h * 6) * Math.PI * 2);
2038
+ const fibre = noiseAt(c, xm, ym, 0.08, 0.002, seed + 17, 2);
2039
+ let knot = 0;
2040
+ const nk = Math.floor(knots * length * h * 2);
2041
+ for (let q = 0;q < nk; q++) {
2042
+ const kx = hash2(k * 13 + q, id, seed + 41) * c.metresU;
2043
+ const ky = boards.y0 + (0.25 + 0.5 * hash2(k, id * 5 + q, seed + 43)) * (boards.y1 - boards.y0);
2044
+ let ddx = Math.abs(xm - kx);
2045
+ if (c.wrapU)
2046
+ ddx = Math.min(ddx, c.metresU - ddx);
2047
+ const rr = Math.sqrt((ddx / 2.2) ** 2 + (ym - ky) ** 2) / (width * 0.12);
2048
+ if (rr < 1)
2049
+ knot = Math.max(knot, 1 - rr);
2050
+ }
2051
+ let height = -depth * (1 - edge) + 0.0004 * ring * edge + 0.0003 * fibre - 0.0008 * knot;
2052
+ let tone = (0.66 + 0.22 * h) * (1 + 0.08 * ring + 0.06 * fibre) * (1 - 0.5 * knot) * (0.5 + 0.5 * edge);
2053
+ let rough = r.roughness + 0.05 * fibre + 0.1 * (1 - edge);
2054
+ const ring2 = Math.sin((vy * ringsN + warp * 2.5 + h * 6) * Math.PI * 2);
2055
+ const late = smooth(0.1, 0.8, ring2) * (0.75 + 0.25 * fibre);
2056
+ let mix = rings * Math.max(late, knot);
2057
+ if (rays > 0) {
2058
+ const fleck = smooth(0.55, 0.8, noiseAt(c, xm, ym, 0.03, 0.004, seed + 23, 2));
2059
+ tone *= 1 + 0.25 * rays * fleck;
2060
+ mix *= 1 - rays * fleck;
2061
+ }
2062
+ if (checks > 0) {
2063
+ const split = smooth(0.45, 0.6, noiseAt(c, xm, ym, 0.3, 0.003, seed + 29 + id, 3));
2064
+ height += checks * (0.0006 * late - 0.002 * split);
2065
+ tone *= 1 - checks * (0.45 * split + 0.08 * (1 - late));
2066
+ rough += checks * 0.1 * (1 - late);
2067
+ mix = Math.max(mix, checks * split);
2068
+ }
2069
+ if (paint > 0) {
2070
+ const n = noiseAt(c, xm, ym, 0.04, 0.03, seed + 31, 4) * 0.5 + 0.5;
2071
+ const nearEdge = 1 - smooth(gap, gap * 6, d);
2072
+ const chip = smooth(0.5, 0.56, n + chips * 0.5 + nearEdge * 0.15 - 0.45);
2073
+ const film = paint * (1 - chip);
2074
+ height = height * (1 - 0.4 * film) - chip * paint * 0.0002;
2075
+ const under = (0.9 + 0.06 * h) * (1 + 0.05 * ring + 0.04 * fibre) * (0.55 + 0.45 * edge);
2076
+ tone = tone * (1 - film) + under * film;
2077
+ rough = rough * (1 - film) + (paintRough + 0.04 * fibre) * film;
2078
+ mix = mix * (1 - film) + paint * chip;
2079
+ }
2080
+ c.height[i] = height;
2081
+ c.tone[i] = tone;
2082
+ c.rough[i] = Math.min(1, rough);
2083
+ c.mix[i] = mix;
2084
+ }
2085
+ }
2086
+ }
2087
+
2088
+ // src/material/paint/plates.ts
2089
+ function along(q, pitch) {
2090
+ const t = q / pitch;
2091
+ return Math.abs(t - Math.round(t)) * pitch;
2092
+ }
2093
+ function paintPlates(c, r, seed) {
2094
+ const p = r.params;
2095
+ const lap = param(p, "lap", 0.003);
2096
+ const rivet = param(p, "rivet", 0.014);
2097
+ const pitch = param(p, "pitch", 0.09);
2098
+ const inset = param(p, "inset", 0.05);
2099
+ const chips = param(p, "chips", 0.3);
2100
+ const metal = r.metalness ?? 0;
2101
+ const plates = new Courses(c, {
2102
+ course: param(p, "height", 0.5),
2103
+ length: param(p, "width", 1),
2104
+ irregular: 0,
2105
+ stagger: 0.5,
2106
+ seed
2107
+ });
2108
+ const pu = c.metresU / c.w;
2109
+ const pv = c.metresV / c.h;
2110
+ for (let y = 0;y < c.h; y++) {
2111
+ const ym = (y + 0.5) * pv;
2112
+ const k = plates.at(ym);
2113
+ const dy = Math.min(ym - plates.y0, plates.y1 - ym);
2114
+ for (let x = 0;x < c.w; x++) {
2115
+ const i = y * c.w + x;
2116
+ const xm = (x + 0.5) * pu;
2117
+ const ux = plates.ux[x] ?? 0;
2118
+ const dx = plates.dx[x] ?? 0;
2119
+ const id = plates.id[x] ?? 0;
2120
+ const hammer = noiseAt(c, xm, ym, 0.05, 0.05, seed + 3, 3);
2121
+ const fine = noiseAt(c, xm, ym, 0.004, 0.004, seed + 9, 2);
2122
+ const seam = Math.min(dx, dy);
2123
+ let h = lap * smooth(0, 0.004, ux) + 0.0004 * hammer - 0.001 * (1 - smooth(0, 0.0015, seam));
2124
+ const ry = Math.abs(dy - inset);
2125
+ const rx = Math.abs(dx - inset);
2126
+ const d1 = Math.sqrt(rx * rx + along(ym - plates.y0, pitch) ** 2);
2127
+ const d2 = Math.sqrt(ry * ry + along(xm, pitch) ** 2);
2128
+ const d = Math.min(dx > inset * 2 ? 1 : d1, dy > inset * 2 ? 1 : d2);
2129
+ const dome = d < rivet ? Math.sqrt(1 - (d / rivet) ** 2) : 0;
2130
+ h += dome * rivet * 0.5;
2131
+ const chip = smooth(0.35, 0.45, noiseAt(c, xm, ym, 0.03, 0.03, seed + 21, 4) - (seam * 6 + 0.2) + chips * 0.3);
2132
+ c.height[i] = h - chip * 0.0003;
2133
+ c.tone[i] = (0.8 + 0.06 * hash2(k, id, seed)) * (1 + 0.06 * hammer + 0.03 * fine) * (1 + 0.15 * dome);
2134
+ c.rough[i] = r.roughness + 0.08 * fine - 0.25 * chip;
2135
+ c.metal[i] = metal + (1 - metal) * chip;
2136
+ }
2137
+ }
2138
+ rustStreaks(c, param(p, "rust", 0.4), seed + 77);
2139
+ }
2140
+
2141
+ // src/material/paint/slate.ts
2142
+ function paintSlate(c, r, seed) {
2143
+ const p = r.params;
2144
+ const course = param(p, "course", 0.11);
2145
+ const gap = param(p, "gap", 0.003);
2146
+ const thick = param(p, "thick", 0.007);
2147
+ const variation = param(p, "variation", 0.3);
2148
+ const rows = new Courses(c, {
2149
+ course,
2150
+ length: param(p, "length", 0.25),
2151
+ irregular: 0.15,
2152
+ stagger: 0.5,
2153
+ seed
2154
+ });
2155
+ const pu = c.metresU / c.w;
2156
+ const pv = c.metresV / c.h;
2157
+ for (let y = 0;y < c.h; y++) {
2158
+ const ym = (y + 0.5) * pv;
2159
+ const k = rows.at(ym);
2160
+ const span = rows.y1 - rows.y0;
2161
+ for (let x = 0;x < c.w; x++) {
2162
+ const i = y * c.w + x;
2163
+ const xm = (x + 0.5) * pu;
2164
+ const id = rows.id[x] ?? 0;
2165
+ const h = hash2(k, id, seed);
2166
+ const chip = (noiseAt(c, xm, ym, 0.015, 0.01, seed + 5, 3) + 0.3) * span * 0.08;
2167
+ const uy = Math.max(0, ym - rows.y0 - Math.max(0, chip));
2168
+ const t = uy / span;
2169
+ const lam = noiseAt(c, xm, ym, 0.12, 0.004, seed + 9, 3);
2170
+ const fine = noiseAt(c, xm, ym, 0.005, 0.005, seed + 13, 2);
2171
+ const sideGap = smooth(gap * 0.5, gap * 1.2, rows.dx[x] ?? 0);
2172
+ const bevel = smooth(0, 0.004, uy);
2173
+ const tilt = (hash2(id, k, seed + 1) - 0.5) * 0.002 * ((rows.ux[x] ?? 0) / 0.25);
2174
+ c.height[i] = (thick * (1 - t) * bevel + 0.0005 * lam + tilt) * sideGap - thick * (1 - sideGap);
2175
+ c.tone[i] = (0.62 - variation * 0.5 + variation * h) * (1 + 0.08 * lam + 0.04 * fine) * (0.55 + 0.45 * sideGap);
2176
+ c.rough[i] = r.roughness + 0.08 * lam + 0.05 * fine;
2177
+ }
2178
+ }
2179
+ }
2180
+
2181
+ // src/material/paint/surface.ts
2182
+ function paintFlat(c, r) {
2183
+ c.tone.fill(param(r.params, "tone", 1));
2184
+ c.rough.fill(r.roughness);
2185
+ c.metal.fill(r.metalness ?? 0);
2186
+ c.height.fill(0);
2187
+ }
2188
+ function paintNoise(c, r, seed) {
2189
+ const tone = param(r.params, "tone", 0.93);
2190
+ const mottle = param(r.params, "mottle", 0.03);
2191
+ const relief = param(r.params, "relief", 0.0002);
2192
+ const size = param(r.params, "size", 0.3);
2193
+ const pu = c.metresU / c.w;
2194
+ const pv = c.metresV / c.h;
2195
+ const metal = r.metalness ?? 0;
2196
+ for (let y = 0;y < c.h; y++) {
2197
+ const ym = (y + 0.5) * pv;
2198
+ for (let x = 0;x < c.w; x++) {
2199
+ const xm = (x + 0.5) * pu;
2200
+ const big = noiseAt(c, xm, ym, size, size, seed, 4);
2201
+ const fine = noiseAt(c, xm, ym, 0.01, 0.01, seed + 1, 2);
2202
+ const i = y * c.w + x;
2203
+ c.tone[i] = tone + mottle * big;
2204
+ c.rough[i] = r.roughness + 0.06 * big + 0.03 * fine;
2205
+ c.metal[i] = metal;
2206
+ c.height[i] = relief * (big + 0.3 * fine);
2207
+ }
2208
+ }
2209
+ }
2210
+ function paintGlass(c, r, seed) {
2211
+ const ripple = param(r.params, "ripple", 0.0002);
2212
+ const smudge = param(r.params, "smudge", 0.08);
2213
+ const pu = c.metresU / c.w;
2214
+ const pv = c.metresV / c.h;
2215
+ for (let y = 0;y < c.h; y++) {
2216
+ const ym = (y + 0.5) * pv;
2217
+ for (let x = 0;x < c.w; x++) {
2218
+ const xm = (x + 0.5) * pu;
2219
+ const wave = noiseAt(c, xm, ym, 0.15, 0.4, seed, 3);
2220
+ const smear = noiseAt(c, xm, ym, 0.08, 0.08, seed + 1, 4);
2221
+ const i = y * c.w + x;
2222
+ c.tone[i] = 0.92 + 0.02 * wave;
2223
+ c.rough[i] = r.roughness + smudge * Math.max(0, smear);
2224
+ c.metal[i] = 0;
2225
+ c.height[i] = ripple * wave;
2226
+ }
2227
+ }
2228
+ grime(c, param(r.params, "grime", 0.4), seed + 9);
2229
+ }
2230
+
2231
+ // src/material/paint/strip.ts
2232
+ var PAINTERS = {
2233
+ bond: paintBond,
2234
+ planks: paintPlanks,
2235
+ plates: paintPlates,
2236
+ slate: paintSlate,
2237
+ concrete: paintConcrete,
2238
+ metal: paintMetal,
2239
+ weave: paintWeave,
2240
+ leather: paintLeather,
2241
+ noise: paintNoise,
2242
+ glass: paintGlass,
2243
+ flat: (c, r) => paintFlat(c, r)
2244
+ };
2245
+ function paintStrip(c, r, seed, where = "strip") {
2246
+ if (r.texture)
2247
+ paintImageStrip(c, r, r.texture, where);
2248
+ else {
2249
+ if (r.albedo || r.albedo2)
2250
+ c.colours = [r.albedo ?? [1, 1, 1], r.albedo2 ?? r.albedo ?? [1, 1, 1]];
2251
+ PAINTERS[r.kind](c, r, seed);
2252
+ if (r.kind !== "glass" && r.kind !== "flat")
2253
+ grime(c, param(r.params, "grime", 0), seed + 101);
2254
+ }
2255
+ if (r.paint) {
2256
+ r.paint(c, paintKit(c, seed + 211));
2257
+ checkFinite(c, where);
2258
+ }
2259
+ }
2260
+
2261
+ // src/material/paint/cell.ts
2262
+ var MOTIFS = {
2263
+ window_sash: paintWindowSash,
2264
+ door_panel: paintDoorPanel,
2265
+ clock: paintClock,
2266
+ smokebox: paintSmokebox,
2267
+ sign: paintSign,
2268
+ porthole: paintPorthole
2269
+ };
2270
+ var LEAF_ROUGHNESS = 0.6;
2271
+ function paintCell(c, id, entry, seed) {
2272
+ if ("image" in entry) {
2273
+ stampImage(c, entry.image);
2274
+ return;
2275
+ }
2276
+ if (isFoliage2(id)) {
2277
+ FOLIAGE_PAINTERS[id](c, seed);
2278
+ const k = entry.roughness / LEAF_ROUGHNESS;
2279
+ for (let i = 0;i < c.rough.length; i++)
2280
+ c.rough[i] = Math.min(1, (c.rough[i] ?? 0.6) * k);
2281
+ return;
2282
+ }
2283
+ paintStrip(c, entry, seed);
2284
+ MOTIFS[id]?.(c, entry, seed + 1);
2285
+ }
2286
+
2287
+ // src/material/paint/normal.ts
2288
+ function neighbours(n, wrap) {
2289
+ const lo = new Int32Array(n);
2290
+ const hi = new Int32Array(n);
2291
+ for (let i = 0;i < n; i++) {
2292
+ lo[i] = i > 0 ? i - 1 : wrap ? n - 1 : 0;
2293
+ hi[i] = i < n - 1 ? i + 1 : wrap ? 0 : n - 1;
2294
+ }
2295
+ return { lo, hi };
2296
+ }
2297
+ function sobelNormals(c, strength = 1) {
2298
+ const { w, h, height } = c;
2299
+ const out = new Uint8Array(w * h * 4);
2300
+ const cx = neighbours(w, c.wrapU);
2301
+ const cy = neighbours(h, false);
2302
+ const sx = strength / (8 * (c.metresU / w));
2303
+ const sy = strength / (8 * (c.metresV / h));
2304
+ for (let y = 0;y < h; y++) {
2305
+ const r0 = (cy.lo[y] ?? 0) * w;
2306
+ const r1 = y * w;
2307
+ const r2 = (cy.hi[y] ?? 0) * w;
2308
+ for (let x = 0;x < w; x++) {
2309
+ const xl = cx.lo[x] ?? 0;
2310
+ const xr = cx.hi[x] ?? 0;
2311
+ const tl = height[r0 + xl] ?? 0;
2312
+ const tc = height[r0 + x] ?? 0;
2313
+ const tr = height[r0 + xr] ?? 0;
2314
+ const ml = height[r1 + xl] ?? 0;
2315
+ const mr = height[r1 + xr] ?? 0;
2316
+ const bl = height[r2 + xl] ?? 0;
2317
+ const bc = height[r2 + x] ?? 0;
2318
+ const br = height[r2 + xr] ?? 0;
2319
+ const gx = (tr + 2 * mr + br - tl - 2 * ml - bl) * sx;
2320
+ const gy = (bl + 2 * bc + br - tl - 2 * tc - tr) * sy;
2321
+ const inv = 1 / Math.sqrt(gx * gx + gy * gy + 1);
2322
+ const o = (r1 + x) * 4;
2323
+ out[o] = Math.round((-gx * inv * 0.5 + 0.5) * 255);
2324
+ out[o + 1] = Math.round((gy * inv * 0.5 + 0.5) * 255);
2325
+ out[o + 2] = Math.round((inv * 0.5 + 0.5) * 255);
2326
+ out[o + 3] = 255;
2327
+ }
2328
+ }
2329
+ return out;
2330
+ }
2331
+
2332
+ // src/material/paint/compose.ts
2333
+ function blankSheet(size) {
2334
+ const n = size * size * 4;
2335
+ const albedo = new Uint8Array(n).fill(255);
2336
+ const normal = new Uint8Array(n);
2337
+ const orm = new Uint8Array(n);
2338
+ const flat = new Uint8Array([128, 128, 255, 255]);
2339
+ const base = new Uint8Array([255, 204, 0, 255]);
2340
+ for (let i = 0;i < n; i += 4) {
2341
+ normal.set(flat, i);
2342
+ orm.set(base, i);
2343
+ }
2344
+ return { size, albedo, normal, orm };
2345
+ }
2346
+ function composeRegion(sheet, c, x0, y0, pad) {
2347
+ const ao = cavityOcclusion(c);
2348
+ const maps = [
2349
+ [sheet.albedo, packAlbedo(c, ao)],
2350
+ [sheet.normal, c.normalRgba ?? sobelNormals(c)],
2351
+ [sheet.orm, packOrm(c, ao)]
2352
+ ];
2353
+ const padX = c.wrapU ? 0 : pad;
2354
+ for (let y = 0;y < c.h; y++) {
2355
+ const sy = Math.min(c.h - 1 - pad, Math.max(pad, y));
2356
+ const dst = ((y0 + y) * sheet.size + x0) * 4;
2357
+ const src = sy * c.w;
2358
+ const l = (src + padX) * 4;
2359
+ const r = (src + c.w - 1 - padX) * 4;
2360
+ for (const [to, from] of maps) {
2361
+ to.set(from.subarray(l, r + 4), dst + padX * 4);
2362
+ for (let x = 0;x < padX; x++) {
2363
+ to.set(from.subarray(l, l + 4), dst + x * 4);
2364
+ to.set(from.subarray(r, r + 4), dst + (c.w - 1 - x) * 4);
2365
+ }
2366
+ }
2367
+ }
2368
+ }
2369
+
2370
+ // src/material/paint/validate.ts
2371
+ var KINDS = [
2372
+ "bond",
2373
+ "planks",
2374
+ "plates",
2375
+ "noise",
2376
+ "flat",
2377
+ "slate",
2378
+ "concrete",
2379
+ "glass",
2380
+ "metal",
2381
+ "weave",
2382
+ "leather"
2383
+ ];
2384
+ var SIZES = [512, 1024, 2048];
2385
+ function fail2(cause, fix) {
2386
+ throw new Ts23dError2("T3D_RECIPE_INVALID", { cause: `sheet recipe: ${cause}`, fix });
2387
+ }
2388
+ var unit2 = (v) => Number.isFinite(v) && v >= 0 && v <= 1;
2389
+ function validateStrip(where, r) {
2390
+ if (!KINDS.includes(r.kind))
2391
+ fail2(`${where}.kind '${r.kind}' is unknown`, `use one of ${KINDS.join(" | ")}`);
2392
+ if (!(Number.isFinite(r.scaleMetres) && r.scaleMetres > 0))
2393
+ fail2(`${where}.scaleMetres is ${r.scaleMetres}`, "set the metres the strip V span represents, > 0 (e.g. 0.6 for brick)");
2394
+ if (!unit2(r.roughness))
2395
+ fail2(`${where}.roughness is ${r.roughness}`, "roughness is 0..1");
2396
+ if (r.metalness !== undefined && !unit2(r.metalness))
2397
+ fail2(`${where}.metalness is ${r.metalness}`, "metalness is 0..1");
2398
+ for (const key of ["albedo", "albedo2"]) {
2399
+ const a = r[key];
2400
+ if (a !== undefined && !(a.length === 3 && a.every(unit2)))
2401
+ fail2(`${where}.${key} is [${a}]`, `${key} is linear RGB, three numbers 0..1 (use hex('#rrggbb'))`);
2402
+ }
2403
+ if (r.paint !== undefined && typeof r.paint !== "function")
2404
+ fail2(`${where}.paint is not a function`, "paint is (surface, kit) => void");
2405
+ for (const [k, v] of Object.entries(r.params ?? {}))
2406
+ if (!Number.isFinite(v))
2407
+ fail2(`${where}.params.${k} is ${v}`, "params are finite numbers");
2408
+ }
2409
+ function validateSheet(recipe) {
2410
+ if (!Number.isInteger(recipe.seed))
2411
+ fail2(`seed ${recipe.seed}`, "seed is an integer");
2412
+ if (recipe.size !== undefined && !SIZES.includes(recipe.size))
2413
+ fail2(`size ${recipe.size}`, "size is 512, 1024 or 2048");
2414
+ for (const [id, r] of Object.entries(recipe.strips)) {
2415
+ if (stripRow2(id) < 0)
2416
+ fail2(`strip '${id}' is not in the layout`, `strips are ${STRIPS2.join(", ")} or a defineMaterial() id`);
2417
+ if (r)
2418
+ validateStrip(`strips.${id}`, r);
2419
+ }
2420
+ for (const [id, e] of Object.entries(recipe.cells ?? {})) {
2421
+ if (!CELLS2.includes(id))
2422
+ fail2(`cell '${id}' is not in the layout`, `cells are ${CELLS2.join(", ")}`);
2423
+ if (!e)
2424
+ continue;
2425
+ if ("image" in e) {
2426
+ if (!(e.image instanceof Uint8Array))
2427
+ fail2(`cells.${id}.image is not bytes`, "pass PNG file bytes as a Uint8Array");
2428
+ } else
2429
+ validateStrip(`cells.${id}`, e);
2430
+ }
2431
+ }
2432
+
2433
+ // src/material/generator.ts
2434
+ var FALLBACK = { kind: "noise", scaleMetres: 1, roughness: 0.6 };
2435
+ function paintTrimSheet2(recipe = DEFAULT_SHEET2) {
2436
+ validateSheet(recipe);
2437
+ const size = recipe.size ?? 2048;
2438
+ const sheet = blankSheet(size);
2439
+ const rng = createRng2(recipe.seed);
2440
+ const seedOf = (name) => rng.fork(name).int(0, 2147483647);
2441
+ const pad = Math.max(2, size / 256);
2442
+ const all = [...STRIPS2, ...customIds()];
2443
+ for (const id of all) {
2444
+ const r = recipe.strips[id] ?? DEFAULT_SHEET2.strips[id] ?? FALLBACK;
2445
+ validateStrip(`strips.${id}`, r);
2446
+ const rect = stripRect2(id);
2447
+ const h = Math.round((rect.v1 - rect.v0) * size);
2448
+ const c = createCanvas(size, h, rect.metres, r.scaleMetres, true);
2449
+ paintStrip(c, r, seedOf(id), `strips.${id}`);
2450
+ composeRegion(sheet, c, 0, Math.round(rect.v0 * size), pad);
2451
+ }
2452
+ for (const id of CELLS2) {
2453
+ const e = recipe.cells?.[id] ?? DEFAULT_SHEET2.cells?.[id] ?? FALLBACK;
2454
+ const rect = cellRect2(id);
2455
+ const w = Math.round((rect.u1 - rect.u0) * size);
2456
+ const c = createCanvas(w, w, rect.metres, rect.metres, false);
2457
+ paintCell(c, id, e, seedOf(id));
2458
+ composeRegion(sheet, c, Math.round(rect.u0 * size), Math.round(rect.v0 * size), pad);
2459
+ }
2460
+ return sheet;
2461
+ }
2462
+ // src/material/shader-godot.ts
2463
+ var f = (n) => Number.isInteger(n) ? `${n}.0` : `${+n.toFixed(6)}`;
2464
+ var rect2 = (r) => `in_rect(UV, vec4(${f(r.u0)}, ${f(r.v0)}, ${f(r.u1)}, ${f(r.v1)}))`;
2465
+ function godotSource(p) {
2466
+ const modes = ["blend_mix", "depth_draw_opaque", p.cutout ? "cull_disabled" : "cull_back"];
2467
+ const weight = p.windAttribute ? "CUSTOM0.x /* import _WIND into CUSTOM0 (glTF import: custom attribute mapping) */" : "smoothstep(0.0, wind_height, VERTEX.z)";
2468
+ return `// Generated by @developerz.ai/ts23d godotShaderSource(). A starting point your project owns:
2469
+ // assign it to a ShaderMaterial and set albedo_tex / normal_tex / orm_tex from the GLB's
2470
+ // ts23d_* material (or the shared trim_sheet_{albedo,normal,orm}.png). Godot 4.x.
2471
+ shader_type spatial;
2472
+ render_mode ${modes.join(", ")};
2473
+
2474
+ uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
2475
+ uniform sampler2D normal_tex : hint_normal, filter_linear_mipmap, repeat_enable;
2476
+ uniform sampler2D orm_tex : hint_default_white, filter_linear_mipmap, repeat_enable;
2477
+
2478
+ uniform float night : hint_range(0.0, 1.0) = 0.0;
2479
+ uniform float wind_strength = ${f(p.windStrength)};
2480
+ uniform float wind_height = ${f(p.windHeight)};
2481
+ uniform float wind_hz = ${f(p.windHz)};
2482
+ uniform vec3 glow_color : source_color = vec3(${f(p.glow[0])}, ${f(p.glow[1])}, ${f(p.glow[2])});
2483
+ uniform float glow_ratio : hint_range(0.0, 1.0) = ${f(p.glowRatio)};
2484
+ uniform float glow_intensity = ${f(p.glowIntensity)};
2485
+ uniform float wear : hint_range(0.0, 1.0) = ${f(p.wear)};
2486
+ uniform float grime_height = ${f(p.grimeHeight)};
2487
+
2488
+ varying vec3 world_pos;
2489
+
2490
+ float in_rect(vec2 uv, vec4 r) {
2491
+ float u = fract(uv.x);
2492
+ return step(r.x, u) * step(u, r.z) * step(r.y, uv.y) * step(uv.y, r.w);
2493
+ }
2494
+
2495
+ float hash13(vec3 p) {
2496
+ p = fract(p * 0.1031);
2497
+ p += dot(p, p.zyx + 31.32);
2498
+ return fract((p.x + p.y) * p.z);
2499
+ }
2500
+
2501
+ void vertex() {
2502
+ // Wind sway: mesh-local Z is the authored up axis; sway in local XY.
2503
+ float weight = ${weight};
2504
+ float phase = VERTEX.x * 0.7 + VERTEX.y * 0.45;
2505
+ float sway = sin(TIME * wind_hz * 6.2832 + phase) * wind_strength * weight * 0.1;
2506
+ VERTEX.xy += vec2(sway, sway * 0.6);
2507
+ world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
2508
+ }
2509
+
2510
+ void fragment() {
2511
+ vec4 base = texture(albedo_tex, UV);
2512
+ vec3 orm = texture(orm_tex, UV).rgb;
2513
+ float ground = 1.0 - smoothstep(0.0, grime_height, world_pos.y);
2514
+ float grime = clamp(wear * max((1.0 - orm.r) * 1.5, ground), 0.0, 1.0);
2515
+ ALBEDO = base.rgb * COLOR.rgb * (1.0 - grime * 0.45);
2516
+ AO = orm.r;
2517
+ ROUGHNESS = clamp(orm.g + grime * 0.2, 0.0, 1.0);
2518
+ METALLIC = orm.b * (1.0 - grime);
2519
+ NORMAL_MAP = texture(normal_tex, UV).rgb;
2520
+ ${p.cutout ? ` ALPHA = base.a;
2521
+ ALPHA_SCISSOR_THRESHOLD = 0.5;
2522
+ ` : p.blend ? ` ALPHA = base.a * 0.6;
2523
+ ` : ""}
2524
+ // Window glow: dark panes in window cells, or the glass strip; 1.5 m world slots, glow_ratio lit.
2525
+ float luma = dot(base.rgb, vec3(0.2126, 0.7152, 0.0722));
2526
+ float panes = (${p.glowRects.map(rect2).join(" + ")}) * step(luma, 0.3);
2527
+ float mask = max(panes, ${rect2(p.glassRect)});
2528
+ float lit = step(hash13(floor(world_pos / 1.5)), glow_ratio);
2529
+ EMISSION = glow_color * mask * lit * night * glow_intensity;
2530
+ }
2531
+ `;
2532
+ }
2533
+
2534
+ // src/material/shader-three.ts
2535
+ var f2 = (n) => Number.isInteger(n) ? `${n}.0` : `${+n.toFixed(6)}`;
2536
+ var rectNode = (r) => `inRect(${f2(r.u0)}, ${f2(r.v0)}, ${f2(r.u1)}, ${f2(r.v1)})`;
2537
+ function threeSource(p) {
2538
+ const weight = p.windAttribute ? "attribute('_wind', 'float')" : "smoothstep(float(0), uWindHeight, positionLocal.z)";
2539
+ const glowCells = p.glowRects.map(rectNode).join(".add(") + ")".repeat(p.glowRects.length - 1);
2540
+ return `// Generated by @developerz.ai/ts23d threeMaterialSource(). A starting point your project owns:
2541
+ // edit freely. Needs three r18x (WebGPURenderer + TSL). ts23d itself has no three dependency.
2542
+ //
2543
+ // Usage: const gltf = await loader.loadAsync('tank.glb'); upgradeTrimMaterials(gltf.scene);
2544
+ // trim.uniforms.night.value = 1; // at dusk
2545
+
2546
+ import { DoubleSide, type Material, type Mesh, MeshStandardMaterial, MeshStandardNodeMaterial, type Object3D, type Texture } from 'three/webgpu';
2547
+ import { attribute, color, float, floor, fract, hash, max, normalMap, positionLocal, positionWorld, sin, smoothstep, step, texture, time, uniform, uv, vec3, vertexColor } from 'three/tsl';
2548
+
2549
+ export interface TrimMaps {
2550
+ readonly albedo: Texture;
2551
+ readonly normal: Texture | null;
2552
+ readonly orm: Texture | null;
2553
+ }
2554
+
2555
+ /** Shared uniforms: one set drives every trim material, so a scene-wide night switch is one write. */
2556
+ export const uniforms = {
2557
+ night: uniform(0),
2558
+ windStrength: uniform(${f2(p.windStrength)}),
2559
+ windHeight: uniform(${f2(p.windHeight)}),
2560
+ windHz: uniform(${f2(p.windHz)}),
2561
+ glowColor: uniform(color(${f2(p.glow[0])}, ${f2(p.glow[1])}, ${f2(p.glow[2])})),
2562
+ glowRatio: uniform(${f2(p.glowRatio)}),
2563
+ glowIntensity: uniform(${f2(p.glowIntensity)}),
2564
+ wear: uniform(${f2(p.wear)}),
2565
+ grimeHeight: uniform(${f2(p.grimeHeight)}),
2566
+ };
2567
+
2568
+ /** 1 inside a trim-sheet UV rect (U wraps: strips tile along U). */
2569
+ function inRect(u0: number, v0: number, u1: number, v1: number) {
2570
+ const t = uv();
2571
+ const u = fract(t.x);
2572
+ return step(u0, u).mul(step(u, u1)).mul(step(v0, t.y)).mul(step(t.y, v1));
2573
+ }
2574
+
2575
+ export function createTrimMaterial(maps: TrimMaps, cutout = ${p.cutout}): MeshStandardNodeMaterial {
2576
+ const { night, windStrength: uWindStrength, windHeight: uWindHeight, windHz, glowColor, glowRatio, glowIntensity, wear, grimeHeight } = uniforms;
2577
+ const m = new MeshStandardNodeMaterial();
2578
+ const base = texture(maps.albedo, uv());
2579
+ const orm = maps.orm ? texture(maps.orm, uv()) : vec3(1, 0.8, 0);
2580
+ const tint = vertexColor();
2581
+
2582
+ // Wear / grime: cavities (low AO) and the first metres above the ground get darker and rougher.
2583
+ const ground = float(1).sub(smoothstep(float(0), grimeHeight, positionWorld.y));
2584
+ const grime = wear.mul(max(float(1).sub(orm.x).mul(1.5), ground)).clamp(0, 1);
2585
+ m.colorNode = base.rgb.mul(tint.rgb).mul(float(1).sub(grime.mul(0.45)));
2586
+ m.roughnessNode = orm.y.add(grime.mul(0.2)).clamp(0, 1);
2587
+ m.metalnessNode = orm.z.mul(float(1).sub(grime));
2588
+ m.aoNode = orm.x;
2589
+ if (maps.normal) m.normalNode = normalMap(texture(maps.normal, uv()));
2590
+ if (cutout) {
2591
+ m.opacityNode = base.a;
2592
+ m.alphaTest = 0.5;
2593
+ m.side = DoubleSide;
2594
+ }
2595
+
2596
+ // Wind sway (authored Z-up, so sway in local XY), phase-shifted across the crown.
2597
+ const windWeight = ${weight};
2598
+ const phase = positionLocal.x.mul(0.7).add(positionLocal.y.mul(0.45));
2599
+ const sway = sin(time.mul(windHz).mul(6.2832).add(phase)).mul(uWindStrength).mul(windWeight).mul(0.1);
2600
+ m.positionNode = positionLocal.add(vec3(sway, sway.mul(0.6), 0));
2601
+
2602
+ // Window glow: dark pane texels in window cells, or the glass strip; a world-space hash lights
2603
+ // \`glowRatio\` of the 1.5 m window slots; scaled by \`night\`.
2604
+ const luma = base.r.mul(0.2126).add(base.g.mul(0.7152)).add(base.b.mul(0.0722));
2605
+ const panes = ${glowCells}.mul(step(luma, 0.3));
2606
+ const mask = max(panes, ${rectNode(p.glassRect)});
2607
+ const slot = floor(positionWorld.div(1.5));
2608
+ const lit = step(hash(slot.x.mul(73).add(slot.y.mul(157)).add(slot.z.mul(311))), glowRatio);
2609
+ m.emissiveNode = glowColor.mul(mask).mul(lit).mul(night).mul(glowIntensity);
2610
+ return m;
2611
+ }
2612
+
2613
+ /** Replace every ts23d_* material under \`root\` with the node material, reusing the GLB's maps. */
2614
+ export function upgradeTrimMaterials(root: Object3D): void {
2615
+ const cache = new Map<Material, MeshStandardNodeMaterial>();
2616
+ root.traverse((o) => {
2617
+ const mesh = o as Mesh;
2618
+ const old = mesh.isMesh ? mesh.material : null;
2619
+ if (!(old instanceof MeshStandardMaterial) || !old.name.startsWith('ts23d') || !old.map) return;
2620
+ const next = cache.get(old) ?? createTrimMaterial({ albedo: old.map, normal: old.normalMap, orm: old.roughnessMap }, old.alphaTest > 0);
2621
+ next.name = old.name;
2622
+ cache.set(old, next);
2623
+ mesh.material = next;
2624
+ });
2625
+ }
2626
+ `;
2627
+ }
2628
+
2629
+ // src/material/shader.ts
2630
+ function fail3(cause, fix) {
2631
+ throw new Ts23dError2("T3D_RECIPE_INVALID", { cause: `shader hints: ${cause}`, fix });
2632
+ }
2633
+ function srgbHex(value) {
2634
+ if (!/^#[0-9a-fA-F]{6}$/.test(value))
2635
+ fail3(`colour '${value}'`, "use '#rrggbb' sRGB hex");
2636
+ const n = Number.parseInt(value.slice(1), 16);
2637
+ return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
2638
+ }
2639
+ function resolveHints(h) {
2640
+ const unit = (v, what) => Number.isFinite(v) && v >= 0 && v <= 1 ? v : fail3(`${what} is ${v}`, `${what} is 0..1`);
2641
+ const pos = (v, what) => Number.isFinite(v) && v > 0 ? v : fail3(`${what} is ${v}`, `${what} is a positive number`);
2642
+ const strength = h.wind ? pos(h.wind.strength, "wind.strength") : 0;
2643
+ return {
2644
+ windStrength: strength,
2645
+ windAttribute: h.wind?.source === "attribute",
2646
+ windHeight: pos(h.wind?.height ?? 4, "wind.height"),
2647
+ windHz: pos(h.wind?.hz ?? 0.4, "wind.hz"),
2648
+ glow: h.windowGlow ? srgbHex(h.windowGlow.color) : [1, 0.75, 0.4],
2649
+ glowRatio: h.windowGlow ? unit(h.windowGlow.ratio, "windowGlow.ratio") : 0,
2650
+ glowIntensity: pos(h.windowGlow?.intensity ?? 2, "windowGlow.intensity"),
2651
+ wear: h.wear ? unit(h.wear.amount, "wear.amount") : 0,
2652
+ grimeHeight: pos(h.wear?.grimeHeight ?? 0.6, "wear.grimeHeight"),
2653
+ cutout: h.surface === "cutout",
2654
+ blend: h.surface === "glass" || h.surface === "translucent",
2655
+ glowRects: [cellRect2("window_sash"), cellRect2("porthole")],
2656
+ glassRect: stripRect2("glass")
2657
+ };
2658
+ }
2659
+ function threeMaterialSource2(hints = {}) {
2660
+ return threeSource(resolveHints(hints));
2661
+ }
2662
+ function godotShaderSource2(hints = {}) {
2663
+ return godotSource(resolveHints(hints));
2664
+ }
2665
+ export { CC0_WOOD_PLANKS_GREY2, CC0_MATERIALS2, MATERIAL_LIBRARY2, hex2, NATURAL2, definePalette2, palettes2, provideTextures2, creditOf2, DEFAULT_SHEET2, STRIP_ROWS2, CUSTOM_STRIP_SLOTS2, TRIM_SHEET_SIZE2, stripRow2, stripRect2, cellRect2, STRIPS2, CLASSIC_STRIPS2, MATERIAL_STRIPS2, CELLS2, DECAL_CELLS2, FOLIAGE_CELLS2, isFoliage2, defineMaterial2, resetMaterials2, materialOf2, textureSources2, stripCredits2, paintTrimSheet2, threeMaterialSource2, godotShaderSource2 };
2666
+
2667
+ //# debugId=06EA6C9948471A9664756E2164756E21
2668
+ //# sourceMappingURL=chunk-y63c6zcv.js.map