@woosh/meep-engine 3.55.0 → 3.57.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 (650) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/DUPLICATION_REVIEW_2026_10_24.md +1072 -0
  4. package/src/core/binary/BitSet.js +1 -1
  5. package/src/core/binary/FLOAT16_MAX.d.ts +2 -0
  6. package/src/core/binary/FLOAT16_MAX.d.ts.map +1 -0
  7. package/src/core/binary/FLOAT16_MAX.js +1 -0
  8. package/src/core/binary/base58/base58_encode.js +2 -2
  9. package/src/core/binary/type/TypeBuilder.js +1 -1
  10. package/src/core/bvh2/bvh3/BVH.js +1 -1
  11. package/src/core/bvh2/bvh3/ebvh_optimize_treelet.d.ts.map +1 -1
  12. package/src/core/bvh2/bvh3/ebvh_optimize_treelet.js +7 -3
  13. package/src/core/bvh2/bvh3/ebvh_update_hierarchy_bounds.d.ts.map +1 -1
  14. package/src/core/bvh2/bvh3/ebvh_update_hierarchy_bounds.js +5 -1
  15. package/src/core/bvh2/bvh3/query/compute_tight_near_far_clipping_planes.d.ts.map +1 -1
  16. package/src/core/bvh2/bvh3/query/compute_tight_near_far_clipping_planes.js +195 -246
  17. package/src/core/collection/CuckooFilter.js +1 -1
  18. package/src/core/collection/array/isArrayEqual.d.ts.map +1 -1
  19. package/src/core/collection/array/isArrayEqual.js +33 -58
  20. package/src/core/collection/array/typed/array_buffer_hash.d.ts.map +1 -1
  21. package/src/core/collection/array/typed/array_buffer_hash.js +41 -39
  22. package/src/core/collection/array/typed/float32_array_hash.js +38 -38
  23. package/src/core/collection/array/typed/uint16_array_hash.d.ts +2 -1
  24. package/src/core/collection/array/typed/uint16_array_hash.d.ts.map +1 -1
  25. package/src/core/collection/array/typed/uint16_array_hash.js +30 -29
  26. package/src/core/collection/array/typed/uint32_array_hash.d.ts +2 -1
  27. package/src/core/collection/array/typed/uint32_array_hash.d.ts.map +1 -1
  28. package/src/core/collection/array/typed/uint32_array_hash.js +21 -20
  29. package/src/core/collection/array/typed/uint8_array_hash.d.ts +2 -1
  30. package/src/core/collection/array/typed/uint8_array_hash.d.ts.map +1 -1
  31. package/src/core/collection/array/typed/uint8_array_hash.js +37 -36
  32. package/src/core/collection/array/typed/uint_array_for_count.js +2 -2
  33. package/src/core/collection/list/List.d.ts.map +1 -1
  34. package/src/core/collection/list/List.js +3 -19
  35. package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
  36. package/src/core/collection/table/RowFirstTable.js +4 -28
  37. package/src/core/debug/matchers/DescribeAs.d.ts.map +1 -1
  38. package/src/core/debug/matchers/DescribeAs.js +62 -64
  39. package/src/core/function/FunctionCompiler.js +4 -4
  40. package/src/core/function/objectsEqual.d.ts.map +1 -1
  41. package/src/core/function/objectsEqual.js +3 -2
  42. package/src/core/geom/2d/bvh/BVH2D.d.ts.map +1 -1
  43. package/src/core/geom/2d/bvh/BVH2D.js +4 -3
  44. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.d.ts +5 -2
  45. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.d.ts.map +1 -1
  46. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.js +49 -11
  47. package/src/core/geom/2d/hash-grid/shg_query_elements_circle.d.ts.map +1 -1
  48. package/src/core/geom/2d/hash-grid/shg_query_elements_circle.js +122 -159
  49. package/src/core/geom/2d/hash-grid/shg_query_elements_line.d.ts.map +1 -1
  50. package/src/core/geom/2d/hash-grid/shg_query_elements_line.js +8 -5
  51. package/src/core/geom/2d/polygon/polygon2_rasterize_coverage.d.ts +18 -0
  52. package/src/core/geom/2d/polygon/polygon2_rasterize_coverage.d.ts.map +1 -0
  53. package/src/core/geom/2d/polygon/polygon2_rasterize_coverage.js +97 -0
  54. package/src/core/geom/2d/r-tree/StaticR2Tree.js +2 -2
  55. package/src/core/geom/2d/triangle/tri2_rasterize_coverage.d.ts +21 -0
  56. package/src/core/geom/2d/triangle/tri2_rasterize_coverage.d.ts.map +1 -0
  57. package/src/core/geom/2d/triangle/tri2_rasterize_coverage.js +54 -0
  58. package/src/core/geom/3d/aabb/aabb3_build_frustum.d.ts.map +1 -1
  59. package/src/core/geom/3d/aabb/aabb3_build_frustum.js +20 -53
  60. package/src/core/geom/3d/aabb/aabb3_score_boxes_SAH.d.ts +1 -1
  61. package/src/core/geom/3d/aabb/aabb3_score_boxes_SAH.d.ts.map +1 -1
  62. package/src/core/geom/3d/aabb/aabb3_score_boxes_SAH.js +11 -42
  63. package/src/core/geom/3d/atlas/pack/atlas_pack_charts.d.ts.map +1 -1
  64. package/src/core/geom/3d/atlas/pack/atlas_pack_charts.js +30 -32
  65. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.d.ts.map +1 -1
  66. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.js +1 -5
  67. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.d.ts.map +1 -1
  68. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.js +1 -15
  69. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
  70. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +105 -116
  71. package/src/core/geom/3d/line/line3_compute_segment_point_distance_sqr.d.ts.map +1 -1
  72. package/src/core/geom/3d/line/line3_compute_segment_point_distance_sqr.js +36 -54
  73. package/src/core/geom/3d/plane/plane3_compute_intersection_objects.d.ts +5 -10
  74. package/src/core/geom/3d/plane/plane3_compute_intersection_objects.d.ts.map +1 -1
  75. package/src/core/geom/3d/plane/plane3_compute_intersection_objects.js +20 -82
  76. package/src/core/geom/3d/plane/plane3_slerp.d.ts.map +1 -1
  77. package/src/core/geom/3d/plane/plane3_slerp.js +87 -102
  78. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +12 -0
  79. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -1
  80. package/src/core/geom/3d/polygon/polygon3_compute_normal.js +107 -60
  81. package/src/core/geom/3d/ray/ray3_compute_closest_point.d.ts.map +1 -1
  82. package/src/core/geom/3d/ray/ray3_compute_closest_point.js +37 -43
  83. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  84. package/src/core/geom/3d/shape/ConvexHullShape3D.js +5 -16
  85. package/src/core/geom/3d/shape/MeshShape3D.js +2 -56
  86. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  87. package/src/core/geom/3d/shape/TransformedShape3D.js +10 -7
  88. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  89. package/src/core/geom/3d/shape/UnionShape3D.js +9 -3
  90. package/src/core/geom/3d/shape/json/shape_to_type.js +2 -2
  91. package/src/core/geom/3d/sphere/harmonics/SH3_COEFFICIENTS.js +17 -17
  92. package/src/core/geom/3d/tetrahedra/TetrahedralMesh.d.ts.map +1 -1
  93. package/src/core/geom/3d/tetrahedra/TetrahedralMesh.js +6 -2
  94. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.d.ts.map +1 -1
  95. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.js +3 -2
  96. package/src/core/geom/3d/tetrahedra/tetrahedron_contains_point.d.ts +20 -2
  97. package/src/core/geom/3d/tetrahedra/tetrahedron_contains_point.d.ts.map +1 -1
  98. package/src/core/geom/3d/tetrahedra/tetrahedron_contains_point.js +52 -37
  99. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_face_normals.d.ts.map +1 -1
  100. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_face_normals.js +40 -56
  101. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
  102. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +251 -260
  103. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.d.ts.map +1 -1
  104. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.js +104 -113
  105. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.d.ts.map +1 -1
  106. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_resolve_t_junctions.js +227 -238
  107. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  108. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +15 -24
  109. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_create.d.ts.map +1 -1
  110. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_create.js +31 -115
  111. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_fuse_duplicate_edges.d.ts.map +1 -1
  112. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_fuse_duplicate_edges.js +142 -150
  113. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_replace.d.ts.map +1 -1
  114. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_replace.js +151 -194
  115. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_incenter.d.ts.map +1 -1
  116. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_incenter.js +60 -85
  117. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_distance_field.d.ts.map +1 -1
  118. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_distance_field.js +156 -171
  119. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path.d.ts.map +1 -1
  120. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path.js +8 -44
  121. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path_polyanya.d.ts.map +1 -1
  122. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_face_find_path_polyanya.js +2 -7
  123. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.d.ts.map +1 -1
  124. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.js +3 -2
  125. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_segment_penetrates_surface.d.ts.map +1 -1
  126. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_segment_penetrates_surface.js +128 -133
  127. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.d.ts +8 -14
  128. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.d.ts.map +1 -1
  129. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_surface_ray_parity.js +36 -82
  130. package/src/core/geom/3d/triangle/tri3_ray_intersection.d.ts.map +1 -1
  131. package/src/core/geom/3d/triangle/tri3_ray_intersection.js +7 -144
  132. package/src/core/geom/ConicRay.d.ts.map +1 -1
  133. package/src/core/geom/ConicRay.js +230 -237
  134. package/src/core/localization/LocaleDataset.d.ts +6 -0
  135. package/src/core/localization/LocaleDataset.d.ts.map +1 -1
  136. package/src/core/localization/LocaleDataset.js +48 -37
  137. package/src/core/localization/LocalizationEngine.d.ts.map +1 -1
  138. package/src/core/localization/LocalizationEngine.js +3 -2
  139. package/src/core/math/lookup/ParameterLookupTable.d.ts.map +1 -1
  140. package/src/core/math/lookup/ParameterLookupTable.js +3 -1
  141. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts.map +1 -1
  142. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.js +330 -400
  143. package/src/core/math/spline/spline3_hermite_nearest_point.d.ts +16 -0
  144. package/src/core/math/spline/spline3_hermite_nearest_point.d.ts.map +1 -1
  145. package/src/core/math/spline/spline3_hermite_nearest_point.js +134 -167
  146. package/src/core/model/DebouncedObservedBoolean.d.ts +1 -2
  147. package/src/core/model/DebouncedObservedBoolean.d.ts.map +1 -1
  148. package/src/core/model/DebouncedObservedBoolean.js +1 -1
  149. package/src/core/model/node-graph/node/NodeInstance.d.ts.map +1 -1
  150. package/src/core/model/node-graph/node/NodeInstance.js +2 -7
  151. package/src/core/model/node-graph/node/parameter/NodeParameterDescription.js +1 -1
  152. package/src/core/model/object/optional_equality.d.ts +3 -3
  153. package/src/core/model/object/optional_equality.d.ts.map +1 -1
  154. package/src/core/model/object/optional_equality.js +10 -27
  155. package/src/core/model/object/write_property.js +1 -1
  156. package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.js +2 -2
  157. package/src/core/primitives/strings/string_compute_byte_size.d.ts +3 -3
  158. package/src/core/primitives/strings/string_compute_byte_size.d.ts.map +1 -1
  159. package/src/core/primitives/strings/string_compute_byte_size.js +8 -21
  160. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  161. package/src/core/process/executor/ConcurrentExecutor.js +14 -2
  162. package/src/engine/achievements/gateway/StorageAchievementGateway.d.ts +6 -3
  163. package/src/engine/achievements/gateway/StorageAchievementGateway.d.ts.map +1 -1
  164. package/src/engine/achievements/gateway/StorageAchievementGateway.js +31 -75
  165. package/src/engine/asset/loaders/SVGAssetLoader.d.ts.map +1 -1
  166. package/src/engine/asset/loaders/SVGAssetLoader.js +33 -31
  167. package/src/engine/control/first-person/math/criticallyDampedSpring.d.ts.map +1 -1
  168. package/src/engine/control/first-person/math/criticallyDampedSpring.js +3 -14
  169. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  170. package/src/engine/ecs/EntityComponentDataset.js +4 -1
  171. package/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter.d.ts +0 -2
  172. package/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter.d.ts.map +1 -1
  173. package/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter.js +3 -3
  174. package/src/engine/ecs/terrain/ecs/layers/TerrainLayer.js +2 -2
  175. package/src/engine/ecs/transform/Transform64.d.ts +21 -0
  176. package/src/engine/ecs/transform/Transform64.d.ts.map +1 -1
  177. package/src/engine/ecs/transform/Transform64.js +26 -0
  178. package/src/engine/graphics/sh3/path_tracer/geometry/sort_triangles_by_distance_to_point.js +1 -1
  179. package/src/engine/graphics/texture/sampler/sampler2d_channel_compute_min_indices.js +1 -1
  180. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  181. package/src/engine/graphics3/WaterSystem.js +2 -1
  182. package/src/engine/graphics3/prefab/prefab_skin_frame.d.ts.map +1 -1
  183. package/src/engine/graphics3/prefab/prefab_skin_frame.js +1 -12
  184. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts +1 -1
  185. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts.map +1 -1
  186. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.js +141 -137
  187. package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
  188. package/src/engine/input/ecs/systems/InputControllerSystem.js +13 -3
  189. package/src/engine/input/ecs/systems/InputSystem.d.ts.map +1 -1
  190. package/src/engine/input/ecs/systems/InputSystem.js +2 -1
  191. package/src/engine/intelligence/behavior/composite/ParallelBehavior.d.ts +1 -1
  192. package/src/engine/intelligence/behavior/composite/ParallelBehavior.d.ts.map +1 -1
  193. package/src/engine/intelligence/behavior/composite/ParallelBehavior.js +4 -1
  194. package/src/engine/intelligence/behavior/decorator/InvertStatusBehavior.d.ts.map +1 -1
  195. package/src/engine/intelligence/behavior/decorator/InvertStatusBehavior.js +3 -1
  196. package/src/engine/intelligence/behavior/decorator/RepeatBehavior.d.ts.map +1 -1
  197. package/src/engine/intelligence/behavior/decorator/RepeatBehavior.js +3 -1
  198. package/src/engine/intelligence/behavior/decorator/RepeatBehaviorSerializationAdapter.d.ts.map +1 -1
  199. package/src/engine/intelligence/behavior/decorator/RepeatBehaviorSerializationAdapter.js +39 -33
  200. package/src/engine/intelligence/behavior/util/ConditionalBehavior.d.ts +5 -0
  201. package/src/engine/intelligence/behavior/util/ConditionalBehavior.d.ts.map +1 -1
  202. package/src/engine/intelligence/behavior/util/ConditionalBehavior.js +93 -75
  203. package/src/engine/intelligence/behavior/util/RotationBehavior.d.ts.map +1 -1
  204. package/src/engine/intelligence/behavior/util/RotationBehavior.js +79 -76
  205. package/src/engine/intelligence/blackboard/BlackboardStack.d.ts +1 -0
  206. package/src/engine/intelligence/blackboard/BlackboardStack.d.ts.map +1 -1
  207. package/src/engine/intelligence/blackboard/BlackboardStack.js +159 -123
  208. package/src/engine/intelligence/mcts/StateNode.d.ts.map +1 -1
  209. package/src/engine/intelligence/mcts/StateNode.js +366 -364
  210. package/src/engine/intelligence/resource/ResourceAllocationSolver.js +1 -1
  211. package/src/engine/navigation/ecs/components/Path.d.ts.map +1 -1
  212. package/src/engine/navigation/ecs/components/Path.js +2 -1
  213. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
  214. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +5 -2
  215. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  216. package/src/engine/physics/body/SolverBodyState.js +2 -6
  217. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  218. package/src/engine/physics/ccd/linear_sweep.js +3 -16
  219. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -1
  220. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +2 -16
  221. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts +13 -1
  222. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  223. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +15 -4
  224. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  225. package/src/engine/physics/ecs/PhysicsSystem.js +2 -5
  226. package/src/engine/physics/inertia/locked_inverse_inertia.d.ts +12 -0
  227. package/src/engine/physics/inertia/locked_inverse_inertia.d.ts.map +1 -0
  228. package/src/engine/physics/inertia/locked_inverse_inertia.js +22 -0
  229. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  230. package/src/engine/physics/integration/integrate_velocity.js +3 -18
  231. package/src/engine/physics/mls-mpm/MLS_MPM.d.ts +1 -0
  232. package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
  233. package/src/engine/physics/mls-mpm/MLS_MPM.js +5 -4
  234. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -1
  235. package/src/engine/physics/narrowphase/convex_decomposition.js +7 -16
  236. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  237. package/src/engine/physics/vehicle/RaycastVehicle.js +15 -5
  238. package/src/engine/save/StorageBackedList.d.ts +5 -0
  239. package/src/engine/save/StorageBackedList.d.ts.map +1 -1
  240. package/src/engine/save/StorageBackedList.js +161 -144
  241. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  242. package/src/engine/simulation/Ticker.js +394 -391
  243. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
  244. package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -19
  245. package/src/engine/sound/simulation/probe/probe_edge_visible.d.ts +15 -0
  246. package/src/engine/sound/simulation/probe/probe_edge_visible.d.ts.map +1 -1
  247. package/src/engine/sound/simulation/probe/probe_edge_visible.js +59 -41
  248. package/src/engine/sound/simulation/probe/probe_refine_connectivity.d.ts.map +1 -1
  249. package/src/engine/sound/simulation/probe/probe_refine_connectivity.js +164 -181
  250. package/src/engine/sound/simulation/render/FoaProbeReverbRenderer.d.ts.map +1 -1
  251. package/src/engine/sound/simulation/render/FoaProbeReverbRenderer.js +20 -5
  252. package/src/format/image/avif/av1/decode/Av1FrameContext.d.ts.map +1 -1
  253. package/src/format/image/avif/av1/decode/Av1FrameContext.js +3 -10
  254. package/src/format/image/avif/av1/decode/reconstruct.d.ts +20 -0
  255. package/src/format/image/avif/av1/decode/reconstruct.d.ts.map +1 -1
  256. package/src/format/image/avif/av1/decode/reconstruct.js +22 -9
  257. package/src/format/image/avif/av1/encode/choose_restoration.js +2 -18
  258. package/src/format/image/avif/av1/encode/quantise.d.ts.map +1 -1
  259. package/src/format/image/avif/av1/encode/quantise.js +1 -55
  260. package/src/format/image/avif/av1/filter/loop_restoration.d.ts +9 -0
  261. package/src/format/image/avif/av1/filter/loop_restoration.d.ts.map +1 -1
  262. package/src/format/image/avif/av1/filter/loop_restoration.js +1 -1
  263. package/src/format/texture/basis/transcode/etc1s_to_bc7.js +2 -33
  264. package/src/format/texture/basis/transcode/fit_endpoints.d.ts +11 -0
  265. package/src/format/texture/basis/transcode/fit_endpoints.d.ts.map +1 -1
  266. package/src/format/texture/basis/transcode/fit_endpoints.js +1 -1
  267. package/src/format/texture/basis/uastc/uastc_decode_block.d.ts.map +1 -1
  268. package/src/format/texture/basis/uastc/uastc_decode_block.js +2 -19
  269. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +220 -0
  270. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +172 -0
  271. package/src/generation/foliage/FOLIAGE_DESIGN.md +165 -15
  272. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +158 -0
  273. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +248 -0
  274. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +694 -0
  275. package/src/generation/foliage/analysis/CrownOpacity.d.ts +118 -0
  276. package/src/generation/foliage/analysis/CrownOpacity.d.ts.map +1 -0
  277. package/src/generation/foliage/analysis/CrownOpacity.js +182 -0
  278. package/src/generation/foliage/analysis/crown_opacity_frames.d.ts +10 -0
  279. package/src/generation/foliage/analysis/crown_opacity_frames.d.ts.map +1 -0
  280. package/src/generation/foliage/analysis/crown_opacity_frames.js +132 -0
  281. package/src/generation/foliage/analysis/crown_opacity_measure.d.ts +51 -0
  282. package/src/generation/foliage/analysis/crown_opacity_measure.d.ts.map +1 -0
  283. package/src/generation/foliage/analysis/crown_opacity_measure.js +649 -0
  284. package/src/generation/foliage/analysis/foliage_measure_leaf_area.d.ts +66 -0
  285. package/src/generation/foliage/analysis/foliage_measure_leaf_area.d.ts.map +1 -0
  286. package/src/generation/foliage/analysis/foliage_measure_leaf_area.js +192 -0
  287. package/src/generation/foliage/analysis/tree_crown_opacity.d.ts +27 -0
  288. package/src/generation/foliage/analysis/tree_crown_opacity.d.ts.map +1 -0
  289. package/src/generation/foliage/analysis/tree_crown_opacity.js +58 -0
  290. package/src/generation/foliage/animation/build_tree_wind_rig.d.ts +3 -2
  291. package/src/generation/foliage/animation/build_tree_wind_rig.d.ts.map +1 -1
  292. package/src/generation/foliage/animation/build_tree_wind_rig.js +12 -30
  293. package/src/generation/foliage/animation/tree_wind_bend_limit.d.ts +12 -0
  294. package/src/generation/foliage/animation/tree_wind_bend_limit.d.ts.map +1 -0
  295. package/src/generation/foliage/animation/tree_wind_bend_limit.js +13 -0
  296. package/src/generation/foliage/asset/TreeAsset.d.ts +5 -0
  297. package/src/generation/foliage/asset/TreeAsset.d.ts.map +1 -1
  298. package/src/generation/foliage/asset/TreeAsset.js +6 -0
  299. package/src/generation/foliage/asset/generate_tree.d.ts.map +1 -1
  300. package/src/generation/foliage/asset/generate_tree.js +23 -6
  301. package/src/generation/foliage/config/{BranchLevelConfig.d.ts → BranchGeneratorConfig.d.ts} +61 -75
  302. package/src/generation/foliage/config/BranchGeneratorConfig.d.ts.map +1 -0
  303. package/src/generation/foliage/config/{BranchLevelConfig.js → BranchGeneratorConfig.js} +83 -126
  304. package/src/generation/foliage/config/BranchSpawnRule.d.ts +118 -0
  305. package/src/generation/foliage/config/BranchSpawnRule.d.ts.map +1 -0
  306. package/src/generation/foliage/config/BranchSpawnRule.js +217 -0
  307. package/src/generation/foliage/config/FoliageCardType.d.ts +1 -1
  308. package/src/generation/foliage/config/FoliageCardType.js +2 -2
  309. package/src/generation/foliage/config/FoliageConfig.d.ts +23 -45
  310. package/src/generation/foliage/config/FoliageConfig.d.ts.map +1 -1
  311. package/src/generation/foliage/config/FoliageConfig.js +47 -93
  312. package/src/generation/foliage/config/FoliageMaterialConfig.d.ts +12 -20
  313. package/src/generation/foliage/config/FoliageMaterialConfig.d.ts.map +1 -1
  314. package/src/generation/foliage/config/FoliageMaterialConfig.js +261 -279
  315. package/src/generation/foliage/config/FoliageSpawnRule.d.ts +77 -0
  316. package/src/generation/foliage/config/FoliageSpawnRule.d.ts.map +1 -0
  317. package/src/generation/foliage/config/FoliageSpawnRule.js +135 -0
  318. package/src/generation/foliage/config/FoliageTileContent.d.ts +9 -0
  319. package/src/generation/foliage/config/FoliageTileContent.d.ts.map +1 -0
  320. package/src/generation/foliage/config/FoliageTileContent.js +12 -0
  321. package/src/generation/foliage/config/LeafApexShape.d.ts +12 -0
  322. package/src/generation/foliage/config/LeafApexShape.d.ts.map +1 -0
  323. package/src/generation/foliage/config/LeafApexShape.js +17 -0
  324. package/src/generation/foliage/config/LeafArchitecture.d.ts +155 -0
  325. package/src/generation/foliage/config/LeafArchitecture.d.ts.map +1 -0
  326. package/src/generation/foliage/config/LeafArchitecture.js +288 -0
  327. package/src/generation/foliage/config/LeafBaseShape.d.ts +14 -0
  328. package/src/generation/foliage/config/LeafBaseShape.d.ts.map +1 -0
  329. package/src/generation/foliage/config/LeafBaseShape.js +21 -0
  330. package/src/generation/foliage/config/LeafLobation.d.ts +10 -0
  331. package/src/generation/foliage/config/LeafLobation.d.ts.map +1 -0
  332. package/src/generation/foliage/config/LeafLobation.js +14 -0
  333. package/src/generation/foliage/config/PhyllotaxisMode.d.ts +11 -0
  334. package/src/generation/foliage/config/PhyllotaxisMode.d.ts.map +1 -1
  335. package/src/generation/foliage/config/PhyllotaxisMode.js +36 -0
  336. package/src/generation/foliage/config/ShootBudZone.d.ts +105 -0
  337. package/src/generation/foliage/config/ShootBudZone.d.ts.map +1 -0
  338. package/src/generation/foliage/config/ShootBudZone.js +219 -0
  339. package/src/generation/foliage/config/ShootGrowthConfig.d.ts +84 -0
  340. package/src/generation/foliage/config/ShootGrowthConfig.d.ts.map +1 -0
  341. package/src/generation/foliage/config/ShootGrowthConfig.js +146 -0
  342. package/src/generation/foliage/config/ShootSystemRule.d.ts +144 -0
  343. package/src/generation/foliage/config/ShootSystemRule.d.ts.map +1 -0
  344. package/src/generation/foliage/config/ShootSystemRule.js +316 -0
  345. package/src/generation/foliage/config/TreeLODConfig.d.ts +1 -1
  346. package/src/generation/foliage/config/TreeLODConfig.js +1 -1
  347. package/src/generation/foliage/config/TreeSpeciesConfig.d.ts +40 -8
  348. package/src/generation/foliage/config/TreeSpeciesConfig.d.ts.map +1 -1
  349. package/src/generation/foliage/config/TreeSpeciesConfig.js +124 -68
  350. package/src/generation/foliage/config/TreeWindConfig.d.ts +23 -10
  351. package/src/generation/foliage/config/TreeWindConfig.d.ts.map +1 -1
  352. package/src/generation/foliage/config/TreeWindConfig.js +34 -25
  353. package/src/generation/foliage/index.d.ts +19 -2
  354. package/src/generation/foliage/index.js +63 -44
  355. package/src/generation/foliage/material/FoliageAtlasCutout.d.ts +59 -0
  356. package/src/generation/foliage/material/FoliageAtlasCutout.d.ts.map +1 -0
  357. package/src/generation/foliage/material/FoliageAtlasCutout.js +93 -0
  358. package/src/generation/foliage/material/compute_foliage_variant_atlas_layout.d.ts +26 -0
  359. package/src/generation/foliage/material/compute_foliage_variant_atlas_layout.d.ts.map +1 -0
  360. package/src/generation/foliage/material/compute_foliage_variant_atlas_layout.js +63 -0
  361. package/src/generation/foliage/material/foliage_atlas_cutout.d.ts +34 -0
  362. package/src/generation/foliage/material/foliage_atlas_cutout.d.ts.map +1 -0
  363. package/src/generation/foliage/material/foliage_atlas_cutout.js +174 -0
  364. package/src/generation/foliage/material/foliage_tile.d.ts +110 -0
  365. package/src/generation/foliage/material/foliage_tile.d.ts.map +1 -0
  366. package/src/generation/foliage/material/foliage_tile.js +286 -0
  367. package/src/generation/foliage/material/leaf_geometry.d.ts +103 -0
  368. package/src/generation/foliage/material/leaf_geometry.d.ts.map +1 -0
  369. package/src/generation/foliage/material/leaf_geometry.js +528 -0
  370. package/src/generation/foliage/material/make_foliage_material.d.ts +13 -33
  371. package/src/generation/foliage/material/make_foliage_material.d.ts.map +1 -1
  372. package/src/generation/foliage/material/make_foliage_material.js +356 -605
  373. package/src/generation/foliage/mesh/foliage_collect_attachments.d.ts +39 -4
  374. package/src/generation/foliage/mesh/foliage_collect_attachments.d.ts.map +1 -1
  375. package/src/generation/foliage/mesh/foliage_collect_attachments.js +637 -569
  376. package/src/generation/foliage/mesh/interpolate_ring_skin_weights4.d.ts +48 -0
  377. package/src/generation/foliage/mesh/interpolate_ring_skin_weights4.d.ts.map +1 -0
  378. package/src/generation/foliage/mesh/interpolate_ring_skin_weights4.js +232 -0
  379. package/src/generation/foliage/mesh/tree_mesh_build_bark.d.ts.map +1 -1
  380. package/src/generation/foliage/mesh/tree_mesh_build_bark.js +1023 -1049
  381. package/src/generation/foliage/mesh/tree_mesh_build_foliage.d.ts +9 -2
  382. package/src/generation/foliage/mesh/tree_mesh_build_foliage.d.ts.map +1 -1
  383. package/src/generation/foliage/mesh/tree_mesh_build_foliage.js +467 -454
  384. package/src/generation/foliage/presets/make_tree_species_preset.d.ts.map +1 -1
  385. package/src/generation/foliage/presets/make_tree_species_preset.js +1648 -870
  386. package/src/generation/foliage/skeleton/ShootFrameworkHash.d.ts +35 -0
  387. package/src/generation/foliage/skeleton/ShootFrameworkHash.d.ts.map +1 -0
  388. package/src/generation/foliage/skeleton/ShootFrameworkHash.js +129 -0
  389. package/src/generation/foliage/skeleton/ShootGirth.d.ts +51 -0
  390. package/src/generation/foliage/skeleton/ShootGirth.d.ts.map +1 -0
  391. package/src/generation/foliage/skeleton/ShootGirth.js +212 -0
  392. package/src/generation/foliage/skeleton/ShootLightField.d.ts +73 -0
  393. package/src/generation/foliage/skeleton/ShootLightField.d.ts.map +1 -0
  394. package/src/generation/foliage/skeleton/ShootLightField.js +197 -0
  395. package/src/generation/foliage/skeleton/TreeSkeleton.d.ts +106 -3
  396. package/src/generation/foliage/skeleton/TreeSkeleton.d.ts.map +1 -1
  397. package/src/generation/foliage/skeleton/TreeSkeleton.js +217 -39
  398. package/src/generation/foliage/skeleton/flare_lobe_phase_offset.d.ts +13 -0
  399. package/src/generation/foliage/skeleton/flare_lobe_phase_offset.d.ts.map +1 -0
  400. package/src/generation/foliage/skeleton/flare_lobe_phase_offset.js +16 -0
  401. package/src/generation/foliage/skeleton/foliage_card_density.d.ts +71 -0
  402. package/src/generation/foliage/skeleton/foliage_card_density.d.ts.map +1 -0
  403. package/src/generation/foliage/skeleton/foliage_card_density.js +101 -0
  404. package/src/generation/foliage/skeleton/foliage_senescence.d.ts +101 -0
  405. package/src/generation/foliage/skeleton/foliage_senescence.d.ts.map +1 -0
  406. package/src/generation/foliage/skeleton/foliage_senescence.js +149 -0
  407. package/src/generation/foliage/skeleton/foliage_unit_cards.d.ts +76 -0
  408. package/src/generation/foliage/skeleton/foliage_unit_cards.d.ts.map +1 -0
  409. package/src/generation/foliage/skeleton/foliage_unit_cards.js +95 -0
  410. package/src/generation/foliage/skeleton/shoot_demography.d.ts +22 -0
  411. package/src/generation/foliage/skeleton/shoot_demography.d.ts.map +1 -0
  412. package/src/generation/foliage/skeleton/shoot_demography.js +96 -0
  413. package/src/generation/foliage/skeleton/shoot_geometry.d.ts +66 -0
  414. package/src/generation/foliage/skeleton/shoot_geometry.d.ts.map +1 -0
  415. package/src/generation/foliage/skeleton/shoot_geometry.js +109 -0
  416. package/src/generation/foliage/skeleton/shoot_systems_simulate.d.ts +166 -0
  417. package/src/generation/foliage/skeleton/shoot_systems_simulate.d.ts.map +1 -0
  418. package/src/generation/foliage/skeleton/shoot_systems_simulate.js +588 -0
  419. package/src/generation/foliage/skeleton/tree_skeleton_assign_skin_joints.d.ts +28 -12
  420. package/src/generation/foliage/skeleton/tree_skeleton_assign_skin_joints.d.ts.map +1 -1
  421. package/src/generation/foliage/skeleton/tree_skeleton_assign_skin_joints.js +137 -102
  422. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.d.ts +112 -0
  423. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.d.ts.map +1 -0
  424. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.js +408 -0
  425. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.d.ts.map +1 -1
  426. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.js +208 -206
  427. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts +5 -2
  428. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts.map +1 -1
  429. package/src/generation/foliage/skeleton/tree_skeleton_grow.js +2048 -1871
  430. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.d.ts +17 -0
  431. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.d.ts.map +1 -0
  432. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.js +231 -0
  433. package/src/generation/foliage/skeleton/tree_skeleton_read_joint_motion.d.ts +14 -0
  434. package/src/generation/foliage/skeleton/tree_skeleton_read_joint_motion.d.ts.map +1 -0
  435. package/src/generation/foliage/skeleton/tree_skeleton_read_joint_motion.js +26 -0
  436. package/src/generation/foliage/skeleton/tree_skeleton_read_joint_parents.d.ts +10 -0
  437. package/src/generation/foliage/skeleton/tree_skeleton_read_joint_parents.d.ts.map +1 -0
  438. package/src/generation/foliage/skeleton/tree_skeleton_read_joint_parents.js +20 -0
  439. package/src/generation/grid/generation/road/GridTaskGenerateRoads.d.ts.map +1 -1
  440. package/src/generation/grid/generation/road/GridTaskGenerateRoads.js +4 -1
  441. package/src/generation/markers/GridCellActionPlaceMarkerGroup.d.ts +1 -0
  442. package/src/generation/markers/GridCellActionPlaceMarkerGroup.d.ts.map +1 -1
  443. package/src/generation/markers/GridCellActionPlaceMarkerGroup.js +98 -86
  444. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +263 -0
  445. package/src/shade/descriptor/WebGPUExtensionType.d.ts +17 -0
  446. package/src/shade/descriptor/WebGPUExtensionType.d.ts.map +1 -1
  447. package/src/shade/descriptor/WebGPUExtensionType.js +31 -1
  448. package/src/shade/playground/foliage_wind/FoliageRigOverlay.d.ts +37 -0
  449. package/src/shade/playground/foliage_wind/FoliageRigOverlay.d.ts.map +1 -0
  450. package/src/shade/playground/foliage_wind/FoliageRigOverlay.js +491 -0
  451. package/src/shade/playground/foliage_wind/foliage_world.d.ts.map +1 -1
  452. package/src/shade/playground/foliage_wind/foliage_world.js +600 -602
  453. package/src/shade/playground/foliage_wind/index.html +6 -0
  454. package/src/shade/playground/foliage_wind/main.js +14 -0
  455. package/src/shade/renderer/Renderer.d.ts +5 -0
  456. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  457. package/src/shade/renderer/Renderer.js +25 -2
  458. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  459. package/src/shade/renderer/animation/GPUAnimationManager.js +2 -1
  460. package/src/shade/renderer/animation/foliage/FoliageWindJoint.d.ts +2 -1
  461. package/src/shade/renderer/animation/foliage/FoliageWindJoint.d.ts.map +1 -1
  462. package/src/shade/renderer/animation/foliage/FoliageWindJoint.js +2 -1
  463. package/src/shade/renderer/animation/foliage/chunk_foliage_wind_step.d.ts.map +1 -1
  464. package/src/shade/renderer/animation/foliage/chunk_foliage_wind_step.js +4 -2
  465. package/src/shade/renderer/buffer/table/GPUTypedTable.js +1 -1
  466. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.d.ts.map +1 -1
  467. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.js +403 -401
  468. package/src/shade/renderer/camera/OrbitalCameraController.d.ts +1 -62
  469. package/src/shade/renderer/camera/OrbitalCameraController.d.ts.map +1 -1
  470. package/src/shade/renderer/camera/OrbitalCameraController.js +1 -374
  471. package/src/shade/renderer/deferred/indirect/chunk_lpv_2d_cache_sample_irradiance.d.ts.map +1 -1
  472. package/src/shade/renderer/deferred/indirect/chunk_lpv_2d_cache_sample_irradiance.js +3 -0
  473. package/src/shade/renderer/deferred/indirect/chunk_lpv_sample_light.d.ts.map +1 -1
  474. package/src/shade/renderer/deferred/indirect/chunk_lpv_sample_light.js +6 -1
  475. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.js +1 -1
  476. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +2 -2
  477. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts.map +1 -1
  478. package/src/shade/renderer/fluid/GPUFluidCascade.js +6 -3
  479. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +361 -0
  480. package/src/shade/renderer/fluid/coupling/chunk_fluid_cascade_parent.d.ts.map +1 -1
  481. package/src/shade/renderer/fluid/coupling/chunk_fluid_cascade_parent.js +335 -241
  482. package/src/shade/renderer/fluid/coupling/shader_fluid_cascade_ghosts.d.ts.map +1 -1
  483. package/src/shade/renderer/fluid/coupling/shader_fluid_cascade_ghosts.js +108 -101
  484. package/src/shade/renderer/fluid/forcing/chunk_fluid_forcing_query_select.d.ts.map +1 -1
  485. package/src/shade/renderer/fluid/forcing/chunk_fluid_forcing_query_select.js +69 -67
  486. package/src/shade/renderer/fluid/forcing/shader_fluid_forcing_apply.d.ts +7 -6
  487. package/src/shade/renderer/fluid/forcing/shader_fluid_forcing_apply.d.ts.map +1 -1
  488. package/src/shade/renderer/fluid/forcing/shader_fluid_forcing_apply.js +341 -287
  489. package/src/shade/renderer/fluid/mac/FluidMacPasses.d.ts +6 -4
  490. package/src/shade/renderer/fluid/mac/FluidMacPasses.d.ts.map +1 -1
  491. package/src/shade/renderer/fluid/mac/FluidMacPasses.js +427 -400
  492. package/src/shade/renderer/fluid/mac/chunk_fluid_mac_seams.d.ts +10 -5
  493. package/src/shade/renderer/fluid/mac/chunk_fluid_mac_seams.d.ts.map +1 -1
  494. package/src/shade/renderer/fluid/mac/chunk_fluid_mac_seams.js +532 -190
  495. package/src/shade/renderer/fluid/mac/chunk_v3_mac_lattice_corners.d.ts.map +1 -1
  496. package/src/shade/renderer/fluid/mac/chunk_v3_mac_lattice_corners.js +231 -237
  497. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_correct.d.ts +2 -2
  498. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_correct.d.ts.map +1 -1
  499. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_correct.js +282 -245
  500. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_predict.d.ts +1 -3
  501. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_predict.d.ts.map +1 -1
  502. package/src/shade/renderer/fluid/mac/shader_fluid_mac_advect_predict.js +139 -109
  503. package/src/shade/renderer/fluid/mac/shader_fluid_mac_curl.d.ts +6 -6
  504. package/src/shade/renderer/fluid/mac/shader_fluid_mac_curl.d.ts.map +1 -1
  505. package/src/shade/renderer/fluid/mac/shader_fluid_mac_curl.js +257 -222
  506. package/src/shade/renderer/fluid/mac/shader_fluid_mac_divergence.d.ts +8 -7
  507. package/src/shade/renderer/fluid/mac/shader_fluid_mac_divergence.d.ts.map +1 -1
  508. package/src/shade/renderer/fluid/mac/shader_fluid_mac_divergence.js +119 -92
  509. package/src/shade/renderer/fluid/mac/shader_fluid_mac_gradient.d.ts +6 -9
  510. package/src/shade/renderer/fluid/mac/shader_fluid_mac_gradient.d.ts.map +1 -1
  511. package/src/shade/renderer/fluid/mac/shader_fluid_mac_gradient.js +229 -181
  512. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +17 -7
  513. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts.map +1 -1
  514. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.js +634 -621
  515. package/src/shade/renderer/fluid/pressure/shader_fluid_mg_residual_restrict.d.ts +8 -6
  516. package/src/shade/renderer/fluid/pressure/shader_fluid_mg_residual_restrict.d.ts.map +1 -1
  517. package/src/shade/renderer/fluid/pressure/shader_fluid_mg_residual_restrict.js +166 -149
  518. package/src/shade/renderer/fluid/pressure/shader_fluid_mg_smooth.d.ts +8 -6
  519. package/src/shade/renderer/fluid/pressure/shader_fluid_mg_smooth.d.ts.map +1 -1
  520. package/src/shade/renderer/fluid/pressure/shader_fluid_mg_smooth.js +219 -183
  521. package/src/shade/renderer/fluid/publish/shader_fluid_wind_publish.d.ts.map +1 -1
  522. package/src/shade/renderer/fluid/publish/shader_fluid_wind_publish.js +142 -122
  523. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +674 -663
  524. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +1 -1
  525. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  526. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +63 -29
  527. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts.map +1 -1
  528. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js +77 -87
  529. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/meshopt_clusterizer.d.ts.map +1 -1
  530. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/meshopt_clusterizer.js +4 -3
  531. package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.d.ts +11 -11
  532. package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.d.ts.map +1 -1
  533. package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.js +17 -19
  534. package/src/shade/renderer/global_illumination/lpv/graph_bake_irradiance_lookup_2d.js +4 -4
  535. package/src/shade/renderer/global_illumination/probe/octahedral/sample/chunk_lpv_mesh_interpolate_radiance_via_atlas.d.ts.map +1 -1
  536. package/src/shade/renderer/global_illumination/probe/octahedral/sample/chunk_lpv_mesh_interpolate_radiance_via_atlas.js +5 -1
  537. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  538. package/src/shade/renderer/loader/gltf/load_gltf.js +11 -35
  539. package/src/shade/renderer/loader/usd/load_usd.d.ts.map +1 -1
  540. package/src/shade/renderer/loader/usd/load_usd.js +6 -19
  541. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +2 -1
  542. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  543. package/src/shade/renderer/particles/GPUParticleSystem.js +1 -1
  544. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +2 -2
  545. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  546. package/src/shade/renderer/particles/graph_particles_avboit.js +4 -9
  547. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  548. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +199 -193
  549. package/src/shade/renderer/particles/shaders/shader_particle_avboit_splat.d.ts.map +1 -1
  550. package/src/shade/renderer/particles/shaders/shader_particle_avboit_splat.js +151 -143
  551. package/src/shade/renderer/postprocess/bloom/chunk_karis_blur4x4_sample.d.ts.map +1 -1
  552. package/src/shade/renderer/postprocess/bloom/chunk_karis_blur4x4_sample.js +11 -5
  553. package/src/shade/renderer/postprocess/denoise/graph_atorus_denoise_luma.d.ts.map +1 -1
  554. package/src/shade/renderer/postprocess/denoise/graph_atorus_denoise_luma.js +6 -18
  555. package/src/shade/renderer/postprocess/gtao/GTAO.js +1 -1
  556. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  557. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +3 -5
  558. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +1 -1
  559. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  560. package/src/shade/renderer/postprocess/nss/chunk_nss_luma_derivative.d.ts.map +1 -1
  561. package/src/shade/renderer/postprocess/nss/chunk_nss_luma_derivative.js +7 -3
  562. package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.d.ts.map +1 -1
  563. package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.js +4 -2
  564. package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.d.ts.map +1 -1
  565. package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.js +4 -1
  566. package/src/shade/renderer/postprocess/ssr/downsample_karis.d.ts.map +1 -1
  567. package/src/shade/renderer/postprocess/ssr/downsample_karis.js +6 -3
  568. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts.map +1 -1
  569. package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.js +4 -3
  570. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader.d.ts.map +1 -1
  571. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader.js +4 -1
  572. package/src/shade/renderer/postprocess/ssr/ssr_spatial_denoise_shader.d.ts.map +1 -1
  573. package/src/shade/renderer/postprocess/ssr/ssr_spatial_denoise_shader.js +4 -2
  574. package/src/shade/renderer/postprocess/taa/chunk_taa_tonemap.d.ts.map +1 -1
  575. package/src/shade/renderer/postprocess/taa/chunk_taa_tonemap.js +35 -39
  576. package/src/shade/renderer/postprocess/taa/chunk_taa_tonemap_inv.d.ts.map +1 -1
  577. package/src/shade/renderer/postprocess/taa/chunk_taa_tonemap_inv.js +20 -17
  578. package/src/shade/renderer/postprocess/taa/shader_taa.d.ts.map +1 -1
  579. package/src/shade/renderer/postprocess/taa/shader_taa.js +6 -3
  580. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  581. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +3 -1
  582. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.js +1 -1
  583. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_splat_slice.js +2 -2
  584. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +1 -1
  585. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_integrate.d.ts.map +1 -1
  586. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_integrate.js +92 -83
  587. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.js +1 -1
  588. package/src/shade/renderer/shader/ShaderDescriptor.d.ts.map +1 -1
  589. package/src/shade/renderer/shader/ShaderDescriptor.js +12 -4
  590. package/src/shade/renderer/shader/chunk/bvh/chunk_bvh_query_aabb_cooperative.d.ts.map +1 -1
  591. package/src/shade/renderer/shader/chunk/bvh/chunk_bvh_query_aabb_cooperative.js +294 -286
  592. package/src/shade/renderer/shader/chunk/light/lpv/chunk_lpv_interpolate_probe_irradiance8.js +5 -5
  593. package/src/shade/renderer/shader/chunk/light/lpv/chunk_lpv_mesh_interpolate_probe.d.ts.map +1 -1
  594. package/src/shade/renderer/shader/chunk/light/lpv/chunk_lpv_mesh_interpolate_probe.js +5 -3
  595. package/src/shade/renderer/shader/chunk/light/lpv/chunk_lpv_mesh_interpolate_probe_irradiance.d.ts.map +1 -1
  596. package/src/shade/renderer/shader/chunk/light/lpv/chunk_lpv_mesh_interpolate_probe_irradiance.js +9 -6
  597. package/src/shade/renderer/shader/chunk/light/lpv/chunk_lpv_sample_sh3_at.d.ts.map +1 -1
  598. package/src/shade/renderer/shader/chunk/light/lpv/chunk_lpv_sample_sh3_at.js +33 -38
  599. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts.map +1 -1
  600. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.js +690 -675
  601. package/src/shade/renderer/shader/type/WebGPUArray.d.ts +3 -0
  602. package/src/shade/renderer/shader/type/WebGPUArray.d.ts.map +1 -1
  603. package/src/shade/renderer/shader/type/WebGPUArray.js +185 -167
  604. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  605. package/src/shade/renderer/shader/type/WebGPUStruct.js +461 -455
  606. package/src/shade/renderer/shader/type/serde/read_gpu_typed_buffer.d.ts.map +1 -1
  607. package/src/shade/renderer/shader/type/serde/read_gpu_typed_buffer.js +335 -211
  608. package/src/shade/renderer/shader/type/serde/write_gpu_typed_buffer.d.ts.map +1 -1
  609. package/src/shade/renderer/shader/type/serde/write_gpu_typed_buffer.js +329 -221
  610. package/src/shade/renderer/shader/type/type_has_f16.d.ts +12 -0
  611. package/src/shade/renderer/shader/type/type_has_f16.d.ts.map +1 -0
  612. package/src/shade/renderer/shader/type/type_has_f16.js +66 -0
  613. package/src/shade/renderer/view/GPUViewContext.d.ts +0 -14
  614. package/src/shade/renderer/view/GPUViewContext.d.ts.map +1 -1
  615. package/src/shade/renderer/view/GPUViewContext.js +0 -29
  616. package/src/shade/renderer/view/ViewManager.d.ts +1 -1
  617. package/src/shade/renderer/view/ViewManager.js +1 -1
  618. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +40 -3
  619. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  620. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +660 -583
  621. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  622. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +16 -2
  623. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.d.ts.map +1 -1
  624. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.js +5 -2
  625. package/src/shade/renderer/water/shader_water_avboit.js +1 -1
  626. package/src/shade/renderer/wind/GPUSceneWind.d.ts +1 -1
  627. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  628. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +260 -23
  629. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts +6 -7
  630. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  631. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +269 -69
  632. package/src/shade/wgsl/validate_wgsl_source.d.ts.map +1 -1
  633. package/src/shade/wgsl/validate_wgsl_source.js +27 -1
  634. package/src/view/controller/controls/ListController.d.ts.map +1 -1
  635. package/src/view/controller/controls/ListController.js +118 -113
  636. package/src/view/controller/controls/Vector1Control.js +97 -97
  637. package/src/view/elements/image/SvgImageView.d.ts +1 -0
  638. package/src/view/elements/image/SvgImageView.d.ts.map +1 -1
  639. package/src/view/elements/image/SvgImageView.js +92 -90
  640. package/src/view/minimap/dom/MinimapTerrainView.d.ts +1 -0
  641. package/src/view/minimap/dom/MinimapTerrainView.d.ts.map +1 -1
  642. package/src/view/minimap/dom/MinimapTerrainView.js +151 -147
  643. package/src/view/tooltip/gml/TooltipParser.js +2 -2
  644. package/src/engine/network/PriorityFetch.d.ts +0 -59
  645. package/src/engine/network/PriorityFetch.d.ts.map +0 -1
  646. package/src/engine/network/PriorityFetch.js +0 -193
  647. package/src/generation/foliage/config/BranchLevelConfig.d.ts.map +0 -1
  648. package/src/shade/renderer/volumetrics/VOLUMETRICS_RESOLUTION_Z.d.ts +0 -2
  649. package/src/shade/renderer/volumetrics/VOLUMETRICS_RESOLUTION_Z.d.ts.map +0 -1
  650. package/src/shade/renderer/volumetrics/VOLUMETRICS_RESOLUTION_Z.js +0 -1
@@ -1,1871 +1,2048 @@
1
- import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
2
- import { bvh_query_user_data_intersects_capsule } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_capsule.js";
3
- import { PointHashGrid3 } from "../../../core/geom/3d/hash-grid/PointHashGrid3.js";
4
- import { line3_compute_segment_segment_closest_points } from "../../../core/geom/3d/line/line3_compute_segment_segment_closest_points.js";
5
- import { GOLDEN_RATIO } from "../../../core/math/GOLDEN_RATIO.js";
6
- import { PI2 } from "../../../core/math/PI2.js";
7
- import { clamp } from "../../../core/math/clamp.js";
8
- import { lerp } from "../../../core/math/lerp.js";
9
- import { curl_noise_3d } from "../../../core/math/noise/curl_noise_3d.js";
10
- import { seededRandom } from "../../../core/math/random/seededRandom.js";
11
- import { PhyllotaxisMode } from "../config/PhyllotaxisMode.js";
12
- import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
13
- import { TreeSkeleton } from "./TreeSkeleton.js";
14
- import { crown_envelope_evaluate, crown_envelope_sdf } from "./crown_envelope_evaluate.js";
15
- import { spine_frame_double_reflection } from "./spine_frame_double_reflection.js";
16
- import { tree_skeleton_compute_pipe_radii } from "./tree_skeleton_compute_pipe_radii.js";
17
-
18
- const GOLDEN_ANGLE = PI2 / (GOLDEN_RATIO * GOLDEN_RATIO); // ~2.399963229728653 rad (137.50776 deg)
19
-
20
- const SCRATCH_CURL = [0, 0, 0];
21
- const SCRATCH_RMF = new Float64Array(6);
22
- const SCRATCH_POS = new Float32Array(3);
23
- const SCRATCH_TAN = new Float32Array(3);
24
- const SCRATCH_P0 = new Float32Array(3);
25
- const SCRATCH_T0 = new Float32Array(3);
26
- const SCRATCH_R0 = new Float32Array(3);
27
- const SCRATCH_M_PREV = new Float32Array(3);
28
- const SCRATCH_M_NEXT = new Float32Array(3);
29
- const SCRATCH_GUIDE_P0 = new Float32Array(3);
30
- const SCRATCH_GUIDE_P1 = new Float32Array(3);
31
- const SCRATCH_GUIDE_T = new Float32Array(3);
32
-
33
- /**
34
- * Evaluates the trunk guide curve position `out_pos` and unit tangent `out_tan` at normalized
35
- * parameter `t_norm` in [0, 1]. Defaults to the vertical upward plumb line `(0, t_norm * trunk_len, 0)`
36
- * when `trunk_guide` is null or omitted.
37
- *
38
- * @param {Float32Array|number[]} out_pos
39
- * @param {Float32Array|number[]} out_tan
40
- * @param {Array<ArrayLike<number>>|Function|null|undefined} trunk_guide
41
- * @param {number} t_norm
42
- * @param {number} trunk_len
43
- */
44
- function evaluate_trunk_guide(out_pos, out_tan, trunk_guide, t_norm, trunk_len) {
45
- const t = clamp(t_norm, 0.0, 1.0);
46
- if (!trunk_guide) {
47
- out_pos[0] = 0.0;
48
- out_pos[1] = t * trunk_len;
49
- out_pos[2] = 0.0;
50
- out_tan[0] = 0.0;
51
- out_tan[1] = 1.0;
52
- out_tan[2] = 0.0;
53
- return;
54
- }
55
-
56
- const sample_pt = (u, dst) => {
57
- if (typeof trunk_guide === "function") {
58
- trunk_guide(u, dst);
59
- return;
60
- }
61
- if (Array.isArray(trunk_guide) && trunk_guide.length >= 2) {
62
- const n = trunk_guide.length - 1;
63
- const su = clamp(u, 0.0, 1.0) * n;
64
- const idx = Math.min(n - 1, Math.floor(su));
65
- const frac = su - idx;
66
- const a = trunk_guide[idx];
67
- const b = trunk_guide[idx + 1];
68
- dst[0] = lerp(a[0] ?? 0.0, b[0] ?? 0.0, frac);
69
- dst[1] = lerp(a[1] ?? 0.0, b[1] ?? 0.0, frac);
70
- dst[2] = lerp(a[2] ?? 0.0, b[2] ?? 0.0, frac);
71
- return;
72
- }
73
- dst[0] = 0.0;
74
- dst[1] = u * trunk_len;
75
- dst[2] = 0.0;
76
- };
77
-
78
- sample_pt(t, out_pos);
79
- const eps = 0.01;
80
- const t_a = Math.max(0.0, t - eps);
81
- const t_b = Math.min(1.0, t + eps);
82
- const pa = [0, 0, 0];
83
- const pb = [0, 0, 0];
84
- sample_pt(t_a, pa);
85
- sample_pt(t_b, pb);
86
- let tx = pb[0] - pa[0];
87
- let ty = pb[1] - pa[1];
88
- let tz = pb[2] - pa[2];
89
- const tl = Math.hypot(tx, ty, tz);
90
- if (tl > 1e-8) {
91
- out_tan[0] = tx / tl;
92
- out_tan[1] = ty / tl;
93
- out_tan[2] = tz / tl;
94
- } else {
95
- out_tan[0] = 0.0;
96
- out_tan[1] = 1.0;
97
- out_tan[2] = 0.0;
98
- }
99
- }
100
-
101
- /**
102
- * Rotates unit vector `(vx, vy, vz)` around unit axis `(ax, ay, az)` by `angle` radians (Rodrigues' formula).
103
- * @param {number[]|Float64Array} out
104
- * @param {number} vx
105
- * @param {number} vy
106
- * @param {number} vz
107
- * @param {number} ax
108
- * @param {number} ay
109
- * @param {number} az
110
- * @param {number} angle
111
- */
112
- function rotate_around_axis(out, vx, vy, vz, ax, ay, az, angle) {
113
- const c = Math.cos(angle);
114
- const s = Math.sin(angle);
115
- const dot = vx * ax + vy * ay + vz * az;
116
- const omc = 1.0 - c;
117
-
118
- const cx = ay * vz - az * vy;
119
- const cy = az * vx - ax * vz;
120
- const cz = ax * vy - ay * vx;
121
-
122
- let rx = vx * c + cx * s + ax * dot * omc;
123
- let ry = vy * c + cy * s + ay * dot * omc;
124
- let rz = vz * c + cz * s + az * dot * omc;
125
-
126
- const len = Math.sqrt(rx * rx + ry * ry + rz * rz);
127
- if (len > 1e-12) {
128
- const inv = 1.0 / len;
129
- rx *= inv;
130
- ry *= inv;
131
- rz *= inv;
132
- }
133
- out[0] = rx;
134
- out[1] = ry;
135
- out[2] = rz;
136
- }
137
-
138
- /**
139
- * Determines the non-exempt sub-segment parameter intervals `[s_min, 1]` on candidate segment A
140
- * and `[t_min, 1]` on existing segment B for branch-branch capsule collision testing.
141
- * Returns `false` if the segment pair is topologically exempt (e.g. direct parent-child base collar).
142
- *
143
- * @param {number} bA
144
- * @param {number} parent_bA
145
- * @param {number} attach_seg_A
146
- * @param {boolean} is_split_A
147
- * @param {number} local_idx_A
148
- * @param {number} bB
149
- * @param {number} parent_bB
150
- * @param {number} attach_seg_B
151
- * @param {boolean} is_split_B
152
- * @param {number} local_idx_B
153
- * @param {Float64Array|number[]} out_bounds `[s_min, t_min]`
154
- * @returns {boolean}
155
- */
156
- function resolve_subsegment_bounds(
157
- bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
158
- bB, parent_bB, attach_seg_B, is_split_B, local_idx_B,
159
- out_bounds
160
- ) {
161
- if (bA === bB) return false;
162
-
163
- // 1. A is a direct child of B
164
- if (parent_bA === bB) {
165
- if (is_split_A) {
166
- if (local_idx_A === 0 && local_idx_B === attach_seg_A) return false;
167
- if (local_idx_A === 0 && local_idx_B === attach_seg_A + 1) {
168
- out_bounds[0] = 0.55;
169
- out_bounds[1] = 0.55;
170
- return true;
171
- }
172
- if (local_idx_A === 1 && (local_idx_B === attach_seg_A || local_idx_B === attach_seg_A + 1)) {
173
- out_bounds[0] = 0.45;
174
- out_bounds[1] = 0.45;
175
- return true;
176
- }
177
- } else {
178
- if (local_idx_A === 0 && Math.abs(local_idx_B - attach_seg_A) <= 1) return false;
179
- if (local_idx_A === 1 && Math.abs(local_idx_B - attach_seg_A) <= 1) {
180
- if (bA < 0 || local_idx_B === attach_seg_A) return false;
181
- out_bounds[0] = 0.35;
182
- out_bounds[1] = 0.0;
183
- return true;
184
- }
185
- }
186
- }
187
-
188
- // 2. B is a direct child of A
189
- if (parent_bB === bA) {
190
- if (is_split_B) {
191
- if (local_idx_B === 0 && local_idx_A === attach_seg_B) return false;
192
- if (local_idx_B === 0 && local_idx_A === attach_seg_B + 1) {
193
- out_bounds[0] = 0.55;
194
- out_bounds[1] = 0.55;
195
- return true;
196
- }
197
- if (local_idx_B === 1 && (local_idx_A === attach_seg_B || local_idx_A === attach_seg_B + 1)) {
198
- out_bounds[0] = 0.45;
199
- out_bounds[1] = 0.45;
200
- return true;
201
- }
202
- } else {
203
- if (local_idx_B <= 1 && Math.abs(local_idx_A - attach_seg_B) <= 1) return false;
204
- }
205
- }
206
-
207
- // 3. A and B are siblings sharing the same parent branch
208
- if (parent_bA >= 0 && parent_bA === parent_bB) {
209
- if (local_idx_A === 0 && local_idx_B === 0) {
210
- out_bounds[0] = 0.50;
211
- out_bounds[1] = 0.50;
212
- return true;
213
- }
214
- if (local_idx_A === 0 && local_idx_B === 1 && Math.abs(attach_seg_A - attach_seg_B) <= 1) {
215
- out_bounds[0] = 0.25;
216
- out_bounds[1] = 0.0;
217
- return true;
218
- }
219
- if (local_idx_A === 1 && local_idx_B === 0 && Math.abs(attach_seg_A - attach_seg_B) <= 1) {
220
- out_bounds[0] = 0.0;
221
- out_bounds[1] = 0.25;
222
- return true;
223
- }
224
- }
225
-
226
- out_bounds[0] = 0.0;
227
- out_bounds[1] = 0.0;
228
- return true;
229
- }
230
-
231
- /**
232
- * Generates a directed botanical `TreeSkeleton` from a `TreeSpeciesConfig`, combining:
233
- * - Multi-level parametric Cubic Hermite spine generation (`BranchLevelConfig`, `AnimationCurve` profiles)
234
- * - Botanical phyllotaxis (`SpiralGolden`, `AlternateDistichous`, `Tristichous`, `Opposite`, `OppositeDecussate`, `Whorled`)
235
- * - Gravitropism (toward `-Y`), phototropism (toward `+Y` and outward crown exterior), and 3D `curl_noise_3d` gnarl
236
- * - Dichotomous / sympodial trunk & bough splitting
237
- * - Space colonization crown competition via `PointHashGrid3`
238
- * - Rotation-Minimizing Frames (`spine_frame_double_reflection`)
239
- * - Pipe Model cross-section radius accumulation (`tree_skeleton_compute_pipe_radii`)
240
- *
241
- * @param {TreeSpeciesConfig|Object} [config]
242
- * @param {TreeSkeleton} [out]
243
- * @returns {TreeSkeleton}
244
- */
245
- export function tree_skeleton_grow(config = new TreeSpeciesConfig(), out = new TreeSkeleton()) {
246
- const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
247
- out.clear();
248
-
249
- const rng = seededRandom(species.seed | 0);
250
- // Seed-derived 3D noise offset so unseeded Simplex/Curl fields are unique and deterministic per seed
251
- const noise_ox = (rng() - 0.5) * 1000.0;
252
- const noise_oy = (rng() - 0.5) * 1000.0;
253
- const noise_oz = (rng() - 0.5) * 1000.0;
254
-
255
- const crown = species.crown;
256
- const crown_cy = crown.base_height + crown.height * 0.5;
257
- out.crown_center[0] = 0.0;
258
- out.crown_center[1] = crown_cy;
259
- out.crown_center[2] = 0.0;
260
-
261
- out.crown_radii[0] = Math.max(0.25, crown.radius);
262
- out.crown_radii[1] = Math.max(0.25, crown.height * 0.5);
263
- out.crown_radii[2] = Math.max(0.25, crown.radius);
264
-
265
- // Build Space Colonization attraction point cloud + PointHashGrid3 once
266
- const use_space_col = crown.space_colonization_weight > 0.0 && crown.attraction_point_count > 0;
267
- const attr_count = use_space_col ? (crown.attraction_point_count | 0) : 0;
268
- const attr_positions = new Float32Array(attr_count * 3);
269
- const attr_alive = new Uint8Array(attr_count);
270
- let attr_grid = null;
271
-
272
- if (use_space_col) {
273
- let placed = 0;
274
- let attempts = 0;
275
- const max_attempts = attr_count * 12;
276
- while (placed < attr_count && attempts < max_attempts) {
277
- attempts++;
278
- const u_h = rng();
279
- const env_r = crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
280
- const rx = (rng() * 2.0 - 1.0) * env_r;
281
- const rz = (rng() * 2.0 - 1.0) * env_r;
282
- if (rx * rx + rz * rz <= env_r * env_r) {
283
- const ry = crown.base_height + u_h * crown.height;
284
- attr_positions[placed * 3] = rx;
285
- attr_positions[placed * 3 + 1] = ry;
286
- attr_positions[placed * 3 + 2] = rz;
287
- attr_alive[placed] = 1;
288
- placed++;
289
- }
290
- }
291
- while (placed < attr_count) {
292
- const theta = rng() * PI2;
293
- const u_h = rng();
294
- const r = Math.sqrt(rng()) * crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
295
- attr_positions[placed * 3] = Math.cos(theta) * r;
296
- attr_positions[placed * 3 + 1] = crown.base_height + u_h * crown.height;
297
- attr_positions[placed * 3 + 2] = Math.sin(theta) * r;
298
- attr_alive[placed] = 1;
299
- placed++;
300
- }
301
-
302
- attr_grid = new PointHashGrid3();
303
- attr_grid.build(attr_positions, attr_count, Math.max(0.25, crown.attraction_radius));
304
- }
305
-
306
- const r_influence_sq = crown.attraction_radius * crown.attraction_radius;
307
- const r_kill_sq = crown.kill_radius * crown.kill_radius;
308
-
309
- // Queue of branches to grow in level-order (guarantees parent branches & vertices precede children)
310
- const queue = [];
311
-
312
- const lvl0 = species.levels[0];
313
- if (!lvl0) {
314
- return out;
315
- }
316
-
317
- const trunk_len = Math.max(0.2, lvl0.length * (1.0 + (rng() * 2.0 - 1.0) * lvl0.length_variance));
318
- const trunk_base_r = Math.max(0.01, trunk_len * lvl0.radius_ratio);
319
-
320
- evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, 0.0, trunk_len);
321
-
322
- // Apply subtle initial basal trunk lean around the guide tangent
323
- const max_lean = lvl0.phyllotaxis_mode === PhyllotaxisMode.Whorled ? 0.025 : 0.06;
324
- const lean_ang = clamp((lvl0.basal_lean ?? 0.02) * (0.75 + 0.5 * rng()), 0.0, max_lean);
325
- const lean_az = rng() * PI2;
326
- const init_tx = clamp(SCRATCH_GUIDE_T[0] + Math.sin(lean_ang) * Math.cos(lean_az), -0.95, 0.95);
327
- const init_ty = Math.max(0.2, SCRATCH_GUIDE_T[1] * Math.cos(lean_ang));
328
- const init_tz = clamp(SCRATCH_GUIDE_T[2] + Math.sin(lean_ang) * Math.sin(lean_az), -0.95, 0.95);
329
- const init_t_len = Math.hypot(init_tx, init_ty, init_tz) || 1.0;
330
- const norm_tx = init_tx / init_t_len;
331
- const norm_ty = init_ty / init_t_len;
332
- const norm_tz = init_tz / init_t_len;
333
-
334
- // Orthonormal initial R perpendicular to norm_T
335
- let init_rx = 1.0 - norm_tx * norm_tx;
336
- let init_ry = -norm_ty * norm_tx;
337
- let init_rz = -norm_tz * norm_tx;
338
- const init_r_len = Math.hypot(init_rx, init_ry, init_rz) || 1.0;
339
- init_rx /= init_r_len;
340
- init_ry /= init_r_len;
341
- init_rz /= init_r_len;
342
-
343
- queue.push({
344
- level: 0,
345
- parent_branch_id: -1,
346
- parent_edge_id: -1,
347
- parent_attach_seg_idx: -1,
348
- parent_attach_t: 0.0,
349
- start_vertex: -1,
350
- px: SCRATCH_GUIDE_P0[0],
351
- py: SCRATCH_GUIDE_P0[1],
352
- pz: SCRATCH_GUIDE_P0[2],
353
- tx: norm_tx,
354
- ty: norm_ty,
355
- tz: norm_tz,
356
- rx: init_rx,
357
- ry: init_ry,
358
- rz: init_rz,
359
- emerge_drx: init_rx,
360
- emerge_dry: init_ry,
361
- emerge_drz: init_rz,
362
- length: trunk_len,
363
- full_branch_length: trunk_len,
364
- base_radius: trunk_base_r,
365
- start_seg: 0,
366
- total_segs: Math.max(1, lvl0.segments),
367
- initial_arc_s: 0.0,
368
- has_split: false,
369
- is_split_fork: false,
370
- cum_t0: 0.0,
371
- cum_t1: 1.0
372
- });
373
-
374
- const rot_scratch = [0, 0, 0];
375
-
376
- // Scope-local BVH and per-segment metadata for branch-branch spatial avoidance & collision prevention
377
- const bvh = new BVH();
378
- const bvh_hits = new Uint32Array(256);
379
- const query_capsule = new Float32Array(7);
380
- const sub_bounds = new Float64Array(2);
381
- const closest_st = new Float64Array(2);
382
-
383
- const seg_branch_id = [];
384
- const seg_local_idx = [];
385
- const seg_p0x = [], seg_p0y = [], seg_p0z = [], seg_r0 = [];
386
- const seg_p1x = [], seg_p1y = [], seg_p1z = [], seg_r1 = [];
387
- const seg_bvh_leaf = [];
388
-
389
- const branch_parent_seg_idx = [];
390
- const branch_is_split_fork = [];
391
-
392
- /**
393
- * Evaluates the minimum signed surface-to-surface clearance between a candidate segment A
394
- * and all non-exempt existing segments in `bvh`.
395
- */
396
- const evaluate_candidate_clearance = (
397
- bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
398
- a0x, a0y, a0z, rA0,
399
- a1x, a1y, a1z, rA1,
400
- extra_margin
401
- ) => {
402
- if (bvh.size === 0) return 10.0;
403
- const max_rA = Math.max(rA0, rA1);
404
- query_capsule[0] = a0x;
405
- query_capsule[1] = a0y;
406
- query_capsule[2] = a0z;
407
- query_capsule[3] = a1x;
408
- query_capsule[4] = a1y;
409
- query_capsule[5] = a1z;
410
- query_capsule[6] = max_rA * 1.15 + extra_margin + 0.04;
411
-
412
- const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
413
- let min_clear = 10.0;
414
-
415
- for (let h = 0; h < hit_count; h++) {
416
- const eB = bvh_hits[h];
417
- const bB = seg_branch_id[eB];
418
- const local_idx_B = seg_local_idx[eB];
419
- const parent_bB = out.branch_parent[bB];
420
- const attach_seg_B = branch_parent_seg_idx[bB];
421
- const is_split_B = branch_is_split_fork[bB];
422
-
423
- if (!resolve_subsegment_bounds(
424
- bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
425
- bB, parent_bB, attach_seg_B, is_split_B, local_idx_B,
426
- sub_bounds
427
- )) {
428
- continue;
429
- }
430
-
431
- const s_min = sub_bounds[0];
432
- const t_min = sub_bounds[1];
433
- const sa0x = a0x + (a1x - a0x) * s_min;
434
- const sa0y = a0y + (a1y - a0y) * s_min;
435
- const sa0z = a0z + (a1z - a0z) * s_min;
436
-
437
- const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
438
- const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
439
- const sb0x = b0x + (b1x - b0x) * t_min;
440
- const sb0y = b0y + (b1y - b0y) * t_min;
441
- const sb0z = b0z + (b1z - b0z) * t_min;
442
-
443
- line3_compute_segment_segment_closest_points(
444
- closest_st,
445
- sa0x, sa0y, sa0z, a1x, a1y, a1z,
446
- sb0x, sb0y, sb0z, b1x, b1y, b1z
447
- );
448
-
449
- const s_f = s_min + (1.0 - s_min) * closest_st[0];
450
- const t_f = t_min + (1.0 - t_min) * closest_st[1];
451
-
452
- const qAx = a0x + (a1x - a0x) * s_f;
453
- const qAy = a0y + (a1y - a0y) * s_f;
454
- const qAz = a0z + (a1z - a0z) * s_f;
455
- const qBx = b0x + (b1x - b0x) * t_f;
456
- const qBy = b0y + (b1y - b0y) * t_f;
457
- const qBz = b0z + (b1z - b0z) * t_f;
458
-
459
- const dist = Math.hypot(qAx - qBx, qAy - qBy, qAz - qBz);
460
- const rA = rA0 + (rA1 - rA0) * s_f;
461
- const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
462
- const r_req = (rA + rB) * 1.08 + extra_margin;
463
- const clear = dist - r_req;
464
- if (clear < min_clear) {
465
- min_clear = clear;
466
- }
467
- }
468
- return min_clear;
469
- };
470
-
471
- for (let q_idx = 0; q_idx < queue.length; q_idx++) {
472
- const job = queue[q_idx];
473
- const level = job.level;
474
- const lvl_cfg = species.levels[level];
475
- if (!lvl_cfg) continue;
476
-
477
- const phase_hash = rng();
478
- const N_seg = job.total_segs;
479
- const remaining_segs = N_seg - job.start_seg;
480
- if (remaining_segs <= 0) continue;
481
-
482
- const step_len = job.length / N_seg;
483
- const prof_p0 = lvl_cfg.radius_profile.evaluate(0.0);
484
- const prof_p1 = lvl_cfg.radius_profile.evaluate(1.0);
485
- const prof_span = prof_p0 - prof_p1;
486
-
487
- let cur_px = job.px;
488
- let cur_py = job.py;
489
- let cur_pz = job.pz;
490
- let cur_tx = job.tx;
491
- let cur_ty = job.ty;
492
- let cur_tz = job.tz;
493
- let cur_rx = job.rx;
494
- let cur_ry = job.ry;
495
- let cur_rz = job.rz;
496
- let cur_arc_s = job.initial_arc_s;
497
-
498
- // Compute binormal S = T x R
499
- let cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
500
- let cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
501
- let cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
502
-
503
- let prev_v = job.start_vertex;
504
- let branch_id = -1;
505
-
506
- if (prev_v < 0) {
507
- const t0 = job.start_seg / N_seg;
508
- const u_prof0 = prof_span > 1e-4
509
- ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(t0)) / prof_span, 0.0, 1.0)
510
- : t0;
511
- const min_r0_frac = level === 0 ? 0.08 : 0.15;
512
- const r0 = Math.max(0.002, job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_prof0, min_r0_frac));
513
-
514
- branch_id = out.add_branch(
515
- 0,
516
- level,
517
- job.parent_branch_id,
518
- job.parent_edge_id,
519
- job.parent_attach_t,
520
- job.length,
521
- r0,
522
- phase_hash
523
- );
524
- prev_v = out.add_node(
525
- cur_px, cur_py, cur_pz,
526
- cur_tx, cur_ty, cur_tz,
527
- cur_rx, cur_ry, cur_rz,
528
- r0,
529
- branch_id,
530
- level,
531
- job.parent_branch_id,
532
- t0,
533
- cur_arc_s,
534
- phase_hash
535
- );
536
- if (job.attach_parent_vertex !== undefined && job.attach_parent_vertex >= 0) {
537
- out.connect_nodes(job.attach_parent_vertex, prev_v);
538
- }
539
- out.branch_root_vertex[branch_id] = prev_v;
540
- out.branch_tip_vertex[branch_id] = prev_v;
541
- } else {
542
- const prev_r = out.node_read_radius(prev_v);
543
- branch_id = out.add_branch(
544
- prev_v,
545
- level,
546
- job.parent_branch_id,
547
- job.parent_edge_id,
548
- job.parent_attach_t,
549
- job.length,
550
- prev_r,
551
- phase_hash
552
- );
553
- }
554
-
555
- branch_parent_seg_idx[branch_id] = job.parent_attach_seg_idx ?? -1;
556
- branch_is_split_fork[branch_id] = Boolean(job.is_split_fork);
557
-
558
- const branch_edges = [];
559
- const branch_edge_t0 = [];
560
- const branch_edge_t1 = [];
561
- const branch_vertices = [prev_v];
562
-
563
- // Pre-schedule phyllotactic child attachment parameters so the parent spine kinks sympodially at lateral emissions
564
- const next_level_preview = level + 1;
565
- const len_fraction_est = level >= 1
566
- ? clamp(Math.pow(job.length / Math.max(0.2, lvl_cfg.length), 0.85), 0.0, 1.0)
567
- : 1.0;
568
- const has_children = next_level_preview < species.levels.length
569
- && lvl_cfg.child_count > 0
570
- && job.base_radius >= 0.006
571
- && (level === 0 || job.length >= 0.55);
572
- const span_fraction_est = job.child_span_fraction ?? Math.max(0.25, (N_seg - job.start_seg) / Math.max(1, N_seg));
573
- const effective_span = span_fraction_est * len_fraction_est;
574
- const planned_child_count = has_children
575
- ? (effective_span < 0.99 ? Math.max(1, Math.round(lvl_cfg.child_count * effective_span)) : lvl_cfg.child_count)
576
- : 0;
577
- const bare_est = clamp(lvl_cfg.bare_length_ratio, 0.0, 0.92);
578
- const t_min_est = Math.max(bare_est, job.start_seg / N_seg);
579
- const raw_base_phi = rng() * PI2;
580
- let base_phi = raw_base_phi;
581
- if (
582
- level >= 1
583
- && (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.AlternateDistichous
584
- || lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Opposite)
585
- ) {
586
- // Align distichous / opposite lateral branching plane with the horizontal plane perpendicular to T
587
- const horiz_phi = Math.atan2(-cur_ry, cur_sy);
588
- base_phi = horiz_phi + (raw_base_phi < Math.PI ? 0.0 : Math.PI) + ((raw_base_phi % Math.PI) / Math.PI - 0.5) * 0.28;
589
- }
590
- const scheduled_children = [];
591
-
592
- if (planned_child_count > 0 && 1.0 > t_min_est + 1e-3) {
593
- for (let c = 0; c < planned_child_count; c++) {
594
- let u_c = planned_child_count > 1
595
- ? (c + 0.35) / (planned_child_count - 0.55)
596
- : 0.5;
597
- if (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Opposite
598
- || lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.OppositeDecussate) {
599
- const pair_idx = c >> 1;
600
- const pair_count = Math.max(1, Math.ceil(planned_child_count / 2));
601
- u_c = pair_count > 1 ? (pair_idx + 0.35) / (pair_count - 0.55) : 0.5;
602
- } else if (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled) {
603
- const w_size = Math.max(1, lvl_cfg.whorl_count | 0);
604
- const whorl_idx = Math.floor(c / w_size);
605
- const whorl_total = Math.max(1, Math.ceil(planned_child_count / w_size));
606
- const u_lin = whorl_total > 1 ? (whorl_idx + 0.35) / (whorl_total - 0.55) : 0.5;
607
- // Self-similar conical internode compression toward the apex (p > 1 => du/dt decreases as u -> 1)
608
- u_c = level === 0 ? 1.0 - Math.pow(1.0 - clamp(u_lin, 0.0, 1.0), 1.42) : u_lin;
609
- }
610
-
611
- const t_att = clamp(
612
- lerp(t_min_est, 0.968, clamp(u_c, 0.0, 1.0)) + (rng() - 0.5) * (0.24 / planned_child_count),
613
- t_min_est,
614
- 0.98
615
- );
616
-
617
- let phi_c = base_phi;
618
- switch (lvl_cfg.phyllotaxis_mode) {
619
- case PhyllotaxisMode.AlternateDistichous:
620
- phi_c += c * Math.PI;
621
- break;
622
- case PhyllotaxisMode.Tristichous:
623
- phi_c += c * (PI2 / 3.0);
624
- break;
625
- case PhyllotaxisMode.Opposite:
626
- phi_c += (c & 1) * Math.PI;
627
- break;
628
- case PhyllotaxisMode.OppositeDecussate:
629
- phi_c += (c & 1) * Math.PI + (c >> 1) * (Math.PI * 0.5);
630
- break;
631
- case PhyllotaxisMode.Whorled: {
632
- const w_size = Math.max(1, lvl_cfg.whorl_count | 0);
633
- const w_idx = Math.floor(c / w_size);
634
- const sub_idx = c % w_size;
635
- phi_c += (sub_idx * PI2) / w_size + (w_idx * Math.PI) / w_size;
636
- break;
637
- }
638
- case PhyllotaxisMode.SpiralGolden:
639
- default:
640
- phi_c += c * GOLDEN_ANGLE;
641
- break;
642
- }
643
- phi_c += (rng() * 2.0 - 1.0) * lvl_cfg.azimuth_jitter;
644
- scheduled_children.push({ t_attach: t_att, phi: phi_c });
645
- }
646
- }
647
-
648
- const k_symp = lvl_cfg.nodal_kink_strength ?? 0.45;
649
- const zig_base = lvl_cfg.internode_zigzag ?? 0.06;
650
- const tip_up = lvl_cfg.tip_upcurl ?? 0.18;
651
-
652
- // Horizontal emergence direction of this branch for outward radial consistency near the trunk centerline
653
- const emerge_horiz_len = Math.hypot(job.tx, job.tz);
654
- const emerge_hx = emerge_horiz_len > 1e-4 ? job.tx / emerge_horiz_len : cur_rx;
655
- const emerge_hz = emerge_horiz_len > 1e-4 ? job.tz / emerge_horiz_len : cur_rz;
656
-
657
- for (let s = job.start_seg + 1; s <= N_seg; s++) {
658
- const t_prev = (s - 1) / N_seg;
659
- const t_curr = s / N_seg;
660
-
661
- // 1. Intrinsic S-curve pitch curvature (zero-mean higher harmonic on trunk so trunk weaves rather than leaning)
662
- let s_wave = level === 0
663
- ? (lvl_cfg.curvature * Math.sin(PI2 * 1.5 * t_curr + phase_hash * PI2))
664
- : (lvl_cfg.curve_back !== 0.0
665
- ? (lvl_cfg.curvature * Math.cos(Math.PI * t_curr) + lvl_cfg.curve_back * Math.sin(PI2 * t_curr))
666
- : lvl_cfg.curvature);
667
- // Attenuate negative (inward-toward-parent) pitch curvature on steep upper-crown insertions
668
- // so upper lateral branches never curve backward across the parent stem axis.
669
- if (level >= 1 && s_wave < 0.0 && job.parent_attach_t > 0.60) {
670
- s_wave *= clamp(1.0 - (job.parent_attach_t - 0.60) * 2.2, 0.15, 1.0);
671
- }
672
- const d_pitch = s_wave / Math.max(2, N_seg * 0.65);
673
-
674
- if (Math.abs(d_pitch) > 1e-6) {
675
- rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, cur_sx, cur_sy, cur_sz, d_pitch);
676
- cur_tx = rot_scratch[0];
677
- cur_ty = rot_scratch[1];
678
- cur_tz = rot_scratch[2];
679
- rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_sx, cur_sy, cur_sz, d_pitch);
680
- cur_rx = rot_scratch[0];
681
- cur_ry = rot_scratch[1];
682
- cur_rz = rot_scratch[2];
683
- }
684
-
685
- // 2. Helical spiral twist around T
686
- if (Math.abs(lvl_cfg.spiral_twist) > 1e-6) {
687
- const d_twist = lvl_cfg.spiral_twist / N_seg;
688
- rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_tx, cur_ty, cur_tz, d_twist);
689
- cur_rx = rot_scratch[0];
690
- cur_ry = rot_scratch[1];
691
- cur_rz = rot_scratch[2];
692
- cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
693
- cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
694
- cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
695
- }
696
-
697
- // 2b. Sympodial Nodal Kinking at scheduled lateral child emissions + Internodal Zig-Zag
698
- let kink_rx = 0.0;
699
- let kink_ry = 0.0;
700
- let kink_rz = 0.0;
701
-
702
- if (k_symp > 0.0 && scheduled_children.length > 0 && lvl_cfg.phyllotaxis_mode !== PhyllotaxisMode.Whorled) {
703
- for (let c = 0; c < scheduled_children.length; c++) {
704
- const sc = scheduled_children[c];
705
- if (sc.t_attach >= t_prev && sc.t_attach <= t_curr) {
706
- // Recoil away from child branch emission azimuth phi
707
- const c_phi = Math.cos(sc.phi);
708
- const s_phi = Math.sin(sc.phi);
709
- const em_x = c_phi * cur_rx + s_phi * cur_sx;
710
- const em_y = c_phi * cur_ry + s_phi * cur_sy;
711
- const em_z = c_phi * cur_rz + s_phi * cur_sz;
712
- const apical_boost = sc.t_attach > 0.85 ? 1.35 : 1.0;
713
- const recoil = k_symp * (level === 0 ? 0.34 : 0.48) * apical_boost * (0.75 + 0.5 * rng());
714
- kink_rx -= em_x * recoil;
715
- kink_ry -= em_y * recoil;
716
- kink_rz -= em_z * recoil;
717
- }
718
- }
719
- }
720
-
721
- if (zig_base > 0.0) {
722
- const z_sign = (s & 1) === 0 ? 1.0 : -1.0;
723
- const z_phi = phase_hash * PI2 + s * 0.85;
724
- const zx = Math.cos(z_phi) * cur_rx + Math.sin(z_phi) * cur_sx;
725
- const zy = Math.cos(z_phi) * cur_ry + Math.sin(z_phi) * cur_sy;
726
- const zz = Math.cos(z_phi) * cur_rz + Math.sin(z_phi) * cur_sz;
727
- const z_amp = zig_base * z_sign * (0.7 + 0.6 * rng());
728
- kink_rx += zx * z_amp;
729
- kink_ry += zy * z_amp;
730
- kink_rz += zz * z_amp;
731
- }
732
-
733
- const kink_mag = Math.hypot(kink_rx, kink_ry, kink_rz);
734
- if (kink_mag > 1e-5) {
735
- const kx = kink_rx / kink_mag;
736
- const ky = kink_ry / kink_mag;
737
- const kz = kink_rz / kink_mag;
738
- // Bend axis = T x K
739
- let bx = cur_ty * kz - cur_tz * ky;
740
- let by = cur_tz * kx - cur_tx * kz;
741
- let bz = cur_tx * ky - cur_ty * kx;
742
- const b_len = Math.hypot(bx, by, bz);
743
- if (b_len > 1e-6) {
744
- bx /= b_len;
745
- by /= b_len;
746
- bz /= b_len;
747
- const clamped_kink = Math.min(0.42, kink_mag);
748
- rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, bx, by, bz, clamped_kink);
749
- cur_tx = rot_scratch[0];
750
- cur_ty = rot_scratch[1];
751
- cur_tz = rot_scratch[2];
752
- rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, bx, by, bz, clamped_kink);
753
- cur_rx = rot_scratch[0];
754
- cur_ry = rot_scratch[1];
755
- cur_rz = rot_scratch[2];
756
- cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
757
- cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
758
- cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
759
- }
760
- }
761
-
762
- // 3. Biomechanical Tropism steering (Gravitropic sag + Terminal Phototropic Up-Curl + Outward spread)
763
- let steer_x = 0.0;
764
- let steer_y = 0.0;
765
- let steer_z = 0.0;
766
-
767
- if (level === 0 && branch_id === 0) {
768
- // Orthotropic upward striving & plumb-line righting reflex along guide curve (default vertical +Y)
769
- evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, t_prev, job.length);
770
- evaluate_trunk_guide(SCRATCH_GUIDE_P1, SCRATCH_GUIDE_T, species.trunk_guide, t_curr, job.length);
771
- const gtx = SCRATCH_GUIDE_T[0];
772
- const gty = SCRATCH_GUIDE_T[1];
773
- const gtz = SCRATCH_GUIDE_T[2];
774
-
775
- const is_conifer = lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled;
776
- const apical_relax = (!is_conifer && (t_curr > 0.80 || job.has_split)) ? 0.55 : 1.0;
777
- const righting = (1.65 + Math.max(0.0, -lvl_cfg.gravitropism) * 2.5 + Math.max(0.0, lvl_cfg.phototropism)) * apical_relax;
778
- steer_x += gtx * righting;
779
- steer_y += gty * righting;
780
- steer_z += gtz * righting;
781
-
782
- // Plumb-line centering force pulling lateral drift back toward the guide curve
783
- const dx_g = cur_px - SCRATCH_GUIDE_P0[0];
784
- const dy_g = cur_py - SCRATCH_GUIDE_P0[1];
785
- const dz_g = cur_pz - SCRATCH_GUIDE_P0[2];
786
- const dot_g = dx_g * gtx + dy_g * gty + dz_g * gtz;
787
- const lat_x = dx_g - dot_g * gtx;
788
- const lat_y = dy_g - dot_g * gty;
789
- const lat_z = dz_g - dot_g * gtz;
790
- const plumb_k = ((is_conifer ? 5.2 : 3.4) * apical_relax) / Math.max(1.0, job.length);
791
- steer_x -= lat_x * plumb_k * N_seg * 0.5;
792
- steer_y -= lat_y * plumb_k * N_seg * 0.5;
793
- steer_z -= lat_z * plumb_k * N_seg * 0.5;
794
- } else if (level === 0 && branch_id > 0) {
795
- // Secondary bifurcated trunk leader (fork): ascend skyward while preserving fork divergence
796
- steer_x += emerge_hx * 0.35;
797
- steer_y += 1.25 + Math.max(0.0, lvl_cfg.phototropism);
798
- steer_z += emerge_hz * 0.35;
799
- } else {
800
- if (lvl_cfg.gravitropism !== 0.0 || tip_up !== 0.0) {
801
- // Continuous blend from cantilever mid-branch sag arch (sin(pi * t^0.75)) to
802
- // pendulous weeping cascade (0.65 + 0.95 * t) based on gravitropism vs. tip_upcurl
803
- const w_pendulous = level >= 2
804
- ? clamp((lvl_cfg.gravitropism - 0.35 - 0.5 * Math.max(0.0, tip_up)) / 0.45, 0.0, 1.0)
805
- : 0.0;
806
- const arch_grav_env = Math.sin(Math.PI * Math.pow(t_curr, 0.75));
807
- const weep_grav_env = 0.65 + 0.95 * t_curr;
808
- const grav_env = lerp(arch_grav_env, weep_grav_env, w_pendulous);
809
- const apical_grav_atten = level === 1 && job.parent_attach_t > 0.75 && w_pendulous < 0.1
810
- ? clamp(1.0 - (job.parent_attach_t - 0.75) * 3.2, 0.15, 1.0)
811
- : 1.0;
812
- const up_env = t_curr * t_curr * (1.0 + 0.5 * Math.max(0.0, -cur_ty));
813
- steer_y += -lvl_cfg.gravitropism * grav_env * apical_grav_atten + tip_up * up_env;
814
- }
815
-
816
- if (lvl_cfg.phototropism !== 0.0) {
817
- const sky_mix = crown.sky_phototropism_mix;
818
- const horiz_r = Math.sqrt(cur_px * cur_px + cur_pz * cur_pz);
819
- let out_x = 0.0;
820
- let out_z = 0.0;
821
- if (horiz_r > 0.52) {
822
- out_x = cur_px / horiz_r;
823
- out_z = cur_pz / horiz_r;
824
- } else if (horiz_r > 1e-4) {
825
- const w_rad = horiz_r / 0.52;
826
- const bx = lerp(emerge_hx, cur_px / horiz_r, w_rad);
827
- const bz = lerp(emerge_hz, cur_pz / horiz_r, w_rad);
828
- const bl = Math.hypot(bx, bz) || 1.0;
829
- out_x = bx / bl;
830
- out_z = bz / bl;
831
- } else {
832
- out_x = emerge_hx;
833
- out_z = emerge_hz;
834
- }
835
-
836
- steer_x += lvl_cfg.phototropism * (1.0 - sky_mix) * out_x;
837
- steer_y += lvl_cfg.phototropism * sky_mix;
838
- steer_z += lvl_cfg.phototropism * (1.0 - sky_mix) * out_z;
839
- }
840
- }
841
-
842
- // 4. 2-Octave 3D Curl Noise gnarl scaled properly by sqrt(6 / N_seg)
843
- if (lvl_cfg.gnarl_strength > 0.0) {
844
- const freq = lvl_cfg.gnarl_frequency;
845
- curl_noise_3d(
846
- SCRATCH_CURL,
847
- cur_px * freq + noise_ox + level * 17.3,
848
- cur_py * freq + noise_oy + level * 31.7,
849
- cur_pz * freq + noise_oz + level * 47.1
850
- );
851
- const c1x = SCRATCH_CURL[0], c1y = SCRATCH_CURL[1], c1z = SCRATCH_CURL[2];
852
- curl_noise_3d(
853
- SCRATCH_CURL,
854
- cur_px * freq * 2.15 + noise_ox + 53.1 + phase_hash * 29.3,
855
- cur_py * freq * 2.15 + noise_oy + 79.3,
856
- cur_pz * freq * 2.15 + noise_oz + 113.7
857
- );
858
- const cx_tot = 0.68 * c1x + 0.32 * SCRATCH_CURL[0];
859
- const cy_tot = 0.68 * c1y + 0.32 * SCRATCH_CURL[1];
860
- const cz_tot = 0.68 * c1z + 0.32 * SCRATCH_CURL[2];
861
-
862
- const gnarl_scale = lvl_cfg.gnarl_strength * Math.sqrt(6.0 / Math.max(2, N_seg));
863
- // Project curl vector perpendicular to T so it steers direction cleanly
864
- const dot_ct = cx_tot * cur_tx + cy_tot * cur_ty + cz_tot * cur_tz;
865
- steer_x += (cx_tot - dot_ct * cur_tx) * gnarl_scale * 2.2;
866
- steer_y += (cy_tot - dot_ct * cur_ty) * gnarl_scale * 1.6;
867
- steer_z += (cz_tot - dot_ct * cur_tz) * gnarl_scale * 2.2;
868
- }
869
-
870
- // 5. Space colonization attraction steering (for branches in crown, level >= 1)
871
- if (use_space_col && level >= 1 && attr_grid !== null) {
872
- let attr_vx = 0.0;
873
- let attr_vy = 0.0;
874
- let attr_vz = 0.0;
875
- let attr_hits = 0;
876
-
877
- attr_grid.forEachNear(cur_px, cur_py, cur_pz, (idx) => {
878
- if (attr_alive[idx] === 0) return;
879
- const ax = attr_positions[idx * 3] - cur_px;
880
- const ay = attr_positions[idx * 3 + 1] - cur_py;
881
- const az = attr_positions[idx * 3 + 2] - cur_pz;
882
- const d2 = ax * ax + ay * ay + az * az;
883
- if (d2 <= r_kill_sq) {
884
- attr_alive[idx] = 0;
885
- } else if (d2 <= r_influence_sq && d2 > 1e-8) {
886
- const inv_d = 1.0 / Math.sqrt(d2);
887
- attr_vx += ax * inv_d;
888
- attr_vy += ay * inv_d;
889
- attr_vz += az * inv_d;
890
- attr_hits++;
891
- }
892
- });
893
-
894
- if (attr_hits > 0) {
895
- const attr_len = Math.sqrt(attr_vx * attr_vx + attr_vy * attr_vy + attr_vz * attr_vz);
896
- if (attr_len > 1e-6) {
897
- const w_sc = crown.space_colonization_weight / attr_len;
898
- steer_x += attr_vx * w_sc;
899
- steer_y += attr_vy * w_sc;
900
- steer_z += attr_vz * w_sc;
901
- }
902
- }
903
- }
904
-
905
- // 6. Crown envelope soft containment for lateral branches (level >= 1)
906
- let seg_len = step_len;
907
- if (level >= 1 && crown.prune_ratio > 0.0) {
908
- const probe_x = cur_px + cur_tx * step_len;
909
- const probe_y = cur_py + cur_ty * step_len;
910
- const probe_z = cur_pz + cur_tz * step_len;
911
- const sdf = crown_envelope_sdf(probe_x, probe_y, probe_z, crown);
912
- if (sdf > 0.0) {
913
- // Steer radially inward toward trunk axis (only pulling vertically if outside vertical crown span)
914
- const crown_top_y = crown.base_height + crown.height;
915
- const to_cx = -probe_x;
916
- const to_cy = probe_y > crown_top_y
917
- ? crown_top_y - probe_y
918
- : (probe_y < crown.base_height ? crown.base_height - probe_y : 0.0);
919
- const to_cz = -probe_z;
920
- const to_c_len = Math.sqrt(to_cx * to_cx + to_cy * to_cy + to_cz * to_cz);
921
- if (to_c_len > 1e-5) {
922
- const pull = crown.prune_ratio * 0.45 / to_c_len;
923
- steer_x += to_cx * pull;
924
- steer_y += to_cy * pull;
925
- steer_z += to_cz * pull;
926
- }
927
- seg_len *= Math.max(0.35, 1.0 - crown.prune_ratio * 0.5);
928
- }
929
- }
930
-
931
- // Compute target radius r_curr for this segment before BVH collision checks
932
- const t_spine = level === 0 ? lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_curr) : t_curr;
933
- const u_prof = prof_span > 1e-4
934
- ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(t_spine)) / prof_span, 0.0, 1.0)
935
- : t_spine;
936
- const min_tip_frac = level === 0 ? 0.12 : (has_children ? 0.14 : 0.24);
937
- const apical_shoot_taper = level === 0
938
- ? (t_spine > 0.65 ? lerp(1.0, 0.11, Math.pow((t_spine - 0.65) / 0.35, 1.45)) : 1.0)
939
- : (has_children && t_curr > 0.80 ? lerp(1.0, 0.62, Math.pow((t_curr - 0.80) / 0.20, 1.35)) : 1.0);
940
- const r_curr = Math.max(
941
- level === 0 ? 0.0015 : 0.002,
942
- job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_prof, min_tip_frac) * apical_shoot_taper
943
- );
944
- const r_prev = out.node_read_radius(prev_v);
945
- const local_seg_idx = branch_edges.length;
946
- const parent_bA = job.parent_branch_id;
947
- const attach_seg_A = job.parent_attach_seg_idx ?? -1;
948
- const is_split_A = Boolean(job.is_split_fork);
949
-
950
- // 6b. Soft BVH Repulsion Field (steers growing shoot smoothly around nearby existing branches)
951
- if ((level >= 1 || branch_id > 0) && bvh.size > 0) {
952
- const look_len = seg_len * 1.55;
953
- const look_x = cur_px + cur_tx * look_len;
954
- const look_y = cur_py + cur_ty * look_len;
955
- const look_z = cur_pz + cur_tz * look_len;
956
- const soft_margin = Math.max(0.08, Math.min(0.24, seg_len * 0.65));
957
-
958
- query_capsule[0] = cur_px;
959
- query_capsule[1] = cur_py;
960
- query_capsule[2] = cur_pz;
961
- query_capsule[3] = look_x;
962
- query_capsule[4] = look_y;
963
- query_capsule[5] = look_z;
964
- query_capsule[6] = Math.max(r_prev, r_curr) * 1.15 + soft_margin;
965
-
966
- const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
967
- for (let h = 0; h < hit_count; h++) {
968
- const eB = bvh_hits[h];
969
- const bB = seg_branch_id[eB];
970
- const local_idx_B = seg_local_idx[eB];
971
- if (!resolve_subsegment_bounds(
972
- branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
973
- bB, out.branch_parent[bB], branch_parent_seg_idx[bB], branch_is_split_fork[bB], local_idx_B,
974
- sub_bounds
975
- )) {
976
- continue;
977
- }
978
-
979
- const s_min = sub_bounds[0];
980
- const t_min = sub_bounds[1];
981
- const sa0x = cur_px + (look_x - cur_px) * s_min;
982
- const sa0y = cur_py + (look_y - cur_py) * s_min;
983
- const sa0z = cur_pz + (look_z - cur_pz) * s_min;
984
-
985
- const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
986
- const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
987
- const sb0x = b0x + (b1x - b0x) * t_min;
988
- const sb0y = b0y + (b1y - b0y) * t_min;
989
- const sb0z = b0z + (b1z - b0z) * t_min;
990
-
991
- line3_compute_segment_segment_closest_points(
992
- closest_st,
993
- sa0x, sa0y, sa0z, look_x, look_y, look_z,
994
- sb0x, sb0y, sb0z, b1x, b1y, b1z
995
- );
996
-
997
- const s_f = s_min + (1.0 - s_min) * closest_st[0];
998
- const t_f = t_min + (1.0 - t_min) * closest_st[1];
999
-
1000
- const qAx = cur_px + (look_x - cur_px) * s_f;
1001
- const qAy = cur_py + (look_y - cur_py) * s_f;
1002
- const qAz = cur_pz + (look_z - cur_pz) * s_f;
1003
- const qBx = b0x + (b1x - b0x) * t_f;
1004
- const qBy = b0y + (b1y - b0y) * t_f;
1005
- const qBz = b0z + (b1z - b0z) * t_f;
1006
-
1007
- const dx = qAx - qBx;
1008
- const dy = qAy - qBy;
1009
- const dz = qAz - qBz;
1010
- const dist = Math.hypot(dx, dy, dz);
1011
- const rA = r_prev + (r_curr - r_prev) * Math.min(1.0, s_f);
1012
- const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
1013
- const r_req = (rA + rB) * 1.12 + 0.005;
1014
-
1015
- if (dist < r_req + soft_margin) {
1016
- let nx = dist > 1e-5 ? dx / dist : cur_rx;
1017
- let ny = dist > 1e-5 ? dy / dist : cur_ry;
1018
- let nz = dist > 1e-5 ? dz / dist : cur_rz;
1019
- // Project repulsion normal perpendicular to cur_T so it steers smoothly around the obstacle
1020
- const dot_nt = nx * cur_tx + ny * cur_ty + nz * cur_tz;
1021
- nx -= dot_nt * cur_tx;
1022
- ny -= dot_nt * cur_ty;
1023
- nz -= dot_nt * cur_tz;
1024
- const n_len = Math.hypot(nx, ny, nz);
1025
- if (n_len > 1e-5) {
1026
- nx /= n_len;
1027
- ny /= n_len;
1028
- nz /= n_len;
1029
- const u_prox = clamp(1.0 - Math.max(0.0, dist - r_req) / soft_margin, 0.0, 1.0);
1030
- const rep_w = u_prox * u_prox * 2.8;
1031
- steer_x += nx * rep_w;
1032
- steer_y += ny * rep_w;
1033
- steer_z += nz * rep_w;
1034
- }
1035
- }
1036
- }
1037
- }
1038
-
1039
- // Apply steering to tangent and normalize
1040
- const steer_div = Math.max(2.0, Math.sqrt(N_seg) * 1.35);
1041
- let next_tx = cur_tx + steer_x / steer_div;
1042
- let next_ty = cur_ty + steer_y / steer_div;
1043
- let next_tz = cur_tz + steer_z / steer_div;
1044
- const t_len = Math.sqrt(next_tx * next_tx + next_ty * next_ty + next_tz * next_tz);
1045
- if (t_len > 1e-12) {
1046
- next_tx /= t_len;
1047
- next_ty /= t_len;
1048
- next_tz /= t_len;
1049
- } else {
1050
- next_tx = cur_tx;
1051
- next_ty = cur_ty;
1052
- next_tz = cur_tz;
1053
- }
1054
-
1055
- let next_px = cur_px + 0.5 * (cur_tx + next_tx) * seg_len;
1056
- let next_py = cur_py + 0.5 * (cur_ty + next_ty) * seg_len;
1057
- let next_pz = cur_pz + 0.5 * (cur_tz + next_tz) * seg_len;
1058
-
1059
- // Orthotropic plumb-tube containment on trunk (level === 0):
1060
- // Preserves local 3D nodal kinks and serpentine sweep while guaranteeing the trunk never drifts sideways
1061
- if (level === 0 && branch_id === 0) {
1062
- const gx = SCRATCH_GUIDE_P1[0];
1063
- const gy = SCRATCH_GUIDE_P1[1];
1064
- const gz = SCRATCH_GUIDE_P1[2];
1065
- const gtx = SCRATCH_GUIDE_T[0];
1066
- const gty = SCRATCH_GUIDE_T[1];
1067
- const gtz = SCRATCH_GUIDE_T[2];
1068
-
1069
- const dx = next_px - gx;
1070
- const dy = next_py - gy;
1071
- const dz = next_pz - gz;
1072
- const dot_g = dx * gtx + dy * gty + dz * gtz;
1073
- const off_x = dx - dot_g * gtx;
1074
- const off_y = dy - dot_g * gty;
1075
- const off_z = dz - dot_g * gtz;
1076
- const off_r = Math.hypot(off_x, off_y, off_z);
1077
-
1078
- const is_conifer = lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled;
1079
- const max_drift_frac = is_conifer ? 0.032 : (job.has_split ? 0.078 : 0.058);
1080
- // Envelope rises from 0 at root, peaks in mid-trunk (allowing 3D sweep/crookedness), and tapers near tip
1081
- const env = Math.sin(Math.PI * Math.min(0.92, t_curr * 0.88)) * (1.0 - 0.38 * t_curr * t_curr);
1082
- const max_r = job.length * max_drift_frac * Math.max(job.has_split ? 0.42 : 0.18, env);
1083
-
1084
- if (off_r > max_r && off_r > 1e-6) {
1085
- const scale_r = max_r / off_r;
1086
- next_px = gx + dot_g * gtx + off_x * scale_r;
1087
- next_py = gy + dot_g * gty + off_y * scale_r;
1088
- next_pz = gz + dot_g * gtz + off_z * scale_r;
1089
-
1090
- let ch_x = next_px - cur_px;
1091
- let ch_y = next_py - cur_py;
1092
- let ch_z = next_pz - cur_pz;
1093
- const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1094
- if (ch_l > 1e-6) {
1095
- next_tx = ch_x / ch_l;
1096
- next_ty = ch_y / ch_l;
1097
- next_tz = ch_z / ch_l;
1098
- }
1099
- }
1100
- }
1101
-
1102
- // 6c. Monotonic Parent Collar Escape + Hard BVH Capsule Contact Projection & Self-Pruning
1103
- if ((level >= 1 || branch_id > 0) && bvh.size > 0) {
1104
- let was_projected = false;
1105
-
1106
- // Ensure lateral child branch exits its parent attachment segment cleanly on local_seg_idx <= 1
1107
- if (job.parent_edge_id >= 0 && !is_split_A && local_seg_idx <= 1) {
1108
- const eP = job.parent_edge_id;
1109
- const p0x = seg_p0x[eP], p0y = seg_p0y[eP], p0z = seg_p0z[eP];
1110
- const p1x = seg_p1x[eP], p1y = seg_p1y[eP], p1z = seg_p1z[eP];
1111
-
1112
- line3_compute_segment_segment_closest_points(
1113
- closest_st,
1114
- next_px, next_py, next_pz, next_px, next_py, next_pz,
1115
- p0x, p0y, p0z, p1x, p1y, p1z
1116
- );
1117
- const t_p = closest_st[1];
1118
- const qPx = p0x + (p1x - p0x) * t_p;
1119
- const qPy = p0y + (p1y - p0y) * t_p;
1120
- const qPz = p0z + (p1z - p0z) * t_p;
1121
- const r_p = seg_r0[eP] + (seg_r1[eP] - seg_r0[eP]) * t_p;
1122
-
1123
- let rx_p = next_px - qPx;
1124
- let ry_p = next_py - qPy;
1125
- let rz_p = next_pz - qPz;
1126
- const d_p = Math.hypot(rx_p, ry_p, rz_p);
1127
- if (local_seg_idx === 0 && (r_p + r_curr) * 1.18 > seg_len) {
1128
- seg_len = (r_p + r_curr) * 1.18;
1129
- }
1130
- const min_d_p = local_seg_idx === 0
1131
- ? Math.min(seg_len * 0.88, (r_p + r_curr) * 1.05)
1132
- : (r_p + r_curr) * 1.10 + 0.004;
1133
-
1134
- const em_drx = job.emerge_drx ?? cur_rx;
1135
- const em_dry = job.emerge_dry ?? cur_ry;
1136
- const em_drz = job.emerge_drz ?? cur_rz;
1137
- const dot_em = rx_p * em_drx + ry_p * em_dry + rz_p * em_drz;
1138
-
1139
- if (d_p < min_d_p || dot_em < 0.0) {
1140
- const ux_p = (d_p > 1e-5 && dot_em > 0.0) ? rx_p / d_p : em_drx;
1141
- const uy_p = (d_p > 1e-5 && dot_em > 0.0) ? ry_p / d_p : em_dry;
1142
- const uz_p = (d_p > 1e-5 && dot_em > 0.0) ? rz_p / d_p : em_drz;
1143
- const push_d = Math.max(0.0, min_d_p - (dot_em > 0.0 ? d_p : -d_p));
1144
- next_px += ux_p * push_d;
1145
- next_py += uy_p * push_d;
1146
- next_pz += uz_p * push_d;
1147
-
1148
- let ch_x = next_px - cur_px;
1149
- let ch_y = next_py - cur_py;
1150
- let ch_z = next_pz - cur_pz;
1151
- const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1152
- if (ch_l > 1e-6) {
1153
- next_tx = ch_x / ch_l;
1154
- next_ty = ch_y / ch_l;
1155
- next_tz = ch_z / ch_l;
1156
- next_px = cur_px + next_tx * seg_len;
1157
- next_py = cur_py + next_ty * seg_len;
1158
- next_pz = cur_pz + next_tz * seg_len;
1159
- was_projected = true;
1160
- }
1161
- }
1162
- }
1163
-
1164
- // Iterative BVH capsule contact projection
1165
- for (let iter = 0; iter < 4; iter++) {
1166
- query_capsule[0] = cur_px;
1167
- query_capsule[1] = cur_py;
1168
- query_capsule[2] = cur_pz;
1169
- query_capsule[3] = next_px;
1170
- query_capsule[4] = next_py;
1171
- query_capsule[5] = next_pz;
1172
- query_capsule[6] = Math.max(r_prev, r_curr) * 1.15 + 0.025;
1173
-
1174
- const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
1175
- let moved = false;
1176
-
1177
- for (let h = 0; h < hit_count; h++) {
1178
- const eB = bvh_hits[h];
1179
- const bB = seg_branch_id[eB];
1180
- const local_idx_B = seg_local_idx[eB];
1181
- if (!resolve_subsegment_bounds(
1182
- branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
1183
- bB, out.branch_parent[bB], branch_parent_seg_idx[bB], branch_is_split_fork[bB], local_idx_B,
1184
- sub_bounds
1185
- )) {
1186
- continue;
1187
- }
1188
-
1189
- const s_min = sub_bounds[0];
1190
- const t_min = sub_bounds[1];
1191
- const sa0x = cur_px + (next_px - cur_px) * s_min;
1192
- const sa0y = cur_py + (next_py - cur_py) * s_min;
1193
- const sa0z = cur_pz + (next_pz - cur_pz) * s_min;
1194
-
1195
- const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
1196
- const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
1197
- const sb0x = b0x + (b1x - b0x) * t_min;
1198
- const sb0y = b0y + (b1y - b0y) * t_min;
1199
- const sb0z = b0z + (b1z - b0z) * t_min;
1200
-
1201
- line3_compute_segment_segment_closest_points(
1202
- closest_st,
1203
- sa0x, sa0y, sa0z, next_px, next_py, next_pz,
1204
- sb0x, sb0y, sb0z, b1x, b1y, b1z
1205
- );
1206
-
1207
- const s_f = s_min + (1.0 - s_min) * closest_st[0];
1208
- const t_f = t_min + (1.0 - t_min) * closest_st[1];
1209
- if (local_seg_idx === 0 && s_f <= 0.05) continue;
1210
-
1211
- const qAx = cur_px + (next_px - cur_px) * s_f;
1212
- const qAy = cur_py + (next_py - cur_py) * s_f;
1213
- const qAz = cur_pz + (next_pz - cur_pz) * s_f;
1214
- const qBx = b0x + (b1x - b0x) * t_f;
1215
- const qBy = b0y + (b1y - b0y) * t_f;
1216
- const qBz = b0z + (b1z - b0z) * t_f;
1217
-
1218
- const dx = qAx - qBx;
1219
- const dy = qAy - qBy;
1220
- const dz = qAz - qBz;
1221
- const dist = Math.hypot(dx, dy, dz);
1222
- const rA = r_prev + (r_curr - r_prev) * s_f;
1223
- const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
1224
- const r_req = (rA + rB) * 1.08 + 0.004;
1225
-
1226
- if (dist < r_req) {
1227
- let nx = dist > 1e-5 ? dx / dist : cur_rx;
1228
- let ny = dist > 1e-5 ? dy / dist : cur_ry;
1229
- let nz = dist > 1e-5 ? dz / dist : cur_rz;
1230
- const push = (r_req - dist + 0.004) / Math.max(0.35, s_f);
1231
- next_px += nx * push;
1232
- next_py += ny * push;
1233
- next_pz += nz * push;
1234
-
1235
- let ch_x = next_px - cur_px;
1236
- let ch_y = next_py - cur_py;
1237
- let ch_z = next_pz - cur_pz;
1238
- const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1239
- if (ch_l > 1e-6) {
1240
- next_tx = ch_x / ch_l;
1241
- next_ty = ch_y / ch_l;
1242
- next_tz = ch_z / ch_l;
1243
- next_px = cur_px + next_tx * seg_len;
1244
- next_py = cur_py + next_ty * seg_len;
1245
- next_pz = cur_pz + next_tz * seg_len;
1246
- moved = true;
1247
- was_projected = true;
1248
- }
1249
- }
1250
- }
1251
- if (!moved) break;
1252
- }
1253
-
1254
- // If segment >= 1 still penetrates an existing branch or was forced into an unnatural sharp reversal,
1255
- // self-prune / arrest the branch at prev_v so it never spears through another bough.
1256
- if (local_seg_idx >= 1) {
1257
- const final_clear = evaluate_candidate_clearance(
1258
- branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
1259
- cur_px, cur_py, cur_pz, r_prev,
1260
- next_px, next_py, next_pz, r_curr,
1261
- -0.004
1262
- );
1263
- const turn_cos = cur_tx * next_tx + cur_ty * next_ty + cur_tz * next_tz;
1264
- if (final_clear < 0.0 || (was_projected && turn_cos < 0.25)) {
1265
- break;
1266
- }
1267
- } else if (was_projected) {
1268
- // Keep root node tangent aligned with projected first segment so base collar has zero S-hook
1269
- cur_tx = next_tx;
1270
- cur_ty = next_ty;
1271
- cur_tz = next_tz;
1272
- out.node_write_tangent(prev_v, cur_tx, cur_ty, cur_tz);
1273
- }
1274
- }
1275
-
1276
- // Transport RMF via Wang et al. Double Reflection
1277
- spine_frame_double_reflection(
1278
- SCRATCH_RMF, 0,
1279
- cur_px, cur_py, cur_pz,
1280
- cur_tx, cur_ty, cur_tz,
1281
- cur_rx, cur_ry, cur_rz,
1282
- next_px, next_py, next_pz,
1283
- next_tx, next_ty, next_tz
1284
- );
1285
- let next_rx = SCRATCH_RMF[0];
1286
- let next_ry = SCRATCH_RMF[1];
1287
- let next_rz = SCRATCH_RMF[2];
1288
- cur_sx = SCRATCH_RMF[3];
1289
- cur_sy = SCRATCH_RMF[4];
1290
- cur_sz = SCRATCH_RMF[5];
1291
-
1292
- cur_arc_s += seg_len;
1293
-
1294
- const curr_v = out.add_node(
1295
- next_px, next_py, next_pz,
1296
- next_tx, next_ty, next_tz,
1297
- next_rx, next_ry, next_rz,
1298
- r_curr,
1299
- branch_id,
1300
- level,
1301
- job.parent_branch_id,
1302
- t_curr,
1303
- cur_arc_s,
1304
- phase_hash
1305
- );
1306
-
1307
- const edge_id = out.add_segment(
1308
- prev_v,
1309
- curr_v,
1310
- cur_tx * seg_len,
1311
- cur_ty * seg_len,
1312
- cur_tz * seg_len,
1313
- next_tx * seg_len,
1314
- next_ty * seg_len,
1315
- next_tz * seg_len,
1316
- seg_len,
1317
- t_prev === 0.0 ? job.parent_attach_t : t_prev
1318
- );
1319
-
1320
- // Register segment in scope-local BVH
1321
- const bvh_pad = Math.max(r_prev, r_curr) * 1.25 + 0.008;
1322
- const bvh_leaf = bvh.allocate_node();
1323
- bvh.node_set_user_data(bvh_leaf, edge_id);
1324
- bvh.node_set_aabb_primitive(
1325
- bvh_leaf,
1326
- Math.min(cur_px, next_px) - bvh_pad,
1327
- Math.min(cur_py, next_py) - bvh_pad,
1328
- Math.min(cur_pz, next_pz) - bvh_pad,
1329
- Math.max(cur_px, next_px) + bvh_pad,
1330
- Math.max(cur_py, next_py) + bvh_pad,
1331
- Math.max(cur_pz, next_pz) + bvh_pad
1332
- );
1333
- bvh.insert_leaf(bvh_leaf);
1334
-
1335
- seg_branch_id[edge_id] = branch_id;
1336
- seg_local_idx[edge_id] = local_seg_idx;
1337
- seg_p0x[edge_id] = cur_px;
1338
- seg_p0y[edge_id] = cur_py;
1339
- seg_p0z[edge_id] = cur_pz;
1340
- seg_r0[edge_id] = r_prev;
1341
- seg_p1x[edge_id] = next_px;
1342
- seg_p1y[edge_id] = next_py;
1343
- seg_p1z[edge_id] = next_pz;
1344
- seg_r1[edge_id] = r_curr;
1345
- seg_bvh_leaf[edge_id] = bvh_leaf;
1346
-
1347
- branch_edges.push(edge_id);
1348
- branch_edge_t0.push(t_prev);
1349
- branch_edge_t1.push(t_curr);
1350
- branch_vertices.push(curr_v);
1351
-
1352
- out.branch_tip_vertex[branch_id] = curr_v;
1353
- out.branch_length[branch_id] = cur_arc_s - job.initial_arc_s;
1354
-
1355
- // Check for dichotomous / sympodial splitting at interior segments (only when stem is structurally thick enough)
1356
- const min_split_seg = level === 0
1357
- ? Math.max(2, Math.floor(N_seg * 0.5))
1358
- : Math.max(1, Math.floor(N_seg * 0.25));
1359
- if (
1360
- !job.has_split
1361
- && lvl_cfg.split_probability > 0.0
1362
- && r_curr >= 0.012
1363
- && s >= min_split_seg
1364
- && s <= Math.floor(N_seg * 0.75)
1365
- && rng() < lvl_cfg.split_probability
1366
- ) {
1367
- job.has_split = true;
1368
- const fork_count = Math.max(2, lvl_cfg.split_count | 0);
1369
- const split_ang = lvl_cfg.split_angle;
1370
- let base_az = rng() * PI2;
1371
-
1372
- // Preserve pre-split orthonormal triad (T, R, S) before deflecting fork 0
1373
- const pre_tx = next_tx, pre_ty = next_ty, pre_tz = next_tz;
1374
- const pre_rx = next_rx, pre_ry = next_ry, pre_rz = next_rz;
1375
- const pre_sx = cur_sx, pre_sy = cur_sy, pre_sz = cur_sz;
1376
-
1377
- // On lateral branches (level >= 1), align the split bend axis toward local vertical (+Y)
1378
- // so the two daughter forks spread horizontally in the canopy rather than pitching into neighbor boughs.
1379
- if (level >= 1) {
1380
- const up_az = Math.atan2(pre_sy, pre_ry);
1381
- const side_sign = base_az < Math.PI ? 1.0 : -1.0;
1382
- base_az = up_az + (side_sign < 0.0 ? Math.PI : 0.0) + ((base_az % Math.PI) / Math.PI - 0.5) * 0.45;
1383
- }
1384
-
1385
- // Current branch deflects as fork 0: rotate both T and R around bend_axis so R remains orthogonal to T
1386
- const fork0_deflect = level === 0 ? split_ang * 0.22 : split_ang * 0.5;
1387
- const bend_axis_x = Math.cos(base_az) * pre_rx + Math.sin(base_az) * pre_sx;
1388
- const bend_axis_y = Math.cos(base_az) * pre_ry + Math.sin(base_az) * pre_sy;
1389
- const bend_axis_z = Math.cos(base_az) * pre_rz + Math.sin(base_az) * pre_sz;
1390
- rotate_around_axis(
1391
- rot_scratch,
1392
- pre_tx, pre_ty, pre_tz,
1393
- bend_axis_x, bend_axis_y, bend_axis_z,
1394
- fork0_deflect
1395
- );
1396
- next_tx = rot_scratch[0];
1397
- next_ty = rot_scratch[1];
1398
- next_tz = rot_scratch[2];
1399
-
1400
- rotate_around_axis(
1401
- rot_scratch,
1402
- pre_rx, pre_ry, pre_rz,
1403
- bend_axis_x, bend_axis_y, bend_axis_z,
1404
- fork0_deflect
1405
- );
1406
- next_rx = rot_scratch[0];
1407
- next_ry = rot_scratch[1];
1408
- next_rz = rot_scratch[2];
1409
-
1410
- cur_sx = next_ty * next_rz - next_tz * next_ry;
1411
- cur_sy = next_tz * next_rx - next_tx * next_rz;
1412
- cur_sz = next_tx * next_ry - next_ty * next_rx;
1413
-
1414
- // Spawn fork 1..fork_count-1 as continuation branches with their own root vertex connected to curr_v
1415
- const rem_segs = Math.max(1, N_seg - s);
1416
- const rem_fraction = Math.max(0.15, rem_segs / Math.max(1, N_seg));
1417
- const fork_cum_t0 = lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_curr);
1418
- const fork_cum_t1 = job.cum_t1 ?? 1.0;
1419
- const fork_len = Math.max(0.15, job.length * rem_fraction);
1420
- const fork_r0 = r_curr * 0.78;
1421
- const fork_step = fork_len / rem_segs;
1422
-
1423
- for (let f = 1; f < fork_count; f++) {
1424
- const f_az = base_az + (f * PI2) / fork_count;
1425
- const fax = Math.cos(f_az) * pre_rx + Math.sin(f_az) * pre_sx;
1426
- const fay = Math.cos(f_az) * pre_ry + Math.sin(f_az) * pre_sy;
1427
- const faz = Math.cos(f_az) * pre_rz + Math.sin(f_az) * pre_sz;
1428
-
1429
- const f_tan = [0, 0, 0];
1430
- rotate_around_axis(f_tan, pre_tx, pre_ty, pre_tz, fax, fay, faz, split_ang * 0.65);
1431
-
1432
- // Verify fork emergence clearance against BVH (and prevent inward-toward-trunk forks on lateral boughs)
1433
- if (level >= 1) {
1434
- const r_horiz_here = Math.hypot(next_px, next_pz);
1435
- if (r_horiz_here > 0.35 && (next_px * f_tan[0] + next_pz * f_tan[2]) < -0.10 * r_horiz_here) {
1436
- continue;
1437
- }
1438
- const f_clear = evaluate_candidate_clearance(
1439
- -1, branch_id, local_seg_idx, true, 0,
1440
- next_px, next_py, next_pz, fork_r0,
1441
- next_px + f_tan[0] * fork_step,
1442
- next_py + f_tan[1] * fork_step,
1443
- next_pz + f_tan[2] * fork_step,
1444
- fork_r0 * 0.85,
1445
- 0.002
1446
- );
1447
- if (f_clear < -0.002) {
1448
- continue;
1449
- }
1450
- }
1451
-
1452
- const f_r = [0, 0, 0];
1453
- rotate_around_axis(f_r, pre_rx, pre_ry, pre_rz, fax, fay, faz, split_ang * 0.65);
1454
-
1455
- queue.push({
1456
- level,
1457
- parent_branch_id: branch_id,
1458
- parent_edge_id: edge_id,
1459
- parent_attach_seg_idx: local_seg_idx,
1460
- parent_attach_t: t_curr,
1461
- attach_parent_vertex: curr_v,
1462
- start_vertex: -1,
1463
- px: next_px,
1464
- py: next_py,
1465
- pz: next_pz,
1466
- tx: f_tan[0],
1467
- ty: f_tan[1],
1468
- tz: f_tan[2],
1469
- rx: f_r[0],
1470
- ry: f_r[1],
1471
- rz: f_r[2],
1472
- emerge_drx: fax,
1473
- emerge_dry: fay,
1474
- emerge_drz: faz,
1475
- length: fork_len,
1476
- full_branch_length: job.full_branch_length ?? job.length,
1477
- base_radius: fork_r0,
1478
- start_seg: 0,
1479
- total_segs: rem_segs,
1480
- initial_arc_s: cur_arc_s,
1481
- has_split: true,
1482
- is_split_fork: true,
1483
- child_span_fraction: rem_fraction,
1484
- cum_t0: fork_cum_t0,
1485
- cum_t1: fork_cum_t1
1486
- });
1487
- }
1488
- }
1489
-
1490
- cur_px = next_px;
1491
- cur_py = next_py;
1492
- cur_pz = next_pz;
1493
- cur_tx = next_tx;
1494
- cur_ty = next_ty;
1495
- cur_tz = next_tz;
1496
- cur_rx = next_rx;
1497
- cur_ry = next_ry;
1498
- cur_rz = next_rz;
1499
- prev_v = curr_v;
1500
- }
1501
-
1502
- // If the branch was self-pruned early on contact (actual_segs < remaining_segs), renormalize
1503
- // t_norm and taper radii across the surviving vertices [0..actual_segs] so its tip terminates cleanly at t_norm = 1.0
1504
- const actual_segs = branch_edges.length;
1505
- if (actual_segs > 0 && actual_segs < remaining_segs) {
1506
- const s_param = /** @type {Float32Array} */ (out.attr_node_spine_param.data);
1507
- for (let i = 0; i < actual_segs; i++) {
1508
- const tn0 = i / actual_segs;
1509
- const tn1 = (i + 1) / actual_segs;
1510
- branch_edge_t0[i] = tn0;
1511
- branch_edge_t1[i] = tn1;
1512
-
1513
- const v_curr = branch_vertices[i + 1];
1514
- s_param[v_curr * 3] = tn1;
1515
-
1516
- const u_p = prof_span > 1e-4
1517
- ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(tn1)) / prof_span, 0.0, 1.0)
1518
- : tn1;
1519
- const min_tf = has_children ? 0.14 : 0.24;
1520
- const ap_taper = has_children && tn1 > 0.80
1521
- ? lerp(1.0, 0.62, Math.pow((tn1 - 0.80) / 0.20, 1.35))
1522
- : 1.0;
1523
- const r_new = Math.max(
1524
- 0.002,
1525
- job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_p, min_tf) * ap_taper
1526
- );
1527
- out.node_write_radius(v_curr, r_new);
1528
-
1529
- const e_curr = branch_edges[i];
1530
- seg_r1[e_curr] = r_new;
1531
- if (i + 1 < actual_segs) {
1532
- seg_r0[branch_edges[i + 1]] = r_new;
1533
- }
1534
- }
1535
- }
1536
-
1537
- // Smooth or preserve crisp interior Hermite tangents across consecutive edges of this branch
1538
- // (preserving branch_edges[0]'s m0 so continuation forks keep their exact fork departure tangent)
1539
- if (branch_edges.length >= 2) {
1540
- const kink_sharp = clamp(lvl_cfg.kink_sharpness ?? 0.5, 0.0, 0.92);
1541
- for (let k = 1; k < branch_edges.length; k++) {
1542
- const e_prev = branch_edges[k - 1];
1543
- const e_next = branch_edges[k];
1544
- const v_mid = branch_vertices[k];
1545
- const l_prev = Math.max(1e-5, out.segment_length(e_prev));
1546
- const l_next = Math.max(1e-5, out.segment_length(e_next));
1547
-
1548
- out.segment_read_m1(SCRATCH_M_PREV, 0, e_prev);
1549
- out.segment_read_m0(SCRATCH_M_NEXT, 0, e_next);
1550
-
1551
- const t_in_x = SCRATCH_M_PREV[0] / l_prev;
1552
- const t_in_y = SCRATCH_M_PREV[1] / l_prev;
1553
- const t_in_z = SCRATCH_M_PREV[2] / l_prev;
1554
- const t_out_x = SCRATCH_M_NEXT[0] / l_next;
1555
- const t_out_y = SCRATCH_M_NEXT[1] / l_next;
1556
- const t_out_z = SCRATCH_M_NEXT[2] / l_next;
1557
-
1558
- let tx = t_in_x + t_out_x;
1559
- let ty = t_in_y + t_out_y;
1560
- let tz = t_in_z + t_out_z;
1561
- const t_len = Math.sqrt(tx * tx + ty * ty + tz * tz);
1562
- if (t_len > 1e-12) {
1563
- tx /= t_len;
1564
- ty /= t_len;
1565
- tz /= t_len;
1566
-
1567
- out.node_write_tangent(v_mid, tx, ty, tz);
1568
-
1569
- // Blend between smooth C1 average tangent and crisp sympodial kink tangents
1570
- let m1x = lerp(tx, t_in_x, kink_sharp);
1571
- let m1y = lerp(ty, t_in_y, kink_sharp);
1572
- let m1z = lerp(tz, t_in_z, kink_sharp);
1573
- const m1_l = Math.sqrt(m1x * m1x + m1y * m1y + m1z * m1z);
1574
- if (m1_l > 1e-12) { m1x /= m1_l; m1y /= m1_l; m1z /= m1_l; }
1575
-
1576
- let m0x = lerp(tx, t_out_x, kink_sharp);
1577
- let m0y = lerp(ty, t_out_y, kink_sharp);
1578
- let m0z = lerp(tz, t_out_z, kink_sharp);
1579
- const m0_l = Math.sqrt(m0x * m0x + m0y * m0y + m0z * m0z);
1580
- if (m0_l > 1e-12) { m0x /= m0_l; m0y /= m0_l; m0z /= m0_l; }
1581
-
1582
- out.segment_write_m1(e_prev, m1x * l_prev, m1y * l_prev, m1z * l_prev);
1583
- out.segment_write_m0(e_next, m0x * l_next, m0y * l_next, m0z * l_next);
1584
- }
1585
- }
1586
- }
1587
-
1588
- // Spawn lateral child branches at level + 1 using pre-scheduled attachments
1589
- const next_level = level + 1;
1590
- if (scheduled_children.length > 0 && branch_edges.length > 0) {
1591
- const next_cfg = species.levels[next_level];
1592
- const t_max = branch_edge_t1[branch_edge_t1.length - 1];
1593
- const PHI_CANDIDATES = [0.0, 0.36, -0.36, 0.72, -0.72, 1.08, -1.08, 1.48, -1.48];
1594
-
1595
- for (let c = 0; c < scheduled_children.length; c++) {
1596
- const sc = scheduled_children[c];
1597
- if (sc.t_attach > t_max * 1.02) continue;
1598
- const t_attach = clamp(sc.t_attach, branch_edge_t0[0], t_max * 0.985);
1599
- const base_phi_c = sc.phi;
1600
-
1601
- // Find parent edge containing t_attach
1602
- let seg_idx = 0;
1603
- for (let i = 0; i < branch_edges.length; i++) {
1604
- if (t_attach >= branch_edge_t0[i] && t_attach <= branch_edge_t1[i]) {
1605
- seg_idx = i;
1606
- break;
1607
- }
1608
- }
1609
-
1610
- const parent_e = branch_edges[seg_idx];
1611
- const t0_e = branch_edge_t0[seg_idx];
1612
- const t1_e = branch_edge_t1[seg_idx];
1613
- const local_u = clamp((t_attach - t0_e) / Math.max(1e-5, t1_e - t0_e), 0.0, 1.0);
1614
-
1615
- out.evaluate_segment_position(SCRATCH_POS, 0, parent_e, local_u);
1616
- out.evaluate_segment_tangent(SCRATCH_TAN, 0, parent_e, local_u);
1617
-
1618
- const p_v0 = branch_vertices[seg_idx];
1619
- const p_v1 = branch_vertices[seg_idx + 1];
1620
- const r_p0 = out.node_read_radius(p_v0);
1621
- const r_p1 = out.node_read_radius(p_v1);
1622
- const parent_r_at_attach = lerp(r_p0, r_p1, local_u);
1623
- if (parent_r_at_attach < 0.004) continue;
1624
-
1625
- // Transport parent RMF R vector from p_v0 along parent_e to SCRATCH_POS at local_u
1626
- out.node_read_position(SCRATCH_P0, 0, p_v0);
1627
- out.node_read_tangent(SCRATCH_T0, 0, p_v0);
1628
- out.node_read_frame_r(SCRATCH_R0, 0, p_v0);
1629
- const p_rx = SCRATCH_R0[0];
1630
- const p_ry = SCRATCH_R0[1];
1631
- const p_rz = SCRATCH_R0[2];
1632
-
1633
- spine_frame_double_reflection(
1634
- SCRATCH_RMF, 0,
1635
- SCRATCH_P0[0], SCRATCH_P0[1], SCRATCH_P0[2],
1636
- SCRATCH_T0[0], SCRATCH_T0[1], SCRATCH_T0[2],
1637
- p_rx, p_ry, p_rz,
1638
- SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2],
1639
- SCRATCH_TAN[0], SCRATCH_TAN[1], SCRATCH_TAN[2]
1640
- );
1641
- const prx = SCRATCH_RMF[0], pry = SCRATCH_RMF[1], prz = SCRATCH_RMF[2];
1642
- const psx = SCRATCH_RMF[3], psy = SCRATCH_RMF[4], psz = SCRATCH_RMF[5];
1643
-
1644
- // Genealogical position along the full parent axis (accounts for continuation forks)
1645
- const t_genealogical = lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_attach);
1646
-
1647
- // Insertion polar angle alpha from parent tangent T_p toward D_rad
1648
- const alpha_scale = lvl_cfg.insertion_angle_profile.evaluate(t_genealogical);
1649
- const alpha = clamp(
1650
- lvl_cfg.insertion_angle * alpha_scale + (rng() * 2.0 - 1.0) * lvl_cfg.insertion_angle_variation,
1651
- 0.1,
1652
- Math.PI * 0.85
1653
- );
1654
- const cos_a = Math.cos(alpha);
1655
- const sin_a = Math.sin(alpha);
1656
- const var_scale = 1.0 + (rng() * 2.0 - 1.0) * next_cfg.length_variance;
1657
- const w_plagio = level >= 1
1658
- ? (1.0 - clamp(((next_cfg.gravitropism ?? 0.0) - 0.45) / 0.35, 0.0, 1.0))
1659
- : 0.0;
1660
-
1661
- // Helper to compute radial normal D_rad and child tangent T_c for a given azimuth phi_cand
1662
- const compute_child_basis = (phi_cand, out_dr, out_ct) => {
1663
- const cp = Math.cos(phi_cand);
1664
- const sp = Math.sin(phi_cand);
1665
- let dx_r = cp * prx + sp * psx;
1666
- let dy_r = cp * pry + sp * psy;
1667
- let dz_r = cp * prz + sp * psz;
1668
-
1669
- if (w_plagio > 1e-3) {
1670
- const flat_dry = dy_r < 0.0 ? dy_r * 0.15 + 0.08 : dy_r * 0.75 + 0.12;
1671
- dy_r = lerp(dy_r, flat_dry, w_plagio);
1672
- const dot_tp = dx_r * SCRATCH_TAN[0] + dy_r * SCRATCH_TAN[1] + dz_r * SCRATCH_TAN[2];
1673
- dx_r -= dot_tp * SCRATCH_TAN[0];
1674
- dy_r -= dot_tp * SCRATCH_TAN[1];
1675
- dz_r -= dot_tp * SCRATCH_TAN[2];
1676
- const d_len = Math.sqrt(dx_r * dx_r + dy_r * dy_r + dz_r * dz_r);
1677
- if (d_len > 1e-6) {
1678
- dx_r /= d_len;
1679
- dy_r /= d_len;
1680
- dz_r /= d_len;
1681
- } else {
1682
- dx_r = cp * prx + sp * psx;
1683
- dy_r = cp * pry + sp * psy;
1684
- dz_r = cp * prz + sp * psz;
1685
- }
1686
- }
1687
- out_dr[0] = dx_r;
1688
- out_dr[1] = dy_r;
1689
- out_dr[2] = dz_r;
1690
-
1691
- let tx_c = cos_a * SCRATCH_TAN[0] + sin_a * dx_r;
1692
- let ty_c = cos_a * SCRATCH_TAN[1] + sin_a * dy_r;
1693
- let tz_c = cos_a * SCRATCH_TAN[2] + sin_a * dz_r;
1694
- const ct_l = Math.sqrt(tx_c * tx_c + ty_c * ty_c + tz_c * tz_c) || 1.0;
1695
- out_ct[0] = tx_c / ct_l;
1696
- out_ct[1] = ty_c / ct_l;
1697
- out_ct[2] = tz_c / ct_l;
1698
- };
1699
-
1700
- const cand_dr = [0, 0, 0];
1701
- const cand_ct = [0, 0, 0];
1702
- compute_child_basis(base_phi_c, cand_dr, cand_ct);
1703
-
1704
- // 1. Raw configured child branch length modulated by length_profile at t_genealogical and crown envelope
1705
- const prof_scale = Math.max(0.12, next_cfg.length_profile.evaluate(t_genealogical));
1706
- const est_mid_y = SCRATCH_POS[1] + Math.max(0.0, cand_ct[1]) * next_cfg.length * prof_scale * 0.35;
1707
- const h_norm = clamp((est_mid_y - crown.base_height) / Math.max(0.1, crown.height), 0.0, 1.0);
1708
- const env_scale = level === 0
1709
- ? crown_envelope_evaluate(crown.envelope_type, h_norm)
1710
- : lerp(1.0, crown_envelope_evaluate(crown.envelope_type, h_norm), 0.5);
1711
- const raw_child_len = Math.max(0.12, next_cfg.length * prof_scale * env_scale * var_scale);
1712
-
1713
- // 2. Continuous Gravity-Orientation & Allometric Slenderness Coupling (Cantilever vs. Pendulous Tension)
1714
- const rho_design = Math.max(0.012, next_cfg.radius_ratio);
1715
- const s_design = 1.0 / (2.0 * rho_design);
1716
- const g_trop = next_cfg.gravitropism ?? 0.0;
1717
- const u_curl = next_cfg.tip_upcurl ?? 0.0;
1718
- const t_eff_y = clamp(cand_ct[1] - 0.85 * g_trop + 0.35 * u_curl, -1.0, 1.0);
1719
-
1720
- // Cantilever load (horizontal bending / upward compression) vs. pendulous tensile compliance
1721
- const k_cantilever = Math.sqrt(Math.max(0.0, 1.0 - t_eff_y * t_eff_y)) + 0.65 * Math.max(0.0, t_eff_y);
1722
- const tau_tension = clamp(g_trop, 0.0, 1.0) * Math.max(0.0, -t_eff_y);
1723
-
1724
- // 3. Parent-relative distal conical envelope (prevents distal shoots or split forks from outgrowing their parent)
1725
- const actual_parent_len = Math.max(0.15, out.branch_length[branch_id]);
1726
- const parent_full_len = Math.max(actual_parent_len, job.full_branch_length ?? actual_parent_len);
1727
- const distal_tip_ratio = lerp(0.32, 0.55, tau_tension);
1728
- const cone_max_len = level === 0
1729
- ? parent_full_len * lerp(0.85, 0.32, Math.pow(t_genealogical, 1.6))
1730
- : Math.min(
1731
- actual_parent_len * lerp(0.90, 1.05, tau_tension),
1732
- parent_full_len * lerp(0.85, distal_tip_ratio, Math.pow(t_genealogical, 1.25))
1733
- );
1734
- const desired_len = Math.max(0.12, Math.min(raw_child_len, cone_max_len));
1735
-
1736
- const nom_len = Math.max(0.15, next_cfg.length);
1737
- const r_design = (nom_len * rho_design * Math.pow(desired_len / nom_len, 0.75)) / (1.0 + 0.65 * tau_tension);
1738
- const child_base_r = Math.min(
1739
- parent_r_at_attach * 0.78,
1740
- Math.max(0.0035, r_design)
1741
- );
1742
-
1743
- const gamma = lerp(0.82, 0.65, tau_tension);
1744
- const allometric_len = child_base_r < r_design
1745
- ? desired_len * Math.pow(child_base_r / Math.max(1e-5, r_design), gamma)
1746
- : desired_len;
1747
- const s_max = s_design * (1.35 + 0.65 * tau_tension) / (0.65 + 0.35 * k_cantilever);
1748
- const child_len = clamp(Math.min(allometric_len, 2.0 * child_base_r * s_max), 0.12, desired_len);
1749
- const child_segs = Math.max(1, next_cfg.segments);
1750
- const child_step = child_len / child_segs;
1751
-
1752
- // 4. Pre-Spawn BVH Bud Clearance Probing (selects open canopy azimuth or inhibits blocked bud)
1753
- let best_drx = cand_dr[0], best_dry = cand_dr[1], best_drz = cand_dr[2];
1754
- let best_ctx = cand_ct[0], best_cty = cand_ct[1], best_ctz = cand_ct[2];
1755
- let best_clear = -Infinity;
1756
- const r_horiz_attach = Math.hypot(SCRATCH_POS[0], SCRATCH_POS[2]);
1757
-
1758
- for (let k = 0; k < PHI_CANDIDATES.length; k++) {
1759
- if (k > 0) {
1760
- compute_child_basis(base_phi_c + PHI_CANDIDATES[k], cand_dr, cand_ct);
1761
- }
1762
- // Prevent lateral shoots on boughs from pointing backward into the trunk core
1763
- if (level >= 1 && r_horiz_attach > 0.30) {
1764
- const radial_dot = (SCRATCH_POS[0] * cand_ct[0] + SCRATCH_POS[2] * cand_ct[2]) / r_horiz_attach;
1765
- if (radial_dot < -0.15) continue;
1766
- }
1767
-
1768
- const p1_x = SCRATCH_POS[0] + cand_ct[0] * child_step;
1769
- const p1_y = SCRATCH_POS[1] + cand_ct[1] * child_step;
1770
- const p1_z = SCRATCH_POS[2] + cand_ct[2] * child_step;
1771
-
1772
- const clear0 = evaluate_candidate_clearance(
1773
- -1, branch_id, seg_idx, false, 0,
1774
- SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2], child_base_r,
1775
- p1_x, p1_y, p1_z, child_base_r * 0.88,
1776
- 0.002
1777
- );
1778
-
1779
- const p2_x = p1_x + cand_ct[0] * child_step;
1780
- const p2_y = p1_y + cand_ct[1] * child_step;
1781
- const p2_z = p1_z + cand_ct[2] * child_step;
1782
- const clear1 = child_segs >= 2
1783
- ? evaluate_candidate_clearance(
1784
- -1, branch_id, seg_idx, false, 1,
1785
- p1_x, p1_y, p1_z, child_base_r * 0.88,
1786
- p2_x, p2_y, p2_z, child_base_r * 0.75,
1787
- 0.002
1788
- )
1789
- : clear0;
1790
-
1791
- const min_c = Math.min(clear0, clear1);
1792
- if (min_c > best_clear) {
1793
- best_clear = min_c;
1794
- best_drx = cand_dr[0];
1795
- best_dry = cand_dr[1];
1796
- best_drz = cand_dr[2];
1797
- best_ctx = cand_ct[0];
1798
- best_cty = cand_ct[1];
1799
- best_ctz = cand_ct[2];
1800
- }
1801
- if (min_c >= 0.004) {
1802
- break;
1803
- }
1804
- }
1805
-
1806
- // Botanical bud inhibition: if all candidate directions are blocked by existing branches, suppress bud
1807
- if (best_clear < -0.004) {
1808
- continue;
1809
- }
1810
-
1811
- const drx = best_drx, dry = best_dry, drz = best_drz;
1812
- const ctx = best_ctx, cty = best_cty, ctz = best_ctz;
1813
-
1814
- // Initial RMF radial vector for child branch:
1815
- // R_c = cos(alpha) * D_rad - sin(alpha) * T_p is exact unit length and orthogonal
1816
- // to T_c = cos(alpha) * T_p + sin(alpha) * D_rad for all alpha in [0, pi],
1817
- // avoiding any singularity at alpha = pi/2.
1818
- let crx = cos_a * drx - sin_a * SCRATCH_TAN[0];
1819
- let cry = cos_a * dry - sin_a * SCRATCH_TAN[1];
1820
- let crz = cos_a * drz - sin_a * SCRATCH_TAN[2];
1821
- const cr_dot = crx * ctx + cry * cty + crz * ctz;
1822
- crx -= cr_dot * ctx;
1823
- cry -= cr_dot * cty;
1824
- crz -= cr_dot * ctz;
1825
- const cr_len = Math.sqrt(crx * crx + cry * cry + crz * crz);
1826
- if (cr_len > 1e-12) {
1827
- crx /= cr_len;
1828
- cry /= cr_len;
1829
- crz /= cr_len;
1830
- }
1831
-
1832
- queue.push({
1833
- level: next_level,
1834
- parent_branch_id: branch_id,
1835
- parent_edge_id: parent_e,
1836
- parent_attach_seg_idx: seg_idx,
1837
- parent_attach_t: t_attach,
1838
- attach_parent_vertex: p_v0,
1839
- start_vertex: -1,
1840
- px: SCRATCH_POS[0],
1841
- py: SCRATCH_POS[1],
1842
- pz: SCRATCH_POS[2],
1843
- tx: ctx,
1844
- ty: cty,
1845
- tz: ctz,
1846
- rx: crx,
1847
- ry: cry,
1848
- rz: crz,
1849
- emerge_drx: drx,
1850
- emerge_dry: dry,
1851
- emerge_drz: drz,
1852
- length: child_len,
1853
- full_branch_length: child_len,
1854
- base_radius: child_base_r,
1855
- start_seg: 0,
1856
- total_segs: child_segs,
1857
- initial_arc_s: 0.0,
1858
- has_split: false,
1859
- is_split_fork: false,
1860
- cum_t0: 0.0,
1861
- cum_t1: 1.0
1862
- });
1863
- }
1864
- }
1865
- }
1866
-
1867
- // Bottom-up Pipe Model radius calculation & blending
1868
- tree_skeleton_compute_pipe_radii(out, species);
1869
-
1870
- return out;
1871
- }
1
+ import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
2
+ import { bvh_query_user_data_intersects_capsule } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_capsule.js";
3
+ import { PointHashGrid3 } from "../../../core/geom/3d/hash-grid/PointHashGrid3.js";
4
+ import { line3_compute_segment_segment_closest_points } from "../../../core/geom/3d/line/line3_compute_segment_segment_closest_points.js";
5
+ import { GOLDEN_RATIO } from "../../../core/math/GOLDEN_RATIO.js";
6
+ import { PI2 } from "../../../core/math/PI2.js";
7
+ import { clamp } from "../../../core/math/clamp.js";
8
+ import { lerp } from "../../../core/math/lerp.js";
9
+ import { curl_noise_3d } from "../../../core/math/noise/curl_noise_3d.js";
10
+ import { seededRandom } from "../../../core/math/random/seededRandom.js";
11
+ import { PhyllotaxisMode, phyllotaxis_sample_angle } from "../config/PhyllotaxisMode.js";
12
+ import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
13
+ import { TreeSkeleton } from "./TreeSkeleton.js";
14
+ import { assert } from "../../../core/assert.js";
15
+ import { flare_lobe_phase_offset } from "./flare_lobe_phase_offset.js";
16
+ import { crown_envelope_evaluate, crown_envelope_sdf } from "./crown_envelope_evaluate.js";
17
+ import { spine_frame_double_reflection } from "./spine_frame_double_reflection.js";
18
+ import { tree_skeleton_compute_pipe_radii } from "./tree_skeleton_compute_pipe_radii.js";
19
+ import { tree_skeleton_grow_shoots } from "./tree_skeleton_grow_shoots.js";
20
+
21
+ const GOLDEN_ANGLE = PI2 / (GOLDEN_RATIO * GOLDEN_RATIO); // ~2.399963229728653 rad (137.50776 deg)
22
+
23
+ /**
24
+ * Canonical order of a generator's outgoing rules: by target, then by content, so the order rules are listed in
25
+ * never changes the tree.
26
+ * @param {import("../config/BranchSpawnRule.js").BranchSpawnRule} a
27
+ * @param {import("../config/BranchSpawnRule.js").BranchSpawnRule} b
28
+ * @returns {number}
29
+ */
30
+ function compare_rule_target(a, b) {
31
+ return (a.target - b.target) || (a.hash() - b.hash());
32
+ }
33
+
34
+ /**
35
+ * Node position `u` of child `c` of a rule laying out `planned` children over its attachment interval, `u` in
36
+ * [0, 1) for `c` in [0, planned). The same layout continued to `c < 0` gives `u < 0`: where the rule's earlier
37
+ * children grew, below its interval.
38
+ *
39
+ * @param {import("../config/BranchSpawnRule.js").BranchSpawnRule} rule
40
+ * @param {number} c
41
+ * @param {number} planned
42
+ * @param {boolean} on_trunk whorls compress toward the apex of a trunk (self-similar conical internodes)
43
+ * @returns {number}
44
+ */
45
+ function rule_node_u(rule, c, planned, on_trunk) {
46
+ switch (rule.phyllotaxis_mode) {
47
+ case PhyllotaxisMode.Opposite:
48
+ case PhyllotaxisMode.OppositeDecussate: {
49
+ const pair_count = Math.max(1, Math.ceil(planned / 2));
50
+ return pair_count > 1 ? ((c >> 1) + 0.35) / (pair_count - 0.55) : 0.5;
51
+ }
52
+ case PhyllotaxisMode.Whorled: {
53
+ const w_size = Math.max(1, rule.whorl_count | 0);
54
+ const whorl_total = Math.max(1, Math.ceil(planned / w_size));
55
+ const u_lin = whorl_total > 1 ? (Math.floor(c / w_size) + 0.35) / (whorl_total - 0.55) : 0.5;
56
+ // p > 1 => du/dt decreases as u -> 1; u_lin < 1 for every c < planned
57
+ return on_trunk ? 1.0 - Math.pow(1.0 - u_lin, 1.42) : u_lin;
58
+ }
59
+ default:
60
+ return planned > 1 ? (c + 0.35) / (planned - 0.55) : 0.5;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Azimuth of child `c` of a rule relative to its family's base azimuth, for any integer `c` (also `c < 0`).
66
+ * `FlareLobes` is placed by the caller.
67
+ *
68
+ * @param {import("../config/BranchSpawnRule.js").BranchSpawnRule} rule
69
+ * @param {number} c
70
+ * @returns {number}
71
+ */
72
+ function rule_node_phi(rule, c) {
73
+ return phyllotaxis_sample_angle(rule.phyllotaxis_mode, c, rule.whorl_count);
74
+ }
75
+
76
+ const SCRATCH_CURL = [0, 0, 0];
77
+ const SCRATCH_RMF = new Float64Array(6);
78
+ const SCRATCH_POS = new Float32Array(3);
79
+ const SCRATCH_TAN = new Float32Array(3);
80
+ const SCRATCH_P0 = new Float32Array(3);
81
+ const SCRATCH_T0 = new Float32Array(3);
82
+ const SCRATCH_R0 = new Float32Array(3);
83
+ const SCRATCH_M_PREV = new Float32Array(3);
84
+ const SCRATCH_M_NEXT = new Float32Array(3);
85
+ const SCRATCH_GUIDE_P0 = new Float32Array(3);
86
+ const SCRATCH_GUIDE_P1 = new Float32Array(3);
87
+ const SCRATCH_GUIDE_T = new Float32Array(3);
88
+
89
+ /**
90
+ * Evaluates the trunk guide curve position `out_pos` and unit tangent `out_tan` at normalized
91
+ * parameter `t_norm` in [0, 1]. Defaults to the vertical upward plumb line `(0, t_norm * trunk_len, 0)`
92
+ * when `trunk_guide` is null or omitted.
93
+ *
94
+ * @param {Float32Array|number[]} out_pos
95
+ * @param {Float32Array|number[]} out_tan
96
+ * @param {Array<ArrayLike<number>>|Function|null|undefined} trunk_guide
97
+ * @param {number} t_norm
98
+ * @param {number} trunk_len
99
+ */
100
+ function evaluate_trunk_guide(out_pos, out_tan, trunk_guide, t_norm, trunk_len) {
101
+ const t = clamp(t_norm, 0.0, 1.0);
102
+ if (!trunk_guide) {
103
+ out_pos[0] = 0.0;
104
+ out_pos[1] = t * trunk_len;
105
+ out_pos[2] = 0.0;
106
+ out_tan[0] = 0.0;
107
+ out_tan[1] = 1.0;
108
+ out_tan[2] = 0.0;
109
+ return;
110
+ }
111
+
112
+ const sample_pt = (u, dst) => {
113
+ if (typeof trunk_guide === "function") {
114
+ trunk_guide(u, dst);
115
+ return;
116
+ }
117
+ if (Array.isArray(trunk_guide) && trunk_guide.length >= 2) {
118
+ const n = trunk_guide.length - 1;
119
+ const su = clamp(u, 0.0, 1.0) * n;
120
+ const idx = Math.min(n - 1, Math.floor(su));
121
+ const frac = su - idx;
122
+ const a = trunk_guide[idx];
123
+ const b = trunk_guide[idx + 1];
124
+ dst[0] = lerp(a[0] ?? 0.0, b[0] ?? 0.0, frac);
125
+ dst[1] = lerp(a[1] ?? 0.0, b[1] ?? 0.0, frac);
126
+ dst[2] = lerp(a[2] ?? 0.0, b[2] ?? 0.0, frac);
127
+ return;
128
+ }
129
+ dst[0] = 0.0;
130
+ dst[1] = u * trunk_len;
131
+ dst[2] = 0.0;
132
+ };
133
+
134
+ sample_pt(t, out_pos);
135
+ const eps = 0.01;
136
+ const t_a = Math.max(0.0, t - eps);
137
+ const t_b = Math.min(1.0, t + eps);
138
+ const pa = [0, 0, 0];
139
+ const pb = [0, 0, 0];
140
+ sample_pt(t_a, pa);
141
+ sample_pt(t_b, pb);
142
+ let tx = pb[0] - pa[0];
143
+ let ty = pb[1] - pa[1];
144
+ let tz = pb[2] - pa[2];
145
+ const tl = Math.hypot(tx, ty, tz);
146
+ if (tl > 1e-8) {
147
+ out_tan[0] = tx / tl;
148
+ out_tan[1] = ty / tl;
149
+ out_tan[2] = tz / tl;
150
+ } else {
151
+ out_tan[0] = 0.0;
152
+ out_tan[1] = 1.0;
153
+ out_tan[2] = 0.0;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Rotates unit vector `(vx, vy, vz)` around unit axis `(ax, ay, az)` by `angle` radians (Rodrigues' formula).
159
+ * @param {number[]|Float64Array} out
160
+ * @param {number} vx
161
+ * @param {number} vy
162
+ * @param {number} vz
163
+ * @param {number} ax
164
+ * @param {number} ay
165
+ * @param {number} az
166
+ * @param {number} angle
167
+ */
168
+ function rotate_around_axis(out, vx, vy, vz, ax, ay, az, angle) {
169
+ const c = Math.cos(angle);
170
+ const s = Math.sin(angle);
171
+ const dot = vx * ax + vy * ay + vz * az;
172
+ const omc = 1.0 - c;
173
+
174
+ const cx = ay * vz - az * vy;
175
+ const cy = az * vx - ax * vz;
176
+ const cz = ax * vy - ay * vx;
177
+
178
+ let rx = vx * c + cx * s + ax * dot * omc;
179
+ let ry = vy * c + cy * s + ay * dot * omc;
180
+ let rz = vz * c + cz * s + az * dot * omc;
181
+
182
+ const len = Math.sqrt(rx * rx + ry * ry + rz * rz);
183
+ if (len > 1e-12) {
184
+ const inv = 1.0 / len;
185
+ rx *= inv;
186
+ ry *= inv;
187
+ rz *= inv;
188
+ }
189
+ out[0] = rx;
190
+ out[1] = ry;
191
+ out[2] = rz;
192
+ }
193
+
194
+ /**
195
+ * Determines the non-exempt sub-segment parameter intervals `[s_min, 1]` on candidate segment A
196
+ * and `[t_min, 1]` on existing segment B for branch-branch capsule collision testing.
197
+ * Returns `false` if the segment pair is topologically exempt (e.g. direct parent-child base collar).
198
+ *
199
+ * @param {number} bA
200
+ * @param {number} parent_bA
201
+ * @param {number} attach_seg_A
202
+ * @param {boolean} is_split_A
203
+ * @param {number} local_idx_A
204
+ * @param {number} bB
205
+ * @param {number} parent_bB
206
+ * @param {number} attach_seg_B
207
+ * @param {boolean} is_split_B
208
+ * @param {number} local_idx_B
209
+ * @param {Float64Array|number[]} out_bounds `[s_min, t_min]`
210
+ * @returns {boolean}
211
+ */
212
+ function resolve_subsegment_bounds(
213
+ bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
214
+ bB, parent_bB, attach_seg_B, is_split_B, local_idx_B,
215
+ out_bounds
216
+ ) {
217
+ if (bA === bB) return false;
218
+
219
+ // 1. A is a direct child of B
220
+ if (parent_bA === bB) {
221
+ if (is_split_A) {
222
+ if (local_idx_A === 0 && local_idx_B === attach_seg_A) return false;
223
+ if (local_idx_A === 0 && local_idx_B === attach_seg_A + 1) {
224
+ out_bounds[0] = 0.55;
225
+ out_bounds[1] = 0.55;
226
+ return true;
227
+ }
228
+ if (local_idx_A === 1 && (local_idx_B === attach_seg_A || local_idx_B === attach_seg_A + 1)) {
229
+ out_bounds[0] = 0.45;
230
+ out_bounds[1] = 0.45;
231
+ return true;
232
+ }
233
+ } else {
234
+ if (local_idx_A === 0 && Math.abs(local_idx_B - attach_seg_A) <= 1) return false;
235
+ if (local_idx_A === 1 && Math.abs(local_idx_B - attach_seg_A) <= 1) {
236
+ if (bA < 0 || local_idx_B === attach_seg_A) return false;
237
+ out_bounds[0] = 0.35;
238
+ out_bounds[1] = 0.0;
239
+ return true;
240
+ }
241
+ }
242
+ }
243
+
244
+ // 2. B is a direct child of A
245
+ if (parent_bB === bA) {
246
+ if (is_split_B) {
247
+ if (local_idx_B === 0 && local_idx_A === attach_seg_B) return false;
248
+ if (local_idx_B === 0 && local_idx_A === attach_seg_B + 1) {
249
+ out_bounds[0] = 0.55;
250
+ out_bounds[1] = 0.55;
251
+ return true;
252
+ }
253
+ if (local_idx_B === 1 && (local_idx_A === attach_seg_B || local_idx_A === attach_seg_B + 1)) {
254
+ out_bounds[0] = 0.45;
255
+ out_bounds[1] = 0.45;
256
+ return true;
257
+ }
258
+ } else {
259
+ if (local_idx_B <= 1 && Math.abs(local_idx_A - attach_seg_B) <= 1) return false;
260
+ }
261
+ }
262
+
263
+ // 3. A and B are siblings sharing the same parent branch
264
+ if (parent_bA >= 0 && parent_bA === parent_bB) {
265
+ if (local_idx_A === 0 && local_idx_B === 0) {
266
+ out_bounds[0] = 0.50;
267
+ out_bounds[1] = 0.50;
268
+ return true;
269
+ }
270
+ if (local_idx_A === 0 && local_idx_B === 1 && Math.abs(attach_seg_A - attach_seg_B) <= 1) {
271
+ out_bounds[0] = 0.25;
272
+ out_bounds[1] = 0.0;
273
+ return true;
274
+ }
275
+ if (local_idx_A === 1 && local_idx_B === 0 && Math.abs(attach_seg_A - attach_seg_B) <= 1) {
276
+ out_bounds[0] = 0.0;
277
+ out_bounds[1] = 0.25;
278
+ return true;
279
+ }
280
+ }
281
+
282
+ out_bounds[0] = 0.0;
283
+ out_bounds[1] = 0.0;
284
+ return true;
285
+ }
286
+
287
+ /**
288
+ * Generates a directed botanical `TreeSkeleton` from a `TreeSpeciesConfig`, combining:
289
+ * - A branch generator graph: `species.branches[0]` grows the trunk, and every `species.branch_rules` edge
290
+ * spawns branches of its `target` generator along each branch of its `source` generator
291
+ * - Parametric Cubic Hermite spine generation (`BranchGeneratorConfig`, `AnimationCurve` profiles)
292
+ * - Botanical phyllotaxis (`SpiralGolden`, `AlternateDistichous`, `Tristichous`, `Opposite`, `OppositeDecussate`, `Whorled`, `FlareLobes`)
293
+ * - Structural surface roots (`BranchGeneratorConfig.root`) that creep along the ground plane `y = 0`
294
+ * - Gravitropism (toward `-Y`), phototropism (toward `+Y` and outward crown exterior), and 3D `curl_noise_3d` gnarl
295
+ * - Dichotomous / sympodial trunk & bough splitting
296
+ * - Space colonization crown competition via `PointHashGrid3`
297
+ * - Rotation-Minimizing Frames (`spine_frame_double_reflection`)
298
+ * - Pipe Model cross-section radius accumulation (`tree_skeleton_compute_pipe_radii`)
299
+ *
300
+ * @param {TreeSpeciesConfig|Object} [config]
301
+ * @param {TreeSkeleton} [out]
302
+ * @returns {TreeSkeleton}
303
+ */
304
+ export function tree_skeleton_grow(config = new TreeSpeciesConfig(), out = new TreeSkeleton()) {
305
+ const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
306
+ out.clear();
307
+
308
+ const rng = seededRandom(species.seed | 0);
309
+ // Seed-derived 3D noise offset so unseeded Simplex/Curl fields are unique and deterministic per seed
310
+ const noise_ox = (rng() - 0.5) * 1000.0;
311
+ const noise_oy = (rng() - 0.5) * 1000.0;
312
+ const noise_oz = (rng() - 0.5) * 1000.0;
313
+
314
+ const crown = species.crown;
315
+ const crown_cy = crown.base_height + crown.height * 0.5;
316
+ out.crown_center[0] = 0.0;
317
+ out.crown_center[1] = crown_cy;
318
+ out.crown_center[2] = 0.0;
319
+
320
+ out.crown_radii[0] = Math.max(0.25, crown.radius);
321
+ out.crown_radii[1] = Math.max(0.25, crown.height * 0.5);
322
+ out.crown_radii[2] = Math.max(0.25, crown.radius);
323
+
324
+ // Build Space Colonization attraction point cloud + PointHashGrid3 once
325
+ const use_space_col = crown.space_colonization_weight > 0.0 && crown.attraction_point_count > 0;
326
+ const attr_count = use_space_col ? (crown.attraction_point_count | 0) : 0;
327
+ const attr_positions = new Float32Array(attr_count * 3);
328
+ const attr_alive = new Uint8Array(attr_count);
329
+ let attr_grid = null;
330
+
331
+ if (use_space_col) {
332
+ let placed = 0;
333
+ let attempts = 0;
334
+ const max_attempts = attr_count * 12;
335
+ while (placed < attr_count && attempts < max_attempts) {
336
+ attempts++;
337
+ const u_h = rng();
338
+ const env_r = crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
339
+ const rx = (rng() * 2.0 - 1.0) * env_r;
340
+ const rz = (rng() * 2.0 - 1.0) * env_r;
341
+ if (rx * rx + rz * rz <= env_r * env_r) {
342
+ const ry = crown.base_height + u_h * crown.height;
343
+ attr_positions[placed * 3] = rx;
344
+ attr_positions[placed * 3 + 1] = ry;
345
+ attr_positions[placed * 3 + 2] = rz;
346
+ attr_alive[placed] = 1;
347
+ placed++;
348
+ }
349
+ }
350
+ while (placed < attr_count) {
351
+ const theta = rng() * PI2;
352
+ const u_h = rng();
353
+ const r = Math.sqrt(rng()) * crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
354
+ attr_positions[placed * 3] = Math.cos(theta) * r;
355
+ attr_positions[placed * 3 + 1] = crown.base_height + u_h * crown.height;
356
+ attr_positions[placed * 3 + 2] = Math.sin(theta) * r;
357
+ attr_alive[placed] = 1;
358
+ placed++;
359
+ }
360
+
361
+ attr_grid = new PointHashGrid3();
362
+ attr_grid.build(attr_positions, attr_count, Math.max(0.25, crown.attraction_radius));
363
+ }
364
+
365
+ const r_influence_sq = crown.attraction_radius * crown.attraction_radius;
366
+ const r_kill_sq = crown.kill_radius * crown.kill_radius;
367
+
368
+ // Queue of branches to grow in level-order (guarantees parent branches & vertices precede children)
369
+ const queue = [];
370
+
371
+ const lvl0 = species.branches[0];
372
+ if (!lvl0) {
373
+ return out;
374
+ }
375
+
376
+ // Outgoing rules of every generator in canonical order, so the order rules are listed in does not change the tree
377
+ const outgoing_rules = species.branches.map(() => []);
378
+ for (let r = 0; r < species.branch_rules.length; r++) {
379
+ const rule = species.branch_rules[r];
380
+ assert.ok(rule.source >= 0 && rule.source < rule.target && rule.target < species.branches.length,
381
+ 'branch rule must satisfy 0 <= source < target < branches.length');
382
+ outgoing_rules[rule.source].push(rule);
383
+ }
384
+ for (let g = 0; g < outgoing_rules.length; g++) {
385
+ outgoing_rules[g].sort(compare_rule_target);
386
+ }
387
+
388
+ const trunk_len = Math.max(0.2, lvl0.length * (1.0 + (rng() * 2.0 - 1.0) * lvl0.length_variance));
389
+ const trunk_base_r = Math.max(0.01, trunk_len * lvl0.radius_ratio);
390
+
391
+ evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, 0.0, trunk_len);
392
+
393
+ // Apply subtle initial basal trunk lean around the guide tangent
394
+ const max_lean = lvl0.excurrent ? 0.025 : 0.06;
395
+ const lean_ang = clamp((lvl0.basal_lean ?? 0.02) * (0.75 + 0.5 * rng()), 0.0, max_lean);
396
+ const lean_az = rng() * PI2;
397
+ const init_tx = clamp(SCRATCH_GUIDE_T[0] + Math.sin(lean_ang) * Math.cos(lean_az), -0.95, 0.95);
398
+ const init_ty = Math.max(0.2, SCRATCH_GUIDE_T[1] * Math.cos(lean_ang));
399
+ const init_tz = clamp(SCRATCH_GUIDE_T[2] + Math.sin(lean_ang) * Math.sin(lean_az), -0.95, 0.95);
400
+ const init_t_len = Math.hypot(init_tx, init_ty, init_tz) || 1.0;
401
+ const norm_tx = init_tx / init_t_len;
402
+ const norm_ty = init_ty / init_t_len;
403
+ const norm_tz = init_tz / init_t_len;
404
+
405
+ // Orthonormal initial R perpendicular to norm_T
406
+ let init_rx = 1.0 - norm_tx * norm_tx;
407
+ let init_ry = -norm_ty * norm_tx;
408
+ let init_rz = -norm_tz * norm_tx;
409
+ const init_r_len = Math.hypot(init_rx, init_ry, init_rz) || 1.0;
410
+ init_rx /= init_r_len;
411
+ init_ry /= init_r_len;
412
+ init_rz /= init_r_len;
413
+
414
+ queue.push({
415
+ level: 0,
416
+ generator: 0,
417
+ in_crown: true,
418
+ parent_branch_id: -1,
419
+ parent_edge_id: -1,
420
+ parent_attach_seg_idx: -1,
421
+ parent_attach_t: 0.0,
422
+ start_vertex: -1,
423
+ px: SCRATCH_GUIDE_P0[0],
424
+ py: SCRATCH_GUIDE_P0[1],
425
+ pz: SCRATCH_GUIDE_P0[2],
426
+ tx: norm_tx,
427
+ ty: norm_ty,
428
+ tz: norm_tz,
429
+ rx: init_rx,
430
+ ry: init_ry,
431
+ rz: init_rz,
432
+ emerge_drx: init_rx,
433
+ emerge_dry: init_ry,
434
+ emerge_drz: init_rz,
435
+ length: trunk_len,
436
+ full_branch_length: trunk_len,
437
+ base_radius: trunk_base_r,
438
+ start_seg: 0,
439
+ total_segs: Math.max(1, lvl0.segments),
440
+ shape_segs: Math.max(1, lvl0.segments),
441
+ initial_arc_s: 0.0,
442
+ has_split: false,
443
+ is_split_fork: false,
444
+ cum_t0: 0.0,
445
+ cum_t1: 1.0
446
+ });
447
+
448
+ const rot_scratch = [0, 0, 0];
449
+
450
+ // Scope-local BVH and per-segment metadata for branch-branch spatial avoidance & collision prevention
451
+ const bvh = new BVH();
452
+ const bvh_hits = new Uint32Array(256);
453
+ const query_capsule = new Float32Array(7);
454
+ const sub_bounds = new Float64Array(2);
455
+ const closest_st = new Float64Array(2);
456
+
457
+ const seg_branch_id = [];
458
+ const seg_local_idx = [];
459
+ const seg_p0x = [], seg_p0y = [], seg_p0z = [], seg_r0 = [];
460
+ const seg_p1x = [], seg_p1y = [], seg_p1z = [], seg_r1 = [];
461
+ const seg_bvh_leaf = [];
462
+
463
+ const branch_parent_seg_idx = [];
464
+ const branch_is_split_fork = [];
465
+
466
+ /**
467
+ * Evaluates the minimum signed surface-to-surface clearance between a candidate segment A
468
+ * and all non-exempt existing segments in `bvh`.
469
+ */
470
+ const evaluate_candidate_clearance = (
471
+ bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
472
+ a0x, a0y, a0z, rA0,
473
+ a1x, a1y, a1z, rA1,
474
+ extra_margin
475
+ ) => {
476
+ if (bvh.size === 0) return 10.0;
477
+ const max_rA = Math.max(rA0, rA1);
478
+ query_capsule[0] = a0x;
479
+ query_capsule[1] = a0y;
480
+ query_capsule[2] = a0z;
481
+ query_capsule[3] = a1x;
482
+ query_capsule[4] = a1y;
483
+ query_capsule[5] = a1z;
484
+ query_capsule[6] = max_rA * 1.15 + extra_margin + 0.04;
485
+
486
+ const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
487
+ let min_clear = 10.0;
488
+
489
+ for (let h = 0; h < hit_count; h++) {
490
+ const eB = bvh_hits[h];
491
+ const bB = seg_branch_id[eB];
492
+ const local_idx_B = seg_local_idx[eB];
493
+ const parent_bB = out.branch_parent[bB];
494
+ const attach_seg_B = branch_parent_seg_idx[bB];
495
+ const is_split_B = branch_is_split_fork[bB];
496
+
497
+ if (!resolve_subsegment_bounds(
498
+ bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
499
+ bB, parent_bB, attach_seg_B, is_split_B, local_idx_B,
500
+ sub_bounds
501
+ )) {
502
+ continue;
503
+ }
504
+
505
+ const s_min = sub_bounds[0];
506
+ const t_min = sub_bounds[1];
507
+ const sa0x = a0x + (a1x - a0x) * s_min;
508
+ const sa0y = a0y + (a1y - a0y) * s_min;
509
+ const sa0z = a0z + (a1z - a0z) * s_min;
510
+
511
+ const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
512
+ const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
513
+ const sb0x = b0x + (b1x - b0x) * t_min;
514
+ const sb0y = b0y + (b1y - b0y) * t_min;
515
+ const sb0z = b0z + (b1z - b0z) * t_min;
516
+
517
+ line3_compute_segment_segment_closest_points(
518
+ closest_st,
519
+ sa0x, sa0y, sa0z, a1x, a1y, a1z,
520
+ sb0x, sb0y, sb0z, b1x, b1y, b1z
521
+ );
522
+
523
+ const s_f = s_min + (1.0 - s_min) * closest_st[0];
524
+ const t_f = t_min + (1.0 - t_min) * closest_st[1];
525
+
526
+ const qAx = a0x + (a1x - a0x) * s_f;
527
+ const qAy = a0y + (a1y - a0y) * s_f;
528
+ const qAz = a0z + (a1z - a0z) * s_f;
529
+ const qBx = b0x + (b1x - b0x) * t_f;
530
+ const qBy = b0y + (b1y - b0y) * t_f;
531
+ const qBz = b0z + (b1z - b0z) * t_f;
532
+
533
+ const dist = Math.hypot(qAx - qBx, qAy - qBy, qAz - qBz);
534
+ const rA = rA0 + (rA1 - rA0) * s_f;
535
+ const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
536
+ const r_req = (rA + rB) * 1.08 + extra_margin;
537
+ const clear = dist - r_req;
538
+ if (clear < min_clear) {
539
+ min_clear = clear;
540
+ }
541
+ }
542
+ return min_clear;
543
+ };
544
+
545
+ for (let q_idx = 0; q_idx < queue.length; q_idx++) {
546
+ const job = queue[q_idx];
547
+ const level = job.level;
548
+ const generator = job.generator;
549
+ const lvl_cfg = species.branches[generator];
550
+
551
+ const phase_hash = rng();
552
+ const N_seg = job.total_segs;
553
+ // Shape is a function of position along the whole branch: envelopes take the genealogical t (cum_t0..cum_t1)
554
+ // and per-step amounts the whole branch's segmentation, so a dead stub bends as the base of its branch did
555
+ const N_shape = job.shape_segs;
556
+ const remaining_segs = N_seg - job.start_seg;
557
+ if (remaining_segs <= 0) continue;
558
+
559
+ const step_len = job.length / N_seg;
560
+ const prof_p0 = lvl_cfg.radius_profile.evaluate(0.0);
561
+ const prof_p1 = lvl_cfg.radius_profile.evaluate(1.0);
562
+ const prof_span = prof_p0 - prof_p1;
563
+
564
+ let cur_px = job.px;
565
+ let cur_py = job.py;
566
+ let cur_pz = job.pz;
567
+ let cur_tx = job.tx;
568
+ let cur_ty = job.ty;
569
+ let cur_tz = job.tz;
570
+ let cur_rx = job.rx;
571
+ let cur_ry = job.ry;
572
+ let cur_rz = job.rz;
573
+ let cur_arc_s = job.initial_arc_s;
574
+
575
+ // Compute binormal S = T x R
576
+ let cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
577
+ let cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
578
+ let cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
579
+
580
+ let prev_v = job.start_vertex;
581
+ let branch_id = -1;
582
+
583
+ if (prev_v < 0) {
584
+ const t0 = job.start_seg / N_seg;
585
+ const u_prof0 = prof_span > 1e-4
586
+ ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(t0)) / prof_span, 0.0, 1.0)
587
+ : t0;
588
+ const min_r0_frac = level === 0 ? 0.08 : 0.15;
589
+ const r0 = Math.max(0.002, job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_prof0, min_r0_frac));
590
+
591
+ branch_id = out.add_branch(
592
+ 0,
593
+ level,
594
+ generator,
595
+ job.parent_branch_id,
596
+ job.parent_edge_id,
597
+ job.parent_attach_t,
598
+ job.length,
599
+ r0,
600
+ phase_hash
601
+ );
602
+ prev_v = out.add_node(
603
+ cur_px, cur_py, cur_pz,
604
+ cur_tx, cur_ty, cur_tz,
605
+ cur_rx, cur_ry, cur_rz,
606
+ r0,
607
+ branch_id,
608
+ level,
609
+ job.parent_branch_id,
610
+ t0,
611
+ cur_arc_s,
612
+ phase_hash
613
+ );
614
+ if (job.attach_parent_vertex !== undefined && job.attach_parent_vertex >= 0) {
615
+ out.connect_nodes(job.attach_parent_vertex, prev_v);
616
+ }
617
+ out.branch_root_vertex[branch_id] = prev_v;
618
+ out.branch_tip_vertex[branch_id] = prev_v;
619
+ } else {
620
+ const prev_r = out.node_read_radius(prev_v);
621
+ branch_id = out.add_branch(
622
+ prev_v,
623
+ level,
624
+ generator,
625
+ job.parent_branch_id,
626
+ job.parent_edge_id,
627
+ job.parent_attach_t,
628
+ job.length,
629
+ prev_r,
630
+ phase_hash
631
+ );
632
+ }
633
+
634
+ branch_parent_seg_idx[branch_id] = job.parent_attach_seg_idx ?? -1;
635
+ branch_is_split_fork[branch_id] = Boolean(job.is_split_fork);
636
+ out.branch_dead[branch_id] = job.dead ? 1 : 0;
637
+
638
+ const branch_edges = [];
639
+ const branch_edge_t0 = [];
640
+ const branch_edge_t1 = [];
641
+ const branch_vertices = [prev_v];
642
+
643
+ // Pre-schedule the phyllotactic child attachments of every outgoing rule so the parent spine kinks
644
+ // sympodially at lateral emissions
645
+ const len_fraction_est = level >= 1
646
+ ? clamp(Math.pow(job.length / Math.max(0.2, lvl_cfg.length), 0.85), 0.0, 1.0)
647
+ : 1.0;
648
+ const can_bear_children = !job.dead && job.base_radius >= 0.006 && (level === 0 || job.length >= 0.55);
649
+ const span_fraction_est = job.child_span_fraction ?? Math.max(0.25, (N_seg - job.start_seg) / Math.max(1, N_seg));
650
+ const effective_span = span_fraction_est * len_fraction_est;
651
+ const t_start_est = job.start_seg / N_seg;
652
+ const raw_base_phi = rng() * PI2;
653
+ const scheduled_children = [];
654
+ let has_children = false;
655
+
656
+ const rules = outgoing_rules[generator];
657
+ for (let r = 0; r < rules.length; r++) {
658
+ const rule = rules[r];
659
+ if (rule.count <= 0 || !can_bear_children) continue;
660
+ has_children = true;
661
+
662
+ const planned_child_count = effective_span < 0.99
663
+ ? Math.max(1, Math.round(rule.count * effective_span))
664
+ : rule.count;
665
+ const t_lo = Math.max(clamp(rule.attach_min, 0.0, 0.92), t_start_est);
666
+ const t_hi = Math.min(0.968, rule.attach_max);
667
+ const t_cap = Math.min(0.98, rule.attach_max);
668
+ if (t_hi <= t_lo + 1e-3) continue;
669
+
670
+ // Each rule turns its family by the golden angle, so families sharing a stretch of the parent interleave
671
+ let base_phi = raw_base_phi + r * GOLDEN_ANGLE;
672
+ if (
673
+ level >= 1
674
+ && (rule.phyllotaxis_mode === PhyllotaxisMode.AlternateDistichous
675
+ || rule.phyllotaxis_mode === PhyllotaxisMode.Opposite)
676
+ ) {
677
+ // Align distichous / opposite lateral branching plane with the horizontal plane perpendicular to T
678
+ const horiz_phi = Math.atan2(-cur_ry, cur_sy);
679
+ base_phi = horiz_phi + (raw_base_phi < Math.PI ? 0.0 : Math.PI) + ((raw_base_phi % Math.PI) / Math.PI - 0.5) * 0.28;
680
+ }
681
+
682
+ for (let c = 0; c < planned_child_count; c++) {
683
+ const u_c = rule_node_u(rule, c, planned_child_count, level === 0);
684
+ const t_att = clamp(
685
+ lerp(t_lo, t_hi, clamp(u_c, 0.0, 1.0)) + (rng() - 0.5) * ((t_hi - t_lo) * 0.25 / planned_child_count),
686
+ t_lo,
687
+ t_cap
688
+ );
689
+
690
+ let phi_c = base_phi + rule_node_phi(rule, c);
691
+ if (rule.phyllotaxis_mode === PhyllotaxisMode.FlareLobes) {
692
+ // On a crest of this branch's basal flare lobes, in the same frame the bark mesher shapes them in
693
+ const lobes = Math.max(1, lvl_cfg.flare_lobes | 0);
694
+ const arc_est = job.initial_arc_s + t_att * job.length;
695
+ phi_c = ((c % lobes) * PI2) / lobes - flare_lobe_phase_offset(arc_est, phase_hash);
696
+ }
697
+ phi_c += (rng() * 2.0 - 1.0) * rule.azimuth_jitter;
698
+ scheduled_children.push({ t_attach: t_att, phi: phi_c, rule, age: 0.0, retained: 1.0 });
699
+ }
700
+
701
+ // Self-pruning: below t_lo this crown rule once bore children too, at the same nodes, which died as the
702
+ // crown rose above them and were shed. Age runs from 0 just below the living children to 1 at the stem's
703
+ // base; a branch that died earlier died smaller, (1 - age) of its size today. It keeps
704
+ // (1 - self_pruning) - age of its length as a dead stub, and once that no longer clears the bark it has
705
+ // been grown over, leaving a scar.
706
+ if (rule.crown_envelope && !job.is_split_fork && planned_child_count > 1) {
707
+ const persistence = 1.0 - species.branches[rule.target].self_pruning;
708
+ let t_node = t_lo;
709
+ for (let c = -1; ; c--) {
710
+ const t_dead = lerp(t_lo, t_hi, rule_node_u(rule, c, planned_child_count, level === 0));
711
+ // past the stem's base, or a layout of a single node (one pair, one whorl) that has no 'below'
712
+ if (t_dead <= 0.0 || t_dead > t_node) break;
713
+ t_node = t_dead;
714
+ const age = (t_lo - t_dead) / t_lo;
715
+ const phi_c = base_phi + rule_node_phi(rule, c) + (rng() * 2.0 - 1.0) * rule.azimuth_jitter;
716
+ scheduled_children.push({ t_attach: t_dead, phi: phi_c, rule, age, retained: persistence - age });
717
+ }
718
+ }
719
+ }
720
+
721
+ const k_symp = lvl_cfg.nodal_kink_strength ?? 0.45;
722
+ const zig_base = lvl_cfg.internode_zigzag ?? 0.06;
723
+ const tip_up = lvl_cfg.tip_upcurl ?? 0.18;
724
+
725
+ // Horizontal emergence direction of this branch for outward radial consistency near the trunk centerline
726
+ const emerge_horiz_len = Math.hypot(job.tx, job.tz);
727
+ const emerge_hx = emerge_horiz_len > 1e-4 ? job.tx / emerge_horiz_len : cur_rx;
728
+ const emerge_hz = emerge_horiz_len > 1e-4 ? job.tz / emerge_horiz_len : cur_rz;
729
+
730
+ // heading that straightness pulls back towards: the bud's, re-aimed by a split for the fork that continues
731
+ let aim_x = job.tx;
732
+ let aim_y = job.ty;
733
+ let aim_z = job.tz;
734
+
735
+ for (let s = job.start_seg + 1; s <= N_seg; s++) {
736
+ const t_prev = (s - 1) / N_seg;
737
+ const t_curr = s / N_seg;
738
+ const t_shape = lerp(job.cum_t0, job.cum_t1, t_curr);
739
+
740
+ // 1. Intrinsic S-curve pitch curvature (zero-mean higher harmonic on trunk so trunk weaves rather than leaning)
741
+ let s_wave = level === 0
742
+ ? (lvl_cfg.curvature * Math.sin(PI2 * 1.5 * t_shape + phase_hash * PI2))
743
+ : (lvl_cfg.curve_back !== 0.0
744
+ ? (lvl_cfg.curvature * Math.cos(Math.PI * t_shape) + lvl_cfg.curve_back * Math.sin(PI2 * t_shape))
745
+ : lvl_cfg.curvature);
746
+ // Attenuate negative (inward-toward-parent) pitch curvature on steep upper-crown insertions
747
+ // so upper lateral branches never curve backward across the parent stem axis.
748
+ if (level >= 1 && s_wave < 0.0 && job.parent_attach_t > 0.60) {
749
+ s_wave *= clamp(1.0 - (job.parent_attach_t - 0.60) * 2.2, 0.15, 1.0);
750
+ }
751
+ const d_pitch = s_wave / Math.max(2, N_shape * 0.65);
752
+
753
+ if (Math.abs(d_pitch) > 1e-6) {
754
+ rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, cur_sx, cur_sy, cur_sz, d_pitch);
755
+ cur_tx = rot_scratch[0];
756
+ cur_ty = rot_scratch[1];
757
+ cur_tz = rot_scratch[2];
758
+ rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_sx, cur_sy, cur_sz, d_pitch);
759
+ cur_rx = rot_scratch[0];
760
+ cur_ry = rot_scratch[1];
761
+ cur_rz = rot_scratch[2];
762
+ }
763
+
764
+ // 2. Helical spiral twist around T
765
+ if (Math.abs(lvl_cfg.spiral_twist) > 1e-6) {
766
+ const d_twist = lvl_cfg.spiral_twist / N_shape;
767
+ rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_tx, cur_ty, cur_tz, d_twist);
768
+ cur_rx = rot_scratch[0];
769
+ cur_ry = rot_scratch[1];
770
+ cur_rz = rot_scratch[2];
771
+ cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
772
+ cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
773
+ cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
774
+ }
775
+
776
+ // 2b. Sympodial Nodal Kinking at scheduled lateral child emissions + Internodal Zig-Zag
777
+ let kink_rx = 0.0;
778
+ let kink_ry = 0.0;
779
+ let kink_rz = 0.0;
780
+
781
+ if (k_symp > 0.0) {
782
+ for (let c = 0; c < scheduled_children.length; c++) {
783
+ const sc = scheduled_children[c];
784
+ if (sc.rule.parent_kink > 0.0 && sc.t_attach >= t_prev && sc.t_attach <= t_curr) {
785
+ // Recoil away from child branch emission azimuth phi
786
+ const c_phi = Math.cos(sc.phi);
787
+ const s_phi = Math.sin(sc.phi);
788
+ const em_x = c_phi * cur_rx + s_phi * cur_sx;
789
+ const em_y = c_phi * cur_ry + s_phi * cur_sy;
790
+ const em_z = c_phi * cur_rz + s_phi * cur_sz;
791
+ const apical_boost = sc.t_attach > 0.85 ? 1.35 : 1.0;
792
+ const recoil = k_symp * sc.rule.parent_kink * (level === 0 ? 0.34 : 0.48) * apical_boost * (0.75 + 0.5 * rng());
793
+ kink_rx -= em_x * recoil;
794
+ kink_ry -= em_y * recoil;
795
+ kink_rz -= em_z * recoil;
796
+ }
797
+ }
798
+ }
799
+
800
+ if (zig_base > 0.0) {
801
+ const z_sign = (s & 1) === 0 ? 1.0 : -1.0;
802
+ const z_phi = phase_hash * PI2 + s * 0.85;
803
+ const zx = Math.cos(z_phi) * cur_rx + Math.sin(z_phi) * cur_sx;
804
+ const zy = Math.cos(z_phi) * cur_ry + Math.sin(z_phi) * cur_sy;
805
+ const zz = Math.cos(z_phi) * cur_rz + Math.sin(z_phi) * cur_sz;
806
+ const z_amp = zig_base * z_sign * (0.7 + 0.6 * rng());
807
+ kink_rx += zx * z_amp;
808
+ kink_ry += zy * z_amp;
809
+ kink_rz += zz * z_amp;
810
+ }
811
+
812
+ const kink_mag = Math.hypot(kink_rx, kink_ry, kink_rz);
813
+ if (kink_mag > 1e-5) {
814
+ const kx = kink_rx / kink_mag;
815
+ const ky = kink_ry / kink_mag;
816
+ const kz = kink_rz / kink_mag;
817
+ // Bend axis = T x K
818
+ let bx = cur_ty * kz - cur_tz * ky;
819
+ let by = cur_tz * kx - cur_tx * kz;
820
+ let bz = cur_tx * ky - cur_ty * kx;
821
+ const b_len = Math.hypot(bx, by, bz);
822
+ if (b_len > 1e-6) {
823
+ bx /= b_len;
824
+ by /= b_len;
825
+ bz /= b_len;
826
+ const clamped_kink = Math.min(0.42, kink_mag);
827
+ rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, bx, by, bz, clamped_kink);
828
+ cur_tx = rot_scratch[0];
829
+ cur_ty = rot_scratch[1];
830
+ cur_tz = rot_scratch[2];
831
+ rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, bx, by, bz, clamped_kink);
832
+ cur_rx = rot_scratch[0];
833
+ cur_ry = rot_scratch[1];
834
+ cur_rz = rot_scratch[2];
835
+ cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
836
+ cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
837
+ cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
838
+ }
839
+ }
840
+
841
+ // 3. Biomechanical Tropism steering (Gravitropic sag + Terminal Phototropic Up-Curl + Outward spread)
842
+ let steer_x = 0.0;
843
+ let steer_y = 0.0;
844
+ let steer_z = 0.0;
845
+
846
+ if (level === 0 && branch_id === 0) {
847
+ // Orthotropic upward striving & plumb-line righting reflex along guide curve (default vertical +Y)
848
+ evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, t_prev, job.length);
849
+ evaluate_trunk_guide(SCRATCH_GUIDE_P1, SCRATCH_GUIDE_T, species.trunk_guide, t_curr, job.length);
850
+ const gtx = SCRATCH_GUIDE_T[0];
851
+ const gty = SCRATCH_GUIDE_T[1];
852
+ const gtz = SCRATCH_GUIDE_T[2];
853
+
854
+ const is_conifer = lvl_cfg.excurrent;
855
+ const apical_relax = (!is_conifer && (t_shape > 0.80 || job.has_split)) ? 0.55 : 1.0;
856
+ const righting = (1.65 + Math.max(0.0, -lvl_cfg.gravitropism) * 2.5 + Math.max(0.0, lvl_cfg.phototropism)) * apical_relax;
857
+ steer_x += gtx * righting;
858
+ steer_y += gty * righting;
859
+ steer_z += gtz * righting;
860
+
861
+ // Plumb-line centering force pulling lateral drift back toward the guide curve
862
+ const dx_g = cur_px - SCRATCH_GUIDE_P0[0];
863
+ const dy_g = cur_py - SCRATCH_GUIDE_P0[1];
864
+ const dz_g = cur_pz - SCRATCH_GUIDE_P0[2];
865
+ const dot_g = dx_g * gtx + dy_g * gty + dz_g * gtz;
866
+ const lat_x = dx_g - dot_g * gtx;
867
+ const lat_y = dy_g - dot_g * gty;
868
+ const lat_z = dz_g - dot_g * gtz;
869
+ const plumb_k = ((is_conifer ? 5.2 : 3.4) * apical_relax) / Math.max(1.0, job.length);
870
+ steer_x -= lat_x * plumb_k * N_seg * 0.5;
871
+ steer_y -= lat_y * plumb_k * N_seg * 0.5;
872
+ steer_z -= lat_z * plumb_k * N_seg * 0.5;
873
+ } else if (level === 0 && branch_id > 0) {
874
+ // Secondary bifurcated trunk leader (fork): ascend skyward while preserving fork divergence
875
+ steer_x += emerge_hx * 0.35;
876
+ steer_y += 1.25 + Math.max(0.0, lvl_cfg.phototropism);
877
+ steer_z += emerge_hz * 0.35;
878
+ } else {
879
+ if (lvl_cfg.gravitropism !== 0.0 || tip_up !== 0.0) {
880
+ // Continuous blend from cantilever mid-branch sag arch (sin(pi * t^0.75)) to
881
+ // pendulous weeping cascade (0.65 + 0.95 * t) based on gravitropism vs. tip_upcurl
882
+ const w_pendulous = level >= 2
883
+ ? clamp((lvl_cfg.gravitropism - 0.35 - 0.5 * Math.max(0.0, tip_up)) / 0.45, 0.0, 1.0)
884
+ : 0.0;
885
+ const arch_grav_env = Math.sin(Math.PI * Math.pow(t_shape, 0.75));
886
+ const weep_grav_env = 0.65 + 0.95 * t_shape;
887
+ const grav_env = lerp(arch_grav_env, weep_grav_env, w_pendulous);
888
+ const apical_grav_atten = level === 1 && job.parent_attach_t > 0.75 && w_pendulous < 0.1
889
+ ? clamp(1.0 - (job.parent_attach_t - 0.75) * 3.2, 0.15, 1.0)
890
+ : 1.0;
891
+ const up_env = t_shape * t_shape * (1.0 + 0.5 * Math.max(0.0, -cur_ty));
892
+ steer_y += -lvl_cfg.gravitropism * grav_env * apical_grav_atten + tip_up * up_env;
893
+ }
894
+
895
+ if (lvl_cfg.phototropism !== 0.0) {
896
+ const sky_mix = crown.sky_phototropism_mix;
897
+ const horiz_r = Math.sqrt(cur_px * cur_px + cur_pz * cur_pz);
898
+ let out_x = 0.0;
899
+ let out_z = 0.0;
900
+ if (horiz_r > 0.52) {
901
+ out_x = cur_px / horiz_r;
902
+ out_z = cur_pz / horiz_r;
903
+ } else if (horiz_r > 1e-4) {
904
+ const w_rad = horiz_r / 0.52;
905
+ const bx = lerp(emerge_hx, cur_px / horiz_r, w_rad);
906
+ const bz = lerp(emerge_hz, cur_pz / horiz_r, w_rad);
907
+ const bl = Math.hypot(bx, bz) || 1.0;
908
+ out_x = bx / bl;
909
+ out_z = bz / bl;
910
+ } else {
911
+ out_x = emerge_hx;
912
+ out_z = emerge_hz;
913
+ }
914
+
915
+ steer_x += lvl_cfg.phototropism * (1.0 - sky_mix) * out_x;
916
+ steer_y += lvl_cfg.phototropism * sky_mix;
917
+ steer_z += lvl_cfg.phototropism * (1.0 - sky_mix) * out_z;
918
+ }
919
+ }
920
+
921
+ // 4. 2-Octave 3D Curl Noise gnarl scaled properly by sqrt(6 / N_seg)
922
+ if (lvl_cfg.gnarl_strength > 0.0) {
923
+ const freq = lvl_cfg.gnarl_frequency;
924
+ curl_noise_3d(
925
+ SCRATCH_CURL,
926
+ cur_px * freq + noise_ox + level * 17.3,
927
+ cur_py * freq + noise_oy + level * 31.7,
928
+ cur_pz * freq + noise_oz + level * 47.1
929
+ );
930
+ const c1x = SCRATCH_CURL[0], c1y = SCRATCH_CURL[1], c1z = SCRATCH_CURL[2];
931
+ curl_noise_3d(
932
+ SCRATCH_CURL,
933
+ cur_px * freq * 2.15 + noise_ox + 53.1 + phase_hash * 29.3,
934
+ cur_py * freq * 2.15 + noise_oy + 79.3,
935
+ cur_pz * freq * 2.15 + noise_oz + 113.7
936
+ );
937
+ const cx_tot = 0.68 * c1x + 0.32 * SCRATCH_CURL[0];
938
+ const cy_tot = 0.68 * c1y + 0.32 * SCRATCH_CURL[1];
939
+ const cz_tot = 0.68 * c1z + 0.32 * SCRATCH_CURL[2];
940
+
941
+ const gnarl_scale = lvl_cfg.gnarl_strength * Math.sqrt(6.0 / Math.max(2, N_shape));
942
+ // Project curl vector perpendicular to T so it steers direction cleanly
943
+ const dot_ct = cx_tot * cur_tx + cy_tot * cur_ty + cz_tot * cur_tz;
944
+ steer_x += (cx_tot - dot_ct * cur_tx) * gnarl_scale * 2.2;
945
+ steer_y += (cy_tot - dot_ct * cur_ty) * gnarl_scale * 1.6;
946
+ steer_z += (cz_tot - dot_ct * cur_tz) * gnarl_scale * 2.2;
947
+ }
948
+
949
+ // 4b. A plagiotropic shoot is pulled towards the horizontal; straightness pulls back towards the bud's heading
950
+ if (level >= 1 && lvl_cfg.plagiotropism > 0.0) {
951
+ steer_y -= lvl_cfg.plagiotropism * cur_ty;
952
+ }
953
+ if (lvl_cfg.straightness > 0.0 && !(level === 0 && branch_id === 0)) {
954
+ steer_x += lvl_cfg.straightness * (aim_x - cur_tx);
955
+ steer_y += lvl_cfg.straightness * (aim_y - cur_ty);
956
+ steer_z += lvl_cfg.straightness * (aim_z - cur_tz);
957
+ }
958
+
959
+ // 5. Space colonization attraction steering (for branches in crown, level >= 1)
960
+ if (use_space_col && level >= 1 && job.in_crown && attr_grid !== null) {
961
+ let attr_vx = 0.0;
962
+ let attr_vy = 0.0;
963
+ let attr_vz = 0.0;
964
+ let attr_hits = 0;
965
+
966
+ attr_grid.forEachNear(cur_px, cur_py, cur_pz, (idx) => {
967
+ if (attr_alive[idx] === 0) return;
968
+ const ax = attr_positions[idx * 3] - cur_px;
969
+ const ay = attr_positions[idx * 3 + 1] - cur_py;
970
+ const az = attr_positions[idx * 3 + 2] - cur_pz;
971
+ const d2 = ax * ax + ay * ay + az * az;
972
+ if (d2 <= r_kill_sq) {
973
+ attr_alive[idx] = 0;
974
+ } else if (d2 <= r_influence_sq && d2 > 1e-8) {
975
+ const inv_d = 1.0 / Math.sqrt(d2);
976
+ attr_vx += ax * inv_d;
977
+ attr_vy += ay * inv_d;
978
+ attr_vz += az * inv_d;
979
+ attr_hits++;
980
+ }
981
+ });
982
+
983
+ if (attr_hits > 0) {
984
+ const attr_len = Math.sqrt(attr_vx * attr_vx + attr_vy * attr_vy + attr_vz * attr_vz);
985
+ if (attr_len > 1e-6) {
986
+ const w_sc = crown.space_colonization_weight / attr_len;
987
+ steer_x += attr_vx * w_sc;
988
+ steer_y += attr_vy * w_sc;
989
+ steer_z += attr_vz * w_sc;
990
+ }
991
+ }
992
+ }
993
+
994
+ // 6. Crown envelope soft containment for lateral branches (level >= 1)
995
+ let seg_len = step_len;
996
+ if (level >= 1 && job.in_crown && crown.prune_ratio > 0.0) {
997
+ const probe_x = cur_px + cur_tx * step_len;
998
+ const probe_y = cur_py + cur_ty * step_len;
999
+ const probe_z = cur_pz + cur_tz * step_len;
1000
+ const sdf = crown_envelope_sdf(probe_x, probe_y, probe_z, crown);
1001
+ if (sdf > 0.0) {
1002
+ // Steer radially inward toward trunk axis (only pulling vertically if outside vertical crown span)
1003
+ const crown_top_y = crown.base_height + crown.height;
1004
+ const to_cx = -probe_x;
1005
+ const to_cy = probe_y > crown_top_y
1006
+ ? crown_top_y - probe_y
1007
+ : (probe_y < crown.base_height ? crown.base_height - probe_y : 0.0);
1008
+ const to_cz = -probe_z;
1009
+ const to_c_len = Math.sqrt(to_cx * to_cx + to_cy * to_cy + to_cz * to_cz);
1010
+ if (to_c_len > 1e-5) {
1011
+ const pull = crown.prune_ratio * 0.45 / to_c_len;
1012
+ steer_x += to_cx * pull;
1013
+ steer_y += to_cy * pull;
1014
+ steer_z += to_cz * pull;
1015
+ }
1016
+ seg_len *= Math.max(0.35, 1.0 - crown.prune_ratio * 0.5);
1017
+ }
1018
+ }
1019
+
1020
+ // Compute target radius r_curr for this segment before BVH collision checks
1021
+ const t_spine = t_shape;
1022
+ const u_prof = prof_span > 1e-4
1023
+ ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(t_spine)) / prof_span, 0.0, 1.0)
1024
+ : t_spine;
1025
+ const min_tip_frac = level === 0 ? 0.12 : (has_children ? 0.14 : 0.24);
1026
+ const apical_shoot_taper = level === 0
1027
+ ? (t_spine > 0.65 ? lerp(1.0, 0.11, Math.pow((t_spine - 0.65) / 0.35, 1.45)) : 1.0)
1028
+ : (has_children && t_curr > 0.80 ? lerp(1.0, 0.62, Math.pow((t_curr - 0.80) / 0.20, 1.35)) : 1.0);
1029
+ const r_curr = Math.max(
1030
+ level === 0 ? 0.0015 : 0.002,
1031
+ job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_prof, min_tip_frac) * apical_shoot_taper
1032
+ );
1033
+ const r_prev = out.node_read_radius(prev_v);
1034
+ const local_seg_idx = branch_edges.length;
1035
+ const parent_bA = job.parent_branch_id;
1036
+ const attach_seg_A = job.parent_attach_seg_idx ?? -1;
1037
+ const is_split_A = Boolean(job.is_split_fork);
1038
+
1039
+ // 6b. Soft BVH Repulsion Field (steers growing shoot smoothly around nearby existing branches)
1040
+ if ((level >= 1 || branch_id > 0) && bvh.size > 0) {
1041
+ const look_len = seg_len * 1.55;
1042
+ const look_x = cur_px + cur_tx * look_len;
1043
+ const look_y = cur_py + cur_ty * look_len;
1044
+ const look_z = cur_pz + cur_tz * look_len;
1045
+ const soft_margin = Math.max(0.08, Math.min(0.24, seg_len * 0.65));
1046
+
1047
+ query_capsule[0] = cur_px;
1048
+ query_capsule[1] = cur_py;
1049
+ query_capsule[2] = cur_pz;
1050
+ query_capsule[3] = look_x;
1051
+ query_capsule[4] = look_y;
1052
+ query_capsule[5] = look_z;
1053
+ query_capsule[6] = Math.max(r_prev, r_curr) * 1.15 + soft_margin;
1054
+
1055
+ const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
1056
+ for (let h = 0; h < hit_count; h++) {
1057
+ const eB = bvh_hits[h];
1058
+ const bB = seg_branch_id[eB];
1059
+ const local_idx_B = seg_local_idx[eB];
1060
+ if (!resolve_subsegment_bounds(
1061
+ branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
1062
+ bB, out.branch_parent[bB], branch_parent_seg_idx[bB], branch_is_split_fork[bB], local_idx_B,
1063
+ sub_bounds
1064
+ )) {
1065
+ continue;
1066
+ }
1067
+
1068
+ const s_min = sub_bounds[0];
1069
+ const t_min = sub_bounds[1];
1070
+ const sa0x = cur_px + (look_x - cur_px) * s_min;
1071
+ const sa0y = cur_py + (look_y - cur_py) * s_min;
1072
+ const sa0z = cur_pz + (look_z - cur_pz) * s_min;
1073
+
1074
+ const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
1075
+ const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
1076
+ const sb0x = b0x + (b1x - b0x) * t_min;
1077
+ const sb0y = b0y + (b1y - b0y) * t_min;
1078
+ const sb0z = b0z + (b1z - b0z) * t_min;
1079
+
1080
+ line3_compute_segment_segment_closest_points(
1081
+ closest_st,
1082
+ sa0x, sa0y, sa0z, look_x, look_y, look_z,
1083
+ sb0x, sb0y, sb0z, b1x, b1y, b1z
1084
+ );
1085
+
1086
+ const s_f = s_min + (1.0 - s_min) * closest_st[0];
1087
+ const t_f = t_min + (1.0 - t_min) * closest_st[1];
1088
+
1089
+ const qAx = cur_px + (look_x - cur_px) * s_f;
1090
+ const qAy = cur_py + (look_y - cur_py) * s_f;
1091
+ const qAz = cur_pz + (look_z - cur_pz) * s_f;
1092
+ const qBx = b0x + (b1x - b0x) * t_f;
1093
+ const qBy = b0y + (b1y - b0y) * t_f;
1094
+ const qBz = b0z + (b1z - b0z) * t_f;
1095
+
1096
+ const dx = qAx - qBx;
1097
+ const dy = qAy - qBy;
1098
+ const dz = qAz - qBz;
1099
+ const dist = Math.hypot(dx, dy, dz);
1100
+ const rA = r_prev + (r_curr - r_prev) * Math.min(1.0, s_f);
1101
+ const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
1102
+ const r_req = (rA + rB) * 1.12 + 0.005;
1103
+
1104
+ if (dist < r_req + soft_margin) {
1105
+ let nx = dist > 1e-5 ? dx / dist : cur_rx;
1106
+ let ny = dist > 1e-5 ? dy / dist : cur_ry;
1107
+ let nz = dist > 1e-5 ? dz / dist : cur_rz;
1108
+ // Project repulsion normal perpendicular to cur_T so it steers smoothly around the obstacle
1109
+ const dot_nt = nx * cur_tx + ny * cur_ty + nz * cur_tz;
1110
+ nx -= dot_nt * cur_tx;
1111
+ ny -= dot_nt * cur_ty;
1112
+ nz -= dot_nt * cur_tz;
1113
+ const n_len = Math.hypot(nx, ny, nz);
1114
+ if (n_len > 1e-5) {
1115
+ nx /= n_len;
1116
+ ny /= n_len;
1117
+ nz /= n_len;
1118
+ const u_prox = clamp(1.0 - Math.max(0.0, dist - r_req) / soft_margin, 0.0, 1.0);
1119
+ const rep_w = u_prox * u_prox * 2.8;
1120
+ steer_x += nx * rep_w;
1121
+ steer_y += ny * rep_w;
1122
+ steer_z += nz * rep_w;
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ // Apply steering to tangent and normalize
1129
+ const steer_div = Math.max(2.0, Math.sqrt(N_shape) * 1.35);
1130
+ let next_tx = cur_tx + steer_x / steer_div;
1131
+ let next_ty = cur_ty + steer_y / steer_div;
1132
+ let next_tz = cur_tz + steer_z / steer_div;
1133
+ const t_len = Math.sqrt(next_tx * next_tx + next_ty * next_ty + next_tz * next_tz);
1134
+ if (t_len > 1e-12) {
1135
+ next_tx /= t_len;
1136
+ next_ty /= t_len;
1137
+ next_tz /= t_len;
1138
+ } else {
1139
+ next_tx = cur_tx;
1140
+ next_ty = cur_ty;
1141
+ next_tz = cur_tz;
1142
+ }
1143
+
1144
+ let next_px = cur_px + 0.5 * (cur_tx + next_tx) * seg_len;
1145
+ let next_py = cur_py + 0.5 * (cur_ty + next_ty) * seg_len;
1146
+ let next_pz = cur_pz + 0.5 * (cur_tz + next_tz) * seg_len;
1147
+
1148
+ // Orthotropic plumb-tube containment on trunk (level === 0):
1149
+ // Preserves local 3D nodal kinks and serpentine sweep while guaranteeing the trunk never drifts sideways
1150
+ if (level === 0 && branch_id === 0) {
1151
+ const gx = SCRATCH_GUIDE_P1[0];
1152
+ const gy = SCRATCH_GUIDE_P1[1];
1153
+ const gz = SCRATCH_GUIDE_P1[2];
1154
+ const gtx = SCRATCH_GUIDE_T[0];
1155
+ const gty = SCRATCH_GUIDE_T[1];
1156
+ const gtz = SCRATCH_GUIDE_T[2];
1157
+
1158
+ const dx = next_px - gx;
1159
+ const dy = next_py - gy;
1160
+ const dz = next_pz - gz;
1161
+ const dot_g = dx * gtx + dy * gty + dz * gtz;
1162
+ const off_x = dx - dot_g * gtx;
1163
+ const off_y = dy - dot_g * gty;
1164
+ const off_z = dz - dot_g * gtz;
1165
+ const off_r = Math.hypot(off_x, off_y, off_z);
1166
+
1167
+ const is_conifer = lvl_cfg.excurrent;
1168
+ const max_drift_frac = is_conifer ? 0.032 : (job.has_split ? 0.078 : 0.058);
1169
+ // Envelope rises from 0 at root, peaks in mid-trunk (allowing 3D sweep/crookedness), and tapers near tip
1170
+ const env = Math.sin(Math.PI * Math.min(0.92, t_curr * 0.88)) * (1.0 - 0.38 * t_curr * t_curr);
1171
+ const max_r = job.length * max_drift_frac * Math.max(job.has_split ? 0.42 : 0.18, env);
1172
+
1173
+ if (off_r > max_r && off_r > 1e-6) {
1174
+ const scale_r = max_r / off_r;
1175
+ next_px = gx + dot_g * gtx + off_x * scale_r;
1176
+ next_py = gy + dot_g * gty + off_y * scale_r;
1177
+ next_pz = gz + dot_g * gtz + off_z * scale_r;
1178
+
1179
+ let ch_x = next_px - cur_px;
1180
+ let ch_y = next_py - cur_py;
1181
+ let ch_z = next_pz - cur_pz;
1182
+ const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1183
+ if (ch_l > 1e-6) {
1184
+ next_tx = ch_x / ch_l;
1185
+ next_ty = ch_y / ch_l;
1186
+ next_tz = ch_z / ch_l;
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ // 6c. Monotonic Parent Collar Escape + Hard BVH Capsule Contact Projection & Self-Pruning
1192
+ if ((level >= 1 || branch_id > 0) && bvh.size > 0) {
1193
+ let was_projected = false;
1194
+
1195
+ // Ensure lateral child branch exits its parent attachment segment cleanly on local_seg_idx <= 1
1196
+ if (job.parent_edge_id >= 0 && !is_split_A && local_seg_idx <= 1) {
1197
+ const eP = job.parent_edge_id;
1198
+ const p0x = seg_p0x[eP], p0y = seg_p0y[eP], p0z = seg_p0z[eP];
1199
+ const p1x = seg_p1x[eP], p1y = seg_p1y[eP], p1z = seg_p1z[eP];
1200
+
1201
+ line3_compute_segment_segment_closest_points(
1202
+ closest_st,
1203
+ next_px, next_py, next_pz, next_px, next_py, next_pz,
1204
+ p0x, p0y, p0z, p1x, p1y, p1z
1205
+ );
1206
+ const t_p = closest_st[1];
1207
+ const qPx = p0x + (p1x - p0x) * t_p;
1208
+ const qPy = p0y + (p1y - p0y) * t_p;
1209
+ const qPz = p0z + (p1z - p0z) * t_p;
1210
+ const r_p = seg_r0[eP] + (seg_r1[eP] - seg_r0[eP]) * t_p;
1211
+
1212
+ let rx_p = next_px - qPx;
1213
+ let ry_p = next_py - qPy;
1214
+ let rz_p = next_pz - qPz;
1215
+ const d_p = Math.hypot(rx_p, ry_p, rz_p);
1216
+ if (local_seg_idx === 0 && (r_p + r_curr) * 1.18 > seg_len) {
1217
+ seg_len = (r_p + r_curr) * 1.18;
1218
+ }
1219
+ const min_d_p = local_seg_idx === 0
1220
+ ? Math.min(seg_len * 0.88, (r_p + r_curr) * 1.05)
1221
+ : (r_p + r_curr) * 1.10 + 0.004;
1222
+
1223
+ const em_drx = job.emerge_drx ?? cur_rx;
1224
+ const em_dry = job.emerge_dry ?? cur_ry;
1225
+ const em_drz = job.emerge_drz ?? cur_rz;
1226
+ const dot_em = rx_p * em_drx + ry_p * em_dry + rz_p * em_drz;
1227
+
1228
+ if (d_p < min_d_p || dot_em < 0.0) {
1229
+ const ux_p = (d_p > 1e-5 && dot_em > 0.0) ? rx_p / d_p : em_drx;
1230
+ const uy_p = (d_p > 1e-5 && dot_em > 0.0) ? ry_p / d_p : em_dry;
1231
+ const uz_p = (d_p > 1e-5 && dot_em > 0.0) ? rz_p / d_p : em_drz;
1232
+ const push_d = Math.max(0.0, min_d_p - (dot_em > 0.0 ? d_p : -d_p));
1233
+ next_px += ux_p * push_d;
1234
+ next_py += uy_p * push_d;
1235
+ next_pz += uz_p * push_d;
1236
+
1237
+ let ch_x = next_px - cur_px;
1238
+ let ch_y = next_py - cur_py;
1239
+ let ch_z = next_pz - cur_pz;
1240
+ const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1241
+ if (ch_l > 1e-6) {
1242
+ next_tx = ch_x / ch_l;
1243
+ next_ty = ch_y / ch_l;
1244
+ next_tz = ch_z / ch_l;
1245
+ next_px = cur_px + next_tx * seg_len;
1246
+ next_py = cur_py + next_ty * seg_len;
1247
+ next_pz = cur_pz + next_tz * seg_len;
1248
+ was_projected = true;
1249
+ }
1250
+ }
1251
+ }
1252
+
1253
+ // Iterative BVH capsule contact projection
1254
+ for (let iter = 0; iter < 4; iter++) {
1255
+ query_capsule[0] = cur_px;
1256
+ query_capsule[1] = cur_py;
1257
+ query_capsule[2] = cur_pz;
1258
+ query_capsule[3] = next_px;
1259
+ query_capsule[4] = next_py;
1260
+ query_capsule[5] = next_pz;
1261
+ query_capsule[6] = Math.max(r_prev, r_curr) * 1.15 + 0.025;
1262
+
1263
+ const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
1264
+ let moved = false;
1265
+
1266
+ for (let h = 0; h < hit_count; h++) {
1267
+ const eB = bvh_hits[h];
1268
+ const bB = seg_branch_id[eB];
1269
+ const local_idx_B = seg_local_idx[eB];
1270
+ if (!resolve_subsegment_bounds(
1271
+ branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
1272
+ bB, out.branch_parent[bB], branch_parent_seg_idx[bB], branch_is_split_fork[bB], local_idx_B,
1273
+ sub_bounds
1274
+ )) {
1275
+ continue;
1276
+ }
1277
+
1278
+ const s_min = sub_bounds[0];
1279
+ const t_min = sub_bounds[1];
1280
+ const sa0x = cur_px + (next_px - cur_px) * s_min;
1281
+ const sa0y = cur_py + (next_py - cur_py) * s_min;
1282
+ const sa0z = cur_pz + (next_pz - cur_pz) * s_min;
1283
+
1284
+ const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
1285
+ const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
1286
+ const sb0x = b0x + (b1x - b0x) * t_min;
1287
+ const sb0y = b0y + (b1y - b0y) * t_min;
1288
+ const sb0z = b0z + (b1z - b0z) * t_min;
1289
+
1290
+ line3_compute_segment_segment_closest_points(
1291
+ closest_st,
1292
+ sa0x, sa0y, sa0z, next_px, next_py, next_pz,
1293
+ sb0x, sb0y, sb0z, b1x, b1y, b1z
1294
+ );
1295
+
1296
+ const s_f = s_min + (1.0 - s_min) * closest_st[0];
1297
+ const t_f = t_min + (1.0 - t_min) * closest_st[1];
1298
+ if (local_seg_idx === 0 && s_f <= 0.05) continue;
1299
+
1300
+ const qAx = cur_px + (next_px - cur_px) * s_f;
1301
+ const qAy = cur_py + (next_py - cur_py) * s_f;
1302
+ const qAz = cur_pz + (next_pz - cur_pz) * s_f;
1303
+ const qBx = b0x + (b1x - b0x) * t_f;
1304
+ const qBy = b0y + (b1y - b0y) * t_f;
1305
+ const qBz = b0z + (b1z - b0z) * t_f;
1306
+
1307
+ const dx = qAx - qBx;
1308
+ const dy = qAy - qBy;
1309
+ const dz = qAz - qBz;
1310
+ const dist = Math.hypot(dx, dy, dz);
1311
+ const rA = r_prev + (r_curr - r_prev) * s_f;
1312
+ const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
1313
+ const r_req = (rA + rB) * 1.08 + 0.004;
1314
+
1315
+ if (dist < r_req) {
1316
+ let nx = dist > 1e-5 ? dx / dist : cur_rx;
1317
+ let ny = dist > 1e-5 ? dy / dist : cur_ry;
1318
+ let nz = dist > 1e-5 ? dz / dist : cur_rz;
1319
+ const push = (r_req - dist + 0.004) / Math.max(0.35, s_f);
1320
+ next_px += nx * push;
1321
+ next_py += ny * push;
1322
+ next_pz += nz * push;
1323
+
1324
+ let ch_x = next_px - cur_px;
1325
+ let ch_y = next_py - cur_py;
1326
+ let ch_z = next_pz - cur_pz;
1327
+ const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1328
+ if (ch_l > 1e-6) {
1329
+ next_tx = ch_x / ch_l;
1330
+ next_ty = ch_y / ch_l;
1331
+ next_tz = ch_z / ch_l;
1332
+ next_px = cur_px + next_tx * seg_len;
1333
+ next_py = cur_py + next_ty * seg_len;
1334
+ next_pz = cur_pz + next_tz * seg_len;
1335
+ moved = true;
1336
+ was_projected = true;
1337
+ }
1338
+ }
1339
+ }
1340
+ if (!moved) break;
1341
+ }
1342
+
1343
+ // If segment >= 1 still penetrates an existing branch or was forced into an unnatural sharp reversal,
1344
+ // self-prune / arrest the branch at prev_v so it never spears through another bough.
1345
+ if (local_seg_idx >= 1) {
1346
+ const final_clear = evaluate_candidate_clearance(
1347
+ branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
1348
+ cur_px, cur_py, cur_pz, r_prev,
1349
+ next_px, next_py, next_pz, r_curr,
1350
+ -0.004
1351
+ );
1352
+ const turn_cos = cur_tx * next_tx + cur_ty * next_ty + cur_tz * next_tz;
1353
+ if (final_clear < 0.0 || (was_projected && turn_cos < 0.25)) {
1354
+ break;
1355
+ }
1356
+ } else if (was_projected) {
1357
+ // Keep root node tangent aligned with projected first segment so base collar has zero S-hook
1358
+ cur_tx = next_tx;
1359
+ cur_ty = next_ty;
1360
+ cur_tz = next_tz;
1361
+ out.node_write_tangent(prev_v, cur_tx, cur_ty, cur_tz);
1362
+ }
1363
+ }
1364
+
1365
+ // Structural surface root: creep along the ground plane y = 0 while sinking into it, its centreline at
1366
+ // r(t) - 2 r_base t: it leaves the stem fully exposed and its top drops by twice its base radius over
1367
+ // its length, so it dissolves into the soil about halfway out and its tip is never seen. Applied after all
1368
+ // steering and contact handling so nothing lifts it off or pushes it under; the first segment keeps
1369
+ // the bud's heading so the root leaves the stem where it was planted (e.g. on a flare lobe crest).
1370
+ if (lvl_cfg.root) {
1371
+ let hx = local_seg_idx === 0 ? emerge_hx : next_px - cur_px;
1372
+ let hz = local_seg_idx === 0 ? emerge_hz : next_pz - cur_pz;
1373
+ let h_len = Math.hypot(hx, hz);
1374
+ if (h_len < 1e-6) {
1375
+ hx = emerge_hx;
1376
+ hz = emerge_hz;
1377
+ h_len = Math.hypot(hx, hz) || 1.0;
1378
+ }
1379
+ hx /= h_len;
1380
+ hz /= h_len;
1381
+ const dy = clamp(r_curr - 2.0 * job.base_radius * t_curr - cur_py, -0.7 * seg_len, 0.7 * seg_len);
1382
+ const h_step = Math.sqrt(seg_len * seg_len - dy * dy);
1383
+ next_px = cur_px + hx * h_step;
1384
+ next_py = cur_py + dy;
1385
+ next_pz = cur_pz + hz * h_step;
1386
+ // arrive following the sinking profile, whose slope along the root is -(2 + taper) r_base / length
1387
+ const sink_slope = -(2.0 + lvl_cfg.taper) * job.base_radius / Math.max(1e-3, job.length);
1388
+ const t_norm_len = Math.hypot(1.0, sink_slope);
1389
+ next_tx = hx / t_norm_len;
1390
+ next_ty = sink_slope / t_norm_len;
1391
+ next_tz = hz / t_norm_len;
1392
+ if (local_seg_idx === 0) {
1393
+ // the root node leaves along its first chord, as a projected collar escape would
1394
+ cur_tx = (next_px - cur_px) / seg_len;
1395
+ cur_ty = (next_py - cur_py) / seg_len;
1396
+ cur_tz = (next_pz - cur_pz) / seg_len;
1397
+ out.node_write_tangent(prev_v, cur_tx, cur_ty, cur_tz);
1398
+ }
1399
+ }
1400
+
1401
+ // Transport RMF via Wang et al. Double Reflection
1402
+ spine_frame_double_reflection(
1403
+ SCRATCH_RMF, 0,
1404
+ cur_px, cur_py, cur_pz,
1405
+ cur_tx, cur_ty, cur_tz,
1406
+ cur_rx, cur_ry, cur_rz,
1407
+ next_px, next_py, next_pz,
1408
+ next_tx, next_ty, next_tz
1409
+ );
1410
+ let next_rx = SCRATCH_RMF[0];
1411
+ let next_ry = SCRATCH_RMF[1];
1412
+ let next_rz = SCRATCH_RMF[2];
1413
+ cur_sx = SCRATCH_RMF[3];
1414
+ cur_sy = SCRATCH_RMF[4];
1415
+ cur_sz = SCRATCH_RMF[5];
1416
+
1417
+ cur_arc_s += seg_len;
1418
+
1419
+ const curr_v = out.add_node(
1420
+ next_px, next_py, next_pz,
1421
+ next_tx, next_ty, next_tz,
1422
+ next_rx, next_ry, next_rz,
1423
+ r_curr,
1424
+ branch_id,
1425
+ level,
1426
+ job.parent_branch_id,
1427
+ t_curr,
1428
+ cur_arc_s,
1429
+ phase_hash
1430
+ );
1431
+
1432
+ const edge_id = out.add_segment(
1433
+ prev_v,
1434
+ curr_v,
1435
+ cur_tx * seg_len,
1436
+ cur_ty * seg_len,
1437
+ cur_tz * seg_len,
1438
+ next_tx * seg_len,
1439
+ next_ty * seg_len,
1440
+ next_tz * seg_len,
1441
+ seg_len,
1442
+ t_prev === 0.0 ? job.parent_attach_t : t_prev
1443
+ );
1444
+
1445
+ // Register segment in scope-local BVH
1446
+ const bvh_pad = Math.max(r_prev, r_curr) * 1.25 + 0.008;
1447
+ const bvh_leaf = bvh.allocate_node();
1448
+ bvh.node_set_user_data(bvh_leaf, edge_id);
1449
+ bvh.node_set_aabb_primitive(
1450
+ bvh_leaf,
1451
+ Math.min(cur_px, next_px) - bvh_pad,
1452
+ Math.min(cur_py, next_py) - bvh_pad,
1453
+ Math.min(cur_pz, next_pz) - bvh_pad,
1454
+ Math.max(cur_px, next_px) + bvh_pad,
1455
+ Math.max(cur_py, next_py) + bvh_pad,
1456
+ Math.max(cur_pz, next_pz) + bvh_pad
1457
+ );
1458
+ bvh.insert_leaf(bvh_leaf);
1459
+
1460
+ seg_branch_id[edge_id] = branch_id;
1461
+ seg_local_idx[edge_id] = local_seg_idx;
1462
+ seg_p0x[edge_id] = cur_px;
1463
+ seg_p0y[edge_id] = cur_py;
1464
+ seg_p0z[edge_id] = cur_pz;
1465
+ seg_r0[edge_id] = r_prev;
1466
+ seg_p1x[edge_id] = next_px;
1467
+ seg_p1y[edge_id] = next_py;
1468
+ seg_p1z[edge_id] = next_pz;
1469
+ seg_r1[edge_id] = r_curr;
1470
+ seg_bvh_leaf[edge_id] = bvh_leaf;
1471
+
1472
+ branch_edges.push(edge_id);
1473
+ branch_edge_t0.push(t_prev);
1474
+ branch_edge_t1.push(t_curr);
1475
+ branch_vertices.push(curr_v);
1476
+
1477
+ out.branch_tip_vertex[branch_id] = curr_v;
1478
+ out.branch_length[branch_id] = cur_arc_s - job.initial_arc_s;
1479
+
1480
+ // Check for dichotomous / sympodial splitting at interior segments (only when stem is structurally thick enough)
1481
+ const min_split_seg = level === 0
1482
+ ? Math.max(2, Math.floor(N_seg * 0.5))
1483
+ : Math.max(1, Math.floor(N_seg * 0.25));
1484
+ if (
1485
+ !job.has_split
1486
+ && !job.dead
1487
+ && lvl_cfg.split_probability > 0.0
1488
+ && r_curr >= 0.012
1489
+ && s >= min_split_seg
1490
+ && s <= Math.floor(N_seg * 0.75)
1491
+ && rng() < lvl_cfg.split_probability
1492
+ ) {
1493
+ job.has_split = true;
1494
+ const fork_count = Math.max(2, lvl_cfg.split_count | 0);
1495
+ const split_ang = lvl_cfg.split_angle;
1496
+ let base_az = rng() * PI2;
1497
+
1498
+ // Preserve pre-split orthonormal triad (T, R, S) before deflecting fork 0
1499
+ const pre_tx = next_tx, pre_ty = next_ty, pre_tz = next_tz;
1500
+ const pre_rx = next_rx, pre_ry = next_ry, pre_rz = next_rz;
1501
+ const pre_sx = cur_sx, pre_sy = cur_sy, pre_sz = cur_sz;
1502
+
1503
+ // On lateral branches (level >= 1), align the split bend axis toward local vertical (+Y)
1504
+ // so the two daughter forks spread horizontally in the canopy rather than pitching into neighbor boughs.
1505
+ if (level >= 1) {
1506
+ const up_az = Math.atan2(pre_sy, pre_ry);
1507
+ const side_sign = base_az < Math.PI ? 1.0 : -1.0;
1508
+ base_az = up_az + (side_sign < 0.0 ? Math.PI : 0.0) + ((base_az % Math.PI) / Math.PI - 0.5) * 0.45;
1509
+ }
1510
+
1511
+ // Current branch deflects as fork 0: rotate both T and R around bend_axis so R remains orthogonal to T
1512
+ const fork0_deflect = level === 0 ? split_ang * 0.22 : split_ang * 0.5;
1513
+ const bend_axis_x = Math.cos(base_az) * pre_rx + Math.sin(base_az) * pre_sx;
1514
+ const bend_axis_y = Math.cos(base_az) * pre_ry + Math.sin(base_az) * pre_sy;
1515
+ const bend_axis_z = Math.cos(base_az) * pre_rz + Math.sin(base_az) * pre_sz;
1516
+ rotate_around_axis(
1517
+ rot_scratch,
1518
+ pre_tx, pre_ty, pre_tz,
1519
+ bend_axis_x, bend_axis_y, bend_axis_z,
1520
+ fork0_deflect
1521
+ );
1522
+ next_tx = rot_scratch[0];
1523
+ next_ty = rot_scratch[1];
1524
+ next_tz = rot_scratch[2];
1525
+ // fork 0 is aimed where it was deflected to, as the other forks are aimed along their own headings
1526
+ aim_x = next_tx;
1527
+ aim_y = next_ty;
1528
+ aim_z = next_tz;
1529
+
1530
+ rotate_around_axis(
1531
+ rot_scratch,
1532
+ pre_rx, pre_ry, pre_rz,
1533
+ bend_axis_x, bend_axis_y, bend_axis_z,
1534
+ fork0_deflect
1535
+ );
1536
+ next_rx = rot_scratch[0];
1537
+ next_ry = rot_scratch[1];
1538
+ next_rz = rot_scratch[2];
1539
+
1540
+ cur_sx = next_ty * next_rz - next_tz * next_ry;
1541
+ cur_sy = next_tz * next_rx - next_tx * next_rz;
1542
+ cur_sz = next_tx * next_ry - next_ty * next_rx;
1543
+
1544
+ // Spawn fork 1..fork_count-1 as continuation branches with their own root vertex connected to curr_v
1545
+ const rem_segs = Math.max(1, N_seg - s);
1546
+ const rem_fraction = Math.max(0.15, rem_segs / Math.max(1, N_seg));
1547
+ const fork_cum_t0 = lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_curr);
1548
+ const fork_cum_t1 = job.cum_t1 ?? 1.0;
1549
+ const fork_len = Math.max(0.15, job.length * rem_fraction);
1550
+ const fork_r0 = r_curr * 0.78;
1551
+ const fork_step = fork_len / rem_segs;
1552
+
1553
+ for (let f = 1; f < fork_count; f++) {
1554
+ const f_az = base_az + (f * PI2) / fork_count;
1555
+ const fax = Math.cos(f_az) * pre_rx + Math.sin(f_az) * pre_sx;
1556
+ const fay = Math.cos(f_az) * pre_ry + Math.sin(f_az) * pre_sy;
1557
+ const faz = Math.cos(f_az) * pre_rz + Math.sin(f_az) * pre_sz;
1558
+
1559
+ const f_tan = [0, 0, 0];
1560
+ rotate_around_axis(f_tan, pre_tx, pre_ty, pre_tz, fax, fay, faz, split_ang * 0.65);
1561
+
1562
+ // Verify fork emergence clearance against BVH (and prevent inward-toward-trunk forks on lateral boughs)
1563
+ if (level >= 1) {
1564
+ const r_horiz_here = Math.hypot(next_px, next_pz);
1565
+ if (r_horiz_here > 0.35 && (next_px * f_tan[0] + next_pz * f_tan[2]) < -0.10 * r_horiz_here) {
1566
+ continue;
1567
+ }
1568
+ const f_clear = evaluate_candidate_clearance(
1569
+ -1, branch_id, local_seg_idx, true, 0,
1570
+ next_px, next_py, next_pz, fork_r0,
1571
+ next_px + f_tan[0] * fork_step,
1572
+ next_py + f_tan[1] * fork_step,
1573
+ next_pz + f_tan[2] * fork_step,
1574
+ fork_r0 * 0.85,
1575
+ 0.002
1576
+ );
1577
+ if (f_clear < -0.002) {
1578
+ continue;
1579
+ }
1580
+ }
1581
+
1582
+ const f_r = [0, 0, 0];
1583
+ rotate_around_axis(f_r, pre_rx, pre_ry, pre_rz, fax, fay, faz, split_ang * 0.65);
1584
+
1585
+ queue.push({
1586
+ level,
1587
+ generator,
1588
+ in_crown: job.in_crown,
1589
+ parent_branch_id: branch_id,
1590
+ parent_edge_id: edge_id,
1591
+ parent_attach_seg_idx: local_seg_idx,
1592
+ parent_attach_t: t_curr,
1593
+ attach_parent_vertex: curr_v,
1594
+ start_vertex: -1,
1595
+ px: next_px,
1596
+ py: next_py,
1597
+ pz: next_pz,
1598
+ tx: f_tan[0],
1599
+ ty: f_tan[1],
1600
+ tz: f_tan[2],
1601
+ rx: f_r[0],
1602
+ ry: f_r[1],
1603
+ rz: f_r[2],
1604
+ emerge_drx: fax,
1605
+ emerge_dry: fay,
1606
+ emerge_drz: faz,
1607
+ length: fork_len,
1608
+ full_branch_length: job.full_branch_length ?? job.length,
1609
+ base_radius: fork_r0,
1610
+ start_seg: 0,
1611
+ total_segs: rem_segs,
1612
+ shape_segs: rem_segs,
1613
+ initial_arc_s: cur_arc_s,
1614
+ has_split: true,
1615
+ is_split_fork: true,
1616
+ child_span_fraction: rem_fraction,
1617
+ cum_t0: fork_cum_t0,
1618
+ cum_t1: fork_cum_t1
1619
+ });
1620
+ }
1621
+ }
1622
+
1623
+ cur_px = next_px;
1624
+ cur_py = next_py;
1625
+ cur_pz = next_pz;
1626
+ cur_tx = next_tx;
1627
+ cur_ty = next_ty;
1628
+ cur_tz = next_tz;
1629
+ cur_rx = next_rx;
1630
+ cur_ry = next_ry;
1631
+ cur_rz = next_rz;
1632
+ prev_v = curr_v;
1633
+ }
1634
+
1635
+ // If the branch was self-pruned early on contact (actual_segs < remaining_segs), renormalize
1636
+ // t_norm and taper radii across the surviving vertices [0..actual_segs] so its tip terminates cleanly at t_norm = 1.0
1637
+ const actual_segs = branch_edges.length;
1638
+ if (actual_segs > 0 && actual_segs < remaining_segs) {
1639
+ const s_param = /** @type {Float32Array} */ (out.attr_node_spine_param.data);
1640
+ for (let i = 0; i < actual_segs; i++) {
1641
+ const tn0 = i / actual_segs;
1642
+ const tn1 = (i + 1) / actual_segs;
1643
+ branch_edge_t0[i] = tn0;
1644
+ branch_edge_t1[i] = tn1;
1645
+
1646
+ const v_curr = branch_vertices[i + 1];
1647
+ s_param[v_curr * 3] = tn1;
1648
+
1649
+ const u_p = prof_span > 1e-4
1650
+ ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(lerp(job.cum_t0, job.cum_t1, tn1))) / prof_span, 0.0, 1.0)
1651
+ : tn1;
1652
+ const min_tf = has_children ? 0.14 : 0.24;
1653
+ const ap_taper = has_children && tn1 > 0.80
1654
+ ? lerp(1.0, 0.62, Math.pow((tn1 - 0.80) / 0.20, 1.35))
1655
+ : 1.0;
1656
+ const r_new = Math.max(
1657
+ 0.002,
1658
+ job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_p, min_tf) * ap_taper
1659
+ );
1660
+ out.node_write_radius(v_curr, r_new);
1661
+
1662
+ const e_curr = branch_edges[i];
1663
+ seg_r1[e_curr] = r_new;
1664
+ if (i + 1 < actual_segs) {
1665
+ seg_r0[branch_edges[i + 1]] = r_new;
1666
+ }
1667
+ }
1668
+ }
1669
+
1670
+ // Smooth or preserve crisp interior Hermite tangents across consecutive edges of this branch
1671
+ // (preserving branch_edges[0]'s m0 so continuation forks keep their exact fork departure tangent)
1672
+ if (branch_edges.length >= 2) {
1673
+ const kink_sharp = clamp(lvl_cfg.kink_sharpness ?? 0.5, 0.0, 0.92);
1674
+ for (let k = 1; k < branch_edges.length; k++) {
1675
+ const e_prev = branch_edges[k - 1];
1676
+ const e_next = branch_edges[k];
1677
+ const v_mid = branch_vertices[k];
1678
+ const l_prev = Math.max(1e-5, out.segment_length(e_prev));
1679
+ const l_next = Math.max(1e-5, out.segment_length(e_next));
1680
+
1681
+ out.segment_read_m1(SCRATCH_M_PREV, 0, e_prev);
1682
+ out.segment_read_m0(SCRATCH_M_NEXT, 0, e_next);
1683
+
1684
+ const t_in_x = SCRATCH_M_PREV[0] / l_prev;
1685
+ const t_in_y = SCRATCH_M_PREV[1] / l_prev;
1686
+ const t_in_z = SCRATCH_M_PREV[2] / l_prev;
1687
+ const t_out_x = SCRATCH_M_NEXT[0] / l_next;
1688
+ const t_out_y = SCRATCH_M_NEXT[1] / l_next;
1689
+ const t_out_z = SCRATCH_M_NEXT[2] / l_next;
1690
+
1691
+ let tx = t_in_x + t_out_x;
1692
+ let ty = t_in_y + t_out_y;
1693
+ let tz = t_in_z + t_out_z;
1694
+ const t_len = Math.sqrt(tx * tx + ty * ty + tz * tz);
1695
+ if (t_len > 1e-12) {
1696
+ tx /= t_len;
1697
+ ty /= t_len;
1698
+ tz /= t_len;
1699
+
1700
+ out.node_write_tangent(v_mid, tx, ty, tz);
1701
+
1702
+ // Blend between smooth C1 average tangent and crisp sympodial kink tangents
1703
+ let m1x = lerp(tx, t_in_x, kink_sharp);
1704
+ let m1y = lerp(ty, t_in_y, kink_sharp);
1705
+ let m1z = lerp(tz, t_in_z, kink_sharp);
1706
+ const m1_l = Math.sqrt(m1x * m1x + m1y * m1y + m1z * m1z);
1707
+ if (m1_l > 1e-12) { m1x /= m1_l; m1y /= m1_l; m1z /= m1_l; }
1708
+
1709
+ let m0x = lerp(tx, t_out_x, kink_sharp);
1710
+ let m0y = lerp(ty, t_out_y, kink_sharp);
1711
+ let m0z = lerp(tz, t_out_z, kink_sharp);
1712
+ const m0_l = Math.sqrt(m0x * m0x + m0y * m0y + m0z * m0z);
1713
+ if (m0_l > 1e-12) { m0x /= m0_l; m0y /= m0_l; m0z /= m0_l; }
1714
+
1715
+ out.segment_write_m1(e_prev, m1x * l_prev, m1y * l_prev, m1z * l_prev);
1716
+ out.segment_write_m0(e_next, m0x * l_next, m0y * l_next, m0z * l_next);
1717
+ }
1718
+ }
1719
+ }
1720
+
1721
+ // Spawn lateral child branches at level + 1 using pre-scheduled attachments
1722
+ const next_level = level + 1;
1723
+ if (scheduled_children.length > 0 && branch_edges.length > 0) {
1724
+ const t_max = branch_edge_t1[branch_edge_t1.length - 1];
1725
+ const PHI_CANDIDATES = [0.0, 0.36, -0.36, 0.72, -0.72, 1.08, -1.08, 1.48, -1.48];
1726
+
1727
+ for (let c = 0; c < scheduled_children.length; c++) {
1728
+ const sc = scheduled_children[c];
1729
+ const rule = sc.rule;
1730
+ const next_cfg = species.branches[rule.target];
1731
+ if (sc.t_attach > t_max * 1.02) continue;
1732
+ const t_attach = clamp(sc.t_attach, branch_edge_t0[0], t_max * 0.985);
1733
+ const base_phi_c = sc.phi;
1734
+
1735
+ // Find parent edge containing t_attach
1736
+ let seg_idx = 0;
1737
+ for (let i = 0; i < branch_edges.length; i++) {
1738
+ if (t_attach >= branch_edge_t0[i] && t_attach <= branch_edge_t1[i]) {
1739
+ seg_idx = i;
1740
+ break;
1741
+ }
1742
+ }
1743
+
1744
+ const parent_e = branch_edges[seg_idx];
1745
+ const t0_e = branch_edge_t0[seg_idx];
1746
+ const t1_e = branch_edge_t1[seg_idx];
1747
+ const local_u = clamp((t_attach - t0_e) / Math.max(1e-5, t1_e - t0_e), 0.0, 1.0);
1748
+
1749
+ out.evaluate_segment_position(SCRATCH_POS, 0, parent_e, local_u);
1750
+ out.evaluate_segment_tangent(SCRATCH_TAN, 0, parent_e, local_u);
1751
+
1752
+ const p_v0 = branch_vertices[seg_idx];
1753
+ const p_v1 = branch_vertices[seg_idx + 1];
1754
+ const r_p0 = out.node_read_radius(p_v0);
1755
+ const r_p1 = out.node_read_radius(p_v1);
1756
+ const parent_r_at_attach = lerp(r_p0, r_p1, local_u);
1757
+ if (parent_r_at_attach < 0.004) continue;
1758
+
1759
+ // Transport parent RMF R vector from p_v0 along parent_e to SCRATCH_POS at local_u
1760
+ out.node_read_position(SCRATCH_P0, 0, p_v0);
1761
+ out.node_read_tangent(SCRATCH_T0, 0, p_v0);
1762
+ out.node_read_frame_r(SCRATCH_R0, 0, p_v0);
1763
+ const p_rx = SCRATCH_R0[0];
1764
+ const p_ry = SCRATCH_R0[1];
1765
+ const p_rz = SCRATCH_R0[2];
1766
+
1767
+ spine_frame_double_reflection(
1768
+ SCRATCH_RMF, 0,
1769
+ SCRATCH_P0[0], SCRATCH_P0[1], SCRATCH_P0[2],
1770
+ SCRATCH_T0[0], SCRATCH_T0[1], SCRATCH_T0[2],
1771
+ p_rx, p_ry, p_rz,
1772
+ SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2],
1773
+ SCRATCH_TAN[0], SCRATCH_TAN[1], SCRATCH_TAN[2]
1774
+ );
1775
+ const prx = SCRATCH_RMF[0], pry = SCRATCH_RMF[1], prz = SCRATCH_RMF[2];
1776
+ const psx = SCRATCH_RMF[3], psy = SCRATCH_RMF[4], psz = SCRATCH_RMF[5];
1777
+
1778
+ // Genealogical position along the full parent axis (accounts for continuation forks)
1779
+ const t_genealogical = lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_attach);
1780
+
1781
+ // Insertion polar angle alpha from parent tangent T_p toward D_rad
1782
+ const alpha_scale = rule.insertion_angle_profile.evaluate(t_genealogical);
1783
+ const alpha = clamp(
1784
+ rule.insertion_angle * alpha_scale + (rng() * 2.0 - 1.0) * rule.insertion_angle_variation,
1785
+ 0.1,
1786
+ Math.PI * 0.85
1787
+ );
1788
+ const cos_a = Math.cos(alpha);
1789
+ const sin_a = Math.sin(alpha);
1790
+ const var_scale = 1.0 + (rng() * 2.0 - 1.0) * next_cfg.length_variance;
1791
+
1792
+ // A plagiotropic parent is dorsiventral: its buds lie in the plane of its tangent T_p and the horizontal
1793
+ // H = T_p x Y across it, whose normal N = H x T_p points up. A vertical parent has no such plane.
1794
+ const tpx = SCRATCH_TAN[0], tpy = SCRATCH_TAN[1], tpz = SCRATCH_TAN[2];
1795
+ const horizontality = Math.hypot(tpx, tpz);
1796
+ const plagio = level >= 1 ? lvl_cfg.plagiotropism : 0.0;
1797
+ const w_plagio = horizontality > 1e-3 ? plagio * horizontality : 0.0;
1798
+ const dv_nx = horizontality > 1e-3 ? (-tpx * tpy) / horizontality : 0.0;
1799
+ const dv_ny = horizontality;
1800
+ const dv_nz = horizontality > 1e-3 ? (-tpy * tpz) / horizontality : 0.0;
1801
+
1802
+ // Helper to compute radial normal D_rad and child tangent T_c for a given azimuth phi_cand
1803
+ const compute_child_basis = (phi_cand, out_dr, out_ct) => {
1804
+ const cp = Math.cos(phi_cand);
1805
+ const sp = Math.sin(phi_cand);
1806
+ let dx_r = cp * prx + sp * psx;
1807
+ let dy_r = cp * pry + sp * psy;
1808
+ let dz_r = cp * prz + sp * psz;
1809
+
1810
+ if (w_plagio > 1e-3) {
1811
+ // fan into the dorsiventral plane: take off w of the radial's component along N
1812
+ const along_n = (dx_r * dv_nx + dy_r * dv_ny + dz_r * dv_nz) * w_plagio;
1813
+ dx_r -= along_n * dv_nx;
1814
+ dy_r -= along_n * dv_ny;
1815
+ dz_r -= along_n * dv_nz;
1816
+ const d_len = Math.sqrt(dx_r * dx_r + dy_r * dy_r + dz_r * dz_r);
1817
+ if (d_len > 1e-6) {
1818
+ dx_r /= d_len;
1819
+ dy_r /= d_len;
1820
+ dz_r /= d_len;
1821
+ } else {
1822
+ // a bud straight above (below) a fully dorsiventral shoot: to the side, along H
1823
+ dx_r = -tpz / horizontality;
1824
+ dy_r = 0.0;
1825
+ dz_r = tpx / horizontality;
1826
+ }
1827
+ }
1828
+ out_dr[0] = dx_r;
1829
+ out_dr[1] = dy_r;
1830
+ out_dr[2] = dz_r;
1831
+
1832
+ let tx_c = cos_a * SCRATCH_TAN[0] + sin_a * dx_r;
1833
+ let ty_c = cos_a * SCRATCH_TAN[1] + sin_a * dy_r;
1834
+ let tz_c = cos_a * SCRATCH_TAN[2] + sin_a * dz_r;
1835
+ const ct_l = Math.sqrt(tx_c * tx_c + ty_c * ty_c + tz_c * tz_c) || 1.0;
1836
+ out_ct[0] = tx_c / ct_l;
1837
+ out_ct[1] = ty_c / ct_l;
1838
+ out_ct[2] = tz_c / ct_l;
1839
+ };
1840
+
1841
+ // Ventral knockout: on a dorsiventral parent a bud whose own heading points into the downward cone of
1842
+ // half-angle plagiotropism * pi / 2 does not break
1843
+ const ventral_cos = Math.cos(plagio * Math.PI * 0.5);
1844
+ if (plagio > 0.0) {
1845
+ const bud_y = cos_a * tpy + sin_a * (Math.cos(base_phi_c) * pry + Math.sin(base_phi_c) * psy);
1846
+ if (-bud_y > ventral_cos) {
1847
+ continue;
1848
+ }
1849
+ }
1850
+
1851
+ const cand_dr = [0, 0, 0];
1852
+ const cand_ct = [0, 0, 0];
1853
+ compute_child_basis(base_phi_c, cand_dr, cand_ct);
1854
+
1855
+ // 1. Raw configured child branch length modulated by length_profile at t_genealogical and crown envelope
1856
+ const prof_scale = Math.max(0.12, rule.length_profile.evaluate(t_genealogical));
1857
+ const est_mid_y = SCRATCH_POS[1] + Math.max(0.0, cand_ct[1]) * next_cfg.length * prof_scale * 0.35;
1858
+ const h_norm = clamp((est_mid_y - crown.base_height) / Math.max(0.1, crown.height), 0.0, 1.0);
1859
+ // a dead branch grew before today's crown took its shape
1860
+ const dead = sc.age > 0.0;
1861
+ const env_scale = !rule.crown_envelope || dead
1862
+ ? 1.0
1863
+ : (level === 0
1864
+ ? crown_envelope_evaluate(crown.envelope_type, h_norm)
1865
+ : lerp(1.0, crown_envelope_evaluate(crown.envelope_type, h_norm), 0.5));
1866
+ const raw_child_len = Math.max(0.12, next_cfg.length * prof_scale * env_scale * var_scale);
1867
+
1868
+ // 2. Continuous Gravity-Orientation & Allometric Slenderness Coupling (Cantilever vs. Pendulous Tension)
1869
+ const rho_design = Math.max(0.012, next_cfg.radius_ratio);
1870
+ const s_design = 1.0 / (2.0 * rho_design);
1871
+ const g_trop = next_cfg.gravitropism ?? 0.0;
1872
+ const u_curl = next_cfg.tip_upcurl ?? 0.0;
1873
+ const t_eff_y = clamp(cand_ct[1] - 0.85 * g_trop + 0.35 * u_curl, -1.0, 1.0);
1874
+
1875
+ // Cantilever load (horizontal bending / upward compression) vs. pendulous tensile compliance
1876
+ const k_cantilever = Math.sqrt(Math.max(0.0, 1.0 - t_eff_y * t_eff_y)) + 0.65 * Math.max(0.0, t_eff_y);
1877
+ const tau_tension = clamp(g_trop, 0.0, 1.0) * Math.max(0.0, -t_eff_y);
1878
+
1879
+ // 3. Parent-relative distal conical envelope (prevents distal shoots or split forks from outgrowing their parent)
1880
+ const actual_parent_len = Math.max(0.15, out.branch_length[branch_id]);
1881
+ const parent_full_len = Math.max(actual_parent_len, job.full_branch_length ?? actual_parent_len);
1882
+ const distal_tip_ratio = lerp(0.32, 0.55, tau_tension);
1883
+ const cone_max_len = level === 0
1884
+ ? parent_full_len * lerp(0.85, 0.32, Math.pow(t_genealogical, 1.6))
1885
+ : Math.min(
1886
+ actual_parent_len * lerp(0.90, 1.05, tau_tension),
1887
+ parent_full_len * lerp(0.85, distal_tip_ratio, Math.pow(t_genealogical, 1.25))
1888
+ );
1889
+ const desired_len = Math.max(0.12, Math.min(raw_child_len, cone_max_len));
1890
+
1891
+ const nom_len = Math.max(0.15, next_cfg.length);
1892
+ const r_design = (nom_len * rho_design * Math.pow(desired_len / nom_len, 0.75)) / (1.0 + 0.65 * tau_tension);
1893
+ // children on the flare crests sit side by side around the stem: N of them fit at most R sin(pi / N) thick
1894
+ const side_by_side_r = rule.phyllotaxis_mode === PhyllotaxisMode.FlareLobes
1895
+ ? parent_r_at_attach * Math.sin(Math.PI / Math.max(2, lvl_cfg.flare_lobes | 0))
1896
+ : Infinity;
1897
+ const child_base_r = Math.min(
1898
+ parent_r_at_attach * 0.78,
1899
+ side_by_side_r,
1900
+ Math.max(0.0035, r_design)
1901
+ );
1902
+
1903
+ const gamma = lerp(0.82, 0.65, tau_tension);
1904
+ const allometric_len = child_base_r < r_design
1905
+ ? desired_len * Math.pow(child_base_r / Math.max(1e-5, r_design), gamma)
1906
+ : desired_len;
1907
+ const s_max = s_design * (1.35 + 0.65 * tau_tension) / (0.65 + 0.35 * k_cantilever);
1908
+ const full_len = clamp(Math.min(allometric_len, 2.0 * child_base_r * s_max), 0.12, desired_len);
1909
+ // a dead branch is what is left of it: its base part, if that still clears the bark; otherwise it has
1910
+ // been grown over and the stem carries its scar
1911
+ const child_len = full_len * sc.retained;
1912
+ if (dead && child_len <= parent_r_at_attach) {
1913
+ out.add_scar(branch_id, t_attach, cand_ct[0], cand_ct[1], cand_ct[2], child_base_r * (1.0 - sc.age));
1914
+ continue;
1915
+ }
1916
+ const child_segs = Math.max(1, Math.ceil(next_cfg.segments * sc.retained));
1917
+ const child_step = child_len / child_segs;
1918
+
1919
+ // 4. Pre-Spawn BVH Bud Clearance Probing (selects open canopy azimuth or inhibits blocked bud)
1920
+ let best_drx = cand_dr[0], best_dry = cand_dr[1], best_drz = cand_dr[2];
1921
+ let best_ctx = cand_ct[0], best_cty = cand_ct[1], best_ctz = cand_ct[2];
1922
+ let best_clear = -Infinity;
1923
+ const r_horiz_attach = Math.hypot(SCRATCH_POS[0], SCRATCH_POS[2]);
1924
+
1925
+ for (let k = 0; k < PHI_CANDIDATES.length; k++) {
1926
+ if (k > 0) {
1927
+ compute_child_basis(base_phi_c + PHI_CANDIDATES[k], cand_dr, cand_ct);
1928
+ }
1929
+ // nor may fanning or a turn for clearance carry it into the cone: a bud with no heading left
1930
+ // outside it does not break
1931
+ if (plagio > 0.0 && -cand_ct[1] > ventral_cos) continue;
1932
+ // Prevent lateral shoots on boughs from pointing backward into the trunk core
1933
+ if (level >= 1 && r_horiz_attach > 0.30) {
1934
+ const radial_dot = (SCRATCH_POS[0] * cand_ct[0] + SCRATCH_POS[2] * cand_ct[2]) / r_horiz_attach;
1935
+ if (radial_dot < -0.15) continue;
1936
+ }
1937
+
1938
+ const p1_x = SCRATCH_POS[0] + cand_ct[0] * child_step;
1939
+ const p1_y = SCRATCH_POS[1] + cand_ct[1] * child_step;
1940
+ const p1_z = SCRATCH_POS[2] + cand_ct[2] * child_step;
1941
+
1942
+ const clear0 = evaluate_candidate_clearance(
1943
+ -1, branch_id, seg_idx, false, 0,
1944
+ SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2], child_base_r,
1945
+ p1_x, p1_y, p1_z, child_base_r * 0.88,
1946
+ 0.002
1947
+ );
1948
+
1949
+ const p2_x = p1_x + cand_ct[0] * child_step;
1950
+ const p2_y = p1_y + cand_ct[1] * child_step;
1951
+ const p2_z = p1_z + cand_ct[2] * child_step;
1952
+ const clear1 = child_segs >= 2
1953
+ ? evaluate_candidate_clearance(
1954
+ -1, branch_id, seg_idx, false, 1,
1955
+ p1_x, p1_y, p1_z, child_base_r * 0.88,
1956
+ p2_x, p2_y, p2_z, child_base_r * 0.75,
1957
+ 0.002
1958
+ )
1959
+ : clear0;
1960
+
1961
+ const min_c = Math.min(clear0, clear1);
1962
+ if (min_c > best_clear) {
1963
+ best_clear = min_c;
1964
+ best_drx = cand_dr[0];
1965
+ best_dry = cand_dr[1];
1966
+ best_drz = cand_dr[2];
1967
+ best_ctx = cand_ct[0];
1968
+ best_cty = cand_ct[1];
1969
+ best_ctz = cand_ct[2];
1970
+ }
1971
+ if (min_c >= 0.004) {
1972
+ break;
1973
+ }
1974
+ }
1975
+
1976
+ // Botanical bud inhibition: if all candidate directions are blocked by existing branches, suppress bud
1977
+ if (best_clear < -0.004) {
1978
+ continue;
1979
+ }
1980
+
1981
+ const drx = best_drx, dry = best_dry, drz = best_drz;
1982
+ const ctx = best_ctx, cty = best_cty, ctz = best_ctz;
1983
+
1984
+ // Initial RMF radial vector for child branch:
1985
+ // R_c = cos(alpha) * D_rad - sin(alpha) * T_p is exact unit length and orthogonal
1986
+ // to T_c = cos(alpha) * T_p + sin(alpha) * D_rad for all alpha in [0, pi],
1987
+ // avoiding any singularity at alpha = pi/2.
1988
+ let crx = cos_a * drx - sin_a * SCRATCH_TAN[0];
1989
+ let cry = cos_a * dry - sin_a * SCRATCH_TAN[1];
1990
+ let crz = cos_a * drz - sin_a * SCRATCH_TAN[2];
1991
+ const cr_dot = crx * ctx + cry * cty + crz * ctz;
1992
+ crx -= cr_dot * ctx;
1993
+ cry -= cr_dot * cty;
1994
+ crz -= cr_dot * ctz;
1995
+ const cr_len = Math.sqrt(crx * crx + cry * cry + crz * crz);
1996
+ if (cr_len > 1e-12) {
1997
+ crx /= cr_len;
1998
+ cry /= cr_len;
1999
+ crz /= cr_len;
2000
+ }
2001
+
2002
+ queue.push({
2003
+ level: next_level,
2004
+ generator: rule.target,
2005
+ in_crown: rule.crown_envelope && !dead,
2006
+ dead,
2007
+ parent_branch_id: branch_id,
2008
+ parent_edge_id: parent_e,
2009
+ parent_attach_seg_idx: seg_idx,
2010
+ parent_attach_t: t_attach,
2011
+ attach_parent_vertex: p_v0,
2012
+ start_vertex: -1,
2013
+ px: SCRATCH_POS[0],
2014
+ py: SCRATCH_POS[1],
2015
+ pz: SCRATCH_POS[2],
2016
+ tx: ctx,
2017
+ ty: cty,
2018
+ tz: ctz,
2019
+ rx: crx,
2020
+ ry: cry,
2021
+ rz: crz,
2022
+ emerge_drx: drx,
2023
+ emerge_dry: dry,
2024
+ emerge_drz: drz,
2025
+ length: child_len,
2026
+ full_branch_length: child_len,
2027
+ base_radius: child_base_r * (1.0 - sc.age),
2028
+ start_seg: 0,
2029
+ total_segs: child_segs,
2030
+ shape_segs: Math.max(1, next_cfg.segments),
2031
+ initial_arc_s: 0.0,
2032
+ has_split: false,
2033
+ is_split_fork: false,
2034
+ cum_t0: 0.0,
2035
+ cum_t1: sc.retained
2036
+ });
2037
+ }
2038
+ }
2039
+ }
2040
+
2041
+ // Bottom-up Pipe Model radius calculation & blending
2042
+ tree_skeleton_compute_pipe_radii(out, species);
2043
+
2044
+ // The peripheral twig systems, grown year by year on the finished framework, its girth final
2045
+ tree_skeleton_grow_shoots(out, species);
2046
+
2047
+ return out;
2048
+ }