@woosh/meep-engine 3.44.0 → 3.46.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 (447) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/assert.d.ts.map +1 -1
  4. package/src/core/assert.js +5 -1
  5. package/src/core/binary/hash/XXH128Hash.d.ts +1 -37
  6. package/src/core/binary/hash/XXH128Hash.d.ts.map +1 -1
  7. package/src/core/binary/hash/XXH128Hash.js +4 -55
  8. package/src/core/binary/hash/XXH3_SECRET.d.ts +1 -12
  9. package/src/core/binary/hash/XXH3_SECRET.d.ts.map +1 -1
  10. package/src/core/binary/hash/XXH3_SECRET.js +4 -25
  11. package/src/core/binary/hash/crc32.d.ts +1 -15
  12. package/src/core/binary/hash/crc32.d.ts.map +1 -1
  13. package/src/core/binary/hash/crc32.js +4 -96
  14. package/src/core/binary/hash/xxh3_128.d.ts +1 -9
  15. package/src/core/binary/hash/xxh3_128.d.ts.map +1 -1
  16. package/src/core/binary/hash/xxh3_128.js +4 -341
  17. package/src/core/binary/hash/xxh3_64.d.ts +1 -8
  18. package/src/core/binary/hash/xxh3_64.d.ts.map +1 -1
  19. package/src/core/binary/hash/xxh3_64.js +4 -241
  20. package/src/core/binary/hash/xxh3_common.d.ts +1 -223
  21. package/src/core/binary/hash/xxh3_common.d.ts.map +1 -1
  22. package/src/core/binary/hash/xxh3_common.js +90 -476
  23. package/src/core/collection/array/array_sort_insertion_ascending.d.ts +19 -0
  24. package/src/core/collection/array/array_sort_insertion_ascending.d.ts.map +1 -0
  25. package/src/core/collection/array/array_sort_insertion_ascending.js +31 -0
  26. package/src/core/collection/array/combine_hash.d.ts +6 -0
  27. package/src/core/collection/array/combine_hash.d.ts.map +1 -1
  28. package/src/core/collection/array/combine_hash.js +6 -0
  29. package/src/core/function/FunctionCompiler.js +2 -2
  30. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.d.ts +27 -0
  31. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.d.ts.map +1 -0
  32. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.js +39 -0
  33. package/src/core/geom/3d/aabb/aabb3_from_v3_array.d.ts.map +1 -1
  34. package/src/core/geom/3d/aabb/aabb3_from_v3_array.js +4 -1
  35. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts +2 -2
  36. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -1
  37. package/src/core/geom/3d/aabb/aabb3_transform_oriented.js +1 -1
  38. package/src/core/geom/3d/mat4/m4_multiply.d.ts +3 -0
  39. package/src/core/geom/3d/mat4/m4_multiply.d.ts.map +1 -1
  40. package/src/core/geom/3d/mat4/m4_multiply.js +3 -0
  41. package/src/core/geom/3d/mat4/m4_multiply_f64.d.ts +30 -0
  42. package/src/core/geom/3d/mat4/m4_multiply_f64.d.ts.map +1 -0
  43. package/src/core/geom/3d/mat4/m4_multiply_f64.js +106 -0
  44. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.d.ts +21 -0
  45. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.d.ts.map +1 -0
  46. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.js +36 -0
  47. package/src/core/geom/3d/pose/pose3_apply_point_array.d.ts +25 -0
  48. package/src/core/geom/3d/pose/pose3_apply_point_array.d.ts.map +1 -0
  49. package/src/core/geom/3d/pose/pose3_apply_point_array.js +40 -0
  50. package/src/core/geom/3d/pose/pose3_compose_array.d.ts +28 -0
  51. package/src/core/geom/3d/pose/pose3_compose_array.d.ts.map +1 -0
  52. package/src/core/geom/3d/pose/pose3_compose_array.js +41 -0
  53. package/src/core/geom/3d/pose/pose3_to_local_array.d.ts +29 -0
  54. package/src/core/geom/3d/pose/pose3_to_local_array.d.ts.map +1 -0
  55. package/src/core/geom/3d/pose/pose3_to_local_array.js +42 -0
  56. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.d.ts +28 -0
  57. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.d.ts.map +1 -0
  58. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.js +66 -0
  59. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.d.ts +22 -0
  60. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.d.ts.map +1 -0
  61. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.js +30 -0
  62. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.d.ts +23 -0
  63. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.d.ts.map +1 -0
  64. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.js +31 -0
  65. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.d.ts +26 -0
  66. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.d.ts.map +1 -0
  67. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.js +46 -0
  68. package/src/core/geom/ConicRay.js +2 -2
  69. package/src/core/geom/Vector4.d.ts.map +1 -1
  70. package/src/core/geom/Vector4.js +8 -5
  71. package/src/core/geom/vec3/v3_distance_array.d.ts +11 -0
  72. package/src/core/geom/vec3/v3_distance_array.d.ts.map +1 -0
  73. package/src/core/geom/vec3/v3_distance_array.js +19 -0
  74. package/src/core/geom/vec3/v3_normalize_array_checked.d.ts +23 -0
  75. package/src/core/geom/vec3/v3_normalize_array_checked.d.ts.map +1 -0
  76. package/src/core/geom/vec3/v3_normalize_array_checked.js +49 -0
  77. package/src/core/geom/vec3/v3_reject_array.d.ts +15 -0
  78. package/src/core/geom/vec3/v3_reject_array.d.ts.map +1 -0
  79. package/src/core/geom/vec3/v3_reject_array.js +32 -0
  80. package/src/core/graph/csr/csr_build_incidence.d.ts +26 -0
  81. package/src/core/graph/csr/csr_build_incidence.d.ts.map +1 -0
  82. package/src/core/graph/csr/csr_build_incidence.js +71 -0
  83. package/src/core/graph/csr/csr_graph_color_greedy.d.ts +25 -0
  84. package/src/core/graph/csr/csr_graph_color_greedy.d.ts.map +1 -0
  85. package/src/core/graph/csr/csr_graph_color_greedy.js +83 -0
  86. package/src/core/graph/csr/csr_graph_validate_coloring.d.ts +15 -0
  87. package/src/core/graph/csr/csr_graph_validate_coloring.d.ts.map +1 -0
  88. package/src/core/graph/csr/csr_graph_validate_coloring.js +33 -0
  89. package/src/core/math/hash/XXH128Hash.d.ts +38 -0
  90. package/src/core/math/hash/XXH128Hash.d.ts.map +1 -0
  91. package/src/core/math/hash/XXH128Hash.js +55 -0
  92. package/src/core/math/hash/XXH3_SECRET.d.ts +13 -0
  93. package/src/core/math/hash/XXH3_SECRET.d.ts.map +1 -0
  94. package/src/core/math/hash/XXH3_SECRET.js +25 -0
  95. package/src/core/math/hash/crc32.d.ts +16 -0
  96. package/src/core/math/hash/crc32.d.ts.map +1 -0
  97. package/src/core/math/hash/crc32.js +96 -0
  98. package/src/core/math/hash/xxh3_128.d.ts +10 -0
  99. package/src/core/math/hash/xxh3_128.d.ts.map +1 -0
  100. package/src/core/math/hash/xxh3_128.js +341 -0
  101. package/src/core/math/hash/xxh3_64.d.ts +9 -0
  102. package/src/core/math/hash/xxh3_64.d.ts.map +1 -0
  103. package/src/core/math/hash/xxh3_64.js +241 -0
  104. package/src/core/math/hash/xxh3_common.d.ts +224 -0
  105. package/src/core/math/hash/xxh3_common.d.ts.map +1 -0
  106. package/src/core/math/hash/xxh3_common.js +476 -0
  107. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.d.ts +26 -0
  108. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.d.ts.map +1 -0
  109. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.js +71 -0
  110. package/src/core/math/lookup/ParameterLookupTable.js +2 -2
  111. package/src/core/math/max2_exact.d.ts +15 -0
  112. package/src/core/math/max2_exact.d.ts.map +1 -0
  113. package/src/core/math/max2_exact.js +30 -0
  114. package/src/core/math/min2_exact.d.ts +20 -0
  115. package/src/core/math/min2_exact.d.ts.map +1 -0
  116. package/src/core/math/min2_exact.js +35 -0
  117. package/src/core/model/node-graph/node/NodeInstancePortReference.js +2 -2
  118. package/src/core/model/reactive/model/ReactiveBinaryExpression.js +2 -2
  119. package/src/core/model/reactive/model/terminal/ReactiveReference.js +2 -2
  120. package/src/engine/asset/AssetDescription.js +2 -2
  121. package/src/engine/ecs/animation/Animation.js +2 -2
  122. package/src/engine/ecs/animation/AnimationClip.d.ts.map +1 -1
  123. package/src/engine/ecs/animation/AnimationClip.js +7 -5
  124. package/src/engine/ecs/gui/GUIElement.d.ts.map +1 -1
  125. package/src/engine/ecs/gui/GUIElement.js +7 -5
  126. package/src/engine/ecs/gui/hud/HeadsUpDisplay.js +2 -2
  127. package/src/engine/ecs/gui/position/ViewportPosition.d.ts.map +1 -1
  128. package/src/engine/ecs/gui/position/ViewportPosition.js +11 -7
  129. package/src/engine/ecs/ik/IKGoalSystem.d.ts.map +1 -1
  130. package/src/engine/ecs/ik/IKGoalSystem.js +3 -19
  131. package/src/engine/ecs/parent/parent_entity_model_of.d.ts +17 -0
  132. package/src/engine/ecs/parent/parent_entity_model_of.d.ts.map +1 -0
  133. package/src/engine/ecs/parent/parent_entity_model_of.js +26 -0
  134. package/src/engine/ecs/transform/Transform64.d.ts +1 -1
  135. package/src/engine/ecs/transform/Transform64.d.ts.map +1 -1
  136. package/src/engine/ecs/transform/Transform64.js +5 -4
  137. package/src/engine/ecs/transform/t64_read_pose_array.d.ts +15 -0
  138. package/src/engine/ecs/transform/t64_read_pose_array.d.ts.map +1 -0
  139. package/src/engine/ecs/transform/t64_read_pose_array.js +24 -0
  140. package/src/engine/graphics/ecs/animation/animator/AnimationClip.d.ts.map +1 -1
  141. package/src/engine/graphics/ecs/animation/animator/AnimationClip.js +8 -5
  142. package/src/engine/graphics/ecs/animation/animator/AnimationClipDefinition.js +2 -2
  143. package/src/engine/graphics/ecs/animation/animator/AnimationNotification.js +2 -2
  144. package/src/engine/graphics/ecs/animation/animator/AnimationNotificationDefinition.js +2 -2
  145. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationGraphDefinition.js +2 -2
  146. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationStateDefinition.js +2 -2
  147. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationTransitionDefinition.d.ts.map +1 -1
  148. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationTransitionDefinition.js +8 -5
  149. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts.map +1 -1
  150. package/src/engine/graphics/ecs/decal/v2/Decal.js +9 -8
  151. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts +2 -0
  152. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts.map +1 -1
  153. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.js +10 -21
  154. package/src/engine/graphics/render/frame_graph/resource/TextureResourceDescriptor.d.ts.map +1 -1
  155. package/src/engine/graphics/render/frame_graph/resource/TextureResourceDescriptor.js +9 -6
  156. package/src/engine/graphics3/AnimationGraphSystem.d.ts +5 -0
  157. package/src/engine/graphics3/AnimationGraphSystem.d.ts.map +1 -1
  158. package/src/engine/graphics3/AnimationGraphSystem.js +48 -2
  159. package/src/engine/graphics3/AnimationSystem.d.ts +5 -0
  160. package/src/engine/graphics3/AnimationSystem.d.ts.map +1 -1
  161. package/src/engine/graphics3/AnimationSystem.js +47 -2
  162. package/src/engine/graphics3/ParticipatingMedia.js +204 -204
  163. package/src/engine/graphics3/animation/AnimationClips.d.ts +5 -0
  164. package/src/engine/graphics3/animation/AnimationClips.d.ts.map +1 -1
  165. package/src/engine/graphics3/animation/AnimationClips.js +5 -0
  166. package/src/engine/graphics3/register_clips.d.ts +29 -0
  167. package/src/engine/graphics3/register_clips.d.ts.map +1 -1
  168. package/src/engine/graphics3/register_clips.js +52 -0
  169. package/src/engine/grid/obstacle/GridObstacle.js +2 -2
  170. package/src/engine/intelligence/behavior/selector/WeightedElement.js +2 -2
  171. package/src/engine/interpolation/Interpolated.js +2 -2
  172. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts +9 -0
  173. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts.map +1 -1
  174. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +3 -1
  175. package/src/engine/physics/cloth/PLAN.md +10 -8
  176. package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts +22 -0
  177. package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts.map +1 -1
  178. package/src/engine/physics/cloth/build/cloth_bind_in_frame.js +51 -19
  179. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -1
  180. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +16 -2
  181. package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.d.ts.map +1 -1
  182. package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.js +19 -24
  183. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +7 -0
  184. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  185. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +85 -67
  186. package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts.map +1 -1
  187. package/src/engine/physics/cloth/collider/cloth_collider_bounds.js +19 -101
  188. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts +2 -5
  189. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts.map +1 -1
  190. package/src/engine/physics/cloth/collider/cloth_collider_pack.js +7 -15
  191. package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts.map +1 -1
  192. package/src/engine/physics/cloth/collider/cloth_collider_sdf.js +16 -61
  193. package/src/engine/physics/cloth/ecs/Cloth.d.ts.map +1 -1
  194. package/src/engine/physics/cloth/ecs/Cloth.js +7 -5
  195. package/src/engine/physics/cloth/ecs/ClothCollider.d.ts.map +1 -1
  196. package/src/engine/physics/cloth/ecs/ClothCollider.js +7 -5
  197. package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts +0 -1
  198. package/src/engine/physics/cloth/ecs/ClothColliderFlags.js +6 -19
  199. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts +1 -2
  200. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts.map +1 -1
  201. package/src/engine/physics/cloth/ecs/ClothColliderSystem.js +3 -12
  202. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  203. package/src/engine/physics/cloth/ecs/ClothDynamics.js +20 -11
  204. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  205. package/src/engine/physics/cloth/ecs/ClothWorld.js +40 -130
  206. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  207. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +17 -58
  208. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  209. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +3 -3
  210. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  211. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +44 -104
  212. package/src/engine/physics/cloth/ecs/cloth_geodesic.d.ts.map +1 -1
  213. package/src/engine/physics/cloth/ecs/cloth_geodesic.js +6 -13
  214. package/src/engine/physics/cloth/ecs/cloth_holds.d.ts.map +1 -1
  215. package/src/engine/physics/cloth/ecs/cloth_holds.js +15 -40
  216. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts.map +1 -1
  217. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.js +45 -79
  218. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  219. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +7 -27
  220. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  221. package/src/engine/physics/cloth/ecs/cloth_write_back.js +30 -62
  222. package/src/engine/physics/cloth/playground/cloth_playground_build.d.ts.map +1 -1
  223. package/src/engine/physics/cloth/playground/cloth_playground_build.js +2 -3
  224. package/src/engine/physics/cloth/playground/collide_main.js +2 -28
  225. package/src/engine/physics/cloth/playground/garment_main.js +4 -19
  226. package/src/engine/physics/cloth/playground/main.js +6 -48
  227. package/src/engine/physics/cloth/playground/rig_main.js +947 -955
  228. package/src/engine/physics/cloth/playground/wind_main.js +8 -7
  229. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  230. package/src/engine/physics/cloth/solver/ClothState.js +16 -76
  231. package/src/engine/physics/cloth/solver/cloth_color_particles.d.ts +10 -8
  232. package/src/engine/physics/cloth/solver/cloth_color_particles.d.ts.map +1 -1
  233. package/src/engine/physics/cloth/solver/cloth_color_particles.js +30 -102
  234. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  235. package/src/engine/physics/cloth/solver/cloth_contact_find.js +3 -2
  236. package/src/engine/physics/cloth/solver/cloth_hold_line.d.ts.map +1 -1
  237. package/src/engine/physics/cloth/solver/cloth_hold_line.js +2 -14
  238. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.d.ts +21 -0
  239. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.d.ts.map +1 -0
  240. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.js +45 -0
  241. package/src/engine/physics/cloth/solver/cloth_particle_coupling.d.ts +32 -0
  242. package/src/engine/physics/cloth/solver/cloth_particle_coupling.d.ts.map +1 -0
  243. package/src/engine/physics/cloth/solver/cloth_particle_coupling.js +133 -0
  244. package/src/engine/physics/cloth/solver/cloth_particle_of_source.d.ts +16 -0
  245. package/src/engine/physics/cloth/solver/cloth_particle_of_source.d.ts.map +1 -0
  246. package/src/engine/physics/cloth/solver/cloth_particle_of_source.js +19 -0
  247. package/src/engine/physics/cloth/solver/cloth_self_find.d.ts.map +1 -1
  248. package/src/engine/physics/cloth/solver/cloth_self_find.js +6 -55
  249. package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts.map +1 -1
  250. package/src/engine/physics/cloth/solver/cloth_solve_vertex.js +30 -65
  251. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  252. package/src/engine/physics/cloth/solver/cloth_step.js +6 -28
  253. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -1
  254. package/src/engine/physics/cloth/solver/cloth_step_instance.js +2 -3
  255. package/src/engine/physics/cloth/solver/cloth_stretch_residual.d.ts +21 -0
  256. package/src/engine/physics/cloth/solver/cloth_stretch_residual.d.ts.map +1 -0
  257. package/src/engine/physics/cloth/solver/cloth_stretch_residual.js +54 -0
  258. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.d.ts +15 -0
  259. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.d.ts.map +1 -1
  260. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.js +28 -0
  261. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.d.ts +30 -0
  262. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.d.ts.map +1 -0
  263. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.js +56 -0
  264. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts.map +1 -1
  265. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.js +3 -17
  266. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +2 -1
  267. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -1
  268. package/src/engine/physics/cloth/wind/ClothFluidWind.js +7 -25
  269. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +3 -1
  270. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
  271. package/src/engine/physics/cloth/wind/cloth_sample_air.js +40 -62
  272. package/src/engine/physics/ecs/ContactProbeSystem.d.ts.map +1 -1
  273. package/src/engine/physics/ecs/ContactProbeSystem.js +2 -18
  274. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  275. package/src/engine/physics/ecs/PhysicsSystem.js +9 -50
  276. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  277. package/src/engine/physics/fluid/FluidField.js +7 -5
  278. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts.map +1 -1
  279. package/src/engine/physics/fluid/ecs/FluidComponent.js +19 -11
  280. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.d.ts +24 -0
  281. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.d.ts.map +1 -0
  282. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.js +42 -0
  283. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.d.ts +25 -0
  284. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.d.ts.map +1 -0
  285. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.js +40 -0
  286. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts +2 -2
  287. package/src/engine/physics/fluid/effector/AbstractFluidEffector.js +2 -2
  288. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts.map +1 -1
  289. package/src/engine/physics/fluid/effector/GlobalFluidEffector.js +11 -7
  290. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.d.ts.map +1 -1
  291. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +11 -7
  292. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts.map +1 -1
  293. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +16 -9
  294. package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.d.ts.map +1 -1
  295. package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.js +40 -90
  296. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  297. package/src/engine/physics/narrowphase/capsule_contacts.js +3 -5
  298. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -1
  299. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +2 -14
  300. package/src/engine/physics/narrowphase/hull_sphere_contact.d.ts.map +1 -1
  301. package/src/engine/physics/narrowphase/hull_sphere_contact.js +2 -5
  302. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  303. package/src/engine/physics/narrowphase/narrowphase_step.js +13 -32
  304. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  305. package/src/engine/physics/narrowphase/sphere_box_contact.js +2 -5
  306. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
  307. package/src/engine/sound/ecs/audio/AudioEmitter.js +12 -7
  308. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
  309. package/src/engine/sound/ecs/trigger/AudioEventTrigger.js +192 -191
  310. package/src/engine/sound/simulation/core/AcousticMedium.js +2 -2
  311. package/src/engine/sound/simulation/definition/AcousticDirectivity.js +2 -2
  312. package/src/engine/sound/simulation/definition/AcousticMaterial.d.ts.map +1 -1
  313. package/src/engine/sound/simulation/definition/AcousticMaterial.js +11 -7
  314. package/src/engine/sound/sopra/definition/BusDefinition.d.ts.map +1 -1
  315. package/src/engine/sound/sopra/definition/BusDefinition.js +5 -4
  316. package/src/engine/sound/sopra/definition/DuckingRule.d.ts.map +1 -1
  317. package/src/engine/sound/sopra/definition/DuckingRule.js +7 -5
  318. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
  319. package/src/engine/sound/sopra/definition/EventDescription.js +23 -13
  320. package/src/engine/sound/sopra/definition/MixerSnapshot.js +2 -2
  321. package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts.map +1 -1
  322. package/src/engine/sound/sopra/definition/ParameterDefinition.js +7 -5
  323. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +2 -2
  324. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
  325. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +4 -3
  326. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +2 -2
  327. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +3 -3
  328. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
  329. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +16 -9
  330. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.js +2 -2
  331. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -1
  332. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  333. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +2 -2
  334. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
  335. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +7 -5
  336. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
  337. package/src/engine/sound/sopra/definition/effect/EqEffect.js +8 -5
  338. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +2 -2
  339. package/src/engine/sound/sopra/legacy/SoundEmitter.d.ts.map +1 -1
  340. package/src/engine/sound/sopra/legacy/SoundEmitter.js +357 -352
  341. package/src/engine/sound/sopra/legacy/SoundTrack.d.ts.map +1 -1
  342. package/src/engine/sound/sopra/legacy/SoundTrack.js +281 -278
  343. package/src/format/image/png/PNGReader.js +644 -644
  344. package/src/format/image/png/crc32.d.ts +1 -1
  345. package/src/format/image/png/crc32.js +7 -8
  346. package/src/format/texture/basis/transcode/etc1s_to_astc.d.ts.map +1 -1
  347. package/src/format/texture/basis/transcode/etc1s_to_astc.js +2 -1
  348. package/src/shade/descriptor/binding/BindGroupLayoutDescriptor.js +2 -2
  349. package/src/shade/descriptor/binding/BindGroupLayoutEntry.d.ts.map +1 -1
  350. package/src/shade/descriptor/binding/BindGroupLayoutEntry.js +12 -8
  351. package/src/shade/descriptor/binding/StorageTextureBindingLayout.js +2 -2
  352. package/src/shade/descriptor/binding/TextureBindingLayout.js +2 -2
  353. package/src/shade/descriptor/blend/BlendComponent.js +2 -2
  354. package/src/shade/descriptor/pipeline/PipelineDescriptorBase.js +2 -2
  355. package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.js +2 -2
  356. package/src/shade/descriptor/pipeline/ProgrammableStage.js +2 -2
  357. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.js +2 -2
  358. package/src/shade/descriptor/pipeline/render/ColorTargetState.js +2 -2
  359. package/src/shade/descriptor/pipeline/render/FragmentState.js +2 -2
  360. package/src/shade/descriptor/pipeline/render/PrimitiveState.js +2 -2
  361. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.d.ts.map +1 -1
  362. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.js +8 -5
  363. package/src/shade/descriptor/pipeline/render/VertexAttribute.js +2 -2
  364. package/src/shade/descriptor/texture/SamplerDescriptor.d.ts.map +1 -1
  365. package/src/shade/descriptor/texture/SamplerDescriptor.js +20 -12
  366. package/src/shade/descriptor/texture/TextureDescriptor.d.ts.map +1 -1
  367. package/src/shade/descriptor/texture/TextureDescriptor.js +11 -7
  368. package/src/shade/device/graph/BufferResourceDescriptor.d.ts.map +1 -1
  369. package/src/shade/device/graph/BufferResourceDescriptor.js +196 -193
  370. package/src/shade/device/graph/TextureResourceDescriptor.d.ts.map +1 -1
  371. package/src/shade/device/graph/TextureResourceDescriptor.js +178 -173
  372. package/src/shade/device/timing/profile/sgpt_read_header.js +1 -1
  373. package/src/shade/device/timing/profile/sgpt_read_records.js +1 -1
  374. package/src/shade/device/timing/profile/sgpt_write_header.js +1 -1
  375. package/src/shade/device/timing/profile/sgpt_write_record.js +1 -1
  376. package/src/shade/playground/image_decode_bench/main.js +2 -1
  377. package/src/shade/playground/skinned_blas_refit/diff_traced_against_rasterized.d.ts.map +1 -1
  378. package/src/shade/playground/skinned_blas_refit/diff_traced_against_rasterized.js +2 -1
  379. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
  380. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +2 -1
  381. package/src/shade/playground/ssr_variance/statistics.d.ts.map +1 -1
  382. package/src/shade/playground/ssr_variance/statistics.js +2 -1
  383. package/src/shade/playground/texture_encode/main.d.ts.map +1 -1
  384. package/src/shade/playground/texture_encode/main.js +2 -1
  385. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -1
  386. package/src/shade/playground/volumetrics_froxel/measure.js +2 -1
  387. package/src/shade/renderer/binding/BufferBinding.js +2 -2
  388. package/src/shade/renderer/buffer/table/gpu_database_check_links.d.ts.map +1 -1
  389. package/src/shade/renderer/buffer/table/gpu_database_check_links.js +2 -1
  390. package/src/shade/renderer/camera/Camera.js +2 -2
  391. package/src/shade/renderer/geometry/Attribute.js +2 -2
  392. package/src/shade/renderer/geometry/Geometry.js +2 -2
  393. package/src/shade/renderer/geometry/geometry_deduplicate_vertices.js +2 -2
  394. package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +2 -2
  395. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts.map +1 -1
  396. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +3 -2
  397. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts.map +1 -1
  398. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.js +2 -1
  399. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts.map +1 -1
  400. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.js +2 -1
  401. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts.map +1 -1
  402. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.js +2 -1
  403. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts.map +1 -1
  404. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.js +3 -2
  405. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -1
  406. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.js +2 -1
  407. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -1
  408. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.js +2 -1
  409. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_asset_id.js +13 -13
  410. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_directory_block_checksum.js +41 -41
  411. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_frame_checksum.js +41 -41
  412. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_header_checksum.js +25 -25
  413. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  414. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +3 -2
  415. package/src/shade/renderer/material/StandardShadeMaterial.d.ts.map +1 -1
  416. package/src/shade/renderer/material/StandardShadeMaterial.js +13 -8
  417. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.d.ts.map +1 -1
  418. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.js +3 -2
  419. package/src/shade/renderer/scene/optimization/geometry/internal/triangle_fingerprint_stream.d.ts.map +1 -1
  420. package/src/shade/renderer/scene/optimization/geometry/internal/triangle_fingerprint_stream.js +14 -5
  421. package/src/shade/renderer/scene/optimization/sampler2d_from_image_bitmap.js +1 -1
  422. package/src/shade/renderer/shader/type/WGSLStructField.js +2 -2
  423. package/src/shade/renderer/shader/type/WebGPUArray.js +2 -2
  424. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  425. package/src/shade/renderer/shader/type/WebGPUStruct.js +9 -6
  426. package/src/shade/renderer/texture/ShadeTexture.d.ts.map +1 -1
  427. package/src/shade/renderer/texture/ShadeTexture.js +17 -10
  428. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +2 -2
  429. package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts +4 -4
  430. package/src/shade/renderer/water/surface/chunk_water_surface_sample.js +6 -6
  431. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.d.ts.map +1 -1
  432. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.js +46 -55
  433. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts +3 -3
  434. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts.map +1 -1
  435. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.js +14 -4
  436. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts +10 -1
  437. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts.map +1 -1
  438. package/src/shade/renderer/water/surface/particles/chunk_water_particle.js +82 -40
  439. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts +4 -1
  440. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts.map +1 -1
  441. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.js +8 -4
  442. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts +98 -86
  443. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts.map +1 -1
  444. package/src/shade/renderer/water/surface/particles/shader_water_particles.js +440 -298
  445. package/src/view/common/HTMLElementCacheKey.d.ts.map +1 -1
  446. package/src/view/common/HTMLElementCacheKey.js +4 -3
  447. package/src/view/minimap/MinimapMarker.js +2 -2
@@ -1,955 +1,947 @@
1
- import {
2
- TRANSFORM64_ROTATION_OFFSET,
3
- TRANSFORM64_TRANSLATION_OFFSET,
4
- Transform64
5
- } from "../../../ecs/transform/Transform64.js";
6
- import { v3_quaternion_apply } from "../../../../core/geom/vec3/v3_quaternion_apply.js";
7
- import { ClothFlags } from "../ecs/ClothFlags.js";
8
- import { Cloth } from "../ecs/Cloth.js";
9
- import { CLOTH_DYNAMICS_BY_NAME } from "../ecs/cloth_dynamics_library.js";
10
- import { find_non_finite_cloth_state } from "../solver/find_non_finite_cloth_state.js";
11
- import { PlaygroundCamera, project } from "./cloth_playground_draw.js";
12
- import { build_cloth_rig, CLOTH_RIGS } from "./rig_build.js";
13
-
14
- /**
15
- * # Cloth rigs — the M1 playground
16
- *
17
- * `npm run dev --workspace @woosh/meep-engine`, then
18
- * <http://localhost:5173/src/engine/physics/cloth/playground/rig.html>.
19
- *
20
- * The sibling of `index.html`, one layer up. That page is the solver with a
21
- * canvas attached and no ECS at all, because at M0 there was none; this one is
22
- * the **entity-subtree path** — a real `EntityManager`, real entities with real
23
- * `TransformAttachment`s, a `Cloth` component on top, and `ClothSystem` walking
24
- * the subtree and writing `Transform64` back. Every point on screen is an
25
- * entity's world transform, read out of the component the system wrote it into.
26
- *
27
- * The rigs are the ones §17 asks M1 to show — a rope, a chain, and a
28
- * multi-strand rig under one `Cloth` — plus two that make the walk's two rules
29
- * visible: a hanging sign whose sign is a `ClothExclude`, and a rope with a
30
- * second `Cloth` partway down. The **cloths / particles** readout is the whole
31
- * of §3.1's coupled-solve claim: five strands, one cloth, one state, one
32
- * colouring.
33
- *
34
- * `globalThis.cloth_rig_playground` exposes `{ control, camera, ready, rebuild,
35
- * step_once, render, scene }` for a driver script. `requestAnimationFrame` does
36
- * not fire while a browser pane is hidden, and everything on this page hangs
37
- * off it — so a driver drives `step_once` and `render` itself and waits for no
38
- * frames at all. `ready` is the first `rebuild`, and `rebuild` returns a
39
- * promise because `EntityManager#startup` does.
40
- *
41
- * @author Alex Goldring
42
- * @copyright Company Named Limited (c) 2026
43
- */
44
-
45
- /** The rate `ClothSystem` is stepped at. */
46
- const FIXED_STEP = 1 / 60;
47
-
48
- /** §17's target, for the budget bar. */
49
- const BUDGET_MS = 2;
50
-
51
- /** How many recent step timings the readout keeps. */
52
- const TIMING_WINDOW = 120;
53
-
54
- const element = (id) => document.getElementById(id);
55
-
56
- const canvas = element('view');
57
- const context = canvas.getContext('2d');
58
-
59
- const camera = new PlaygroundCamera();
60
-
61
- const control = {
62
- rig: 'rope',
63
- dynamics: 'ROPE',
64
- links: 24,
65
- spacing: 0.05,
66
-
67
- stretch: 0.98,
68
- bend: 0.06,
69
- damping: 0.1,
70
- slack: 1,
71
- inertia: 1,
72
- blend: 1,
73
- substeps: 2,
74
- iterations: 4,
75
-
76
- motion: 'still',
77
- keep: false,
78
- never_sleep: false,
79
-
80
- view: 'links',
81
- running: true
82
- };
83
-
84
- /** @type {import('./rig_build.js').ClothRigScene} */
85
- let scene = null;
86
-
87
- const timing = [];
88
-
89
- let last_ms = 0;
90
- let elapsed = 0;
91
-
92
- const SCREEN = new Float64Array(2);
93
- const POINT = new Float64Array(3);
94
-
95
- // ---------------------------------------------------------------- the rig
96
-
97
- async function rebuild() {
98
- scene = null;
99
-
100
- scene = await build_cloth_rig({
101
- rig: control.rig,
102
- dynamics: control.dynamics,
103
- links: control.links,
104
- spacing: control.spacing
105
- });
106
-
107
- apply_dynamics();
108
-
109
- timing.length = 0;
110
- elapsed = 0;
111
-
112
- element('tripwire').textContent = 'ok';
113
- element('tripwire').className = 'good';
114
-
115
- // One step so the subtree is walked and the readout has something to say.
116
- step_once();
117
-
118
- frame_camera();
119
- }
120
-
121
- /**
122
- * Write the sliders into every `Cloth` in the rig. The library entries are
123
- * frozen and shared, so each cloth owns a clone — which is exactly the
124
- * `.clone()` step §3.3 describes, done by a page rather than by an author.
125
- */
126
- function apply_dynamics() {
127
- if (scene === null) {
128
- return;
129
- }
130
-
131
- for (const anchor of scene.anchor) {
132
- const cloth = scene.ecd.getComponent(anchor, Cloth);
133
-
134
- if (cloth === undefined) {
135
- continue;
136
- }
137
-
138
- if (cloth.dynamics.isClothDynamics !== true || Object.isFrozen(cloth.dynamics)) {
139
- cloth.dynamics = cloth.dynamics.clone();
140
- }
141
-
142
- cloth.dynamics.stretch = control.stretch;
143
- cloth.dynamics.bend = control.bend;
144
- cloth.dynamics.damping = control.damping;
145
- cloth.dynamics.slack = control.slack;
146
- cloth.dynamics.inertia = control.inertia;
147
- cloth.dynamics.substeps = control.substeps;
148
- cloth.dynamics.iterations = control.iterations;
149
-
150
- cloth.blend = control.blend;
151
-
152
- cloth.flags = (control.keep ? ClothFlags.TeleportKeep : 0)
153
- | (control.never_sleep ? ClothFlags.NeverSleep : 0);
154
- }
155
- }
156
-
157
- /**
158
- * Pull the sliders from a library entry, so picking one shows what it is made
159
- * of rather than silently overriding it.
160
- */
161
- function adopt_library(name) {
162
- const entry = CLOTH_DYNAMICS_BY_NAME[name];
163
-
164
- if (entry === undefined) {
165
- return;
166
- }
167
-
168
- control.dynamics = name;
169
- control.stretch = entry.stretch;
170
- control.bend = entry.bend;
171
- control.damping = entry.damping;
172
- control.slack = entry.slack;
173
- control.inertia = entry.inertia;
174
- control.substeps = entry.substeps;
175
- control.iterations = entry.iterations;
176
- }
177
-
178
- function frame_camera() {
179
- let min_x = Infinity, min_y = Infinity, min_z = Infinity;
180
- let max_x = -Infinity, max_y = -Infinity, max_z = -Infinity;
181
-
182
- for (const chain of scene.strand) {
183
- for (const entity of chain) {
184
- const t = scene.ecd.getComponent(entity, Transform64);
185
-
186
- if (t === undefined) {
187
- continue;
188
- }
189
-
190
- min_x = Math.min(min_x, t[TRANSFORM64_TRANSLATION_OFFSET]);
191
- max_x = Math.max(max_x, t[TRANSFORM64_TRANSLATION_OFFSET]);
192
- min_y = Math.min(min_y, t[TRANSFORM64_TRANSLATION_OFFSET + 1]);
193
- max_y = Math.max(max_y, t[TRANSFORM64_TRANSLATION_OFFSET + 1]);
194
- min_z = Math.min(min_z, t[TRANSFORM64_TRANSLATION_OFFSET + 2]);
195
- max_z = Math.max(max_z, t[TRANSFORM64_TRANSLATION_OFFSET + 2]);
196
- }
197
- }
198
-
199
- if (!Number.isFinite(min_x)) {
200
- return;
201
- }
202
-
203
- camera.target[0] = (min_x + max_x) * 0.5;
204
- camera.target[1] = (min_y + max_y) * 0.5;
205
- camera.target[2] = (min_z + max_z) * 0.5;
206
-
207
- camera.distance = Math.max(max_x - min_x, max_y - min_y, max_z - min_z, 0.05) * 2.2;
208
- }
209
-
210
- // ---------------------------------------------------------------- the loop
211
-
212
- /**
213
- * Drive the anchor. This is the only thing on the page that writes a transform
214
- * the cloth does not own, and it stands in for whatever poses an anchor in a
215
- * real scene — an animation, a path follower, a vehicle.
216
- */
217
- function drive_anchor() {
218
- if (scene === null || scene.anchor.length === 0) {
219
- return;
220
- }
221
-
222
- const transform = scene.ecd.getComponent(scene.anchor[0], Transform64);
223
-
224
- if (transform === undefined) {
225
- return;
226
- }
227
-
228
- const reach = control.links * control.spacing;
229
-
230
- switch (control.motion) {
231
- case 'sway':
232
- transform.setTranslation(Math.sin(elapsed * 2.2) * reach * 0.7, 0, 0);
233
- break;
234
-
235
- case 'sprint': {
236
- // A square wave in velocity, which is §7 P7's own profile: the
237
- // worst thing locomotion does to a hanging thing.
238
- const phase = Math.floor(elapsed) % 4;
239
-
240
- const speed = phase === 0 ? 6 : (phase === 2 ? -6 : 0);
241
-
242
- transform[TRANSFORM64_TRANSLATION_OFFSET] += speed * FIXED_STEP;
243
- break;
244
- }
245
-
246
- case 'spin':
247
- transform.setRotation(0, Math.sin(elapsed * 1.6), 0, Math.cos(elapsed * 1.6));
248
- break;
249
-
250
- case 'orbit':
251
- transform.setTranslation(
252
- Math.cos(elapsed * 1.8) * reach * 0.6,
253
- Math.sin(elapsed * 2.6) * reach * 0.25,
254
- Math.sin(elapsed * 1.8) * reach * 0.6
255
- );
256
- break;
257
-
258
- default:
259
- break;
260
- }
261
-
262
- transform.updateMatrix();
263
- }
264
-
265
- function step_once() {
266
- if (scene === null) {
267
- return;
268
- }
269
-
270
- elapsed += FIXED_STEP;
271
-
272
- drive_anchor();
273
-
274
- const started = performance.now();
275
-
276
- try {
277
- scene.cloth.fixedUpdate(FIXED_STEP);
278
- } catch (error) {
279
- control.running = false;
280
-
281
- element('tripwire').textContent = String(error.message).slice(0, 90);
282
- element('tripwire').className = 'bad';
283
- element('pause').textContent = 'run';
284
-
285
- return;
286
- }
287
-
288
- last_ms = performance.now() - started;
289
-
290
- timing.push(last_ms);
291
-
292
- if (timing.length > TIMING_WINDOW) {
293
- timing.shift();
294
- }
295
- }
296
-
297
- /**
298
- * Paint one frame from the state as it stands, stepping nothing. Separate from
299
- * {@link frame} so a driver can ask for a picture without also asking for a
300
- * simulation step, and so it can get one at all — see the module docblock on
301
- * `requestAnimationFrame`.
302
- */
303
- function render() {
304
- if (scene === null) {
305
- return;
306
- }
307
-
308
- draw();
309
- refresh_readout();
310
- }
311
-
312
- function frame() {
313
- if (control.running) {
314
- step_once();
315
- }
316
-
317
- render();
318
-
319
- requestAnimationFrame(frame);
320
- }
321
-
322
- // ---------------------------------------------------------------- drawing
323
-
324
- function draw() {
325
- const width = canvas.width = canvas.clientWidth;
326
- const height = canvas.height = canvas.clientHeight;
327
-
328
- context.clearRect(0, 0, width, height);
329
- context.fillStyle = '#0d0f12';
330
- context.fillRect(0, 0, width, height);
331
-
332
- draw_ground(width, height);
333
-
334
- if (control.view === 'animated') {
335
- draw_animated(width, height);
336
- }
337
-
338
- for (const chain of scene.strand) {
339
- draw_chain(chain, width, height);
340
- }
341
-
342
- if (control.view === 'frames') {
343
- for (const chain of scene.strand) {
344
- for (const entity of chain) {
345
- draw_frame(entity, width, height);
346
- }
347
- }
348
- }
349
-
350
- for (const entity of scene.excluded) {
351
- draw_marker(entity, width, height, '#e5b54a', 6);
352
- }
353
-
354
- for (const anchor of scene.anchor) {
355
- draw_marker(anchor, width, height, '#e5533d', 5);
356
- }
357
- }
358
-
359
- /**
360
- * @param {number[]} chain
361
- * @param {number} width
362
- * @param {number} height
363
- */
364
- function draw_chain(chain, width, height) {
365
- context.strokeStyle = '#7f8b9c';
366
- context.lineWidth = 2;
367
- context.beginPath();
368
-
369
- let started = false;
370
-
371
- for (const entity of chain) {
372
- const t = scene.ecd.getComponent(entity, Transform64);
373
-
374
- if (t === undefined) {
375
- continue;
376
- }
377
-
378
- const depth = project(
379
- SCREEN, camera,
380
- t[TRANSFORM64_TRANSLATION_OFFSET],
381
- t[TRANSFORM64_TRANSLATION_OFFSET + 1],
382
- t[TRANSFORM64_TRANSLATION_OFFSET + 2],
383
- width, height
384
- );
385
-
386
- if (depth <= 0.01) {
387
- started = false;
388
-
389
- continue;
390
- }
391
-
392
- if (started) {
393
- context.lineTo(SCREEN[0], SCREEN[1]);
394
- } else {
395
- context.moveTo(SCREEN[0], SCREEN[1]);
396
-
397
- started = true;
398
- }
399
- }
400
-
401
- context.stroke();
402
-
403
- context.fillStyle = '#c9d2de';
404
-
405
- for (const entity of chain) {
406
- const t = scene.ecd.getComponent(entity, Transform64);
407
-
408
- if (t === undefined) {
409
- continue;
410
- }
411
-
412
- const depth = project(
413
- SCREEN, camera,
414
- t[TRANSFORM64_TRANSLATION_OFFSET],
415
- t[TRANSFORM64_TRANSLATION_OFFSET + 1],
416
- t[TRANSFORM64_TRANSLATION_OFFSET + 2],
417
- width, height
418
- );
419
-
420
- if (depth <= 0.01) {
421
- continue;
422
- }
423
-
424
- context.beginPath();
425
- context.arc(SCREEN[0], SCREEN[1], 2.5, 0, Math.PI * 2);
426
- context.fill();
427
- }
428
- }
429
-
430
- /**
431
- * The pose the cloth is leashed to — `blend = 0`'s answer, drawn faintly beside
432
- * the simulated one. The gap between the two lines is what `slack` bounds.
433
- */
434
- function draw_animated(width, height) {
435
- for (const anchor of scene.anchor) {
436
- const instance = scene.cloth.instanceOf(anchor);
437
-
438
- if (instance === undefined || !instance.seeded) {
439
- continue;
440
- }
441
-
442
- const transform = instance.node_transform[0];
443
-
444
- context.strokeStyle = 'rgba(111, 168, 220, 0.45)';
445
- context.lineWidth = 1;
446
-
447
- for (let node = 1; node < instance.node_count; node++) {
448
- const parent = instance.node_parent[node];
449
-
450
- context.beginPath();
451
-
452
- let ok = true;
453
-
454
- for (const which of [parent, node]) {
455
- v3_quaternion_apply(
456
- POINT, 0,
457
- instance.animated_position[which * 3],
458
- instance.animated_position[which * 3 + 1],
459
- instance.animated_position[which * 3 + 2],
460
- transform[TRANSFORM64_ROTATION_OFFSET],
461
- transform[TRANSFORM64_ROTATION_OFFSET + 1],
462
- transform[TRANSFORM64_ROTATION_OFFSET + 2],
463
- transform[TRANSFORM64_ROTATION_OFFSET + 3]
464
- );
465
-
466
- const depth = project(
467
- SCREEN, camera,
468
- POINT[0] + transform[TRANSFORM64_TRANSLATION_OFFSET],
469
- POINT[1] + transform[TRANSFORM64_TRANSLATION_OFFSET + 1],
470
- POINT[2] + transform[TRANSFORM64_TRANSLATION_OFFSET + 2],
471
- width, height
472
- );
473
-
474
- if (depth <= 0.01) {
475
- ok = false;
476
-
477
- break;
478
- }
479
-
480
- if (which === parent) {
481
- context.moveTo(SCREEN[0], SCREEN[1]);
482
- } else {
483
- context.lineTo(SCREEN[0], SCREEN[1]);
484
- }
485
- }
486
-
487
- if (ok) {
488
- context.stroke();
489
- }
490
- }
491
- }
492
- }
493
-
494
- /**
495
- * The orientation the write-back reconstructed, as a short axis triple. §11.1's
496
- * "rotation derived from the direction to the child" is only visible here.
497
- */
498
- function draw_frame(entity, width, height) {
499
- const t = scene.ecd.getComponent(entity, Transform64);
500
-
501
- if (t === undefined) {
502
- return;
503
- }
504
-
505
- const length = control.spacing * 0.6;
506
-
507
- const axes = [
508
- [length, 0, 0, '#e5533d'],
509
- [0, length, 0, '#5ad19a'],
510
- [0, 0, length, '#6fa8dc']
511
- ];
512
-
513
- const origin_depth = project(
514
- SCREEN, camera,
515
- t[TRANSFORM64_TRANSLATION_OFFSET],
516
- t[TRANSFORM64_TRANSLATION_OFFSET + 1],
517
- t[TRANSFORM64_TRANSLATION_OFFSET + 2],
518
- width, height
519
- );
520
-
521
- if (origin_depth <= 0.01) {
522
- return;
523
- }
524
-
525
- const ox = SCREEN[0];
526
- const oy = SCREEN[1];
527
-
528
- for (const [ax, ay, az, colour] of axes) {
529
- v3_quaternion_apply(
530
- POINT, 0, ax, ay, az,
531
- t[TRANSFORM64_ROTATION_OFFSET],
532
- t[TRANSFORM64_ROTATION_OFFSET + 1],
533
- t[TRANSFORM64_ROTATION_OFFSET + 2],
534
- t[TRANSFORM64_ROTATION_OFFSET + 3]
535
- );
536
-
537
- const depth = project(
538
- SCREEN, camera,
539
- t[TRANSFORM64_TRANSLATION_OFFSET] + POINT[0],
540
- t[TRANSFORM64_TRANSLATION_OFFSET + 1] + POINT[1],
541
- t[TRANSFORM64_TRANSLATION_OFFSET + 2] + POINT[2],
542
- width, height
543
- );
544
-
545
- if (depth <= 0.01) {
546
- continue;
547
- }
548
-
549
- context.strokeStyle = colour;
550
- context.lineWidth = 1;
551
- context.beginPath();
552
- context.moveTo(ox, oy);
553
- context.lineTo(SCREEN[0], SCREEN[1]);
554
- context.stroke();
555
- }
556
- }
557
-
558
- function draw_marker(entity, width, height, colour, radius) {
559
- const t = scene.ecd.getComponent(entity, Transform64);
560
-
561
- if (t === undefined) {
562
- return;
563
- }
564
-
565
- const depth = project(
566
- SCREEN, camera,
567
- t[TRANSFORM64_TRANSLATION_OFFSET],
568
- t[TRANSFORM64_TRANSLATION_OFFSET + 1],
569
- t[TRANSFORM64_TRANSLATION_OFFSET + 2],
570
- width, height
571
- );
572
-
573
- if (depth <= 0.01) {
574
- return;
575
- }
576
-
577
- context.fillStyle = colour;
578
- context.beginPath();
579
- context.arc(SCREEN[0], SCREEN[1], radius, 0, Math.PI * 2);
580
- context.fill();
581
- }
582
-
583
- function draw_ground(width, height) {
584
- const step = Math.max(control.spacing * 4, 0.1);
585
-
586
- context.strokeStyle = 'rgba(139, 147, 159, 0.16)';
587
- context.lineWidth = 1;
588
-
589
- const extent = 8;
590
-
591
- for (let i = -extent; i <= extent; i++) {
592
- for (const [ax, az, bx, bz] of [
593
- [i * step, -extent * step, i * step, extent * step],
594
- [-extent * step, i * step, extent * step, i * step]
595
- ]) {
596
- const a = project(SCREEN, camera, ax, -control.links * control.spacing * 1.15, az, width, height);
597
-
598
- const x0 = SCREEN[0];
599
- const y0 = SCREEN[1];
600
-
601
- const b = project(SCREEN, camera, bx, -control.links * control.spacing * 1.15, bz, width, height);
602
-
603
- if (a <= 0.01 || b <= 0.01) {
604
- continue;
605
- }
606
-
607
- context.beginPath();
608
- context.moveTo(x0, y0);
609
- context.lineTo(SCREEN[0], SCREEN[1]);
610
- context.stroke();
611
- }
612
- }
613
- }
614
-
615
- // ---------------------------------------------------------------- readout
616
-
617
- function refresh_readout() {
618
- element('ms').textContent = last_ms.toFixed(3);
619
-
620
- if (timing.length > 0) {
621
- const sorted = timing.slice().sort((a, b) => a - b);
622
-
623
- element('ms_min').textContent = `(min ${sorted[0].toFixed(3)})`;
624
-
625
- const bar = element('budget');
626
-
627
- bar.classList.toggle('over', sorted[0] > BUDGET_MS);
628
- bar.firstElementChild.style.width = `${Math.min(100, sorted[0] / BUDGET_MS * 100)}%`;
629
- }
630
-
631
- let particles = 0;
632
- let stretch = 0;
633
- let bend = 0;
634
- let leash = 0;
635
- let colours = 0;
636
- let tether = 0;
637
- let speed = 0;
638
- let teleports = 0;
639
- let asleep = 0;
640
- let seeded = 0;
641
- let trouble = "";
642
-
643
- for (const anchor of scene.anchor) {
644
- const instance = scene.cloth.instanceOf(anchor);
645
-
646
- if (instance === undefined || !instance.seeded) {
647
- continue;
648
- }
649
-
650
- seeded++;
651
-
652
- const state = instance.state;
653
-
654
- particles += state.particle_count;
655
- stretch += state.stretch_count;
656
- bend += state.bend_count;
657
- leash += state.leash_count;
658
- colours = Math.max(colours, state.colour_count);
659
- teleports += instance.teleport_count;
660
-
661
- if (instance.asleep) {
662
- asleep++;
663
- }
664
-
665
- if (trouble === "") {
666
- trouble = find_non_finite_cloth_state(state);
667
- }
668
-
669
- const origin = instance.particle_of_node[0] * 3;
670
-
671
- for (let node = 1; node < instance.node_count; node++) {
672
- const p = instance.particle_of_node[node] * 3;
673
-
674
- const distance = Math.hypot(
675
- state.position[p] - state.position[origin],
676
- state.position[p + 1] - state.position[origin + 1],
677
- state.position[p + 2] - state.position[origin + 2]
678
- );
679
-
680
- tether = Math.max(tether, distance / instance.geodesic[node]);
681
- }
682
-
683
- for (let i = 0; i < state.particle_count; i++) {
684
- const p = i * 3;
685
-
686
- speed = Math.max(speed, Math.hypot(state.velocity[p], state.velocity[p + 1], state.velocity[p + 2]));
687
- }
688
- }
689
-
690
- element('counts').textContent = `${seeded} / ${particles}`;
691
- element('rows').textContent = `${stretch} / ${bend} / ${leash}`;
692
- element('colours').textContent = String(colours);
693
- element('tether').textContent = tether.toFixed(4);
694
- element('speed').textContent = `${speed.toFixed(2)} m/s`;
695
- element('teleports').textContent = String(teleports);
696
-
697
- const sleep_cell = element('asleep');
698
-
699
- sleep_cell.textContent = asleep === 0 ? 'no' : `${asleep} of ${seeded}`;
700
- sleep_cell.className = asleep > 0 ? 'yes' : '';
701
-
702
- if (trouble !== "" && element('tripwire').className !== 'bad') {
703
- element('tripwire').textContent = trouble.slice(0, 90);
704
- element('tripwire').className = 'bad';
705
- }
706
- }
707
-
708
- // ---------------------------------------------------------------- controls
709
-
710
- function build_rig_buttons() {
711
- const host = element('rigs');
712
-
713
- for (const rig of CLOTH_RIGS) {
714
- const button = document.createElement('button');
715
-
716
- button.innerHTML = `${rig.label}<small>${rig.blurb}</small>`;
717
-
718
- button.addEventListener('click', () => {
719
- control.rig = rig.id;
720
-
721
- adopt_library(rig.dynamics);
722
-
723
- sync_inputs();
724
- rebuild();
725
- });
726
-
727
- host.appendChild(button);
728
- }
729
- }
730
-
731
- function build_library_options() {
732
- const select = element('dynamics');
733
-
734
- for (const name of Object.keys(CLOTH_DYNAMICS_BY_NAME)) {
735
- const option = document.createElement('option');
736
-
737
- option.value = name;
738
- option.textContent = name.toLowerCase();
739
-
740
- select.appendChild(option);
741
- }
742
- }
743
-
744
- /**
745
- * @param {string} id
746
- * @param {string} key
747
- * @param {function(number): string} format
748
- * @param {boolean} rebuilds
749
- */
750
- function bind_range(id, key, format, rebuilds) {
751
- const input = element(id);
752
- const output = element(`${id}_out`);
753
-
754
- const refresh = () => {
755
- input.value = String(control[key]);
756
- output.textContent = format(control[key]);
757
- };
758
-
759
- input.addEventListener('input', () => {
760
- control[key] = Number(input.value);
761
-
762
- output.textContent = format(control[key]);
763
-
764
- if (rebuilds) {
765
- rebuild();
766
- } else {
767
- apply_dynamics();
768
- }
769
- });
770
-
771
- refresh();
772
-
773
- return refresh;
774
- }
775
-
776
- const refreshers = [];
777
-
778
- function sync_inputs() {
779
- for (const refresh of refreshers) {
780
- refresh();
781
- }
782
-
783
- element('dynamics').value = control.dynamics;
784
- element('motion').value = control.motion;
785
- }
786
-
787
- function bind_controls() {
788
- refreshers.push(bind_range('links', 'links', v => String(v), true));
789
- refreshers.push(bind_range('spacing', 'spacing', v => `${(v * 100).toFixed(1)} cm`, true));
790
-
791
- refreshers.push(bind_range('stretch', 'stretch', v => v.toFixed(2), false));
792
- refreshers.push(bind_range('bend', 'bend', v => v.toFixed(2), false));
793
- refreshers.push(bind_range('damping', 'damping', v => v.toFixed(2), false));
794
- refreshers.push(bind_range('slack', 'slack', v => v.toFixed(2), false));
795
- refreshers.push(bind_range('inertia', 'inertia', v => v.toFixed(2), false));
796
- refreshers.push(bind_range('blend', 'blend', v => v.toFixed(2), false));
797
- refreshers.push(bind_range('substeps', 'substeps', v => String(v), false));
798
- refreshers.push(bind_range('iterations', 'iterations', v => String(v), false));
799
-
800
- element('dynamics').addEventListener('change', (event) => {
801
- adopt_library(event.target.value);
802
-
803
- sync_inputs();
804
- apply_dynamics();
805
- });
806
-
807
- element('motion').addEventListener('change', (event) => {
808
- control.motion = event.target.value;
809
- });
810
-
811
- element('teleport').addEventListener('click', () => {
812
- if (scene === null) {
813
- return;
814
- }
815
-
816
- const transform = scene.ecd.getComponent(scene.anchor[0], Transform64);
817
-
818
- transform[TRANSFORM64_TRANSLATION_OFFSET] += 1000;
819
- transform.updateMatrix();
820
-
821
- // Chase it, or there is nothing to look at.
822
- camera.target[0] += 1000;
823
- });
824
-
825
- for (const [id, key] of [['keep', 'keep'], ['never_sleep', 'never_sleep']]) {
826
- element(id).addEventListener('click', () => {
827
- control[key] = !control[key];
828
-
829
- element(id).classList.toggle('on', control[key]);
830
-
831
- apply_dynamics();
832
- });
833
- }
834
-
835
- for (const [id, view] of [['view_links', 'links'], ['view_animated', 'animated'], ['view_frames', 'frames']]) {
836
- element(id).addEventListener('click', () => {
837
- control.view = view;
838
-
839
- for (const other of ['view_links', 'view_animated', 'view_frames']) {
840
- element(other).classList.toggle('on', other === id);
841
- }
842
- });
843
- }
844
-
845
- element('pause').addEventListener('click', () => {
846
- control.running = !control.running;
847
-
848
- element('pause').textContent = control.running ? 'pause' : 'run';
849
- });
850
-
851
- element('single').addEventListener('click', () => step_once());
852
-
853
- element('reset').addEventListener('click', () => rebuild());
854
-
855
- element('poke').addEventListener('click', () => {
856
- if (scene === null) {
857
- return;
858
- }
859
-
860
- for (const anchor of scene.anchor) {
861
- const instance = scene.cloth.instanceOf(anchor);
862
-
863
- if (instance === undefined || !instance.seeded) {
864
- continue;
865
- }
866
-
867
- scene.cloth.wake(anchor);
868
-
869
- for (let i = 0; i < instance.state.particle_count; i++) {
870
- instance.state.velocity[i * 3] += (Math.random() - 0.5) * 6;
871
- instance.state.velocity[i * 3 + 2] += (Math.random() - 0.5) * 6;
872
- }
873
- }
874
- });
875
- }
876
-
877
- // ---------------------------------------------------------------- camera
878
-
879
- let dragging = false;
880
- let panning = false;
881
- let last_x = 0;
882
- let last_y = 0;
883
-
884
- canvas.addEventListener('pointerdown', (event) => {
885
- dragging = true;
886
- panning = event.shiftKey || event.button === 2;
887
-
888
- last_x = event.clientX;
889
- last_y = event.clientY;
890
-
891
- canvas.classList.add('grabbing');
892
- canvas.setPointerCapture(event.pointerId);
893
- });
894
-
895
- canvas.addEventListener('pointermove', (event) => {
896
- if (!dragging) {
897
- return;
898
- }
899
-
900
- const dx = event.clientX - last_x;
901
- const dy = event.clientY - last_y;
902
-
903
- last_x = event.clientX;
904
- last_y = event.clientY;
905
-
906
- if (panning) {
907
- const scale = camera.distance * 0.0016;
908
-
909
- camera.target[0] -= (dx * Math.cos(camera.yaw)) * scale;
910
- camera.target[2] += (dx * Math.sin(camera.yaw)) * scale;
911
- camera.target[1] += dy * scale;
912
- } else {
913
- camera.yaw -= dx * 0.006;
914
- camera.pitch = Math.max(-1.4, Math.min(1.4, camera.pitch + dy * 0.006));
915
- }
916
- });
917
-
918
- canvas.addEventListener('pointerup', (event) => {
919
- dragging = false;
920
-
921
- canvas.classList.remove('grabbing');
922
- canvas.releasePointerCapture(event.pointerId);
923
- });
924
-
925
- canvas.addEventListener('wheel', (event) => {
926
- event.preventDefault();
927
-
928
- camera.distance = Math.max(0.05, camera.distance * Math.exp(event.deltaY * 0.001));
929
- }, { passive: false });
930
-
931
- canvas.addEventListener('contextmenu', (event) => event.preventDefault());
932
-
933
- // ---------------------------------------------------------------- start
934
-
935
- build_rig_buttons();
936
- build_library_options();
937
- adopt_library('ROPE');
938
- bind_controls();
939
- sync_inputs();
940
-
941
- const ready = rebuild();
942
-
943
- globalThis.cloth_rig_playground = {
944
- control,
945
- camera,
946
- rebuild,
947
- step_once,
948
- render,
949
- ready,
950
- get scene() {
951
- return scene;
952
- }
953
- };
954
-
955
- requestAnimationFrame(frame);
1
+ import {
2
+ TRANSFORM64_ROTATION_OFFSET,
3
+ TRANSFORM64_TRANSLATION_OFFSET,
4
+ Transform64
5
+ } from "../../../ecs/transform/Transform64.js";
6
+ import { v3_quaternion_apply } from "../../../../core/geom/vec3/v3_quaternion_apply.js";
7
+ import { ClothFlags } from "../ecs/ClothFlags.js";
8
+ import { Cloth } from "../ecs/Cloth.js";
9
+ import { CLOTH_DYNAMICS_BY_NAME } from "../ecs/cloth_dynamics_library.js";
10
+ import { find_non_finite_cloth_state } from "../solver/find_non_finite_cloth_state.js";
11
+ import { PlaygroundCamera, project } from "./cloth_playground_draw.js";
12
+ import { build_cloth_rig, CLOTH_RIGS } from "./rig_build.js";
13
+ import { number_compare_ascending } from "../../../../core/primitives/numbers/number_compare_ascending.js";
14
+ import { cloth_worst_tether_ratio } from "../solver/cloth_worst_tether_ratio.js";
15
+
16
+ /**
17
+ * # Cloth rigs — the M1 playground
18
+ *
19
+ * `npm run dev --workspace @woosh/meep-engine`, then
20
+ * <http://localhost:5173/src/engine/physics/cloth/playground/rig.html>.
21
+ *
22
+ * The sibling of `index.html`, one layer up. That page is the solver with a
23
+ * canvas attached and no ECS at all, because at M0 there was none; this one is
24
+ * the **entity-subtree path** — a real `EntityManager`, real entities with real
25
+ * `TransformAttachment`s, a `Cloth` component on top, and `ClothSystem` walking
26
+ * the subtree and writing `Transform64` back. Every point on screen is an
27
+ * entity's world transform, read out of the component the system wrote it into.
28
+ *
29
+ * The rigs are the ones §17 asks M1 to show — a rope, a chain, and a
30
+ * multi-strand rig under one `Cloth` — plus two that make the walk's two rules
31
+ * visible: a hanging sign whose sign is a `ClothExclude`, and a rope with a
32
+ * second `Cloth` partway down. The **cloths / particles** readout is the whole
33
+ * of §3.1's coupled-solve claim: five strands, one cloth, one state, one
34
+ * colouring.
35
+ *
36
+ * `globalThis.cloth_rig_playground` exposes `{ control, camera, ready, rebuild,
37
+ * step_once, render, scene }` for a driver script. `requestAnimationFrame` does
38
+ * not fire while a browser pane is hidden, and everything on this page hangs
39
+ * off it — so a driver drives `step_once` and `render` itself and waits for no
40
+ * frames at all. `ready` is the first `rebuild`, and `rebuild` returns a
41
+ * promise because `EntityManager#startup` does.
42
+ *
43
+ * @author Alex Goldring
44
+ * @copyright Company Named Limited (c) 2026
45
+ */
46
+
47
+ /** The rate `ClothSystem` is stepped at. */
48
+ const FIXED_STEP = 1 / 60;
49
+
50
+ /** §17's target, for the budget bar. */
51
+ const BUDGET_MS = 2;
52
+
53
+ /** How many recent step timings the readout keeps. */
54
+ const TIMING_WINDOW = 120;
55
+
56
+ const element = (id) => document.getElementById(id);
57
+
58
+ const canvas = element('view');
59
+ const context = canvas.getContext('2d');
60
+
61
+ const camera = new PlaygroundCamera();
62
+
63
+ const control = {
64
+ rig: 'rope',
65
+ dynamics: 'ROPE',
66
+ links: 24,
67
+ spacing: 0.05,
68
+
69
+ stretch: 0.98,
70
+ bend: 0.06,
71
+ damping: 0.1,
72
+ slack: 1,
73
+ inertia: 1,
74
+ blend: 1,
75
+ substeps: 2,
76
+ iterations: 4,
77
+
78
+ motion: 'still',
79
+ keep: false,
80
+ never_sleep: false,
81
+
82
+ view: 'links',
83
+ running: true
84
+ };
85
+
86
+ /** @type {import('./rig_build.js').ClothRigScene} */
87
+ let scene = null;
88
+
89
+ const timing = [];
90
+
91
+ let last_ms = 0;
92
+ let elapsed = 0;
93
+
94
+ const SCREEN = new Float64Array(2);
95
+ const POINT = new Float64Array(3);
96
+
97
+ // ---------------------------------------------------------------- the rig
98
+
99
+ async function rebuild() {
100
+ scene = null;
101
+
102
+ scene = await build_cloth_rig({
103
+ rig: control.rig,
104
+ dynamics: control.dynamics,
105
+ links: control.links,
106
+ spacing: control.spacing
107
+ });
108
+
109
+ apply_dynamics();
110
+
111
+ timing.length = 0;
112
+ elapsed = 0;
113
+
114
+ element('tripwire').textContent = 'ok';
115
+ element('tripwire').className = 'good';
116
+
117
+ // One step so the subtree is walked and the readout has something to say.
118
+ step_once();
119
+
120
+ frame_camera();
121
+ }
122
+
123
+ /**
124
+ * Write the sliders into every `Cloth` in the rig. The library entries are
125
+ * frozen and shared, so each cloth owns a clone — which is exactly the
126
+ * `.clone()` step §3.3 describes, done by a page rather than by an author.
127
+ */
128
+ function apply_dynamics() {
129
+ if (scene === null) {
130
+ return;
131
+ }
132
+
133
+ for (const anchor of scene.anchor) {
134
+ const cloth = scene.ecd.getComponent(anchor, Cloth);
135
+
136
+ if (cloth === undefined) {
137
+ continue;
138
+ }
139
+
140
+ if (cloth.dynamics.isClothDynamics !== true || Object.isFrozen(cloth.dynamics)) {
141
+ cloth.dynamics = cloth.dynamics.clone();
142
+ }
143
+
144
+ cloth.dynamics.stretch = control.stretch;
145
+ cloth.dynamics.bend = control.bend;
146
+ cloth.dynamics.damping = control.damping;
147
+ cloth.dynamics.slack = control.slack;
148
+ cloth.dynamics.inertia = control.inertia;
149
+ cloth.dynamics.substeps = control.substeps;
150
+ cloth.dynamics.iterations = control.iterations;
151
+
152
+ cloth.blend = control.blend;
153
+
154
+ cloth.flags = (control.keep ? ClothFlags.TeleportKeep : 0)
155
+ | (control.never_sleep ? ClothFlags.NeverSleep : 0);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Pull the sliders from a library entry, so picking one shows what it is made
161
+ * of rather than silently overriding it.
162
+ */
163
+ function adopt_library(name) {
164
+ const entry = CLOTH_DYNAMICS_BY_NAME[name];
165
+
166
+ if (entry === undefined) {
167
+ return;
168
+ }
169
+
170
+ control.dynamics = name;
171
+ control.stretch = entry.stretch;
172
+ control.bend = entry.bend;
173
+ control.damping = entry.damping;
174
+ control.slack = entry.slack;
175
+ control.inertia = entry.inertia;
176
+ control.substeps = entry.substeps;
177
+ control.iterations = entry.iterations;
178
+ }
179
+
180
+ function frame_camera() {
181
+ let min_x = Infinity, min_y = Infinity, min_z = Infinity;
182
+ let max_x = -Infinity, max_y = -Infinity, max_z = -Infinity;
183
+
184
+ for (const chain of scene.strand) {
185
+ for (const entity of chain) {
186
+ const t = scene.ecd.getComponent(entity, Transform64);
187
+
188
+ if (t === undefined) {
189
+ continue;
190
+ }
191
+
192
+ min_x = Math.min(min_x, t[TRANSFORM64_TRANSLATION_OFFSET]);
193
+ max_x = Math.max(max_x, t[TRANSFORM64_TRANSLATION_OFFSET]);
194
+ min_y = Math.min(min_y, t[TRANSFORM64_TRANSLATION_OFFSET + 1]);
195
+ max_y = Math.max(max_y, t[TRANSFORM64_TRANSLATION_OFFSET + 1]);
196
+ min_z = Math.min(min_z, t[TRANSFORM64_TRANSLATION_OFFSET + 2]);
197
+ max_z = Math.max(max_z, t[TRANSFORM64_TRANSLATION_OFFSET + 2]);
198
+ }
199
+ }
200
+
201
+ if (!Number.isFinite(min_x)) {
202
+ return;
203
+ }
204
+
205
+ camera.target[0] = (min_x + max_x) * 0.5;
206
+ camera.target[1] = (min_y + max_y) * 0.5;
207
+ camera.target[2] = (min_z + max_z) * 0.5;
208
+
209
+ camera.distance = Math.max(max_x - min_x, max_y - min_y, max_z - min_z, 0.05) * 2.2;
210
+ }
211
+
212
+ // ---------------------------------------------------------------- the loop
213
+
214
+ /**
215
+ * Drive the anchor. This is the only thing on the page that writes a transform
216
+ * the cloth does not own, and it stands in for whatever poses an anchor in a
217
+ * real scene — an animation, a path follower, a vehicle.
218
+ */
219
+ function drive_anchor() {
220
+ if (scene === null || scene.anchor.length === 0) {
221
+ return;
222
+ }
223
+
224
+ const transform = scene.ecd.getComponent(scene.anchor[0], Transform64);
225
+
226
+ if (transform === undefined) {
227
+ return;
228
+ }
229
+
230
+ const reach = control.links * control.spacing;
231
+
232
+ switch (control.motion) {
233
+ case 'sway':
234
+ transform.setTranslation(Math.sin(elapsed * 2.2) * reach * 0.7, 0, 0);
235
+ break;
236
+
237
+ case 'sprint': {
238
+ // A square wave in velocity, which is §7 P7's own profile: the
239
+ // worst thing locomotion does to a hanging thing.
240
+ const phase = Math.floor(elapsed) % 4;
241
+
242
+ const speed = phase === 0 ? 6 : (phase === 2 ? -6 : 0);
243
+
244
+ transform[TRANSFORM64_TRANSLATION_OFFSET] += speed * FIXED_STEP;
245
+ break;
246
+ }
247
+
248
+ case 'spin':
249
+ transform.setRotation(0, Math.sin(elapsed * 1.6), 0, Math.cos(elapsed * 1.6));
250
+ break;
251
+
252
+ case 'orbit':
253
+ transform.setTranslation(
254
+ Math.cos(elapsed * 1.8) * reach * 0.6,
255
+ Math.sin(elapsed * 2.6) * reach * 0.25,
256
+ Math.sin(elapsed * 1.8) * reach * 0.6
257
+ );
258
+ break;
259
+
260
+ default:
261
+ break;
262
+ }
263
+
264
+ transform.updateMatrix();
265
+ }
266
+
267
+ function step_once() {
268
+ if (scene === null) {
269
+ return;
270
+ }
271
+
272
+ elapsed += FIXED_STEP;
273
+
274
+ drive_anchor();
275
+
276
+ const started = performance.now();
277
+
278
+ try {
279
+ scene.cloth.fixedUpdate(FIXED_STEP);
280
+ } catch (error) {
281
+ control.running = false;
282
+
283
+ element('tripwire').textContent = String(error.message).slice(0, 90);
284
+ element('tripwire').className = 'bad';
285
+ element('pause').textContent = 'run';
286
+
287
+ return;
288
+ }
289
+
290
+ last_ms = performance.now() - started;
291
+
292
+ timing.push(last_ms);
293
+
294
+ if (timing.length > TIMING_WINDOW) {
295
+ timing.shift();
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Paint one frame from the state as it stands, stepping nothing. Separate from
301
+ * {@link frame} so a driver can ask for a picture without also asking for a
302
+ * simulation step, and so it can get one at all — see the module docblock on
303
+ * `requestAnimationFrame`.
304
+ */
305
+ function render() {
306
+ if (scene === null) {
307
+ return;
308
+ }
309
+
310
+ draw();
311
+ refresh_readout();
312
+ }
313
+
314
+ function frame() {
315
+ if (control.running) {
316
+ step_once();
317
+ }
318
+
319
+ render();
320
+
321
+ requestAnimationFrame(frame);
322
+ }
323
+
324
+ // ---------------------------------------------------------------- drawing
325
+
326
+ function draw() {
327
+ const width = canvas.width = canvas.clientWidth;
328
+ const height = canvas.height = canvas.clientHeight;
329
+
330
+ context.clearRect(0, 0, width, height);
331
+ context.fillStyle = '#0d0f12';
332
+ context.fillRect(0, 0, width, height);
333
+
334
+ draw_ground(width, height);
335
+
336
+ if (control.view === 'animated') {
337
+ draw_animated(width, height);
338
+ }
339
+
340
+ for (const chain of scene.strand) {
341
+ draw_chain(chain, width, height);
342
+ }
343
+
344
+ if (control.view === 'frames') {
345
+ for (const chain of scene.strand) {
346
+ for (const entity of chain) {
347
+ draw_frame(entity, width, height);
348
+ }
349
+ }
350
+ }
351
+
352
+ for (const entity of scene.excluded) {
353
+ draw_marker(entity, width, height, '#e5b54a', 6);
354
+ }
355
+
356
+ for (const anchor of scene.anchor) {
357
+ draw_marker(anchor, width, height, '#e5533d', 5);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * @param {number[]} chain
363
+ * @param {number} width
364
+ * @param {number} height
365
+ */
366
+ function draw_chain(chain, width, height) {
367
+ context.strokeStyle = '#7f8b9c';
368
+ context.lineWidth = 2;
369
+ context.beginPath();
370
+
371
+ let started = false;
372
+
373
+ for (const entity of chain) {
374
+ const t = scene.ecd.getComponent(entity, Transform64);
375
+
376
+ if (t === undefined) {
377
+ continue;
378
+ }
379
+
380
+ const depth = project(
381
+ SCREEN, camera,
382
+ t[TRANSFORM64_TRANSLATION_OFFSET],
383
+ t[TRANSFORM64_TRANSLATION_OFFSET + 1],
384
+ t[TRANSFORM64_TRANSLATION_OFFSET + 2],
385
+ width, height
386
+ );
387
+
388
+ if (depth <= 0.01) {
389
+ started = false;
390
+
391
+ continue;
392
+ }
393
+
394
+ if (started) {
395
+ context.lineTo(SCREEN[0], SCREEN[1]);
396
+ } else {
397
+ context.moveTo(SCREEN[0], SCREEN[1]);
398
+
399
+ started = true;
400
+ }
401
+ }
402
+
403
+ context.stroke();
404
+
405
+ context.fillStyle = '#c9d2de';
406
+
407
+ for (const entity of chain) {
408
+ const t = scene.ecd.getComponent(entity, Transform64);
409
+
410
+ if (t === undefined) {
411
+ continue;
412
+ }
413
+
414
+ const depth = project(
415
+ SCREEN, camera,
416
+ t[TRANSFORM64_TRANSLATION_OFFSET],
417
+ t[TRANSFORM64_TRANSLATION_OFFSET + 1],
418
+ t[TRANSFORM64_TRANSLATION_OFFSET + 2],
419
+ width, height
420
+ );
421
+
422
+ if (depth <= 0.01) {
423
+ continue;
424
+ }
425
+
426
+ context.beginPath();
427
+ context.arc(SCREEN[0], SCREEN[1], 2.5, 0, Math.PI * 2);
428
+ context.fill();
429
+ }
430
+ }
431
+
432
+ /**
433
+ * The pose the cloth is leashed to — `blend = 0`'s answer, drawn faintly beside
434
+ * the simulated one. The gap between the two lines is what `slack` bounds.
435
+ */
436
+ function draw_animated(width, height) {
437
+ for (const anchor of scene.anchor) {
438
+ const instance = scene.cloth.instanceOf(anchor);
439
+
440
+ if (instance === undefined || !instance.seeded) {
441
+ continue;
442
+ }
443
+
444
+ const transform = instance.node_transform[0];
445
+
446
+ context.strokeStyle = 'rgba(111, 168, 220, 0.45)';
447
+ context.lineWidth = 1;
448
+
449
+ for (let node = 1; node < instance.node_count; node++) {
450
+ const parent = instance.node_parent[node];
451
+
452
+ context.beginPath();
453
+
454
+ let ok = true;
455
+
456
+ for (const which of [parent, node]) {
457
+ v3_quaternion_apply(
458
+ POINT, 0,
459
+ instance.animated_position[which * 3],
460
+ instance.animated_position[which * 3 + 1],
461
+ instance.animated_position[which * 3 + 2],
462
+ transform[TRANSFORM64_ROTATION_OFFSET],
463
+ transform[TRANSFORM64_ROTATION_OFFSET + 1],
464
+ transform[TRANSFORM64_ROTATION_OFFSET + 2],
465
+ transform[TRANSFORM64_ROTATION_OFFSET + 3]
466
+ );
467
+
468
+ const depth = project(
469
+ SCREEN, camera,
470
+ POINT[0] + transform[TRANSFORM64_TRANSLATION_OFFSET],
471
+ POINT[1] + transform[TRANSFORM64_TRANSLATION_OFFSET + 1],
472
+ POINT[2] + transform[TRANSFORM64_TRANSLATION_OFFSET + 2],
473
+ width, height
474
+ );
475
+
476
+ if (depth <= 0.01) {
477
+ ok = false;
478
+
479
+ break;
480
+ }
481
+
482
+ if (which === parent) {
483
+ context.moveTo(SCREEN[0], SCREEN[1]);
484
+ } else {
485
+ context.lineTo(SCREEN[0], SCREEN[1]);
486
+ }
487
+ }
488
+
489
+ if (ok) {
490
+ context.stroke();
491
+ }
492
+ }
493
+ }
494
+ }
495
+
496
+ /**
497
+ * The orientation the write-back reconstructed, as a short axis triple. §11.1's
498
+ * "rotation derived from the direction to the child" is only visible here.
499
+ */
500
+ function draw_frame(entity, width, height) {
501
+ const t = scene.ecd.getComponent(entity, Transform64);
502
+
503
+ if (t === undefined) {
504
+ return;
505
+ }
506
+
507
+ const length = control.spacing * 0.6;
508
+
509
+ const axes = [
510
+ [length, 0, 0, '#e5533d'],
511
+ [0, length, 0, '#5ad19a'],
512
+ [0, 0, length, '#6fa8dc']
513
+ ];
514
+
515
+ const origin_depth = project(
516
+ SCREEN, camera,
517
+ t[TRANSFORM64_TRANSLATION_OFFSET],
518
+ t[TRANSFORM64_TRANSLATION_OFFSET + 1],
519
+ t[TRANSFORM64_TRANSLATION_OFFSET + 2],
520
+ width, height
521
+ );
522
+
523
+ if (origin_depth <= 0.01) {
524
+ return;
525
+ }
526
+
527
+ const ox = SCREEN[0];
528
+ const oy = SCREEN[1];
529
+
530
+ for (const [ax, ay, az, colour] of axes) {
531
+ v3_quaternion_apply(
532
+ POINT, 0, ax, ay, az,
533
+ t[TRANSFORM64_ROTATION_OFFSET],
534
+ t[TRANSFORM64_ROTATION_OFFSET + 1],
535
+ t[TRANSFORM64_ROTATION_OFFSET + 2],
536
+ t[TRANSFORM64_ROTATION_OFFSET + 3]
537
+ );
538
+
539
+ const depth = project(
540
+ SCREEN, camera,
541
+ t[TRANSFORM64_TRANSLATION_OFFSET] + POINT[0],
542
+ t[TRANSFORM64_TRANSLATION_OFFSET + 1] + POINT[1],
543
+ t[TRANSFORM64_TRANSLATION_OFFSET + 2] + POINT[2],
544
+ width, height
545
+ );
546
+
547
+ if (depth <= 0.01) {
548
+ continue;
549
+ }
550
+
551
+ context.strokeStyle = colour;
552
+ context.lineWidth = 1;
553
+ context.beginPath();
554
+ context.moveTo(ox, oy);
555
+ context.lineTo(SCREEN[0], SCREEN[1]);
556
+ context.stroke();
557
+ }
558
+ }
559
+
560
+ function draw_marker(entity, width, height, colour, radius) {
561
+ const t = scene.ecd.getComponent(entity, Transform64);
562
+
563
+ if (t === undefined) {
564
+ return;
565
+ }
566
+
567
+ const depth = project(
568
+ SCREEN, camera,
569
+ t[TRANSFORM64_TRANSLATION_OFFSET],
570
+ t[TRANSFORM64_TRANSLATION_OFFSET + 1],
571
+ t[TRANSFORM64_TRANSLATION_OFFSET + 2],
572
+ width, height
573
+ );
574
+
575
+ if (depth <= 0.01) {
576
+ return;
577
+ }
578
+
579
+ context.fillStyle = colour;
580
+ context.beginPath();
581
+ context.arc(SCREEN[0], SCREEN[1], radius, 0, Math.PI * 2);
582
+ context.fill();
583
+ }
584
+
585
+ function draw_ground(width, height) {
586
+ const step = Math.max(control.spacing * 4, 0.1);
587
+
588
+ context.strokeStyle = 'rgba(139, 147, 159, 0.16)';
589
+ context.lineWidth = 1;
590
+
591
+ const extent = 8;
592
+
593
+ for (let i = -extent; i <= extent; i++) {
594
+ for (const [ax, az, bx, bz] of [
595
+ [i * step, -extent * step, i * step, extent * step],
596
+ [-extent * step, i * step, extent * step, i * step]
597
+ ]) {
598
+ const a = project(SCREEN, camera, ax, -control.links * control.spacing * 1.15, az, width, height);
599
+
600
+ const x0 = SCREEN[0];
601
+ const y0 = SCREEN[1];
602
+
603
+ const b = project(SCREEN, camera, bx, -control.links * control.spacing * 1.15, bz, width, height);
604
+
605
+ if (a <= 0.01 || b <= 0.01) {
606
+ continue;
607
+ }
608
+
609
+ context.beginPath();
610
+ context.moveTo(x0, y0);
611
+ context.lineTo(SCREEN[0], SCREEN[1]);
612
+ context.stroke();
613
+ }
614
+ }
615
+ }
616
+
617
+ // ---------------------------------------------------------------- readout
618
+
619
+ function refresh_readout() {
620
+ element('ms').textContent = last_ms.toFixed(3);
621
+
622
+ if (timing.length > 0) {
623
+ const sorted = timing.slice().sort(number_compare_ascending);
624
+
625
+ element('ms_min').textContent = `(min ${sorted[0].toFixed(3)})`;
626
+
627
+ const bar = element('budget');
628
+
629
+ bar.classList.toggle('over', sorted[0] > BUDGET_MS);
630
+ bar.firstElementChild.style.width = `${Math.min(100, sorted[0] / BUDGET_MS * 100)}%`;
631
+ }
632
+
633
+ let particles = 0;
634
+ let stretch = 0;
635
+ let bend = 0;
636
+ let leash = 0;
637
+ let colours = 0;
638
+ let tether = 0;
639
+ let speed = 0;
640
+ let teleports = 0;
641
+ let asleep = 0;
642
+ let seeded = 0;
643
+ let trouble = "";
644
+
645
+ for (const anchor of scene.anchor) {
646
+ const instance = scene.cloth.instanceOf(anchor);
647
+
648
+ if (instance === undefined || !instance.seeded) {
649
+ continue;
650
+ }
651
+
652
+ seeded++;
653
+
654
+ const state = instance.state;
655
+
656
+ particles += state.particle_count;
657
+ stretch += state.stretch_count;
658
+ bend += state.bend_count;
659
+ leash += state.leash_count;
660
+ colours = Math.max(colours, state.colour_count);
661
+ teleports += instance.teleport_count;
662
+
663
+ if (instance.asleep) {
664
+ asleep++;
665
+ }
666
+
667
+ if (trouble === "") {
668
+ trouble = find_non_finite_cloth_state(state);
669
+ }
670
+
671
+ tether = Math.max(tether, cloth_worst_tether_ratio(
672
+ state.position, instance.particle_of_node, instance.geodesic, instance.node_count
673
+ ));
674
+
675
+ for (let i = 0; i < state.particle_count; i++) {
676
+ const p = i * 3;
677
+
678
+ speed = Math.max(speed, Math.hypot(state.velocity[p], state.velocity[p + 1], state.velocity[p + 2]));
679
+ }
680
+ }
681
+
682
+ element('counts').textContent = `${seeded} / ${particles}`;
683
+ element('rows').textContent = `${stretch} / ${bend} / ${leash}`;
684
+ element('colours').textContent = String(colours);
685
+ element('tether').textContent = tether.toFixed(4);
686
+ element('speed').textContent = `${speed.toFixed(2)} m/s`;
687
+ element('teleports').textContent = String(teleports);
688
+
689
+ const sleep_cell = element('asleep');
690
+
691
+ sleep_cell.textContent = asleep === 0 ? 'no' : `${asleep} of ${seeded}`;
692
+ sleep_cell.className = asleep > 0 ? 'yes' : '';
693
+
694
+ if (trouble !== "" && element('tripwire').className !== 'bad') {
695
+ element('tripwire').textContent = trouble.slice(0, 90);
696
+ element('tripwire').className = 'bad';
697
+ }
698
+ }
699
+
700
+ // ---------------------------------------------------------------- controls
701
+
702
+ function build_rig_buttons() {
703
+ const host = element('rigs');
704
+
705
+ for (const rig of CLOTH_RIGS) {
706
+ const button = document.createElement('button');
707
+
708
+ button.innerHTML = `${rig.label}<small>${rig.blurb}</small>`;
709
+
710
+ button.addEventListener('click', () => {
711
+ control.rig = rig.id;
712
+
713
+ adopt_library(rig.dynamics);
714
+
715
+ sync_inputs();
716
+ rebuild();
717
+ });
718
+
719
+ host.appendChild(button);
720
+ }
721
+ }
722
+
723
+ function build_library_options() {
724
+ const select = element('dynamics');
725
+
726
+ for (const name of Object.keys(CLOTH_DYNAMICS_BY_NAME)) {
727
+ const option = document.createElement('option');
728
+
729
+ option.value = name;
730
+ option.textContent = name.toLowerCase();
731
+
732
+ select.appendChild(option);
733
+ }
734
+ }
735
+
736
+ /**
737
+ * @param {string} id
738
+ * @param {string} key
739
+ * @param {function(number): string} format
740
+ * @param {boolean} rebuilds
741
+ */
742
+ function bind_range(id, key, format, rebuilds) {
743
+ const input = element(id);
744
+ const output = element(`${id}_out`);
745
+
746
+ const refresh = () => {
747
+ input.value = String(control[key]);
748
+ output.textContent = format(control[key]);
749
+ };
750
+
751
+ input.addEventListener('input', () => {
752
+ control[key] = Number(input.value);
753
+
754
+ output.textContent = format(control[key]);
755
+
756
+ if (rebuilds) {
757
+ rebuild();
758
+ } else {
759
+ apply_dynamics();
760
+ }
761
+ });
762
+
763
+ refresh();
764
+
765
+ return refresh;
766
+ }
767
+
768
+ const refreshers = [];
769
+
770
+ function sync_inputs() {
771
+ for (const refresh of refreshers) {
772
+ refresh();
773
+ }
774
+
775
+ element('dynamics').value = control.dynamics;
776
+ element('motion').value = control.motion;
777
+ }
778
+
779
+ function bind_controls() {
780
+ refreshers.push(bind_range('links', 'links', v => String(v), true));
781
+ refreshers.push(bind_range('spacing', 'spacing', v => `${(v * 100).toFixed(1)} cm`, true));
782
+
783
+ refreshers.push(bind_range('stretch', 'stretch', v => v.toFixed(2), false));
784
+ refreshers.push(bind_range('bend', 'bend', v => v.toFixed(2), false));
785
+ refreshers.push(bind_range('damping', 'damping', v => v.toFixed(2), false));
786
+ refreshers.push(bind_range('slack', 'slack', v => v.toFixed(2), false));
787
+ refreshers.push(bind_range('inertia', 'inertia', v => v.toFixed(2), false));
788
+ refreshers.push(bind_range('blend', 'blend', v => v.toFixed(2), false));
789
+ refreshers.push(bind_range('substeps', 'substeps', v => String(v), false));
790
+ refreshers.push(bind_range('iterations', 'iterations', v => String(v), false));
791
+
792
+ element('dynamics').addEventListener('change', (event) => {
793
+ adopt_library(event.target.value);
794
+
795
+ sync_inputs();
796
+ apply_dynamics();
797
+ });
798
+
799
+ element('motion').addEventListener('change', (event) => {
800
+ control.motion = event.target.value;
801
+ });
802
+
803
+ element('teleport').addEventListener('click', () => {
804
+ if (scene === null) {
805
+ return;
806
+ }
807
+
808
+ const transform = scene.ecd.getComponent(scene.anchor[0], Transform64);
809
+
810
+ transform[TRANSFORM64_TRANSLATION_OFFSET] += 1000;
811
+ transform.updateMatrix();
812
+
813
+ // Chase it, or there is nothing to look at.
814
+ camera.target[0] += 1000;
815
+ });
816
+
817
+ for (const [id, key] of [['keep', 'keep'], ['never_sleep', 'never_sleep']]) {
818
+ element(id).addEventListener('click', () => {
819
+ control[key] = !control[key];
820
+
821
+ element(id).classList.toggle('on', control[key]);
822
+
823
+ apply_dynamics();
824
+ });
825
+ }
826
+
827
+ for (const [id, view] of [['view_links', 'links'], ['view_animated', 'animated'], ['view_frames', 'frames']]) {
828
+ element(id).addEventListener('click', () => {
829
+ control.view = view;
830
+
831
+ for (const other of ['view_links', 'view_animated', 'view_frames']) {
832
+ element(other).classList.toggle('on', other === id);
833
+ }
834
+ });
835
+ }
836
+
837
+ element('pause').addEventListener('click', () => {
838
+ control.running = !control.running;
839
+
840
+ element('pause').textContent = control.running ? 'pause' : 'run';
841
+ });
842
+
843
+ element('single').addEventListener('click', () => step_once());
844
+
845
+ element('reset').addEventListener('click', () => rebuild());
846
+
847
+ element('poke').addEventListener('click', () => {
848
+ if (scene === null) {
849
+ return;
850
+ }
851
+
852
+ for (const anchor of scene.anchor) {
853
+ const instance = scene.cloth.instanceOf(anchor);
854
+
855
+ if (instance === undefined || !instance.seeded) {
856
+ continue;
857
+ }
858
+
859
+ scene.cloth.wake(anchor);
860
+
861
+ for (let i = 0; i < instance.state.particle_count; i++) {
862
+ instance.state.velocity[i * 3] += (Math.random() - 0.5) * 6;
863
+ instance.state.velocity[i * 3 + 2] += (Math.random() - 0.5) * 6;
864
+ }
865
+ }
866
+ });
867
+ }
868
+
869
+ // ---------------------------------------------------------------- camera
870
+
871
+ let dragging = false;
872
+ let panning = false;
873
+ let last_x = 0;
874
+ let last_y = 0;
875
+
876
+ canvas.addEventListener('pointerdown', (event) => {
877
+ dragging = true;
878
+ panning = event.shiftKey || event.button === 2;
879
+
880
+ last_x = event.clientX;
881
+ last_y = event.clientY;
882
+
883
+ canvas.classList.add('grabbing');
884
+ canvas.setPointerCapture(event.pointerId);
885
+ });
886
+
887
+ canvas.addEventListener('pointermove', (event) => {
888
+ if (!dragging) {
889
+ return;
890
+ }
891
+
892
+ const dx = event.clientX - last_x;
893
+ const dy = event.clientY - last_y;
894
+
895
+ last_x = event.clientX;
896
+ last_y = event.clientY;
897
+
898
+ if (panning) {
899
+ const scale = camera.distance * 0.0016;
900
+
901
+ camera.target[0] -= (dx * Math.cos(camera.yaw)) * scale;
902
+ camera.target[2] += (dx * Math.sin(camera.yaw)) * scale;
903
+ camera.target[1] += dy * scale;
904
+ } else {
905
+ camera.yaw -= dx * 0.006;
906
+ camera.pitch = Math.max(-1.4, Math.min(1.4, camera.pitch + dy * 0.006));
907
+ }
908
+ });
909
+
910
+ canvas.addEventListener('pointerup', (event) => {
911
+ dragging = false;
912
+
913
+ canvas.classList.remove('grabbing');
914
+ canvas.releasePointerCapture(event.pointerId);
915
+ });
916
+
917
+ canvas.addEventListener('wheel', (event) => {
918
+ event.preventDefault();
919
+
920
+ camera.distance = Math.max(0.05, camera.distance * Math.exp(event.deltaY * 0.001));
921
+ }, { passive: false });
922
+
923
+ canvas.addEventListener('contextmenu', (event) => event.preventDefault());
924
+
925
+ // ---------------------------------------------------------------- start
926
+
927
+ build_rig_buttons();
928
+ build_library_options();
929
+ adopt_library('ROPE');
930
+ bind_controls();
931
+ sync_inputs();
932
+
933
+ const ready = rebuild();
934
+
935
+ globalThis.cloth_rig_playground = {
936
+ control,
937
+ camera,
938
+ rebuild,
939
+ step_once,
940
+ render,
941
+ ready,
942
+ get scene() {
943
+ return scene;
944
+ }
945
+ };
946
+
947
+ requestAnimationFrame(frame);