@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.
@@ -0,0 +1,142 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { stepPhysics2D } from './step';
4
+ import {
5
+ addPhysics2DBody,
6
+ findPhysics2DBody,
7
+ createPhysics2DCollider,
8
+ createPhysics2DSolverConfig,
9
+ createPhysics2DWorld,
10
+ createRigidBody2D,
11
+ isPhysics2DPairOrdered,
12
+ removePhysics2DBody,
13
+ } from './world';
14
+
15
+ const STONE = { density: 1, friction: 0.3, restitution: 0 };
16
+
17
+ function boxBody(x: number, y: number) {
18
+ const body = createRigidBody2D('dynamic', x, y);
19
+ body.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -0.5, minY: -0.5, maxX: 0.5, maxY: 0.5 }, STONE));
20
+ return body;
21
+ }
22
+
23
+ describe('addPhysics2DBody', () => {
24
+ it('assigns indices from a monotonic counter, so a removed body never hands its identity on', () => {
25
+ // Reusing an array slot as identity would let a stale contact be revived against whichever body
26
+ // inherited it, warm-starting a pair with a force that belonged to something else.
27
+ const world = createPhysics2DWorld();
28
+ const first = addPhysics2DBody(world, boxBody(0, 0));
29
+ const second = addPhysics2DBody(world, boxBody(1, 0));
30
+ removePhysics2DBody(world, first);
31
+ const third = addPhysics2DBody(world, boxBody(2, 0));
32
+
33
+ expect(second.index).toBe(1);
34
+ expect(third.index).toBe(2);
35
+ expect(third.index).not.toBe(first.index);
36
+ });
37
+
38
+ it('derives mass properties on insertion so a body is never simulated massless', () => {
39
+ const world = createPhysics2DWorld();
40
+ const body = addPhysics2DBody(world, boxBody(0, 0));
41
+ expect(body.mass).toBeCloseTo(1);
42
+ expect(body.inverseMass).toBeCloseTo(1);
43
+ expect(body.inertia).toBeGreaterThan(0);
44
+ });
45
+ });
46
+
47
+ describe('createPhysics2DCollider', () => {
48
+ it('allocates a world shape up front so the step transforms in place', () => {
49
+ const collider = createPhysics2DCollider({ kind: 'circle', x: 0, y: 0, radius: 1 }, STONE);
50
+ expect(collider.world).toBeDefined();
51
+ expect(collider.sensor).toBe(false);
52
+ });
53
+
54
+ it('marks a sensor collider so the solver reports its overlaps without resolving them', () => {
55
+ expect(createPhysics2DCollider({ kind: 'circle', x: 0, y: 0, radius: 1 }, STONE, true).sensor).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe('createPhysics2DSolverConfig', () => {
60
+ it('defaults to a tuning that converges for game-scale stacks', () => {
61
+ const config = createPhysics2DSolverConfig();
62
+ expect(config.velocityIterations).toBeGreaterThanOrEqual(8);
63
+ expect(config.warmStarting).toBe(true);
64
+ expect(config.penetrationSlop).toBeGreaterThan(0);
65
+ expect(config.positionCorrection).toBeGreaterThan(0);
66
+ expect(config.positionCorrection).toBeLessThan(1);
67
+ });
68
+ });
69
+
70
+ describe('createPhysics2DWorld', () => {
71
+ it('starts empty with a default broadphase index', () => {
72
+ const world = createPhysics2DWorld();
73
+ expect(world.bodies).toHaveLength(0);
74
+ expect(world.contacts).toHaveLength(0);
75
+ expect(world.index).toBeDefined();
76
+ expect(world.gravityY).toBeLessThan(0);
77
+ });
78
+ });
79
+
80
+ describe('createRigidBody2D', () => {
81
+ it('creates a body at rest with no mass until a world derives it from the colliders', () => {
82
+ const body = createRigidBody2D('dynamic', 3, 4, 0.5);
83
+ expect(body.x).toBe(3);
84
+ expect(body.angle).toBe(0.5);
85
+ expect(body.mass).toBe(0);
86
+ expect(body.index).toBe(-1);
87
+ });
88
+ });
89
+
90
+ describe('findPhysics2DBody', () => {
91
+ it('resolves a contact stored index back to its body', () => {
92
+ const world = createPhysics2DWorld();
93
+ const first = addPhysics2DBody(world, boxBody(0, 0));
94
+ const second = addPhysics2DBody(world, boxBody(1, 0));
95
+ expect(findPhysics2DBody(world, first.index)).toBe(first);
96
+ expect(findPhysics2DBody(world, second.index)).toBe(second);
97
+ });
98
+
99
+ it('reports null once the body is gone rather than resurrecting a slot', () => {
100
+ // Contacts hold indices, not references, so a lookup after removal has to fail cleanly — returning
101
+ // whatever now occupies the position would warm-start a pair with a stranger's impulse.
102
+ const world = createPhysics2DWorld();
103
+ const first = addPhysics2DBody(world, boxBody(0, 0));
104
+ addPhysics2DBody(world, boxBody(1, 0));
105
+ removePhysics2DBody(world, first);
106
+ expect(findPhysics2DBody(world, first.index)).toBeNull();
107
+ });
108
+ });
109
+
110
+ describe('isPhysics2DPairOrdered', () => {
111
+ it('orders by persistent index, which is the only key that survives motion', () => {
112
+ const world = createPhysics2DWorld();
113
+ const first = addPhysics2DBody(world, boxBody(0, 0));
114
+ const second = addPhysics2DBody(world, boxBody(1, 0));
115
+ expect(isPhysics2DPairOrdered(first, second)).toBe(true);
116
+ expect(isPhysics2DPairOrdered(second, first)).toBe(false);
117
+
118
+ // Moving a body past the other must not change the order — geometry-derived orderings flip here.
119
+ first.x = 100;
120
+ expect(isPhysics2DPairOrdered(first, second)).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe('removePhysics2DBody', () => {
125
+ it('drops every contact naming the removed body', () => {
126
+ const world = createPhysics2DWorld();
127
+ const floor = createRigidBody2D('static', 0, 0);
128
+ floor.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -5, minY: -1, maxX: 5, maxY: 0 }, STONE));
129
+ addPhysics2DBody(world, floor);
130
+ const crate = addPhysics2DBody(world, boxBody(0, 0.4));
131
+ stepPhysics2D(world, 1 / 60);
132
+ expect(world.contacts.length).toBeGreaterThan(0);
133
+
134
+ expect(removePhysics2DBody(world, crate)).toBe(true);
135
+ expect(world.contacts).toHaveLength(0);
136
+ expect(world.bodies).toHaveLength(1);
137
+ });
138
+
139
+ it('reports false for a body the world does not hold', () => {
140
+ expect(removePhysics2DBody(createPhysics2DWorld(), boxBody(0, 0))).toBe(false);
141
+ });
142
+ });