@woosh/meep-engine 3.44.0 → 3.45.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 (233) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/collection/array/array_sort_insertion_ascending.d.ts +19 -0
  4. package/src/core/collection/array/array_sort_insertion_ascending.d.ts.map +1 -0
  5. package/src/core/collection/array/array_sort_insertion_ascending.js +31 -0
  6. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.d.ts +27 -0
  7. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.d.ts.map +1 -0
  8. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.js +39 -0
  9. package/src/core/geom/3d/aabb/aabb3_from_v3_array.d.ts.map +1 -1
  10. package/src/core/geom/3d/aabb/aabb3_from_v3_array.js +4 -1
  11. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts +2 -2
  12. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -1
  13. package/src/core/geom/3d/aabb/aabb3_transform_oriented.js +1 -1
  14. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.d.ts +21 -0
  15. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.d.ts.map +1 -0
  16. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.js +36 -0
  17. package/src/core/geom/3d/pose/pose3_apply_point_array.d.ts +25 -0
  18. package/src/core/geom/3d/pose/pose3_apply_point_array.d.ts.map +1 -0
  19. package/src/core/geom/3d/pose/pose3_apply_point_array.js +40 -0
  20. package/src/core/geom/3d/pose/pose3_compose_array.d.ts +28 -0
  21. package/src/core/geom/3d/pose/pose3_compose_array.d.ts.map +1 -0
  22. package/src/core/geom/3d/pose/pose3_compose_array.js +41 -0
  23. package/src/core/geom/3d/pose/pose3_to_local_array.d.ts +29 -0
  24. package/src/core/geom/3d/pose/pose3_to_local_array.d.ts.map +1 -0
  25. package/src/core/geom/3d/pose/pose3_to_local_array.js +42 -0
  26. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.d.ts +28 -0
  27. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.d.ts.map +1 -0
  28. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.js +66 -0
  29. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.d.ts +22 -0
  30. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.d.ts.map +1 -0
  31. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.js +30 -0
  32. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.d.ts +23 -0
  33. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.d.ts.map +1 -0
  34. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.js +31 -0
  35. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.d.ts +26 -0
  36. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.d.ts.map +1 -0
  37. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.js +46 -0
  38. package/src/core/geom/vec3/v3_distance_array.d.ts +11 -0
  39. package/src/core/geom/vec3/v3_distance_array.d.ts.map +1 -0
  40. package/src/core/geom/vec3/v3_distance_array.js +19 -0
  41. package/src/core/geom/vec3/v3_normalize_array_checked.d.ts +23 -0
  42. package/src/core/geom/vec3/v3_normalize_array_checked.d.ts.map +1 -0
  43. package/src/core/geom/vec3/v3_normalize_array_checked.js +49 -0
  44. package/src/core/geom/vec3/v3_reject_array.d.ts +15 -0
  45. package/src/core/geom/vec3/v3_reject_array.d.ts.map +1 -0
  46. package/src/core/geom/vec3/v3_reject_array.js +32 -0
  47. package/src/core/graph/csr/csr_build_incidence.d.ts +26 -0
  48. package/src/core/graph/csr/csr_build_incidence.d.ts.map +1 -0
  49. package/src/core/graph/csr/csr_build_incidence.js +71 -0
  50. package/src/core/graph/csr/csr_graph_color_greedy.d.ts +25 -0
  51. package/src/core/graph/csr/csr_graph_color_greedy.d.ts.map +1 -0
  52. package/src/core/graph/csr/csr_graph_color_greedy.js +83 -0
  53. package/src/core/graph/csr/csr_graph_validate_coloring.d.ts +15 -0
  54. package/src/core/graph/csr/csr_graph_validate_coloring.d.ts.map +1 -0
  55. package/src/core/graph/csr/csr_graph_validate_coloring.js +33 -0
  56. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.d.ts +26 -0
  57. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.d.ts.map +1 -0
  58. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.js +71 -0
  59. package/src/core/math/max2_exact.d.ts +15 -0
  60. package/src/core/math/max2_exact.d.ts.map +1 -0
  61. package/src/core/math/max2_exact.js +30 -0
  62. package/src/core/math/min2_exact.d.ts +20 -0
  63. package/src/core/math/min2_exact.d.ts.map +1 -0
  64. package/src/core/math/min2_exact.js +35 -0
  65. package/src/engine/ecs/ik/IKGoalSystem.d.ts.map +1 -1
  66. package/src/engine/ecs/ik/IKGoalSystem.js +3 -19
  67. package/src/engine/ecs/parent/parent_entity_model_of.d.ts +17 -0
  68. package/src/engine/ecs/parent/parent_entity_model_of.d.ts.map +1 -0
  69. package/src/engine/ecs/parent/parent_entity_model_of.js +26 -0
  70. package/src/engine/ecs/transform/t64_read_pose_array.d.ts +15 -0
  71. package/src/engine/ecs/transform/t64_read_pose_array.d.ts.map +1 -0
  72. package/src/engine/ecs/transform/t64_read_pose_array.js +24 -0
  73. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts +2 -0
  74. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts.map +1 -1
  75. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.js +10 -21
  76. package/src/engine/graphics3/AnimationGraphSystem.d.ts +5 -0
  77. package/src/engine/graphics3/AnimationGraphSystem.d.ts.map +1 -1
  78. package/src/engine/graphics3/AnimationGraphSystem.js +48 -2
  79. package/src/engine/graphics3/AnimationSystem.d.ts +5 -0
  80. package/src/engine/graphics3/AnimationSystem.d.ts.map +1 -1
  81. package/src/engine/graphics3/AnimationSystem.js +47 -2
  82. package/src/engine/graphics3/register_clips.d.ts +29 -0
  83. package/src/engine/graphics3/register_clips.d.ts.map +1 -1
  84. package/src/engine/graphics3/register_clips.js +52 -0
  85. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts +9 -0
  86. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts.map +1 -1
  87. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +3 -1
  88. package/src/engine/physics/cloth/PLAN.md +10 -8
  89. package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts +22 -0
  90. package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts.map +1 -1
  91. package/src/engine/physics/cloth/build/cloth_bind_in_frame.js +51 -19
  92. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -1
  93. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +16 -2
  94. package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.d.ts.map +1 -1
  95. package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.js +19 -24
  96. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +7 -0
  97. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  98. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +85 -67
  99. package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts.map +1 -1
  100. package/src/engine/physics/cloth/collider/cloth_collider_bounds.js +19 -101
  101. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts +2 -5
  102. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts.map +1 -1
  103. package/src/engine/physics/cloth/collider/cloth_collider_pack.js +7 -15
  104. package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts.map +1 -1
  105. package/src/engine/physics/cloth/collider/cloth_collider_sdf.js +16 -61
  106. package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts +0 -1
  107. package/src/engine/physics/cloth/ecs/ClothColliderFlags.js +6 -19
  108. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts +1 -2
  109. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts.map +1 -1
  110. package/src/engine/physics/cloth/ecs/ClothColliderSystem.js +3 -12
  111. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  112. package/src/engine/physics/cloth/ecs/ClothWorld.js +40 -130
  113. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  114. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +17 -58
  115. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  116. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +3 -3
  117. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  118. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +44 -104
  119. package/src/engine/physics/cloth/ecs/cloth_geodesic.d.ts.map +1 -1
  120. package/src/engine/physics/cloth/ecs/cloth_geodesic.js +6 -13
  121. package/src/engine/physics/cloth/ecs/cloth_holds.d.ts.map +1 -1
  122. package/src/engine/physics/cloth/ecs/cloth_holds.js +15 -40
  123. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts.map +1 -1
  124. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.js +45 -79
  125. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  126. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +7 -27
  127. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  128. package/src/engine/physics/cloth/ecs/cloth_write_back.js +30 -62
  129. package/src/engine/physics/cloth/playground/cloth_playground_build.d.ts.map +1 -1
  130. package/src/engine/physics/cloth/playground/cloth_playground_build.js +2 -3
  131. package/src/engine/physics/cloth/playground/collide_main.js +2 -28
  132. package/src/engine/physics/cloth/playground/garment_main.js +4 -19
  133. package/src/engine/physics/cloth/playground/main.js +6 -48
  134. package/src/engine/physics/cloth/playground/rig_main.js +947 -955
  135. package/src/engine/physics/cloth/playground/wind_main.js +8 -7
  136. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  137. package/src/engine/physics/cloth/solver/ClothState.js +16 -76
  138. package/src/engine/physics/cloth/solver/cloth_color_particles.d.ts +10 -8
  139. package/src/engine/physics/cloth/solver/cloth_color_particles.d.ts.map +1 -1
  140. package/src/engine/physics/cloth/solver/cloth_color_particles.js +30 -102
  141. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  142. package/src/engine/physics/cloth/solver/cloth_contact_find.js +3 -2
  143. package/src/engine/physics/cloth/solver/cloth_hold_line.d.ts.map +1 -1
  144. package/src/engine/physics/cloth/solver/cloth_hold_line.js +2 -14
  145. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.d.ts +21 -0
  146. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.d.ts.map +1 -0
  147. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.js +45 -0
  148. package/src/engine/physics/cloth/solver/cloth_particle_coupling.d.ts +32 -0
  149. package/src/engine/physics/cloth/solver/cloth_particle_coupling.d.ts.map +1 -0
  150. package/src/engine/physics/cloth/solver/cloth_particle_coupling.js +133 -0
  151. package/src/engine/physics/cloth/solver/cloth_particle_of_source.d.ts +16 -0
  152. package/src/engine/physics/cloth/solver/cloth_particle_of_source.d.ts.map +1 -0
  153. package/src/engine/physics/cloth/solver/cloth_particle_of_source.js +19 -0
  154. package/src/engine/physics/cloth/solver/cloth_self_find.d.ts.map +1 -1
  155. package/src/engine/physics/cloth/solver/cloth_self_find.js +6 -55
  156. package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts.map +1 -1
  157. package/src/engine/physics/cloth/solver/cloth_solve_vertex.js +30 -65
  158. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  159. package/src/engine/physics/cloth/solver/cloth_step.js +6 -28
  160. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -1
  161. package/src/engine/physics/cloth/solver/cloth_step_instance.js +2 -3
  162. package/src/engine/physics/cloth/solver/cloth_stretch_residual.d.ts +21 -0
  163. package/src/engine/physics/cloth/solver/cloth_stretch_residual.d.ts.map +1 -0
  164. package/src/engine/physics/cloth/solver/cloth_stretch_residual.js +54 -0
  165. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.d.ts +15 -0
  166. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.d.ts.map +1 -1
  167. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.js +28 -0
  168. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.d.ts +30 -0
  169. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.d.ts.map +1 -0
  170. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.js +56 -0
  171. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts.map +1 -1
  172. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.js +3 -17
  173. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +2 -1
  174. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -1
  175. package/src/engine/physics/cloth/wind/ClothFluidWind.js +7 -25
  176. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +3 -1
  177. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
  178. package/src/engine/physics/cloth/wind/cloth_sample_air.js +40 -62
  179. package/src/engine/physics/ecs/ContactProbeSystem.d.ts.map +1 -1
  180. package/src/engine/physics/ecs/ContactProbeSystem.js +2 -18
  181. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  182. package/src/engine/physics/ecs/PhysicsSystem.js +9 -50
  183. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.d.ts +24 -0
  184. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.d.ts.map +1 -0
  185. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.js +42 -0
  186. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.d.ts +25 -0
  187. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.d.ts.map +1 -0
  188. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.js +40 -0
  189. package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.d.ts.map +1 -1
  190. package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.js +40 -90
  191. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  192. package/src/engine/physics/narrowphase/capsule_contacts.js +3 -5
  193. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -1
  194. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +2 -14
  195. package/src/engine/physics/narrowphase/hull_sphere_contact.d.ts.map +1 -1
  196. package/src/engine/physics/narrowphase/hull_sphere_contact.js +2 -5
  197. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  198. package/src/engine/physics/narrowphase/narrowphase_step.js +13 -32
  199. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  200. package/src/engine/physics/narrowphase/sphere_box_contact.js +2 -5
  201. package/src/format/texture/basis/transcode/etc1s_to_astc.d.ts.map +1 -1
  202. package/src/format/texture/basis/transcode/etc1s_to_astc.js +2 -1
  203. package/src/shade/playground/image_decode_bench/main.js +2 -1
  204. package/src/shade/playground/skinned_blas_refit/diff_traced_against_rasterized.d.ts.map +1 -1
  205. package/src/shade/playground/skinned_blas_refit/diff_traced_against_rasterized.js +2 -1
  206. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
  207. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +2 -1
  208. package/src/shade/playground/ssr_variance/statistics.d.ts.map +1 -1
  209. package/src/shade/playground/ssr_variance/statistics.js +2 -1
  210. package/src/shade/playground/texture_encode/main.d.ts.map +1 -1
  211. package/src/shade/playground/texture_encode/main.js +2 -1
  212. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -1
  213. package/src/shade/playground/volumetrics_froxel/measure.js +2 -1
  214. package/src/shade/renderer/buffer/table/gpu_database_check_links.d.ts.map +1 -1
  215. package/src/shade/renderer/buffer/table/gpu_database_check_links.js +2 -1
  216. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts.map +1 -1
  217. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +3 -2
  218. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts.map +1 -1
  219. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.js +2 -1
  220. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts.map +1 -1
  221. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.js +2 -1
  222. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts.map +1 -1
  223. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.js +2 -1
  224. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts.map +1 -1
  225. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.js +3 -2
  226. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -1
  227. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.js +2 -1
  228. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -1
  229. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.js +2 -1
  230. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  231. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +3 -2
  232. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.d.ts.map +1 -1
  233. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.js +3 -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);