@woosh/meep-engine 3.25.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 (63) 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/physics/cloth/MEASUREMENTS.md +582 -0
  12. package/src/engine/physics/cloth/PLAN.md +330 -49
  13. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +84 -17
  14. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  15. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +451 -42
  16. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +65 -7
  17. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  18. package/src/engine/physics/cloth/ecs/ClothInstance.js +72 -8
  19. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +25 -4
  20. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  21. package/src/engine/physics/cloth/ecs/ClothSystem.js +75 -8
  22. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +41 -4
  23. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  24. package/src/engine/physics/cloth/ecs/ClothWorld.js +207 -63
  25. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +10 -0
  26. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -1
  27. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +17 -1
  28. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +27 -3
  29. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  30. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +28 -3
  31. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +75 -10
  32. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  33. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +131 -12
  34. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -0
  35. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
  36. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +249 -201
  37. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -0
  38. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  39. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +62 -1
  40. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  41. package/src/engine/physics/cloth/ecs/cloth_write_back.js +18 -10
  42. package/src/engine/physics/cloth/playground/collide_main.js +1 -1
  43. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +29 -8
  44. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  45. package/src/engine/physics/cloth/solver/cloth_contact_find.js +37 -9
  46. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +12 -2
  47. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  48. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +12 -2
  49. package/src/engine/physics/cloth/solver/cloth_step_instance.js +1 -1
  50. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +9 -2
  51. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -1
  52. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +9 -2
  53. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +23 -9
  54. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -1
  55. package/src/engine/physics/cloth/wind/AbstractClothWind.js +23 -9
  56. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +7 -5
  57. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -1
  58. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +12 -9
  59. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -1
  60. package/src/engine/physics/cloth/wind/ClothFluidWind.js +5 -2
  61. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +7 -2
  62. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
  63. package/src/engine/physics/cloth/wind/cloth_sample_air.js +12 -5
@@ -134,12 +134,19 @@ documented as *not* serialised because shapes are shared assets the pipeline wir
134
134
  `ParticleEffect` holds a `ParticleLayout` and a `ParticleProgram` the same way. `ClothRig` serialises
135
135
  the proxy's identity and the skin ordinal, and the asset pipeline resolves the rest.
136
136
 
137
- **The proxy addresses joints by index, never by name.** A joint is a `Node3D` inside a
138
- `SceneBundleInstance`, not an entity, so an `EntityReference` to one is impossible by construction —
139
- and reintroducing strings at runtime to work around that would be the wrong answer. Instead the
140
- binding is resolved **once, at import**: the builder has `SceneBundleInstance#nodes` (the by-name map
141
- the engine keeps because sockets and effect anchors are authored as bone names) and turns names into
142
- 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.
143
150
 
144
151
  ### 3.3 `ClothDynamics`
145
152
 
@@ -156,7 +163,7 @@ export class ClothDynamics {
156
163
  drag = 0.1; // 0..1 — aerodynamic coupling to the air (§10)
157
164
  substeps = 2; // solver budget
158
165
  iterations = 4; // solver budget
159
- flags = 0; // ClothDynamicsFlags.SelfCollision, ...
166
+ flags = ClothDynamicsFlags.StrainLimit; // | SelfCollision, ...
160
167
  }
161
168
  ```
162
169
 
@@ -520,10 +527,22 @@ are in `MEASUREMENTS.md`; the corrections are folded into the listing above and
520
527
  it has no effect at all. The ramp exists for **transient** rows, a contact that should arrive soft
521
528
  rather than shock the mesh on its first frame, and M0 has none. M2 has to measure it.
522
529
 
523
- Chosen: **α = 0.25, γ = 1, β = 1e5 (unexercised), k_min = k_max = 8·(m/h²)**. γ sits at 1 because its
524
- second job — decaying the penalty — has nothing to do while the band is a single point, so the whole
525
- decay lives in α, whose only job is the multiplier. When M2's contacts make the band real, γ becomes
526
- 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.
527
546
 
528
547
  **Adaptive initialisation** is VBD's: seed the iteration at `x + h·v + h²·ã` where `ã` interpolates
529
548
  between zero and full external acceleration according to whether the previous step looked like free
@@ -739,8 +758,16 @@ what the fabric row does unaided and it is simply inert.
739
758
  So **`stretch` is the weakest field in §3.3's parameter set**, and the honest statement is that at low
740
759
  budgets the bound belongs to the budget rather than to the fabric. The row is worth its 13–15% of a
741
760
  garment frame where the residual is visible and is not worth it where the residual is already around
742
- one per cent, which is why it is a flag — `ClothDynamicsFlags.StrainLimit` — rather than always on,
743
- 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.
744
771
 
745
772
  ---
746
773
 
@@ -778,8 +805,12 @@ descent direction, and the update takes a full step (§5.3). What is asserted in
778
805
  every LDLᵀ pivot positive on every update (which *is* positive definiteness, checked where it is
779
806
  used); nothing non-finite across 10,000 steps of absurd cloth; and full-step energy increases bounded
780
807
  and rare — 0.004% of updates on a realistic cape, never above a fifth of the local energy scale, and
781
- 1.5% / 1.0 across the absurd fuzz. Those measured figures are the thresholds the standing gate holds
782
- 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.
783
814
 
784
815
  **P2 — Over-stretch, the rubber cape.**
785
816
  *Mechanism:* long-range attachments (Kim et al. 2012). Each particle carries the geodesic distance to
@@ -824,12 +855,35 @@ of `θ` it scales the shape it poses by `cos(θ/2)`. At the half-radian per step
824
855
  `CLOTH_ANCHOR_ROTATION_LIMIT` already calls a spin, a 25 cm collider loses 7.8 mm — twice a silk
825
856
  garment's thickness — halfway through every step. `MEASUREMENTS.md` §19.
826
857
 
827
- *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
828
872
  point on its surface travels more than its own radius within one substep can still be missed, because
829
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.
830
882
  *Spec:* a capsule driven through a hanging sheet at 2 to 40 m/s, swept against unswept; assert the
831
883
  swept pass creates rows at every speed and that no particle spends a step more than half a collider
832
- 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.
833
887
 
834
888
  **P4 — Folds, tangles, and stays stuck.** The named requirement, so four mechanisms:
835
889
 
@@ -869,10 +923,21 @@ radius inside. `cloth_collision.spec.js` carries the ECS-level companion.
869
923
  solver whose whole design commitment is that it needs none, and no authored scene is that
870
924
  symmetric. `MEASUREMENTS.md` §22.
871
925
 
872
- *(d) A bounded escape hatch.* If a particle's constraint residual stays above a threshold for N
873
- consecutive steps, it blends to its animated position over a short window. Local, bounded, on by
874
- default with a conservative threshold, and counted in `ClothDiagnostics` so that a garment relying on
875
- 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.
876
941
 
877
942
  *Spec:* deliberately invert half a sheet through itself, run, assert the residual falls below threshold
878
943
  and the mesh is self-intersection-free within N steps; separately assert the escape hatch is not
@@ -881,7 +946,38 @@ triggered by a correctly authored garment under normal animation.
881
946
  **P5 — Jitter at rest.**
882
947
  *Mechanism:* the multiplier decay `α·γ < 1` bleeds accumulated force out of a resting configuration so
883
948
  it cannot ring; per-cloth sleep when the maximum particle displacement over a window falls below a
884
- 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.
885
981
  *M0 found the gap, and it is wider than "nothing is damping it".* The mechanism reaches **hard** rows
886
982
  only, because a soft row has no multiplier to decay — and a soft row whose penalty exceeds about
887
983
  `8·(m/h²)` does not merely fail to dissipate, it makes the step **add** energy. Measured on a hanging
@@ -903,7 +999,8 @@ pose is seven exact numbers and a field is a continuum.
903
999
 
904
1000
  *M1 landed both halves and measured them.* Every library fabric, on a 12-link chain set swinging at
905
1001
  3 m/s, sleeps: **220 steps for denim, 770 for silk**, everything else between, and the order is the
906
- 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"
907
1004
  a fact rather than a hope — a pose that is not rewritten cannot drift, and one rewritten from unchanged
908
1005
  inputs is only *probably* identical. A cloth wakes on anchor motion, on a `ClothDynamics` change, on a
909
1006
  `blend` change, and on an explicit `ClothSystem#wake` for anything it cannot see.
@@ -1008,8 +1105,18 @@ loop, so §14 makes the analytic gradients shared code that both sides use.
1008
1105
  ### 8.3 Friction
1009
1106
 
1010
1107
  Coulomb friction as a row on the same contact, with `μ` combining `Collider#friction` with
1011
- `ClothCollider#friction_scale`. The combine rule is the one `physics/contact/combine_material.js`
1012
- 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.
1013
1120
 
1014
1121
  **M2 made three of those words exact.** The clamp is `[0, μ·f_n]` rather than `±μ·f_n`, because the
1015
1122
  row's `C` is the *magnitude* of the tangential slip — §6 carries the derivation and what writing it
@@ -1235,6 +1342,19 @@ for the same reason — a local transform is what a joint angle *is*.
1235
1342
  The authored pose is not lost by being overwritten. It is captured once, when the cloth seeds, and
1236
1343
  lives in the cloth from then on — which is what `blend = 0` returns to, what the leash centres are
1237
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.
1238
1358
  The consequence, stated: **in the entity path the animated pose is constant in the anchor's frame.**
1239
1359
  That is a property of the case rather than a limitation of the code — a subtree's authored pose is
1240
1360
  rigid relative to its anchor — but it does mean a subtree whose *links* are independently animated is
@@ -1271,15 +1391,22 @@ how anything moves.
1271
1391
 
1272
1392
  ### 11.2 Skinned garments — `Cloth` + `ClothRig`
1273
1393
 
1274
- A character's cape is not an entity hierarchy and cannot be made into one.
1275
- `shade_node_to_entity_composition` expands a model into entities and attaches a `ShadedGeometry` to
1276
- each node that draws — but a skinned node contributes its transform and its name and **no geometry**,
1277
- because its vertices deform against a skeleton the renderer owns and the GPU poses. Its joints are
1278
- `Node3D`s inside the entity's `SceneBundleInstance`, and making them entities is explicitly a design
1279
- this program does not do (D47). So for a skinned garment there is no `ShadedGeometry` component and no
1280
- bone entities: the geometry is registered with `GPUAnimationManager` through
1281
- `register_instance_animation`, and the only writable handle is the joint's `Node3D.transform_local` —
1282
- 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`.
1283
1410
 
1284
1411
  **And cloth has to *claim* those joints, which §11.2 originally read as though it did not.** The
1285
1412
  sentence above says the handle is "the same handle the animation systems write", and they do not
@@ -1295,6 +1422,14 @@ race the enum exists to prevent, and for a cape that is the point of simulating
1295
1422
  whose joints are also keyed it is an authoring error, and the claim is where it surfaces.
1296
1423
  `MEASUREMENTS.md` §25.
1297
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
+
1298
1433
  **The anchor's world pose is computed, never read back.** A joint of an animated skeleton has no
1299
1434
  CPU-side `transform_global` — nothing maintains it — so the rig path evaluates the anchor with
1300
1435
  `pose_evaluate_world` over the clips the entity is playing, which is the engine's own sanctioned
@@ -1378,9 +1513,14 @@ For a **rigged** garment, only steps 5–8 run, over the joint graph rather than
1378
1513
  10. **Validate**: manifoldness, no zero-length edges, and — the P4(c) precondition — that the skinned
1379
1514
  bind pose is collision-free against the character's `ClothCollider` set.
1380
1515
 
1381
- Vertex painting (per-vertex stiffness or slack multiplier) is supported through an optional texture or
1382
- vertex-colour channel and is never required. Steps 5 and 6 are the default, and the intent is that most
1383
- 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.
1384
1524
 
1385
1525
  ---
1386
1526
 
@@ -1444,6 +1584,33 @@ steps the world has taken rather than the ticks it has been asked about: it adva
1444
1584
  the right semantic on its own terms — a paused simulation should not have the wind move.
1445
1585
  `MEASUREMENTS.md` §50.
1446
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.
1613
+
1447
1614
  `Float32` for particle state, not `Float64`. The simulation runs in the cloth's **local frame**, so
1448
1615
  coordinates are garment-sized rather than world-sized and f32 has ample precision; the payoff is half
1449
1616
  the bandwidth in the loop that dominates. The local frame is not a memory optimisation that happens to
@@ -1527,7 +1694,7 @@ What this design takes from the engine rather than building:
1527
1694
  | "Opt a collider into a second simulation" | `FluidObstacle` + `FluidObstacleSystem`, `AcousticBody` |
1528
1695
  | Collider index shape | `AcousticOccluderIndex` + `OccluderBody` |
1529
1696
  | Component holds a built shared asset | `Collider#shape`, `ParticleEffect#program` — the convention `ClothRig#proxy` follows |
1530
- | 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 |
1531
1698
  | Constraint-row doctrine | `physics/constraint/solve_constraints.js` |
1532
1699
  | Subtree walk | `TransformAttachmentSystem#findChildrenOf` / `#countChildrenOf`, `TransformAttachment` |
1533
1700
  | Animated targets | `mesh_apply_skeletal_vertex_skinning` (flat arrays, worker-safe) |
@@ -1838,9 +2005,12 @@ available, loses on every axis and is gone.
1838
2005
  **Deliberately not in M2:** P4(d)'s bounded escape hatch, still — M2 has the contacts that make a
1839
2006
  particle get stuck, and also measured the one stuck configuration whose *residual is zero*, which is
1840
2007
  the quantity the escape hatch was to watch (§7 P4). It belongs with the self-collision of M4, where a
1841
- particle can be pinched between two rows rather than merely held by one. The backstop row is built,
1842
- specced and driven by the playground, but nothing **authors** one: its plane comes from a garment's own
1843
- skinned surface and a skinned garment is M3. Two-way coupling, per §2. The serialization adapters, per
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
1844
2014
  M1's own reasoning — which, as M1 now records, reached only `ClothRig`'s. The other two were not
1845
2015
  blocked by anything M2 had in hand or M3 was about to add, and this line is the first of the two
1846
2016
  restatements that carried them anyway.
@@ -2056,6 +2226,52 @@ one-way rule: cloth reads the air and does not disturb it, and anything that sho
2056
2226
  at 58 ns a particle — the one part of this that is not free is the one part per-substep sampling
2057
2227
  would double.
2058
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.
2274
+
2059
2275
  **Performance budget — measured at M0, re-measured at M1, and it still does not hold on its own.** One
2060
2276
  character's full cloth complement — 1,024 particles at 2 substeps × 4 iterations, on a triangulated
2061
2277
  patch carrying 16.8 row incidences per particle — cost **3.50 ms per frame** at best at M0, against a
@@ -2199,15 +2415,42 @@ arbitrary planar configuration. Four coplanar particles that are not an affine i
2199
2415
  read as bent. *Mitigation:* accepted for the fabrics the library targets. If a case appears, the fix is
2200
2416
  a rest-curvature offset in the stencil, which is a change to one row and not to the solver.
2201
2417
 
2202
- **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
2203
2419
  states the limit. M2 found that the half nobody had written down — sweeping the contact **set**, not
2204
2420
  just the poses — was the half that mattered: without it a capsule at 20 m/s created zero rows and the
2205
2421
  sheet was never touched. With the collider's travel folded into the discovery reach, 2 to 40 m/s all
2206
- behave. What remains is that both halves are built from the collider's *centre*: a collider spinning
2207
- fast enough that a point on its surface travels more than its own radius within one substep can still
2208
- be missed. *Mitigation:* more substeps in the affected `ClothDynamics`; genuine CCD stays out of scope.
2209
- The shape of a fix, if one is ever wanted, is to build the discovery margin from the swept **surface**
2210
- 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.
2211
2454
 
2212
2455
  **Open — a rigid collider driven through pinned cloth wraps rather than parts.** A contact row bounds
2213
2456
  the interpenetration; it cannot prevent it, because the cloth has nowhere to go. The playground's
@@ -2263,6 +2506,30 @@ shed vortex, not from a relaxation toward a local velocity, and this design has
2263
2506
  is moved by it. It is named because "a banner in a wake looks alive" is what a reader will expect from
2264
2507
  §10 and is not what a drag row alone produces. `MEASUREMENTS.md` §51.
2265
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
+
2266
2533
  **Open — the ambient is uniform in space and the field boundary is a step.** Two limits of §10's
2267
2534
  fallback, both deliberate and both cheap to see: a gust lifts a whole banner at once rather than
2268
2535
  travelling along it, and a garment carried out of a fluid volume crosses a discontinuity rather than a
@@ -2300,8 +2567,12 @@ What M5 changed is the shape of the remaining work, in three findings that betwe
2300
2567
  sentence this note used to end with.
2301
2568
 
2302
2569
  - **`PhysicsSystem`'s producer / `__interp_restore` contract cannot express half the feature.**
2303
- `Interpolated` is an entity-keyed marker and a rigged garment's joints are `Node3D`s, not entities
2304
- (§13.1). There is nothing to put the component on and no key to file a snapshot under.
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.
2305
2576
  - **Cloth needs only the publish half.** `__interp_restore` undoes render-time writes before the
2306
2577
  simulation reads them; cloth's state is its particle array and its write-back never touches the
2307
2578
  anchor it reads, so there is nothing to restore. The restore it *does* depend on — an anchor on a
@@ -2327,3 +2598,13 @@ contract `TransformAttachment#parent` already carries, and it is the right defau
2327
2598
  have is a way to *notice*. An entity added under a live `Cloth` is silently not simulated until
2328
2599
  someone re-seeds, which is a class of authoring mistake with no diagnostic behind it. `ClothDiagnostics`
2329
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.