@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- package/src/world.ts +961 -0
package/dist/shape.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/** What a shape with no faces carries: a sphere, a capsule, or any cloud too flat to bound one. */
|
|
2
|
+
const EMPTY_FACES = {
|
|
3
|
+
facePlanes: new Float32Array(0),
|
|
4
|
+
faceVertexStart: new Uint16Array(0),
|
|
5
|
+
faceVertexIndices: new Uint16Array(0),
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Hard cap on hull input size. The builder is O(n^4); at 64 points that is a
|
|
9
|
+
* few million build-time operations, and anything larger is a sign the caller
|
|
10
|
+
* is feeding it a render mesh instead of an element's defining points.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_POINTS = 64;
|
|
13
|
+
/** Points closer than this are the same point. */
|
|
14
|
+
const POINT_EPS = 1e-6;
|
|
15
|
+
/** Distance within which a point counts as lying on a plane, metres. */
|
|
16
|
+
const COPLANAR_M = 1e-4;
|
|
17
|
+
/** Dot product above which two directions are the same line. */
|
|
18
|
+
const DIR_DEDUP = 1 - 1e-7;
|
|
19
|
+
/** The exact shape `MeshBuilder.addBox` draws: eight corners, three axes. */
|
|
20
|
+
export function boxShape(hx, hy, hz) {
|
|
21
|
+
const vertices = new Float32Array(24);
|
|
22
|
+
let v = 0;
|
|
23
|
+
for (let i = 0; i < 8; i++) {
|
|
24
|
+
vertices[v++] = i & 1 ? hx : -hx;
|
|
25
|
+
vertices[v++] = i & 2 ? hy : -hy;
|
|
26
|
+
vertices[v++] = i & 4 ? hz : -hz;
|
|
27
|
+
}
|
|
28
|
+
const axes = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
29
|
+
/*
|
|
30
|
+
* Six faces, where `axes` above is three directions. Corner `i` has bit 0 for +x, bit 1 for +y
|
|
31
|
+
* and bit 2 for +z, so each face is the four corners sharing one bit, listed counter-clockwise
|
|
32
|
+
* seen from outside. Written out rather than derived because the loop that derives it is longer
|
|
33
|
+
* than the table and this is build-time code that runs once per shape.
|
|
34
|
+
*/
|
|
35
|
+
const facePlanes = new Float32Array([
|
|
36
|
+
1,
|
|
37
|
+
0,
|
|
38
|
+
0,
|
|
39
|
+
hx,
|
|
40
|
+
-1,
|
|
41
|
+
0,
|
|
42
|
+
0,
|
|
43
|
+
hx,
|
|
44
|
+
0,
|
|
45
|
+
1,
|
|
46
|
+
0,
|
|
47
|
+
hy,
|
|
48
|
+
0,
|
|
49
|
+
-1,
|
|
50
|
+
0,
|
|
51
|
+
hy,
|
|
52
|
+
0,
|
|
53
|
+
0,
|
|
54
|
+
1,
|
|
55
|
+
hz,
|
|
56
|
+
0,
|
|
57
|
+
0,
|
|
58
|
+
-1,
|
|
59
|
+
hz,
|
|
60
|
+
]);
|
|
61
|
+
const faceVertexIndices = new Uint16Array([
|
|
62
|
+
1, 3, 7, 5, 4, 6, 2, 0, 2, 6, 7, 3, 0, 1, 5, 4, 4, 5, 7, 6, 0, 2, 3, 1,
|
|
63
|
+
]);
|
|
64
|
+
const faceVertexStart = new Uint16Array([0, 4, 8, 12, 16, 20, 24]);
|
|
65
|
+
return {
|
|
66
|
+
vertices,
|
|
67
|
+
faceNormals: axes,
|
|
68
|
+
edgeDirs: axes,
|
|
69
|
+
radius: 0,
|
|
70
|
+
sideRadius: 0,
|
|
71
|
+
boundRadius: Math.sqrt(hx * hx + hy * hy + hz * hz),
|
|
72
|
+
facePlanes,
|
|
73
|
+
faceVertexStart,
|
|
74
|
+
faceVertexIndices,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** A ball: one point, all radius. It separates on other shapes' features. */
|
|
78
|
+
export function sphereShape(radius) {
|
|
79
|
+
return {
|
|
80
|
+
vertices: new Float32Array(3),
|
|
81
|
+
faceNormals: new Float32Array(0),
|
|
82
|
+
edgeDirs: new Float32Array(0),
|
|
83
|
+
radius,
|
|
84
|
+
sideRadius: 0,
|
|
85
|
+
boundRadius: radius,
|
|
86
|
+
...EMPTY_FACES,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A capsule: a segment along y with a rounding radius.
|
|
91
|
+
*
|
|
92
|
+
* **No new representation.** Point-cloud-plus-radius already expresses a capsule exactly, so this
|
|
93
|
+
* is a named constructor over `hullShape` rather than a shape type. A named thing is discoverable
|
|
94
|
+
* and an idiom is not. It stands along y because that is the axis a character capsule uses, and a
|
|
95
|
+
* rotation nobody has to apply is a rotation nobody gets wrong.
|
|
96
|
+
*
|
|
97
|
+
* At `halfHeight` zero the two points dedupe to one and the result *is* a sphere, which is the
|
|
98
|
+
* right degenerate case rather than an error.
|
|
99
|
+
*
|
|
100
|
+
* **A cylinder is a different shape and has its own constructor**, `cylinderShape` below. The
|
|
101
|
+
* rounding radius here grows uniformly around every feature, so applying it to a segment can only
|
|
102
|
+
* ever give a capsule: it would round a cylinder's rim as well as its side. `sideRadius` is the
|
|
103
|
+
* field that expresses the difference, and it was added on 2026-08-27 with the second condition
|
|
104
|
+
* this file had been recording — a wheel rim, a rolling barrel — as its reason.
|
|
105
|
+
*
|
|
106
|
+
* The advice until then was "build one as an n-gon prism with `hullShape` and accept the facets",
|
|
107
|
+
* and it had a measured ceiling: `hullShape` refuses more than 64 points, a prism spends two a
|
|
108
|
+
* side, so thirty-two sides was the wall, and a thirty-two-sided wheel of radius 1 rolling at
|
|
109
|
+
* 6 m/s bobbed 16 mm that no side count could reduce. `prismRoll.test.ts` still measures that,
|
|
110
|
+
* beside the same wheel built as a cylinder, because the comparison is the reason the shape kind
|
|
111
|
+
* exists.
|
|
112
|
+
*/
|
|
113
|
+
export function capsuleShape(radius, halfHeight) {
|
|
114
|
+
if (!(radius > 0))
|
|
115
|
+
throw new Error(`capsuleShape: radius must be positive, got ${radius}`);
|
|
116
|
+
if (!(halfHeight >= 0)) {
|
|
117
|
+
throw new Error(`capsuleShape: halfHeight must not be negative, got ${halfHeight}`);
|
|
118
|
+
}
|
|
119
|
+
return hullShape([0, -halfHeight, 0, 0, halfHeight, 0], radius);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A cylinder: a segment along y, grown by a disc rather than by a ball.
|
|
123
|
+
*
|
|
124
|
+
* **The one shape a point cloud plus a rounding radius cannot express**, and the reason it needed
|
|
125
|
+
* a second kind of rounding rather than a second shape type. `radius` grows a shape uniformly, so
|
|
126
|
+
* applying it to a segment gives a capsule; `sideRadius` grows it only perpendicular to that
|
|
127
|
+
* segment, which gives two flat cap discs, a curved side, and a sharp rim between them. That rim
|
|
128
|
+
* is the whole difference, and it is what a wheel rolls on.
|
|
129
|
+
*
|
|
130
|
+
* **What this replaces is an n-gon prism with a measured floor.** `prismRoll.test.ts` had the
|
|
131
|
+
* numbers: `hullShape` refuses more than 64 points, a prism spends two a side, so thirty-two sides
|
|
132
|
+
* was the wall, and a thirty-two-sided wheel of radius 1 rolling at 6 m/s bobbed **16 mm** — which
|
|
133
|
+
* no side count could reduce. A cylinder's support along any direction perpendicular to its axis
|
|
134
|
+
* is exactly `sideRadius`, at every angle, so the same wheel bobs by nothing at all. The same test
|
|
135
|
+
* file now measures both and compares them.
|
|
136
|
+
*
|
|
137
|
+
* It stands along y for `capsuleShape`'s reason: a rotation nobody has to apply is a rotation
|
|
138
|
+
* nobody gets wrong. A wheel wants its axis across the vehicle, and the body carries a quaternion
|
|
139
|
+
* for exactly that. A static collider baked into world space carries its axis in its own two
|
|
140
|
+
* points, so nothing stores a direction that could disagree with them.
|
|
141
|
+
*
|
|
142
|
+
* **`halfHeight` must be positive**, unlike `capsuleShape`, whose zero case degenerates into a
|
|
143
|
+
* sphere and is right. A cylinder of zero height is a disc: a shape with no interior, whose mass
|
|
144
|
+
* properties are zero and whose contact normal is undefined on its rim. Refused rather than
|
|
145
|
+
* silently produced.
|
|
146
|
+
*/
|
|
147
|
+
export function cylinderShape(radius, halfHeight) {
|
|
148
|
+
if (!(radius > 0))
|
|
149
|
+
throw new Error(`cylinderShape: radius must be positive, got ${radius}`);
|
|
150
|
+
if (!(halfHeight > 0)) {
|
|
151
|
+
throw new Error(`cylinderShape: halfHeight must be positive, got ${halfHeight}`);
|
|
152
|
+
}
|
|
153
|
+
/*
|
|
154
|
+
* One axis in `faceNormals` and in `edgeDirs`, which is the honest answer for both: the caps are
|
|
155
|
+
* the only planar faces a cylinder has, and they share one direction, while the axis is the only
|
|
156
|
+
* straight edge direction on it. The curved side has neither, and the contact code reaches it
|
|
157
|
+
* through `sideRadius` rather than through an enumerated axis — which is the point of the field.
|
|
158
|
+
*/
|
|
159
|
+
const axis = new Float32Array([0, 1, 0]);
|
|
160
|
+
return {
|
|
161
|
+
vertices: new Float32Array([0, -halfHeight, 0, 0, halfHeight, 0]),
|
|
162
|
+
faceNormals: axis,
|
|
163
|
+
edgeDirs: axis,
|
|
164
|
+
radius: 0,
|
|
165
|
+
sideRadius: radius,
|
|
166
|
+
boundRadius: Math.sqrt(halfHeight * halfHeight + radius * radius),
|
|
167
|
+
...EMPTY_FACES,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The convex hull of a point cloud, with its separating features enumerated.
|
|
172
|
+
*
|
|
173
|
+
* Brute force by design: every point triple proposes a plane, and a plane with
|
|
174
|
+
* every point on one side is a face. Edges are point pairs lying on two
|
|
175
|
+
* distinct faces. No incremental hull, no floating-point pivoting — the same
|
|
176
|
+
* input yields the same features in the same order on every machine, which the
|
|
177
|
+
* replay contract needs from anything that feeds collision.
|
|
178
|
+
*
|
|
179
|
+
* **Every length here is `Math.sqrt` of a sum of squares rather than `Math.hypot`**, and that is
|
|
180
|
+
* what makes the sentence above true rather than merely intended. ECMAScript declines to specify
|
|
181
|
+
* `hypot` precisely, so two engines may differ by an ulp — and an ulp here is not a slightly
|
|
182
|
+
* different axis. `proposePlane`'s normal classifies every point against `COPLANAR_M`, so it
|
|
183
|
+
* decides whether a plane becomes a face at all; `appendDirection`'s is compared against
|
|
184
|
+
* `DIR_DEDUP`, so it decides how many separating axes exist and in what order. SAT then takes a
|
|
185
|
+
* minimum over that list.
|
|
186
|
+
*
|
|
187
|
+
* `hypot` is the more accurate of the two, guarding against intermediate overflow at magnitudes a
|
|
188
|
+
* collision shape does not reach. **What would make this wrong** is a shape built at astronomical
|
|
189
|
+
* or atomic scale, where that guard is worth more than the reproducibility; the answer then is an
|
|
190
|
+
* explicit rescale rather than the call back. Asserted by `scripts/determinism.test.mjs`.
|
|
191
|
+
*/
|
|
192
|
+
export function hullShape(points, radius = 0) {
|
|
193
|
+
const pts = dedupePoints(points);
|
|
194
|
+
const n = pts.length / 3;
|
|
195
|
+
if (n === 0)
|
|
196
|
+
throw new Error('hullShape: no points');
|
|
197
|
+
if (n > MAX_POINTS) {
|
|
198
|
+
throw new Error(`hullShape: ${n} points exceeds ${MAX_POINTS} — derive shapes from defining points, not meshes`);
|
|
199
|
+
}
|
|
200
|
+
let boundRadius = 0;
|
|
201
|
+
for (let i = 0; i < n; i++) {
|
|
202
|
+
const px = pts[i * 3] ?? 0;
|
|
203
|
+
const py = pts[i * 3 + 1] ?? 0;
|
|
204
|
+
const pz = pts[i * 3 + 2] ?? 0;
|
|
205
|
+
const d = Math.sqrt(px * px + py * py + pz * pz);
|
|
206
|
+
if (d > boundRadius)
|
|
207
|
+
boundRadius = d;
|
|
208
|
+
}
|
|
209
|
+
boundRadius += radius;
|
|
210
|
+
// Every plane that supports the hull: (nx, ny, nz, offset), outward.
|
|
211
|
+
const planes = [];
|
|
212
|
+
for (let i = 0; i < n - 2; i++) {
|
|
213
|
+
for (let j = i + 1; j < n - 1; j++) {
|
|
214
|
+
for (let k = j + 1; k < n; k++) {
|
|
215
|
+
proposePlane(pts, n, i, j, k, planes);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const faceNormals = dedupeDirections(planes, 4);
|
|
220
|
+
const edgeDirs = planes.length >= 8
|
|
221
|
+
? hullEdges(pts, n, planes)
|
|
222
|
+
: // Flat or degenerate cloud (one plane or none): there are no two faces
|
|
223
|
+
// to intersect, so every pairwise direction stands in for the rim.
|
|
224
|
+
pairwiseDirections(pts, n);
|
|
225
|
+
/*
|
|
226
|
+
* The planes are what `proposePlane` already found; only their offsets were being discarded. A
|
|
227
|
+
* face is one plane plus the points lying on it, ordered around the normal — which is what
|
|
228
|
+
* clipping and an inertia tensor both need, and what `faceNormals` cannot give, being deduped
|
|
229
|
+
* directions rather than faces.
|
|
230
|
+
*/
|
|
231
|
+
const faces = planes.length >= 4 ? buildFaces(pts, n, planes) : EMPTY_FACES;
|
|
232
|
+
return { vertices: pts, faceNormals, edgeDirs, radius, sideRadius: 0, boundRadius, ...faces };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* World-axis bounds of a shape as placed (its points are its placement).
|
|
236
|
+
*
|
|
237
|
+
* **`radius` and `sideRadius` grow the box differently, and the second one is not `sideRadius` on
|
|
238
|
+
* every axis.** A ball grows a box by its radius along all three; a disc perpendicular to a unit
|
|
239
|
+
* axis `a` grows it by `sideRadius * sqrt(1 - a[i]^2)` along axis `i` — the disc's own extent seen
|
|
240
|
+
* down that axis, which is the full radius across the axis and exactly nothing along it. Using
|
|
241
|
+
* `sideRadius` on all three instead would bound a lying cylinder as if it were a sphere, which is
|
|
242
|
+
* conservative and therefore not wrong, and would cost the broad phase the difference on every
|
|
243
|
+
* cylinder in the world.
|
|
244
|
+
*/
|
|
245
|
+
export function shapeBounds(shape, out) {
|
|
246
|
+
const v = shape.vertices;
|
|
247
|
+
let minX = Infinity;
|
|
248
|
+
let minY = Infinity;
|
|
249
|
+
let minZ = Infinity;
|
|
250
|
+
let maxX = -Infinity;
|
|
251
|
+
let maxY = -Infinity;
|
|
252
|
+
let maxZ = -Infinity;
|
|
253
|
+
for (let i = 0; i < v.length; i += 3) {
|
|
254
|
+
const x = v[i] ?? 0;
|
|
255
|
+
const y = v[i + 1] ?? 0;
|
|
256
|
+
const z = v[i + 2] ?? 0;
|
|
257
|
+
if (x < minX)
|
|
258
|
+
minX = x;
|
|
259
|
+
if (x > maxX)
|
|
260
|
+
maxX = x;
|
|
261
|
+
if (y < minY)
|
|
262
|
+
minY = y;
|
|
263
|
+
if (y > maxY)
|
|
264
|
+
maxY = y;
|
|
265
|
+
if (z < minZ)
|
|
266
|
+
minZ = z;
|
|
267
|
+
if (z > maxZ)
|
|
268
|
+
maxZ = z;
|
|
269
|
+
}
|
|
270
|
+
let growX = shape.radius;
|
|
271
|
+
let growY = shape.radius;
|
|
272
|
+
let growZ = shape.radius;
|
|
273
|
+
if (shape.sideRadius > 0) {
|
|
274
|
+
const ax = (v[3] ?? 0) - (v[0] ?? 0);
|
|
275
|
+
const ay = (v[4] ?? 0) - (v[1] ?? 0);
|
|
276
|
+
const az = (v[5] ?? 0) - (v[2] ?? 0);
|
|
277
|
+
const length = Math.sqrt(ax * ax + ay * ay + az * az) || 1;
|
|
278
|
+
const ux = ax / length;
|
|
279
|
+
const uy = ay / length;
|
|
280
|
+
const uz = az / length;
|
|
281
|
+
growX += shape.sideRadius * Math.sqrt(Math.max(0, 1 - ux * ux));
|
|
282
|
+
growY += shape.sideRadius * Math.sqrt(Math.max(0, 1 - uy * uy));
|
|
283
|
+
growZ += shape.sideRadius * Math.sqrt(Math.max(0, 1 - uz * uz));
|
|
284
|
+
}
|
|
285
|
+
out.minX = minX - growX;
|
|
286
|
+
out.minY = minY - growY;
|
|
287
|
+
out.minZ = minZ - growZ;
|
|
288
|
+
out.maxX = maxX + growX;
|
|
289
|
+
out.maxY = maxY + growY;
|
|
290
|
+
out.maxZ = maxZ + growZ;
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
function dedupePoints(points) {
|
|
294
|
+
const kept = [];
|
|
295
|
+
for (let i = 0; i + 2 < points.length; i += 3) {
|
|
296
|
+
const x = points[i] ?? 0;
|
|
297
|
+
const y = points[i + 1] ?? 0;
|
|
298
|
+
const z = points[i + 2] ?? 0;
|
|
299
|
+
let duplicate = false;
|
|
300
|
+
for (let k = 0; k < kept.length; k += 3) {
|
|
301
|
+
if (Math.abs((kept[k] ?? 0) - x) < POINT_EPS &&
|
|
302
|
+
Math.abs((kept[k + 1] ?? 0) - y) < POINT_EPS &&
|
|
303
|
+
Math.abs((kept[k + 2] ?? 0) - z) < POINT_EPS) {
|
|
304
|
+
duplicate = true;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!duplicate)
|
|
309
|
+
kept.push(x, y, z);
|
|
310
|
+
}
|
|
311
|
+
return new Float32Array(kept);
|
|
312
|
+
}
|
|
313
|
+
/** If the triple (i, j, k) spans a supporting plane, record it outward in `planes`. */
|
|
314
|
+
function proposePlane(pts, n, i, j, k, planes) {
|
|
315
|
+
const ax = pts[i * 3] ?? 0;
|
|
316
|
+
const ay = pts[i * 3 + 1] ?? 0;
|
|
317
|
+
const az = pts[i * 3 + 2] ?? 0;
|
|
318
|
+
const ux = (pts[j * 3] ?? 0) - ax;
|
|
319
|
+
const uy = (pts[j * 3 + 1] ?? 0) - ay;
|
|
320
|
+
const uz = (pts[j * 3 + 2] ?? 0) - az;
|
|
321
|
+
const vx = (pts[k * 3] ?? 0) - ax;
|
|
322
|
+
const vy = (pts[k * 3 + 1] ?? 0) - ay;
|
|
323
|
+
const vz = (pts[k * 3 + 2] ?? 0) - az;
|
|
324
|
+
let nx = uy * vz - uz * vy;
|
|
325
|
+
let ny = uz * vx - ux * vz;
|
|
326
|
+
let nz = ux * vy - uy * vx;
|
|
327
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
328
|
+
if (len < 1e-9)
|
|
329
|
+
return;
|
|
330
|
+
nx /= len;
|
|
331
|
+
ny /= len;
|
|
332
|
+
nz /= len;
|
|
333
|
+
let above = 0;
|
|
334
|
+
let below = 0;
|
|
335
|
+
for (let p = 0; p < n; p++) {
|
|
336
|
+
const d = ((pts[p * 3] ?? 0) - ax) * nx +
|
|
337
|
+
((pts[p * 3 + 1] ?? 0) - ay) * ny +
|
|
338
|
+
((pts[p * 3 + 2] ?? 0) - az) * nz;
|
|
339
|
+
if (d > COPLANAR_M)
|
|
340
|
+
above++;
|
|
341
|
+
else if (d < -COPLANAR_M)
|
|
342
|
+
below++;
|
|
343
|
+
if (above > 0 && below > 0)
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Outward means every other point sits behind the plane.
|
|
347
|
+
if (above === 0)
|
|
348
|
+
recordPlane(planes, nx, ny, nz, nx * ax + ny * ay + nz * az);
|
|
349
|
+
if (below === 0)
|
|
350
|
+
recordPlane(planes, -nx, -ny, -nz, -(nx * ax + ny * ay + nz * az));
|
|
351
|
+
}
|
|
352
|
+
function recordPlane(planes, nx, ny, nz, d) {
|
|
353
|
+
for (let p = 0; p < planes.length; p += 4) {
|
|
354
|
+
const dot = (planes[p] ?? 0) * nx + (planes[p + 1] ?? 0) * ny + (planes[p + 2] ?? 0) * nz;
|
|
355
|
+
if (dot > DIR_DEDUP && Math.abs((planes[p + 3] ?? 0) - d) < COPLANAR_M)
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
planes.push(nx, ny, nz, d);
|
|
359
|
+
}
|
|
360
|
+
/** Unique directions (as lines) from a packed list with the given stride. */
|
|
361
|
+
function dedupeDirections(packed, stride) {
|
|
362
|
+
const dirs = [];
|
|
363
|
+
for (let p = 0; p < packed.length; p += stride) {
|
|
364
|
+
const x = packed[p] ?? 0;
|
|
365
|
+
const y = packed[p + 1] ?? 0;
|
|
366
|
+
const z = packed[p + 2] ?? 0;
|
|
367
|
+
appendDirection(dirs, x, y, z);
|
|
368
|
+
}
|
|
369
|
+
return new Float32Array(dirs);
|
|
370
|
+
}
|
|
371
|
+
function appendDirection(dirs, x, y, z) {
|
|
372
|
+
const len = Math.sqrt(x * x + y * y + z * z);
|
|
373
|
+
if (len < 1e-9)
|
|
374
|
+
return;
|
|
375
|
+
const nx = x / len;
|
|
376
|
+
const ny = y / len;
|
|
377
|
+
const nz = z / len;
|
|
378
|
+
for (let d = 0; d < dirs.length; d += 3) {
|
|
379
|
+
const dot = (dirs[d] ?? 0) * nx + (dirs[d + 1] ?? 0) * ny + (dirs[d + 2] ?? 0) * nz;
|
|
380
|
+
if (Math.abs(dot) > DIR_DEDUP)
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
dirs.push(nx, ny, nz);
|
|
384
|
+
}
|
|
385
|
+
/** Edge directions: point pairs lying on at least two distinct supporting planes. */
|
|
386
|
+
function hullEdges(pts, n, planes) {
|
|
387
|
+
const dirs = [];
|
|
388
|
+
for (let i = 0; i < n - 1; i++) {
|
|
389
|
+
for (let j = i + 1; j < n; j++) {
|
|
390
|
+
let shared = 0;
|
|
391
|
+
for (let p = 0; p < planes.length && shared < 2; p += 4) {
|
|
392
|
+
const nx = planes[p] ?? 0;
|
|
393
|
+
const ny = planes[p + 1] ?? 0;
|
|
394
|
+
const nz = planes[p + 2] ?? 0;
|
|
395
|
+
const d = planes[p + 3] ?? 0;
|
|
396
|
+
const di = (pts[i * 3] ?? 0) * nx + (pts[i * 3 + 1] ?? 0) * ny + (pts[i * 3 + 2] ?? 0) * nz - d;
|
|
397
|
+
if (Math.abs(di) > COPLANAR_M)
|
|
398
|
+
continue;
|
|
399
|
+
const dj = (pts[j * 3] ?? 0) * nx + (pts[j * 3 + 1] ?? 0) * ny + (pts[j * 3 + 2] ?? 0) * nz - d;
|
|
400
|
+
if (Math.abs(dj) > COPLANAR_M)
|
|
401
|
+
continue;
|
|
402
|
+
shared++;
|
|
403
|
+
}
|
|
404
|
+
if (shared < 2)
|
|
405
|
+
continue;
|
|
406
|
+
appendDirection(dirs, (pts[j * 3] ?? 0) - (pts[i * 3] ?? 0), (pts[j * 3 + 1] ?? 0) - (pts[i * 3 + 1] ?? 0), (pts[j * 3 + 2] ?? 0) - (pts[i * 3 + 2] ?? 0));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return new Float32Array(dirs);
|
|
410
|
+
}
|
|
411
|
+
function pairwiseDirections(pts, n) {
|
|
412
|
+
const dirs = [];
|
|
413
|
+
for (let i = 0; i < n - 1; i++) {
|
|
414
|
+
for (let j = i + 1; j < n; j++) {
|
|
415
|
+
appendDirection(dirs, (pts[j * 3] ?? 0) - (pts[i * 3] ?? 0), (pts[j * 3 + 1] ?? 0) - (pts[i * 3 + 1] ?? 0), (pts[j * 3 + 2] ?? 0) - (pts[i * 3 + 2] ?? 0));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return new Float32Array(dirs);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Group the supporting planes into faces and order each face's points around its normal.
|
|
422
|
+
*
|
|
423
|
+
* `proposePlane` records one plane per point triple, so a square face arrives four times over. The
|
|
424
|
+
* dedupe here is by *direction and offset together*, unlike `appendDirection`, which folds a plane
|
|
425
|
+
* into its opposite because a separating axis has no sign. A face does.
|
|
426
|
+
*
|
|
427
|
+
* **What this gives up:** a face with fewer than three points on it is dropped, so a shape flat
|
|
428
|
+
* enough to have no interior yields no faces and falls back to the empty set. **What would make it
|
|
429
|
+
* wrong** is a caller expecting a degenerate cloud to clip, which it cannot: there is no volume to
|
|
430
|
+
* clip against.
|
|
431
|
+
*/
|
|
432
|
+
function buildFaces(pts, n, planes) {
|
|
433
|
+
const kept = [];
|
|
434
|
+
const loops = [];
|
|
435
|
+
for (let p = 0; p + 3 < planes.length; p += 4) {
|
|
436
|
+
const nx = planes[p] ?? 0;
|
|
437
|
+
const ny = planes[p + 1] ?? 0;
|
|
438
|
+
const nz = planes[p + 2] ?? 0;
|
|
439
|
+
const d = planes[p + 3] ?? 0;
|
|
440
|
+
let seen = false;
|
|
441
|
+
for (let k = 0; k + 3 < kept.length; k += 4) {
|
|
442
|
+
const dot = (kept[k] ?? 0) * nx + (kept[k + 1] ?? 0) * ny + (kept[k + 2] ?? 0) * nz;
|
|
443
|
+
if (dot > DIR_DEDUP && Math.abs((kept[k + 3] ?? 0) - d) < COPLANAR_M) {
|
|
444
|
+
seen = true;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (seen)
|
|
449
|
+
continue;
|
|
450
|
+
const on = [];
|
|
451
|
+
for (let i = 0; i < n; i++) {
|
|
452
|
+
const x = pts[i * 3] ?? 0;
|
|
453
|
+
const y = pts[i * 3 + 1] ?? 0;
|
|
454
|
+
const z = pts[i * 3 + 2] ?? 0;
|
|
455
|
+
if (Math.abs(nx * x + ny * y + nz * z - d) < COPLANAR_M)
|
|
456
|
+
on.push(i);
|
|
457
|
+
}
|
|
458
|
+
if (on.length < 3)
|
|
459
|
+
continue;
|
|
460
|
+
kept.push(nx, ny, nz, d);
|
|
461
|
+
loops.push(orderAroundNormal(pts, on, nx, ny, nz));
|
|
462
|
+
}
|
|
463
|
+
if (loops.length === 0)
|
|
464
|
+
return EMPTY_FACES;
|
|
465
|
+
const starts = new Uint16Array(loops.length + 1);
|
|
466
|
+
let total = 0;
|
|
467
|
+
for (let i = 0; i < loops.length; i++) {
|
|
468
|
+
starts[i] = total;
|
|
469
|
+
total += loops[i]?.length ?? 0;
|
|
470
|
+
}
|
|
471
|
+
starts[loops.length] = total;
|
|
472
|
+
const indices = new Uint16Array(total);
|
|
473
|
+
let at = 0;
|
|
474
|
+
for (const loop of loops)
|
|
475
|
+
for (const index of loop)
|
|
476
|
+
indices[at++] = index;
|
|
477
|
+
return {
|
|
478
|
+
facePlanes: new Float32Array(kept),
|
|
479
|
+
faceVertexStart: starts,
|
|
480
|
+
faceVertexIndices: indices,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Sort a face's points counter-clockwise about its outward normal.
|
|
485
|
+
*
|
|
486
|
+
* A comparison sort on a key computed from the two in-plane axes, **not `Math.atan2`** — the
|
|
487
|
+
* determinism gate bans that outright, and the ordering only has to be *consistent*, which a
|
|
488
|
+
* pseudo-angle gives for free. `Array.prototype.sort` is stable by specification, and `on` is built
|
|
489
|
+
* in ascending index order, so two coincident points keep a defined order.
|
|
490
|
+
*/
|
|
491
|
+
function orderAroundNormal(pts, on, nx, ny, nz) {
|
|
492
|
+
// Any axis not parallel to the normal gives an in-plane basis.
|
|
493
|
+
const ax = Math.abs(nx) < 0.5 ? 1 : 0;
|
|
494
|
+
const ay = Math.abs(nx) < 0.5 ? 0 : 1;
|
|
495
|
+
let ux = ay * nz;
|
|
496
|
+
let uy = -ax * nz;
|
|
497
|
+
let uz = ax * ny - ay * nx;
|
|
498
|
+
const ulen = Math.sqrt(ux * ux + uy * uy + uz * uz) || 1;
|
|
499
|
+
ux /= ulen;
|
|
500
|
+
uy /= ulen;
|
|
501
|
+
uz /= ulen;
|
|
502
|
+
const vx = ny * uz - nz * uy;
|
|
503
|
+
const vy = nz * ux - nx * uz;
|
|
504
|
+
const vz = nx * uy - ny * ux;
|
|
505
|
+
let cx = 0;
|
|
506
|
+
let cy = 0;
|
|
507
|
+
let cz = 0;
|
|
508
|
+
for (const i of on) {
|
|
509
|
+
cx += pts[i * 3] ?? 0;
|
|
510
|
+
cy += pts[i * 3 + 1] ?? 0;
|
|
511
|
+
cz += pts[i * 3 + 2] ?? 0;
|
|
512
|
+
}
|
|
513
|
+
cx /= on.length;
|
|
514
|
+
cy /= on.length;
|
|
515
|
+
cz /= on.length;
|
|
516
|
+
const key = new Map();
|
|
517
|
+
for (const i of on) {
|
|
518
|
+
const dx = (pts[i * 3] ?? 0) - cx;
|
|
519
|
+
const dy = (pts[i * 3 + 1] ?? 0) - cy;
|
|
520
|
+
const dz = (pts[i * 3 + 2] ?? 0) - cz;
|
|
521
|
+
key.set(i, pseudoAngle(dx * ux + dy * uy + dz * uz, dx * vx + dy * vy + dz * vz));
|
|
522
|
+
}
|
|
523
|
+
// A copy, because `on` is the caller's and sorting in place would surprise it.
|
|
524
|
+
return [...on].sort((p, q) => (key.get(p) ?? 0) - (key.get(q) ?? 0));
|
|
525
|
+
}
|
|
526
|
+
/** A monotonic stand-in for `atan2`, in [0, 4), costing one divide and no transcendental. */
|
|
527
|
+
function pseudoAngle(x, y) {
|
|
528
|
+
const sum = Math.abs(x) + Math.abs(y);
|
|
529
|
+
if (sum === 0)
|
|
530
|
+
return 0;
|
|
531
|
+
const t = y / sum;
|
|
532
|
+
return x < 0 ? 2 - t : t < 0 ? 4 + t : t;
|
|
533
|
+
}
|
package/dist/solver.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { BodySet } from './bodies.ts';
|
|
2
|
+
import type { Manifold } from './manifold.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Contact constraints, and the substepped soft solve over them.
|
|
5
|
+
*
|
|
6
|
+
* **Soft rather than Baumgarte, and the difference is a property rather than a preference.** Every
|
|
7
|
+
* constraint carries a *hertz* and a damping ratio, from which the bias rate, mass scale and
|
|
8
|
+
* impulse scale are derived using the substep length. Stating stiffness as a frequency is what
|
|
9
|
+
* makes the substep count a quality dial: turn it up and nothing moves differently. A Baumgarte
|
|
10
|
+
* bias is a fraction of penetration per step, so it means something different at every step length,
|
|
11
|
+
* and the mitigations for that are all tuning constants — which `AGENTS.md` says never to assert.
|
|
12
|
+
*
|
|
13
|
+
* **Nothing here recomputes a manifold.** Anchors are stored in body-local frame at prepare time
|
|
14
|
+
* along with a separation offset, so the current separation is exact at any point inside the tick
|
|
15
|
+
* without re-running narrowphase per substep. `s = sBase + dot((pB + rB') − (pA + rA'), n)` is that
|
|
16
|
+
* identity, and it holds because `sBase` is defined to make it hold at prepare.
|
|
17
|
+
*
|
|
18
|
+
* **Every entry point takes an order array rather than a contiguous range.** Islands are disjoint
|
|
19
|
+
* by construction, so they can be solved in any order or simultaneously — but a constraint list
|
|
20
|
+
* built in pair order is not grouped by island. Indirecting through an order array groups them
|
|
21
|
+
* without permuting eleven parallel arrays, and it is what lets one `solveIsland` serve both
|
|
22
|
+
* executors.
|
|
23
|
+
*
|
|
24
|
+
* **Friction is solved before the normal**, against the normal impulse accumulated so far. Solving
|
|
25
|
+
* it after means the first iteration has no normal impulse to bound against and applies none, so a
|
|
26
|
+
* resting body slides for one iteration every substep.
|
|
27
|
+
*/
|
|
28
|
+
/** Contacts start with this much slack allowed before the bias fights them, in metres. */
|
|
29
|
+
export declare const LINEAR_SLOP = 0.005;
|
|
30
|
+
/** How fast the bias may push overlap out, in metres per second. */
|
|
31
|
+
export declare const MAX_BIAS_VELOCITY = 4;
|
|
32
|
+
/** Below this approach speed a bounce is not worth having, and jitters if it is applied. */
|
|
33
|
+
export declare const RESTITUTION_THRESHOLD = 1;
|
|
34
|
+
export declare class ContactConstraints {
|
|
35
|
+
/** One entry per manifold. */
|
|
36
|
+
count: number;
|
|
37
|
+
bodyA: Int32Array;
|
|
38
|
+
bodyB: Int32Array;
|
|
39
|
+
nx: Float32Array;
|
|
40
|
+
ny: Float32Array;
|
|
41
|
+
nz: Float32Array;
|
|
42
|
+
t1x: Float32Array;
|
|
43
|
+
t1y: Float32Array;
|
|
44
|
+
t1z: Float32Array;
|
|
45
|
+
t2x: Float32Array;
|
|
46
|
+
t2y: Float32Array;
|
|
47
|
+
t2z: Float32Array;
|
|
48
|
+
friction: Float32Array;
|
|
49
|
+
restitution: Float32Array;
|
|
50
|
+
pointStart: Int32Array;
|
|
51
|
+
pointCount: Int32Array;
|
|
52
|
+
/** One entry per contact point. */
|
|
53
|
+
points: number;
|
|
54
|
+
localA: Float32Array;
|
|
55
|
+
localB: Float32Array;
|
|
56
|
+
sBase: Float32Array;
|
|
57
|
+
normalMass: Float32Array;
|
|
58
|
+
tangent1Mass: Float32Array;
|
|
59
|
+
tangent2Mass: Float32Array;
|
|
60
|
+
normalImpulse: Float32Array;
|
|
61
|
+
tangent1Impulse: Float32Array;
|
|
62
|
+
tangent2Impulse: Float32Array;
|
|
63
|
+
/** Approach speed when the constraint was prepared, which is what restitution answers to. */
|
|
64
|
+
approach: Float32Array;
|
|
65
|
+
featureId: Int32Array;
|
|
66
|
+
/**
|
|
67
|
+
* Whether each side's anchor sits on a curved surface, one byte per point.
|
|
68
|
+
*
|
|
69
|
+
* A curved anchor is held in world frame rather than in the body's, for the reason `Manifold`
|
|
70
|
+
* states at length: the point where a ball touches the floor does not turn with the ball, and an
|
|
71
|
+
* anchor that turns anyway climbs its side and reports a gap that is not there. Per point rather
|
|
72
|
+
* than per manifold so `worldAnchors` needs only the point index, which is all its callers have.
|
|
73
|
+
*/
|
|
74
|
+
curvedA: Uint8Array;
|
|
75
|
+
curvedB: Uint8Array;
|
|
76
|
+
private cap;
|
|
77
|
+
private pcap;
|
|
78
|
+
constructor(capacity?: number);
|
|
79
|
+
clear(): void;
|
|
80
|
+
reserve(pointsNeeded: number): void;
|
|
81
|
+
private growConstraints;
|
|
82
|
+
private growPoints;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Turn one manifold into a constraint, with its effective masses and its local anchors.
|
|
86
|
+
*
|
|
87
|
+
* `warm` supplies the previous tick's impulses by feature id, or −1 where there is none. Warm
|
|
88
|
+
* starting is what lets a stack converge in a handful of iterations rather than a hundred.
|
|
89
|
+
*/
|
|
90
|
+
export declare function prepareContact(bodies: BodySet, a: number, b: number, m: Manifold, c: ContactConstraints, warmNormal: Float32Array | null, warmT1: Float32Array | null, warmT2: Float32Array | null): void;
|
|
91
|
+
/** Apply the impulses carried over from the previous tick, before the first iteration. */
|
|
92
|
+
export declare function warmStart(bodies: BodySet, c: ContactConstraints, order: Int32Array, from: number, to: number): void;
|
|
93
|
+
/**
|
|
94
|
+
* One iteration over a range of constraints.
|
|
95
|
+
*
|
|
96
|
+
* `useBias` is false on the relax pass, which is what removes the energy the bias added and is why
|
|
97
|
+
* a stack stops breathing.
|
|
98
|
+
*/
|
|
99
|
+
/**
|
|
100
|
+
* How the two tangent rows are bounded against each other.
|
|
101
|
+
*
|
|
102
|
+
* **`box` is two independent clamps and is what this solver has always done.** Each axis is bounded
|
|
103
|
+
* at `mu * N` on its own, so along the diagonal between them the pair reaches `sqrt(2) * mu * N` —
|
|
104
|
+
* forty-one per cent more friction than the same surface offers along an axis. The tangent basis is
|
|
105
|
+
* constructed from the contact normal, so *which* headings are cheap is an artefact of that
|
|
106
|
+
* construction rather than a fact about the world, and a box measurably slides further in some
|
|
107
|
+
* directions than others.
|
|
108
|
+
*
|
|
109
|
+
* **`elliptical` couples them and friction becomes isotropic**, which is what it physically is. It
|
|
110
|
+
* is a correctness fix and it is **not the default**, because every stacking result moves under it:
|
|
111
|
+
* `fingerprintBodies`, both baselines, and any golden fingerprint or stored replay a consumer has
|
|
112
|
+
* kept. A production engine does not silently invalidate those. *What would reverse
|
|
113
|
+
* that:* a major version, where the honest thing is to make the ellipse the default and say so in
|
|
114
|
+
* the porting guide.
|
|
115
|
+
*
|
|
116
|
+
* **One coefficient, not two, and that is a decision rather than an omission.** Anisotropic
|
|
117
|
+
* friction is the natural API for an ellipse — different `mu` along each tangent — and it needs a
|
|
118
|
+
* tangent frame anchored to a *material*, which this solver does not have: `prepareContact` builds
|
|
119
|
+
* the tangents from the normal, so they rotate arbitrarily as a contact moves and a per-axis
|
|
120
|
+
* coefficient would mean something different from one tick to the next. That is a silent wrong
|
|
121
|
+
* answer, so the ellipse here is a circle. *What would reverse it:* a tangent frame carried by the
|
|
122
|
+
* body, which is a larger change than the cone; `vehicle.ts` already needs that and answers it its
|
|
123
|
+
* own way, with a grip table rather than a coefficient.
|
|
124
|
+
*/
|
|
125
|
+
export type FrictionModel = 'box' | 'elliptical';
|
|
126
|
+
export declare function solveContacts(bodies: BodySet, c: ContactConstraints, order: Int32Array, from: number, to: number, h: number, useBias: boolean, hertz: number, damping: number, friction?: FrictionModel): void;
|
|
127
|
+
/**
|
|
128
|
+
* One pass after every substep, giving back the bounce the solve suppressed.
|
|
129
|
+
*
|
|
130
|
+
* Applied once against the approach speed captured at prepare, so a drop returns to the same height
|
|
131
|
+
* whatever the substep count. A bounce driven from the *current* velocity instead compounds with
|
|
132
|
+
* every substep and turns the coefficient into a function of the dial.
|
|
133
|
+
*/
|
|
134
|
+
export declare function applyRestitution(bodies: BodySet, c: ContactConstraints, order: Int32Array, from: number, to: number): void;
|
|
135
|
+
/** Whether a body is one the solver may move. */
|
|
136
|
+
export declare function isDynamicBody(bodies: BodySet, i: number): boolean;
|