@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
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import type { Aabb } from './collide/index.ts';
|
|
2
|
+
import { fieldCandidates, fieldConvexEdges, fieldCorners, fieldPlane } from './heightfieldShape.ts';
|
|
3
|
+
import { collideShapes, createManifold } from './manifold.ts';
|
|
4
|
+
import type { Manifold, ShapePose } from './manifold.ts';
|
|
5
|
+
import type { ConvexShape } from './shape.ts';
|
|
6
|
+
import { shapeBounds } from './shape.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Contacts between a convex shape and a static triangle mesh.
|
|
10
|
+
*
|
|
11
|
+
* **Many manifolds, not one, and that is the whole structural difference.** Every other pair in
|
|
12
|
+
* this engine has a single contact plane; a body in the corner of a mesh room has two or three, and
|
|
13
|
+
* reducing them to one would pick a wall and let the body through the others. So this fills a set,
|
|
14
|
+
* and `PhysicsWorld` prepares a constraint per entry — which the solver already supports, because
|
|
15
|
+
* `prepareContact` appends rather than replaces.
|
|
16
|
+
*
|
|
17
|
+
* **Each triangle goes through `collideShapes` unchanged**, as a three-point polytope written into
|
|
18
|
+
* one scratch shape. That is the reuse that makes this small: the separating-axis test, the
|
|
19
|
+
* clipping, the sphere and capsule paths and the cylinder's own module are all exact against a
|
|
20
|
+
* triangle already, and a second implementation of any of them would be a second thing to be wrong.
|
|
21
|
+
* *What it costs* is that the moving shape is transformed into world space once per candidate
|
|
22
|
+
* triangle rather than once per pair — eight vertices times twenty candidates for a box on a floor.
|
|
23
|
+
* *What would make it wrong* is a consumer resting a 64-point hull on a dense mesh, where the
|
|
24
|
+
* answer is to hoist the transform out of `collideShapes` rather than to write a second narrow
|
|
25
|
+
* phase.
|
|
26
|
+
*
|
|
27
|
+
* **Two sources of triangles and one set of rules.** A level's triangles are stored and found
|
|
28
|
+
* through a tree; a heightfield's are generated from four samples and found by index. Everything
|
|
29
|
+
* below is written once for both — the one-sided rule, the many manifolds, the interior-edge
|
|
30
|
+
* filter — and the only thing that differs is where a triangle's corners, plane and edge
|
|
31
|
+
* classification come from. `heightfieldShape.ts` is the other source, and the split is the
|
|
32
|
+
* 2026-08-13 rule applied to the one file here it would be most expensive to have two of.
|
|
33
|
+
*
|
|
34
|
+
* **One-sided.** A contact whose normal points into the back of a triangle is dropped. A level mesh
|
|
35
|
+
* is a surface rather than a solid, so "which side" is the only thing that distinguishes standing
|
|
36
|
+
* on a floor from being pushed up through it, and a two-sided triangle catches anything that has
|
|
37
|
+
* already sunk below it and fires it out the wrong way.
|
|
38
|
+
*
|
|
39
|
+
* **And the interior-edge filter, which is the part that decides whether it feels right.** A box
|
|
40
|
+
* sliding across two coplanar triangles meets their shared edge, where the narrow phase finds an
|
|
41
|
+
* edge normal rather than the face's and shoves the box backwards along it — a character stumbling
|
|
42
|
+
* on a flat floor once a metre. `meshShape` classifies every edge once as convex or not; a normal
|
|
43
|
+
* that came off a non-convex edge is replaced with the face's, and the separation is recomputed
|
|
44
|
+
* from the face plane. *What that gives up:* a genuinely convex ridge still produces its edge
|
|
45
|
+
* normal, which is correct, so a consumer who wants a ridge softened has to bevel it.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** How many contact planes one pair may produce. A body in a corner needs three; eight is generous. */
|
|
49
|
+
export const MAX_MESH_MANIFOLDS = 8;
|
|
50
|
+
|
|
51
|
+
/** How parallel a contact normal must be to the face before it counts as coming off the face. */
|
|
52
|
+
const FACE_DOT = 0.999;
|
|
53
|
+
|
|
54
|
+
/** The candidate list from the tree, grown rather than capped. */
|
|
55
|
+
let candidates = new Int32Array(256);
|
|
56
|
+
const region: Aabb = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
|
|
57
|
+
const local: Aabb = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
|
|
58
|
+
const scratch = createManifold();
|
|
59
|
+
const rot = new Float32Array(9);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A triangle as a polytope, rewritten per candidate.
|
|
63
|
+
*
|
|
64
|
+
* One face rather than two, which is what makes the pair one-sided at the level below as well:
|
|
65
|
+
* `collideShapes` looks for a reference face and there is only the front one to find.
|
|
66
|
+
*/
|
|
67
|
+
const TRIANGLE: ConvexShape = {
|
|
68
|
+
vertices: new Float32Array(9),
|
|
69
|
+
faceNormals: new Float32Array(3),
|
|
70
|
+
edgeDirs: new Float32Array(9),
|
|
71
|
+
radius: 0,
|
|
72
|
+
sideRadius: 0,
|
|
73
|
+
boundRadius: 0,
|
|
74
|
+
facePlanes: new Float32Array(4),
|
|
75
|
+
faceVertexStart: new Uint16Array([0, 3]),
|
|
76
|
+
faceVertexIndices: new Uint16Array([0, 1, 2]),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether a shape is a static triangle mesh, which is the one question the dispatch asks.
|
|
81
|
+
*
|
|
82
|
+
* Takes an absent shape as *not* a mesh, because a compound body has no single shape and the
|
|
83
|
+
* dispatch asks about both sides before it knows which kind of pair it has.
|
|
84
|
+
*/
|
|
85
|
+
export function isMesh(shape: ConvexShape | undefined): boolean {
|
|
86
|
+
return shape?.triangles !== undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Fill `out` with up to `out.length` manifolds and return how many.
|
|
91
|
+
*
|
|
92
|
+
* `out` is the caller's, allocated once — this runs in the tick. The normal of each manifold points
|
|
93
|
+
* from `shapeA` to `shapeB`, exactly as `collideShapes` promises, whichever of the two is the mesh.
|
|
94
|
+
*/
|
|
95
|
+
export function collideMesh(
|
|
96
|
+
shapeA: ConvexShape,
|
|
97
|
+
poseA: ShapePose,
|
|
98
|
+
shapeB: ConvexShape,
|
|
99
|
+
poseB: ShapePose,
|
|
100
|
+
margin: number,
|
|
101
|
+
out: readonly Manifold[],
|
|
102
|
+
): number {
|
|
103
|
+
const meshIsA = isMesh(shapeA);
|
|
104
|
+
const mesh = (meshIsA ? shapeA : shapeB).triangles;
|
|
105
|
+
if (mesh === undefined) return 0;
|
|
106
|
+
const meshPose = meshIsA ? poseA : poseB;
|
|
107
|
+
const other = meshIsA ? shapeB : shapeA;
|
|
108
|
+
const otherPose = meshIsA ? poseB : poseA;
|
|
109
|
+
/* Two meshes never touch: both are static, so nothing would move if they did. */
|
|
110
|
+
if (isMesh(other)) return 0;
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* The moving shape's bounds, carried into the mesh's own frame — because the tree is in that
|
|
114
|
+
* frame and rebuilding it per frame is exactly what static means it never has to do. The box is
|
|
115
|
+
* transformed rather than the tree: a rotated box's axis-aligned bounds are a little larger than
|
|
116
|
+
* the shape, which costs a few extra candidates and nothing else.
|
|
117
|
+
*/
|
|
118
|
+
shapeBounds(other, region);
|
|
119
|
+
worldRegion(region, otherPose, meshPose, margin, local);
|
|
120
|
+
let found = candidatesFor(mesh, local);
|
|
121
|
+
while (found === candidates.length) {
|
|
122
|
+
candidates = new Int32Array(candidates.length * 2);
|
|
123
|
+
found = candidatesFor(mesh, local);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let count = 0;
|
|
127
|
+
for (let i = 0; i < found; i++) {
|
|
128
|
+
const triangle = candidates[i] ?? 0;
|
|
129
|
+
if (!writeTriangle(mesh, triangle)) continue;
|
|
130
|
+
/* A to B, with the triangle standing in for the mesh wherever the mesh stood. */
|
|
131
|
+
const hit = meshIsA
|
|
132
|
+
? collideShapes(TRIANGLE, meshPose, other, otherPose, margin, scratch)
|
|
133
|
+
: collideShapes(other, otherPose, TRIANGLE, meshPose, margin, scratch);
|
|
134
|
+
if (!hit) continue;
|
|
135
|
+
/* The face normal in world, pointing away from the front of the triangle. */
|
|
136
|
+
planeOf(mesh, triangle);
|
|
137
|
+
rotateInto(meshPose, PLANE, 0, FACE);
|
|
138
|
+
/* Mesh-to-body, whichever way the manifold was asked for. */
|
|
139
|
+
const sign = meshIsA ? 1 : -1;
|
|
140
|
+
const away =
|
|
141
|
+
sign * scratch.nx * FACE[0] + sign * scratch.ny * FACE[1] + sign * scratch.nz * FACE[2];
|
|
142
|
+
/* One-sided: a normal pointing into the back of the triangle is a body already through it. */
|
|
143
|
+
if (away <= 0) continue;
|
|
144
|
+
if (away < FACE_DOT) filterInteriorEdge(mesh, triangle, meshPose, sign);
|
|
145
|
+
|
|
146
|
+
const target = out[count];
|
|
147
|
+
if (target === undefined) break;
|
|
148
|
+
copyInto(scratch, target, triangle);
|
|
149
|
+
count++;
|
|
150
|
+
if (count === out.length) break;
|
|
151
|
+
}
|
|
152
|
+
return count;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `[nx, ny, nz]` of the current triangle's world face normal. */
|
|
156
|
+
const FACE = new Float64Array(3);
|
|
157
|
+
/** The current triangle's plane in the mesh's own frame, from wherever it came. */
|
|
158
|
+
const PLANE = new Float32Array(4);
|
|
159
|
+
/** The current triangle's three corners, likewise. */
|
|
160
|
+
const CORNERS = new Float64Array(9);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The candidates a box could touch: a tree query for a level, an index range for a field.
|
|
164
|
+
*
|
|
165
|
+
* Both fill to `candidates.length` and stop, so the caller's grow-and-retry loop is the same one
|
|
166
|
+
* either way.
|
|
167
|
+
*/
|
|
168
|
+
function candidatesFor(mesh: NonNullable<ConvexShape['triangles']>, local: Aabb): number {
|
|
169
|
+
if (mesh.field !== undefined) return fieldCandidates(mesh.field, local, candidates);
|
|
170
|
+
return mesh.tree?.query(local, candidates) ?? 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The current triangle's plane into `PLANE`: read for a level, computed for a field. */
|
|
174
|
+
function planeOf(mesh: NonNullable<ConvexShape['triangles']>, triangle: number): void {
|
|
175
|
+
if (mesh.field !== undefined) {
|
|
176
|
+
fieldPlane(mesh.field, triangle, PLANE_SCRATCH);
|
|
177
|
+
PLANE[0] = PLANE_SCRATCH[0] ?? 0;
|
|
178
|
+
PLANE[1] = PLANE_SCRATCH[1] ?? 0;
|
|
179
|
+
PLANE[2] = PLANE_SCRATCH[2] ?? 0;
|
|
180
|
+
PLANE[3] = PLANE_SCRATCH[3] ?? 0;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
PLANE[0] = mesh.planes[triangle * 4] ?? 0;
|
|
184
|
+
PLANE[1] = mesh.planes[triangle * 4 + 1] ?? 0;
|
|
185
|
+
PLANE[2] = mesh.planes[triangle * 4 + 2] ?? 0;
|
|
186
|
+
PLANE[3] = mesh.planes[triangle * 4 + 3] ?? 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const PLANE_SCRATCH = new Float64Array(4);
|
|
190
|
+
|
|
191
|
+
/** The current triangle's corners into `CORNERS`, in the mesh's own frame. */
|
|
192
|
+
function cornersOf(mesh: NonNullable<ConvexShape['triangles']>, triangle: number): void {
|
|
193
|
+
if (mesh.field !== undefined) {
|
|
194
|
+
fieldCorners(mesh.field, triangle, CORNERS);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (let k = 0; k < 3; k++) {
|
|
198
|
+
const index = mesh.indices[triangle * 3 + k] ?? 0;
|
|
199
|
+
CORNERS[k * 3] = mesh.positions[index * 3] ?? 0;
|
|
200
|
+
CORNERS[k * 3 + 1] = mesh.positions[index * 3 + 1] ?? 0;
|
|
201
|
+
CORNERS[k * 3 + 2] = mesh.positions[index * 3 + 2] ?? 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Which of a triangle's edges may produce a normal: stored for a level, computed for a field. */
|
|
206
|
+
function convexEdgesOf(mesh: NonNullable<ConvexShape['triangles']>, triangle: number): number {
|
|
207
|
+
if (mesh.field !== undefined) return fieldConvexEdges(mesh.field, triangle);
|
|
208
|
+
return mesh.convexEdges[triangle] ?? 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** One corner of a triangle, in world. */
|
|
212
|
+
function worldCorner(
|
|
213
|
+
mesh: NonNullable<ConvexShape['triangles']>,
|
|
214
|
+
triangle: number,
|
|
215
|
+
corner: number,
|
|
216
|
+
pose: ShapePose,
|
|
217
|
+
out: Float64Array,
|
|
218
|
+
at: number,
|
|
219
|
+
): void {
|
|
220
|
+
cornersOf(mesh, triangle);
|
|
221
|
+
poseRotation(pose);
|
|
222
|
+
const x = CORNERS[corner * 3] ?? 0;
|
|
223
|
+
const y = CORNERS[corner * 3 + 1] ?? 0;
|
|
224
|
+
const z = CORNERS[corner * 3 + 2] ?? 0;
|
|
225
|
+
out[at] = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z + pose.x;
|
|
226
|
+
out[at + 1] = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z + pose.y;
|
|
227
|
+
out[at + 2] = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z + pose.z;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Replace an edge normal with the face's, where the edge it came off is not a real one.
|
|
232
|
+
*
|
|
233
|
+
* The deepest contact point decides which edge, by distance to each of the three in the triangle's
|
|
234
|
+
* own plane — a vertex contact falls to whichever of its two edges is nearer, and both are treated
|
|
235
|
+
* the same way, which is what the filter wants.
|
|
236
|
+
*
|
|
237
|
+
* The separations are recomputed from the face plane rather than scaled, because the direction has
|
|
238
|
+
* changed and a scaled depth would hold a body off the floor by the cosine of an angle nobody
|
|
239
|
+
* chose.
|
|
240
|
+
*/
|
|
241
|
+
function filterInteriorEdge(
|
|
242
|
+
mesh: NonNullable<ConvexShape['triangles']>,
|
|
243
|
+
triangle: number,
|
|
244
|
+
meshPose: ShapePose,
|
|
245
|
+
sign: number,
|
|
246
|
+
): void {
|
|
247
|
+
let deepest = 0;
|
|
248
|
+
for (let i = 1; i < scratch.count; i++) {
|
|
249
|
+
if ((scratch.separations[i] ?? 0) < (scratch.separations[deepest] ?? 0)) deepest = i;
|
|
250
|
+
}
|
|
251
|
+
const px = scratch.points[deepest * 3] ?? 0;
|
|
252
|
+
const py = scratch.points[deepest * 3 + 1] ?? 0;
|
|
253
|
+
const pz = scratch.points[deepest * 3 + 2] ?? 0;
|
|
254
|
+
|
|
255
|
+
let nearest = 0;
|
|
256
|
+
let best = Infinity;
|
|
257
|
+
for (let e = 0; e < 3; e++) {
|
|
258
|
+
worldCorner(mesh, triangle, e, meshPose, EDGE, 0);
|
|
259
|
+
worldCorner(mesh, triangle, (e + 1) % 3, meshPose, EDGE, 3);
|
|
260
|
+
const distance = pointSegmentSquared(px, py, pz);
|
|
261
|
+
if (distance < best) {
|
|
262
|
+
best = distance;
|
|
263
|
+
nearest = e;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/* A convex edge is a real one: the ridge is there and the body should feel it. */
|
|
267
|
+
if ((convexEdgesOf(mesh, triangle) >> nearest) & 1) return;
|
|
268
|
+
|
|
269
|
+
scratch.nx = sign * FACE[0];
|
|
270
|
+
scratch.ny = sign * FACE[1];
|
|
271
|
+
scratch.nz = sign * FACE[2];
|
|
272
|
+
worldPlaneAnchor(mesh, triangle, meshPose, ANCHOR);
|
|
273
|
+
const plane = FACE[0] * ANCHOR[0] + FACE[1] * ANCHOR[1] + FACE[2] * ANCHOR[2];
|
|
274
|
+
for (let i = 0; i < scratch.count; i++) {
|
|
275
|
+
scratch.separations[i] =
|
|
276
|
+
(scratch.points[i * 3] ?? 0) * FACE[0] +
|
|
277
|
+
(scratch.points[i * 3 + 1] ?? 0) * FACE[1] +
|
|
278
|
+
(scratch.points[i * 3 + 2] ?? 0) * FACE[2] -
|
|
279
|
+
plane;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const EDGE = new Float64Array(6);
|
|
284
|
+
const ANCHOR = new Float64Array(3);
|
|
285
|
+
|
|
286
|
+
function pointSegmentSquared(px: number, py: number, pz: number): number {
|
|
287
|
+
const ax = EDGE[0];
|
|
288
|
+
const ay = EDGE[1];
|
|
289
|
+
const az = EDGE[2];
|
|
290
|
+
const ex = EDGE[3] - ax;
|
|
291
|
+
const ey = EDGE[4] - ay;
|
|
292
|
+
const ez = EDGE[5] - az;
|
|
293
|
+
const ee = ex * ex + ey * ey + ez * ez;
|
|
294
|
+
let t = ee > 1e-12 ? ((px - ax) * ex + (py - ay) * ey + (pz - az) * ez) / ee : 0;
|
|
295
|
+
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
296
|
+
const dx = px - (ax + ex * t);
|
|
297
|
+
const dy = py - (ay + ey * t);
|
|
298
|
+
const dz = pz - (az + ez * t);
|
|
299
|
+
return dx * dx + dy * dy + dz * dz;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Write one triangle into the scratch shape. Returns false for a degenerate one. */
|
|
303
|
+
function writeTriangle(mesh: NonNullable<ConvexShape['triangles']>, triangle: number): boolean {
|
|
304
|
+
planeOf(mesh, triangle);
|
|
305
|
+
const nx = PLANE[0] ?? 0;
|
|
306
|
+
const ny = PLANE[1] ?? 0;
|
|
307
|
+
const nz = PLANE[2] ?? 0;
|
|
308
|
+
if (nx === 0 && ny === 0 && nz === 0) return false;
|
|
309
|
+
|
|
310
|
+
cornersOf(mesh, triangle);
|
|
311
|
+
const v = TRIANGLE.vertices;
|
|
312
|
+
for (let k = 0; k < 9; k++) v[k] = CORNERS[k] ?? 0;
|
|
313
|
+
TRIANGLE.faceNormals[0] = nx;
|
|
314
|
+
TRIANGLE.faceNormals[1] = ny;
|
|
315
|
+
TRIANGLE.faceNormals[2] = nz;
|
|
316
|
+
TRIANGLE.facePlanes[0] = nx;
|
|
317
|
+
TRIANGLE.facePlanes[1] = ny;
|
|
318
|
+
TRIANGLE.facePlanes[2] = nz;
|
|
319
|
+
TRIANGLE.facePlanes[3] = PLANE[3] ?? 0;
|
|
320
|
+
for (let e = 0; e < 3; e++) {
|
|
321
|
+
const j = (e + 1) % 3;
|
|
322
|
+
TRIANGLE.edgeDirs[e * 3] = (v[j * 3] ?? 0) - (v[e * 3] ?? 0);
|
|
323
|
+
TRIANGLE.edgeDirs[e * 3 + 1] = (v[j * 3 + 1] ?? 0) - (v[e * 3 + 1] ?? 0);
|
|
324
|
+
TRIANGLE.edgeDirs[e * 3 + 2] = (v[j * 3 + 2] ?? 0) - (v[e * 3 + 2] ?? 0);
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Copy a manifold out of the scratch, folding the triangle into every feature id.
|
|
331
|
+
*
|
|
332
|
+
* **Warm starting is why the triangle has to be in the id.** Impulses persist across ticks by
|
|
333
|
+
* feature id, and two triangles under one body produce the same clipping features — so without
|
|
334
|
+
* this a box on a seam would hand triangle A's accumulated impulse to triangle B every tick, which
|
|
335
|
+
* reads as a stack that will not settle.
|
|
336
|
+
*/
|
|
337
|
+
function copyInto(from: Manifold, to: Manifold, triangle: number): void {
|
|
338
|
+
to.nx = from.nx;
|
|
339
|
+
to.ny = from.ny;
|
|
340
|
+
to.nz = from.nz;
|
|
341
|
+
to.count = from.count;
|
|
342
|
+
to.curvedA = from.curvedA;
|
|
343
|
+
to.curvedB = from.curvedB;
|
|
344
|
+
for (let i = 0; i < from.count; i++) {
|
|
345
|
+
to.points[i * 3] = from.points[i * 3] ?? 0;
|
|
346
|
+
to.points[i * 3 + 1] = from.points[i * 3 + 1] ?? 0;
|
|
347
|
+
to.points[i * 3 + 2] = from.points[i * 3 + 2] ?? 0;
|
|
348
|
+
to.separations[i] = from.separations[i] ?? 0;
|
|
349
|
+
to.featureIds[i] = (((triangle & 0xffff) << 12) ^ (from.featureIds[i] ?? 0)) | 0;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The triangle's first corner, in world, which is a point the face plane passes through. */
|
|
354
|
+
function worldPlaneAnchor(
|
|
355
|
+
mesh: NonNullable<ConvexShape['triangles']>,
|
|
356
|
+
triangle: number,
|
|
357
|
+
pose: ShapePose,
|
|
358
|
+
out: Float64Array,
|
|
359
|
+
): void {
|
|
360
|
+
worldCorner(mesh, triangle, 0, pose, out, 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Rotate a direction out of a packed array into `out`. */
|
|
364
|
+
function rotateInto(pose: ShapePose, source: Float32Array, at: number, out: Float64Array): void {
|
|
365
|
+
poseRotation(pose);
|
|
366
|
+
const x = source[at] ?? 0;
|
|
367
|
+
const y = source[at + 1] ?? 0;
|
|
368
|
+
const z = source[at + 2] ?? 0;
|
|
369
|
+
out[0] = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z;
|
|
370
|
+
out[1] = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z;
|
|
371
|
+
out[2] = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The moving shape's world bounds, expressed in the mesh's frame and grown by the margin.
|
|
376
|
+
*
|
|
377
|
+
* Eight corners through the inverse of the mesh's pose, re-bounded: a rotated box's axis-aligned
|
|
378
|
+
* bounds in another frame are larger than the shape, which costs a few candidates the narrow phase
|
|
379
|
+
* then rejects and is far cheaper than transforming the geometry.
|
|
380
|
+
*/
|
|
381
|
+
function worldRegion(
|
|
382
|
+
bounds: Aabb,
|
|
383
|
+
otherPose: ShapePose,
|
|
384
|
+
meshPose: ShapePose,
|
|
385
|
+
margin: number,
|
|
386
|
+
out: Aabb,
|
|
387
|
+
): void {
|
|
388
|
+
out.minX = Infinity;
|
|
389
|
+
out.minY = Infinity;
|
|
390
|
+
out.minZ = Infinity;
|
|
391
|
+
out.maxX = -Infinity;
|
|
392
|
+
out.maxY = -Infinity;
|
|
393
|
+
out.maxZ = -Infinity;
|
|
394
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
395
|
+
const x = corner & 1 ? bounds.maxX : bounds.minX;
|
|
396
|
+
const y = corner & 2 ? bounds.maxY : bounds.minY;
|
|
397
|
+
const z = corner & 4 ? bounds.maxZ : bounds.minZ;
|
|
398
|
+
/* Out of the shape's own frame into the world, then into the mesh's. */
|
|
399
|
+
poseRotation(otherPose);
|
|
400
|
+
const wx = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z + otherPose.x;
|
|
401
|
+
const wy = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z + otherPose.y;
|
|
402
|
+
const wz = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z + otherPose.z;
|
|
403
|
+
unrotate(meshPose, wx - meshPose.x, wy - meshPose.y, wz - meshPose.z, ANCHOR);
|
|
404
|
+
out.minX = Math.min(out.minX, ANCHOR[0]);
|
|
405
|
+
out.minY = Math.min(out.minY, ANCHOR[1]);
|
|
406
|
+
out.minZ = Math.min(out.minZ, ANCHOR[2]);
|
|
407
|
+
out.maxX = Math.max(out.maxX, ANCHOR[0]);
|
|
408
|
+
out.maxY = Math.max(out.maxY, ANCHOR[1]);
|
|
409
|
+
out.maxZ = Math.max(out.maxZ, ANCHOR[2]);
|
|
410
|
+
}
|
|
411
|
+
out.minX -= margin;
|
|
412
|
+
out.minY -= margin;
|
|
413
|
+
out.minZ -= margin;
|
|
414
|
+
out.maxX += margin;
|
|
415
|
+
out.maxY += margin;
|
|
416
|
+
out.maxZ += margin;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function poseRotation(p: ShapePose): void {
|
|
420
|
+
const { qx: x, qy: y, qz: z, qw: w } = p;
|
|
421
|
+
rot[0] = 1 - 2 * (y * y + z * z);
|
|
422
|
+
rot[1] = 2 * (x * y - z * w);
|
|
423
|
+
rot[2] = 2 * (x * z + y * w);
|
|
424
|
+
rot[3] = 2 * (x * y + z * w);
|
|
425
|
+
rot[4] = 1 - 2 * (x * x + z * z);
|
|
426
|
+
rot[5] = 2 * (y * z - x * w);
|
|
427
|
+
rot[6] = 2 * (x * z - y * w);
|
|
428
|
+
rot[7] = 2 * (y * z + x * w);
|
|
429
|
+
rot[8] = 1 - 2 * (x * x + y * y);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function unrotate(p: ShapePose, x: number, y: number, z: number, out: Float64Array): void {
|
|
433
|
+
const qx = -p.qx;
|
|
434
|
+
const qy = -p.qy;
|
|
435
|
+
const qz = -p.qz;
|
|
436
|
+
const qw = p.qw;
|
|
437
|
+
const tx = 2 * (qy * z - qz * y);
|
|
438
|
+
const ty = 2 * (qz * x - qx * z);
|
|
439
|
+
const tz = 2 * (qx * y - qy * x);
|
|
440
|
+
out[0] = x + qw * tx + (qy * tz - qz * ty);
|
|
441
|
+
out[1] = y + qw * ty + (qz * tx - qx * tz);
|
|
442
|
+
out[2] = z + qw * tz + (qx * ty - qy * tx);
|
|
443
|
+
}
|