@driftengine/physics 3.61.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 (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. package/src/world.ts +961 -0
@@ -0,0 +1,73 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ /**
3
+ * Collision shapes, the spatial hash over them, and the swept kinematic sweep.
4
+ *
5
+ * **This package imports no other engine package**, the same shape as `@driftengine/drft` and
6
+ * `@driftengine/entities`. What it buys is a simulation that runs with no renderer in its module
7
+ * graph — in Node, in a worker, or on a server — which is what a server-authoritative consumer
8
+ * needs and what a peer dependency on core would have priced at core's whole gzipped weight.
9
+ *
10
+ * `@driftengine/core` re-exports every name here, so a consumer that reaches for collision through
11
+ * the core barrel is unaffected by where the files live. **Core depends on this package rather
12
+ * than the other way round**, which is the opposite direction from every other optional package
13
+ * and is not a choice: the cameras, the contact probe and the ribbon builder all need the sweep.
14
+ * **What would make it wrong** is the dynamics landing here becoming reachable from
15
+ * `createRenderer`, which `scripts/size-gate.test.mjs` measures rather than trusts.
16
+ */
17
+ export type { Aabb, Axis, Body, BodyPart } from './collide/index.ts';
18
+ export { AXIS_X, AXIS_Y, AXIS_Z, aabbFromCenter, bodyBounds, createBodyBounds, ejectFromSolid, moveAxis, segmentHit, } from './collide/index.ts';
19
+ export { faceCount, faceVertices } from './faces.ts';
20
+ export type { BodyDesc, BodyType } from './bodies.ts';
21
+ export { BODY_DYNAMIC, BODY_KINEMATIC, BODY_SLEEPING, BODY_STATIC, BodySet } from './bodies.ts';
22
+ export { DynamicTree } from './tree.ts';
23
+ export { MAX_BODIES, PairSet, layersInteract, pairA, pairB, pairKey } from './pairs.ts';
24
+ export type { Manifold, ShapePose } from './manifold.ts';
25
+ export { MAX_CONTACTS, collideShapes, createManifold } from './manifold.ts';
26
+ export type { Parallelism, WorldOptions } from './world.ts';
27
+ export { PhysicsWorld } from './world.ts';
28
+ export { ContactConstraints, LINEAR_SLOP, MAX_BIAS_VELOCITY, RESTITUTION_THRESHOLD, applyRestitution, isDynamicBody, prepareContact, solveContacts, warmStart, } from './solver.ts';
29
+ export { IslandSet } from './island.ts';
30
+ export type { FrictionModel } from './solver.ts';
31
+ export type { Executor, IslandOverlap, IslandSolver } from './executor.ts';
32
+ export { DisjointExecutor, SerialExecutor, ShuffledExecutor } from './executor.ts';
33
+ export type { IslandSolveState } from './islandSolve.ts';
34
+ export { solveIslandInto } from './islandSolve.ts';
35
+ export type { LanePlan, StagePlan } from './stage.ts';
36
+ export type { PoolOutcome, PoolRequest, PoolWorker } from './workerPool.ts';
37
+ export type { DofMode, JointDesc, JointType } from './joints.ts';
38
+ export { DOF_COUNT, DOF_FREE, DOF_LIMITED, DOF_LOCKED, JOINT_CONE_TWIST, JOINT_DISTANCE, JOINT_FIXED, JOINT_PRISMATIC, JOINT_REVOLUTE, JOINT_SIX_DOF, JOINT_SPHERICAL, JointSet, resetJointImpulses, solveJoints, } from './joints.ts';
39
+ export type { QueryFilter, RayHit } from './query.ts';
40
+ export { createRayHit, isStaticBody, overlapWorld, raycastWorld, shapecastWorld } from './query.ts';
41
+ export type { EventKind } from './events.ts';
42
+ export { ContactEvents, EVENT_ENTER, EVENT_EXIT, EVENT_STAY } from './events.ts';
43
+ export { BODY_SENSOR } from './bodies.ts';
44
+ export type { ControllerInput, ControllerOptions, ControllerState, GroundProbe, } from './controller.ts';
45
+ export { AIRBORNE, CharacterController, GROUNDED, SLIDING } from './controller.ts';
46
+ export type { PoseTarget, Ragdoll, RagdollOptions } from './ragdoll.ts';
47
+ export { ragdollFromBones } from './ragdoll.ts';
48
+ export type { TyreCurve, VehicleInput, VehicleOptions, WheelOptions } from './vehicle.ts';
49
+ export { Vehicle, defaultTyreCurve, sampleTyreCurve } from './vehicle.ts';
50
+ export type { SweptState, SweptStepOptions, WorldOpinion } from './sweptStep.ts';
51
+ export { SweptStep } from './sweptStep.ts';
52
+ export type { PlanarState } from './groundMotor.ts';
53
+ export { accelerateAlong } from './groundMotor.ts';
54
+ export type { Buoyancy, BuoyancyOptions, BuoyantState } from './buoyancy.ts';
55
+ export { applyBuoyancy, createBuoyancy } from './buoyancy.ts';
56
+ export type { EscapeResult, EscapeVeto, StallEscapeOptions, StallInput } from './stallEscape.ts';
57
+ export { ESCAPE_EASED, ESCAPE_IDLE, ESCAPE_PUSHED, ESCAPE_SEALED, StallEscape, } from './stallEscape.ts';
58
+ export type { ClothOptions } from './cloth.ts';
59
+ export { ClothBody, makeClothGrid } from './cloth.ts';
60
+ export type { Collider, ColliderBytes, ColliderGroup } from './colliderSet.ts';
61
+ export { ColliderSet, boxCollider, colliderFromShape } from './colliderSet.ts';
62
+ export { fingerprintBodies, fingerprintColliders } from './fingerprint.ts';
63
+ export type { MassProperties } from './mass.ts';
64
+ export { combineMassProperties, createMassProperties, shapeMassProperties } from './mass.ts';
65
+ export type { TriangleMesh } from './meshShape.ts';
66
+ export { meshShape } from './meshShape.ts';
67
+ export { heightfieldShape } from './heightfieldShape.ts';
68
+ export type { Heightfield } from './heightfieldShape.ts';
69
+ export type { ConvexShape } from './shape.ts';
70
+ export { boxShape, capsuleShape, cylinderShape, hullShape, shapeBounds, sphereShape, } from './shape.ts';
71
+ export { MAX_BODY_PARTS, MAX_PAIR_MANIFOLDS, collideCompound, partsBoundRadius, } from './compoundContact.ts';
72
+ export type { ConvexPart, Decomposition, DecomposeFill, DecomposeOptions } from './decompose.ts';
73
+ export { DEFAULT_MAX_HULLS, DEFAULT_RESOLUTION, MAX_RESOLUTION, SUPPORT_DIRECTIONS, decomposeConvex, } from './decompose.ts';
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ export { AXIS_X, AXIS_Y, AXIS_Z, aabbFromCenter, bodyBounds, createBodyBounds, ejectFromSolid, moveAxis, segmentHit, } from './collide/index.js';
3
+ export { faceCount, faceVertices } from './faces.js';
4
+ export { BODY_DYNAMIC, BODY_KINEMATIC, BODY_SLEEPING, BODY_STATIC, BodySet } from './bodies.js';
5
+ export { DynamicTree } from './tree.js';
6
+ export { MAX_BODIES, PairSet, layersInteract, pairA, pairB, pairKey } from './pairs.js';
7
+ export { MAX_CONTACTS, collideShapes, createManifold } from './manifold.js';
8
+ export { PhysicsWorld } from './world.js';
9
+ export { ContactConstraints, LINEAR_SLOP, MAX_BIAS_VELOCITY, RESTITUTION_THRESHOLD, applyRestitution, isDynamicBody, prepareContact, solveContacts, warmStart, } from './solver.js';
10
+ export { IslandSet } from './island.js';
11
+ export { DisjointExecutor, SerialExecutor, ShuffledExecutor } from './executor.js';
12
+ export { solveIslandInto } from './islandSolve.js';
13
+ export { DOF_COUNT, DOF_FREE, DOF_LIMITED, DOF_LOCKED, JOINT_CONE_TWIST, JOINT_DISTANCE, JOINT_FIXED, JOINT_PRISMATIC, JOINT_REVOLUTE, JOINT_SIX_DOF, JOINT_SPHERICAL, JointSet, resetJointImpulses, solveJoints, } from './joints.js';
14
+ export { createRayHit, isStaticBody, overlapWorld, raycastWorld, shapecastWorld } from './query.js';
15
+ export { ContactEvents, EVENT_ENTER, EVENT_EXIT, EVENT_STAY } from './events.js';
16
+ export { BODY_SENSOR } from './bodies.js';
17
+ export { AIRBORNE, CharacterController, GROUNDED, SLIDING } from './controller.js';
18
+ export { ragdollFromBones } from './ragdoll.js';
19
+ export { Vehicle, defaultTyreCurve, sampleTyreCurve } from './vehicle.js';
20
+ export { SweptStep } from './sweptStep.js';
21
+ export { accelerateAlong } from './groundMotor.js';
22
+ export { applyBuoyancy, createBuoyancy } from './buoyancy.js';
23
+ export { ESCAPE_EASED, ESCAPE_IDLE, ESCAPE_PUSHED, ESCAPE_SEALED, StallEscape, } from './stallEscape.js';
24
+ export { ClothBody, makeClothGrid } from './cloth.js';
25
+ export { ColliderSet, boxCollider, colliderFromShape } from './colliderSet.js';
26
+ export { fingerprintBodies, fingerprintColliders } from './fingerprint.js';
27
+ export { combineMassProperties, createMassProperties, shapeMassProperties } from './mass.js';
28
+ export { meshShape } from './meshShape.js';
29
+ /*
30
+ * A heightfield as a collider: the samples, and every triangle generated from them.
31
+ *
32
+ * `Heightfield` is deliberately shaped so that a `Terrain` from `@driftengine/terrain` *is* one —
33
+ * same names, same meanings — which is how a package that imports no other engine package still
34
+ * takes one. See `heightfieldShape.ts`.
35
+ */
36
+ export { heightfieldShape } from './heightfieldShape.js';
37
+ export { boxShape, capsuleShape, cylinderShape, hullShape, shapeBounds, sphereShape, } from './shape.js';
38
+ /*
39
+ * Convex decomposition: offline, and the only thing in this package that is not meant for a frame.
40
+ *
41
+ * Exported from the barrel because it produces `ConvexShape` point sets and belongs beside
42
+ * `hullShape`. A consumer who never names it never bundles it — the package is `sideEffects: false`
43
+ * and `physics-only` measures the consequence.
44
+ */
45
+ /* A body made of several shapes: the half of convex decomposition that lives in the tick. */
46
+ export { MAX_BODY_PARTS, MAX_PAIR_MANIFOLDS, collideCompound, partsBoundRadius, } from './compoundContact.js';
47
+ export { DEFAULT_MAX_HULLS, DEFAULT_RESOLUTION, MAX_RESOLUTION, SUPPORT_DIRECTIONS, decomposeConvex, } from './decompose.js';
@@ -0,0 +1,55 @@
1
+ import type { BodySet } from './bodies.ts';
2
+ import type { JointSet } from './joints.ts';
3
+ import type { ContactConstraints } from './solver.ts';
4
+ /**
5
+ * Connected components of the contact graph, and the sleeping that rides on them.
6
+ *
7
+ * **Only dynamic bodies join islands.** A static floor touches everything standing on it, so
8
+ * letting it merge would make one island of the whole world and nothing would ever sleep. That is
9
+ * not an optimisation: it is what an island *means*, and it is why two towers on one floor are two.
10
+ *
11
+ * **A body with no contacts is a singleton island**, so every dynamic body belongs to exactly one
12
+ * and the executor can own the whole substep loop for it rather than integration being a separate
13
+ * global pass. That is the shape a worker pool needs.
14
+ *
15
+ * **Islands are ordered by their lowest body index, and bodies within one by index.** That comes
16
+ * from the *ascending scan* below, which numbers an island the first time it meets a member — not
17
+ * from union-find's root choice, which is why swapping the root to the larger index changes no
18
+ * result and no test catches it. Keeping the smaller one is convention and shortens paths a little;
19
+ * nothing depends on it. Nothing here iterates a `Map` or a `Set`.
20
+ */
21
+ export declare class IslandSet {
22
+ /** How many islands the last build found. */
23
+ count: number;
24
+ /** Constraint indices grouped by island. */
25
+ constraintOrder: Int32Array;
26
+ /** Offsets into `constraintOrder`, length `count + 1`. */
27
+ constraintStart: Int32Array;
28
+ /** Dynamic body indices grouped by island. */
29
+ bodyOrder: Int32Array;
30
+ /** Offsets into `bodyOrder`, length `count + 1`. */
31
+ bodyStart: Int32Array;
32
+ /** Which island a body is in, or −1 for a body that is not dynamic. */
33
+ islandOf: Int32Array;
34
+ private parent;
35
+ private islandIndex;
36
+ constructor(capacity?: number);
37
+ /** Joint indices grouped by island. */
38
+ jointOrder: Int32Array;
39
+ /** Offsets into `jointOrder`, length `count + 1`. */
40
+ jointStart: Int32Array;
41
+ build(bodies: BodySet, c: ContactConstraints, joints: JointSet): number;
42
+ private islandOfJoint;
43
+ private islandOfConstraint;
44
+ /**
45
+ * The smaller index wins, which shortens paths slightly and is otherwise arbitrary.
46
+ *
47
+ * **It is not what orders islands** — the ascending scan in `build` is. Said here because the
48
+ * version of this comment that claimed otherwise sent a reader looking for a property the code
49
+ * does not have and does not need.
50
+ */
51
+ private union;
52
+ private find;
53
+ private ensureBodies;
54
+ private ensureConstraints;
55
+ }
package/dist/island.js ADDED
@@ -0,0 +1,227 @@
1
+ import { BODY_DYNAMIC } from './bodies.js';
2
+ /**
3
+ * Connected components of the contact graph, and the sleeping that rides on them.
4
+ *
5
+ * **Only dynamic bodies join islands.** A static floor touches everything standing on it, so
6
+ * letting it merge would make one island of the whole world and nothing would ever sleep. That is
7
+ * not an optimisation: it is what an island *means*, and it is why two towers on one floor are two.
8
+ *
9
+ * **A body with no contacts is a singleton island**, so every dynamic body belongs to exactly one
10
+ * and the executor can own the whole substep loop for it rather than integration being a separate
11
+ * global pass. That is the shape a worker pool needs.
12
+ *
13
+ * **Islands are ordered by their lowest body index, and bodies within one by index.** That comes
14
+ * from the *ascending scan* below, which numbers an island the first time it meets a member — not
15
+ * from union-find's root choice, which is why swapping the root to the larger index changes no
16
+ * result and no test catches it. Keeping the smaller one is convention and shortens paths a little;
17
+ * nothing depends on it. Nothing here iterates a `Map` or a `Set`.
18
+ */
19
+ export class IslandSet {
20
+ /** How many islands the last build found. */
21
+ count = 0;
22
+ /** Constraint indices grouped by island. */
23
+ constraintOrder;
24
+ /** Offsets into `constraintOrder`, length `count + 1`. */
25
+ constraintStart;
26
+ /** Dynamic body indices grouped by island. */
27
+ bodyOrder;
28
+ /** Offsets into `bodyOrder`, length `count + 1`. */
29
+ bodyStart;
30
+ /** Which island a body is in, or −1 for a body that is not dynamic. */
31
+ islandOf;
32
+ parent;
33
+ islandIndex;
34
+ constructor(capacity = 64) {
35
+ this.constraintOrder = new Int32Array(capacity);
36
+ this.constraintStart = new Int32Array(capacity + 1);
37
+ this.bodyOrder = new Int32Array(capacity);
38
+ this.bodyStart = new Int32Array(capacity + 1);
39
+ this.islandOf = new Int32Array(capacity);
40
+ this.parent = new Int32Array(capacity);
41
+ this.islandIndex = new Int32Array(capacity);
42
+ }
43
+ /** Joint indices grouped by island. */
44
+ jointOrder = new Int32Array(32);
45
+ /** Offsets into `jointOrder`, length `count + 1`. */
46
+ jointStart = new Int32Array(33);
47
+ build(bodies, c, joints) {
48
+ const n = bodies.count;
49
+ this.ensureBodies(n);
50
+ this.ensureConstraints(c.count);
51
+ for (let i = 0; i < n; i++)
52
+ this.parent[i] = i;
53
+ for (let k = 0; k < c.count; k++) {
54
+ const a = c.bodyA[k] ?? 0;
55
+ const b = c.bodyB[k] ?? 0;
56
+ if (bodies.type[a] !== BODY_DYNAMIC || bodies.type[b] !== BODY_DYNAMIC)
57
+ continue;
58
+ this.union(a, b);
59
+ }
60
+ /*
61
+ * A joint is an edge in the same graph a contact is. Two bodies held together would otherwise
62
+ * sleep independently and be woken independently, and a jointed pair whose halves disagree
63
+ * about being asleep pulls apart.
64
+ */
65
+ for (let k = 0; k < joints.count; k++) {
66
+ if (joints.broken[k])
67
+ continue;
68
+ const a = joints.bodyA[k] ?? 0;
69
+ const b = joints.bodyB[k] ?? 0;
70
+ if (bodies.type[a] !== BODY_DYNAMIC || bodies.type[b] !== BODY_DYNAMIC)
71
+ continue;
72
+ this.union(a, b);
73
+ }
74
+ // Roots in ascending order become islands 0, 1, 2 …, so ordering follows body index.
75
+ let islands = 0;
76
+ for (let i = 0; i < n; i++) {
77
+ this.islandOf[i] = -1;
78
+ this.islandIndex[i] = -1;
79
+ }
80
+ for (let i = 0; i < n; i++) {
81
+ if (bodies.type[i] !== BODY_DYNAMIC)
82
+ continue;
83
+ const root = this.find(i);
84
+ if (this.islandIndex[root] < 0)
85
+ this.islandIndex[root] = islands++;
86
+ this.islandOf[i] = this.islandIndex[root] ?? -1;
87
+ }
88
+ this.count = islands;
89
+ if (islands + 1 > this.bodyStart.length) {
90
+ this.bodyStart = new Int32Array(islands + 1);
91
+ this.constraintStart = new Int32Array(islands + 1);
92
+ }
93
+ // Counting sort of bodies into islands: count, prefix, place.
94
+ this.bodyStart.fill(0);
95
+ for (let i = 0; i < n; i++) {
96
+ const island = this.islandOf[i] ?? -1;
97
+ if (island >= 0)
98
+ this.bodyStart[island + 1]++;
99
+ }
100
+ for (let i = 0; i < islands; i++) {
101
+ this.bodyStart[i + 1] = (this.bodyStart[i + 1] ?? 0) + (this.bodyStart[i] ?? 0);
102
+ }
103
+ const bodyAt = new Int32Array(islands);
104
+ for (let i = 0; i < n; i++) {
105
+ const island = this.islandOf[i] ?? -1;
106
+ if (island < 0)
107
+ continue;
108
+ this.bodyOrder[(this.bodyStart[island] ?? 0) + (bodyAt[island] ?? 0)] = i;
109
+ bodyAt[island]++;
110
+ }
111
+ // And of constraints, by whichever of their bodies is dynamic.
112
+ this.constraintStart.fill(0);
113
+ for (let k = 0; k < c.count; k++) {
114
+ const island = this.islandOfConstraint(bodies, c, k);
115
+ if (island >= 0)
116
+ this.constraintStart[island + 1]++;
117
+ }
118
+ for (let i = 0; i < islands; i++) {
119
+ this.constraintStart[i + 1] =
120
+ (this.constraintStart[i + 1] ?? 0) + (this.constraintStart[i] ?? 0);
121
+ }
122
+ const conAt = new Int32Array(islands);
123
+ for (let k = 0; k < c.count; k++) {
124
+ const island = this.islandOfConstraint(bodies, c, k);
125
+ if (island < 0)
126
+ continue;
127
+ this.constraintOrder[(this.constraintStart[island] ?? 0) + (conAt[island] ?? 0)] = k;
128
+ conAt[island]++;
129
+ }
130
+ // And of joints, the same way.
131
+ if (islands + 1 > this.jointStart.length)
132
+ this.jointStart = new Int32Array(islands + 1);
133
+ if (joints.count > this.jointOrder.length) {
134
+ let size = this.jointOrder.length || 1;
135
+ while (size < joints.count)
136
+ size *= 2;
137
+ this.jointOrder = new Int32Array(size);
138
+ }
139
+ this.jointStart.fill(0);
140
+ for (let k = 0; k < joints.count; k++) {
141
+ const island = this.islandOfJoint(bodies, joints, k);
142
+ if (island >= 0)
143
+ this.jointStart[island + 1]++;
144
+ }
145
+ for (let i = 0; i < islands; i++) {
146
+ this.jointStart[i + 1] = (this.jointStart[i + 1] ?? 0) + (this.jointStart[i] ?? 0);
147
+ }
148
+ const jointAt = new Int32Array(islands);
149
+ for (let k = 0; k < joints.count; k++) {
150
+ const island = this.islandOfJoint(bodies, joints, k);
151
+ if (island < 0)
152
+ continue;
153
+ this.jointOrder[(this.jointStart[island] ?? 0) + (jointAt[island] ?? 0)] = k;
154
+ jointAt[island]++;
155
+ }
156
+ return islands;
157
+ }
158
+ islandOfJoint(bodies, joints, k) {
159
+ if (joints.broken[k])
160
+ return -1;
161
+ const a = joints.bodyA[k] ?? 0;
162
+ const b = joints.bodyB[k] ?? 0;
163
+ if (bodies.type[a] === BODY_DYNAMIC)
164
+ return this.islandOf[a] ?? -1;
165
+ if (bodies.type[b] === BODY_DYNAMIC)
166
+ return this.islandOf[b] ?? -1;
167
+ return -1;
168
+ }
169
+ islandOfConstraint(bodies, c, k) {
170
+ const a = c.bodyA[k] ?? 0;
171
+ const b = c.bodyB[k] ?? 0;
172
+ if (bodies.type[a] === BODY_DYNAMIC)
173
+ return this.islandOf[a] ?? -1;
174
+ if (bodies.type[b] === BODY_DYNAMIC)
175
+ return this.islandOf[b] ?? -1;
176
+ return -1;
177
+ }
178
+ /**
179
+ * The smaller index wins, which shortens paths slightly and is otherwise arbitrary.
180
+ *
181
+ * **It is not what orders islands** — the ascending scan in `build` is. Said here because the
182
+ * version of this comment that claimed otherwise sent a reader looking for a property the code
183
+ * does not have and does not need.
184
+ */
185
+ union(a, b) {
186
+ const ra = this.find(a);
187
+ const rb = this.find(b);
188
+ if (ra === rb)
189
+ return;
190
+ if (ra < rb)
191
+ this.parent[rb] = ra;
192
+ else
193
+ this.parent[ra] = rb;
194
+ }
195
+ find(i) {
196
+ let root = i;
197
+ while ((this.parent[root] ?? root) !== root)
198
+ root = this.parent[root] ?? root;
199
+ // Path compression, so a long chain is walked once.
200
+ let at = i;
201
+ while ((this.parent[at] ?? at) !== at) {
202
+ const next = this.parent[at] ?? at;
203
+ this.parent[at] = root;
204
+ at = next;
205
+ }
206
+ return root;
207
+ }
208
+ ensureBodies(n) {
209
+ if (n <= this.parent.length)
210
+ return;
211
+ let size = this.parent.length || 1;
212
+ while (size < n)
213
+ size *= 2;
214
+ this.parent = new Int32Array(size);
215
+ this.islandOf = new Int32Array(size);
216
+ this.islandIndex = new Int32Array(size);
217
+ this.bodyOrder = new Int32Array(size);
218
+ }
219
+ ensureConstraints(n) {
220
+ if (n <= this.constraintOrder.length)
221
+ return;
222
+ let size = this.constraintOrder.length || 1;
223
+ while (size < n)
224
+ size *= 2;
225
+ this.constraintOrder = new Int32Array(size);
226
+ }
227
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * One island's substep loop, as a function over flat state, so a worker can run the same one.
3
+ *
4
+ * **This exists because `PhysicsWorld` cannot cross a realm boundary and an island's solve can.**
5
+ * The world holds `ConvexShape` objects, a `DynamicTree` and a `Map` of warm impulses, none of which
6
+ * a `SharedArrayBuffer` can carry. What the solve actually reads is four sets of typed arrays and
7
+ * eleven numbers, and that much is portable.
8
+ *
9
+ * **One implementation, two callers.** `PhysicsWorld.solveIsland` delegates here and so does
10
+ * `islandWorker.ts`; there is no second copy of the substep order anywhere. The rule against two
11
+ * implementations of one decision is what makes that non-negotiable, and `splatSortWorker.ts` is the
12
+ * precedent for satisfying it across a worker boundary — there by stringifying one function, here by
13
+ * both sides importing one module. **What would make this wrong** is a substep loop appearing inside
14
+ * the worker.
15
+ *
16
+ * The dials are a `Float64Array` and not an object because the worker reads them out of shared
17
+ * memory, and a shape both sides agree on without a per-tick message is the cheapest way to let a
18
+ * consumer change gravity between ticks and have the pool see it.
19
+ */
20
+ import type { BodySet } from './bodies.ts';
21
+ import type { IslandSet } from './island.ts';
22
+ import type { JointSet } from './joints.ts';
23
+ import type { ContactConstraints, FrictionModel } from './solver.ts';
24
+ export declare const DIAL_SUBSTEPS = 0;
25
+ export declare const DIAL_ITERATIONS = 1;
26
+ export declare const DIAL_DT = 2;
27
+ export declare const DIAL_GRAVITY_X = 3;
28
+ export declare const DIAL_GRAVITY_Y = 4;
29
+ export declare const DIAL_GRAVITY_Z = 5;
30
+ export declare const DIAL_LINEAR_DAMPING = 6;
31
+ export declare const DIAL_ANGULAR_DAMPING = 7;
32
+ export declare const DIAL_CONTACT_HERTZ = 8;
33
+ export declare const DIAL_CONTACT_DAMPING = 9;
34
+ export declare const DIAL_JOINT_HERTZ = 10;
35
+ export declare const DIAL_JOINT_DAMPING = 11;
36
+ export declare const DIAL_ALLOW_SLEEP = 12;
37
+ export declare const DIAL_FRICTION_MODEL = 13;
38
+ /** How many slots a dial block has. */
39
+ export declare const DIAL_COUNT = 14;
40
+ /**
41
+ * `FrictionModel` crosses as a number, and the mapping is written once here.
42
+ *
43
+ * A string would cross a `postMessage` perfectly well; it would not survive being read out of a
44
+ * `Float64Array`, which is where the dials live so that no message is needed at all.
45
+ */
46
+ export declare const FRICTION_BOX = 0;
47
+ export declare const FRICTION_ELLIPTICAL = 1;
48
+ export declare function frictionDial(model: FrictionModel): number;
49
+ export declare function frictionModelOf(dial: number): FrictionModel;
50
+ /** Ticks below the sleep thresholds before a body is willing to sleep. */
51
+ export declare const SLEEP_TICKS = 60;
52
+ /** Everything one island's solve reaches, and nothing else the world holds. */
53
+ export interface IslandSolveState {
54
+ bodies: BodySet;
55
+ constraints: ContactConstraints;
56
+ joints: JointSet;
57
+ islands: IslandSet;
58
+ dials: Float64Array<ArrayBufferLike>;
59
+ }
60
+ /** Whether a body has been still long enough to stop being solved. */
61
+ export declare function bodyAsleep(bodies: BodySet, allowSleep: boolean, i: number): boolean;
62
+ /** Whether every body in an island is asleep, which is what lets the island be skipped whole. */
63
+ export declare function islandAsleep(state: IslandSolveState, island: number): boolean;
64
+ /**
65
+ * Advance one island through every substep.
66
+ *
67
+ * Integration is over this island's bodies only, which is what makes the call independent of every
68
+ * other island rather than merely ordered against them.
69
+ */
70
+ export declare function solveIslandInto(state: IslandSolveState, island: number): void;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * One island's substep loop, as a function over flat state, so a worker can run the same one.
3
+ *
4
+ * **This exists because `PhysicsWorld` cannot cross a realm boundary and an island's solve can.**
5
+ * The world holds `ConvexShape` objects, a `DynamicTree` and a `Map` of warm impulses, none of which
6
+ * a `SharedArrayBuffer` can carry. What the solve actually reads is four sets of typed arrays and
7
+ * eleven numbers, and that much is portable.
8
+ *
9
+ * **One implementation, two callers.** `PhysicsWorld.solveIsland` delegates here and so does
10
+ * `islandWorker.ts`; there is no second copy of the substep order anywhere. The rule against two
11
+ * implementations of one decision is what makes that non-negotiable, and `splatSortWorker.ts` is the
12
+ * precedent for satisfying it across a worker boundary — there by stringifying one function, here by
13
+ * both sides importing one module. **What would make this wrong** is a substep loop appearing inside
14
+ * the worker.
15
+ *
16
+ * The dials are a `Float64Array` and not an object because the worker reads them out of shared
17
+ * memory, and a shape both sides agree on without a per-tick message is the cheapest way to let a
18
+ * consumer change gravity between ticks and have the pool see it.
19
+ */
20
+ import { BODY_DYNAMIC, BODY_STATIC } from './bodies.js';
21
+ import { solveJoints } from './joints.js';
22
+ import { applyRestitution, solveContacts, warmStart } from './solver.js';
23
+ /* Slots in the dial block. Named because a worker reads them from shared memory by index. */
24
+ export const DIAL_SUBSTEPS = 0;
25
+ export const DIAL_ITERATIONS = 1;
26
+ export const DIAL_DT = 2;
27
+ export const DIAL_GRAVITY_X = 3;
28
+ export const DIAL_GRAVITY_Y = 4;
29
+ export const DIAL_GRAVITY_Z = 5;
30
+ export const DIAL_LINEAR_DAMPING = 6;
31
+ export const DIAL_ANGULAR_DAMPING = 7;
32
+ export const DIAL_CONTACT_HERTZ = 8;
33
+ export const DIAL_CONTACT_DAMPING = 9;
34
+ export const DIAL_JOINT_HERTZ = 10;
35
+ export const DIAL_JOINT_DAMPING = 11;
36
+ export const DIAL_ALLOW_SLEEP = 12;
37
+ export const DIAL_FRICTION_MODEL = 13;
38
+ /** How many slots a dial block has. */
39
+ export const DIAL_COUNT = 14;
40
+ /**
41
+ * `FrictionModel` crosses as a number, and the mapping is written once here.
42
+ *
43
+ * A string would cross a `postMessage` perfectly well; it would not survive being read out of a
44
+ * `Float64Array`, which is where the dials live so that no message is needed at all.
45
+ */
46
+ export const FRICTION_BOX = 0;
47
+ export const FRICTION_ELLIPTICAL = 1;
48
+ export function frictionDial(model) {
49
+ return model === 'elliptical' ? FRICTION_ELLIPTICAL : FRICTION_BOX;
50
+ }
51
+ export function frictionModelOf(dial) {
52
+ return dial === FRICTION_ELLIPTICAL ? 'elliptical' : 'box';
53
+ }
54
+ /** Ticks below the sleep thresholds before a body is willing to sleep. */
55
+ export const SLEEP_TICKS = 60;
56
+ /** Whether a body has been still long enough to stop being solved. */
57
+ export function bodyAsleep(bodies, allowSleep, i) {
58
+ return allowSleep && (bodies.sleepTicks[i] ?? 0) >= SLEEP_TICKS;
59
+ }
60
+ /** Whether every body in an island is asleep, which is what lets the island be skipped whole. */
61
+ export function islandAsleep(state, island) {
62
+ const allowSleep = (state.dials[DIAL_ALLOW_SLEEP] ?? 0) !== 0;
63
+ const from = state.islands.bodyStart[island] ?? 0;
64
+ const to = state.islands.bodyStart[island + 1] ?? from;
65
+ for (let at = from; at < to; at++) {
66
+ if (!bodyAsleep(state.bodies, allowSleep, state.islands.bodyOrder[at] ?? 0))
67
+ return false;
68
+ }
69
+ return to > from;
70
+ }
71
+ function integrateVelocities(state, h, from, to) {
72
+ const bodies = state.bodies;
73
+ const d = state.dials;
74
+ const allowSleep = (d[DIAL_ALLOW_SLEEP] ?? 0) !== 0;
75
+ const gx = d[DIAL_GRAVITY_X] ?? 0;
76
+ const gy = d[DIAL_GRAVITY_Y] ?? 0;
77
+ const gz = d[DIAL_GRAVITY_Z] ?? 0;
78
+ const ld = 1 / (1 + h * (d[DIAL_LINEAR_DAMPING] ?? 0));
79
+ const ad = 1 / (1 + h * (d[DIAL_ANGULAR_DAMPING] ?? 0));
80
+ for (let at = from; at < to; at++) {
81
+ const i = state.islands.bodyOrder[at] ?? 0;
82
+ if (bodies.type[i] !== BODY_DYNAMIC || bodyAsleep(bodies, allowSleep, i))
83
+ continue;
84
+ bodies.velX[i] = ((bodies.velX[i] ?? 0) + h * gx) * ld;
85
+ bodies.velY[i] = ((bodies.velY[i] ?? 0) + h * gy) * ld;
86
+ bodies.velZ[i] = ((bodies.velZ[i] ?? 0) + h * gz) * ld;
87
+ bodies.angX[i] = (bodies.angX[i] ?? 0) * ad;
88
+ bodies.angY[i] = (bodies.angY[i] ?? 0) * ad;
89
+ bodies.angZ[i] = (bodies.angZ[i] ?? 0) * ad;
90
+ }
91
+ }
92
+ function integratePositions(state, h, from, to) {
93
+ const bodies = state.bodies;
94
+ const allowSleep = (state.dials[DIAL_ALLOW_SLEEP] ?? 0) !== 0;
95
+ for (let at = from; at < to; at++) {
96
+ const i = state.islands.bodyOrder[at] ?? 0;
97
+ if (bodies.type[i] === BODY_STATIC || bodyAsleep(bodies, allowSleep, i))
98
+ continue;
99
+ bodies.posX[i] = (bodies.posX[i] ?? 0) + h * (bodies.velX[i] ?? 0);
100
+ bodies.posY[i] = (bodies.posY[i] ?? 0) + h * (bodies.velY[i] ?? 0);
101
+ bodies.posZ[i] = (bodies.posZ[i] ?? 0) + h * (bodies.velZ[i] ?? 0);
102
+ // First-order exponential map, then renormalise. No sin, no cos.
103
+ const qx = bodies.rotX[i] ?? 0;
104
+ const qy = bodies.rotY[i] ?? 0;
105
+ const qz = bodies.rotZ[i] ?? 0;
106
+ const qw = bodies.rotW[i] ?? 1;
107
+ const wx = bodies.angX[i] ?? 0;
108
+ const wy = bodies.angY[i] ?? 0;
109
+ const wz = bodies.angZ[i] ?? 0;
110
+ const half = 0.5 * h;
111
+ let nx = qx + half * (wx * qw + wy * qz - wz * qy);
112
+ let ny = qy + half * (wy * qw + wz * qx - wx * qz);
113
+ let nz = qz + half * (wz * qw + wx * qy - wy * qx);
114
+ let nw = qw - half * (wx * qx + wy * qy + wz * qz);
115
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz + nw * nw) || 1;
116
+ nx /= len;
117
+ ny /= len;
118
+ nz /= len;
119
+ nw /= len;
120
+ bodies.rotX[i] = nx;
121
+ bodies.rotY[i] = ny;
122
+ bodies.rotZ[i] = nz;
123
+ bodies.rotW[i] = nw;
124
+ }
125
+ }
126
+ /**
127
+ * Advance one island through every substep.
128
+ *
129
+ * Integration is over this island's bodies only, which is what makes the call independent of every
130
+ * other island rather than merely ordered against them.
131
+ */
132
+ export function solveIslandInto(state, island) {
133
+ if (islandAsleep(state, island))
134
+ return;
135
+ const d = state.dials;
136
+ const substeps = d[DIAL_SUBSTEPS] ?? 1;
137
+ const iterations = d[DIAL_ITERATIONS] ?? 1;
138
+ const dt = d[DIAL_DT] ?? 0;
139
+ const contactHertz = d[DIAL_CONTACT_HERTZ] ?? 0;
140
+ const contactDamping = d[DIAL_CONTACT_DAMPING] ?? 0;
141
+ const jointHertz = d[DIAL_JOINT_HERTZ] ?? 0;
142
+ const jointDamping = d[DIAL_JOINT_DAMPING] ?? 0;
143
+ const friction = frictionModelOf(d[DIAL_FRICTION_MODEL] ?? 0);
144
+ const h = dt / substeps;
145
+ const bodies = state.bodies;
146
+ const joints = state.joints;
147
+ const c = state.constraints;
148
+ const order = state.islands.constraintOrder;
149
+ const from = state.islands.constraintStart[island] ?? 0;
150
+ const to = state.islands.constraintStart[island + 1] ?? from;
151
+ const bodyFrom = state.islands.bodyStart[island] ?? 0;
152
+ const bodyTo = state.islands.bodyStart[island + 1] ?? bodyFrom;
153
+ const jointOrder = state.islands.jointOrder;
154
+ const jointFrom = state.islands.jointStart[island] ?? 0;
155
+ const jointTo = state.islands.jointStart[island + 1] ?? jointFrom;
156
+ for (let s = 0; s < substeps; s++) {
157
+ integrateVelocities(state, h, bodyFrom, bodyTo);
158
+ warmStart(bodies, c, order, from, to);
159
+ for (let it = 0; it < iterations; it++) {
160
+ // Joints before contacts: a joint is structural and a contact is transient, so letting a
161
+ // contact win produces a limb that pulls out of its socket when something lands on it.
162
+ solveJoints(bodies, joints, jointOrder, jointFrom, jointTo, h, dt, true, jointHertz, jointDamping);
163
+ solveContacts(bodies, c, order, from, to, h, true, contactHertz, contactDamping, friction);
164
+ }
165
+ integratePositions(state, h, bodyFrom, bodyTo);
166
+ // The relax pass, with the bias off, giving back the energy the bias added.
167
+ solveJoints(bodies, joints, jointOrder, jointFrom, jointTo, h, dt, false, jointHertz, jointDamping);
168
+ solveContacts(bodies, c, order, from, to, h, false, contactHertz, contactDamping, friction);
169
+ }
170
+ applyRestitution(bodies, c, order, from, to);
171
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The worker end of the island pool.
3
+ *
4
+ * **It contains no physics.** It adopts views over the staging buffer, claims islands from a shared
5
+ * cursor, and calls `solveIslandInto` — the same function `PhysicsWorld.solveIsland` calls. A
6
+ * substep loop appearing in this file would be the defect the whole arrangement is shaped to
7
+ * prevent, because a second copy of the solve order drifts from the first the first time either is
8
+ * touched and nothing fails until a consumer's replay does.
9
+ *
10
+ * **It parks rather than polls.** `Atomics.wait` is permitted off a browser's main thread, so an
11
+ * idle worker costs nothing between ticks and wakes on an epoch bump with no message sent.
12
+ *
13
+ * **Two hosts, one file.** A browser module worker delivers messages through `self.onmessage`;
14
+ * `node:worker_threads` delivers them through `parentPort`. The difference is eight lines at the
15
+ * bottom, and paying them is what lets the gate spawn this exact entry point in Node instead of a
16
+ * stand-in that would prove nothing about threads.
17
+ */
18
+ export {};