@woosh/meep-engine 3.22.0 → 3.23.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 (267) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts +118 -0
  3. package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts.map +1 -0
  4. package/src/core/geom/3d/hash-grid/PointHashGrid3.js +283 -0
  5. package/src/core/geom/3d/shape/BoxShape3D.d.ts.map +1 -1
  6. package/src/core/geom/3d/shape/BoxShape3D.js +6 -17
  7. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts.map +1 -1
  8. package/src/core/geom/3d/shape/CapsuleShape3D.js +3 -14
  9. package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -1
  10. package/src/core/geom/3d/shape/CylinderShape3D.js +3 -21
  11. package/src/core/geom/3d/shape/PlaneShape3D.d.ts +105 -0
  12. package/src/core/geom/3d/shape/PlaneShape3D.d.ts.map +1 -0
  13. package/src/core/geom/3d/shape/PlaneShape3D.js +251 -0
  14. package/src/core/geom/3d/shape/SphereShape3D.js +3 -3
  15. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  16. package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
  17. package/src/core/geom/3d/shape/json/type_adapters.d.ts +14 -0
  18. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  19. package/src/core/geom/3d/shape/json/type_adapters.js +15 -0
  20. package/src/core/geom/3d/shape/sdf/sdf3_box.d.ts +51 -0
  21. package/src/core/geom/3d/shape/sdf/sdf3_box.d.ts.map +1 -0
  22. package/src/core/geom/3d/shape/sdf/sdf3_box.js +113 -0
  23. package/src/core/geom/3d/shape/sdf/sdf3_capsule.d.ts +46 -0
  24. package/src/core/geom/3d/shape/sdf/sdf3_capsule.d.ts.map +1 -0
  25. package/src/core/geom/3d/shape/sdf/sdf3_capsule.js +82 -0
  26. package/src/core/geom/3d/shape/sdf/sdf3_cylinder.d.ts +60 -0
  27. package/src/core/geom/3d/shape/sdf/sdf3_cylinder.d.ts.map +1 -0
  28. package/src/core/geom/3d/shape/sdf/sdf3_cylinder.js +126 -0
  29. package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.d.ts +50 -0
  30. package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.d.ts.map +1 -0
  31. package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.js +53 -0
  32. package/src/core/geom/3d/shape/sdf/sdf3_plane.d.ts +52 -0
  33. package/src/core/geom/3d/shape/sdf/sdf3_plane.d.ts.map +1 -0
  34. package/src/core/geom/3d/shape/sdf/sdf3_plane.js +61 -0
  35. package/src/core/geom/3d/shape/sdf/sdf3_sphere.d.ts +41 -0
  36. package/src/core/geom/3d/shape/sdf/sdf3_sphere.d.ts.map +1 -0
  37. package/src/core/geom/3d/shape/sdf/sdf3_sphere.js +71 -0
  38. package/src/core/process/ConcurrencyGate.d.ts +103 -0
  39. package/src/core/process/ConcurrencyGate.d.ts.map +1 -0
  40. package/src/core/process/ConcurrencyGate.js +207 -0
  41. package/src/engine/asset/AssetManager.d.ts +10 -0
  42. package/src/engine/asset/AssetManager.d.ts.map +1 -1
  43. package/src/engine/asset/AssetManager.js +10 -0
  44. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +30 -13
  45. package/src/engine/graphics3/pose/collect_entity_playbacks.d.ts +26 -0
  46. package/src/engine/graphics3/pose/collect_entity_playbacks.d.ts.map +1 -0
  47. package/src/engine/graphics3/pose/collect_entity_playbacks.js +40 -0
  48. package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts.map +1 -1
  49. package/src/engine/graphics3/pose/query_entity_node_world_pose.js +74 -101
  50. package/src/engine/physics/cloth/MEASUREMENTS.md +1287 -4
  51. package/src/engine/physics/cloth/PLAN.md +1967 -1581
  52. package/src/engine/physics/cloth/build/ClothProxy.d.ts +147 -0
  53. package/src/engine/physics/cloth/build/ClothProxy.d.ts.map +1 -0
  54. package/src/engine/physics/cloth/build/ClothProxy.js +207 -0
  55. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts +70 -0
  56. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -0
  57. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +256 -0
  58. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +176 -0
  59. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -0
  60. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +435 -0
  61. package/src/engine/physics/cloth/collider/ClothColliderKind.d.ts +12 -0
  62. package/src/engine/physics/cloth/collider/ClothColliderKind.d.ts.map +1 -0
  63. package/src/engine/physics/cloth/collider/ClothColliderKind.js +38 -0
  64. package/src/engine/physics/cloth/collider/ClothColliderRecord.d.ts +120 -0
  65. package/src/engine/physics/cloth/collider/ClothColliderRecord.d.ts.map +1 -0
  66. package/src/engine/physics/cloth/collider/ClothColliderRecord.js +137 -0
  67. package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts +25 -0
  68. package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts.map +1 -0
  69. package/src/engine/physics/cloth/collider/cloth_collider_bounds.js +190 -0
  70. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts +44 -0
  71. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts.map +1 -0
  72. package/src/engine/physics/cloth/collider/cloth_collider_pack.js +173 -0
  73. package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts +94 -0
  74. package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts.map +1 -0
  75. package/src/engine/physics/cloth/collider/cloth_collider_sdf.js +275 -0
  76. package/src/engine/physics/cloth/ecs/ClothCollider.d.ts +126 -0
  77. package/src/engine/physics/cloth/ecs/ClothCollider.d.ts.map +1 -0
  78. package/src/engine/physics/cloth/ecs/ClothCollider.js +199 -0
  79. package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts +9 -0
  80. package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts.map +1 -0
  81. package/src/engine/physics/cloth/ecs/ClothColliderFlags.js +32 -0
  82. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts +114 -0
  83. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts.map +1 -0
  84. package/src/engine/physics/cloth/ecs/ClothColliderSystem.js +243 -0
  85. package/src/engine/physics/cloth/ecs/ClothDynamicsFlags.js +66 -57
  86. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +109 -1
  87. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  88. package/src/engine/physics/cloth/ecs/ClothInstance.js +479 -363
  89. package/src/engine/physics/cloth/ecs/ClothRig.d.ts +95 -0
  90. package/src/engine/physics/cloth/ecs/ClothRig.d.ts.map +1 -0
  91. package/src/engine/physics/cloth/ecs/ClothRig.js +140 -0
  92. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +30 -1
  93. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  94. package/src/engine/physics/cloth/ecs/ClothSystem.js +1021 -773
  95. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts +90 -0
  96. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -0
  97. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +374 -0
  98. package/src/engine/physics/cloth/ecs/cloth_dynamics_library.d.ts +32 -3
  99. package/src/engine/physics/cloth/ecs/cloth_dynamics_library.d.ts.map +1 -1
  100. package/src/engine/physics/cloth/ecs/cloth_dynamics_library.js +283 -254
  101. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +59 -0
  102. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -0
  103. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +328 -0
  104. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts +13 -0
  105. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts.map +1 -0
  106. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.js +288 -0
  107. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +58 -0
  108. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -0
  109. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +199 -0
  110. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +8 -63
  111. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  112. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +317 -645
  113. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts +61 -0
  114. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  115. package/src/engine/physics/cloth/ecs/cloth_write_back.js +212 -449
  116. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts +49 -0
  117. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts.map +1 -0
  118. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.js +115 -0
  119. package/src/engine/physics/cloth/playground/README.md +155 -1
  120. package/src/engine/physics/cloth/playground/collide.html +178 -0
  121. package/src/engine/physics/cloth/playground/collide_build.d.ts +95 -0
  122. package/src/engine/physics/cloth/playground/collide_build.d.ts.map +1 -0
  123. package/src/engine/physics/cloth/playground/collide_build.js +315 -0
  124. package/src/engine/physics/cloth/playground/collide_main.d.ts +2 -0
  125. package/src/engine/physics/cloth/playground/collide_main.d.ts.map +1 -0
  126. package/src/engine/physics/cloth/playground/collide_main.js +720 -0
  127. package/src/engine/physics/cloth/playground/garment.html +182 -0
  128. package/src/engine/physics/cloth/playground/garment_build.d.ts +78 -0
  129. package/src/engine/physics/cloth/playground/garment_build.d.ts.map +1 -0
  130. package/src/engine/physics/cloth/playground/garment_build.js +359 -0
  131. package/src/engine/physics/cloth/playground/garment_main.d.ts +2 -0
  132. package/src/engine/physics/cloth/playground/garment_main.d.ts.map +1 -0
  133. package/src/engine/physics/cloth/playground/garment_main.js +633 -0
  134. package/src/engine/physics/cloth/solver/ClothState.d.ts +208 -5
  135. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  136. package/src/engine/physics/cloth/solver/ClothState.js +302 -8
  137. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +87 -0
  138. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -0
  139. package/src/engine/physics/cloth/solver/cloth_contact_find.js +343 -0
  140. package/src/engine/physics/cloth/solver/cloth_self_find.d.ts +112 -0
  141. package/src/engine/physics/cloth/solver/cloth_self_find.d.ts.map +1 -0
  142. package/src/engine/physics/cloth/solver/cloth_self_find.js +448 -0
  143. package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts +4 -1
  144. package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts.map +1 -1
  145. package/src/engine/physics/cloth/solver/cloth_solve_vertex.js +16 -1
  146. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +112 -5
  147. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  148. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +117 -6
  149. package/src/engine/physics/cloth/solver/cloth_step.d.ts +40 -9
  150. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  151. package/src/engine/physics/cloth/solver/cloth_step.js +122 -10
  152. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +95 -0
  153. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -0
  154. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +285 -0
  155. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts +129 -0
  156. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts.map +1 -0
  157. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.js +501 -0
  158. package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts +75 -0
  159. package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts.map +1 -0
  160. package/src/engine/physics/cloth/solver/constraint/cloth_self_row.js +202 -0
  161. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  162. package/src/shade/playground/vgeo_viewer/README.md +18 -0
  163. package/src/shade/playground/vgeo_viewer/main.js +87 -50
  164. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
  165. package/src/shade/renderer/buffer/table/GPUDatabase.js +191 -62
  166. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts +94 -2
  167. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  168. package/src/shade/renderer/buffer/table/GPUTypedTable.js +414 -2
  169. package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.d.ts +14 -0
  170. package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.d.ts.map +1 -0
  171. package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.js +13 -0
  172. package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.d.ts +17 -0
  173. package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.d.ts.map +1 -0
  174. package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.js +116 -0
  175. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts +9 -4
  176. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  177. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +843 -734
  178. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts +12 -0
  179. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts.map +1 -1
  180. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.js +13 -0
  181. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +17 -0
  182. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  183. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +185 -30
  184. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +17 -0
  185. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +141 -24
  186. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +204 -64
  187. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +110 -10
  188. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +19 -0
  189. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts +19 -6
  190. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts.map +1 -1
  191. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.js +26 -7
  192. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts +173 -14
  193. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  194. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +458 -51
  195. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts +49 -13
  196. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts.map +1 -1
  197. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.js +52 -14
  198. package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.d.ts +20 -0
  199. package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.d.ts.map +1 -0
  200. package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.js +19 -0
  201. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.d.ts +27 -0
  202. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.d.ts.map +1 -0
  203. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.js +48 -0
  204. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.d.ts +29 -0
  205. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.d.ts.map +1 -0
  206. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.js +60 -0
  207. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts +8 -1
  208. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts.map +1 -1
  209. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js +32 -2
  210. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +258 -0
  211. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +1 -0
  212. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.js +723 -0
  213. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +49 -0
  214. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +1 -0
  215. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.js +50 -0
  216. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  217. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  218. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.d.ts +3 -0
  219. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.d.ts.map +1 -1
  220. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.js +121 -21
  221. package/src/shade/renderer/texture/virtual/README.md +6 -4
  222. package/src/shade/renderer/texture/virtual/VirtualTextureManager.d.ts.map +1 -1
  223. package/src/shade/renderer/texture/virtual/VirtualTextureManager.js +7 -0
  224. package/src/shade/renderer/texture/virtual/source/VTSourceTiled.d.ts +3 -3
  225. package/src/shade/renderer/texture/virtual/source/VTSourceTiled.js +3 -3
  226. package/src/engine/ecs/system/SystemExecutionPhase.d.ts +0 -9
  227. package/src/engine/ecs/system/SystemExecutionPhase.d.ts.map +0 -1
  228. package/src/engine/graphics/texture/virtual/NOTES.md +0 -128
  229. package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.d.ts +0 -6
  230. package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.d.ts.map +0 -1
  231. package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.js +0 -5
  232. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts +0 -49
  233. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts.map +0 -1
  234. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.js +0 -264
  235. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts +0 -44
  236. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts.map +0 -1
  237. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.js +0 -228
  238. package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.d.ts +0 -3
  239. package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.d.ts.map +0 -1
  240. package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.js +0 -73
  241. package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.d.ts +0 -14
  242. package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.d.ts.map +0 -1
  243. package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.js +0 -85
  244. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts +0 -12
  245. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts.map +0 -1
  246. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.js +0 -63
  247. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts +0 -12
  248. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts.map +0 -1
  249. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.js +0 -243
  250. package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.d.ts +0 -30
  251. package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.d.ts.map +0 -1
  252. package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.js +0 -39
  253. package/src/engine/graphics/texture/virtual/tile/compose_finger_print.d.ts +0 -9
  254. package/src/engine/graphics/texture/virtual/tile/compose_finger_print.d.ts.map +0 -1
  255. package/src/engine/graphics/texture/virtual/tile/compose_finger_print.js +0 -24
  256. package/src/engine/graphics/texture/virtual/tile/compose_tile_address.d.ts +0 -9
  257. package/src/engine/graphics/texture/virtual/tile/compose_tile_address.d.ts.map +0 -1
  258. package/src/engine/graphics/texture/virtual/tile/compose_tile_address.js +0 -26
  259. package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.d.ts +0 -11
  260. package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.d.ts.map +0 -1
  261. package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.js +0 -12
  262. package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.d.ts +0 -7
  263. package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.d.ts.map +0 -1
  264. package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.js +0 -16
  265. package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.d.ts +0 -7
  266. package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.d.ts.map +0 -1
  267. package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.js +0 -35
@@ -1,773 +1,1021 @@
1
- import { assert } from "../../../../core/assert.js";
2
- import { quat3_multiply } from "../../../../core/geom/3d/quaternion/quat3_multiply.js";
3
- import { quat3_nlerp } from "../../../../core/geom/3d/quaternion/quat3_nlerp.js";
4
- import { v3_quaternion_apply } from "../../../../core/geom/vec3/v3_quaternion_apply.js";
5
- import { v3_quaternion_apply_inverse } from "../../../../core/geom/vec3/v3_quaternion_apply_inverse.js";
6
- import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
7
- import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
8
- import { System } from "../../../ecs/System.js";
9
- import {
10
- TransformAttachment
11
- } from "../../../ecs/transform-attachment/TransformAttachment.js";
12
- import {
13
- TransformAttachmentSystem
14
- } from "../../../ecs/transform-attachment/TransformAttachmentSystem.js";
15
- import {
16
- TRANSFORM64_ROTATION_OFFSET,
17
- TRANSFORM64_TRANSLATION_OFFSET,
18
- Transform64
19
- } from "../../../ecs/transform/Transform64.js";
20
- import { cloth_step } from "../solver/cloth_step.js";
21
- import { Cloth } from "./Cloth.js";
22
- import { ClothExclude } from "./ClothExclude.js";
23
- import { ClothFlags } from "./ClothFlags.js";
24
- import { ClothInstance } from "./ClothInstance.js";
25
- import {
26
- CLOTH_ANCHOR_ROTATION_LIMIT,
27
- CLOTH_ANCHOR_TRAVEL_REACH,
28
- CLOTH_SLEEP_SPEED_RATIO,
29
- CLOTH_SLEEP_STEPS,
30
- CLOTH_STABILISE_STEPS,
31
- CLOTH_TELEPORT_FLOOR,
32
- CLOTH_TELEPORT_REACH,
33
- cloth_velocity_limit,
34
- cloth_velocity_retention
35
- } from "./cloth_dynamics_map.js";
36
- import {
37
- cloth_dynamics_change_needs_reseed,
38
- cloth_reparameterise,
39
- cloth_seed_subtree
40
- } from "./cloth_seed_subtree.js";
41
- import { cloth_write_back } from "./cloth_write_back.js";
42
-
43
- /**
44
- * # The system — gather, step, write back
45
- *
46
- * One {@link ClothInstance} per {@link Cloth} component, stepped at the fixed
47
- * rate. Per step, per cloth: move the local frame with the anchor, point the
48
- * leash rows at where they measure from, step, and put the result back on the
49
- * subtree. The animated pose the leashes are measured against is not recomposed
50
- * per step — in the entity path it is constant in the anchor's own frame, and
51
- * `cloth_seed_subtree.js` bakes it.
52
- *
53
- * ## Where in the frame this runs
54
- *
55
- * Wherever the derived order puts it, and **nothing here depends on that**.
56
- *
57
- * The engine has two ways to order systems — registration order, and what each
58
- * system declares in `components_used` — and neither can express "after
59
- * whatever wrote this transform, before whatever reads it", because a system
60
- * that reads and writes one component is on both sides of that component's own
61
- * rule. Cloth is such a system: it reads a world transform and writes world
62
- * transforms. So rather than reach for a third mechanism, the write-back is
63
- * built so the order does not matter: it writes each node's **local** transform
64
- * alongside its world one, which makes a hierarchy recomposition reproduce the
65
- * simulated pose instead of replacing it with the authored one
66
- * (`cloth_write_back.js`).
67
- *
68
- * What is left is a **quality** question rather than a correctness one, and it
69
- * is smaller than it looks. Running before the system that poses the anchor
70
- * means reading last step's anchor pose, so the cloth's *shape* is one fixed
71
- * step stale; its *position* is not, because the attachment cascade carries the
72
- * whole subtree rigidly with the anchor from cloth's own local transforms.
73
- * `ClothSystem.spec.js` measures that lag rather than asserting a placement,
74
- * and asserts the property that actually matters: the pose survives being
75
- * recomposed.
76
- *
77
- * The declaration is the honest one — cloth reads `Cloth` and `ClothExclude`,
78
- * and reads and writes both `Transform64` and `TransformAttachment`. That puts
79
- * it before `TransformAttachmentSystem`, which is right: cloth writes the local
80
- * transforms and the attachment system is their reader.
81
- *
82
- * ## The local frame and the inertia model — §7 P7
83
- *
84
- * Particles live in the anchor's own frame. When the anchor moves, the cloth is
85
- * asked how much of that motion it would like to feel:
86
- * `ClothDynamics#inertia = 1` keeps every particle's **world** position and
87
- * velocity, so a cape trails a sprinting character; `0` keeps their **local**
88
- * ones, so the cloth rides the anchor as though it were standing still. The
89
- * blend between the two is a lerp in the local frame, which is all the frame
90
- * change is.
91
- *
92
- * The felt motion is limited before the blend, which is what makes "character
93
- * sprints, cape behaves" a default rather than a tuning exercise, and the limits
94
- * are derived from the cloth's own reach rather than authored.
95
- *
96
- * ## Teleport — §7 P6
97
- *
98
- * Anchor motion past a threshold is not locomotion and is not treated as any
99
- * amount of it. `ClothFlags.TeleportKeep` carries the shape across, which in a
100
- * local frame is literally doing nothing; the default re-seeds at the animated
101
- * pose with zero velocity and opens a stabilisation window in which the
102
- * velocity limit ramps back from nothing, so the first steps after a reset
103
- * cannot produce a whip.
104
- *
105
- * @author Alex Goldring
106
- * @copyright Company Named Limited (c) 2026
107
- */
108
- export class ClothSystem extends System {
109
-
110
- dependencies = [Cloth, Transform64];
111
-
112
- components_used = [
113
- ResourceAccessSpecification.from(Cloth, ResourceAccessKind.Read),
114
- ResourceAccessSpecification.from(Transform64, ResourceAccessKind.Read | ResourceAccessKind.Write),
115
- ResourceAccessSpecification.from(TransformAttachment, ResourceAccessKind.Read | ResourceAccessKind.Write),
116
- ResourceAccessSpecification.from(ClothExclude, ResourceAccessKind.Read)
117
- ];
118
-
119
- /**
120
- * World gravity, m/s². Rotated into each cloth's local frame per step.
121
- * @type {Float64Array}
122
- */
123
- gravity = new Float64Array([0, -9.81, 0]);
124
-
125
- /**
126
- * Live cloths, ordered so that an instance whose anchor sits inside another
127
- * instance's subtree steps after it.
128
- * @type {ClothInstance[]}
129
- */
130
- instances = [];
131
-
132
- /**
133
- * @type {Map<number, ClothInstance>}
134
- * @private
135
- */
136
- __by_entity = new Map();
137
-
138
- /**
139
- * @type {TransformAttachmentSystem|null}
140
- * @private
141
- */
142
- __attachments = null;
143
-
144
- /**
145
- * @type {boolean}
146
- * @private
147
- */
148
- __order_dirty = false;
149
-
150
- /**
151
- * @param {EntityManager} entityManager
152
- * @returns {Promise<void>}
153
- */
154
- async startup(entityManager) {
155
- this.__attachments = entityManager.getSystem(TransformAttachmentSystem);
156
-
157
- assert.notNull(
158
- this.__attachments,
159
- 'ClothSystem needs a TransformAttachmentSystem: the subtree walk is its parent-to-children index'
160
- );
161
- }
162
-
163
- /**
164
- * @param {Cloth} cloth
165
- * @param {Transform64} transform
166
- * @param {number} entity
167
- */
168
- link(cloth, transform, entity) {
169
- const instance = new ClothInstance();
170
-
171
- instance.entity = entity;
172
- instance.cloth = cloth;
173
- instance.transform = transform;
174
-
175
- this.instances.push(instance);
176
- this.__by_entity.set(entity, instance);
177
-
178
- this.__order_dirty = true;
179
- }
180
-
181
- /**
182
- * @param {Cloth} cloth
183
- * @param {Transform64} transform
184
- * @param {number} entity
185
- */
186
- unlink(cloth, transform, entity) {
187
- const instance = this.__by_entity.get(entity);
188
-
189
- if (instance === undefined) {
190
- return;
191
- }
192
-
193
- this.__by_entity.delete(entity);
194
-
195
- const index = this.instances.indexOf(instance);
196
-
197
- if (index >= 0) {
198
- this.instances.splice(index, 1);
199
- }
200
- }
201
-
202
- /**
203
- * Throw away a cloth's binding so the next step walks its subtree again.
204
- *
205
- * The walk is done once, because a rope's links do not come and go. When
206
- * they do — a chain gains a link, a rig is rebuilt — this is how you say so,
207
- * and it is the same contract `TransformAttachment#parent` already carries:
208
- * the structure is immutable while it is attached, and changing it is an
209
- * explicit act rather than something the engine polls for.
210
- *
211
- * @param {number} entity
212
- * @returns {void}
213
- */
214
- reseed(entity) {
215
- const instance = this.__by_entity.get(entity);
216
-
217
- if (instance === undefined) {
218
- return;
219
- }
220
-
221
- instance.seeded = false;
222
- instance.anchor_valid = false;
223
-
224
- this.__order_dirty = true;
225
- }
226
-
227
- /**
228
- * Wake a sleeping cloth. Anchor motion and a `ClothDynamics` change do this
229
- * on their own; a caller that has changed the world in some way the cloth
230
- * cannot see — M6's wind, for one — says so here.
231
- *
232
- * @param {number} entity
233
- * @returns {void}
234
- */
235
- wake(entity) {
236
- const instance = this.__by_entity.get(entity);
237
-
238
- if (instance !== undefined) {
239
- instance.asleep = false;
240
- instance.still_steps = 0;
241
- }
242
- }
243
-
244
- /**
245
- * @param {number} entity
246
- * @returns {ClothInstance|undefined}
247
- */
248
- instanceOf(entity) {
249
- return this.__by_entity.get(entity);
250
- }
251
-
252
- /**
253
- * @param {number} dt seconds
254
- */
255
- fixedUpdate(dt) {
256
- const ecd = this.entityManager.dataset;
257
-
258
- if (ecd === null || ecd === undefined) {
259
- return;
260
- }
261
-
262
- const instances = this.instances;
263
-
264
- if (this.__order_dirty) {
265
- this.#reorder(ecd);
266
- }
267
-
268
- for (let i = 0; i < instances.length; i++) {
269
- const instance = instances[i];
270
-
271
- if (!instance.seeded) {
272
- cloth_seed_subtree(instance, ecd, this.__attachments, dt);
273
- } else if (!instance.cloth.dynamics.equals(instance.dynamics_seen)) {
274
- if (cloth_dynamics_change_needs_reseed(instance.dynamics_seen, instance.cloth.dynamics)) {
275
- cloth_seed_subtree(instance, ecd, this.__attachments, dt);
276
- } else {
277
- cloth_reparameterise(instance, dt);
278
- }
279
-
280
- instance.asleep = false;
281
- instance.still_steps = 0;
282
- }
283
-
284
- this.#advance(instance, ecd, dt);
285
- }
286
- }
287
-
288
- /**
289
- * Depth-first order by hierarchy depth, so a nested cloth reads an anchor
290
- * the cloth above it has already written.
291
- *
292
- * @param {EntityComponentDataset} ecd
293
- * @private
294
- */
295
- #reorder(ecd) {
296
- const instances = this.instances;
297
-
298
- for (let i = 0; i < instances.length; i++) {
299
- const instance = instances[i];
300
-
301
- let depth = 0;
302
- let entity = instance.entity;
303
-
304
- // Bounded by the number of entities; a cycle in the attachment
305
- // graph is not this system's to diagnose, but it is this loop's to
306
- // survive.
307
- for (let guard = 0; guard < 4096; guard++) {
308
- const attachment = ecd.getComponent(entity, TransformAttachment);
309
-
310
- if (attachment === undefined) {
311
- break;
312
- }
313
-
314
- entity = attachment.parent;
315
- depth++;
316
- }
317
-
318
- instance.depth = depth;
319
- }
320
-
321
- instances.sort((a, b) => a.depth - b.depth);
322
-
323
- this.__order_dirty = false;
324
- }
325
-
326
- /**
327
- * @param {ClothInstance} instance
328
- * @param {EntityComponentDataset} ecd
329
- * @param {number} dt
330
- * @private
331
- */
332
- #advance(instance, ecd, dt) {
333
- const transform = instance.transform;
334
-
335
- const tx = transform[TRANSFORM64_TRANSLATION_OFFSET];
336
- const ty = transform[TRANSFORM64_TRANSLATION_OFFSET + 1];
337
- const tz = transform[TRANSFORM64_TRANSLATION_OFFSET + 2];
338
-
339
- const qx = transform[TRANSFORM64_ROTATION_OFFSET];
340
- const qy = transform[TRANSFORM64_ROTATION_OFFSET + 1];
341
- const qz = transform[TRANSFORM64_ROTATION_OFFSET + 2];
342
- const qw = transform[TRANSFORM64_ROTATION_OFFSET + 3];
343
-
344
- const previous_translation = instance.anchor_translation;
345
- const previous_rotation = instance.anchor_rotation;
346
-
347
- if (instance.cloth.blend !== instance.blend_seen) {
348
- instance.asleep = false;
349
- instance.still_steps = 0;
350
- }
351
-
352
- const moved = instance.anchor_valid && (
353
- previous_translation[0] !== tx
354
- || previous_translation[1] !== ty
355
- || previous_translation[2] !== tz
356
- || previous_rotation[0] !== qx
357
- || previous_rotation[1] !== qy
358
- || previous_rotation[2] !== qz
359
- || previous_rotation[3] !== qw
360
- );
361
-
362
- if (moved) {
363
- instance.asleep = false;
364
- instance.still_steps = 0;
365
- }
366
-
367
- if (instance.asleep) {
368
- // Nothing is stepped and nothing is written: the pose on the
369
- // subtree is already the pose, and rewriting it is what would make
370
- // §7 P5's "bit-identical across the following 300 steps" an
371
- // aspiration rather than a fact.
372
- return;
373
- }
374
-
375
- if (instance.anchor_valid) {
376
- const displacement = Math.hypot(tx - previous_translation[0], ty - previous_translation[1], tz - previous_translation[2]);
377
-
378
- const teleport = displacement > Math.max(
379
- CLOTH_TELEPORT_REACH * instance.reach,
380
- CLOTH_TELEPORT_FLOOR
381
- );
382
-
383
- if (teleport) {
384
- this.#teleport(instance);
385
- } else {
386
- move_local_frame(
387
- instance,
388
- tx, ty, tz, qx, qy, qz, qw,
389
- previous_translation, previous_rotation
390
- );
391
- }
392
- }
393
-
394
- previous_translation[0] = tx;
395
- previous_translation[1] = ty;
396
- previous_translation[2] = tz;
397
-
398
- previous_rotation[0] = qx;
399
- previous_rotation[1] = qy;
400
- previous_rotation[2] = qz;
401
- previous_rotation[3] = qw;
402
-
403
- instance.anchor_valid = true;
404
-
405
- refresh_leash_targets(instance);
406
-
407
- const dynamics = instance.cloth.dynamics;
408
-
409
- damp(instance, cloth_velocity_retention(dynamics.damping, dt));
410
-
411
- // Gravity is a world vector and the solve is in the anchor's frame.
412
- const gravity = this.gravity;
413
-
414
- v3_quaternion_apply_inverse(LOCAL_GRAVITY, 0, gravity[0], gravity[1], gravity[2], qx, qy, qz, qw);
415
-
416
- const state = instance.state;
417
-
418
- cloth_step(
419
- state, dt,
420
- LOCAL_GRAVITY[0], LOCAL_GRAVITY[1], LOCAL_GRAVITY[2],
421
- dynamics.substeps, dynamics.iterations
422
- );
423
-
424
- let limit = cloth_velocity_limit(
425
- instance.shortest_row, dynamics.substeps, dynamics.iterations, dt
426
- );
427
-
428
- if (instance.stabilise_remaining > 0) {
429
- // Ramp back from nothing over the window. The first step after a
430
- // reset is allowed no velocity at all, which is what stops a
431
- // re-seeded cloth from snapping taut and whipping.
432
- limit *= (CLOTH_STABILISE_STEPS - instance.stabilise_remaining) / CLOTH_STABILISE_STEPS;
433
-
434
- instance.stabilise_remaining--;
435
- }
436
-
437
- const speed = clamp_velocity(instance, limit);
438
-
439
- if ((instance.cloth.flags & ClothFlags.NeverSleep) === 0) {
440
- if (speed < CLOTH_SLEEP_SPEED_RATIO * instance.reach) {
441
- instance.still_steps++;
442
-
443
- if (instance.still_steps >= CLOTH_SLEEP_STEPS) {
444
- instance.asleep = true;
445
- }
446
- } else {
447
- instance.still_steps = 0;
448
- }
449
- }
450
-
451
- instance.blend_seen = instance.cloth.blend;
452
-
453
- cloth_write_back(instance, ecd);
454
- }
455
-
456
- /**
457
- * @param {ClothInstance} instance
458
- * @private
459
- */
460
- #teleport(instance) {
461
- instance.teleport_count++;
462
-
463
- if ((instance.cloth.flags & ClothFlags.TeleportKeep) !== 0) {
464
- // `Keep` carries the cloth's shape across the jump. Particles are
465
- // stored relative to the anchor, so that is exactly what happens if
466
- // this function does nothing to them — the frame moved and they did
467
- // not, which is MagicaCloth2's rigid delta expressed in the frame
468
- // that makes it free.
469
- return;
470
- }
471
-
472
- const state = instance.state;
473
-
474
- const node_count = instance.node_count;
475
- const animated = instance.animated_position;
476
- const particle_of_node = instance.particle_of_node;
477
-
478
- for (let node = 0; node < node_count; node++) {
479
- const p = particle_of_node[node] * 3;
480
- const n = node * 3;
481
-
482
- state.position[p] = animated[n];
483
- state.position[p + 1] = animated[n + 1];
484
- state.position[p + 2] = animated[n + 2];
485
- }
486
-
487
- state.position_previous.set(state.position);
488
- state.velocity.fill(0);
489
- state.inertia_blend.fill(0);
490
-
491
- // The multipliers describe forces in a configuration that no longer
492
- // exists. Carrying them across would apply the old pose's tension to
493
- // the new one on the first iteration, which is a whip with extra steps.
494
- state.stretch_lambda.fill(0);
495
- state.bend_lambda.fill(0);
496
- state.leash_lambda.fill(0);
497
-
498
- instance.stabilise_remaining = CLOTH_STABILISE_STEPS;
499
- }
500
- }
501
-
502
- /**
503
- * @type {Float64Array}
504
- * @private
505
- */
506
- const LOCAL_GRAVITY = new Float64Array(3);
507
-
508
- /**
509
- * @type {Float64Array}
510
- * @private
511
- */
512
- const FRAME = new Float64Array(8);
513
-
514
- /**
515
- * Carry the particles across a change of anchor pose, by however much of it the
516
- * fabric is willing to feel. §7 P7.
517
- *
518
- * A particle at local `p` was at world `t_prev + q_prev·p`. Holding that world
519
- * position fixed, its local coordinates become `D + Q·p` with
520
- * `D = q_now⁻¹·(t_prev − t_now)` and `Q = q_now⁻¹ ⊗ q_prev`. Feeling *all* of
521
- * the anchor's motion is taking that; feeling *none* is leaving `p` alone; and
522
- * `inertia` is the lerp between them. Velocity is carried the same way, minus
523
- * the translation, since a translation of the frame does not rotate a vector.
524
- *
525
- * `D` and `Q` are limited before the lerp. The limits are the cloth's own reach
526
- * per step and a fixed angle per step, which is what makes an anchor that
527
- * teleports a metre — or spins — something a cape *lags behind* rather than
528
- * something it is fired out of.
529
- *
530
- * @param {ClothInstance} instance
531
- * @param {number} tx
532
- * @param {number} ty
533
- * @param {number} tz
534
- * @param {number} qx
535
- * @param {number} qy
536
- * @param {number} qz
537
- * @param {number} qw
538
- * @param {Float64Array} previous_translation
539
- * @param {Float64Array} previous_rotation
540
- * @private
541
- */
542
- function move_local_frame(
543
- instance,
544
- tx, ty, tz, qx, qy, qz, qw,
545
- previous_translation, previous_rotation
546
- ) {
547
- let inertia = instance.cloth.dynamics.inertia;
548
-
549
- if (inertia <= 0) {
550
- return;
551
- }
552
-
553
- if (inertia > 1) {
554
- inertia = 1;
555
- }
556
-
557
- v3_quaternion_apply_inverse(
558
- FRAME, 0,
559
- previous_translation[0] - tx,
560
- previous_translation[1] - ty,
561
- previous_translation[2] - tz,
562
- qx, qy, qz, qw
563
- );
564
-
565
- const travel_limit = CLOTH_ANCHOR_TRAVEL_REACH * instance.reach;
566
-
567
- const travel = Math.hypot(FRAME[0], FRAME[1], FRAME[2]);
568
-
569
- if (travel > travel_limit && travel > 0) {
570
- const scale = travel_limit / travel;
571
-
572
- FRAME[0] *= scale;
573
- FRAME[1] *= scale;
574
- FRAME[2] *= scale;
575
- }
576
-
577
- quat3_multiply(
578
- FRAME, 4,
579
- -qx, -qy, -qz, qw,
580
- previous_rotation[0], previous_rotation[1], previous_rotation[2], previous_rotation[3]
581
- );
582
-
583
- // `|w|` is `cos(θ/2)`, so this is the angle test without an `acos`.
584
- const half_angle_cos = FRAME[7] < 0 ? -FRAME[7] : FRAME[7];
585
-
586
- if (half_angle_cos < Math.cos(CLOTH_ANCHOR_ROTATION_LIMIT * 0.5)) {
587
- const angle = 2 * Math.acos(half_angle_cos > 1 ? 1 : half_angle_cos);
588
-
589
- quat3_nlerp(
590
- FRAME, 4,
591
- 0, 0, 0, 1,
592
- FRAME[4], FRAME[5], FRAME[6], FRAME[7],
593
- CLOTH_ANCHOR_ROTATION_LIMIT / angle
594
- );
595
- }
596
-
597
- const state = instance.state;
598
-
599
- const particle_count = state.particle_count;
600
-
601
- const position = state.position;
602
- const velocity = state.velocity;
603
-
604
- const dx = FRAME[0];
605
- const dy = FRAME[1];
606
- const dz = FRAME[2];
607
-
608
- const rx = FRAME[4];
609
- const ry = FRAME[5];
610
- const rz = FRAME[6];
611
- const rw = FRAME[7];
612
-
613
- for (let i = 0; i < particle_count; i++) {
614
- const p = i * 3;
615
-
616
- v3_quaternion_apply(CARRIED, 0, position[p], position[p + 1], position[p + 2], rx, ry, rz, rw);
617
-
618
- position[p] += inertia * (CARRIED[0] + dx - position[p]);
619
- position[p + 1] += inertia * (CARRIED[1] + dy - position[p + 1]);
620
- position[p + 2] += inertia * (CARRIED[2] + dz - position[p + 2]);
621
-
622
- v3_quaternion_apply(CARRIED, 0, velocity[p], velocity[p + 1], velocity[p + 2], rx, ry, rz, rw);
623
-
624
- velocity[p] += inertia * (CARRIED[0] - velocity[p]);
625
- velocity[p + 1] += inertia * (CARRIED[1] - velocity[p + 1]);
626
- velocity[p + 2] += inertia * (CARRIED[2] - velocity[p + 2]);
627
- }
628
-
629
- // The anchor is pinned and is the origin of the frame it anchors, so it is
630
- // the one particle that must not be carried anywhere.
631
- const anchor = instance.particle_of_node[0] * 3;
632
-
633
- position[anchor] = 0;
634
- position[anchor + 1] = 0;
635
- position[anchor + 2] = 0;
636
-
637
- velocity[anchor] = 0;
638
- velocity[anchor + 1] = 0;
639
- velocity[anchor + 2] = 0;
640
- }
641
-
642
- /**
643
- * @type {Float64Array}
644
- * @private
645
- */
646
- const CARRIED = new Float64Array(3);
647
-
648
- /**
649
- * Point every leash row at where it measures from: a slack row at the
650
- * particle's animated position, a tether at the anchor.
651
- *
652
- * @param {ClothInstance} instance
653
- * @private
654
- */
655
- function refresh_leash_targets(instance) {
656
- const state = instance.state;
657
-
658
- const target = state.leash_target;
659
-
660
- const animated = instance.animated_position;
661
-
662
- const anchor = instance.particle_of_node[0] * 3;
663
-
664
- const anchor_x = state.position[anchor];
665
- const anchor_y = state.position[anchor + 1];
666
- const anchor_z = state.position[anchor + 2];
667
-
668
- const node_count = instance.node_count;
669
-
670
- const slack_row = instance.slack_row;
671
- const tether_row = instance.tether_row;
672
-
673
- for (let node = 1; node < node_count; node++) {
674
- const tether = tether_row[node];
675
-
676
- if (tether >= 0) {
677
- target[tether * 3] = anchor_x;
678
- target[tether * 3 + 1] = anchor_y;
679
- target[tether * 3 + 2] = anchor_z;
680
- }
681
-
682
- const slack = slack_row[node];
683
-
684
- if (slack >= 0) {
685
- target[slack * 3] = animated[node * 3];
686
- target[slack * 3 + 1] = animated[node * 3 + 1];
687
- target[slack * 3 + 2] = animated[node * 3 + 2];
688
- }
689
- }
690
- }
691
-
692
- /**
693
- * Scale every particle's velocity, which is §7 P5's other half.
694
- *
695
- * The multiplier decay `α·γ` bleeds accumulated force out of a **hard** row,
696
- * and a soft row has no multiplier to decay — M0 measured a soft cloth landing
697
- * in a bounded limit cycle that 2,000 further steps do not move. This is what
698
- * reaches it, and it is why `ClothDynamics#damping` is part of the correctness
699
- * argument rather than a taste control. `MEASUREMENTS.md` §6.
700
- *
701
- * @param {ClothInstance} instance
702
- * @param {number} retention `[0, 1]`
703
- * @private
704
- */
705
- function damp(instance, retention) {
706
- if (retention >= 1) {
707
- return;
708
- }
709
-
710
- const velocity = instance.state.velocity;
711
-
712
- for (let i = 0; i < velocity.length; i++) {
713
- velocity[i] *= retention;
714
- }
715
- }
716
-
717
- /**
718
- * Hold every particle's speed under the limit derived from
719
- * `ClothDynamics#thickness`, and report the largest speed left afterwards so
720
- * the sleep test has something to read.
721
- *
722
- * §7 P1 names this clamp and M0 deliberately left it out, because a clamp would
723
- * have masked the property M0 existed to measure. It is here rather than inside
724
- * `cloth_step` for the same reason: the solver's stability is still its own.
725
- *
726
- * Speed rather than displacement is what the sleep test reads, because
727
- * `position_previous` after a multi-substep step holds the start of the *last*
728
- * substep rather than the start of the step, and a motion measure that quietly
729
- * divides by the substep count is a sleep threshold that means something
730
- * different for every fabric.
731
- *
732
- * @param {ClothInstance} instance
733
- * @param {number} limit m/s
734
- * @returns {number} largest particle speed after clamping, m/s
735
- * @private
736
- */
737
- function clamp_velocity(instance, limit) {
738
- const state = instance.state;
739
-
740
- const particle_count = state.particle_count;
741
-
742
- const velocity = state.velocity;
743
-
744
- const limit_squared = limit * limit;
745
-
746
- let worst_squared = 0;
747
-
748
- for (let i = 0; i < particle_count; i++) {
749
- const p = i * 3;
750
-
751
- const vx = velocity[p];
752
- const vy = velocity[p + 1];
753
- const vz = velocity[p + 2];
754
-
755
- let speed_squared = vx * vx + vy * vy + vz * vz;
756
-
757
- if (speed_squared > limit_squared) {
758
- const scale = limit / Math.sqrt(speed_squared);
759
-
760
- velocity[p] = vx * scale;
761
- velocity[p + 1] = vy * scale;
762
- velocity[p + 2] = vz * scale;
763
-
764
- speed_squared = limit_squared;
765
- }
766
-
767
- if (speed_squared > worst_squared) {
768
- worst_squared = speed_squared;
769
- }
770
- }
771
-
772
- return Math.sqrt(worst_squared);
773
- }
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { quat3_multiply } from "../../../../core/geom/3d/quaternion/quat3_multiply.js";
3
+ import { quat3_nlerp } from "../../../../core/geom/3d/quaternion/quat3_nlerp.js";
4
+ import { v3_quaternion_apply } from "../../../../core/geom/vec3/v3_quaternion_apply.js";
5
+ import { v3_quaternion_apply_inverse } from "../../../../core/geom/vec3/v3_quaternion_apply_inverse.js";
6
+ import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
7
+ import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
8
+ import { System } from "../../../ecs/System.js";
9
+ import {
10
+ TransformAttachment
11
+ } from "../../../ecs/transform-attachment/TransformAttachment.js";
12
+ import {
13
+ TransformAttachmentSystem
14
+ } from "../../../ecs/transform-attachment/TransformAttachmentSystem.js";
15
+ import {
16
+ TRANSFORM64_ROTATION_OFFSET,
17
+ TRANSFORM64_TRANSLATION_OFFSET,
18
+ Transform64
19
+ } from "../../../ecs/transform/Transform64.js";
20
+ import {
21
+ collect_entity_playbacks
22
+ } from "../../../graphics3/pose/collect_entity_playbacks.js";
23
+ import { MeshSystem } from "../../../graphics3/MeshSystem.js";
24
+ import {
25
+ pose_evaluate_world
26
+ } from "../../../../shade/renderer/animation/pose/pose_evaluate_world.js";
27
+ import { cloth_contact_find } from "../solver/cloth_contact_find.js";
28
+ import { cloth_self_find } from "../solver/cloth_self_find.js";
29
+ import { CLOTH_SELF_RADIUS_MAX_RATIO, CLOTH_SELF_RADIUS_MIN_RATIO } from "../solver/cloth_solver_constants.js";
30
+ import { cloth_step } from "../solver/cloth_step.js";
31
+ import { Cloth } from "./Cloth.js";
32
+ import { ClothCollider } from "./ClothCollider.js";
33
+ import { ClothColliderSystem } from "./ClothColliderSystem.js";
34
+ import { ClothExclude } from "./ClothExclude.js";
35
+ import { ClothDynamicsFlags } from "./ClothDynamicsFlags.js";
36
+ import { ClothFlags } from "./ClothFlags.js";
37
+ import { ClothInstance } from "./ClothInstance.js";
38
+ import { ClothRig } from "./ClothRig.js";
39
+ import {
40
+ CLOTH_ANCHOR_ROTATION_LIMIT,
41
+ CLOTH_ANCHOR_TRAVEL_REACH,
42
+ CLOTH_SLEEP_SPEED_RATIO,
43
+ CLOTH_SLEEP_STEPS,
44
+ CLOTH_STABILISE_STEPS,
45
+ CLOTH_TELEPORT_FLOOR,
46
+ CLOTH_TELEPORT_REACH,
47
+ cloth_velocity_limit,
48
+ cloth_velocity_retention
49
+ } from "./cloth_dynamics_map.js";
50
+ import {
51
+ cloth_colliders_moved,
52
+ cloth_gather_colliders
53
+ } from "./cloth_gather_colliders.js";
54
+ import {
55
+ cloth_dynamics_change_needs_reseed,
56
+ cloth_reparameterise
57
+ } from "./cloth_build_rows.js";
58
+ import { cloth_seed_subtree } from "./cloth_seed_subtree.js";
59
+ import { cloth_seed_rig } from "./cloth_seed_rig.js";
60
+ import { cloth_write_back } from "./cloth_write_back.js";
61
+ import { cloth_write_back_rig } from "./cloth_write_back_rig.js";
62
+
63
+ /**
64
+ * # The system — gather, step, write back
65
+ *
66
+ * One {@link ClothInstance} per {@link Cloth} component, stepped at the fixed
67
+ * rate. Per step, per cloth: move the local frame with the anchor, point the
68
+ * leash rows at where they measure from, step, and put the result back on the
69
+ * subtree. The animated pose the leashes are measured against is not recomposed
70
+ * per step — in the entity path it is constant in the anchor's own frame, and
71
+ * `cloth_seed_subtree.js` bakes it.
72
+ *
73
+ * ## Where in the frame this runs
74
+ *
75
+ * Wherever the derived order puts it, and **nothing here depends on that**.
76
+ *
77
+ * The engine has two ways to order systems — registration order, and what each
78
+ * system declares in `components_used` — and neither can express "after
79
+ * whatever wrote this transform, before whatever reads it", because a system
80
+ * that reads and writes one component is on both sides of that component's own
81
+ * rule. Cloth is such a system: it reads a world transform and writes world
82
+ * transforms. So rather than reach for a third mechanism, the write-back is
83
+ * built so the order does not matter: it writes each node's **local** transform
84
+ * alongside its world one, which makes a hierarchy recomposition reproduce the
85
+ * simulated pose instead of replacing it with the authored one
86
+ * (`cloth_write_back.js`).
87
+ *
88
+ * What is left is a **quality** question rather than a correctness one, and it
89
+ * is smaller than it looks. Running before the system that poses the anchor
90
+ * means reading last step's anchor pose, so the cloth's *shape* is one fixed
91
+ * step stale; its *position* is not, because the attachment cascade carries the
92
+ * whole subtree rigidly with the anchor from cloth's own local transforms.
93
+ * `ClothSystem.spec.js` measures that lag rather than asserting a placement,
94
+ * and asserts the property that actually matters: the pose survives being
95
+ * recomposed.
96
+ *
97
+ * The declaration is the honest one — cloth reads `Cloth` and `ClothExclude`,
98
+ * and reads and writes both `Transform64` and `TransformAttachment`. That puts
99
+ * it before `TransformAttachmentSystem`, which is right: cloth writes the local
100
+ * transforms and the attachment system is their reader.
101
+ *
102
+ * ## The local frame and the inertia model — §7 P7
103
+ *
104
+ * Particles live in the anchor's own frame. When the anchor moves, the cloth is
105
+ * asked how much of that motion it would like to feel:
106
+ * `ClothDynamics#inertia = 1` keeps every particle's **world** position and
107
+ * velocity, so a cape trails a sprinting character; `0` keeps their **local**
108
+ * ones, so the cloth rides the anchor as though it were standing still. The
109
+ * blend between the two is a lerp in the local frame, which is all the frame
110
+ * change is.
111
+ *
112
+ * The felt motion is limited before the blend, which is what makes "character
113
+ * sprints, cape behaves" a default rather than a tuning exercise, and the limits
114
+ * are derived from the cloth's own reach rather than authored.
115
+ *
116
+ * ## Teleport — §7 P6
117
+ *
118
+ * Anchor motion past a threshold is not locomotion and is not treated as any
119
+ * amount of it. `ClothFlags.TeleportKeep` carries the shape across, which in a
120
+ * local frame is literally doing nothing; the default re-seeds at the animated
121
+ * pose with zero velocity and opens a stabilisation window in which the
122
+ * velocity limit ramps back from nothing, so the first steps after a reset
123
+ * cannot produce a whip.
124
+ *
125
+ * @author Alex Goldring
126
+ * @copyright Company Named Limited (c) 2026
127
+ */
128
+ export class ClothSystem extends System {
129
+
130
+ dependencies = [Cloth, Transform64];
131
+
132
+ components_used = [
133
+ ResourceAccessSpecification.from(Cloth, ResourceAccessKind.Read),
134
+ ResourceAccessSpecification.from(Transform64, ResourceAccessKind.Read | ResourceAccessKind.Write),
135
+ ResourceAccessSpecification.from(TransformAttachment, ResourceAccessKind.Read | ResourceAccessKind.Write),
136
+ ResourceAccessSpecification.from(ClothExclude, ResourceAccessKind.Read),
137
+ ResourceAccessSpecification.from(ClothCollider, ResourceAccessKind.Read),
138
+ ResourceAccessSpecification.from(ClothRig, ResourceAccessKind.Read)
139
+ ];
140
+
141
+ /**
142
+ * World gravity, m/s². Rotated into each cloth's local frame per step.
143
+ * @type {Float64Array}
144
+ */
145
+ gravity = new Float64Array([0, -9.81, 0]);
146
+
147
+ /**
148
+ * Live cloths, ordered so that an instance whose anchor sits inside another
149
+ * instance's subtree steps after it.
150
+ * @type {ClothInstance[]}
151
+ */
152
+ instances = [];
153
+
154
+ /**
155
+ * @type {Map<number, ClothInstance>}
156
+ * @private
157
+ */
158
+ __by_entity = new Map();
159
+
160
+ /**
161
+ * @type {TransformAttachmentSystem|null}
162
+ * @private
163
+ */
164
+ __attachments = null;
165
+
166
+ /**
167
+ * Where a skinned model instance comes from. Anything that answers
168
+ * `instance_of(entity)` with a `SceneBundleInstance` will do, and
169
+ * {@link MeshSystem} is what does in a running engine.
170
+ *
171
+ * Public and assignable because a garment's joints are a *rendering*
172
+ * structure and `MeshSystem` needs a graphics engine, a scene and a loader
173
+ * to exist — so a spec that wants joints without a renderer sets this
174
+ * instead, and gets exactly the interface the system uses.
175
+ *
176
+ * `null` means no rig can seed, which is the same additive shape the
177
+ * collider index has: a scene without one gets M1's cloth rather than an
178
+ * error.
179
+ * @type {{instance_of: function(number): (SceneBundleInstance|null)}|null}
180
+ */
181
+ models = null;
182
+
183
+ /**
184
+ * The collider index's owner, or `null` when the manager has none — in
185
+ * which case cloth simply does not collide, which is exactly M1's
186
+ * behaviour and is what makes the collision layer additive rather than a
187
+ * new requirement on every scene.
188
+ * @type {ClothColliderSystem|null}
189
+ * @private
190
+ */
191
+ __colliders = null;
192
+
193
+ /**
194
+ * @type {boolean}
195
+ * @private
196
+ */
197
+ __order_dirty = false;
198
+
199
+ /**
200
+ * @param {EntityManager} entityManager
201
+ * @returns {Promise<void>}
202
+ */
203
+ async startup(entityManager) {
204
+ this.__attachments = entityManager.getSystem(TransformAttachmentSystem);
205
+
206
+ assert.notNull(
207
+ this.__attachments,
208
+ 'ClothSystem needs a TransformAttachmentSystem: the subtree walk is its parent-to-children index'
209
+ );
210
+
211
+ // Optional by design. A scene with no ClothColliderSystem gets cloth
212
+ // that does not collide, rather than cloth that throws.
213
+ this.__colliders = entityManager.getSystem(ClothColliderSystem) ?? null;
214
+
215
+ if (this.models === null) {
216
+ this.models = entityManager.getSystem(MeshSystem) ?? null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * @param {Cloth} cloth
222
+ * @param {Transform64} transform
223
+ * @param {number} entity
224
+ */
225
+ link(cloth, transform, entity) {
226
+ const instance = new ClothInstance();
227
+
228
+ instance.entity = entity;
229
+ instance.cloth = cloth;
230
+ instance.transform = transform;
231
+
232
+ this.instances.push(instance);
233
+ this.__by_entity.set(entity, instance);
234
+
235
+ this.__order_dirty = true;
236
+ }
237
+
238
+ /**
239
+ * @param {Cloth} cloth
240
+ * @param {Transform64} transform
241
+ * @param {number} entity
242
+ */
243
+ unlink(cloth, transform, entity) {
244
+ const instance = this.__by_entity.get(entity);
245
+
246
+ if (instance === undefined) {
247
+ return;
248
+ }
249
+
250
+ this.__by_entity.delete(entity);
251
+
252
+ const index = this.instances.indexOf(instance);
253
+
254
+ if (index >= 0) {
255
+ this.instances.splice(index, 1);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Throw away a cloth's binding so the next step walks its subtree again.
261
+ *
262
+ * The walk is done once, because a rope's links do not come and go. When
263
+ * they do — a chain gains a link, a rig is rebuilt — this is how you say so,
264
+ * and it is the same contract `TransformAttachment#parent` already carries:
265
+ * the structure is immutable while it is attached, and changing it is an
266
+ * explicit act rather than something the engine polls for.
267
+ *
268
+ * @param {number} entity
269
+ * @returns {void}
270
+ */
271
+ reseed(entity) {
272
+ const instance = this.__by_entity.get(entity);
273
+
274
+ if (instance === undefined) {
275
+ return;
276
+ }
277
+
278
+ instance.seeded = false;
279
+ instance.anchor_valid = false;
280
+
281
+ this.__order_dirty = true;
282
+ }
283
+
284
+ /**
285
+ * Wake a sleeping cloth. Anchor motion and a `ClothDynamics` change do this
286
+ * on their own; a caller that has changed the world in some way the cloth
287
+ * cannot see — M6's wind, for one — says so here.
288
+ *
289
+ * @param {number} entity
290
+ * @returns {void}
291
+ */
292
+ wake(entity) {
293
+ const instance = this.__by_entity.get(entity);
294
+
295
+ if (instance !== undefined) {
296
+ instance.asleep = false;
297
+ instance.still_steps = 0;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * @param {number} entity
303
+ * @returns {ClothInstance|undefined}
304
+ */
305
+ instanceOf(entity) {
306
+ return this.__by_entity.get(entity);
307
+ }
308
+
309
+ /**
310
+ * @param {number} dt seconds
311
+ */
312
+ fixedUpdate(dt) {
313
+ const ecd = this.entityManager.dataset;
314
+
315
+ if (ecd === null || ecd === undefined) {
316
+ return;
317
+ }
318
+
319
+ const instances = this.instances;
320
+
321
+ if (this.__order_dirty) {
322
+ this.#reorder(ecd);
323
+ }
324
+
325
+ // The index is refreshed here rather than in its own system's fixed
326
+ // update, because the engine's ordering cannot put a reader after a
327
+ // writer of the same component and `ClothColliderSystem` reads strictly
328
+ // less than this system does. §4.1's conclusion, applied a second time:
329
+ // the answer is not a third mechanism, it is one call site.
330
+ if (this.__colliders !== null) {
331
+ this.__colliders.refresh();
332
+ }
333
+
334
+ for (let i = 0; i < instances.length; i++) {
335
+ const instance = instances[i];
336
+
337
+ if (instance.refused) {
338
+ continue;
339
+ }
340
+
341
+ if (!instance.seeded) {
342
+ this.#seed(instance, ecd, dt);
343
+
344
+ if (!instance.seeded) {
345
+ // A rig whose model has not finished loading. Nothing is
346
+ // wrong and nothing is retried-with-backoff: the next step
347
+ // asks again, and a model that never loads costs one map
348
+ // lookup a step.
349
+ continue;
350
+ }
351
+ } else if (!instance.cloth.dynamics.equals(instance.dynamics_seen)) {
352
+ if (cloth_dynamics_change_needs_reseed(instance.dynamics_seen, instance.cloth.dynamics)) {
353
+ this.#seed(instance, ecd, dt);
354
+ } else {
355
+ cloth_reparameterise(instance, dt);
356
+ }
357
+
358
+ instance.asleep = false;
359
+ instance.still_steps = 0;
360
+ }
361
+
362
+ this.#advance(instance, ecd, dt);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Walk whichever hierarchy this cloth is built on.
368
+ *
369
+ * **The discriminator is a component's presence** (§3.1): `Cloth` alone
370
+ * simulates the entity's own transform subtree, `Cloth` + {@link ClothRig}
371
+ * simulates the joints of the skinned model the entity is showing. There is
372
+ * no mode field anywhere, and the solver does not know which ran.
373
+ *
374
+ * A rig can fail to seed for reasons that are not errors — the model has
375
+ * not loaded, or nothing in the manager hands out model instances — and one
376
+ * that does is left unseeded for the next step to try again. A rig that
377
+ * fails for a reason that *is* an error says so once and then stops, because
378
+ * a proxy that does not fit its skin will not start fitting it.
379
+ *
380
+ * @param {ClothInstance} instance
381
+ * @param {EntityComponentDataset} ecd
382
+ * @param {number} dt
383
+ * @private
384
+ */
385
+ #seed(instance, ecd, dt) {
386
+ const rig = ecd.getComponent(instance.entity, ClothRig);
387
+
388
+ if (rig === undefined || rig === null || rig.proxy === null) {
389
+ instance.rig = null;
390
+
391
+ cloth_seed_subtree(instance, ecd, this.__attachments, dt);
392
+
393
+ return;
394
+ }
395
+
396
+ if (this.models === null) {
397
+ this.#refuse(instance, `entity ${instance.entity} has a ClothRig and nothing hands out model instances`);
398
+
399
+ return;
400
+ }
401
+
402
+ const model = this.models.instance_of(instance.entity);
403
+
404
+ if (model === null || model === undefined) {
405
+ // Still loading. Not an error, and not worth a message.
406
+ return;
407
+ }
408
+
409
+ const skin = model.skins[rig.skin];
410
+
411
+ const error = rig.proxy.checkAgainst(skin);
412
+
413
+ if (error !== "") {
414
+ this.#refuse(instance, `entity ${instance.entity}: ${error}`);
415
+
416
+ return;
417
+ }
418
+
419
+ instance.rig = rig;
420
+
421
+ cloth_seed_rig(instance, rig.proxy, skin.joints, dt);
422
+ }
423
+
424
+ /**
425
+ * Say once why a rig will not seed, and stop asking.
426
+ *
427
+ * @param {ClothInstance} instance
428
+ * @param {string} message
429
+ * @private
430
+ */
431
+ #refuse(instance, message) {
432
+ if (instance.refused) {
433
+ return;
434
+ }
435
+
436
+ instance.refused = true;
437
+
438
+ console.warn(`ClothSystem: ${message}`);
439
+ }
440
+
441
+ /**
442
+ * Depth-first order by hierarchy depth, so a nested cloth reads an anchor
443
+ * the cloth above it has already written.
444
+ *
445
+ * @param {EntityComponentDataset} ecd
446
+ * @private
447
+ */
448
+ #reorder(ecd) {
449
+ const instances = this.instances;
450
+
451
+ for (let i = 0; i < instances.length; i++) {
452
+ const instance = instances[i];
453
+
454
+ let depth = 0;
455
+ let entity = instance.entity;
456
+
457
+ // Bounded by the number of entities; a cycle in the attachment
458
+ // graph is not this system's to diagnose, but it is this loop's to
459
+ // survive.
460
+ for (let guard = 0; guard < 4096; guard++) {
461
+ const attachment = ecd.getComponent(entity, TransformAttachment);
462
+
463
+ if (attachment === undefined) {
464
+ break;
465
+ }
466
+
467
+ entity = attachment.parent;
468
+ depth++;
469
+ }
470
+
471
+ instance.depth = depth;
472
+ }
473
+
474
+ instances.sort((a, b) => a.depth - b.depth);
475
+
476
+ this.__order_dirty = false;
477
+ }
478
+
479
+ /**
480
+ * @param {ClothInstance} instance
481
+ * @param {EntityComponentDataset} ecd
482
+ * @param {number} dt
483
+ * @private
484
+ */
485
+ #advance(instance, ecd, dt) {
486
+ const transform = instance.rig === null
487
+ ? instance.transform
488
+ : this.#anchor_pose(instance);
489
+
490
+ const tx = transform[TRANSFORM64_TRANSLATION_OFFSET];
491
+ const ty = transform[TRANSFORM64_TRANSLATION_OFFSET + 1];
492
+ const tz = transform[TRANSFORM64_TRANSLATION_OFFSET + 2];
493
+
494
+ const qx = transform[TRANSFORM64_ROTATION_OFFSET];
495
+ const qy = transform[TRANSFORM64_ROTATION_OFFSET + 1];
496
+ const qz = transform[TRANSFORM64_ROTATION_OFFSET + 2];
497
+ const qw = transform[TRANSFORM64_ROTATION_OFFSET + 3];
498
+
499
+ const previous_translation = instance.anchor_translation;
500
+ const previous_rotation = instance.anchor_rotation;
501
+
502
+ if (instance.cloth.blend !== instance.blend_seen) {
503
+ instance.asleep = false;
504
+ instance.still_steps = 0;
505
+ }
506
+
507
+ const moved = instance.anchor_valid && (
508
+ previous_translation[0] !== tx
509
+ || previous_translation[1] !== ty
510
+ || previous_translation[2] !== tz
511
+ || previous_rotation[0] !== qx
512
+ || previous_rotation[1] !== qy
513
+ || previous_rotation[2] !== qz
514
+ || previous_rotation[3] !== qw
515
+ );
516
+
517
+ if (moved) {
518
+ instance.asleep = false;
519
+ instance.still_steps = 0;
520
+ }
521
+
522
+ if (instance.asleep && this.__colliders !== null && instance.collider_count >= 0) {
523
+ // §7 P5's "wake on collider motion". A sleeping cloth is neither
524
+ // stepped nor written back, so something has to notice the world it
525
+ // was resting against moving out from under it, and a broadphase
526
+ // query plus a pose comparison is three orders of magnitude cheaper
527
+ // than the step it decides whether to skip.
528
+ if (cloth_colliders_moved(instance, this.__colliders.index)) {
529
+ instance.asleep = false;
530
+ instance.still_steps = 0;
531
+ }
532
+ }
533
+
534
+ if (instance.asleep) {
535
+ // Nothing is stepped and nothing is written: the pose on the
536
+ // subtree is already the pose, and rewriting it is what would make
537
+ // §7 P5's "bit-identical across the following 300 steps" an
538
+ // aspiration rather than a fact.
539
+ return;
540
+ }
541
+
542
+ if (instance.anchor_valid) {
543
+ const displacement = Math.hypot(tx - previous_translation[0], ty - previous_translation[1], tz - previous_translation[2]);
544
+
545
+ const teleport = displacement > Math.max(
546
+ CLOTH_TELEPORT_REACH * instance.reach,
547
+ CLOTH_TELEPORT_FLOOR
548
+ );
549
+
550
+ if (teleport) {
551
+ this.#teleport(instance);
552
+ } else {
553
+ move_local_frame(
554
+ instance,
555
+ tx, ty, tz, qx, qy, qz, qw,
556
+ previous_translation, previous_rotation
557
+ );
558
+ }
559
+ }
560
+
561
+ previous_translation[0] = tx;
562
+ previous_translation[1] = ty;
563
+ previous_translation[2] = tz;
564
+
565
+ previous_rotation[0] = qx;
566
+ previous_rotation[1] = qy;
567
+ previous_rotation[2] = qz;
568
+ previous_rotation[3] = qw;
569
+
570
+ instance.anchor_valid = true;
571
+
572
+ refresh_leash_targets(instance);
573
+
574
+ const dynamics = instance.cloth.dynamics;
575
+
576
+ damp(instance, cloth_velocity_retention(dynamics.damping, dt));
577
+
578
+ // Gravity is a world vector and the solve is in the anchor's frame.
579
+ const gravity = this.gravity;
580
+
581
+ v3_quaternion_apply_inverse(LOCAL_GRAVITY, 0, gravity[0], gravity[1], gravity[2], qx, qy, qz, qw);
582
+
583
+ const state = instance.state;
584
+
585
+ let limit = cloth_velocity_limit(
586
+ instance.shortest_row, dynamics.substeps, dynamics.iterations, dt
587
+ );
588
+
589
+ state.contact_radius = dynamics.thickness;
590
+
591
+ if (this.__colliders !== null) {
592
+ // The gather's margin is how far a particle may travel this step,
593
+ // which is the velocity clamp read as a distance — the quantity it
594
+ // was built out of (`cloth_dynamics_map.js`).
595
+ cloth_gather_colliders(instance, this.__colliders.index, limit * dt);
596
+
597
+ instance.contact_count = cloth_contact_find(
598
+ state, instance.collider_table, instance.collider_key, instance.collider_count
599
+ );
600
+ } else {
601
+ instance.collider_count = 0;
602
+ instance.contact_count = 0;
603
+ }
604
+
605
+ if ((dynamics.flags & ClothDynamicsFlags.SelfCollision) !== 0) {
606
+ // Half the shortest row, so the row's diameter is one particle
607
+ // spacing. `ClothState#self_radius` carries why the fabric's own
608
+ // thickness is the wrong length here and `MEASUREMENTS.md` §31 is the
609
+ // sweep that settled it. A fabric thicker than its proxy is coarse
610
+ // keeps its thickness — and is then clamped under the closest pair
611
+ // the proxy has with no row between them, because a diameter that
612
+ // reaches that pair puts a cloth which is not folded at all in
613
+ // permanent contact with itself (§36, §37).
614
+ state.self_radius = Math.min(
615
+ Math.max(dynamics.thickness, instance.shortest_row * CLOTH_SELF_RADIUS_MIN_RATIO),
616
+ instance.closest_free_pair * CLOTH_SELF_RADIUS_MAX_RATIO
617
+ );
618
+
619
+ instance.self_pairs = cloth_self_find(state, instance.self_exclusions);
620
+ } else {
621
+ state.self_count = 0;
622
+ instance.self_pairs = 0;
623
+ }
624
+
625
+ cloth_step(
626
+ state, dt,
627
+ LOCAL_GRAVITY[0], LOCAL_GRAVITY[1], LOCAL_GRAVITY[2],
628
+ dynamics.substeps, dynamics.iterations,
629
+ instance.collider_table, instance.collider_count
630
+ );
631
+
632
+ if (instance.stabilise_remaining > 0) {
633
+ // Ramp back from nothing over the window. The first step after a
634
+ // reset is allowed no velocity at all, which is what stops a
635
+ // re-seeded cloth from snapping taut and whipping.
636
+ limit *= (CLOTH_STABILISE_STEPS - instance.stabilise_remaining) / CLOTH_STABILISE_STEPS;
637
+
638
+ instance.stabilise_remaining--;
639
+ }
640
+
641
+ const speed = clamp_velocity(instance, limit);
642
+
643
+ if ((instance.cloth.flags & ClothFlags.NeverSleep) === 0) {
644
+ if (speed < CLOTH_SLEEP_SPEED_RATIO * instance.reach) {
645
+ instance.still_steps++;
646
+
647
+ if (instance.still_steps >= CLOTH_SLEEP_STEPS) {
648
+ instance.asleep = true;
649
+ }
650
+ } else {
651
+ instance.still_steps = 0;
652
+ }
653
+ }
654
+
655
+ instance.blend_seen = instance.cloth.blend;
656
+
657
+ if (instance.rig === null) {
658
+ cloth_write_back(instance, ecd);
659
+ } else {
660
+ cloth_write_back_rig(instance, transform);
661
+ }
662
+ }
663
+
664
+ /**
665
+ * The anchor joint's world pose, computed on the CPU from the clips the
666
+ * entity is playing.
667
+ *
668
+ * **Computed, never read back**, for two reasons that are both the engine's
669
+ * own. A joint of an animated skeleton is under `TransformAuthority.GPU`,
670
+ * so nothing maintains its CPU-side `transform_global` and reading it gives
671
+ * whatever the node was last posed at by hand. And GPU readback is a frame
672
+ * late and asynchronous, so anything depending on it would depend on frame
673
+ * pacing — which is why `query_entity_node_world_pose` exists and is
674
+ * documented as the only sanctioned way to ask this question. A cape and a
675
+ * scabbard on the same shoulder get their answer from the same evaluator,
676
+ * off the same clock, which is what keeps them on the same shoulder.
677
+ *
678
+ * @param {ClothInstance} instance
679
+ * @returns {Transform64}
680
+ * @private
681
+ */
682
+ #anchor_pose(instance) {
683
+ const playbacks = PLAYBACKS;
684
+
685
+ playbacks.length = 0;
686
+
687
+ collect_entity_playbacks(playbacks, this.entityManager, instance.entity);
688
+
689
+ pose_evaluate_world(instance.anchor_pose, instance.node_joint[0], playbacks);
690
+
691
+ playbacks.length = 0;
692
+
693
+ return instance.anchor_pose;
694
+ }
695
+
696
+ /**
697
+ * @param {ClothInstance} instance
698
+ * @private
699
+ */
700
+ #teleport(instance) {
701
+ instance.teleport_count++;
702
+
703
+ if ((instance.cloth.flags & ClothFlags.TeleportKeep) !== 0) {
704
+ // `Keep` carries the cloth's shape across the jump. Particles are
705
+ // stored relative to the anchor, so that is exactly what happens if
706
+ // this function does nothing to them — the frame moved and they did
707
+ // not, which is MagicaCloth2's rigid delta expressed in the frame
708
+ // that makes it free.
709
+ return;
710
+ }
711
+
712
+ const state = instance.state;
713
+
714
+ const node_count = instance.node_count;
715
+ const animated = instance.animated_position;
716
+ const particle_of_node = instance.particle_of_node;
717
+
718
+ for (let node = 0; node < node_count; node++) {
719
+ const p = particle_of_node[node] * 3;
720
+ const n = node * 3;
721
+
722
+ state.position[p] = animated[n];
723
+ state.position[p + 1] = animated[n + 1];
724
+ state.position[p + 2] = animated[n + 2];
725
+ }
726
+
727
+ state.position_previous.set(state.position);
728
+ state.velocity.fill(0);
729
+ state.inertia_blend.fill(0);
730
+
731
+ // The multipliers describe forces in a configuration that no longer
732
+ // exists. Carrying them across would apply the old pose's tension to
733
+ // the new one on the first iteration, which is a whip with extra steps.
734
+ state.stretch_lambda.fill(0);
735
+ state.bend_lambda.fill(0);
736
+ state.leash_lambda.fill(0);
737
+
738
+ instance.stabilise_remaining = CLOTH_STABILISE_STEPS;
739
+ }
740
+ }
741
+
742
+ /**
743
+ * The clip set handed to the pose evaluator, rebuilt per call from the
744
+ * animation systems' own entries. Nothing in here outlives the call.
745
+ * @type {PosePlayback[]}
746
+ * @private
747
+ */
748
+ const PLAYBACKS = [];
749
+
750
+ /**
751
+ * @type {Float64Array}
752
+ * @private
753
+ */
754
+ const LOCAL_GRAVITY = new Float64Array(3);
755
+
756
+ /**
757
+ * @type {Float64Array}
758
+ * @private
759
+ */
760
+ const FRAME = new Float64Array(8);
761
+
762
+ /**
763
+ * Carry the particles across a change of anchor pose, by however much of it the
764
+ * fabric is willing to feel. §7 P7.
765
+ *
766
+ * A particle at local `p` was at world `t_prev + q_prev·p`. Holding that world
767
+ * position fixed, its local coordinates become `D + Q·p` with
768
+ * `D = q_now⁻¹·(t_prev − t_now)` and `Q = q_now⁻¹ ⊗ q_prev`. Feeling *all* of
769
+ * the anchor's motion is taking that; feeling *none* is leaving `p` alone; and
770
+ * `inertia` is the lerp between them. Velocity is carried the same way, minus
771
+ * the translation, since a translation of the frame does not rotate a vector.
772
+ *
773
+ * `D` and `Q` are limited before the lerp. The limits are the cloth's own reach
774
+ * per step and a fixed angle per step, which is what makes an anchor that
775
+ * teleports a metre — or spins — something a cape *lags behind* rather than
776
+ * something it is fired out of.
777
+ *
778
+ * @param {ClothInstance} instance
779
+ * @param {number} tx
780
+ * @param {number} ty
781
+ * @param {number} tz
782
+ * @param {number} qx
783
+ * @param {number} qy
784
+ * @param {number} qz
785
+ * @param {number} qw
786
+ * @param {Float64Array} previous_translation
787
+ * @param {Float64Array} previous_rotation
788
+ * @private
789
+ */
790
+ function move_local_frame(
791
+ instance,
792
+ tx, ty, tz, qx, qy, qz, qw,
793
+ previous_translation, previous_rotation
794
+ ) {
795
+ let inertia = instance.cloth.dynamics.inertia;
796
+
797
+ if (inertia <= 0) {
798
+ return;
799
+ }
800
+
801
+ if (inertia > 1) {
802
+ inertia = 1;
803
+ }
804
+
805
+ v3_quaternion_apply_inverse(
806
+ FRAME, 0,
807
+ previous_translation[0] - tx,
808
+ previous_translation[1] - ty,
809
+ previous_translation[2] - tz,
810
+ qx, qy, qz, qw
811
+ );
812
+
813
+ const travel_limit = CLOTH_ANCHOR_TRAVEL_REACH * instance.reach;
814
+
815
+ const travel = Math.hypot(FRAME[0], FRAME[1], FRAME[2]);
816
+
817
+ if (travel > travel_limit && travel > 0) {
818
+ const scale = travel_limit / travel;
819
+
820
+ FRAME[0] *= scale;
821
+ FRAME[1] *= scale;
822
+ FRAME[2] *= scale;
823
+ }
824
+
825
+ quat3_multiply(
826
+ FRAME, 4,
827
+ -qx, -qy, -qz, qw,
828
+ previous_rotation[0], previous_rotation[1], previous_rotation[2], previous_rotation[3]
829
+ );
830
+
831
+ // `|w|` is `cos(θ/2)`, so this is the angle test without an `acos`.
832
+ const half_angle_cos = FRAME[7] < 0 ? -FRAME[7] : FRAME[7];
833
+
834
+ if (half_angle_cos < Math.cos(CLOTH_ANCHOR_ROTATION_LIMIT * 0.5)) {
835
+ const angle = 2 * Math.acos(half_angle_cos > 1 ? 1 : half_angle_cos);
836
+
837
+ quat3_nlerp(
838
+ FRAME, 4,
839
+ 0, 0, 0, 1,
840
+ FRAME[4], FRAME[5], FRAME[6], FRAME[7],
841
+ CLOTH_ANCHOR_ROTATION_LIMIT / angle
842
+ );
843
+ }
844
+
845
+ const state = instance.state;
846
+
847
+ const particle_count = state.particle_count;
848
+
849
+ const position = state.position;
850
+ const velocity = state.velocity;
851
+
852
+ const dx = FRAME[0];
853
+ const dy = FRAME[1];
854
+ const dz = FRAME[2];
855
+
856
+ const rx = FRAME[4];
857
+ const ry = FRAME[5];
858
+ const rz = FRAME[6];
859
+ const rw = FRAME[7];
860
+
861
+ for (let i = 0; i < particle_count; i++) {
862
+ const p = i * 3;
863
+
864
+ v3_quaternion_apply(CARRIED, 0, position[p], position[p + 1], position[p + 2], rx, ry, rz, rw);
865
+
866
+ position[p] += inertia * (CARRIED[0] + dx - position[p]);
867
+ position[p + 1] += inertia * (CARRIED[1] + dy - position[p + 1]);
868
+ position[p + 2] += inertia * (CARRIED[2] + dz - position[p + 2]);
869
+
870
+ v3_quaternion_apply(CARRIED, 0, velocity[p], velocity[p + 1], velocity[p + 2], rx, ry, rz, rw);
871
+
872
+ velocity[p] += inertia * (CARRIED[0] - velocity[p]);
873
+ velocity[p + 1] += inertia * (CARRIED[1] - velocity[p + 1]);
874
+ velocity[p + 2] += inertia * (CARRIED[2] - velocity[p + 2]);
875
+ }
876
+
877
+ // The anchor is pinned and is the origin of the frame it anchors, so it is
878
+ // the one particle that must not be carried anywhere.
879
+ const anchor = instance.particle_of_node[0] * 3;
880
+
881
+ position[anchor] = 0;
882
+ position[anchor + 1] = 0;
883
+ position[anchor + 2] = 0;
884
+
885
+ velocity[anchor] = 0;
886
+ velocity[anchor + 1] = 0;
887
+ velocity[anchor + 2] = 0;
888
+ }
889
+
890
+ /**
891
+ * @type {Float64Array}
892
+ * @private
893
+ */
894
+ const CARRIED = new Float64Array(3);
895
+
896
+ /**
897
+ * Point every leash row at where it measures from: a slack row at the
898
+ * particle's animated position, a tether at the anchor.
899
+ *
900
+ * @param {ClothInstance} instance
901
+ * @private
902
+ */
903
+ function refresh_leash_targets(instance) {
904
+ const state = instance.state;
905
+
906
+ const target = state.leash_target;
907
+
908
+ const animated = instance.animated_position;
909
+
910
+ const anchor = instance.particle_of_node[0] * 3;
911
+
912
+ const anchor_x = state.position[anchor];
913
+ const anchor_y = state.position[anchor + 1];
914
+ const anchor_z = state.position[anchor + 2];
915
+
916
+ const node_count = instance.node_count;
917
+
918
+ const slack_row = instance.slack_row;
919
+ const tether_row = instance.tether_row;
920
+
921
+ for (let node = 1; node < node_count; node++) {
922
+ const tether = tether_row[node];
923
+
924
+ if (tether >= 0) {
925
+ target[tether * 3] = anchor_x;
926
+ target[tether * 3 + 1] = anchor_y;
927
+ target[tether * 3 + 2] = anchor_z;
928
+ }
929
+
930
+ const slack = slack_row[node];
931
+
932
+ if (slack >= 0) {
933
+ target[slack * 3] = animated[node * 3];
934
+ target[slack * 3 + 1] = animated[node * 3 + 1];
935
+ target[slack * 3 + 2] = animated[node * 3 + 2];
936
+ }
937
+ }
938
+ }
939
+
940
+ /**
941
+ * Scale every particle's velocity, which is §7 P5's other half.
942
+ *
943
+ * The multiplier decay `α·γ` bleeds accumulated force out of a **hard** row,
944
+ * and a soft row has no multiplier to decay — M0 measured a soft cloth landing
945
+ * in a bounded limit cycle that 2,000 further steps do not move. This is what
946
+ * reaches it, and it is why `ClothDynamics#damping` is part of the correctness
947
+ * argument rather than a taste control. `MEASUREMENTS.md` §6.
948
+ *
949
+ * @param {ClothInstance} instance
950
+ * @param {number} retention `[0, 1]`
951
+ * @private
952
+ */
953
+ function damp(instance, retention) {
954
+ if (retention >= 1) {
955
+ return;
956
+ }
957
+
958
+ const velocity = instance.state.velocity;
959
+
960
+ for (let i = 0; i < velocity.length; i++) {
961
+ velocity[i] *= retention;
962
+ }
963
+ }
964
+
965
+ /**
966
+ * Hold every particle's speed under the limit derived from
967
+ * `ClothDynamics#thickness`, and report the largest speed left afterwards so
968
+ * the sleep test has something to read.
969
+ *
970
+ * §7 P1 names this clamp and M0 deliberately left it out, because a clamp would
971
+ * have masked the property M0 existed to measure. It is here rather than inside
972
+ * `cloth_step` for the same reason: the solver's stability is still its own.
973
+ *
974
+ * Speed rather than displacement is what the sleep test reads, because
975
+ * `position_previous` after a multi-substep step holds the start of the *last*
976
+ * substep rather than the start of the step, and a motion measure that quietly
977
+ * divides by the substep count is a sleep threshold that means something
978
+ * different for every fabric.
979
+ *
980
+ * @param {ClothInstance} instance
981
+ * @param {number} limit m/s
982
+ * @returns {number} largest particle speed after clamping, m/s
983
+ * @private
984
+ */
985
+ function clamp_velocity(instance, limit) {
986
+ const state = instance.state;
987
+
988
+ const particle_count = state.particle_count;
989
+
990
+ const velocity = state.velocity;
991
+
992
+ const limit_squared = limit * limit;
993
+
994
+ let worst_squared = 0;
995
+
996
+ for (let i = 0; i < particle_count; i++) {
997
+ const p = i * 3;
998
+
999
+ const vx = velocity[p];
1000
+ const vy = velocity[p + 1];
1001
+ const vz = velocity[p + 2];
1002
+
1003
+ let speed_squared = vx * vx + vy * vy + vz * vz;
1004
+
1005
+ if (speed_squared > limit_squared) {
1006
+ const scale = limit / Math.sqrt(speed_squared);
1007
+
1008
+ velocity[p] = vx * scale;
1009
+ velocity[p + 1] = vy * scale;
1010
+ velocity[p + 2] = vz * scale;
1011
+
1012
+ speed_squared = limit_squared;
1013
+ }
1014
+
1015
+ if (speed_squared > worst_squared) {
1016
+ worst_squared = speed_squared;
1017
+ }
1018
+ }
1019
+
1020
+ return Math.sqrt(worst_squared);
1021
+ }