@flighthq/physics2d 0.3.0-next.1021.f5f1ee0
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/dist/colliderTransform.d.ts +10 -0
- package/dist/colliderTransform.d.ts.map +1 -0
- package/dist/colliderTransform.js +137 -0
- package/dist/colliderTransform.js.map +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +6 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/massProperties.d.ts +4 -0
- package/dist/massProperties.d.ts.map +1 -0
- package/dist/massProperties.js +146 -0
- package/dist/massProperties.js.map +1 -0
- package/dist/solver.d.ts +6 -0
- package/dist/solver.d.ts.map +1 -0
- package/dist/solver.js +119 -0
- package/dist/solver.js.map +1 -0
- package/dist/step.d.ts +3 -0
- package/dist/step.d.ts.map +1 -0
- package/dist/step.js +268 -0
- package/dist/step.js.map +1 -0
- package/dist/world.d.ts +10 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +116 -0
- package/dist/world.js.map +1 -0
- package/package.json +49 -0
- package/src/colliderTransform.test.ts +115 -0
- package/src/massProperties.test.ts +185 -0
- package/src/solver.test.ts +167 -0
- package/src/step.test.ts +236 -0
- package/src/world.test.ts +142 -0
package/dist/step.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { collideContactManifold, createCollisionContactManifold } from '@flighthq/collision/contract';
|
|
2
|
+
import { updatePhysics2DColliderWorldShape, writePhysics2DColliderBounds } from './colliderTransform';
|
|
3
|
+
import { relativeNormalVelocity, solvePhysics2DContacts, warmStartPhysics2DContacts } from './solver';
|
|
4
|
+
import { findPhysics2DBody, isPhysics2DPairOrdered } from './world';
|
|
5
|
+
// Refreshes every collider's world shape and republishes its bounds to the broadphase index.
|
|
6
|
+
function updatePhysics2DBroadphase(world) {
|
|
7
|
+
for (const body of world.bodies) {
|
|
8
|
+
let minX = Infinity;
|
|
9
|
+
let minY = Infinity;
|
|
10
|
+
let maxX = -Infinity;
|
|
11
|
+
let maxY = -Infinity;
|
|
12
|
+
for (const collider of body.colliders) {
|
|
13
|
+
updatePhysics2DColliderWorldShape(collider, body);
|
|
14
|
+
writePhysics2DColliderBounds(collider, boundsScratch);
|
|
15
|
+
if (boundsScratch.minX < minX)
|
|
16
|
+
minX = boundsScratch.minX;
|
|
17
|
+
if (boundsScratch.minY < minY)
|
|
18
|
+
minY = boundsScratch.minY;
|
|
19
|
+
if (boundsScratch.maxX > maxX)
|
|
20
|
+
maxX = boundsScratch.maxX;
|
|
21
|
+
if (boundsScratch.maxY > maxY)
|
|
22
|
+
maxY = boundsScratch.maxY;
|
|
23
|
+
}
|
|
24
|
+
if (minX > maxX)
|
|
25
|
+
continue;
|
|
26
|
+
bodyBounds.minX = minX;
|
|
27
|
+
bodyBounds.minY = minY;
|
|
28
|
+
bodyBounds.maxX = maxX;
|
|
29
|
+
bodyBounds.maxY = maxY;
|
|
30
|
+
world.index.updateSpatialObject(body.index, bodyBounds);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Builds this step's contact set from the broadphase pairs, preserving each surviving contact's cached
|
|
34
|
+
// impulses.
|
|
35
|
+
//
|
|
36
|
+
// TWO ORDERINGS ARE ENFORCED HERE, and they are different obligations:
|
|
37
|
+
//
|
|
38
|
+
// 1. Each PAIR is ordered by body index before the narrow phase is called, because collision resolves
|
|
39
|
+
// contact points on the reference shape and ties toward its first argument. Reversed arguments move
|
|
40
|
+
// the points and renumber their feature ids, which discards the warm-start cache.
|
|
41
|
+
// 2. The contact LIST is sorted, because a sequential-impulse solver is order-dependent by
|
|
42
|
+
// construction — each impulse is applied against the velocities the previous ones left behind — and
|
|
43
|
+
// `querySpatialPairs` walks a Map of Sets, so its order follows insertion and movement history.
|
|
44
|
+
//
|
|
45
|
+
// Neither substitutes for the other: the first fixes contact identity, the second fixes solve order.
|
|
46
|
+
// A determinism harness that shuffles only insertion order would pass with the first one broken.
|
|
47
|
+
function buildPhysics2DContacts(world) {
|
|
48
|
+
world.index.querySpatialPairs(pairScratch);
|
|
49
|
+
for (const contact of world.contacts)
|
|
50
|
+
contact.touching = false;
|
|
51
|
+
for (const pair of pairScratch) {
|
|
52
|
+
const first = findPhysics2DBody(world, pair.a);
|
|
53
|
+
const second = findPhysics2DBody(world, pair.b);
|
|
54
|
+
if (first === null || second === null)
|
|
55
|
+
continue;
|
|
56
|
+
// Two bodies that cannot both move have no constraint to solve; skipping them here keeps the
|
|
57
|
+
// solver's body list free of pairs whose every impulse would be multiplied by zero.
|
|
58
|
+
if (first.inverseMass === 0 && second.inverseMass === 0)
|
|
59
|
+
continue;
|
|
60
|
+
const ordered = isPhysics2DPairOrdered(first, second);
|
|
61
|
+
const bodyA = ordered ? first : second;
|
|
62
|
+
const bodyB = ordered ? second : first;
|
|
63
|
+
for (let i = 0; i < bodyA.colliders.length; i++) {
|
|
64
|
+
for (let j = 0; j < bodyB.colliders.length; j++) {
|
|
65
|
+
const colliderA = bodyA.colliders[i];
|
|
66
|
+
const colliderB = bodyB.colliders[j];
|
|
67
|
+
if (!collideContactManifold(colliderA.world, colliderB.world, manifoldScratch))
|
|
68
|
+
continue;
|
|
69
|
+
mergePhysics2DContact(world, bodyA.index, bodyB.index, i, j, manifoldScratch, colliderA.sensor || colliderB.sensor, (colliderA.material.friction + colliderB.material.friction) / 2, Math.max(colliderA.material.restitution, colliderB.material.restitution));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (let i = world.contacts.length - 1; i >= 0; i--) {
|
|
74
|
+
if (!world.contacts[i].touching)
|
|
75
|
+
world.contacts.splice(i, 1);
|
|
76
|
+
}
|
|
77
|
+
world.contacts.sort(comparePhysics2DContacts);
|
|
78
|
+
}
|
|
79
|
+
// Writes this step's manifold into the persistent contact for the pair, creating it if new, and carries
|
|
80
|
+
// each point's converged impulses across by FEATURE ID rather than by slot. Matching by slot would hand
|
|
81
|
+
// a point the impulse of whatever happened to occupy that array position last step, which is a different
|
|
82
|
+
// contact feature the moment the manifold's point count or order changes.
|
|
83
|
+
function mergePhysics2DContact(world, bodyA, bodyB, colliderA, colliderB, manifold, sensor, friction, restitution) {
|
|
84
|
+
let contact = null;
|
|
85
|
+
for (const existing of world.contacts) {
|
|
86
|
+
if (existing.bodyA === bodyA &&
|
|
87
|
+
existing.bodyB === bodyB &&
|
|
88
|
+
existing.colliderA === colliderA &&
|
|
89
|
+
existing.colliderB === colliderB) {
|
|
90
|
+
contact = existing;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (contact === null) {
|
|
95
|
+
contact = {
|
|
96
|
+
bodyA,
|
|
97
|
+
bodyB,
|
|
98
|
+
colliderA,
|
|
99
|
+
colliderB,
|
|
100
|
+
normalX: 0,
|
|
101
|
+
normalY: 0,
|
|
102
|
+
pointCount: 0,
|
|
103
|
+
points: [createPhysics2DContactPoint(), createPhysics2DContactPoint()],
|
|
104
|
+
friction,
|
|
105
|
+
restitution,
|
|
106
|
+
sensor,
|
|
107
|
+
touching: true,
|
|
108
|
+
};
|
|
109
|
+
world.contacts.push(contact);
|
|
110
|
+
}
|
|
111
|
+
contact.normalX = manifold.normalX;
|
|
112
|
+
contact.normalY = manifold.normalY;
|
|
113
|
+
contact.friction = friction;
|
|
114
|
+
contact.restitution = restitution;
|
|
115
|
+
contact.sensor = sensor;
|
|
116
|
+
contact.touching = true;
|
|
117
|
+
for (let i = 0; i < manifold.pointCount; i++) {
|
|
118
|
+
const source = manifold.points[i];
|
|
119
|
+
const target = contact.points[i];
|
|
120
|
+
let normalImpulse = 0;
|
|
121
|
+
let tangentImpulse = 0;
|
|
122
|
+
for (let j = 0; j < contact.pointCount; j++) {
|
|
123
|
+
if (contact.points[j].featureId === source.featureId) {
|
|
124
|
+
normalImpulse = contact.points[j].normalImpulse;
|
|
125
|
+
tangentImpulse = contact.points[j].tangentImpulse;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
target.x = source.x;
|
|
130
|
+
target.y = source.y;
|
|
131
|
+
target.depth = source.depth;
|
|
132
|
+
target.featureId = source.featureId;
|
|
133
|
+
target.normalImpulse = normalImpulse;
|
|
134
|
+
target.tangentImpulse = tangentImpulse;
|
|
135
|
+
}
|
|
136
|
+
contact.pointCount = manifold.pointCount;
|
|
137
|
+
}
|
|
138
|
+
// Rebuilds every contact's lever arms, effective masses, and velocity bias for this step's geometry.
|
|
139
|
+
// These are recomputed rather than cached because they depend on the bodies' current positions, which
|
|
140
|
+
// the previous step moved.
|
|
141
|
+
function preparePhysics2DConstraints(world, dt) {
|
|
142
|
+
const config = world.config;
|
|
143
|
+
for (const contact of world.contacts) {
|
|
144
|
+
if (contact.sensor)
|
|
145
|
+
continue;
|
|
146
|
+
const bodyA = findPhysics2DBody(world, contact.bodyA);
|
|
147
|
+
const bodyB = findPhysics2DBody(world, contact.bodyB);
|
|
148
|
+
if (bodyA === null || bodyB === null)
|
|
149
|
+
continue;
|
|
150
|
+
const centerAX = bodyA.x + bodyA.centerX * Math.cos(bodyA.angle) - bodyA.centerY * Math.sin(bodyA.angle);
|
|
151
|
+
const centerAY = bodyA.y + bodyA.centerX * Math.sin(bodyA.angle) + bodyA.centerY * Math.cos(bodyA.angle);
|
|
152
|
+
const centerBX = bodyB.x + bodyB.centerX * Math.cos(bodyB.angle) - bodyB.centerY * Math.sin(bodyB.angle);
|
|
153
|
+
const centerBY = bodyB.y + bodyB.centerX * Math.sin(bodyB.angle) + bodyB.centerY * Math.cos(bodyB.angle);
|
|
154
|
+
const normalX = contact.normalX;
|
|
155
|
+
const normalY = contact.normalY;
|
|
156
|
+
const tangentX = -normalY;
|
|
157
|
+
const tangentY = normalX;
|
|
158
|
+
for (let i = 0; i < contact.pointCount; i++) {
|
|
159
|
+
const point = contact.points[i];
|
|
160
|
+
point.rAX = point.x - centerAX;
|
|
161
|
+
point.rAY = point.y - centerAY;
|
|
162
|
+
point.rBX = point.x - centerBX;
|
|
163
|
+
point.rBY = point.y - centerBY;
|
|
164
|
+
point.normalMass = effectiveMass(bodyA, bodyB, point.rAX, point.rAY, point.rBX, point.rBY, normalX, normalY);
|
|
165
|
+
point.tangentMass = effectiveMass(bodyA, bodyB, point.rAX, point.rAY, point.rBX, point.rBY, tangentX, tangentY);
|
|
166
|
+
// Penetration recovery, softened by a slop the solver deliberately leaves unresolved so a resting
|
|
167
|
+
// contact is not fighting a target of exactly zero overlap every step.
|
|
168
|
+
const excess = point.depth - config.penetrationSlop;
|
|
169
|
+
point.bias = excess > 0 ? -(config.positionCorrection / dt) * excess : 0;
|
|
170
|
+
// Restitution is dropped below a threshold approach speed. Without that, a ball bounces forever at
|
|
171
|
+
// ever smaller amplitudes and never comes to rest.
|
|
172
|
+
const approach = relativeNormalVelocity(bodyA, bodyB, point, normalX, normalY);
|
|
173
|
+
if (approach < -config.restitutionThreshold)
|
|
174
|
+
point.bias += contact.restitution * approach;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// The constraint's effective mass along `axis`: the inverse of how much the pair's velocity at the
|
|
179
|
+
// contact point changes per unit impulse. The angular terms are the lever arm crossed with the axis,
|
|
180
|
+
// which is the entire reason a contact point can produce torque — with only a minimum-translation vector
|
|
181
|
+
// and no point, both terms vanish and a box slides down a slope without ever tipping.
|
|
182
|
+
function effectiveMass(bodyA, bodyB, rAX, rAY, rBX, rBY, axisX, axisY) {
|
|
183
|
+
const crossA = rAX * axisY - rAY * axisX;
|
|
184
|
+
const crossB = rBX * axisY - rBY * axisX;
|
|
185
|
+
const total = bodyA.inverseMass +
|
|
186
|
+
bodyB.inverseMass +
|
|
187
|
+
bodyA.inverseInertia * crossA * crossA +
|
|
188
|
+
bodyB.inverseInertia * crossB * crossB;
|
|
189
|
+
return total > 0 ? 1 / total : 0;
|
|
190
|
+
}
|
|
191
|
+
// Advances the simulation by `dt` seconds. Everything the step does, it does because the caller asked:
|
|
192
|
+
// there is no implicit accumulation, no fixed-timestep loop hidden inside, and no allocation once the
|
|
193
|
+
// world's bodies and contacts exist.
|
|
194
|
+
//
|
|
195
|
+
// The order of the phases is not arbitrary. World shapes must be current before the broadphase sees
|
|
196
|
+
// their bounds; the contact set must be built before impulses can warm-start from it; velocities are
|
|
197
|
+
// solved before positions are integrated, so the integration moves bodies that have already had their
|
|
198
|
+
// constraints applied rather than moving them and correcting afterwards.
|
|
199
|
+
export function stepPhysics2D(world, dt) {
|
|
200
|
+
if (!(dt > 0))
|
|
201
|
+
return;
|
|
202
|
+
updatePhysics2DBroadphase(world);
|
|
203
|
+
buildPhysics2DContacts(world);
|
|
204
|
+
const bodies = world.bodies;
|
|
205
|
+
const config = world.config;
|
|
206
|
+
for (const body of bodies) {
|
|
207
|
+
if (body.type !== 'dynamic')
|
|
208
|
+
continue;
|
|
209
|
+
body.velocityX += (body.forceX * body.inverseMass + world.gravityX * body.gravityScale) * dt;
|
|
210
|
+
body.velocityY += (body.forceY * body.inverseMass + world.gravityY * body.gravityScale) * dt;
|
|
211
|
+
body.angularVelocity += body.torque * body.inverseInertia * dt;
|
|
212
|
+
// Damping is applied as a multiplicative decay rather than a subtracted force so it stays stable at
|
|
213
|
+
// any timestep: a force-shaped damping term large enough to matter can reverse the velocity it is
|
|
214
|
+
// damping when dt is big.
|
|
215
|
+
body.velocityX /= 1 + body.linearDamping * dt;
|
|
216
|
+
body.velocityY /= 1 + body.linearDamping * dt;
|
|
217
|
+
body.angularVelocity /= 1 + body.angularDamping * dt;
|
|
218
|
+
}
|
|
219
|
+
preparePhysics2DConstraints(world, dt);
|
|
220
|
+
if (config.warmStarting)
|
|
221
|
+
warmStartPhysics2DContacts(world);
|
|
222
|
+
solvePhysics2DContacts(world);
|
|
223
|
+
for (const body of bodies) {
|
|
224
|
+
if (body.type === 'static')
|
|
225
|
+
continue;
|
|
226
|
+
body.x += body.velocityX * dt;
|
|
227
|
+
body.y += body.velocityY * dt;
|
|
228
|
+
body.angle += body.angularVelocity * dt;
|
|
229
|
+
}
|
|
230
|
+
for (const body of bodies) {
|
|
231
|
+
body.forceX = 0;
|
|
232
|
+
body.forceY = 0;
|
|
233
|
+
body.torque = 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function createPhysics2DContactPoint() {
|
|
237
|
+
return {
|
|
238
|
+
x: 0,
|
|
239
|
+
y: 0,
|
|
240
|
+
depth: 0,
|
|
241
|
+
featureId: 0,
|
|
242
|
+
rAX: 0,
|
|
243
|
+
rAY: 0,
|
|
244
|
+
rBX: 0,
|
|
245
|
+
rBY: 0,
|
|
246
|
+
normalImpulse: 0,
|
|
247
|
+
tangentImpulse: 0,
|
|
248
|
+
normalMass: 0,
|
|
249
|
+
tangentMass: 0,
|
|
250
|
+
bias: 0,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Total order over contacts, by the four identities that define one. Every field is a persistent index,
|
|
254
|
+
// so the order is stable across steps and independent of the broadphase's history.
|
|
255
|
+
function comparePhysics2DContacts(left, right) {
|
|
256
|
+
if (left.bodyA !== right.bodyA)
|
|
257
|
+
return left.bodyA - right.bodyA;
|
|
258
|
+
if (left.bodyB !== right.bodyB)
|
|
259
|
+
return left.bodyB - right.bodyB;
|
|
260
|
+
if (left.colliderA !== right.colliderA)
|
|
261
|
+
return left.colliderA - right.colliderA;
|
|
262
|
+
return left.colliderB - right.colliderB;
|
|
263
|
+
}
|
|
264
|
+
const boundsScratch = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
265
|
+
const bodyBounds = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
266
|
+
const pairScratch = [];
|
|
267
|
+
const manifoldScratch = createCollisionContactManifold();
|
|
268
|
+
//# sourceMappingURL=step.js.map
|
package/dist/step.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"step.js","sourceRoot":"","sources":["../src/step.ts"],"names":[],"mappings":"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"}
|
package/dist/world.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CollisionShape, Physics2DCollider, Physics2DMaterial, Physics2DSolverConfig, Physics2DWorld, RigidBody2D, SpatialIndexBackend } from '@flighthq/types/contract';
|
|
2
|
+
export declare function addPhysics2DBody(world: Physics2DWorld, body: RigidBody2D): RigidBody2D;
|
|
3
|
+
export declare function createPhysics2DCollider(local: CollisionShape, material: Physics2DMaterial, sensor?: boolean): Physics2DCollider;
|
|
4
|
+
export declare function createPhysics2DSolverConfig(): Physics2DSolverConfig;
|
|
5
|
+
export declare function createPhysics2DWorld(gravityX?: number, gravityY?: number, index?: SpatialIndexBackend): Physics2DWorld;
|
|
6
|
+
export declare function createRigidBody2D(type: RigidBody2D['type'], x: number, y: number, angle?: number): RigidBody2D;
|
|
7
|
+
export declare function findPhysics2DBody(world: Readonly<Physics2DWorld>, index: number): RigidBody2D | null;
|
|
8
|
+
export declare function isPhysics2DPairOrdered(a: Readonly<RigidBody2D>, b: Readonly<RigidBody2D>): boolean;
|
|
9
|
+
export declare function removePhysics2DBody(world: Physics2DWorld, body: Readonly<RigidBody2D>): boolean;
|
|
10
|
+
//# sourceMappingURL=world.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../src/world.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EACd,WAAW,EACX,mBAAmB,EACpB,MAAM,0BAA0B,CAAC;AASlC,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,WAAW,CAKtF;AAID,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,UAAQ,GACb,iBAAiB,CAEnB;AAMD,wBAAgB,2BAA2B,IAAI,qBAAqB,CASnE;AAKD,wBAAgB,oBAAoB,CAAC,QAAQ,SAAI,EAAE,QAAQ,SAAQ,EAAE,KAAK,CAAC,EAAE,mBAAmB,GAAG,cAAc,CAUhH;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,WAAW,CAwBzG;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAKpG;AAcD,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,OAAO,CAElG;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,OAAO,CAU/F"}
|
package/dist/world.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createUniformGridSpatialBackend } from '@flighthq/spatial/contract';
|
|
2
|
+
import { createPhysics2DColliderWorldShape } from './colliderTransform';
|
|
3
|
+
import { updateRigidBody2DMassData } from './massProperties';
|
|
4
|
+
// Adds `body` to `world`, assigning it the persistent index every contact is keyed and ordered by, and
|
|
5
|
+
// returns it. The index comes from a monotonic counter rather than the array position, so removing a
|
|
6
|
+
// body can never hand its identity to a later one — a stale contact would otherwise be revived against
|
|
7
|
+
// whichever body inherited the slot, warm-starting it with a force that belonged to something else.
|
|
8
|
+
export function addPhysics2DBody(world, body) {
|
|
9
|
+
body.index = world.nextBodyIndex++;
|
|
10
|
+
world.bodies.push(body);
|
|
11
|
+
updateRigidBody2DMassData(body);
|
|
12
|
+
return body;
|
|
13
|
+
}
|
|
14
|
+
// Creates a collider from a LOCAL-space shape, allocating the world-space shape it will be transformed
|
|
15
|
+
// into. Both shapes exist for the collider's lifetime; the step rewrites the world one in place.
|
|
16
|
+
export function createPhysics2DCollider(local, material, sensor = false) {
|
|
17
|
+
return { local, world: createPhysics2DColliderWorldShape(local), material, sensor };
|
|
18
|
+
}
|
|
19
|
+
// The default solver tuning. Ten velocity iterations and three position iterations is the range every
|
|
20
|
+
// sequential-impulse engine converges on for game-scale stacks; the slop and correction factor are what
|
|
21
|
+
// stop a resting contact from either twitching against a target of exactly zero overlap or exploding
|
|
22
|
+
// outward when a deep overlap is corrected all at once.
|
|
23
|
+
export function createPhysics2DSolverConfig() {
|
|
24
|
+
return {
|
|
25
|
+
velocityIterations: 10,
|
|
26
|
+
positionIterations: 3,
|
|
27
|
+
penetrationSlop: 0.005,
|
|
28
|
+
positionCorrection: 0.2,
|
|
29
|
+
restitutionThreshold: 1,
|
|
30
|
+
warmStarting: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Creates an empty world. `index` defaults to a uniform grid; pass one to swap the broadphase structure
|
|
34
|
+
// without this package knowing which it got — `SpatialIndexBackend` is already that seam, so physics
|
|
35
|
+
// adds no second one over it.
|
|
36
|
+
export function createPhysics2DWorld(gravityX = 0, gravityY = -9.81, index) {
|
|
37
|
+
return {
|
|
38
|
+
bodies: [],
|
|
39
|
+
contacts: [],
|
|
40
|
+
index: index ?? createUniformGridSpatialBackend(1),
|
|
41
|
+
config: createPhysics2DSolverConfig(),
|
|
42
|
+
gravityX,
|
|
43
|
+
gravityY,
|
|
44
|
+
nextBodyIndex: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Creates a rigid body at rest. Mass properties stay zero until the body is added to a world, which
|
|
48
|
+
// derives them from the colliders — a body's mass is never assigned, only derived, so it cannot
|
|
49
|
+
// disagree with its shape.
|
|
50
|
+
export function createRigidBody2D(type, x, y, angle = 0) {
|
|
51
|
+
return {
|
|
52
|
+
index: -1,
|
|
53
|
+
type,
|
|
54
|
+
x,
|
|
55
|
+
y,
|
|
56
|
+
angle,
|
|
57
|
+
velocityX: 0,
|
|
58
|
+
velocityY: 0,
|
|
59
|
+
angularVelocity: 0,
|
|
60
|
+
forceX: 0,
|
|
61
|
+
forceY: 0,
|
|
62
|
+
torque: 0,
|
|
63
|
+
mass: 0,
|
|
64
|
+
inverseMass: 0,
|
|
65
|
+
inertia: 0,
|
|
66
|
+
inverseInertia: 0,
|
|
67
|
+
centerX: 0,
|
|
68
|
+
centerY: 0,
|
|
69
|
+
linearDamping: 0,
|
|
70
|
+
angularDamping: 0,
|
|
71
|
+
gravityScale: 1,
|
|
72
|
+
colliders: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// The body carrying `index`, or null when the world no longer holds it. Contacts store body indices
|
|
76
|
+
// rather than references so a contact can outlive one step without pinning a removed body alive; this is
|
|
77
|
+
// the lookup that turns one back into the other.
|
|
78
|
+
export function findPhysics2DBody(world, index) {
|
|
79
|
+
for (const body of world.bodies) {
|
|
80
|
+
if (body.index === index)
|
|
81
|
+
return body;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// The canonical order of a body pair: lower index first. Every contact in the world is created through
|
|
86
|
+
// this, which is what discharges the ordering obligation `@flighthq/collision` cannot.
|
|
87
|
+
//
|
|
88
|
+
// Collision resolves contact points on the reference shape's surface and, for two shapes of the same
|
|
89
|
+
// kind, ties toward its first argument — so passing a pair the other way round moves the points to the
|
|
90
|
+
// opposite surface and renumbers their feature ids. Broadphase pair order follows insertion and movement
|
|
91
|
+
// history, so without a canonical order the warm-start cache silently resets whenever a body is added
|
|
92
|
+
// mid-simulation. The order has to come from persistent identity rather than geometry, because any order
|
|
93
|
+
// derived from coordinates flips the moment those coordinates cross.
|
|
94
|
+
//
|
|
95
|
+
// It returns a boolean rather than sorting in place so the caller keeps both the ordered pair and the
|
|
96
|
+
// knowledge of whether it was reversed, which the collider indices must follow.
|
|
97
|
+
export function isPhysics2DPairOrdered(a, b) {
|
|
98
|
+
return a.index <= b.index;
|
|
99
|
+
}
|
|
100
|
+
// Removes `body` from `world`, along with every contact naming it. Contacts are dropped rather than
|
|
101
|
+
// kept, because a contact whose body is gone has no constraint to solve and its cached impulse belongs
|
|
102
|
+
// to a pair that no longer exists.
|
|
103
|
+
export function removePhysics2DBody(world, body) {
|
|
104
|
+
const at = world.bodies.indexOf(body);
|
|
105
|
+
if (at < 0)
|
|
106
|
+
return false;
|
|
107
|
+
world.bodies.splice(at, 1);
|
|
108
|
+
world.index.removeSpatialObject(body.index);
|
|
109
|
+
for (let i = world.contacts.length - 1; i >= 0; i--) {
|
|
110
|
+
const contact = world.contacts[i];
|
|
111
|
+
if (contact.bodyA === body.index || contact.bodyB === body.index)
|
|
112
|
+
world.contacts.splice(i, 1);
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=world.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.js","sourceRoot":"","sources":["../src/world.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/physics2d",
|
|
3
|
+
"version": "0.3.0-next.1021.f5f1ee0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
7
|
+
"directory": "packages/physics2d"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./contract": {
|
|
18
|
+
"types": "./dist/contract.d.ts",
|
|
19
|
+
"default": "./dist/contract.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src/**/*.test.ts",
|
|
25
|
+
"!dist/**/*.test.js",
|
|
26
|
+
"!dist/**/*.test.d.ts",
|
|
27
|
+
"!dist/**/*.test.js.map",
|
|
28
|
+
"!dist/**/*.test.d.ts.map"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -b",
|
|
32
|
+
"clean": "tsc -b --clean",
|
|
33
|
+
"test": "vitest run --config vitest.config.ts",
|
|
34
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
35
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
36
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@flighthq/collision": "0.3.0-next.1021.f5f1ee0",
|
|
40
|
+
"@flighthq/math": "0.3.0-next.1021.f5f1ee0",
|
|
41
|
+
"@flighthq/spatial": "0.3.0-next.1021.f5f1ee0",
|
|
42
|
+
"@flighthq/types": "0.3.0-next.1021.f5f1ee0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.3.0"
|
|
46
|
+
},
|
|
47
|
+
"description": "2D rigid-body dynamics — deterministic sequential-impulse solver over collision manifolds and a spatial broadphase, with explicit stepping and no hidden per-frame allocation",
|
|
48
|
+
"sideEffects": false
|
|
49
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { CollisionShape } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPhysics2DColliderWorldShape,
|
|
6
|
+
updatePhysics2DColliderWorldShape,
|
|
7
|
+
writePhysics2DColliderBounds,
|
|
8
|
+
} from './colliderTransform';
|
|
9
|
+
import { createPhysics2DCollider, createRigidBody2D } from './world';
|
|
10
|
+
|
|
11
|
+
const STONE = { density: 1, friction: 0.3, restitution: 0 };
|
|
12
|
+
|
|
13
|
+
function bounds() {
|
|
14
|
+
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('createPhysics2DColliderWorldShape', () => {
|
|
18
|
+
it('promotes an axis-aligned box to an oriented box, since a rotated box is not axis-aligned', () => {
|
|
19
|
+
expect(createPhysics2DColliderWorldShape({ kind: 'aabb', minX: 0, minY: 0, maxX: 1, maxY: 1 }).kind).toBe('obb');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps the kind for shapes a rigid transform preserves', () => {
|
|
23
|
+
expect(createPhysics2DColliderWorldShape({ kind: 'circle', x: 0, y: 0, radius: 1 }).kind).toBe('circle');
|
|
24
|
+
expect(createPhysics2DColliderWorldShape({ kind: 'polygon', points: [0, 0, 1, 0, 0, 1] }).kind).toBe('polygon');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('gives a polygon its own points array so the per-step transform cannot write through to the local shape', () => {
|
|
28
|
+
const local: CollisionShape = { kind: 'polygon', points: [0, 0, 1, 0, 0, 1] };
|
|
29
|
+
const world = createPhysics2DColliderWorldShape(local);
|
|
30
|
+
expect(world.kind === 'polygon' && world.points).not.toBe(local.points);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('updatePhysics2DColliderWorldShape', () => {
|
|
35
|
+
it('rotates and translates a circle offset from the body origin', () => {
|
|
36
|
+
const collider = createPhysics2DCollider({ kind: 'circle', x: 1, y: 0, radius: 0.5 }, STONE);
|
|
37
|
+
const body = createRigidBody2D('dynamic', 10, 5, Math.PI / 2);
|
|
38
|
+
updatePhysics2DColliderWorldShape(collider, body);
|
|
39
|
+
expect(collider.world.kind).toBe('circle');
|
|
40
|
+
if (collider.world.kind !== 'circle') return;
|
|
41
|
+
expect(collider.world.x).toBeCloseTo(10);
|
|
42
|
+
expect(collider.world.y).toBeCloseTo(6);
|
|
43
|
+
expect(collider.world.radius).toBe(0.5);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('carries the body angle onto the promoted oriented box rather than growing its extents', () => {
|
|
47
|
+
const collider = createPhysics2DCollider({ kind: 'aabb', minX: -1, minY: -0.5, maxX: 1, maxY: 0.5 }, STONE);
|
|
48
|
+
const body = createRigidBody2D('dynamic', 0, 0, 0.7);
|
|
49
|
+
updatePhysics2DColliderWorldShape(collider, body);
|
|
50
|
+
expect(collider.world.kind).toBe('obb');
|
|
51
|
+
if (collider.world.kind !== 'obb') return;
|
|
52
|
+
expect(collider.world.rotation).toBeCloseTo(0.7);
|
|
53
|
+
expect(collider.world.halfW).toBeCloseTo(1);
|
|
54
|
+
expect(collider.world.halfH).toBeCloseTo(0.5);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('composes the body angle with an oriented box collider own rotation', () => {
|
|
58
|
+
const collider = createPhysics2DCollider({ kind: 'obb', x: 0, y: 0, halfW: 1, halfH: 1, rotation: 0.2 }, STONE);
|
|
59
|
+
const body = createRigidBody2D('dynamic', 0, 0, 0.5);
|
|
60
|
+
updatePhysics2DColliderWorldShape(collider, body);
|
|
61
|
+
if (collider.world.kind !== 'obb') return;
|
|
62
|
+
expect(collider.world.rotation).toBeCloseTo(0.7);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('transforms every polygon vertex in place', () => {
|
|
66
|
+
const collider = createPhysics2DCollider({ kind: 'polygon', points: [0, 0, 1, 0, 0, 1] }, STONE);
|
|
67
|
+
const body = createRigidBody2D('dynamic', 3, 4, 0);
|
|
68
|
+
updatePhysics2DColliderWorldShape(collider, body);
|
|
69
|
+
if (collider.world.kind !== 'polygon') return;
|
|
70
|
+
expect(Array.from(collider.world.points)).toEqual([3, 4, 4, 4, 3, 5]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('writePhysics2DColliderBounds', () => {
|
|
75
|
+
it('bounds a circle by its radius', () => {
|
|
76
|
+
const collider = createPhysics2DCollider({ kind: 'circle', x: 0, y: 0, radius: 2 }, STONE);
|
|
77
|
+
updatePhysics2DColliderWorldShape(collider, createRigidBody2D('dynamic', 1, 1));
|
|
78
|
+
const out = bounds();
|
|
79
|
+
writePhysics2DColliderBounds(collider, out);
|
|
80
|
+
expect(out).toEqual({ minX: -1, minY: -1, maxX: 3, maxY: 3 });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('grows an oriented box bound as it turns, and returns to its extents at a quarter turn', () => {
|
|
84
|
+
// The bound of a rotated box is larger than the box; at 45 degrees it is largest, and at 90 it is the
|
|
85
|
+
// original extents with width and height exchanged. Pinned because a bound that did not grow would
|
|
86
|
+
// let the broadphase miss a genuine overlap.
|
|
87
|
+
const collider = createPhysics2DCollider({ kind: 'aabb', minX: -1, minY: -0.5, maxX: 1, maxY: 0.5 }, STONE);
|
|
88
|
+
const out = bounds();
|
|
89
|
+
|
|
90
|
+
updatePhysics2DColliderWorldShape(collider, createRigidBody2D('dynamic', 0, 0, Math.PI / 4));
|
|
91
|
+
writePhysics2DColliderBounds(collider, out);
|
|
92
|
+
expect(out.maxX).toBeGreaterThan(1);
|
|
93
|
+
|
|
94
|
+
updatePhysics2DColliderWorldShape(collider, createRigidBody2D('dynamic', 0, 0, Math.PI / 2));
|
|
95
|
+
writePhysics2DColliderBounds(collider, out);
|
|
96
|
+
expect(out.maxX).toBeCloseTo(0.5);
|
|
97
|
+
expect(out.maxY).toBeCloseTo(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('bounds a polygon by its extreme vertices', () => {
|
|
101
|
+
const collider = createPhysics2DCollider({ kind: 'polygon', points: [0, 0, 4, 1, 2, 5] }, STONE);
|
|
102
|
+
updatePhysics2DColliderWorldShape(collider, createRigidBody2D('dynamic', 0, 0));
|
|
103
|
+
const out = bounds();
|
|
104
|
+
writePhysics2DColliderBounds(collider, out);
|
|
105
|
+
expect(out).toEqual({ minX: 0, minY: 0, maxX: 4, maxY: 5 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('gives an area-less shape empty bounds rather than infinities', () => {
|
|
109
|
+
const collider = createPhysics2DCollider({ kind: 'point', x: 1, y: 1 }, STONE);
|
|
110
|
+
const out = bounds();
|
|
111
|
+
writePhysics2DColliderBounds(collider, out);
|
|
112
|
+
expect(Number.isFinite(out.minX)).toBe(true);
|
|
113
|
+
expect(Number.isFinite(out.maxY)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|