@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
@@ -3,6 +3,7 @@ import { quat3_multiply } from "../../../../core/geom/3d/quaternion/quat3_multip
3
3
  import { quat3_nlerp } from "../../../../core/geom/3d/quaternion/quat3_nlerp.js";
4
4
  import { v3_quaternion_apply } from "../../../../core/geom/vec3/v3_quaternion_apply.js";
5
5
  import { v3_quaternion_apply_inverse } from "../../../../core/geom/vec3/v3_quaternion_apply_inverse.js";
6
+ import { EntityObserver } from "../../../ecs/EntityObserver.js";
6
7
  import {
7
8
  TransformAttachment
8
9
  } from "../../../ecs/transform-attachment/TransformAttachment.js";
@@ -30,6 +31,7 @@ import { ClothRig } from "./ClothRig.js";
30
31
  import {
31
32
  CLOTH_ANCHOR_ROTATION_LIMIT,
32
33
  CLOTH_ANCHOR_TRAVEL_REACH,
34
+ CLOTH_SLEEP_DISPLACEMENT_RATIO,
33
35
  CLOTH_SLEEP_SPEED_RATIO,
34
36
  CLOTH_SLEEP_STEPS,
35
37
  CLOTH_STABILISE_STEPS,
@@ -41,14 +43,17 @@ import {
41
43
  } from "./cloth_dynamics_map.js";
42
44
  import {
43
45
  cloth_colliders_moved,
44
- cloth_gather_colliders
46
+ cloth_gather_colliders,
47
+ cloth_gather_reach,
48
+ cloth_remember_colliders
45
49
  } from "./cloth_gather_colliders.js";
46
50
  import {
47
51
  cloth_dynamics_change_needs_reseed,
48
52
  cloth_reparameterise
49
53
  } from "./cloth_build_rows.js";
50
- import { cloth_seed_subtree } from "./cloth_seed_subtree.js";
51
- import { cloth_seed_rig } from "./cloth_seed_rig.js";
54
+ import { Cloth } from "./Cloth.js";
55
+ import { cloth_release_subtree, cloth_seed_subtree } from "./cloth_seed_subtree.js";
56
+ import { cloth_release_rig, cloth_seed_rig } from "./cloth_seed_rig.js";
52
57
  import { cloth_write_back } from "./cloth_write_back.js";
53
58
  import { cloth_write_back_rig } from "./cloth_write_back_rig.js";
54
59
 
@@ -143,8 +148,16 @@ export class ClothWorld {
143
148
  gravity = new Float64Array([0, -9.81, 0]);
144
149
 
145
150
  /**
146
- * Live cloths, ordered so that an instance whose anchor sits inside another
147
- * instance's subtree steps after it.
151
+ * Live cloths, in the order they were linked.
152
+ *
153
+ * **Not sorted by hierarchy depth**, and it used to be. The sort existed so
154
+ * that a nested cloth advanced against an anchor the cloth above it had
155
+ * already written this step — which `WorkerClothSystem` cannot do, because
156
+ * the step it is writing back happened on another thread a tick ago. Both
157
+ * systems now advance every cloth, then step every cloth, then finish every
158
+ * cloth, so a nested cloth reads its parent one fixed step stale on both
159
+ * sides and the sort has nothing left to decide. `ClothSystem#fixedUpdate`
160
+ * carries the argument; `MEASUREMENTS.md` §56 carries the measurement.
148
161
  * @type {ClothInstance[]}
149
162
  */
150
163
  instances = [];
@@ -252,10 +265,28 @@ export class ClothWorld {
252
265
  __by_entity = new Map();
253
266
 
254
267
  /**
255
- * @type {boolean}
268
+ * Watches `Cloth` + {@link ClothRig} so the rig path is a **live**
269
+ * discriminator rather than one read once.
270
+ *
271
+ * `ClothSystem#dependencies` is `[Cloth, Transform64]` — the tuple that
272
+ * makes something a cloth — so a `ClothRig` added to a live cloth completes
273
+ * no tuple the system observes and a `ClothRig` removed from one breaks
274
+ * none. §3.1's "the discriminator is a component's presence" then holds only
275
+ * for a character spawned whole, which is the one case a spec constructs and
276
+ * the least likely one in a scene where a model loads asynchronously.
277
+ *
278
+ * Both edges re-seed. The removing edge also gives the joints back first,
279
+ * because the instance that knows which they were is about to stop being a
280
+ * rig.
281
+ * @type {EntityObserver}
256
282
  * @private
257
283
  */
258
- __order_dirty = false;
284
+ __rig_observer = new EntityObserver(
285
+ [Cloth, ClothRig],
286
+ (cloth, rig, entity) => this.reseed(entity),
287
+ (cloth, rig, entity) => this.#rig_removed(entity),
288
+ this
289
+ );
259
290
 
260
291
  /**
261
292
  * Resolve the systems a cloth world reads. Called from the owning system's
@@ -299,11 +330,30 @@ export class ClothWorld {
299
330
  this.instances.push(instance);
300
331
  this.__by_entity.set(entity, instance);
301
332
 
302
- this.__order_dirty = true;
303
-
304
333
  return instance;
305
334
  }
306
335
 
336
+ /**
337
+ * Watch this dataset for a {@link ClothRig} arriving at or leaving a live
338
+ * cloth. Called from the owning system's `handleDatasetAttached`; see
339
+ * {@link ClothWorld#__rig_observer} for why the system's own dependency
340
+ * tuple cannot answer this.
341
+ *
342
+ * @param {EntityComponentDataset} dataset
343
+ * @returns {void}
344
+ */
345
+ attach_dataset(dataset) {
346
+ dataset.addObserver(this.__rig_observer, true);
347
+ }
348
+
349
+ /**
350
+ * @param {EntityComponentDataset} dataset
351
+ * @returns {void}
352
+ */
353
+ detach_dataset(dataset) {
354
+ dataset.removeObserver(this.__rig_observer, false);
355
+ }
356
+
307
357
  /**
308
358
  * @param {number} entity
309
359
  * @returns {ClothInstance|undefined} the instance that was removed
@@ -315,6 +365,14 @@ export class ClothWorld {
315
365
  return undefined;
316
366
  }
317
367
 
368
+ // Give back what the seeding took. Seeding is not a read: it revokes
369
+ // the GPU's authority over a rig's joints, and it leaves the entity
370
+ // path's authored local transforms nowhere but inside the cloth,
371
+ // because `cloth_write_back` overwrites them every step. A cloth
372
+ // removed without this leaves joints nothing is writing and a subtree
373
+ // frozen in the shape the solver stopped in.
374
+ this.#release(instance);
375
+
318
376
  this.__by_entity.delete(entity);
319
377
 
320
378
  const index = this.instances.indexOf(instance);
@@ -326,6 +384,51 @@ export class ClothWorld {
326
384
  return instance;
327
385
  }
328
386
 
387
+ /**
388
+ * Hand back whatever this instance's seeding took from the scene.
389
+ *
390
+ * @param {ClothInstance} instance
391
+ * @private
392
+ */
393
+ #release(instance) {
394
+ if (!instance.seeded) {
395
+ return;
396
+ }
397
+
398
+ const ecd = this.entityManager === null ? null : this.entityManager.dataset;
399
+
400
+ if (ecd === null || ecd === undefined) {
401
+ return;
402
+ }
403
+
404
+ if (instance.rig === null) {
405
+ cloth_release_subtree(instance, ecd);
406
+ } else {
407
+ cloth_release_rig(instance, ecd);
408
+ }
409
+ }
410
+
411
+ /**
412
+ * A {@link ClothRig} has left a live cloth: give its joints back, then
413
+ * re-seed, which without a rig is the entity path.
414
+ *
415
+ * @param {number} entity
416
+ * @private
417
+ */
418
+ #rig_removed(entity) {
419
+ const instance = this.__by_entity.get(entity);
420
+
421
+ if (instance === undefined) {
422
+ return;
423
+ }
424
+
425
+ this.#release(instance);
426
+
427
+ instance.rig = null;
428
+
429
+ this.reseed(entity);
430
+ }
431
+
329
432
  /**
330
433
  * Throw away a cloth's binding so the next step walks its subtree again.
331
434
  *
@@ -348,8 +451,6 @@ export class ClothWorld {
348
451
  instance.seeded = false;
349
452
  instance.anchor_valid = false;
350
453
 
351
- this.__order_dirty = true;
352
-
353
454
  return instance;
354
455
  }
355
456
 
@@ -385,10 +486,6 @@ export class ClothWorld {
385
486
  * @param {number} dt seconds
386
487
  */
387
488
  begin(ecd, dt) {
388
- if (this.__order_dirty) {
389
- this.#reorder(ecd);
390
- }
391
-
392
489
  // Owed to the clock, and spent by the first `advance` — see {@link time}
393
490
  // for why the two are not the same moment. Assigned rather than set, so a
394
491
  // world whose source is taken away does not carry a debt.
@@ -457,10 +554,8 @@ export class ClothWorld {
457
554
  instance.contact_count = result[ClothStepResult.CONTACT_COUNT];
458
555
  instance.self_pairs = result[ClothStepResult.SELF_PAIRS];
459
556
 
460
- const speed = result[ClothStepResult.SPEED];
461
-
462
557
  if ((instance.cloth.flags & ClothFlags.NeverSleep) === 0) {
463
- if (speed < CLOTH_SLEEP_SPEED_RATIO * instance.reach) {
558
+ if (cloth_inside_sleep_window(instance)) {
464
559
  instance.still_steps++;
465
560
 
466
561
  if (instance.still_steps >= CLOTH_SLEEP_STEPS) {
@@ -502,7 +597,7 @@ export class ClothWorld {
502
597
  #seed(instance, ecd, dt) {
503
598
  const rig = ecd.getComponent(instance.entity, ClothRig);
504
599
 
505
- if (rig === undefined || rig === null || rig.proxy === null) {
600
+ if (rig === undefined || rig === null) {
506
601
  instance.rig = null;
507
602
 
508
603
  cloth_seed_subtree(instance, ecd, this.attachments, dt);
@@ -510,6 +605,17 @@ export class ClothWorld {
510
605
  return;
511
606
  }
512
607
 
608
+ if (rig.proxy === null) {
609
+ // Present and not ready. The same branch as a model that has not
610
+ // loaded, and for the same reason: a proxy filled in a few ticks
611
+ // after the component is a rig that is arriving, not a rig that is
612
+ // absent. Treating it as absent walks the character's own transform
613
+ // subtree — which under a spawned model is the whole skeleton — and
614
+ // seeds a skeleton as one rope, throwing in a dev build through
615
+ // `gpu_authority_assert_cpu` and simulating it in a shipped one.
616
+ return;
617
+ }
618
+
513
619
  if (this.models === null) {
514
620
  this.#refuse(instance, `entity ${instance.entity} has a ClothRig and nothing hands out model instances`);
515
621
 
@@ -555,44 +661,6 @@ export class ClothWorld {
555
661
  console.warn(`ClothSystem: ${message}`);
556
662
  }
557
663
 
558
- /**
559
- * Depth-first order by hierarchy depth, so a nested cloth reads an anchor
560
- * the cloth above it has already written.
561
- *
562
- * @param {EntityComponentDataset} ecd
563
- * @private
564
- */
565
- #reorder(ecd) {
566
- const instances = this.instances;
567
-
568
- for (let i = 0; i < instances.length; i++) {
569
- const instance = instances[i];
570
-
571
- let depth = 0;
572
- let entity = instance.entity;
573
-
574
- // Bounded by the number of entities; a cycle in the attachment
575
- // graph is not this system's to diagnose, but it is this loop's to
576
- // survive.
577
- for (let guard = 0; guard < 4096; guard++) {
578
- const attachment = ecd.getComponent(entity, TransformAttachment);
579
-
580
- if (attachment === undefined) {
581
- break;
582
- }
583
-
584
- entity = attachment.parent;
585
- depth++;
586
- }
587
-
588
- instance.depth = depth;
589
- }
590
-
591
- instances.sort((a, b) => a.depth - b.depth);
592
-
593
- this.__order_dirty = false;
594
- }
595
-
596
664
  /**
597
665
  * Carry one seeded cloth to the edge of its step and write the parameters
598
666
  * the step will run on: whatever the anchor did to the local frame, where
@@ -606,6 +674,11 @@ export class ClothWorld {
606
674
  * that is asleep, and then nothing has been written to `params`
607
675
  */
608
676
  advance(instance, dt, params, offset) {
677
+ // This cloth's own clock, moved before anything reads it — including
678
+ // the wake tests below, which are asking about the step that would
679
+ // follow. See {@link ClothInstance#age}.
680
+ instance.age += dt;
681
+
609
682
  if (this.__wind_pending !== 0) {
610
683
  this.time += this.__wind_pending;
611
684
 
@@ -686,7 +759,8 @@ export class ClothWorld {
686
759
  if (cloth_air_changed(
687
760
  instance.state, this.wind,
688
761
  instance.anchor_translation, instance.anchor_rotation,
689
- instance.air_seen, CLOTH_SLEEP_SPEED_RATIO * instance.reach
762
+ instance.air_seen, CLOTH_SLEEP_SPEED_RATIO * instance.reach,
763
+ instance.age
690
764
  )) {
691
765
  instance.asleep = false;
692
766
  instance.still_steps = 0;
@@ -749,12 +823,23 @@ export class ClothWorld {
749
823
  state.contact_radius = dynamics.thickness;
750
824
 
751
825
  if (this.colliders !== null) {
752
- // The gather's margin is how far a particle may travel this step,
753
- // which is the velocity clamp read as a distance — the quantity it
754
- // was built out of (`cloth_dynamics_map.js`).
755
- cloth_gather_colliders(instance, this.colliders.index, limit * dt);
826
+ // The gather's margin is what the narrowphase will reach for and
827
+ // nothing beyond it — `cloth_gather_reach` carries the derivation,
828
+ // and why the velocity clamp that used to stand here was both far
829
+ // too wide and missing two terms.
830
+ const reach = cloth_gather_reach(state, this.colliders.index, dt);
831
+
832
+ cloth_gather_colliders(instance, this.colliders.index, reach);
833
+
834
+ // What this cloth is about to step against, for the next wake test
835
+ // to compare with. Here rather than inside the gather because the
836
+ // wake test gathers too, and what it asks about is the set the last
837
+ // *step* saw.
838
+ cloth_remember_colliders(instance, reach);
756
839
  } else {
757
840
  state.collider_count = 0;
841
+
842
+ instance.collider_key_count = 0;
758
843
  }
759
844
 
760
845
  // §10. The one piece of a cloth's step that has to read the world and
@@ -770,7 +855,7 @@ export class ClothWorld {
770
855
  instance.air_varies = cloth_sample_air(
771
856
  state, this.wind,
772
857
  instance.anchor_translation, instance.anchor_rotation,
773
- instance.air_seen
858
+ instance.air_seen, instance.age
774
859
  );
775
860
  } else {
776
861
  instance.air_varies = false;
@@ -1059,6 +1144,65 @@ function move_local_frame(
1059
1144
  */
1060
1145
  const CARRIED = new Float64Array(3);
1061
1146
 
1147
+ /**
1148
+ * Whether every particle is still within the sleep window's own anchor — §7
1149
+ * P5's "maximum particle displacement over a window", and the reason the test
1150
+ * is not the instantaneous speed it used to be.
1151
+ *
1152
+ * A cloth **resting on a collider** does not stop: its contact rows carry a
1153
+ * penalty that `γ = 1` never decays, and what it settles into is a bounded
1154
+ * limit cycle. Measured on an 8-link rope on a floor, that is a tenth of a
1155
+ * millimetre of travel a step — nothing anyone can see — at a peak speed of
1156
+ * 1.5e-2 m/s, eighteen times the threshold a speed test allowed. So the speed
1157
+ * test held four of the seven library fabrics awake indefinitely on a floor
1158
+ * while the same fabrics slept in 283 steps hanging free. Distance covered
1159
+ * since the window opened tells those two apart; a speed sampled at one instant
1160
+ * cannot. `MEASUREMENTS.md` §53.
1161
+ *
1162
+ * The anchor is re-taken whenever a particle leaves the threshold, so the
1163
+ * window measures "how far has anything moved since the cloth last moved"
1164
+ * rather than chopping time into fixed epochs a slow drift could hide inside.
1165
+ *
1166
+ * @param {ClothInstance} instance
1167
+ * @returns {boolean}
1168
+ * @private
1169
+ */
1170
+ function cloth_inside_sleep_window(instance) {
1171
+ const position = instance.state.position;
1172
+
1173
+ let reference = instance.sleep_reference;
1174
+
1175
+ if (reference === null || reference.length !== position.length) {
1176
+ // First step, or a re-seed that changed the particle count. There is no
1177
+ // window yet, so this step opens one.
1178
+ instance.sleep_reference = Float32Array.from(position);
1179
+
1180
+ return false;
1181
+ }
1182
+
1183
+ const threshold = CLOTH_SLEEP_DISPLACEMENT_RATIO * instance.reach;
1184
+
1185
+ const threshold_squared = threshold * threshold;
1186
+
1187
+ const particle_count = instance.state.particle_count;
1188
+
1189
+ for (let i = 0; i < particle_count; i++) {
1190
+ const p = i * 3;
1191
+
1192
+ const dx = position[p] - reference[p];
1193
+ const dy = position[p + 1] - reference[p + 1];
1194
+ const dz = position[p + 2] - reference[p + 2];
1195
+
1196
+ if (dx * dx + dy * dy + dz * dz > threshold_squared) {
1197
+ reference.set(position);
1198
+
1199
+ return false;
1200
+ }
1201
+ }
1202
+
1203
+ return true;
1204
+ }
1205
+
1062
1206
  /**
1063
1207
  * Point every leash row at where it measures from: a slack row at the
1064
1208
  * particle's animated position, a tether at the anchor.
@@ -133,6 +133,16 @@ export class WorkerClothSystem extends WorkerSystem {
133
133
  get models(): {
134
134
  instance_of: (arg0: number) => (PrefabInstance | null);
135
135
  };
136
+ /**
137
+ * @param {EntityComponentDataset} dataset
138
+ * @returns {void}
139
+ */
140
+ handleDatasetAttached(dataset: EntityComponentDataset): void;
141
+ /**
142
+ * @param {EntityComponentDataset} dataset
143
+ * @returns {void}
144
+ */
145
+ handleDatasetDetached(dataset: EntityComponentDataset): void;
136
146
  /**
137
147
  * @param {Cloth} cloth
138
148
  * @param {Transform64} transform
@@ -1 +1 @@
1
- {"version":3,"file":"WorkerClothSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/cloth/ecs/WorkerClothSystem.js"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH;IAoEI;;;;;;;;OAQG;IACH;QAPiC,cAAc,EAApC,MAAM,MAAM;QAIK,eAAe,GAAhC,MAAM;OAYhB;IApFD,oDAAoC;IAEpC,iUAOE;IAEF;;;;;OAKG;IACH,OAFU,UAAU,CAEK;IA6EzB;;OAEG;IACH,iCAEC;IAbD;;;OAGG;IACH,4BAEC;IAoBD;;OAEG;IACH,qBAEC;IAhBD;;;;;;OAMG;IACH,gBAEC;IASD;;OAEG;IACH,iCAEC;IASD;;OAEG;IACH;4BAToC,MAAM,KAAG,CAAC,iBAAe,IAAI,CAAC;OAWjE;IAZD;;OAEG;IACH;4BAFoC,MAAM,KAAG,CAAC,iBAAe,IAAI,CAAC;MAIjE;IA6CD;;;;OAIG;IACH,YAJW,KAAK,aACL,WAAW,UACX,MAAM,QAIhB;IAED;;;;OAIG;IACH,cAJW,KAAK,aACL,WAAW,UACX,MAAM,QAQhB;IAED;;;;OAIG;IACH,eAHW,MAAM,GACJ,IAAI,CAIhB;IAED;;;;OAIG;IACH,aAHW,MAAM,GACJ,IAAI,CAIhB;IAED;;;OAGG;IACH,mBAHW,MAAM,GACJ,gBAAc,SAAS,CAInC;IAED;;;;OAIG;IACH,iCAEC;;CAmOJ;6BA/iB4B,oCAAoC;4BAIrC,uCAAuC;sBAE7C,YAAY;4CAPU,uDAAuD;oCAI5F,0DAA0D;8BAInC,oBAAoB;6BACrB,mBAAmB;yBACvB,eAAe;2BACb,iBAAiB"}
1
+ {"version":3,"file":"WorkerClothSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/cloth/ecs/WorkerClothSystem.js"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH;IAoEI;;;;;;;;OAQG;IACH;QAPiC,cAAc,EAApC,MAAM,MAAM;QAIK,eAAe,GAAhC,MAAM;OAYhB;IApFD,oDAAoC;IAEpC,iUAOE;IAEF;;;;;OAKG;IACH,OAFU,UAAU,CAEK;IA6EzB;;OAEG;IACH,iCAEC;IAbD;;;OAGG;IACH,4BAEC;IAoBD;;OAEG;IACH,qBAEC;IAhBD;;;;;;OAMG;IACH,gBAEC;IASD;;OAEG;IACH,iCAEC;IASD;;OAEG;IACH;4BAToC,MAAM,KAAG,CAAC,iBAAe,IAAI,CAAC;OAWjE;IAZD;;OAEG;IACH;4BAFoC,MAAM,KAAG,CAAC,iBAAe,IAAI,CAAC;MAIjE;IAiCD;;;OAGG;IACH,wDAFa,IAAI,CAIhB;IAED;;;OAGG;IACH,wDAFa,IAAI,CAIhB;IAcD;;;;OAIG;IACH,YAJW,KAAK,aACL,WAAW,UACX,MAAM,QAIhB;IAED;;;;OAIG;IACH,cAJW,KAAK,aACL,WAAW,UACX,MAAM,QAQhB;IAED;;;;OAIG;IACH,eAHW,MAAM,GACJ,IAAI,CAIhB;IAED;;;;OAIG;IACH,aAHW,MAAM,GACJ,IAAI,CAIhB;IAED;;;OAGG;IACH,mBAHW,MAAM,GACJ,gBAAc,SAAS,CAInC;IAED;;;;OAIG;IACH,iCAEC;;CAmOJ;6BA/jB4B,oCAAoC;4BAIrC,uCAAuC;sBAE7C,YAAY;4CAPU,uDAAuD;oCAI5F,0DAA0D;8BAInC,oBAAoB;6BACrB,mBAAmB;yBACvB,eAAe;2BACb,iBAAiB"}
@@ -22,7 +22,7 @@ import {
22
22
  * which is allocated once at startup and cannot grow.
23
23
  * @type {number}
24
24
  */
25
- const DEFAULT_MAX_CLOTH_COUNT = 32;
25
+ const DEFAULT_MAX_CLOTH_COUNT = 64;
26
26
 
27
27
  /**
28
28
  * Drop-in alternative to {@link ClothSystem} that runs the solve on a worker
@@ -265,6 +265,22 @@ export class WorkerClothSystem extends WorkerSystem {
265
265
  await super.startup(entityManager);
266
266
  }
267
267
 
268
+ /**
269
+ * @param {EntityComponentDataset} dataset
270
+ * @returns {void}
271
+ */
272
+ handleDatasetAttached(dataset) {
273
+ this.world.attach_dataset(dataset);
274
+ }
275
+
276
+ /**
277
+ * @param {EntityComponentDataset} dataset
278
+ * @returns {void}
279
+ */
280
+ handleDatasetDetached(dataset) {
281
+ this.world.detach_dataset(dataset);
282
+ }
283
+
268
284
  /**
269
285
  * @param {EntityManager} entityManager
270
286
  * @returns {Promise<void>}
@@ -277,12 +277,36 @@ export const CLOTH_STABILISE_STEPS: number;
277
277
  */
278
278
  export const CLOTH_DRAG_RATE_MAX: number;
279
279
  /**
280
- * Speed below which a particle counts as still, as a fraction of the cloth's
281
- * reach per second. A metre-long rope sleeps once nothing on it is moving
282
- * faster than a millimetre a second.
280
+ * Speed below which the air counts as unchanged, as a fraction of the cloth's
281
+ * reach per second — §10's wake test, which asks whether a gust has arrived.
282
+ *
283
+ * This was the sleep test's own threshold until the sleep test stopped reading
284
+ * a speed; see {@link CLOTH_SLEEP_DISPLACEMENT_RATIO} for why.
283
285
  * @type {number}
284
286
  */
285
287
  export const CLOTH_SLEEP_SPEED_RATIO: number;
288
+ /**
289
+ * How far a particle may travel from the window's anchor and still count as
290
+ * still, as a fraction of the cloth's reach (§7 P5).
291
+ *
292
+ * **Displacement rather than speed, and a contact is why.** A cloth resting on
293
+ * a collider carries contact rows whose penalty `γ = 1` never decays, and the
294
+ * configuration it settles into is a *bounded limit cycle* rather than a rest
295
+ * state: measured on an 8-link rope on a floor, a tenth of a millimetre of
296
+ * travel per step — invisible — at a peak particle speed of 1.5e-2 m/s, which
297
+ * is eighteen times what the old speed threshold allowed. An instantaneous
298
+ * speed cannot tell a cloth vibrating in place apart from one going somewhere;
299
+ * the distance it has covered since the window opened can, and that is what
300
+ * §7 P5's own text has always described.
301
+ *
302
+ * `5e-4` rather than {@link CLOTH_SLEEP_SPEED_RATIO}'s `1e-3` so the gate is no
303
+ * *looser* than the speed test it replaces: over the {@link CLOTH_SLEEP_STEPS}
304
+ * window at the engine's 60 Hz fixed step, half a thousandth of a reach is the
305
+ * same millimetre a second. Every library entry on a floor or a sphere sleeps
306
+ * inside 961 steps at this value. `MEASUREMENTS.md` §53.
307
+ * @type {number}
308
+ */
309
+ export const CLOTH_SLEEP_DISPLACEMENT_RATIO: number;
286
310
  /**
287
311
  * How many consecutive still steps put a cloth to sleep (§7 P5). Half a second
288
312
  * at 60 Hz, which is long enough that a cloth pausing at the top of a swing
@@ -1 +1 @@
1
- {"version":3,"file":"cloth_dynamics_map.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/cloth/ecs/cloth_dynamics_map.js"],"names":[],"mappings":"AA0OA;;;;;;;;GAQG;AACH,gDAHW,MAAM,GACJ,MAAM,CAMlB;AAED;;;;;;;;GAQG;AACH,uCAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;GAWG;AACH,mDAPW,MAAM,YAEN,MAAM,cACN,MAAM,MACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,0CAJW,MAAM,MACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;;GAQG;AACH,kDAJW,MAAM,MACN,MAAM,GACJ,MAAM,CAQlB;AApVD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH;;;;;GAKG;AACH,+BAFU,MAAM,CAEqB;AAErC;;;;;GAKG;AACH,+BAFU,MAAM,CAEqB;AAErC;;;;;;;;;;;GAWG;AACH,mCAFU,MAAM,CAEsB;AAEtC;;;;;GAKG;AACH,mCAFU,MAAM,CAEyB;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,+CAFU,MAAM,CAEkC;AAElD;;;;;;;;GAQG;AACH,iCAFU,MAAM,CAEqB;AAErC;;;;;;;;;;;;;GAaG;AACH,wCAFU,MAAM,CAE6B;AAE7C;;;;;;;;GAQG;AACH,0CAFU,MAAM,CAE+B;AAE/C;;;;;;;;GAQG;AACH,mCAFU,MAAM,CAEsB;AAEtC;;;;;;;;GAQG;AACH,mCAFU,MAAM,CAEsB;AAEtC;;;;;;;GAOG;AACH,oCAFU,MAAM,CAEwB;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,kCAFU,MAAM,CAEsB;AAEtC;;;;;GAKG;AACH,sCAFU,MAAM,CAE4B;AAE5C;;;;;GAKG;AACH,gCAFU,MAAM,CAEoB"}
1
+ {"version":3,"file":"cloth_dynamics_map.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/cloth/ecs/cloth_dynamics_map.js"],"names":[],"mappings":"AAmQA;;;;;;;;GAQG;AACH,gDAHW,MAAM,GACJ,MAAM,CAMlB;AAED;;;;;;;;GAQG;AACH,uCAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;GAWG;AACH,mDAPW,MAAM,YAEN,MAAM,cACN,MAAM,MACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,0CAJW,MAAM,MACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;;GAQG;AACH,kDAJW,MAAM,MACN,MAAM,GACJ,MAAM,CAQlB;AA7WD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH;;;;;GAKG;AACH,+BAFU,MAAM,CAEqB;AAErC;;;;;GAKG;AACH,+BAFU,MAAM,CAEqB;AAErC;;;;;;;;;;;GAWG;AACH,mCAFU,MAAM,CAEsB;AAEtC;;;;;GAKG;AACH,mCAFU,MAAM,CAEyB;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,+CAFU,MAAM,CAEkC;AAElD;;;;;;;;GAQG;AACH,iCAFU,MAAM,CAEqB;AAErC;;;;;;;;;;;;;GAaG;AACH,wCAFU,MAAM,CAE6B;AAE7C;;;;;;;;GAQG;AACH,0CAFU,MAAM,CAE+B;AAE/C;;;;;;;;GAQG;AACH,mCAFU,MAAM,CAEsB;AAEtC;;;;;;;;GAQG;AACH,mCAFU,MAAM,CAEsB;AAEtC;;;;;;;GAOG;AACH,oCAFU,MAAM,CAEwB;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,kCAFU,MAAM,CAEsB;AAEtC;;;;;;;GAOG;AACH,sCAFU,MAAM,CAE4B;AAE5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,6CAFU,MAAM,CAEmC;AAEnD;;;;;GAKG;AACH,gCAFU,MAAM,CAEoB"}
@@ -217,13 +217,38 @@ export const CLOTH_STABILISE_STEPS = 20;
217
217
  export const CLOTH_DRAG_RATE_MAX = 60;
218
218
 
219
219
  /**
220
- * Speed below which a particle counts as still, as a fraction of the cloth's
221
- * reach per second. A metre-long rope sleeps once nothing on it is moving
222
- * faster than a millimetre a second.
220
+ * Speed below which the air counts as unchanged, as a fraction of the cloth's
221
+ * reach per second — §10's wake test, which asks whether a gust has arrived.
222
+ *
223
+ * This was the sleep test's own threshold until the sleep test stopped reading
224
+ * a speed; see {@link CLOTH_SLEEP_DISPLACEMENT_RATIO} for why.
223
225
  * @type {number}
224
226
  */
225
227
  export const CLOTH_SLEEP_SPEED_RATIO = 1e-3;
226
228
 
229
+ /**
230
+ * How far a particle may travel from the window's anchor and still count as
231
+ * still, as a fraction of the cloth's reach (§7 P5).
232
+ *
233
+ * **Displacement rather than speed, and a contact is why.** A cloth resting on
234
+ * a collider carries contact rows whose penalty `γ = 1` never decays, and the
235
+ * configuration it settles into is a *bounded limit cycle* rather than a rest
236
+ * state: measured on an 8-link rope on a floor, a tenth of a millimetre of
237
+ * travel per step — invisible — at a peak particle speed of 1.5e-2 m/s, which
238
+ * is eighteen times what the old speed threshold allowed. An instantaneous
239
+ * speed cannot tell a cloth vibrating in place apart from one going somewhere;
240
+ * the distance it has covered since the window opened can, and that is what
241
+ * §7 P5's own text has always described.
242
+ *
243
+ * `5e-4` rather than {@link CLOTH_SLEEP_SPEED_RATIO}'s `1e-3` so the gate is no
244
+ * *looser* than the speed test it replaces: over the {@link CLOTH_SLEEP_STEPS}
245
+ * window at the engine's 60 Hz fixed step, half a thousandth of a reach is the
246
+ * same millimetre a second. Every library entry on a floor or a sphere sleeps
247
+ * inside 961 steps at this value. `MEASUREMENTS.md` §53.
248
+ * @type {number}
249
+ */
250
+ export const CLOTH_SLEEP_DISPLACEMENT_RATIO = 5e-4;
251
+
227
252
  /**
228
253
  * How many consecutive still steps put a cloth to sleep (§7 P5). Half a second
229
254
  * at 60 Hz, which is long enough that a cloth pausing at the top of a swing