@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,202 @@
1
+ import type { ConvexShape } from '../shape.ts';
2
+ /**
3
+ * A body: what it is shaped like, what bounds it presents, and the frame it stands in.
4
+ *
5
+ * Separate from the sweep because these describe a body at rest. `bodyFrame` is here rather
6
+ * than with the sweep, despite being sweep machinery in spirit, because `bodyBounds` calls
7
+ * it — putting it there would make the two files import each other.
8
+ */
9
+ /** Moving body: center position + half extents. Mutated in place by moveAxis. */
10
+ export interface Body {
11
+ x: number;
12
+ y: number;
13
+ z: number;
14
+ hx: number;
15
+ hy: number;
16
+ hz: number;
17
+ /**
18
+ * Which way is *up* for this body, as a unit vector. Omitted means world up.
19
+ *
20
+ * A body is a box, and a box has an orientation whether or not anything models one.
21
+ * Nothing here did: every volume in this module was world-axis, so a body tilted to
22
+ * lie along a banked deck — which is how a route is drawn and how a character is drawn
23
+ * standing on it — collided as though it were still standing bolt upright. The
24
+ * drawn body then reaches outside its own collision volume by roughly
25
+ * `hy * sin(tilt)`, which on the 41-degree deck where every measured failure sits is
26
+ * about half a metre of body with nothing solid around it: it passes into geometry
27
+ * that looks solid, and geometry that looks clear stops it.
28
+ *
29
+ * Only the up axis, deliberately. It is what a surface has to give (`SurfaceHit`
30
+ * carries a normal, not a frame) and what a body resting on one needs; roll about
31
+ * that axis cannot change a box's world-axis bounds by more than its own
32
+ * cross-section, and inventing a full frame here would mean inventing a heading for
33
+ * bodies that have none. `bodyBounds` treats the remaining freedom conservatively.
34
+ *
35
+ * Two invariants for anything that sets it: it must be **unit length**, because
36
+ * `bodyBounds` trusts that rather than paying a square root per query in the tick;
37
+ * and it must be *set*, not accumulated, so a fixed-timestep replay cannot drift.
38
+ */
39
+ upX?: number;
40
+ upY?: number;
41
+ upZ?: number;
42
+ /**
43
+ * Which way this body *faces*, as a hint. Omitted means +Z, and it is only a hint:
44
+ * `bodyFrame` orthogonalises it against `up`, so a caller with a yaw and a surface
45
+ * normal does not have to reconcile them itself.
46
+ *
47
+ * Matters only for a body whose cross-section is not square — a box that is wider than
48
+ * it is deep collides differently depending on which way it is turned, and that is
49
+ * exactly the "crossing objects with different orientation" case.
50
+ */
51
+ fwdX?: number;
52
+ fwdY?: number;
53
+ fwdZ?: number;
54
+ /**
55
+ * The volume this body really is, in its own frame. Unset means the
56
+ * `hx/hy/hz` box, which is also what the shape's absence has always meant.
57
+ */
58
+ shape?: ConvexShape;
59
+ /**
60
+ * A body made of parts — a character is a torso, a head, limbs and skates, and
61
+ * with parts set it collides as exactly those, wherever its frame tilts
62
+ * them. Takes precedence over `shape`. Set, not accumulated, like `up`.
63
+ */
64
+ parts?: readonly BodyPart[];
65
+ /**
66
+ * Where the frame rotates, as an offset from the centre along local Y.
67
+ * Unset means the centre; a standing character wants its *feet* (`-hy`),
68
+ * because that is where the drawn body pivots when it leans onto a bank —
69
+ * a centre-pivoted tilt swings the feet through the floor and buries a
70
+ * ground-snapped body up to 0.4 m into the very deck it stands on, while a
71
+ * feet-pivoted one keeps them planted and swings the head, exactly like the
72
+ * avatar it collides for. Upright bodies are unaffected bit for bit.
73
+ */
74
+ pivotY?: number;
75
+ /**
76
+ * Whether the contact that stopped the last blocked *downward* `moveAxis`
77
+ * was support — the body arriving on top of a face whose plane is
78
+ * walkably horizontal — as opposed to a graze against a side, an edge or an
79
+ * underside. Written by `moveAxis` on downward Y moves only; `true` when the
80
+ * move was not blocked at all.
81
+ *
82
+ * Exists because "the fall was stopped" and "the body is standing on
83
+ * something" are different facts, and every rest rule that conflated them
84
+ * let a body hang in mid-air by a skate corner kissing a slab's flank —
85
+ * measured live as a character standing over the void beside a drawn pad,
86
+ * half a metre below its top. A caller that never reads it loses nothing:
87
+ * the resolution itself is unchanged, bit for bit.
88
+ */
89
+ contactSupport?: boolean;
90
+ /**
91
+ * The way **off** a face this body is perched on the rim of, as a horizontal unit vector.
92
+ * Zero when the last downward move was supported, was not blocked, or was refused for any
93
+ * other reason.
94
+ *
95
+ * **A rim perch's contact normal points straight up and that is not a mistake** — up is
96
+ * genuinely the way out of the clamp. It is also of no use to a caller trying to get a body
97
+ * off something it has been told it is not standing on: the only direction that helps is
98
+ * sideways, and which sideways is a fact about the obstacle's footprint that nothing outside
99
+ * the sweep can see.
100
+ *
101
+ * Without it a body could be held in clear air indefinitely. `contactSupport` correctly
102
+ * refuses a foot's corner on a nine-centimetre sliver of a slab, so nothing grounds; the
103
+ * blocked fall re-clamps the vertical speed every tick, so gravity never accumulates; and a
104
+ * consumer sliding along the contact normal finds no horizontal component to slide along.
105
+ * Measured on a courtyard pillar: five seconds of held input, zero metres of descent, and the
106
+ * body still exactly where it started.
107
+ */
108
+ contactRimX?: number;
109
+ contactRimZ?: number;
110
+ /**
111
+ * Which way is *out* of whatever clamped the last blocked `moveAxis` — a unit
112
+ * vector pointing from the obstacle toward the body.
113
+ *
114
+ * Only meaningful on a move that was actually blocked (compare the returned
115
+ * distance with the requested one); left as it was otherwise, so a caller reading
116
+ * it after an unobstructed move is reading history.
117
+ *
118
+ * The sweep has always known this and always discarded it. `foldAxis` picks the
119
+ * separating axis that decides the contact and already judges *which side of it*
120
+ * the body is on — that is exactly `contactSupport`, reduced to a boolean about
121
+ * one special case. Keeping the direction instead of the boolean is what lets a
122
+ * caller **slide along** a face rather than stop dead against it, and that
123
+ * distinction turns out to be the difference between a wall and a trap: a body
124
+ * whose every axis is refused has nowhere to go, and axis-separated movement
125
+ * refuses both axes at any corner. Measured on two separate reports as a character
126
+ * held motionless and unable to jump for as long as anyone waited.
127
+ *
128
+ * Written, not accumulated, exactly like `up` — a fixed-timestep replay cannot
129
+ * afford a field that drifts.
130
+ */
131
+ contactNormalX?: number;
132
+ contactNormalY?: number;
133
+ contactNormalZ?: number;
134
+ }
135
+ /** One piece of a compound body, offset from the body's centre in its own frame. */
136
+ export interface BodyPart {
137
+ shape: ConvexShape;
138
+ ox: number;
139
+ oy: number;
140
+ oz: number;
141
+ }
142
+ /**
143
+ * World-axis half-extents of a body, accounting for the tilt of its own up axis.
144
+ *
145
+ * The whole point of the exercise: a tilted box needs a bigger axis-aligned box to hold
146
+ * it, and every query in this module is axis-aligned. For an upright body this returns
147
+ * exactly `hx, hy, hz`, so nothing that has not asked for an orientation changes by a
148
+ * floating-point bit.
149
+ *
150
+ * Conservative about the one freedom `Body` does not model. With only an up axis given,
151
+ * the body may be rolled arbitrarily about it, so the two cross-axes are bounded by the
152
+ * larger of the two half-widths — a box is at worst its own diagonal wide, and being
153
+ * generous here costs a little forgiveness at a corner while being mean would cost the
154
+ * penetration this exists to stop.
155
+ *
156
+ * Allocation-free by contract: writes into `out` and returns it (the tick calls this and
157
+ * the tick may not allocate).
158
+ */
159
+ export interface BodyBounds {
160
+ hx: number;
161
+ hy: number;
162
+ hz: number;
163
+ }
164
+ export declare function createBodyBounds(): BodyBounds;
165
+ export declare function bodyBounds(body: Body, out: BodyBounds): BodyBounds;
166
+ export declare const AXIS_X = 0;
167
+ export declare const AXIS_Y = 1;
168
+ export declare const AXIS_Z = 2;
169
+ export type Axis = typeof AXIS_X | typeof AXIS_Y | typeof AXIS_Z;
170
+ /**
171
+ * A body's orientation as three orthonormal axes, written out so the tick allocates nothing.
172
+ *
173
+ * Built from the body's `up` and, where it has one, its `forward`. Roll about `up` is what
174
+ * `forward` supplies; without it any perpendicular will do, because a box whose cross-section
175
+ * is square (`hx === hz`) is unchanged by roll and one that is not has no heading to honour.
176
+ */
177
+ export interface BodyFrame {
178
+ /** Across the body, to its right. */
179
+ rx: number;
180
+ ry: number;
181
+ rz: number;
182
+ /** The body's own up. */
183
+ ux: number;
184
+ uy: number;
185
+ uz: number;
186
+ /** The body's own forward. */
187
+ fx: number;
188
+ fy: number;
189
+ fz: number;
190
+ /** False where the frame is the world's, so callers can take the cheap path. */
191
+ tilted: boolean;
192
+ }
193
+ export declare function createBodyFrame(): BodyFrame;
194
+ /**
195
+ * Resolve a body's orientation into an orthonormal frame.
196
+ *
197
+ * Gram-Schmidt against `up`, so `forward` is a *hint* about heading rather than a constraint
198
+ * that has to arrive already perpendicular — a caller with a yaw and a surface normal has no
199
+ * cheap way to make those two agree, and demanding it would push the arithmetic into every
200
+ * call site.
201
+ */
202
+ export declare function bodyFrame(body: Body, out: BodyFrame): BodyFrame;
@@ -0,0 +1,142 @@
1
+ export function createBodyBounds() {
2
+ return { hx: 0, hy: 0, hz: 0 };
3
+ }
4
+ export function bodyBounds(body, out) {
5
+ /*
6
+ * A shaped body is bounded by its enclosing radius, whichever way it turns.
7
+ * Broad phase only — over-asking costs a candidate test, under-asking costs
8
+ * a body through a wall — and it saves re-deriving oriented extents for a
9
+ * volume the narrow phase is about to resolve exactly anyway.
10
+ */
11
+ if (body.parts !== undefined) {
12
+ /*
13
+ * The union of each part's ball, placed where the frame and pivot really
14
+ * put it. A single enclosing sphere was measured 2.5 m across for a
15
+ * feet-pivoted character — every query dragged in half the junction — while
16
+ * this stays within centimetres of the true extents.
17
+ */
18
+ const f = bodyFrame(body, boundsFrame);
19
+ const pivotY = body.pivotY ?? 0;
20
+ out.hx = 0;
21
+ out.hy = 0;
22
+ out.hz = 0;
23
+ for (let i = 0; i < body.parts.length; i++) {
24
+ const part = body.parts[i];
25
+ if (part === undefined)
26
+ continue;
27
+ const oy = part.oy - pivotY;
28
+ const cx = f.rx * part.ox + f.ux * oy + f.fx * part.oz;
29
+ const cy = pivotY + f.ry * part.ox + f.uy * oy + f.fy * part.oz;
30
+ const cz = f.rz * part.ox + f.uz * oy + f.fz * part.oz;
31
+ const r = part.shape.boundRadius;
32
+ if (Math.abs(cx) + r > out.hx)
33
+ out.hx = Math.abs(cx) + r;
34
+ if (Math.abs(cy) + r > out.hy)
35
+ out.hy = Math.abs(cy) + r;
36
+ if (Math.abs(cz) + r > out.hz)
37
+ out.hz = Math.abs(cz) + r;
38
+ }
39
+ return out;
40
+ }
41
+ if (body.shape !== undefined) {
42
+ const radius = body.shape.boundRadius + Math.abs(body.pivotY ?? 0) * 2;
43
+ out.hx = radius;
44
+ out.hy = radius;
45
+ out.hz = radius;
46
+ return out;
47
+ }
48
+ const ux = body.upX ?? 0;
49
+ const uy = body.upY ?? 1;
50
+ const uz = body.upZ ?? 0;
51
+ // Upright, which is almost every body almost always: the exact old numbers, no work.
52
+ if (ux === 0 && uz === 0 && uy > 0) {
53
+ out.hx = body.hx;
54
+ out.hy = body.hy;
55
+ out.hz = body.hz;
56
+ return out;
57
+ }
58
+ /*
59
+ * The up axis contributes `hy * |component|` to each world axis. The two cross-axes
60
+ * span the plane perpendicular to it, and their contribution to a world axis is the
61
+ * length of that axis's projection onto that plane — `sqrt(1 - component^2)` — times
62
+ * the wider of the two half-widths, since roll about `up` is unmodelled and may point
63
+ * either of them anywhere in the plane.
64
+ */
65
+ const across = Math.max(body.hx, body.hz);
66
+ out.hx = Math.abs(ux) * body.hy + Math.sqrt(Math.max(0, 1 - ux * ux)) * across;
67
+ out.hy = Math.abs(uy) * body.hy + Math.sqrt(Math.max(0, 1 - uy * uy)) * across;
68
+ out.hz = Math.abs(uz) * body.hy + Math.sqrt(Math.max(0, 1 - uz * uz)) * across;
69
+ return out;
70
+ }
71
+ export const AXIS_X = 0;
72
+ export const AXIS_Y = 1;
73
+ export const AXIS_Z = 2;
74
+ export function createBodyFrame() {
75
+ return { rx: 1, ry: 0, rz: 0, ux: 0, uy: 1, uz: 0, fx: 0, fy: 0, fz: 1, tilted: false };
76
+ }
77
+ /**
78
+ * Resolve a body's orientation into an orthonormal frame.
79
+ *
80
+ * Gram-Schmidt against `up`, so `forward` is a *hint* about heading rather than a constraint
81
+ * that has to arrive already perpendicular — a caller with a yaw and a surface normal has no
82
+ * cheap way to make those two agree, and demanding it would push the arithmetic into every
83
+ * call site.
84
+ */
85
+ export function bodyFrame(body, out) {
86
+ const ux = body.upX ?? 0;
87
+ const uy = body.upY ?? 1;
88
+ const uz = body.upZ ?? 0;
89
+ if (ux === 0 && uz === 0 && uy > 0 && body.fwdX === undefined) {
90
+ out.rx = 1;
91
+ out.ry = 0;
92
+ out.rz = 0;
93
+ out.ux = 0;
94
+ out.uy = 1;
95
+ out.uz = 0;
96
+ out.fx = 0;
97
+ out.fy = 0;
98
+ out.fz = 1;
99
+ out.tilted = false;
100
+ return out;
101
+ }
102
+ const ulen = Math.sqrt(ux * ux + uy * uy + uz * uz) || 1;
103
+ out.ux = ux / ulen;
104
+ out.uy = uy / ulen;
105
+ out.uz = uz / ulen;
106
+ // A heading hint, or any direction not parallel to up.
107
+ let fx = body.fwdX ?? 0;
108
+ let fy = body.fwdY ?? 0;
109
+ let fz = body.fwdZ ?? 1;
110
+ // Remove the part along up, which is what makes the result orthonormal.
111
+ const along = fx * out.ux + fy * out.uy + fz * out.uz;
112
+ fx -= out.ux * along;
113
+ fy -= out.uy * along;
114
+ fz -= out.uz * along;
115
+ let flen = Math.sqrt(fx * fx + fy * fy + fz * fz);
116
+ if (flen < 1e-6) {
117
+ // The hint was parallel to up: any perpendicular will do, so take the world axis
118
+ // that up leans on least.
119
+ if (Math.abs(out.ux) < 0.9) {
120
+ fx = 1 - out.ux * out.ux;
121
+ fy = -out.uy * out.ux;
122
+ fz = -out.uz * out.ux;
123
+ }
124
+ else {
125
+ fx = -out.ux * out.uz;
126
+ fy = -out.uy * out.uz;
127
+ fz = 1 - out.uz * out.uz;
128
+ }
129
+ flen = Math.sqrt(fx * fx + fy * fy + fz * fz) || 1;
130
+ }
131
+ out.fx = fx / flen;
132
+ out.fy = fy / flen;
133
+ out.fz = fz / flen;
134
+ // right = forward x up, so the three form a consistent hand.
135
+ out.rx = out.fy * out.uz - out.fz * out.uy;
136
+ out.ry = out.fz * out.ux - out.fx * out.uz;
137
+ out.rz = out.fx * out.uy - out.fy * out.ux;
138
+ out.tilted = true;
139
+ return out;
140
+ }
141
+ /** Scratch for `bodyBounds`; the tick may not allocate. */
142
+ const boundsFrame = createBodyFrame();
@@ -0,0 +1,39 @@
1
+ import type { Aabb } from './aabb.ts';
2
+ import type { ColliderSet } from '../colliderSet.ts';
3
+ import type { Body } from './body.ts';
4
+ /**
5
+ * Push a body out of solid geometry it has ended up **inside**.
6
+ *
7
+ * **This is not collision, and that distinction is the whole of why it exists.** `moveAxis`
8
+ * resolves a move against what is in the way, and it is a *swept* test: it asks how far a body
9
+ * may travel before it touches something. A body that is already interpenetrating has nothing
10
+ * left to sweep against — both of the plain path's clamps test a face the body is already past,
11
+ * so neither fires, and an overlapping body moves **freely**. That is the mechanism behind the
12
+ * complaint that a character "sometimes passes through" a surface: one frame inside a column and
13
+ * the column has stopped existing for as long as the body remains in it.
14
+ *
15
+ * It is also why such a body can stand in mid-air inside a slab and be told it is grounded:
16
+ * nothing clamped the downward move, so `contactSupport` keeps the `true` it starts with, and
17
+ * every backstop a consumer has — a wedge counter, a refused-move flag — is waiting for the
18
+ * world to push back, which it has stopped doing.
19
+ *
20
+ * **Gated on the body's centre, not on overlap.** Overlap on its own is ordinary and expected:
21
+ * a deck's hull sits a give below the surface a character rides, so a grounded body's feet are
22
+ * inside it by design, and a pad's box is sunk under the pad. What none of those produce is a
23
+ * *centre* inside the box — a standing body's centre is half its height above its feet. So the
24
+ * centre test separates riding from burial exactly, and this can be run every tick without ever
25
+ * firing on a body that is merely resting on something.
26
+ *
27
+ * **Hulls are skipped, deliberately.** A hull collider's box is its broad phase and its real
28
+ * volume is the convex shape inside it, so a centre inside the box says nothing about whether
29
+ * the body is inside the *hull*. Pushing on that evidence would move bodies that are simply
30
+ * near a deck. What this covers is every plain collider — the columns, the props, the pads,
31
+ * the arch feet — which is where the reported pass-throughs were captured.
32
+ *
33
+ * The push is the shortest way out and it is applied whole: half a resolution leaves the body
34
+ * inside, which is the state this exists to end. Returns true when something moved, so a caller
35
+ * can treat it as the event it is rather than polling.
36
+ *
37
+ * Allocation-free: the caller owns `scratch` and `probe`, as everywhere else in this module.
38
+ */
39
+ export declare function ejectFromSolid(body: Body, colliders: ColliderSet, scratch: Int32Array, probe: Aabb): boolean;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Push a body out of solid geometry it has ended up **inside**.
3
+ *
4
+ * **This is not collision, and that distinction is the whole of why it exists.** `moveAxis`
5
+ * resolves a move against what is in the way, and it is a *swept* test: it asks how far a body
6
+ * may travel before it touches something. A body that is already interpenetrating has nothing
7
+ * left to sweep against — both of the plain path's clamps test a face the body is already past,
8
+ * so neither fires, and an overlapping body moves **freely**. That is the mechanism behind the
9
+ * complaint that a character "sometimes passes through" a surface: one frame inside a column and
10
+ * the column has stopped existing for as long as the body remains in it.
11
+ *
12
+ * It is also why such a body can stand in mid-air inside a slab and be told it is grounded:
13
+ * nothing clamped the downward move, so `contactSupport` keeps the `true` it starts with, and
14
+ * every backstop a consumer has — a wedge counter, a refused-move flag — is waiting for the
15
+ * world to push back, which it has stopped doing.
16
+ *
17
+ * **Gated on the body's centre, not on overlap.** Overlap on its own is ordinary and expected:
18
+ * a deck's hull sits a give below the surface a character rides, so a grounded body's feet are
19
+ * inside it by design, and a pad's box is sunk under the pad. What none of those produce is a
20
+ * *centre* inside the box — a standing body's centre is half its height above its feet. So the
21
+ * centre test separates riding from burial exactly, and this can be run every tick without ever
22
+ * firing on a body that is merely resting on something.
23
+ *
24
+ * **Hulls are skipped, deliberately.** A hull collider's box is its broad phase and its real
25
+ * volume is the convex shape inside it, so a centre inside the box says nothing about whether
26
+ * the body is inside the *hull*. Pushing on that evidence would move bodies that are simply
27
+ * near a deck. What this covers is every plain collider — the columns, the props, the pads,
28
+ * the arch feet — which is where the reported pass-throughs were captured.
29
+ *
30
+ * The push is the shortest way out and it is applied whole: half a resolution leaves the body
31
+ * inside, which is the state this exists to end. Returns true when something moved, so a caller
32
+ * can treat it as the event it is rather than polling.
33
+ *
34
+ * Allocation-free: the caller owns `scratch` and `probe`, as everywhere else in this module.
35
+ */
36
+ export function ejectFromSolid(body, colliders, scratch, probe) {
37
+ const found = colliders.query(body.x - body.hx, body.y - body.hy, body.z - body.hz, body.x + body.hx, body.y + body.hy, body.z + body.hz, scratch);
38
+ /*
39
+ * How far each way out has to go to clear **everything** at once, which is the whole reason
40
+ * this is two passes rather than a push per collider.
41
+ *
42
+ * Resolved one at a time it oscillates, and the case that shows it is the one this was
43
+ * measured on: a body 0.70 m wide between two columns 0.41 m apart overlaps both, and each
44
+ * one's own shortest exit is sideways into the other. It would be shoved back and forth for
45
+ * ever and never leave. Asking every candidate direction how far it would have to go to clear
46
+ * the lot, and then taking the cheapest, picks the way the body came in — the only way out
47
+ * there actually is.
48
+ */
49
+ let outXPlus = 0;
50
+ let outXMinus = 0;
51
+ let outZPlus = 0;
52
+ let outZMinus = 0;
53
+ let outYPlus = 0;
54
+ let buried = false;
55
+ for (let k = 0; k < found; k++) {
56
+ const index = scratch[k];
57
+ /* A hull's box is broad phase only. See the note above. */
58
+ if (colliders.shapeAt(index) !== undefined)
59
+ continue;
60
+ colliders.bounds(index, probe);
61
+ /*
62
+ * Vertically inside, and horizontally overlapping. The vertical half is what separates
63
+ * burial from the give every deck and pad is authored with: a collider is sunk under the
64
+ * surface it draws, so a character standing on one has its feet in the box by design and its
65
+ * centre half a body above the box's lid. A centre *between* the lid and the floor is a
66
+ * body in the solid rather than on it.
67
+ */
68
+ if (body.y <= probe.minY || body.y >= probe.maxY)
69
+ continue;
70
+ if (body.x + body.hx <= probe.minX || body.x - body.hx >= probe.maxX)
71
+ continue;
72
+ if (body.z + body.hz <= probe.minZ || body.z - body.hz >= probe.maxZ)
73
+ continue;
74
+ /*
75
+ * **Overlapping is not the same as being in it, and only the second arms this.**
76
+ *
77
+ * A skate corner clips the flank of a slab in passing, and a body falling past a ledge
78
+ * shares a couple of centimetres with it for a tick. Ejecting on that fights gravity: the
79
+ * upward preference below lifts the body back over whatever it was falling past, which is a
80
+ * hover rather than a rescue. Measured on the rim capture as 1.77 s of it, against 0.20 s
81
+ * with this gate in place.
82
+ *
83
+ * So a collider has to hold more than a quarter of the body's width on **both** horizontal
84
+ * axes before it counts as one the body is inside. Expressed against the body's own extents
85
+ * rather than in metres, because what "barely touching" means is a fact about the body.
86
+ */
87
+ const heldX = Math.min(body.x + body.hx, probe.maxX) - Math.max(body.x - body.hx, probe.minX);
88
+ const heldZ = Math.min(body.z + body.hz, probe.maxZ) - Math.max(body.z - body.hz, probe.minZ);
89
+ if (heldX > body.hx * 0.5 && heldZ > body.hz * 0.5)
90
+ buried = true;
91
+ outXPlus = Math.max(outXPlus, probe.maxX - (body.x - body.hx));
92
+ outXMinus = Math.max(outXMinus, body.x + body.hx - probe.minX);
93
+ outZPlus = Math.max(outZPlus, probe.maxZ - (body.z - body.hz));
94
+ outZMinus = Math.max(outZMinus, body.z + body.hz - probe.minZ);
95
+ outYPlus = Math.max(outYPlus, probe.maxY - (body.y - body.hy));
96
+ }
97
+ if (!buried)
98
+ return false;
99
+ /*
100
+ * Up wins a tie and wins a near-tie, which is a decision about characters rather than about
101
+ * geometry: a body pushed sideways out of a column arrives beside it in mid-air, and a body
102
+ * pushed up arrives on top of it. Only when up is genuinely the long way round does a
103
+ * sideways exit win.
104
+ */
105
+ let bestAxis = 1;
106
+ let bestPush = outYPlus / UP_PREFERENCE;
107
+ if (outXPlus < bestPush) {
108
+ bestPush = outXPlus;
109
+ bestAxis = 2;
110
+ }
111
+ if (outXMinus < bestPush) {
112
+ bestPush = outXMinus;
113
+ bestAxis = 3;
114
+ }
115
+ if (outZPlus < bestPush) {
116
+ bestPush = outZPlus;
117
+ bestAxis = 4;
118
+ }
119
+ if (outZMinus < bestPush) {
120
+ bestPush = outZMinus;
121
+ bestAxis = 5;
122
+ }
123
+ if (bestAxis === 1)
124
+ body.y += outYPlus;
125
+ else if (bestAxis === 2)
126
+ body.x += outXPlus;
127
+ else if (bestAxis === 3)
128
+ body.x -= outXMinus;
129
+ else if (bestAxis === 4)
130
+ body.z += outZPlus;
131
+ else
132
+ body.z -= outZMinus;
133
+ return true;
134
+ }
135
+ /**
136
+ * How much longer the vertical exit may be than the shortest horizontal one and still be taken.
137
+ *
138
+ * A body ejected sideways out of a column lands beside it with nothing under it, which reads as
139
+ * being spat into the void; ejected upward it lands on top, which reads as climbing out. Worth
140
+ * up to twice the distance to get the second.
141
+ */
142
+ const UP_PREFERENCE = 2;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Axis-separated collision, in four parts.
3
+ *
4
+ * Split from one 1,064-line file along the concerns its sixteen exports already described:
5
+ * an axis-aligned box and the arithmetic on it, a body and the bounds it presents, the sweep
6
+ * that resolves movement one axis at a time, and the segment test that answers what a ray
7
+ * hits.
8
+ *
9
+ * Everything the single file exported is re-exported here, so `from './collide'` resolves
10
+ * exactly as it did and no caller moved.
11
+ */
12
+ export type { Aabb } from './aabb.ts';
13
+ export { aabbFromCenter } from './aabb.ts';
14
+ export type { Axis, Body, BodyBounds, BodyFrame, BodyPart } from './body.ts';
15
+ export { AXIS_X, AXIS_Y, AXIS_Z, bodyBounds, bodyFrame, createBodyBounds, createBodyFrame, } from './body.ts';
16
+ export { ejectFromSolid } from './eject.ts';
17
+ export { moveAxis } from './sweep.ts';
18
+ export { segmentHit } from './segment.ts';
@@ -0,0 +1,5 @@
1
+ export { aabbFromCenter } from './aabb.js';
2
+ export { AXIS_X, AXIS_Y, AXIS_Z, bodyBounds, bodyFrame, createBodyBounds, createBodyFrame, } from './body.js';
3
+ export { ejectFromSolid } from './eject.js';
4
+ export { moveAxis } from './sweep.js';
5
+ export { segmentHit } from './segment.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The index buffer a `ColliderSet` query writes into.
3
+ *
4
+ * Shared by the sweep and the segment test rather than duplicated, because it was one buffer
5
+ * in the file these were split from and neither caller is re-entrant. Giving each its own
6
+ * would be a behaviour change smuggled into a move.
7
+ */
8
+ export declare const hits: Int32Array<ArrayBuffer>;
@@ -0,0 +1,9 @@
1
+ import { ColliderSet } from '../colliderSet.js';
2
+ /**
3
+ * The index buffer a `ColliderSet` query writes into.
4
+ *
5
+ * Shared by the sweep and the segment test rather than duplicated, because it was one buffer
6
+ * in the file these were split from and neither caller is re-entrant. Giving each its own
7
+ * would be a behaviour change smuggled into a move.
8
+ */
9
+ export const hits = new Int32Array(ColliderSet.MAX_HITS);
@@ -0,0 +1,12 @@
1
+ import { ColliderSet } from '../colliderSet.ts';
2
+ /**
3
+ * Nearest obstruction along the segment `origin → origin + delta`, returned as
4
+ * a fraction of the segment in [0, 1] — 1 meaning unobstructed.
5
+ *
6
+ * Boxes are expanded by `radius`, which approximates sweeping a sphere: the
7
+ * caller (the third-person camera boom) then stops clear of surfaces instead of
8
+ * grazing them. Allocation-free; slab method, one pass over the boxes.
9
+ *
10
+ * Render-side only. Nothing here may be called from `simulate()`.
11
+ */
12
+ export declare function segmentHit(ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, boxes: ColliderSet, radius: number): number;