@woosh/meep-engine 3.24.0 → 3.26.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 (349) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +59 -0
  3. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/HeightMapShape3D.js +285 -0
  5. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  6. package/src/core/geom/3d/shape/PosedShape3D.js +57 -0
  7. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  8. package/src/core/geom/3d/shape/TransformedShape3D.js +102 -0
  9. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  10. package/src/core/geom/3d/shape/UnionShape3D.js +64 -0
  11. package/src/engine/graphics3/PickingSystem.d.ts +3 -3
  12. package/src/engine/graphics3/PickingSystem.d.ts.map +1 -1
  13. package/src/engine/graphics3/PickingSystem.js +52 -55
  14. package/src/engine/graphics3/shader_picking_gather.d.ts +40 -0
  15. package/src/engine/graphics3/shader_picking_gather.d.ts.map +1 -0
  16. package/src/engine/graphics3/shader_picking_gather.js +120 -0
  17. package/src/engine/physics/cloth/MEASUREMENTS.md +1632 -0
  18. package/src/engine/physics/cloth/PLAN.md +738 -95
  19. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +84 -17
  20. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  21. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +451 -42
  22. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts +52 -0
  23. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts.map +1 -0
  24. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.js +70 -0
  25. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts +12 -3
  26. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  27. package/src/engine/physics/cloth/ecs/ClothDynamics.js +340 -331
  28. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +101 -29
  29. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  30. package/src/engine/physics/cloth/ecs/ClothInstance.js +110 -31
  31. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts +84 -0
  32. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts.map +1 -0
  33. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.js +141 -0
  34. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +71 -81
  35. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  36. package/src/engine/physics/cloth/ecs/ClothSystem.js +135 -856
  37. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts +48 -0
  38. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts.map +1 -0
  39. package/src/engine/physics/cloth/ecs/ClothWorkerCore.js +107 -0
  40. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +323 -0
  41. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -0
  42. package/src/engine/physics/cloth/ecs/ClothWorld.js +1248 -0
  43. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +192 -0
  44. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -0
  45. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +578 -0
  46. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts +2 -0
  47. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts.map +1 -0
  48. package/src/engine/physics/cloth/ecs/cloth.worker.js +12 -0
  49. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  50. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +2 -1
  51. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +85 -3
  52. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  53. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +366 -271
  54. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +95 -12
  55. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  56. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +170 -27
  57. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -0
  58. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
  59. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +249 -201
  60. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -0
  61. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  62. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +62 -1
  63. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts +29 -0
  64. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts.map +1 -0
  65. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.js +78 -0
  66. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  67. package/src/engine/physics/cloth/ecs/cloth_write_back.js +18 -10
  68. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts +17 -0
  69. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts.map +1 -0
  70. package/src/engine/physics/cloth/ecs/makeClothWorker.js +18 -0
  71. package/src/engine/physics/cloth/playground/README.md +73 -2
  72. package/src/engine/physics/cloth/playground/collide_main.js +1 -1
  73. package/src/engine/physics/cloth/playground/wind.html +166 -0
  74. package/src/engine/physics/cloth/playground/wind_build.d.ts +81 -0
  75. package/src/engine/physics/cloth/playground/wind_build.d.ts.map +1 -0
  76. package/src/engine/physics/cloth/playground/wind_build.js +141 -0
  77. package/src/engine/physics/cloth/playground/wind_main.d.ts +2 -0
  78. package/src/engine/physics/cloth/playground/wind_main.d.ts.map +1 -0
  79. package/src/engine/physics/cloth/playground/wind_main.js +681 -0
  80. package/src/engine/physics/cloth/solver/ClothState.d.ts +134 -79
  81. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  82. package/src/engine/physics/cloth/solver/ClothState.js +273 -12
  83. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +29 -8
  84. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  85. package/src/engine/physics/cloth/solver/cloth_contact_find.js +37 -9
  86. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +45 -2
  87. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  88. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +46 -2
  89. package/src/engine/physics/cloth/solver/cloth_step.d.ts +21 -2
  90. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  91. package/src/engine/physics/cloth/solver/cloth_step.js +173 -4
  92. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts +40 -0
  93. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -0
  94. package/src/engine/physics/cloth/solver/cloth_step_instance.js +284 -0
  95. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts +107 -0
  96. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts.map +1 -0
  97. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.js +263 -0
  98. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +9 -2
  99. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -1
  100. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +9 -2
  101. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +102 -0
  102. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -0
  103. package/src/engine/physics/cloth/wind/AbstractClothWind.js +114 -0
  104. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +87 -0
  105. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -0
  106. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +185 -0
  107. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +79 -0
  108. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -0
  109. package/src/engine/physics/cloth/wind/ClothFluidWind.js +213 -0
  110. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +106 -0
  111. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -0
  112. package/src/engine/physics/cloth/wind/cloth_sample_air.js +297 -0
  113. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  114. package/src/shade/device/mock/texture_copy.d.ts.map +1 -1
  115. package/src/shade/device/mock/texture_copy.js +28 -0
  116. package/src/shade/playground/vgeo_runtime/README.md +94 -0
  117. package/src/shade/playground/vgeo_runtime/index.html +62 -0
  118. package/src/shade/playground/vgeo_runtime/main.d.ts +2 -0
  119. package/src/shade/playground/vgeo_runtime/main.d.ts.map +1 -0
  120. package/src/shade/playground/vgeo_runtime/main.js +399 -0
  121. package/src/shade/playground/vgeo_viewer/sample_asset.js +1 -1
  122. package/src/shade/renderer/camera/Camera.d.ts +14 -0
  123. package/src/shade/renderer/camera/Camera.d.ts.map +1 -1
  124. package/src/shade/renderer/camera/Camera.js +7 -1
  125. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.d.ts.map +1 -1
  126. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.js +16 -1
  127. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  128. package/src/shade/renderer/geometry/GPUGeometryManager.js +29 -9
  129. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts +10 -0
  130. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts.map +1 -1
  131. package/src/shade/renderer/geometry/GPUGeometryMetadata.js +13 -0
  132. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +46 -18
  133. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +70 -22
  134. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +97 -19
  135. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +12 -2
  136. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  137. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +14 -3
  138. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts +2 -2
  139. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +1 -1
  140. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.js +124 -6
  141. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts +22 -0
  142. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +1 -0
  143. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.js +53 -0
  144. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +8 -1
  145. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  146. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +8 -1
  147. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts +16 -0
  148. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts.map +1 -0
  149. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.js +15 -0
  150. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts +54 -0
  151. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts.map +1 -0
  152. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.js +53 -0
  153. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +13 -2
  154. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -1
  155. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +14 -2
  156. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +100 -0
  157. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  158. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +113 -0
  159. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  160. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +5 -0
  161. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  162. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +108 -0
  163. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  164. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +23 -0
  165. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  166. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +40 -0
  167. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +228 -0
  168. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -0
  169. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +932 -0
  170. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +176 -0
  171. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -0
  172. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +425 -0
  173. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +241 -0
  174. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +574 -0
  175. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +120 -0
  176. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -0
  177. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +674 -0
  178. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts +287 -0
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -0
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +976 -0
  181. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +42 -0
  182. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -0
  183. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +96 -0
  184. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts +16 -0
  185. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts.map +1 -0
  186. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.js +15 -0
  187. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts +19 -0
  188. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -0
  189. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +83 -0
  190. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts +9 -0
  191. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts.map +1 -0
  192. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.js +8 -0
  193. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +19 -0
  194. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -0
  195. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +160 -0
  196. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts +3 -0
  197. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -0
  198. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +255 -0
  199. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts +3 -0
  200. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts.map +1 -0
  201. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.js +80 -0
  202. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts +3 -0
  203. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -0
  204. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +144 -0
  205. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts +3 -0
  206. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -0
  207. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +530 -0
  208. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +39 -0
  209. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -0
  210. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +54 -0
  211. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts +41 -0
  212. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts.map +1 -0
  213. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.js +56 -0
  214. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts +49 -0
  215. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -0
  216. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +328 -0
  217. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +10 -0
  218. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts.map +1 -0
  219. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +51 -0
  220. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +13 -0
  221. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -0
  222. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +12 -0
  223. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +17 -0
  224. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts.map +1 -0
  225. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +99 -0
  226. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts +9 -0
  227. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts.map +1 -0
  228. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +8 -0
  229. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts +7 -0
  230. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts.map +1 -0
  231. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +27 -0
  232. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts +14 -0
  233. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts.map +1 -0
  234. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.js +13 -0
  235. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts +7 -0
  236. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts.map +1 -0
  237. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.js +35 -0
  238. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts +9 -0
  239. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts.map +1 -0
  240. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.js +8 -0
  241. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts +15 -0
  242. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts.map +1 -0
  243. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.js +14 -0
  244. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts +7 -0
  245. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts.map +1 -0
  246. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.js +36 -0
  247. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts +10 -0
  248. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts.map +1 -0
  249. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.js +9 -0
  250. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts +11 -0
  251. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts.map +1 -0
  252. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.js +54 -0
  253. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts +9 -0
  254. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts.map +1 -0
  255. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.js +8 -0
  256. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts +11 -0
  257. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts.map +1 -0
  258. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.js +10 -0
  259. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts +7 -0
  260. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts.map +1 -0
  261. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.js +30 -0
  262. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts +9 -0
  263. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts.map +1 -0
  264. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.js +8 -0
  265. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts +13 -0
  266. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts.map +1 -0
  267. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.js +12 -0
  268. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts +14 -0
  269. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts.map +1 -0
  270. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.js +13 -0
  271. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts +11 -0
  272. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts.map +1 -0
  273. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.js +61 -0
  274. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts +9 -0
  275. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts.map +1 -0
  276. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.js +8 -0
  277. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts +13 -0
  278. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts.map +1 -0
  279. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.js +12 -0
  280. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts +9 -0
  281. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts.map +1 -0
  282. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.js +8 -0
  283. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts +20 -0
  284. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts.map +1 -0
  285. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.js +19 -0
  286. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts +23 -0
  287. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -0
  288. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +70 -0
  289. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts +18 -0
  290. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts.map +1 -0
  291. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.js +25 -0
  292. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +35 -0
  293. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -0
  294. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +41 -0
  295. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts +32 -0
  296. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -0
  297. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +138 -0
  298. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  299. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  300. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  301. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +9 -0
  302. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  303. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +12 -1
  304. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  305. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +12 -1
  306. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  307. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +10 -0
  308. package/src/shade/renderer/scene/GPUSceneContext.d.ts +18 -0
  309. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  310. package/src/shade/renderer/scene/GPUSceneContext.js +647 -594
  311. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  312. package/src/shade/renderer/scene/rows/GPUSceneRows.js +22 -0
  313. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts +35 -0
  314. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts.map +1 -0
  315. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.js +91 -0
  316. package/src/engine/graphics3/instantiate_scene_bundle.d.ts +0 -54
  317. package/src/engine/graphics3/instantiate_scene_bundle.d.ts.map +0 -1
  318. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts +0 -39
  319. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts.map +0 -1
  320. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts +0 -35
  321. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts.map +0 -1
  322. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts +0 -96
  323. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts.map +0 -1
  324. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts +0 -20
  325. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts.map +0 -1
  326. package/src/shade/renderer/animation/skin_bind_transform.d.ts +0 -25
  327. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +0 -1
  328. package/src/shade/renderer/animation/skin_frame_defect.d.ts +0 -34
  329. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +0 -1
  330. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +0 -24
  331. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +0 -1
  332. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +0 -70
  333. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +0 -1
  334. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts +0 -96
  335. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts.map +0 -1
  336. package/src/shade/renderer/scene/InstanceBatch.d.ts +0 -92
  337. package/src/shade/renderer/scene/InstanceBatch.d.ts.map +0 -1
  338. package/src/shade/renderer/scene/Mesh.d.ts +0 -55
  339. package/src/shade/renderer/scene/Mesh.d.ts.map +0 -1
  340. package/src/shade/renderer/scene/Node3D.d.ts +0 -159
  341. package/src/shade/renderer/scene/Node3D.d.ts.map +0 -1
  342. package/src/shade/renderer/scene/Node3DDirtyList.d.ts +0 -59
  343. package/src/shade/renderer/scene/Node3DDirtyList.d.ts.map +0 -1
  344. package/src/shade/renderer/scene/SceneNodeBinding.d.ts +0 -52
  345. package/src/shade/renderer/scene/SceneNodeBinding.d.ts.map +0 -1
  346. package/src/shade/renderer/scene/SkinnedMesh.d.ts +0 -27
  347. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +0 -1
  348. package/src/shade/renderer/scene/TransformAuthority.d.ts +0 -19
  349. package/src/shade/renderer/scene/TransformAuthority.d.ts.map +0 -1
@@ -1,7 +1,8 @@
1
1
  # Cloth — Design
2
2
 
3
- > Design document. Written 2026-09-10; **M0, M1, M2 and M3's rigged path are implemented**, and
4
- > every place they contradicted it has been corrected in place with the measurement named beside it.
3
+ > Design document. Written 2026-09-10; **M0 through M5 are implemented** — the solver, chains,
4
+ > collision, the rigged path, self-collision and the worker — and every place they contradicted it has
5
+ > been corrected in place with the measurement named beside it.
5
6
  > §17 says what is done and what is not; `MEASUREMENTS.md` is the evidence.
6
7
  >
7
8
  > Scope: a cloth and soft-chain simulator that lives inside `engine/physics`, collides against the
@@ -133,12 +134,19 @@ documented as *not* serialised because shapes are shared assets the pipeline wir
133
134
  `ParticleEffect` holds a `ParticleLayout` and a `ParticleProgram` the same way. `ClothRig` serialises
134
135
  the proxy's identity and the skin ordinal, and the asset pipeline resolves the rest.
135
136
 
136
- **The proxy addresses joints by index, never by name.** A joint is a `Node3D` inside a
137
- `SceneBundleInstance`, not an entity, so an `EntityReference` to one is impossible by construction —
138
- and reintroducing strings at runtime to work around that would be the wrong answer. Instead the
139
- binding is resolved **once, at import**: the builder has `SceneBundleInstance#nodes` (the by-name map
140
- the engine keeps because sockets and effect anchors are authored as bone names) and turns names into
141
- integer indices into `Skin#joints`. The runtime component holds integers.
137
+ **The proxy addresses joints by index, never by name.** The binding is resolved **once, at import**:
138
+ the builder turns authored bone names into integer indices into `Skin#joints`, and the runtime
139
+ component holds integers. Reintroducing strings at runtime would be the wrong answer.
140
+
141
+ **A joint is an entity, and this paragraph used to say it could not be.** It said "a joint is a
142
+ `Node3D` inside a `SceneBundleInstance`, not an entity, so an `EntityReference` to one is impossible
143
+ by construction", and the engine moved: a model is spawned by `prefab_instantiate`, its joints *are*
144
+ entities, each carrying a `TransformAttachment` holding its offset from its parent joint, and that
145
+ attachment is what `cloth_write_back_rig` writes. Names are resolved through `Prefab`/`PrefabInstance`
146
+ rather than through `SceneBundleInstance#nodes`. The *conclusion* is unchanged and is now better
147
+ supported — indices into `Skin#joints` are stable across respawns where entity ids are not, so the
148
+ proxy still holds integers — but it is a choice rather than an impossibility. §11.2 and §13 carried
149
+ the same description.
142
150
 
143
151
  ### 3.3 `ClothDynamics`
144
152
 
@@ -155,7 +163,7 @@ export class ClothDynamics {
155
163
  drag = 0.1; // 0..1 — aerodynamic coupling to the air (§10)
156
164
  substeps = 2; // solver budget
157
165
  iterations = 4; // solver budget
158
- flags = 0; // ClothDynamicsFlags.SelfCollision, ...
166
+ flags = ClothDynamicsFlags.StrainLimit; // | SelfCollision, ...
159
167
  }
160
168
  ```
161
169
 
@@ -271,7 +279,8 @@ engine/physics/cloth/
271
279
  cloth_dynamics_map.js normalised coordinates → row parameters; every constant measured
272
280
  ClothCollider.js component: the RigidBody-analogue for collision
273
281
  ClothColliderFlags.js
274
- ClothSystem.js main-thread system: gather, step, write back
282
+ ClothWorld.js every cloth in a scene, and everything that is not arithmetic
283
+ ClothSystem.js main-thread system: a ClothWorld stepped inline
275
284
  ClothInstance.js one live cloth: the binding between a subtree and a ClothState
276
285
  cloth_seed_subtree.js §11.1's walk: entities → node_parent + animated_position
277
286
  cloth_seed_rig.js §11.2's proxy: joints → node_parent + animated_position
@@ -280,14 +289,14 @@ engine/physics/cloth/
280
289
  cloth_write_back.js that pose → Transform64 + TransformAttachment
281
290
  cloth_write_back_rig.js that pose → Node3D.transform_local
282
291
  cloth_gather_colliders.js the index's overlapping records, in the cloth's own frame
283
- WorkerClothSystem.js drop-in worker variant (WorkerSystem subclass)
284
- ClothWorkerCore.js
285
- cloth.worker.js
286
- makeClothWorker.js
287
- cloth_worker_protocol.js
288
- ClothSerializationAdapter.js
289
- ClothRigSerializationAdapter.js
290
- ClothColliderSerializationAdapter.js
292
+ WorkerClothSystem.js drop-in worker variant: the same ClothWorld, stepped over there
293
+ ClothWorkerCore.js worker side: a ClothState view per registered cloth
294
+ cloth.worker.js the entry point, a real module worker
295
+ makeClothWorker.js the spawn expression, isolated because it needs import.meta
296
+ cloth_worker_protocol.js the command region: a count, then a block per cloth
297
+ ClothSerializationAdapter.js the component + its ClothDynamics, by value
298
+ ClothColliderSerializationAdapter.js five authored scalars, the same way
299
+ ClothRigSerializationAdapter.js NOT BUILT — §3.2's asset identity, and the proxy codec
291
300
  collider/
292
301
  ClothColliderIndex.js the index of live cloth colliders + their swept AABBs
293
302
  ClothColliderRecord.js the packed layout: kind, params, both poses, bounds, interpolated pose
@@ -295,10 +304,17 @@ engine/physics/cloth/
295
304
  cloth_collider_pack.js AbstractShape3D → packed record
296
305
  cloth_collider_bounds.js the swept AABB, shared by the index and each cloth's own table
297
306
  cloth_collider_sdf.js pose interpolation, localisation, and the field dispatch
307
+ wind/
308
+ AbstractClothWind.js §10's one interface: sample the air, and say whether it varies
309
+ ClothAmbientWind.js a constant plus a band-limited gust — the air where no field is
310
+ ClothFluidWind.js every live FluidComponent, with the ambient underneath it
311
+ cloth_sample_air.js the air into the cloth's own frame, on the main thread
298
312
  solver/
299
313
  ClothState.js SoA particle + topology + constraint storage over one buffer
314
+ cloth_step_instance.js one cloth, one step, over nothing but memory — what crosses
300
315
  cloth_step.js the fixed step: predict, warm start, iterate, dual update, integrate
301
316
  cloth_solve_vertex.js the per-vertex 3x3 block update
317
+ cloth_wind_accelerate.js §10's aerodynamic term, and the factor that makes a flag a flag
302
318
  constraint/ one module per row type (§6)
303
319
  cloth_color_particles.js graph colouring of the particle graph
304
320
  constraint/cloth_leash_row.js §6's slack row and its tether row, which are one row model
@@ -340,10 +356,19 @@ Two systems:
340
356
  - **`ClothSystem`** owns the simulation. Per fixed step: refresh the animated targets, gather the
341
357
  overlapping colliders from the index, step, write back.
342
358
 
343
- Cloth runs at the fixed rate and is interpolated for display, exactly as `PhysicsSystem` does — the
344
- same producer/`__interp_restore` contract, so a cloth-driven `Transform64` is authoritative during the
345
- fixed pass and interpolated outside it. Reusing the existing contract rather than inventing a second
346
- one is what keeps a cloth-driven bone from fighting an animation-driven one.
359
+ **M5 note: it owns it jointly with `WorkerClothSystem`, and "two systems" is now three.** The two
360
+ are interchangeable — same components, same declared access, same position in the derived order —
361
+ and they share a `ClothWorld`, which is the registry and every phase of a step that is not
362
+ arithmetic. `ClothSystem` calls `cloth_step_instance` between the two halves; `WorkerClothSystem`
363
+ puts a thread there. §12 has why that is what makes the bit-parity gate structural.
364
+
365
+ Cloth runs at the fixed rate and its poses are **held** between ticks rather than interpolated, which
366
+ on a display faster than the fixed step is visible and is not wrong. The first draft of this paragraph
367
+ said cloth reuses `PhysicsSystem`'s producer/`__interp_restore` contract; M5 measured that it cannot —
368
+ that contract is entity-keyed and a rigged garment's joints are not entities — and §13.1 and §18 carry
369
+ what the cloth-shaped version would be and what it would cost. What keeps a cloth-driven bone from
370
+ fighting an animation-driven one is not interpolation but `TransformAuthority`, which `cloth_seed_rig`
371
+ claims (§11.2, `MEASUREMENTS.md` §25).
347
372
 
348
373
  ### 4.1 Ordering, and why the answer is not to be ordered
349
374
 
@@ -502,10 +527,22 @@ are in `MEASUREMENTS.md`; the corrections are folded into the listing above and
502
527
  it has no effect at all. The ramp exists for **transient** rows, a contact that should arrive soft
503
528
  rather than shock the mesh on its first frame, and M0 has none. M2 has to measure it.
504
529
 
505
- Chosen: **α = 0.25, γ = 1, β = 1e5 (unexercised), k_min = k_max = 8·(m/h²)**. γ sits at 1 because its
506
- second job — decaying the penalty — has nothing to do while the band is a single point, so the whole
507
- decay lives in α, whose only job is the multiplier. When M2's contacts make the band real, γ becomes
508
- live and has to be re-measured.
530
+ Chosen at M0: **α = 0.25, γ = 1, β = 1e5 (unexercised), k_min = k_max = 8·(m/h²)**. γ sits at 1
531
+ because its second job — decaying the penalty — has nothing to do while the band is a single point, so
532
+ the whole decay lives in α, whose only job is the multiplier.
533
+
534
+ **M2 made the band real and the line above is out of date in three places.** `β` is **1e3**, not 1e5:
535
+ M2's contacts are the first transient rows in the system and the ramp is finally live, and both ends
536
+ of the range measured worse than the middle — no ramp at all settles at 1.95 J of residual kinetic
537
+ energy against 0.12 J at 1e3, a fast ramp gives up most of what the low floor bought
538
+ (`MEASUREMENTS.md` §17, and `cloth_solver_constants.js`'s own docblock carries the argument). The band
539
+ is no longer a single point for a **contact**: `CLOTH_CONTACT_K_MIN_RATIO = 0.25` is its floor,
540
+ against a ceiling of `8·(m/h²)` — a margin decision in the middle of a flat region, with two decades
541
+ of ramp above it. And `γ` **is still 1, and was never re-measured**: this paragraph said it would be
542
+ "when M2's contacts make the band real", M2 made the band real, and the sweep was not run. That is now
543
+ a decision rather than an omission — nothing measured since has asked for a decaying penalty, and the
544
+ one place it would help is recorded in §18 as a coupling rather than as a bug — but it is an
545
+ **unmeasured** constant and the only one left in the set, so it is named here as one.
509
546
 
510
547
  **Adaptive initialisation** is VBD's: seed the iteration at `x + h·v + h²·ã` where `ã` interpolates
511
548
  between zero and full external acceleration according to whether the previous step looked like free
@@ -721,8 +758,16 @@ what the fabric row does unaided and it is simply inert.
721
758
  So **`stretch` is the weakest field in §3.3's parameter set**, and the honest statement is that at low
722
759
  budgets the bound belongs to the budget rather than to the fabric. The row is worth its 13–15% of a
723
760
  garment frame where the residual is visible and is not worth it where the residual is already around
724
- one per cent, which is why it is a flag — `ClothDynamicsFlags.StrainLimit` — rather than always on,
725
- and why the library entries at 2×4 clear it. `MEASUREMENTS.md` §11.
761
+ one per cent, which is why it is a flag — `ClothDynamicsFlags.StrainLimit` — rather than always on.
762
+
763
+ **And the flag defaults to *set*, which is the opposite of what §3.3 used to show.** `ClothDynamics`
764
+ starts at `ClothDynamicsFlags.StrainLimit`, so a garment that says nothing gets the row. Of the four
765
+ library entries at 2×4, three clear it — silk, cotton and hair, and they clear it by writing
766
+ `flags: ClothDynamicsFlags.SelfCollision`, which *replaces* the default rather than adding to it — and
767
+ **`CLOTH_ROPE` does not**: it sets no flags at all, so it keeps the default and runs the strain-limit
768
+ row at 2×4. That is the right answer for a rope, whose `stretch` is its most characteristic property
769
+ and whose forty particles make the row free (`cloth_dynamics_library.js` says exactly that), but "the
770
+ library entries at 2×4 clear it" was never true of it. `MEASUREMENTS.md` §11.
726
771
 
727
772
  ---
728
773
 
@@ -760,8 +805,12 @@ descent direction, and the update takes a full step (§5.3). What is asserted in
760
805
  every LDLᵀ pivot positive on every update (which *is* positive definiteness, checked where it is
761
806
  used); nothing non-finite across 10,000 steps of absurd cloth; and full-step energy increases bounded
762
807
  and rare — 0.004% of updates on a realistic cape, never above a fifth of the local energy scale, and
763
- 1.5% / 1.0 across the absurd fuzz. Those measured figures are the thresholds the standing gate holds
764
- the solver to, so a change that makes it worse fails.
808
+ 1.5% / 1.0 across the absurd fuzz. **The gate is those figures doubled, not those figures**: the spec
809
+ holds 3% / 2 on the absurd fuzz and 0.01% / 0.5 on the cape, which is twice each measurement rather
810
+ than the measurement itself. The last sentence of this paragraph used to claim otherwise. The margin
811
+ is deliberate — a full step's energy behaviour is a distribution over a fuzz corpus and a gate pinned
812
+ to its own sample is a gate that fails on an unrelated change — but "a change that makes it worse
813
+ fails" is true only of a change that makes it *twice* as bad.
765
814
 
766
815
  **P2 — Over-stretch, the rubber cape.**
767
816
  *Mechanism:* long-range attachments (Kim et al. 2012). Each particle carries the geodesic distance to
@@ -806,12 +855,35 @@ of `θ` it scales the shape it poses by `cos(θ/2)`. At the half-radian per step
806
855
  `CLOTH_ANCHOR_ROTATION_LIMIT` already calls a spin, a 25 cm collider loses 7.8 mm — twice a silk
807
856
  garment's thickness — halfway through every step. `MEASUREMENTS.md` §19.
808
857
 
809
- *Limit, stated:* both halves are swept in translation first. A collider spinning fast enough that a
858
+ **Post-M6 found the mirror case, and M2 had argued it away rather than measured it.** (a) folds the
859
+ *collider's* travel into the discovery reach and nothing folds in the *particle's*, so a **static**
860
+ collider is only ever discovered within `2·(thickness + inflation)` — 16 mm for a garment, which is
861
+ half a metre a second of approach. The docblock said anything faster was already bounded by §7 P1's
862
+ velocity clamp; that clamp is `4 · shortest_row · substeps · iterations / dt`, about **154 m/s** for a
863
+ garment, and everything in between was uncovered. Measured, one particle at 8 mm thrown at a static
864
+ 3 cm capsule with no gravity at 2 × 4: **no row is ever created before the particle is already
865
+ inside, at any speed**, and from **3 m/s** the row the next step finally makes is evaluated on the far
866
+ side and pushes it the rest of the way out — which is P4(a)'s orientation-over-history choice
867
+ collecting its bill when discovery is late. The reach now carries `|v_i|·dt` per particle as well.
868
+ That is free at rest and 0.8% of a step for a cloth falling at 3 m/s, and `cloth_library_bench.mjs`
869
+ cannot see it because the bench has no colliders. `MEASUREMENTS.md` §54.
870
+
871
+ *Limit, stated:* all three are swept in translation first. A collider spinning fast enough that a
810
872
  point on its surface travels more than its own radius within one substep can still be missed, because
811
873
  the discovery margin is built from the *centre's* travel. Full CCD for cloth is out of scope.
874
+
875
+ *And the second limit, which post-M6 measured rather than assumed:* **discovery being right is
876
+ necessary and not sufficient.** A row that exists is still evaluated at substep positions, so a
877
+ particle crossing a whole collider inside one substep has a row that never sees it touching. Swept,
878
+ in half-metre steps from 1 to 12 m/s, the particle is stopped up to **6.0 m/s at two substeps and
879
+ through the whole range at four** — a clean threshold, and it is `ClothDynamics#substeps` rather than
880
+ the reach. This row has always said "more substeps in the affected `ClothDynamics`"; that is where it
881
+ lands.
812
882
  *Spec:* a capsule driven through a hanging sheet at 2 to 40 m/s, swept against unswept; assert the
813
883
  swept pass creates rows at every speed and that no particle spends a step more than half a collider
814
- radius inside. `cloth_collision.spec.js` carries the ECS-level companion.
884
+ radius inside. The mirror — a particle thrown at a *static* capsule from 1 to 10 m/s, with a step
885
+ length of zero standing in for the unswept side — is in `cloth_contact_row.spec.js`.
886
+ `cloth_collision.spec.js` carries the ECS-level companion of both.
815
887
 
816
888
  **P4 — Folds, tangles, and stays stuck.** The named requirement, so four mechanisms:
817
889
 
@@ -851,10 +923,21 @@ radius inside. `cloth_collision.spec.js` carries the ECS-level companion.
851
923
  solver whose whole design commitment is that it needs none, and no authored scene is that
852
924
  symmetric. `MEASUREMENTS.md` §22.
853
925
 
854
- *(d) A bounded escape hatch.* If a particle's constraint residual stays above a threshold for N
855
- consecutive steps, it blends to its animated position over a short window. Local, bounded, on by
856
- default with a conservative threshold, and counted in `ClothDiagnostics` so that a garment relying on
857
- it is visible rather than silently rescued.
926
+ *(d) A bounded escape hatch. **NOT BUILT, and this row is the design for one rather than a
927
+ description of one.*** If a particle's constraint residual stays above a threshold for N consecutive
928
+ steps, it blends to its animated position over a short window. Local, bounded, and counted in
929
+ `ClothDiagnostics` so that a garment relying on it is visible rather than silently rescued.
930
+
931
+ **It has been deferred at every milestone that could have built it** — M1 for want of a residual
932
+ monitor, M2 to M4 on the grounds that self-collision is where a particle gets *pinched* rather than
933
+ merely held, M4 onward as still open — and it is deferred deliberately rather than forgotten: M2
934
+ measured the one stuck configuration whose residual is **zero** (§7 P4's own note, `MEASUREMENTS.md`
935
+ §22), which is the quantity this row watches, so the mechanism would not have caught the case that
936
+ motivated looking for it. The sentence that used to stand here said "on by default with a
937
+ conservative threshold", which read as a shipped default and was never true of any build.
938
+ `ClothDiagnostics` is not built either; every reference to it in this document is a design for one.
939
+ What *is* shipped and does the job an author reaches for is `Cloth#blend` (§3.1), which is the same
940
+ blend applied deliberately instead of automatically.
858
941
 
859
942
  *Spec:* deliberately invert half a sheet through itself, run, assert the residual falls below threshold
860
943
  and the mesh is self-intersection-free within N steps; separately assert the escape hatch is not
@@ -863,7 +946,38 @@ triggered by a correctly authored garment under normal animation.
863
946
  **P5 — Jitter at rest.**
864
947
  *Mechanism:* the multiplier decay `α·γ < 1` bleeds accumulated force out of a resting configuration so
865
948
  it cannot ring; per-cloth sleep when the maximum particle displacement over a window falls below a
866
- threshold; wake on collider motion, anchor motion, wind change, or a `ClothDynamics` change.
949
+ threshold; wake on collider motion, anchor motion, wind change, a collider arriving or leaving, or a
950
+ `ClothDynamics` change.
951
+
952
+ **Post-M6 note, and "wake on collider motion" was the wrong question.** Motion is what the test asked
953
+ and it is not what a sleeping cloth needs to know. `ClothColliderSystem#link` **places** a new
954
+ collider — previous pose equal to current, deliberately, so its first step sweeps nothing — so a
955
+ collider *spawned* inside a sleeping cloth has not moved and never woke it. Measured: a 6-link rope
956
+ settled and asleep, a sphere of radius 0.2 spawned around its lower half, still asleep 300 steps later
957
+ with **4 of its 6 links inside**. §1's first commitment names exactly this case. The test now also
958
+ compares the **set** of handles the broadphase returns with the set the cloth last stepped against —
959
+ one integer per record, against a pose comparison that was already there — so a collider appearing or
960
+ leaving wakes it whatever the poses say. Its own limit, stated: a `PlaneShape3D`'s AABB is unbounded,
961
+ so a plane is in every cloth's set the moment it exists and every sleeping cloth wakes once when one
962
+ appears anywhere in the scene. It finds nothing, and sleeps again thirty steps later.
963
+
964
+ **Post-M6 note, and it is this row's own sentence coming true two milestones late.** "Maximum particle
965
+ displacement over a window" is what this row has said since M0 and it is not what M1 built: M1's test
966
+ is the largest particle **speed** the step produced, held under `CLOTH_SLEEP_SPEED_RATIO · reach` for
967
+ `CLOTH_SLEEP_STEPS`. On a free-hanging chain the two agree. On a cloth **resting on a collider** they
968
+ do not, and the gap is total: four of the seven library fabrics — cotton on a floor with or without
969
+ friction, leather and rope on a floor with it — **never sleep at all**, where the same rope with no
970
+ collider under it sleeps at step 283. The cause is not friction, which the frictionless column rules
971
+ out; it is that a contact's penalty ramps by `β·|C|` and `γ = 1` never brings it down, so what a
972
+ resting cloth settles into is a **bounded limit cycle** rather than a rest state: measured on cotton,
973
+ a tenth of a millimetre of travel a step — invisible — at a peak speed of 1.5e-2 m/s, eighteen times
974
+ the threshold. Displacement tells those apart and an instantaneous speed cannot. The sleep test now
975
+ reads displacement from a window anchor, at `CLOTH_SLEEP_DISPLACEMENT_RATIO = 5e-4` of reach, chosen
976
+ to be no *looser* than the speed test it replaces rather than the fastest value that works; every
977
+ entry on a floor and on a sphere then sleeps within **961 steps**, and the free-hanging column gets
978
+ uniformly faster rather than slower. The limit cycle itself is recorded and not fixed — `γ` is
979
+ unmeasured-by-decision (§5.2) and a tenth of a millimetre is not a rendering problem.
980
+ `MEASUREMENTS.md` §53.
867
981
  *M0 found the gap, and it is wider than "nothing is damping it".* The mechanism reaches **hard** rows
868
982
  only, because a soft row has no multiplier to decay — and a soft row whose penalty exceeds about
869
983
  `8·(m/h²)` does not merely fail to dissipate, it makes the step **add** energy. Measured on a hanging
@@ -874,9 +988,19 @@ which arrives with the component in M1 — so **damping is load-bearing for P5 r
874
988
  control**, and the M1 spec has to gate on a soft cloth actually settling. `MEASUREMENTS.md` §6.
875
989
  *Spec:* a settled cloth reaches sleep within a bounded number of steps and its positions are bit-identical
876
990
  across the following 300.
991
+ **M6 note: a cloth also wakes on the air changing, and the test has one sample in it.** §10's "sleep
992
+ wakes on a wind change" is the same shape as the collider test beside it — ask a question three orders
993
+ of magnitude cheaper than the step it decides whether to skip — and the question is the air at the
994
+ cloth's own centre against the air the last *applied* step saw. The last applied rather than the last
995
+ sampled, so a gust that builds slowly accumulates instead of being reset by every step that declined
996
+ to act on it. *Its limit, stated:* one point. A gust that reaches only the hem of a sleeping banner
997
+ and leaves its middle still will not wake it. The collider test has no such gap because a collider's
998
+ pose is seven exact numbers and a field is a continuum.
999
+
877
1000
  *M1 landed both halves and measured them.* Every library fabric, on a 12-link chain set swinging at
878
1001
  3 m/s, sleeps: **220 steps for denim, 770 for silk**, everything else between, and the order is the
879
- damping order. Sleep stops the **write-back** as well as the step, which is what makes "bit-identical"
1002
+ damping order. (Those two figures are the speed test's; the post-M6 note above replaced it, and every
1003
+ entry sleeps sooner under the displacement window than it did under the speed one.) Sleep stops the **write-back** as well as the step, which is what makes "bit-identical"
880
1004
  a fact rather than a hope — a pose that is not rewritten cannot drift, and one rewritten from unchanged
881
1005
  inputs is only *probably* identical. A cloth wakes on anchor motion, on a `ClothDynamics` change, on a
882
1006
  `blend` change, and on an explicit `ClothSystem#wake` for anything it cannot see.
@@ -981,8 +1105,18 @@ loop, so §14 makes the analytic gradients shared code that both sides use.
981
1105
  ### 8.3 Friction
982
1106
 
983
1107
  Coulomb friction as a row on the same contact, with `μ` combining `Collider#friction` with
984
- `ClothCollider#friction_scale`. The combine rule is the one `physics/contact/combine_material.js`
985
- already applies, so cloth and rigid bodies agree about what a surface feels like.
1108
+ `ClothCollider#friction_scale`.
1109
+
1110
+ **The combine rule is *not* `physics/contact/combine_material.js`, and this paragraph used to say it
1111
+ was.** Nothing in `cloth/` imports that module. `combine_material` answers a different question — how
1112
+ two *bodies'* materials combine into one contact coefficient, which is a `sqrt(μ₁·μ₂)`-shaped rule
1113
+ over a symmetric pair — and cloth's question is not symmetric: one side is a collider with a material
1114
+ and the other is a garment with an opt-in scale on top of it. `ClothColliderSystem` therefore packs
1115
+ `Collider#friction · ClothCollider#friction_scale` into the index record, one multiply at link time,
1116
+ and a `friction_scale` of 1 means "whatever this surface already is". The *claim* the old sentence was
1117
+ making — that cloth and rigid bodies agree about what a surface feels like — is still true and is why
1118
+ `Collider#friction` is the base: what is not true is that the agreement comes from sharing a function.
1119
+ §13's reuse ledger carried the same wrong entry.
986
1120
 
987
1121
  **M2 made three of those words exact.** The clamp is `[0, μ·f_n]` rather than `±μ·f_n`, because the
988
1122
  row's `C` is the *magnitude* of the tangential slip — §6 carries the derivation and what writing it
@@ -1090,8 +1224,7 @@ the bodies in the scene.
1090
1224
 
1091
1225
  So the design is: **a cloth overlapping a `FluidComponent` samples its velocity field per particle and
1092
1226
  applies aerodynamic drag against it**, scaled by `ClothDynamics#drag`. The term is an external
1093
- acceleration, `a_drag = drag · (u_air − v) · |n·û|`, with the normal-facing factor making a sheet catch
1094
- the wind broadside and slice through it edge-on — the difference between a flag and a rope.
1227
+ acceleration, `a_drag = response · facing · (u_air − v)`.
1095
1228
 
1096
1229
  The payoff is that a character running past a cape gets a wake, because the fluid solver already
1097
1230
  produces one; a fire's convection lifts a banner; a door slamming pushes a curtain. None of that needs
@@ -1106,8 +1239,69 @@ Cost control: sampling is per particle per *step*, not per iteration, and a clot
1106
1239
  field's bounds skips it entirely. Sleep (P5) wakes on a wind change so a settled banner does not stay
1107
1240
  asleep through a gust.
1108
1241
 
1109
- This is deferred to M6 — it is the feature most likely to be cut for schedule, and nothing else depends
1110
- on it.
1242
+ **M6 built it and corrected this section in four places.**
1243
+
1244
+ **The factor above is not `|n·û|`, because half of what this system simulates has no `n`.** This
1245
+ paragraph originally wrote the term as `a_drag = drag · (u_air − v) · |n·û|`, "with the normal-facing
1246
+ factor making a sheet catch the wind broadside and slice through it edge-on — the difference between a
1247
+ flag and a rope". It is right about what it wants and wrong that a normal is available to want it
1248
+ with. A chain's particles span a **line** — every direction perpendicular to it is equally a normal,
1249
+ so picking one is the history-dependent fabrication §7 P4(a) exists to keep out — and a rope's
1250
+ aerodynamics are the *complement* of a sheet's anyway: it catches the flow across it and slices along
1251
+ it. Both are one quantity, **the part of the flow the particle's own incident rows cannot let past**:
1252
+ take the span `T` of the directions to its stretch-row neighbours in the current configuration, and
1253
+ `facing = ‖û − proj_T(û)‖`.
1254
+
1255
+ | rank of `T` | what the particle is | the factor |
1256
+ |---|---|---|
1257
+ | 2 | a point on a surface | `\|û·n\|` with `n = b₁×b₂` — this paragraph's own expression |
1258
+ | 1 | a link of a chain | `√(1 − (û·t)²)` — the cross-flow, which is the rope's answer |
1259
+ | 0 | an isolated particle | `1` — a bead has no preferred direction |
1260
+
1261
+ One expression, three topologies, no stored normal, no orientation, no sign, nothing carried between
1262
+ steps. `MEASUREMENTS.md` §47.
1263
+
1264
+ **And the factor's headline claim is about *laminar* air, which is a limit rather than a fault.** On
1265
+ the playground's banner, turning a uniform wind through ninety degrees takes the mean factor from
1266
+ 0.221 to **0.000** and the aerodynamic acceleration from 43.2 m/s² to 0.08 — a flag becoming a rope,
1267
+ exactly as promised. Turning a **wake** through ninety degrees does nothing at all: 0.216 to 0.280. A
1268
+ wake has velocity in every direction, so no orientation of a surface is edge-on to one, and the factor
1269
+ is correctly reporting that there is nothing to slice through. That makes this section's argument for
1270
+ reading a simulation *stronger* than this section knew. `MEASUREMENTS.md` §51.
1271
+
1272
+ **`drag` is not what multiplies the flow, and the difference is a stability argument.** One external
1273
+ acceleration is held for the whole fixed step, so an explicit `rate · (u_air − v)` overshoots the air
1274
+ once `rate·dt > 1` and diverges past 2 — and §1's first commitment is "not at 5 fps", which is a `dt`
1275
+ of 0.2 s and a rate of 10. What the step multiplies by is the **per-step response**
1276
+ `(1 − e^(−rate·dt))/dt` (`cloth_drag_response`), the exact solution of the same linear ODE, which
1277
+ closes a fraction of the gap that is strictly below one for every rate and every step length. It is
1278
+ also the step `GlobalFluidEffector` already takes for the air itself, which is a symmetry rather than
1279
+ a coincidence: both are relaxations toward a velocity. `MEASUREMENTS.md` §49.
1280
+
1281
+ **The field answers in grid cells per second and cloth needs metres.** `FluidField#sampleVelocity`
1282
+ documents that it deliberately does not rescale, "on the assumption that callers (vegetation sway,
1283
+ dust drift) interpret the field as a direction × magnitude wind". Cloth is not such a caller: its row
1284
+ is a relaxation toward a velocity, so without the multiply by `cell_size` the same wind voxelised at
1285
+ 0.25 m instead of 1 m blows a garment four times as hard. `ClothFluidWind` converts.
1286
+ `MEASUREMENTS.md` §46.
1287
+
1288
+ **And this section never said which thread samples, which M5 made a question.** It cannot be the
1289
+ worker's: a fluid field is a hundred thousand floats its own solver rewrites every step and may itself
1290
+ live on another worker, while the *answer* is three floats a particle. So `cloth_sample_air` runs on
1291
+ the **main thread** in both systems and writes into `ClothState#air_velocity` — a region of the
1292
+ cloth's own `SharedArrayBuffer`, so nothing is copied — and everything built from it runs in
1293
+ `cloth_step_instance` with the rest of the arithmetic, which is what keeps §12's parity structural.
1294
+ Measured: 58–62 ns a particle for a field sample, a twenty-fifth of that for the ambient (one sample
1295
+ for the whole cloth, which is what §10's "skips it entirely" is worth), and one per cent of a step for
1296
+ the term itself. `MEASUREMENTS.md` §48, §49.
1297
+
1298
+ **Where the air comes from is a scene's choice and there is no default.** `ClothWorld#wind` is `null`
1299
+ until something assigns a source, exactly as the collider index is: a scene without one gets M5's
1300
+ cloth rather than an error. Null rather than a still-air ambient, because a source answering zero
1301
+ everywhere is not "no wind" — the row is a relaxation toward the air, so still air still resists a
1302
+ *moving* cloth, anisotropically. That is better physics than `ClothDynamics#damping`'s isotropic decay
1303
+ and it is not free, so it is a thing a scene asks for. What a cloth does in air that is not moving is
1304
+ `damping`'s job (§7 P5, where it is load-bearing); what it does in air that *is* is this.
1111
1305
 
1112
1306
  ---
1113
1307
 
@@ -1148,6 +1342,19 @@ for the same reason — a local transform is what a joint angle *is*.
1148
1342
  The authored pose is not lost by being overwritten. It is captured once, when the cloth seeds, and
1149
1343
  lives in the cloth from then on — which is what `blend = 0` returns to, what the leash centres are
1150
1344
  measured from, and what a re-seed of a live cloth reads instead of the shape the solver left behind.
1345
+
1346
+ **Post-M6 found both halves of that last clause to be false in the code**, and they are the same
1347
+ mistake twice: the authored pose lives in the cloth, so the cloth is the only thing that can give it
1348
+ back, and it did not. `ClothWorld#reseed` cleared `ClothInstance#seeded` *before* the walk, and the
1349
+ capture was keyed on `seeded` — so the one path that exists to re-walk a live cloth was the one path
1350
+ that could remember nothing, and `read_rest_pose` fell back to `TransformAttachment#transform`, which
1351
+ this very paragraph says the write-back has been overwriting every step. Reproduced on a 6-link rope
1352
+ authored horizontally and settled for 300 steps: a re-seed took the authored tip from **x 0.600,
1353
+ y −0.012 to x −0.003, y −0.600**, and `blend = 0` then landed on the sagged pose. The capture is now
1354
+ keyed on there *being* a previous capture. And the same for removal: `unlink` dropped the registry
1355
+ entry and nothing put the authored local transforms back, so a `Cloth` removed left its subtree frozen
1356
+ in the solver's last shape and a `Cloth` added back seeded *that* as the pose it returns to.
1357
+ `cloth_release_subtree` is the other half of the seed, announced once on the anchor.
1151
1358
  The consequence, stated: **in the entity path the animated pose is constant in the anchor's frame.**
1152
1359
  That is a property of the case rather than a limitation of the code — a subtree's authored pose is
1153
1360
  rigid relative to its anchor — but it does mean a subtree whose *links* are independently animated is
@@ -1184,15 +1391,22 @@ how anything moves.
1184
1391
 
1185
1392
  ### 11.2 Skinned garments — `Cloth` + `ClothRig`
1186
1393
 
1187
- A character's cape is not an entity hierarchy and cannot be made into one.
1188
- `shade_node_to_entity_composition` expands a model into entities and attaches a `ShadedGeometry` to
1189
- each node that draws — but a skinned node contributes its transform and its name and **no geometry**,
1190
- because its vertices deform against a skeleton the renderer owns and the GPU poses. Its joints are
1191
- `Node3D`s inside the entity's `SceneBundleInstance`, and making them entities is explicitly a design
1192
- this program does not do (D47). So for a skinned garment there is no `ShadedGeometry` component and no
1193
- bone entities: the geometry is registered with `GPUAnimationManager` through
1194
- `register_instance_animation`, and the only writable handle is the joint's `Node3D.transform_local` —
1195
- the same handle the animation systems write.
1394
+ A character's cape is not an entity hierarchy **of its own**: a skinned node contributes its transform
1395
+ and its name and **no geometry**, because its vertices deform against a skeleton the renderer owns and
1396
+ the GPU poses. The geometry is registered with `GPUAnimationManager`, and what cloth writes is a
1397
+ joint's offset from its parent joint.
1398
+
1399
+ **Where that offset lives is no longer a `Node3D`, and the three paragraphs that follow were written
1400
+ when it was.** M3 wrote `Node3D.transform_local` on joints inside a `SceneBundleInstance`, and this
1401
+ section still describes that, including a flat assertion that bone entities are "explicitly a design
1402
+ this program does not do (D47)". The engine's Rows-Not-Nodes work deleted `Node3D`: a model is spawned
1403
+ by `prefab_instantiate`, its joints **are** entities, and the writable handle is
1404
+ `TransformAttachment#transform` on the joint entity — which is what `cloth_seed_rig` collects and
1405
+ `cloth_write_back_rig` writes. Nothing about the design changes with it and one thing gets simpler:
1406
+ the rig path and the entity path now write the *same component*, so §11's "the solver is identical
1407
+ either way; only seeding and write-back differ" is now true of the write-back's data type as well as
1408
+ of its shape. Read `Node3D.transform_local` below as `TransformAttachment#transform` on a joint
1409
+ entity, and `SceneBundleInstance` as `PrefabInstance`.
1196
1410
 
1197
1411
  **And cloth has to *claim* those joints, which §11.2 originally read as though it did not.** The
1198
1412
  sentence above says the handle is "the same handle the animation systems write", and they do not
@@ -1208,6 +1422,14 @@ race the enum exists to prevent, and for a cape that is the point of simulating
1208
1422
  whose joints are also keyed it is an authoring error, and the claim is where it surfaces.
1209
1423
  `MEASUREMENTS.md` §25.
1210
1424
 
1425
+ **Post-M6: the claim is a loan, and it was written as a gift.** The consequence above is the right
1426
+ trade only while the cloth is *there*. `cloth_seed_rig` revoked the GPU's authority over every joint
1427
+ it drove and nothing ever re-granted it, so a garment removed left its joints owned by a CPU that had
1428
+ stopped writing them — neither animated nor simulated, stuck in the pose the solver last produced, and
1429
+ with no component left anywhere naming which joints those were. The seed now records exactly the
1430
+ joints its revoke *changed*, and `cloth_release_rig` hands those back when the `Cloth` is removed or
1431
+ the `ClothRig` is taken away; a joint that was already the CPU's before cloth arrived stays the CPU's.
1432
+
1211
1433
  **The anchor's world pose is computed, never read back.** A joint of an animated skeleton has no
1212
1434
  CPU-side `transform_global` — nothing maintains it — so the rig path evaluates the anchor with
1213
1435
  `pose_evaluate_world` over the clips the entity is playing, which is the engine's own sanctioned
@@ -1291,20 +1513,32 @@ For a **rigged** garment, only steps 5–8 run, over the joint graph rather than
1291
1513
  10. **Validate**: manifoldness, no zero-length edges, and — the P4(c) precondition — that the skinned
1292
1514
  bind pose is collision-free against the character's `ClothCollider` set.
1293
1515
 
1294
- Vertex painting (per-vertex stiffness or slack multiplier) is supported through an optional texture or
1295
- vertex-colour channel and is never required. Steps 5 and 6 are the default, and the intent is that most
1296
- garments never override them.
1516
+ Vertex painting (per-vertex stiffness or slack multiplier) — **not built.** The sentence that used to
1517
+ stand here said it "is supported through an optional texture or vertex-colour channel"; nothing reads
1518
+ a texture or a colour channel, and `ClothProxy` has no per-vertex override to read one into. Steps 5
1519
+ and 6 are the whole of what a garment gets, which is the design's own claim that most garments never
1520
+ need to override them, made by there being no override. The shape of one, if a garment ever needs it,
1521
+ is a per-particle multiplier beside the geodesic — one array, read where `slack` and `stretch` are
1522
+ read — and it is the kind of authored knob §3.4 spent a section removing, so it should arrive with a
1523
+ garment that demonstrably needs it rather than ahead of one.
1297
1524
 
1298
1525
  ---
1299
1526
 
1300
1527
  ## 12. Data layout, threading, the worker
1301
1528
 
1302
- **One `SharedArrayBuffer` for the whole cloth world**, not one per instance. `WorkerSystem`'s model is
1303
- a single shared buffer, a single command region and a single atomic publishing one in-flight step, and
1304
- `WorkerFluidSystem` already shows how a variable set of simulated things fits that model: the command
1305
- region carries a count and then a list of ids to step. Cloth does the same — each instance gets a
1306
- region inside the buffer, and the command region names which instances are live this step, so sleeping
1307
- (P5) and culled (§12.1) instances cost nothing but their absence from a list.
1529
+ **One `SharedArrayBuffer` per cloth**, and the command region names which cloths are live this step —
1530
+ so sleeping (P5) and culled (§12.1) cloths cost nothing but their absence from a list. `WorkerSystem`'s
1531
+ model is a single shared buffer, a single command region and a single atomic publishing one in-flight
1532
+ step, and `WorkerFluidSystem` shows how a variable set of simulated things fits it.
1533
+
1534
+ **M5 corrected "one buffer for the whole cloth world", and `reseed` is why.** A `ClothState` is sized
1535
+ at seed from *that cloth's* particle and row counts, and `ClothSystem#reseed` — a chain gaining a link,
1536
+ a `ClothDynamics` change the row tables depend on, a rigged garment whose model has just loaded —
1537
+ builds a different one while other cloths have steps in flight. A world buffer would therefore need an
1538
+ allocator with relocation, and a relocation invalidates every view of every region on both threads at
1539
+ once. One buffer per cloth costs one cold-plane message per seed (`add_cloth` / `remove_cloth`, the
1540
+ shape `WorkerFluidSystem` already has) and makes a re-seed a re-registration, which is the same code
1541
+ path as a first seed. `MEASUREMENTS.md` §39.
1308
1542
 
1309
1543
  SoA throughout:
1310
1544
 
@@ -1315,13 +1549,67 @@ SoA throughout:
1315
1549
  | topology | `Int32Array` | edge pairs, bend stencils, colour ranges, tether anchors |
1316
1550
  | rest data | `Float32Array` | rest lengths, bend weights, `d_geo` |
1317
1551
  | duals | `Float32Array` | `λ` and `k` per persistent constraint |
1318
- | colliders | `Float32Array` | packed records for this step |
1552
+ | colliders | `Float32Array` | packed records for this step — `CLOTH_COLLIDER_SLOTS` of them |
1319
1553
  | contacts | `Float32Array` | transient rows + their warm-start `λ`, keyed by (particle, collider slot) |
1320
1554
 
1321
- `ClothDynamics` reaches the worker through the command region, written every step — a dozen words into
1322
- memory the worker already shares, cheaper than tracking dirtiness, and it keeps
1555
+ The **step's parameters** reach the worker through the command region, written every step — fourteen
1556
+ words into memory the worker already shares, cheaper than tracking dirtiness, and it keeps
1323
1557
  `cloth.dynamics.slack = 0.2` working live, which is the point of a value object rather than a preset
1324
- that has to be re-applied.
1558
+ that has to be re-applied. (Thirteen at M5; M6's §10 adds the drag response, and the air itself
1559
+ travels as a region of the same buffer rather than as words — `ClothState#air_velocity`, written by
1560
+ the main thread and read by the step, which is the one direction the buffer could always carry and
1561
+ nothing had used.)
1562
+
1563
+ **M5 corrected "`ClothDynamics` reaches the worker".** By the time the step can use them a cloth's
1564
+ dynamics have been through `cloth_dynamics_map.js` — a damping retention, a velocity clamp, a
1565
+ self-collision radius, gravity rotated into the local frame — and sending the raw component would mean
1566
+ running that mapping a second time on the far side, with no way to prove the two copies agreed. What
1567
+ crosses is `ClothStepParam`: `dt`, local gravity, substeps, iterations, retention, clamp, contact
1568
+ radius, self radius, collider count, and whether self-collision is on. `Float64` throughout, so the
1569
+ round trip is exact — an `f32` command region would round the clamp, and a clamp that rounds is a
1570
+ velocity that rounds. `MEASUREMENTS.md` §40.
1571
+
1572
+ The same block is what makes the parity gate below structural rather than aspirational:
1573
+ `ClothSystem` fills the words of a private scratch array and calls `cloth_step_instance`;
1574
+ `WorkerClothSystem` fills the same words of the shared command region and the worker calls the same
1575
+ function. Neither system computes a step parameter of its own, so neither can compute one differently.
1576
+
1577
+ **M6 found the one thing that is not a parameter and still differs: the clock.** A gust is a function
1578
+ of time, and a worker cloth is not advanced until its registration is acknowledged — so the two
1579
+ systems reach a cloth's *first step* at different tick counts, and a clock that counted ticks hands
1580
+ that cloth a different gust on the same step. Measured with the clock put back on ticks, the gate
1581
+ fails at step 1, byte 0, which is the step's own result word. `ClothWorld#time` therefore counts the
1582
+ steps the world has taken rather than the ticks it has been asked about: it advances at the first
1583
+ `advance` of a tick, so a tick in which nothing is advanced does not move the weather. That is also
1584
+ the right semantic on its own terms — a paused simulation should not have the wind move.
1585
+ `MEASUREMENTS.md` §50.
1586
+
1587
+ **Post-M6: a world clock fixes the first cloth in a scene and no other.** The paragraph above is
1588
+ right about *one* cloth and the gate it was measured against had one. With cloth A already running,
1589
+ cloth B's registration takes k ticks during which A is still being advanced — so the world clock
1590
+ moves, and B reaches its own first step at a different clock value under the two systems. Measured
1591
+ with a two-tick delivery: **0.1000 s against 0.1333 s** at B's first step, and the gate fails at B's
1592
+ step 1, byte 0, exactly as it did for the tick clock. A gust of zero gives zero differences, which is
1593
+ what identifies the clock as the cause rather than the latency. So the gust reads **the asking
1594
+ cloth's own age** — `ClothInstance#age`, seconds *this instance* has stepped — and `ClothWorld#time`
1595
+ is left to do the one job it can do correctly, which is telling a source that reads the scene when to
1596
+ walk it. The visible consequence is that two garments spawned a second apart are a second apart in
1597
+ the gust rather than in phase with it; for a bounded sum of sinusoids that is indistinguishable from
1598
+ weather, which is why this is free. `MEASUREMENTS.md` §57.
1599
+
1600
+ **And post-M6 found the other one, which is not a clock: the two systems ran a tick's phases in
1601
+ different orders.** `ClothSystem` ran advance, step and finish per cloth, in hierarchy-depth order, so
1602
+ a **nested** cloth advanced against an anchor the cloth above it had already written *this* step;
1603
+ `WorkerClothSystem` cannot do that — its `apply` writes step T−1 back at the top of a tick and its
1604
+ `collect` then advances everything for step T. Measured on a 4-link rope hung from link 3 of a 6-link
1605
+ rope, no wind, 120 steps: the parent identical, the child differing at **step 1, byte 24**. Both
1606
+ systems now run three loops — advance every cloth, then step every cloth, then finish every cloth — so
1607
+ a nested cloth reads its parent one fixed step stale on both sides, and `ClothWorld`'s depth sort,
1608
+ which existed only to make the other answer true, is gone. That was necessary and not sufficient: the
1609
+ write-back also composed each node's world pose against the **live** anchor rather than the anchor the
1610
+ step ran on, which for a worker writing step T−1 during tick T is a different anchor. It now uses the
1611
+ pose `advance` cached, which is the same value for `ClothSystem` and the honest one for both.
1612
+ `MEASUREMENTS.md` §56.
1325
1613
 
1326
1614
  `Float32` for particle state, not `Float64`. The simulation runs in the cloth's **local frame**, so
1327
1615
  coordinates are garment-sized rather than world-sized and f32 has ample precision; the payoff is half
@@ -1340,6 +1628,16 @@ fixed `CLOTH_CONTACT_SLOTS` per particle inside `ClothState`'s single buffer —
1340
1628
  saturate on a twenty-collider scene and to average a tenth of one — which keeps §12's one-allocation
1341
1629
  property and bounds the worst case at the four *most violated* colliders, which is the four that matter.
1342
1630
 
1631
+ **And M5 moved the per-step collider table itself into that buffer**, at a fixed
1632
+ `CLOTH_COLLIDER_SLOTS`, which is where the table above always put it. M2 kept it on `ClothInstance`
1633
+ and grew it by doubling, on the correct grounds that its size is a property of the world rather than of
1634
+ the cloth; what decides is that an array which can be *replaced* mid-run is memory the worker has to be
1635
+ told about again before it can name it. So the size stays a property of the world and the region gets a
1636
+ capacity. **Sixty-four**, because the measurement showed the bound is a *scene* rather than a garment —
1637
+ a cloth's table holds every collider on every character standing inside its query box, which is ten for
1638
+ one body and thirty for three — and because an overflow discards colliders in the index's own order
1639
+ rather than in order of how much they matter. It warns once. `MEASUREMENTS.md` §41.
1640
+
1343
1641
  **The worker.** `WorkerClothSystem` is a `WorkerSystem` subclass and follows `WorkerFluidSystem`
1344
1642
  exactly, because that pattern is already proven in this repo: `collect` packs colliders and animated
1345
1643
  targets into shared memory and writes the command words, a single `Atomics.store` publishes the step,
@@ -1353,6 +1651,15 @@ identical order. That is what makes the worker a deployment choice rather than a
1353
1651
  it is why the core is a pure function over typed arrays from milestone zero even though the worker does
1354
1652
  not arrive until milestone five.
1355
1653
 
1654
+ **M5 measured it and the number is zero**, at every step of a run rather than at the end of one: the
1655
+ whole `ClothState` buffer — particles, velocities, every dual and penalty, the collider table, the
1656
+ warm-started contact multipliers, the self-collision pair list — compared byte for byte after each of
1657
+ 420 steps across two scenes, one with a walking anchor and one with colliders and self-collision live.
1658
+ What the scene sees differs by exactly one tick: the pose written at tick `T` is step `T − 1`'s. That
1659
+ is §4.1's own sentence in a second setting — the cloth's *shape* is one fixed step stale and its
1660
+ *position* is not, because the attachment cascade carries the subtree rigidly with the anchor.
1661
+ `MEASUREMENTS.md` §40.
1662
+
1356
1663
  Testing the bridge needs no worker: `SystemWorkerLoopback` gives a real `SystemWorkerHost`, a real
1357
1664
  `SharedArrayBuffer` and real `Atomics` in-process, with message delivery and step execution as explicit
1358
1665
  events a spec can order. Only the thread is faked.
@@ -1387,13 +1694,13 @@ What this design takes from the engine rather than building:
1387
1694
  | "Opt a collider into a second simulation" | `FluidObstacle` + `FluidObstacleSystem`, `AcousticBody` |
1388
1695
  | Collider index shape | `AcousticOccluderIndex` + `OccluderBody` |
1389
1696
  | Component holds a built shared asset | `Collider#shape`, `ParticleEffect#program` — the convention `ClothRig#proxy` follows |
1390
- | Surface material combination | `physics/contact/combine_material.js` |
1697
+ | Surface material combination | ~~`physics/contact/combine_material.js`~~ — **not reused**, see §8.3: cloth's `μ` is `Collider#friction · ClothCollider#friction_scale`, which is a scale on one material rather than a combination of two |
1391
1698
  | Constraint-row doctrine | `physics/constraint/solve_constraints.js` |
1392
1699
  | Subtree walk | `TransformAttachmentSystem#findChildrenOf` / `#countChildrenOf`, `TransformAttachment` |
1393
1700
  | Animated targets | `mesh_apply_skeletal_vertex_skinning` (flat arrays, worker-safe) |
1394
- | Wind / aerodynamics | `FluidField#sampleVelocity`, `fluid_build_world_to_grid`, `GlobalFluidEffector` |
1701
+ | Wind / aerodynamics | `FluidField#sampleVelocity` (in grid cells a second — §10 converts), `fluid_build_world_to_grid`, `GlobalFluidEffector` as the shape of the ambient *and* as the exact-ODE relaxation step `cloth_drag_response` copies |
1395
1702
  | Worker offload | `ecs/async/WorkerSystem.js`, `SystemWorkerHost`, `SystemWorkerLoopback`, `WorkerFluidSystem` as the worked example |
1396
- | Fixed step + render interpolation | `PhysicsSystem`'s producer/`__interp_restore` contract |
1703
+ | Fixed step | `EntityManager`'s fixed-step loop, gate and `fixedStepTick` |
1397
1704
  | Transforms | `Transform64`, `Node3D` |
1398
1705
  | Particle → render-vertex mapping | `Skin` — joints + inverse-bind matrices + the four-weight palette; cloth writes joints and the existing pass does the rest |
1399
1706
  | Reaching a model's joints from its entity | `SceneBundleInstance` (`skins`, `nodes` by name), `register_instance_animation` |
@@ -1404,6 +1711,24 @@ What this design takes from the engine rather than building:
1404
1711
 
1405
1712
  ---
1406
1713
 
1714
+ ### 13.1 What is *not* reusable — render interpolation
1715
+
1716
+ `PhysicsSystem`'s producer/`__interp_restore` contract was in the ledger above and M5 took it out,
1717
+ because it cannot express a rigged garment. `Interpolated` is an **entity-keyed marker**: the
1718
+ `InterpolationSystem` files snapshots under a `key` it assigns from the entity id at link time. A
1719
+ rope's links are entities and a skinned garment's joints are not — `shade_node_to_entity_composition`
1720
+ does not expand a skinned node, "and that is a boundary rather than an omission" (§3) — so half the
1721
+ feature has nothing to put the component on.
1722
+
1723
+ Cloth also needs only half of what that contract does. `__interp_restore` exists to hand the
1724
+ simulation its authoritative pose back before it integrates, because the render blend writes into the
1725
+ components the simulation reads. Cloth's state is `ClothState.position` in the cloth's local frame and
1726
+ its write-back never touches the anchor it reads, so **cloth never reads back what it writes** and has
1727
+ nothing to restore. It needs the publish half, over a pose per node rather than a component per entity.
1728
+ §18 carries what that would cost. `MEASUREMENTS.md` §43.
1729
+
1730
+ ---
1731
+
1407
1732
  ## 14. New shared code this justifies
1408
1733
 
1409
1734
  Three pieces belong in `core`, not in `cloth`, because the cloth need exposes a gap the engine has
@@ -1623,13 +1948,36 @@ placement into a bounded quality question and leaves the scheduler alone.
1623
1948
  and belongs with the collision rows that make a particle get *stuck* in the first place — a chain has
1624
1949
  no configuration to be stuck in. Chebyshev acceleration, still gated on contacts that arrive at M2. `β`,
1625
1950
  which M0 could not choose and M1 has no transient rows to choose it with either — every row M1 adds is
1626
- persistent. `ClothSerializationAdapter` and its siblings: nothing in M1's scope list names them, and a
1627
- codec written before `ClothRig` exists would be revised by M3 before it was ever loaded.
1951
+ persistent. The serialization adapters: nothing in M1's scope list names them, and a codec written
1952
+ before `ClothRig` exists would be revised by M3 before it was ever loaded — **true of one of the three;
1953
+ the correction follows this block.**
1628
1954
  Interpolation: cloth writes `Transform64` at the fixed rate and §12's producer/`__interp_restore`
1629
1955
  contract is not wired, which shows as a cloth updating at the fixed rate rather than the frame rate
1630
1956
  and is a visible-but-not-wrong gap. M5 is where it belongs, with the worker.
1631
1957
  **This is the first shippable feature.**
1632
1958
 
1959
+ **Corrected 2026-09-13: the deferral above only ever fitted `ClothRig`'s adapter, and it was applied to
1960
+ all three of them from here to the end of the plan.** It fits `ClothRigSerializationAdapter`, whose
1961
+ component points at a built, shared, immutable asset and therefore needs the asset-identity story §3.2
1962
+ only gestures at; and it fits `ClothProxy`'s own codec, which genuinely does depend on what §11.3
1963
+ step 9 adds to a proxy. It never fitted `Cloth` or `ClothCollider`. Those two are scalars — `blend`,
1964
+ `layer`, `mask`, `flags` and a `ClothDynamics` value object of ten more; `inflation`,
1965
+ `friction_scale`, `layer`, `mask`, `flags` — and neither holds a proxy, a skin, or anything either
1966
+ geometry path introduces. The evidence that the revision this sentence feared was never coming is M3
1967
+ itself: it changed the design in four places (`MEASUREMENTS.md` §24–29) and not one of them was a
1968
+ field on either component.
1969
+
1970
+ What turned a sequencing call into a hole is that it was **re-deferred rather than re-examined**. M2
1971
+ deferred all three "per M1's own reasoning"; M3 deferred them again for M1's reason while, in the same
1972
+ list, deferring the unrigged path that the reason had just been made to wait on; and §11.3 step 9 is
1973
+ in no milestone at all. A wait with no scheduled end is not a sequencing decision, and what it cost is
1974
+ that M6 closed this plan with a subsystem a scene could not contain: a `Cloth` could be added from
1975
+ code and from nowhere else. `ClothSerializationAdapter` and `ClothColliderSerializationAdapter` are
1976
+ built and specced as of 2026-09-13, outside any milestone and with no `MEASUREMENTS.md` section,
1977
+ because there is no measurement behind them — a codec has nothing to measure, and that file's sections
1978
+ belong to milestones. `ClothRigSerializationAdapter` and the proxy codec stay deferred, on the reason
1979
+ as narrowed here.
1980
+
1633
1981
  **M2 — collision. DONE, 2026-09-10.** `ClothCollider`, `ClothColliderFlags`, `ClothColliderSystem`,
1634
1982
  `ClothColliderIndex`, the packed record and its swept bounds, analytic SDF gradients and
1635
1983
  `PlaneShape3D` in `core` (§14.1, §14.3), the contact row and its friction row, the backstop row, and a
@@ -1657,10 +2005,15 @@ available, loses on every axis and is gone.
1657
2005
  **Deliberately not in M2:** P4(d)'s bounded escape hatch, still — M2 has the contacts that make a
1658
2006
  particle get stuck, and also measured the one stuck configuration whose *residual is zero*, which is
1659
2007
  the quantity the escape hatch was to watch (§7 P4). It belongs with the self-collision of M4, where a
1660
- particle can be pinched between two rows rather than merely held by one. The backstop row is built,
1661
- specced and driven by the playground, but nothing **authors** one: its plane comes from a garment's own
1662
- skinned surface and a skinned garment is M3. Two-way coupling, per §2. `ClothSerializationAdapter` and
1663
- its siblings, per M1's own reasoning.
2008
+ particle can be pinched between two rows rather than merely held by one. The backstop row is built and
2009
+ specced, but nothing **authors** one: its plane comes from a garment's own skinned surface and a
2010
+ skinned garment is M3. (**"and driven by the playground" was not true and has been removed** — no page
2011
+ under `playground/` writes `ClothState#backstop_plane`, then or now. The row's own docblock made the
2012
+ same claim and has been corrected. What holds it up is `cloth_backstop_row.spec.js`, which is a
2013
+ solver-level spec rather than a scene, and that is the honest status of a row with no author.) Two-way coupling, per §2. The serialization adapters, per
2014
+ M1's own reasoning — which, as M1 now records, reached only `ClothRig`'s. The other two were not
2015
+ blocked by anything M2 had in hand or M3 was about to add, and this line is the first of the two
2016
+ restatements that carried them anyway.
1664
2017
 
1665
2018
  **M3 — skinned garments, the rigged path. DONE, 2026-09-10.** `ClothRig`, `ClothProxy`,
1666
2019
  `cloth_proxy_from_joints` (§11.3 steps 5–8), the rig seeder, and joint write-back into
@@ -1695,7 +2048,13 @@ budget. §11.3 step 10's bind-pose collision validation, which `cloth_proxy_from
1695
2048
  because it has a joint tree and no collider set — and which `MEASUREMENTS.md` §29 makes the argument
1696
2049
  for, because a garment authored inside its own body reads as a *tether* failure and costs an hour to
1697
2050
  diagnose. `ClothProxy`'s binary codec and `ClothRigSerializationAdapter`, for M1's own reason: a
1698
- codec written before the unrigged path exists would be revised before it was ever loaded.
2051
+ codec written before the unrigged path exists would be revised before it was ever loaded. **Those two
2052
+ only, corrected 2026-09-13.** As written this sentence carried `Cloth`'s and `ClothCollider`'s
2053
+ adapters with them, over a set of three, for a reason that held for one — and it did so in the same
2054
+ list that defers the unrigged path the reason had just been made to wait on, which is what closed the
2055
+ loop: the dependency and the thing depending on it were deferred by the same paragraph. Neither scalar
2056
+ component holds a proxy, so §3.2's asset-identity question — the real content of M1's reason — was
2057
+ never theirs to answer. Both adapters were built on 2026-09-13; M1 carries the full correction.
1699
2058
 
1700
2059
  **M4 — self-collision. DONE, 2026-09-11.** `core/geom/3d/hash-grid/PointHashGrid3.js`, the
1701
2060
  particle-particle row (`solver/constraint/cloth_self_row.js`), the pair search
@@ -1762,13 +2121,156 @@ realisations, a self-tangled sheet unties itself every time the gust stops, so M
1762
2121
  configuration to escape from. The case P4(d) names is a particle wedged between a collider *and* a
1763
2122
  fold, which is collision and self-collision together, and M4 has not measured it.
1764
2123
 
1765
- **M5 — the worker.** `WorkerClothSystem`, `ClothWorkerCore`, `cloth.worker.js`, the protocol.
1766
- *Proofs:* bit-parity between `ClothSystem` and `WorkerClothSystem` as a standing gate; loopback specs
1767
- covering step ordering, message ordering and shutdown; a measured main-thread cost reduction.
1768
-
1769
- **M6 — wind.** Fluid-field sampling, aerodynamic drag, the ambient fallback.
1770
- *Proofs:* a playground with a banner in a simulated wake; a measured cost showing the per-step sampling
1771
- stays inside budget. Cuttable without affecting anything else.
2124
+ **M5 — the worker. DONE, 2026-09-12.** `WorkerClothSystem`, `ClothWorkerCore`, `cloth.worker.js`,
2125
+ `makeClothWorker` and the protocol — plus the two pieces that had to exist for the parity gate to be
2126
+ structural rather than hopeful: `ClothWorld`, which is every cloth in a scene and everything about
2127
+ stepping one that is not arithmetic, owned by *both* systems; and `cloth_step_instance`, which is the
2128
+ arithmetic, driven by a `Float64` parameter block that is also the command region's per-cloth record.
2129
+ `ClothState` gains shared-memory backing, a descriptor, the collider table and a result region.
2130
+ *Proofs, all green:* bit-parity over the **whole `ClothState` buffer at every step** of two runs — a
2131
+ walking anchor for 240 steps, and colliders plus self-collision for 180 — with the write-back's
2132
+ one-tick lag pinned tick by tick rather than asserted at the end; the state a worker steps being the
2133
+ main thread's memory rather than a copy; a cloth being neither stepped **nor advanced** until its
2134
+ registration is acknowledged, which is what keeps the two systems' step *sequences* identical rather
2135
+ than merely similar; a re-seed retiring its registration and waiting again; a despawn between dispatch
2136
+ and step not failing the bridge; sleep removing a cloth from the dispatch and `wake` putting it back;
2137
+ shutdown settling the step in flight, releasing the gate and dropping the worker; and the derived
2138
+ execution order landing `WorkerClothSystem` exactly where `ClothSystem` lands. **10 tests in one new
2139
+ spec file**, with the M0–M4 suites unchanged at 183. The engine suite is green at 18,579.
2140
+
2141
+ **M5 changed the design in four places**, each with the measurement behind it in `MEASUREMENTS.md`
2142
+ §38–44 and each folded back above. In one line each: §12's one buffer for the whole cloth world is one
2143
+ buffer per cloth, because `reseed` resizes one while others are in flight; §12's collider table does
2144
+ belong in that buffer and therefore needs a capacity rather than a growth policy, and the capacity is
2145
+ bounded by the *scene* — thirty records for three characters — rather than by the garment; §12's
2146
+ "`ClothDynamics` reaches the worker through the command region" is the *mapped* step parameters rather
2147
+ than the component, because mapping them twice is two copies that cannot be proved equal; and §13's
2148
+ reuse of `PhysicsSystem`'s interpolation contract does not hold, because that contract is entity-keyed
2149
+ and a rigged garment's joints are not entities.
2150
+
2151
+ **And the strongest result is again a number rather than a correction.** §12 asks the two systems for
2152
+ bit-identical results. Over 420 steps across two scenes, comparing the entire state buffer after every
2153
+ one of them, the number of differing bytes is **zero** — and it is zero by construction rather than by
2154
+ luck, because there is one `ClothWorld`, one `cloth_step_instance`, and one parameter block that both
2155
+ systems fill and neither system interprets. The main thread's share of a thousand-particle garment goes
2156
+ from 3.62 ms to 0.18 ms; across the library's shapes, 92% to 96% of it crosses. `MEASUREMENTS.md` §40,
2157
+ §41.
2158
+
2159
+ **Deliberately not in M5:** **render interpolation**, which §18 said would land here and which M5
2160
+ decided against with three findings rather than a deferral — the named contract is entity-keyed and
2161
+ cannot reach a rigged garment's joints, cloth needs only the half of it that has no name, and the
2162
+ worker removes the cheap implementation by owning `ClothState` outside the fixed pass. What it needs
2163
+ instead is a published pose slot, and §18 now carries what that costs (about 0.12 ms a frame on the
2164
+ §17 workload, which roughly triples the main thread's share of cloth and leaves it at a seventh of
2165
+ its pre-M5 cost) and what it buys (half a link length a step at a walk, a whole one at a sprint).
2166
+ `MEASUREMENTS.md` §43. **A playground page**, for a reason about the dev server rather than the cloth:
2167
+ `SharedArrayBuffer` needs a cross-origin isolated page, and adding `COOP`/`COEP` to
2168
+ `vite.config.mjs` changes how every other playground page in the package loads its subresources —
2169
+ to demonstrate a bridge `SystemWorkerLoopback` already tests more precisely than a page could.
2170
+ **Culling (§12.1)**, which is a policy over the same dispatch list sleep already uses and belongs with
2171
+ a camera. And **a deeper pipeline**: `WorkerSystem` allows one step in flight and the fixed-step gate
2172
+ means a cloth could not use a second.
2173
+
2174
+ **M6 — wind. DONE, 2026-09-13.** §10 end to end: `wind/AbstractClothWind.js` and the two sources
2175
+ behind it (`ClothAmbientWind`, `ClothFluidWind`), `wind/cloth_sample_air.js`,
2176
+ `solver/cloth_wind_accelerate.js`, `ClothState`'s air and external-acceleration regions,
2177
+ `cloth_drag_response`, the parameter block's fourteenth word, and `ClothWorld`'s wind source, clock
2178
+ and wake test.
2179
+ *Proofs, all green:* the normal-facing factor on both topologies, which is the whole of what M6 adds
2180
+ to the physics — a sheet reading 1.000 broadside and 0.000 edge-on, a chain reading the reverse, and
2181
+ neither a skip row nor a strain-limit row able to fabricate a plane out of a line; the two sources
2182
+ through the interface, including a field answering in metres rather than grid cells and `varies()`
2183
+ being a promise the sampler acts on; the ECS path end to end — a garment blown downwind, one inside a
2184
+ `FluidComponent` reading the simulation while one ten metres away reads the weather, a settled banner
2185
+ sleeping in a steady wind and waking on a gust, and a scene with no source producing the cloth M5
2186
+ shipped **byte for byte** across 120 steps; the drag response closing a fraction of the gap to the
2187
+ air that is strictly below one at every step length from 1/240 s to 1 s; and §12's parity gate
2188
+ extended to a third run with wind live. **25 tests across three new spec files** — the term's 9, the
2189
+ sources' 10, the ECS path's 6 — plus the parity spec's eleventh, for 26 in all, with the M0–M5
2190
+ suites unchanged at 193. The engine suite is green at **18,638**, and the same suite run against M5's
2191
+ own commit in a detached worktree reads 18,612 on this machine, so the difference is exactly M6's
2192
+ twenty-six. A fifth playground page, `playground/wind.html`, with a banner hanging in a real
2193
+ `FluidSimulator`'s wake.
2194
+
2195
+ **M6 changed the design in five places**, each with the measurement behind it in `MEASUREMENTS.md`
2196
+ §45–§52 and each folded back above. In one line each: §10's `|n·û|` cannot be written for a chain,
2197
+ which has no normal and wants the complementary factor anyway, so both are one expression over the
2198
+ span of the incident rows; §10's "samples its velocity field" needs a unit conversion, because
2199
+ `FluidField` answers in grid cells a second and a garment blowing at a speed that depends on the
2200
+ voxel size is not a garment; §10 never said which thread samples and it cannot be the worker's, so
2201
+ the field stays where it is and three floats a particle cross in the cloth's own buffer; `drag`
2202
+ cannot multiply the flow directly, because one acceleration held for a whole fixed step overshoots
2203
+ the air once `rate·dt > 1`; and §12's thirteen-word block is fourteen, with a clock that has to count
2204
+ steps rather than ticks or the two systems' weather diverges at step 1.
2205
+
2206
+ **And the strongest result is again a number rather than a correction.** §10 offers the normal-facing
2207
+ factor as the difference between a flag and a rope. In uniform air it is: turning the wind ninety
2208
+ degrees at the same speed takes the banner's aerodynamic acceleration from **43.2 m/s² to 0.08**, a
2209
+ factor of five hundred. In a **wake** it does nothing at all — 0.216 against 0.280 — because a wake
2210
+ has velocity in every direction and no orientation of a surface is edge-on to one. That is not a
2211
+ failure of the factor; it is the measurement saying §10's argument for reading a simulation is
2212
+ stronger than §10 knew, and that the flag-versus-rope claim is a statement about laminar air.
2213
+ `MEASUREMENTS.md` §51.
2214
+
2215
+ **Deliberately not in M6:** **a spatially varying ambient.** The gust is uniform in space, so a long
2216
+ banner does not see it arrive at one end before the other — and that is what makes the no-simulation
2217
+ case cost one sample for a whole garment instead of one a particle (§48). Spatial structure is what a
2218
+ fluid field is *for*, and a second, worse one inside the fallback is the duplication §10 exists to
2219
+ avoid. **A blend at a field's edge**: a point inside a volume reads the volume and a point outside
2220
+ reads the ambient, so a garment straddling the boundary sees a step rather than a ramp. Where the
2221
+ ambient is the far field the volume relaxes toward — `GlobalFluidEffector`'s own `wind` — the step is
2222
+ small and is mostly the disturbance the volume exists to carry; where it is not, the answer is a
2223
+ volume large enough to hold what moves through it. **Two-way coupling into the fluid**, per §2's
2224
+ one-way rule: cloth reads the air and does not disturb it, and anything that should has
2225
+ `FluidObstacle`. And **per-substep sampling**, because §10 says per step and §48 measures the sample
2226
+ at 58 ns a particle — the one part of this that is not free is the one part per-substep sampling
2227
+ would double.
2228
+
2229
+ **Post-M6 — six defects, six fixes, and the plan corrected where the code proved it wrong. DONE,
2230
+ 2026-09-13.** No new mechanism and no new milestone: a pass over what M6 left, taken in the order the
2231
+ defects were found.
2232
+
2233
+ 1. **A cloth resting on a collider never slept.** §7 P5 has asked for "maximum particle displacement
2234
+ over a window" since M0 and M1 built an instantaneous speed; four of the seven library fabrics
2235
+ never slept at all on a floor, because a resting contact is a bounded limit cycle whose *speed* is
2236
+ eighteen times the threshold and whose *travel* is a tenth of a millimetre a step.
2237
+ `CLOTH_SLEEP_DISPLACEMENT_RATIO`, and every entry on a floor or a sphere sleeps.
2238
+ `MEASUREMENTS.md` §53.
2239
+ 2. **A static collider was tunnelled from 3 m/s.** M2 folded the collider's travel into the discovery
2240
+ reach and argued the particle's own away with a velocity clamp three orders of magnitude too large
2241
+ to be the bound. The reach carries `|v_i|·dt` too; what is left is the substep count, measured.
2242
+ §7 P3, `MEASUREMENTS.md` §54.
2243
+ 3. **Seeding took three things from the scene and gave none of them back**: the authored pose across a
2244
+ re-seed, the authored pose on removal, and the GPU's authority over a rig's joints. And `ClothRig`
2245
+ was read once rather than watched, so a rig arriving a fixed step late seeded a skeleton as a rope.
2246
+ §11.1, §11.2, §18.
2247
+ 4. **A collider arriving and a collider retiring were both asked about a pose.** A collider spawned
2248
+ inside a sleeping cloth has not *moved*; a handle retired and reused inside one tick is the same
2249
+ handle. The wake test compares the handle set, and a retired slot is cold for a step.
2250
+ `MEASUREMENTS.md` §55.
2251
+ 5. **The parity gate had one cloth in it.** A nested pair and a staggered spawn are two scenes in
2252
+ which the two systems could differ and did — a tick's phases ran in different orders, the
2253
+ write-back composed against a newer anchor than its own step, and the gust read a world clock that
2254
+ moves while a second cloth is registering. §12, `MEASUREMENTS.md` §56, §57.
2255
+ 6. **The plan said eight things the code does not do**, each corrected in place above: P4(d)'s escape
2256
+ hatch and `ClothDiagnostics` recorded as never built rather than "on by default"; §5.2's constants
2257
+ line brought to `β = 1e3` with the contact floor recorded and `γ = 1` named as the one unmeasured
2258
+ constant left; §7 P1's gate stated as twice its measurements rather than as them; §3.3's `flags`
2259
+ default; §6's claim about the library at 2×4, which was never true of `CLOTH_ROPE`; §8.3 and §13's
2260
+ `combine_material.js`, which nothing imports; §3.2 and §11.2's joints-are-`Node3D`s, which the
2261
+ engine's own Rows-Not-Nodes work made false; §11.3's vertex painting; and the backstop row's
2262
+ playground, which does not exist. §18 gains the wind span rank as a limit, recorded not fixed.
2263
+
2264
+ *Proofs, all green:* eleven specs that were red before their fix and green after — the P5 collider
2265
+ sleep gate over the whole library × three scenes, the swept-versus-unswept throw at a static capsule
2266
+ and its three companions, the re-seed and the removal on the entity path, the rig's authority and the
2267
+ two `ClothRig` edges, the four collider-lifecycle cases, and the two parity scenes. **The cloth suite
2268
+ is 251 tests across 26 files**, against M6's 234.
2269
+
2270
+ **Deliberately not in this pass:** the `γ` sweep; tightening §7 P1's gate to its measurements;
2271
+ building the escape hatch or `ClothDiagnostics`; render interpolation; culling; the unrigged path;
2272
+ M4b; the wind span rank, which is recorded in §18 and not fixed; the `ClothRig` serialization adapter;
2273
+ and the physics adapter registry gap.
1772
2274
 
1773
2275
  **Performance budget — measured at M0, re-measured at M1, and it still does not hold on its own.** One
1774
2276
  character's full cloth complement — 1,024 particles at 2 substeps × 4 iterations, on a triangulated
@@ -1818,7 +2320,31 @@ attributing cost by deletion applies unchanged. What it licenses is that the col
1818
2320
  between nothing and a quarter of a frame and does not scale with the collider count over the range a
1819
2321
  character occupies. Every library entry's own budget is untouched. `MEASUREMENTS.md` §23.
1820
2322
 
1821
- Re-measure at M4 and M5.
2323
+ **Re-measured at M5, and no entry is re-sized — but not for the reason the table reads.** The absolute
2324
+ column came back 15% to 25% above M4's, on a machine carrying nineteen other node processes at 30–38%
2325
+ background CPU, which is exactly the swing M4's own lesson predicts. What licenses leaving the library
2326
+ alone is a *paired* measurement instead: `cloth_library_bench.mjs` never touches
2327
+ `WorkerClothSystem` — it drives `cloth_step` over a `ClothState` directly — so the only thing M5
2328
+ changed that it can see is the buffer layout, and an interleaved A/B of the two layouts in one process
2329
+ reads 0.95 to 0.98 with the two states staying bit-identical for 1,600 steps. §33's ordering stands
2330
+ entry for entry. `MEASUREMENTS.md` §39, §42.
2331
+
2332
+ **Re-measured at M6, and no entry is re-sized — now with a measured reason rather than only a
2333
+ disclaimer.** §10's aerodynamic term, priced in the bench's own §33 scenario, moves each entry by
2334
+ −2.7% to +9.3% across three runs, with the sign not monotone in `drag`; and with the *physics* held fixed so the two
2335
+ sides simulate the same cloth, the whole feature is **one per cent of a step**. The rest of that
2336
+ column is a different cloth — a rope blown horizontal costs 3.24× a rope hanging straight down, which
2337
+ is §7's warning arriving in a new setting and is about the rope. Three runs of the same bench within
2338
+ an hour put the §17 baseline row at 3.49, 3.83 and 4.26 ms, so the absolute column is still the
2339
+ machine: M5's advice to re-measure it quietly stands, and M6 adds that a process count is a proxy for
2340
+ load rather than a measure of it. `MEASUREMENTS.md` §49, §52.
2341
+
2342
+ **And the budget now means something slightly different.** The worker moves the cost rather than
2343
+ reducing it, so 2 ms is still 2 ms — but it is 2 ms on a thread that is not drawing the frame, and the
2344
+ question it answers has become "does all the cloth in the scene fit in one worker tick" rather than
2345
+ "does one garment fit in a frame". The main thread's share of the §17 workload is 0.18 ms.
2346
+ `MEASUREMENTS.md` §41. Re-measure the absolute column on a quiet machine, and check the process count
2347
+ first.
1822
2348
 
1823
2349
  ---
1824
2350
 
@@ -1889,15 +2415,42 @@ arbitrary planar configuration. Four coplanar particles that are not an affine i
1889
2415
  read as bent. *Mitigation:* accepted for the fabrics the library targets. If a case appears, the fix is
1890
2416
  a rest-curvature offset in the stencil, which is a change to one row and not to the solver.
1891
2417
 
1892
- **Swept collision is translation-first, and it is two mechanisms rather than one. — MEASURED.** P3
2418
+ **Swept collision is translation-first, and it is three mechanisms rather than one. — MEASURED.** P3
1893
2419
  states the limit. M2 found that the half nobody had written down — sweeping the contact **set**, not
1894
2420
  just the poses — was the half that mattered: without it a capsule at 20 m/s created zero rows and the
1895
2421
  sheet was never touched. With the collider's travel folded into the discovery reach, 2 to 40 m/s all
1896
- behave. What remains is that both halves are built from the collider's *centre*: a collider spinning
1897
- fast enough that a point on its surface travels more than its own radius within one substep can still
1898
- be missed. *Mitigation:* more substeps in the affected `ClothDynamics`; genuine CCD stays out of scope.
1899
- The shape of a fix, if one is ever wanted, is to build the discovery margin from the swept **surface**
1900
- travel — `|Δt| + θ·R` — which is one more term and no new structure. `MEASUREMENTS.md` §20.
2422
+ behave. Post-M6 found the third: the *particle's* own travel, which M2 argued was covered by §7 P1's
2423
+ velocity clamp and is not, by three orders of magnitude — so a **static** collider was tunnelled from
2424
+ 3 m/s. That is fixed and measured (§7 P3, `MEASUREMENTS.md` §54). What remains is that all three are
2425
+ built from the collider's *centre*: a collider spinning fast enough that a point on its surface
2426
+ travels more than its own radius within one substep can still be missed. *Mitigation:* more substeps
2427
+ in the affected `ClothDynamics`; genuine CCD stays out of scope. The shape of a fix, if one is ever
2428
+ wanted, is to build the discovery margin from the swept **surface** travel — `|Δt| + θ·R` — which is
2429
+ one more term and no new structure. `MEASUREMENTS.md` §20.
2430
+
2431
+ **A handle is a slot index, and a slot was reused the instant it was retired. — FIXED.** §12 says a
2432
+ cloth drops its warm-started contacts when the collider behind a handle changes, and `ClothColliderIndex`
2433
+ docblocks said the same in two places; the mechanism was `cloth_contact_find` matching on the handle
2434
+ alone, which can only work if the handle is *absent* for a step. `add` popped the retired slot
2435
+ immediately, so a collider removed and another added within one tick were the same handle. Measured: a
2436
+ particle settled on the old floor carried **lambda 0.0981 and a friction anchor in the old collider's
2437
+ frame** onto the new one. `remove` now retires into a list `advance` drains one step later — every
2438
+ cloth has by then stepped against an index the handle was not in, which is what makes the slot cold.
2439
+ It composes with the wake test above: a sleeping cloth notices the handle leaving its set and wakes,
2440
+ so it clears its slots before anything can take the number.
2441
+
2442
+ **Open — the contact penalty ramps on the *unsigned* residual, and that is load-bearing by accident.
2443
+ — MEASURED.** `cloth_contact_dual_update` climbs `k` by `β·|C|`, so a row that is **not touching**
2444
+ ramps as hard as one that is, and with `γ = 1` nothing brings it back down: every contact in a scene
2445
+ is at `k_max` within a few steps of being discovered, which makes §5.2's measured choice of `β`
2446
+ describe a ramp that is not running. Post-M6 made this visible rather than introducing it — with the
2447
+ particle's own travel in the discovery reach, rows now appear tens of centimetres out instead of
2448
+ sixteen millimetres, so the pre-stiffening happens long before contact. *And the obvious repair is a
2449
+ net loss:* making the ramp one-sided takes the speed at which a thrown particle is still stopped from
2450
+ 12 m/s down to 9.5 with holes, at four substeps — trading cloth passing through a character, which is
2451
+ visible, for a hair rope creeping for thirty-seven seconds instead of three, which is not. Recorded
2452
+ rather than fixed; the thing that would actually resolve it is a `γ < 1` for contacts, which is
2453
+ §5.2's own open number. `MEASUREMENTS.md` §54.
1901
2454
 
1902
2455
  **Open — a rigid collider driven through pinned cloth wraps rather than parts.** A contact row bounds
1903
2456
  the interpenetration; it cannot prevent it, because the cloth has nowhere to go. The playground's
@@ -1942,6 +2495,57 @@ set.
1942
2495
  garment or a runtime hierarchy. Culling (§12.1) covers the off-screen case; the far-but-visible case is
1943
2496
  unaddressed.
1944
2497
 
2498
+ **Open — a wake has no direction, and the aerodynamic model has only one term. — MEASURED.** §10's
2499
+ normal-facing factor is the whole of M6's aerodynamics, and in uniform air it does exactly what §10
2500
+ promises: ninety degrees of flow angle is a factor of five hundred in the force a banner feels. In a
2501
+ fluid wake it does nothing — 0.216 against 0.280 — because a wake has velocity in every direction.
2502
+ What that leaves open is whether a cloth *in* a wake wants something the linear drag row does not
2503
+ carry: the fluttering a real flag does in one comes from a pressure difference across the sheet and a
2504
+ shed vortex, not from a relaxation toward a local velocity, and this design has no term for either.
2505
+ *Mitigation:* none is needed for what M6 claims — the cloth reads the air the simulation produces and
2506
+ is moved by it. It is named because "a banner in a wake looks alive" is what a reader will expect from
2507
+ §10 and is not what a drag row alone produces. `MEASUREMENTS.md` §51.
2508
+
2509
+ **Open — the aerodynamic factor decides what a particle *is* on a 14.5° threshold, and a bent chain
2510
+ reads as a sheet.** §10's normal-facing factor is one expression over the rank of the span of a
2511
+ particle's incident stretch rows (`cloth_wind_accelerate.js`): rank 2 is a surface and gets `|û·n|`,
2512
+ rank 1 is a link of a chain and gets the complementary `√(1 − (û·t)²)`, rank 0 is a bead and gets 1.
2513
+ Rank is decided by `CLOTH_WIND_SPAN_MIN = 0.25` — how much of a second incident direction has to
2514
+ survive projection out of the first — and a quarter is **about fourteen and a half degrees**. Below it
2515
+ two rows describe the same line and their cross product is a normal built out of rounding, which is
2516
+ the failure the threshold exists to prevent and is why it is not smaller.
2517
+
2518
+ What it costs is that a **chain bent past that angle at a link is a surface at that link**, and gets
2519
+ the factor for one. A rope hanging straight reads as a rope; a rope draped over a collider reads as a
2520
+ sheet at every link on the curve, and does so for as long as it is draped, because the rank is
2521
+ recomputed from the configuration every step and the configuration is not changing. The magnitude is
2522
+ §51's: turning the flow ninety degrees is a factor of five hundred in a banner's acceleration, so the
2523
+ two branches are not close together, and a rope reading as a sheet catches the wind broadside where it
2524
+ should be slicing it.
2525
+
2526
+ *Not fixed, and recorded rather than guessed at.* The threshold is a single number doing two jobs — a
2527
+ numerical guard against a degenerate cross product, and a physical statement about when a chain stops
2528
+ being a line — and separating them is the shape of a fix: rank the span with a tolerance chosen for
2529
+ conditioning, and blend between the two factors over an angular window chosen for looks. Both halves
2530
+ would need a measurement neither M6 nor this pass has, and what a draped rope does in wind is a
2531
+ question about a scene rather than about a step.
2532
+
2533
+ **Open — the ambient is uniform in space and the field boundary is a step.** Two limits of §10's
2534
+ fallback, both deliberate and both cheap to see: a gust lifts a whole banner at once rather than
2535
+ travelling along it, and a garment carried out of a fluid volume crosses a discontinuity rather than a
2536
+ ramp. The first is what makes the no-simulation case cost one sample rather than one a particle; the
2537
+ second is small where the ambient is the far field the volume relaxes toward and visible where it is
2538
+ not. A blend would want a falloff distance, which is an authored quantity of exactly the kind §3.4
2539
+ spent a section removing.
2540
+
2541
+ **Open — a fluid volume's wake is steady at a grid a frame can afford. — MEASURED, and it is the
2542
+ fluid system's rather than cloth's.** At 18, 26 and 34 cells across, with the simulator's damping and
2543
+ vorticity confinement at both ends of their range, the air a banner's width behind a solid post varies
2544
+ by under 0.03 m/s: a velocity deficit, not a shedding street. The deficit is real and is a third of
2545
+ the free stream, which is what the cloth reads; what is not there is the unsteadiness. Resolving a
2546
+ shedding shear layer costs 15 ms a step at 26 cells and 35 at 34, against 5 ms at 18.
2547
+ `MEASUREMENTS.md` §51.
2548
+
1945
2549
  **Open — `stretch` is a weak control, and it is not obvious what would make it a strong one.** M1
1946
2550
  measured the strain-limit row removing about a third of the peak over-stretch and never reaching its
1947
2551
  own allowance, because it is a second hard row competing with the first for the same trust region and
@@ -1953,11 +2557,40 @@ penalty band, making `k_max/(m/h²)` a material property rather than a global (w
1953
2557
  §4 shows trades stretchiness against settling); or accepting that near-inextensibility is what fabric
1954
2558
  *is* and demoting `stretch` to a documented weak knob. M2's contacts do not bear on it either way.
1955
2559
 
1956
- **Open — the write-back has no interpolation.** §12 gives cloth `PhysicsSystem`'s producer /
1957
- `__interp_restore` contract, and M1 does not wire it: cloth writes `Transform64` at the fixed rate and
1958
- nothing blends between ticks, so a cloth updates at 60 Hz on a 165 Hz display. That is visible and it
1959
- is not wrong — the poses are authoritative, they are simply held. It lands with M5, where the worker
1960
- makes the fixed-step producer contract load-bearing for a second reason.
2560
+ **Open — the write-back has no interpolation, and M5 decided it does not land with the worker.** Cloth
2561
+ writes `Transform64` at the fixed rate and nothing blends between ticks, so a cloth updates at 60 Hz on
2562
+ a 165 Hz display — 105 of every 165 frames repeat a pose. That is visible and it is not wrong: the
2563
+ poses are authoritative, they are simply held. **How** visible is now measured, on M3's cape geometry:
2564
+ a node covers 0.47 link lengths in a fixed step at a walk and 1.33 at a sprint (`MEASUREMENTS.md` §43).
2565
+
2566
+ What M5 changed is the shape of the remaining work, in three findings that between them delete the
2567
+ sentence this note used to end with.
2568
+
2569
+ - **`PhysicsSystem`'s producer / `__interp_restore` contract cannot express half the feature.**
2570
+ `Interpolated` is an entity-keyed marker and a rigged garment's joints were `Node3D`s, not entities
2571
+ (§13.1). There was nothing to put the component on and no key to file a snapshot under.
2572
+ **Post-M6: a joint is an entity now** (§3.2, §11.2), so this finding's *reason* is gone even though
2573
+ its conclusion is not — the published-pose slot below is still the cheaper half and still the one
2574
+ cloth needs, and an entity-keyed marker on a hundred joints a garment is not obviously better than
2575
+ one array copied at join. What changed is that it is a trade rather than an impossibility.
2576
+ - **Cloth needs only the publish half.** `__interp_restore` undoes render-time writes before the
2577
+ simulation reads them; cloth's state is its particle array and its write-back never touches the
2578
+ anchor it reads, so there is nothing to restore. The restore it *does* depend on — an anchor on a
2579
+ physics-driven character — it already gets from `PhysicsSystem`.
2580
+ - **The worker removes the cheap implementation.** A render-time pass cannot read
2581
+ `ClothState.position`, because the worker owns it from dispatch until the next tick's join, which is
2582
+ essentially all of the time outside the fixed pass. What is needed is a **published pose slot**: the
2583
+ last two steps' node poses, copied out at join and blended at render time — the same double-buffered
2584
+ publish slot `WorkerFluidSystem` names as the unbuilt prerequisite for its own render-time reads.
2585
+
2586
+ The slot is nearly free to fill, because `cloth_write_back` already computes every node's local TRS
2587
+ and would only have to keep it; what costs is the per-frame pass. For the §17 workload that is about
2588
+ 0.12 ms a frame — 20.5 ms a second at 165 Hz, against the 10.9 ms a second M5 leaves on the main
2589
+ thread and the 217 ms a second it cost before. **Interpolating a cloth would roughly triple the main
2590
+ thread's share of it and leave it at a seventh of what it was**, which is the honest shape of the
2591
+ trade: the feature is affordable because M5 happened, and it is not free. The cloth-shaped version is
2592
+ *simpler* than the engine's, not harder: no restore pass, no serialization adapters, no log, and it
2593
+ covers the rigged and entity paths identically because both end in a pose per node.
1961
2594
 
1962
2595
  **Open — the subtree is walked once.** A rope's links do not come and go, so `cloth_seed_subtree` runs
1963
2596
  on first step and `ClothSystem#reseed` is how a caller says the structure changed. That is the same
@@ -1965,3 +2598,13 @@ contract `TransformAttachment#parent` already carries, and it is the right defau
1965
2598
  have is a way to *notice*. An entity added under a live `Cloth` is silently not simulated until
1966
2599
  someone re-seeds, which is a class of authoring mistake with no diagnostic behind it. `ClothDiagnostics`
1967
2600
  is where one would go.
2601
+
2602
+ **Post-M6 narrowed this: a `ClothRig` is not part of it and never was.** §3.1 says the discriminator
2603
+ between the two paths is a component's *presence*, and `ClothSystem#dependencies` is
2604
+ `[Cloth, Transform64]` — so a `ClothRig` added to a live cloth completed no tuple the system observed
2605
+ and one removed broke none. A character whose rig arrived a fixed step after its `Cloth` simulated its
2606
+ **whole skeleton as one rope** (in a dev build, through `gpu_authority_assert_cpu`, it threw), and one
2607
+ whose rig was taken away kept driving the joints. Both edges are now watched by an `EntityObserver` on
2608
+ `Cloth` + `ClothRig` inside `ClothWorld`, so the discriminator is read whenever it changes rather than
2609
+ once. The **subtree** case above is untouched and stays open: an entity appearing under a live rope is
2610
+ still a `reseed` the caller has to ask for.