@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/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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step.js","sourceRoot":"","sources":["../src/step.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,8BAA8B,EAAE,MAAM,8BAA8B,CAAC;AAWtG,OAAO,EAAE,iCAAiC,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACtG,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AACtG,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEpE,6FAA6F;AAC7F,SAAS,yBAAyB,CAAC,KAAqB;IACtD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,IAAI,IAAI,GAAG,QAAQ,CAAC;QACpB,IAAI,IAAI,GAAG,QAAQ,CAAC;QACpB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;QACrB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;QACrB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,iCAAiC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAClD,4BAA4B,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;YACtD,IAAI,aAAa,CAAC,IAAI,GAAG,IAAI;gBAAE,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;YACzD,IAAI,aAAa,CAAC,IAAI,GAAG,IAAI;gBAAE,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;YACzD,IAAI,aAAa,CAAC,IAAI,GAAG,IAAI;gBAAE,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;YACzD,IAAI,aAAa,CAAC,IAAI,GAAG,IAAI;gBAAE,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;QAC3D,CAAC;QACD,IAAI,IAAI,GAAG,IAAI;YAAE,SAAS;QAC1B,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC;QACvB,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC;QACvB,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC;QACvB,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC;QACvB,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC;AAED,uGAAuG;AACvG,YAAY;AACZ,EAAE;AACF,uEAAuE;AACvE,EAAE;AACF,wGAAwG;AACxG,yGAAyG;AACzG,uFAAuF;AACvF,6FAA6F;AAC7F,yGAAyG;AACzG,qGAAqG;AACrG,EAAE;AACF,qGAAqG;AACrG,iGAAiG;AACjG,SAAS,sBAAsB,CAAC,KAAqB;IACnD,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAE3C,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ;QAAE,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAC;IAE/D,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAChD,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI;YAAE,SAAS;QAChD,6FAA6F;QAC7F,oFAAoF;QACpF,IAAI,KAAK,CAAC,WAAW,KAAK,CAAC,IAAI,MAAM,CAAC,WAAW,KAAK,CAAC;YAAE,SAAS;QAElE,MAAM,OAAO,GAAG,sBAAsB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QACvC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;QAEvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAChD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBACrC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBACrC,IAAI,CAAC,sBAAsB,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,eAAe,CAAC;oBAAE,SAAS;gBACzF,qBAAqB,CACnB,KAAK,EACL,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,KAAK,EACX,CAAC,EACD,CAAC,EACD,eAAe,EACf,SAAS,CAAC,MAAM,IAAI,SAAS,CAAC,MAAM,EACpC,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAC/D,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,CACzE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ;YAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;AAChD,CAAC;AAED,wGAAwG;AACxG,wGAAwG;AACxG,yGAAyG;AACzG,0EAA0E;AAC1E,SAAS,qBAAqB,CAC5B,KAAqB,EACrB,KAAa,EACb,KAAa,EACb,SAAiB,EACjB,SAAiB,EACjB,QAA4C,EAC5C,MAAe,EACf,QAAgB,EAChB,WAAmB;IAEnB,IAAI,OAAO,GAA4B,IAAI,CAAC;IAC5C,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACtC,IACE,QAAQ,CAAC,KAAK,KAAK,KAAK;YACxB,QAAQ,CAAC,KAAK,KAAK,KAAK;YACxB,QAAQ,CAAC,SAAS,KAAK,SAAS;YAChC,QAAQ,CAAC,SAAS,KAAK,SAAS,EAChC,CAAC;YACD,OAAO,GAAG,QAAQ,CAAC;YACnB,MAAM;QACR,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,GAAG;YACR,KAAK;YACL,KAAK;YACL,SAAS;YACT,SAAS;YACT,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;YACV,UAAU,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,2BAA2B,EAAE,EAAE,2BAA2B,EAAE,CAAC;YACtE,QAAQ;YACR,WAAW;YACX,MAAM;YACN,QAAQ,EAAE,IAAI;SACf,CAAC;QACF,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IACnC,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IACnC,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC5B,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;IAClC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAExB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,aAAa,GAAG,CAAC,CAAC;QACtB,IAAI,cAAc,GAAG,CAAC,CAAC;QACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;gBACrD,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;gBAChD,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC;gBAClD,MAAM;YACR,CAAC;QACH,CAAC;QACD,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;QACpB,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;QACpB,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAC5B,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QACpC,MAAM,CAAC,aAAa,GAAG,aAAa,CAAC;QACrC,MAAM,CAAC,cAAc,GAAG,cAAc,CAAC;IACzC,CAAC;IACD,OAAO,CAAC,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC;AAC3C,CAAC;AAED,qGAAqG;AACrG,sGAAsG;AACtG,2BAA2B;AAC3B,SAAS,2BAA2B,CAAC,KAAqB,EAAE,EAAU;IACpE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,MAAM;YAAE,SAAS;QAC7B,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACtD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QAE/C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzG,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzG,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzG,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAEzG,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC;QAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC;QAEzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAChC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC;YAC/B,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC;YAC/B,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC;YAC/B,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC;YAE/B,KAAK,CAAC,UAAU,GAAG,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7G,KAAK,CAAC,WAAW,GAAG,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAEhH,kGAAkG;YAClG,uEAAuE;YACvE,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC;YACpD,KAAK,CAAC,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,kBAAkB,GAAG,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAEzE,mGAAmG;YACnG,mDAAmD;YACnD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;YAC/E,IAAI,QAAQ,GAAG,CAAC,MAAM,CAAC,oBAAoB;gBAAE,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,QAAQ,CAAC;QAC5F,CAAC;IACH,CAAC;AACH,CAAC;AAED,mGAAmG;AACnG,qGAAqG;AACrG,yGAAyG;AACzG,sFAAsF;AACtF,SAAS,aAAa,CACpB,KAA4B,EAC5B,KAA4B,EAC5B,GAAW,EACX,GAAW,EACX,GAAW,EACX,GAAW,EACX,KAAa,EACb,KAAa;IAEb,MAAM,MAAM,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,GAAG,KAAK,CAAC;IACzC,MAAM,MAAM,GAAG,GAAG,GAAG,KAAK,GAAG,GAAG,GAAG,KAAK,CAAC;IACzC,MAAM,KAAK,GACT,KAAK,CAAC,WAAW;QACjB,KAAK,CAAC,WAAW;QACjB,KAAK,CAAC,cAAc,GAAG,MAAM,GAAG,MAAM;QACtC,KAAK,CAAC,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;IACzC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,uGAAuG;AACvG,sGAAsG;AACtG,qCAAqC;AACrC,EAAE;AACF,oGAAoG;AACpG,qGAAqG;AACrG,sGAAsG;AACtG,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAAC,KAAqB,EAAE,EAAU;IAC7D,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAAE,OAAO;IAEtB,yBAAyB,CAAC,KAAK,CAAC,CAAC;IACjC,sBAAsB,CAAC,KAAK,CAAC,CAAC;IAE9B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAE5B,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACtC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC;QAC7F,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC;QAC7F,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;QAC/D,oGAAoG;QACpG,kGAAkG;QAClG,0BAA0B;QAC1B,IAAI,CAAC,SAAS,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;QAC9C,IAAI,CAAC,SAAS,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;QAC9C,IAAI,CAAC,eAAe,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;IACvD,CAAC;IAED,2BAA2B,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvC,IAAI,MAAM,CAAC,YAAY;QAAE,0BAA0B,CAAC,KAAK,CAAC,CAAC;IAC3D,sBAAsB,CAAC,KAAK,CAAC,CAAC;IAE9B,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,SAAS;QACrC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QAC9B,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,SAAS,2BAA2B;IAClC,OAAO;QACL,CAAC,EAAE,CAAC;QACJ,CAAC,EAAE,CAAC;QACJ,KAAK,EAAE,CAAC;QACR,SAAS,EAAE,CAAC;QACZ,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,CAAC;QACN,GAAG,EAAE,CAAC;QACN,aAAa,EAAE,CAAC;QAChB,cAAc,EAAE,CAAC;QACjB,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,CAAC;QACd,IAAI,EAAE,CAAC;KACR,CAAC;AACJ,CAAC;AAED,wGAAwG;AACxG,mFAAmF;AACnF,SAAS,wBAAwB,CAAC,IAAgC,EAAE,KAAiC;IACnG,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAChE,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAChE,IAAI,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAChF,OAAO,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;AAC1C,CAAC;AAED,MAAM,aAAa,GAAgB,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC1E,MAAM,UAAU,GAAgB,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AACvE,MAAM,WAAW,GAAkB,EAAE,CAAC;AACtC,MAAM,eAAe,GAA6B,8BAA8B,EAAE,CAAC"}
@@ -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
+ });