@flighthq/physics2d 0.4.0-next.1815.e47cc3c → 0.4.0-next.1821.efb1cbc
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/enablePhysics2DGuards.d.ts.map +1 -1
- package/dist/enablePhysics2DGuards.js +20 -29
- package/dist/enablePhysics2DGuards.js.map +1 -1
- package/dist/explainPhysics2DCollision.d.ts.map +1 -1
- package/dist/explainPhysics2DCollision.js +10 -8
- package/dist/explainPhysics2DCollision.js.map +1 -1
- package/package.json +6 -6
- package/src/enablePhysics2DGuards.test.ts +12 -16
- package/src/explainPhysics2DCollision.test.ts +9 -8
- package/src/step.test.ts +93 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enablePhysics2DGuards.d.ts","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"AAaA,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAK7C;
|
|
1
|
+
{"version":3,"file":"enablePhysics2DGuards.d.ts","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"AAaA,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAK7C;AAyBD,wBAAgB,qBAAqB,IAAI,IAAI,CAK5C"}
|
|
@@ -29,10 +29,11 @@ export function disablePhysics2DGuards() {
|
|
|
29
29
|
// precisely so unused solvers tree-shake out, which makes forgetting `registerBuiltInPhysics2DJointSolvers`
|
|
30
30
|
// an ordinary mistake rather than an exotic one.
|
|
31
31
|
//
|
|
32
|
-
// The third is a collider the contact dispatcher has no arm for
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// non-overlapping. A body carrying one falls
|
|
32
|
+
// The third is a collider the contact dispatcher has no arm for — in practice an area-less `segment` or
|
|
33
|
+
// `point` used as a rigid body's shape. `collideContactManifold2D` is a closed switch, so unlike 3D there
|
|
34
|
+
// is nothing to REGISTER and no way to forget to, but its coverage is narrower than the collider type
|
|
35
|
+
// allows and a kind it does not answer for is reported as non-overlapping. A body carrying one falls
|
|
36
|
+
// through the floor with nothing failing anywhere.
|
|
36
37
|
//
|
|
37
38
|
// Applications that never import this module shed both the message text and `@flighthq/log`.
|
|
38
39
|
export function enablePhysics2DGuards() {
|
|
@@ -41,31 +42,6 @@ export function enablePhysics2DGuards() {
|
|
|
41
42
|
setPhysics2DContactIntakeGuard(warnOnUndetectablePhysics2DColliders);
|
|
42
43
|
physics2DGuardsEnabled = true;
|
|
43
44
|
}
|
|
44
|
-
// Warns once per distinct set of undetectable collider kinds.
|
|
45
|
-
//
|
|
46
|
-
// The advice differs by kind, because the two causes are not the same problem. `segment` and `point` are
|
|
47
|
-
// area-less by definition and carry no contact to find, so putting one on a rigid body is a modelling
|
|
48
|
-
// mistake with no fix in this package. A `capsule` has no pair functions YET, which is a gap rather than
|
|
49
|
-
// a rule — and a caller deserves to be told which of the two it has hit.
|
|
50
|
-
function warnOnUndetectablePhysics2DColliders(world) {
|
|
51
|
-
const explanation = explainPhysics2DCollision(world);
|
|
52
|
-
if (explanation.status === 'ready')
|
|
53
|
-
return;
|
|
54
|
-
const kinds = explanation.unsupportedKinds;
|
|
55
|
-
const arealess = kinds.filter((kind) => kind === 'segment' || kind === 'point');
|
|
56
|
-
const unimplemented = kinds.filter((kind) => kind !== 'segment' && kind !== 'point');
|
|
57
|
-
const areaFix = arealess.length > 0
|
|
58
|
-
? ` ${arealess.join(' and ')} carr${arealess.length === 1 ? 'ies' : 'y'} no area and so no contact, and cannot be a rigid body's collider — give the body a shape with area.`
|
|
59
|
-
: '';
|
|
60
|
-
const gapFix = unimplemented.length > 0
|
|
61
|
-
? ` collideContactManifold2D has no pair functions for ${unimplemented.join(', ')} yet, so ${unimplemented.length === 1 ? 'it is' : 'they are'} usable for queries and raycasts but not as a simulated collider.`
|
|
62
|
-
: '';
|
|
63
|
-
logOnce(`physics2d:${explanation.status}:${kinds.join(',')}`, LogLevel.Warn, {
|
|
64
|
-
kinds,
|
|
65
|
-
message: `stepPhysics2D: collider kind${kinds.length === 1 ? '' : 's'} ${kinds.join(', ')} generate${kinds.length === 1 ? 's' : ''} no contacts, so the bodies carrying ${kinds.length === 1 ? 'it' : 'them'} pass through everything.${areaFix}${gapFix} Call explainPhysics2DCollision(world) for the same finding as data.`,
|
|
66
|
-
status: explanation.status,
|
|
67
|
-
}, 'physics2d');
|
|
68
|
-
}
|
|
69
45
|
// The names of the flags that are false, in the explanation's own field order so the key is stable for a
|
|
70
46
|
// given set of faults however they arose.
|
|
71
47
|
function getFailingPhysics2DPreconditions(explanation) {
|
|
@@ -90,6 +66,21 @@ function getUnresolvedPhysics2DJointFaults(explanation) {
|
|
|
90
66
|
}
|
|
91
67
|
return [...faults].sort();
|
|
92
68
|
}
|
|
69
|
+
// Warns once per distinct set of undetectable collider kinds.
|
|
70
|
+
//
|
|
71
|
+
// Keyed on the kinds rather than logged per step, so a 60Hz loop says this once — and a world that later
|
|
72
|
+
// gains a second undetectable kind still says so.
|
|
73
|
+
function warnOnUndetectablePhysics2DColliders(world) {
|
|
74
|
+
const explanation = explainPhysics2DCollision(world);
|
|
75
|
+
if (explanation.status === 'ready')
|
|
76
|
+
return;
|
|
77
|
+
const kinds = explanation.unsupportedKinds;
|
|
78
|
+
logOnce(`physics2d:${explanation.status}:${kinds.join(',')}`, LogLevel.Warn, {
|
|
79
|
+
kinds,
|
|
80
|
+
message: `stepPhysics2D: collider kind${kinds.length === 1 ? '' : 's'} ${kinds.join(', ')} enclose${kinds.length === 1 ? 's' : ''} no area and so generate${kinds.length === 1 ? 's' : ''} no contacts, and the bodies carrying ${kinds.length === 1 ? 'it' : 'them'} pass through everything — give the body a shape with area. Call explainPhysics2DCollision(world) for the same finding as data.`,
|
|
81
|
+
status: explanation.status,
|
|
82
|
+
}, 'physics2d');
|
|
83
|
+
}
|
|
93
84
|
// Warns once per distinct set of unresolved joint faults.
|
|
94
85
|
//
|
|
95
86
|
// The two fault families need different advice, so the message names whichever applies rather than
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enablePhysics2DGuards.js","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAMjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,8BAA8B,EAAE,gCAAgC,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAEjH,MAAM,UAAU,yBAAyB;IACvC,OAAO,sBAAsB,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAC5B,gCAAgC,CAAC,IAAI,CAAC,CAAC;IACvC,8BAA8B,CAAC,IAAI,CAAC,CAAC;IACrC,sBAAsB,GAAG,KAAK,CAAC;AACjC,CAAC;AAED,yGAAyG;AACzG,EAAE;AACF,wGAAwG;AACxG,uGAAuG;AACvG,yGAAyG;AACzG,yGAAyG;AACzG,aAAa;AACb,EAAE;AACF,0GAA0G;AAC1G,mGAAmG;AACnG,uGAAuG;AACvG,qGAAqG;AACrG,qGAAqG;AACrG,4GAA4G;AAC5G,iDAAiD;AACjD,EAAE;AACF,wGAAwG;AACxG,
|
|
1
|
+
{"version":3,"file":"enablePhysics2DGuards.js","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"explainPhysics2DCollision.d.ts","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwB,6BAA6B,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"explainPhysics2DCollision.d.ts","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwB,6BAA6B,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAiBpH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,6BAA6B,CAaxG"}
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
//
|
|
4
4
|
// This exists because the dispatcher's coverage is NARROWER than the collider type allows. Every kind in
|
|
5
5
|
// `CollisionBuiltInShape2D` may be authored onto a body, but `collideContactManifold2D` answers only for
|
|
6
|
-
// the
|
|
7
|
-
//
|
|
8
|
-
// find — and it is a GAP for `capsule`, whose pair functions are not written yet.
|
|
6
|
+
// the shapes that enclose an area; `segment` and `point` are area-less by definition and carry no contact
|
|
7
|
+
// to find, so a body built from one is reported as touching nothing and falls through the world.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// That is a modelling mistake with no fix inside this package, and it is invisible from inside the
|
|
10
|
+
// simulation — nothing fails, nothing is slow, the body simply never collides. Which is exactly why the
|
|
11
|
+
// question has to be answerable from outside it.
|
|
12
|
+
//
|
|
13
|
+
// The list stays a general query rather than a hardcoded "segment or point" test, because the answer is a
|
|
14
|
+
// property of the dispatcher and not of this file: a kind added to the collider union without pair
|
|
15
|
+
// functions would show up here on its own, which is how the capsule surfaced before its were written.
|
|
14
16
|
export function explainPhysics2DCollision(world) {
|
|
15
17
|
const unsupported = new Set();
|
|
16
18
|
for (const body of world.bodies) {
|
|
@@ -30,6 +32,6 @@ export function explainPhysics2DCollision(world) {
|
|
|
30
32
|
// through the dispatcher because asking the dispatcher would mean synthesizing a shape of each kind to
|
|
31
33
|
// test with, and a diagnostic must not be able to change what it measures.
|
|
32
34
|
function isPhysics2DContactSupportedKind(kind) {
|
|
33
|
-
return kind === 'circle' || kind === 'aabb' || kind === 'obb' || kind === 'polygon';
|
|
35
|
+
return kind === 'circle' || kind === 'capsule' || kind === 'aabb' || kind === 'obb' || kind === 'polygon';
|
|
34
36
|
}
|
|
35
37
|
//# sourceMappingURL=explainPhysics2DCollision.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"explainPhysics2DCollision.js","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAEA,wGAAwG;AACxG,+CAA+C;AAC/C,EAAE;AACF,yGAAyG;AACzG,yGAAyG;AACzG,
|
|
1
|
+
{"version":3,"file":"explainPhysics2DCollision.js","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAEA,wGAAwG;AACxG,+CAA+C;AAC/C,EAAE;AACF,yGAAyG;AACzG,yGAAyG;AACzG,0GAA0G;AAC1G,iGAAiG;AACjG,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,iDAAiD;AACjD,EAAE;AACF,0GAA0G;AAC1G,mGAAmG;AACnG,sGAAsG;AACtG,MAAM,UAAU,yBAAyB,CAAC,KAA+B;IACvE,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,+BAA+B,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnE,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,MAAM,gBAAgB,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO;QACL,MAAM,EAAE,gBAAgB,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB;QAC3E,gBAAgB;KACjB,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,uGAAuG;AACvG,2EAA2E;AAC3E,SAAS,+BAA+B,CAAC,IAA0B;IACjE,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,SAAS,CAAC;AAC5G,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/physics2d",
|
|
3
|
-
"version": "0.4.0-next.
|
|
3
|
+
"version": "0.4.0-next.1821.efb1cbc",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/collision": "0.4.0-next.
|
|
42
|
-
"@flighthq/log": "0.4.0-next.
|
|
43
|
-
"@flighthq/math": "0.4.0-next.
|
|
44
|
-
"@flighthq/spatial": "0.4.0-next.
|
|
45
|
-
"@flighthq/types": "0.4.0-next.
|
|
41
|
+
"@flighthq/collision": "0.4.0-next.1821.efb1cbc",
|
|
42
|
+
"@flighthq/log": "0.4.0-next.1821.efb1cbc",
|
|
43
|
+
"@flighthq/math": "0.4.0-next.1821.efb1cbc",
|
|
44
|
+
"@flighthq/spatial": "0.4.0-next.1821.efb1cbc",
|
|
45
|
+
"@flighthq/types": "0.4.0-next.1821.efb1cbc"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"typescript": "^5.3.0"
|
|
@@ -102,14 +102,13 @@ describe('disablePhysics2DGuards', () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe('enablePhysics2DGuards', () => {
|
|
105
|
-
it('reports
|
|
106
|
-
// The third seam
|
|
107
|
-
//
|
|
108
|
-
// through the world with nothing failing anywhere.
|
|
105
|
+
it('reports an area-less collider used as a body shape', () => {
|
|
106
|
+
// The third seam. A segment on a rigid body is a modelling mistake the simulation cannot show you:
|
|
107
|
+
// nothing fails, nothing is slow, the body simply never collides with anything.
|
|
109
108
|
enablePhysics2DGuards();
|
|
110
109
|
const world = createPhysics2DWorld(0, -10);
|
|
111
110
|
const body = createRigidBody2D('dynamic', 0, 0);
|
|
112
|
-
body.colliders.push(createPhysics2DCollider({ kind: '
|
|
111
|
+
body.colliders.push(createPhysics2DCollider({ kind: 'segment', x0: 0, y0: 0, x1: 1, y1: 0 }, STONE));
|
|
113
112
|
addPhysics2DBody(world, body);
|
|
114
113
|
|
|
115
114
|
const entries = captureLog(() => stepPhysics2D(world, 1 / 60));
|
|
@@ -117,24 +116,21 @@ describe('enablePhysics2DGuards', () => {
|
|
|
117
116
|
expect(entries).toHaveLength(1);
|
|
118
117
|
const data = entries[0].data as { kinds: string[]; message: string; status: string };
|
|
119
118
|
expect(data.status).toBe('missing-contact-support');
|
|
120
|
-
expect(data.kinds).toEqual(['
|
|
121
|
-
|
|
122
|
-
expect(data.message).toContain('no pair functions');
|
|
123
|
-
expect(data.message).not.toContain('no area');
|
|
119
|
+
expect(data.kinds).toEqual(['segment']);
|
|
120
|
+
expect(data.message).toContain('no area');
|
|
124
121
|
});
|
|
125
122
|
|
|
126
|
-
it('
|
|
123
|
+
it('says nothing about a capsule, which the dispatcher answers for', () => {
|
|
124
|
+
// This test is the inverse of one that used to assert the opposite. The capsule was named by this
|
|
125
|
+
// guard while its pair functions were missing and stopped being named when they landed, which is the
|
|
126
|
+
// seam working in both directions rather than a list someone edits by hand.
|
|
127
127
|
enablePhysics2DGuards();
|
|
128
128
|
const world = createPhysics2DWorld(0, -10);
|
|
129
129
|
const body = createRigidBody2D('dynamic', 0, 0);
|
|
130
|
-
body.colliders.push(createPhysics2DCollider({ kind: '
|
|
130
|
+
body.colliders.push(createPhysics2DCollider({ kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.4 }, STONE));
|
|
131
131
|
addPhysics2DBody(world, body);
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const data = entries[0].data as { message: string };
|
|
136
|
-
expect(data.message).toContain('no area');
|
|
137
|
-
expect(data.message).not.toContain('no pair functions');
|
|
133
|
+
expect(captureLog(() => stepPhysics2D(world, 1 / 60))).toHaveLength(0);
|
|
138
134
|
});
|
|
139
135
|
|
|
140
136
|
it('says nothing about colliders the dispatcher does answer for', () => {
|
|
@@ -24,6 +24,7 @@ describe('explainPhysics2DCollision', () => {
|
|
|
24
24
|
const world = worldWith(
|
|
25
25
|
{ kind: 'circle', x: 0, y: 0, radius: 1 },
|
|
26
26
|
{ kind: 'aabb', minX: -1, minY: -1, maxX: 1, maxY: 1 },
|
|
27
|
+
{ kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.5 },
|
|
27
28
|
{ kind: 'polygon', points: [0, 0, 1, 0, 1, 1] },
|
|
28
29
|
);
|
|
29
30
|
expect(explainPhysics2DCollision(world)).toEqual({ status: 'ready', unsupportedKinds: [] });
|
|
@@ -33,12 +34,12 @@ describe('explainPhysics2DCollision', () => {
|
|
|
33
34
|
expect(explainPhysics2DCollision(createPhysics2DWorld(0, -10)).status).toBe('ready');
|
|
34
35
|
});
|
|
35
36
|
|
|
36
|
-
it('
|
|
37
|
+
it('reports ready for a capsule, which the dispatcher now answers for', () => {
|
|
38
|
+
// This test previously asserted the opposite, and the change is the point: the capsule was named
|
|
39
|
+
// here while its pair functions were missing, and it stopped being named when they landed. That is
|
|
40
|
+
// the seam doing its job in both directions rather than a list someone has to remember to edit.
|
|
37
41
|
const world = worldWith({ kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.5 });
|
|
38
|
-
expect(explainPhysics2DCollision(world)).toEqual({
|
|
39
|
-
status: 'missing-contact-support',
|
|
40
|
-
unsupportedKinds: ['capsule'],
|
|
41
|
-
});
|
|
42
|
+
expect(explainPhysics2DCollision(world)).toEqual({ status: 'ready', unsupportedKinds: [] });
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
it('names the area-less kinds too, since they are equally invisible to the solver', () => {
|
|
@@ -50,11 +51,11 @@ describe('explainPhysics2DCollision', () => {
|
|
|
50
51
|
// A level with four hundred capsules is one mistake. Sorted so the answer does not depend on the
|
|
51
52
|
// order bodies were added, which would make the same world report differently twice.
|
|
52
53
|
const world = worldWith(
|
|
54
|
+
{ kind: 'point', x: 0, y: 0 },
|
|
53
55
|
{ kind: 'segment', x0: 0, y0: 0, x1: 1, y1: 0 },
|
|
54
|
-
{ kind: '
|
|
55
|
-
{ kind: 'capsule', x0: 2, y0: 0, x1: 3, y1: 0, radius: 0.5 },
|
|
56
|
+
{ kind: 'segment', x0: 2, y0: 0, x1: 3, y1: 0 },
|
|
56
57
|
{ kind: 'circle', x: 0, y: 0, radius: 1 },
|
|
57
58
|
);
|
|
58
|
-
expect(explainPhysics2DCollision(world).unsupportedKinds).toEqual(['
|
|
59
|
+
expect(explainPhysics2DCollision(world).unsupportedKinds).toEqual(['point', 'segment']);
|
|
59
60
|
});
|
|
60
61
|
});
|
package/src/step.test.ts
CHANGED
|
@@ -1611,6 +1611,99 @@ describe('stepPhysics2D with breakable joints', () => {
|
|
|
1611
1611
|
});
|
|
1612
1612
|
});
|
|
1613
1613
|
|
|
1614
|
+
describe('stepPhysics2D with capsule colliders', () => {
|
|
1615
|
+
// The behaviour the capsule's two-point floor manifold exists for. A capsule held by a single contact
|
|
1616
|
+
// point pivots about it, and a player sees a character that will not stand still — which is why this is
|
|
1617
|
+
// tested through the STEP rather than only at the manifold.
|
|
1618
|
+
it('settles a horizontal capsule flat on the ground without rocking, and lets it sleep', () => {
|
|
1619
|
+
const world = createPhysics2DWorld(0, -10);
|
|
1620
|
+
const floor = createRigidBody2D('static', 0, 0);
|
|
1621
|
+
floor.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -20, minY: -1, maxX: 20, maxY: 0 }, STONE));
|
|
1622
|
+
addPhysics2DBody(world, floor);
|
|
1623
|
+
const capsule = createRigidBody2D('dynamic', 0, 2);
|
|
1624
|
+
capsule.colliders.push(
|
|
1625
|
+
createPhysics2DCollider({ kind: 'capsule', x0: -1, y0: 0, x1: 1, y1: 0, radius: 0.5 }, STONE),
|
|
1626
|
+
);
|
|
1627
|
+
addPhysics2DBody(world, capsule);
|
|
1628
|
+
|
|
1629
|
+
let worstAngleAfterSettling = 0;
|
|
1630
|
+
for (let step = 0; step < 900; step++) {
|
|
1631
|
+
stepPhysics2D(world, 1 / 240);
|
|
1632
|
+
if (step > 500) worstAngleAfterSettling = Math.max(worstAngleAfterSettling, Math.abs(capsule.angle));
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// Resting on its radius, less the solver's penetration slop.
|
|
1636
|
+
expect(capsule.y).toBeCloseTo(0.5, 2);
|
|
1637
|
+
// Flat, and not merely flat at the last sample: nothing after settling rotated it either.
|
|
1638
|
+
expect(worstAngleAfterSettling).toBeLessThan(1e-6);
|
|
1639
|
+
expect(capsule.sleeping).toBe(true);
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
it('stops a capsule bullet at a thin wall instead of tunnelling through it', () => {
|
|
1643
|
+
// The reason the capsule needed a sweep arm at all. Without one the CCD path finds no impact for a
|
|
1644
|
+
// capsule and the body teleports past the wall in a single step — and it fails SILENTLY, because a
|
|
1645
|
+
// sweep that reports no hit is indistinguishable from a clear path.
|
|
1646
|
+
const world = createPhysics2DWorld(0, 0);
|
|
1647
|
+
world.config.continuousCollision = true;
|
|
1648
|
+
const wall = createRigidBody2D('static', 0, 0);
|
|
1649
|
+
wall.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: 9.9, minY: -5, maxX: 10, maxY: 5 }, STONE));
|
|
1650
|
+
addPhysics2DBody(world, wall);
|
|
1651
|
+
const bullet = createRigidBody2D('dynamic', 0, 0);
|
|
1652
|
+
bullet.bullet = true;
|
|
1653
|
+
bullet.colliders.push(
|
|
1654
|
+
createPhysics2DCollider({ kind: 'capsule', x0: -0.2, y0: 0, x1: 0.2, y1: 0, radius: 0.1 }, STONE),
|
|
1655
|
+
);
|
|
1656
|
+
addPhysics2DBody(world, bullet);
|
|
1657
|
+
// Fast enough to clear the wall's whole thickness in one step several times over.
|
|
1658
|
+
bullet.velocityX = 600;
|
|
1659
|
+
|
|
1660
|
+
for (let step = 0; step < 10; step++) stepPhysics2D(world, 1 / 60);
|
|
1661
|
+
|
|
1662
|
+
expect(bullet.x).toBeLessThan(10);
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
it('rolls a capsule down a slope rather than sticking on a corner', () => {
|
|
1666
|
+
// A rounded end on an inclined face is the corner case the vertex axes exist for. With the wrong
|
|
1667
|
+
// contact normal the capsule catches on the slope instead of sliding down it.
|
|
1668
|
+
const world = createPhysics2DWorld(0, -10);
|
|
1669
|
+
const slope = createRigidBody2D('static', 0, 0);
|
|
1670
|
+
slope.angle = -0.3;
|
|
1671
|
+
slope.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -20, minY: -1, maxX: 20, maxY: 0 }, STONE));
|
|
1672
|
+
addPhysics2DBody(world, slope);
|
|
1673
|
+
const capsule = createRigidBody2D('dynamic', -2, 2);
|
|
1674
|
+
capsule.colliders.push(
|
|
1675
|
+
createPhysics2DCollider({ kind: 'capsule', x0: -0.4, y0: 0, x1: 0.4, y1: 0, radius: 0.3 }, STONE),
|
|
1676
|
+
);
|
|
1677
|
+
addPhysics2DBody(world, capsule);
|
|
1678
|
+
|
|
1679
|
+
for (let step = 0; step < 600; step++) stepPhysics2D(world, 1 / 240);
|
|
1680
|
+
|
|
1681
|
+
expect(Number.isFinite(capsule.x)).toBe(true);
|
|
1682
|
+
// Downhill is +x for a slope tilted this way, and it must not have sunk through.
|
|
1683
|
+
expect(capsule.x).toBeGreaterThan(-2);
|
|
1684
|
+
expect(capsule.y).toBeGreaterThan(-2);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
it('stacks a capsule on a capsule without either falling through', () => {
|
|
1688
|
+
const world = createPhysics2DWorld(0, -10);
|
|
1689
|
+
const floor = createRigidBody2D('static', 0, 0);
|
|
1690
|
+
floor.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -20, minY: -1, maxX: 20, maxY: 0 }, STONE));
|
|
1691
|
+
addPhysics2DBody(world, floor);
|
|
1692
|
+
const lower = createRigidBody2D('dynamic', 0, 1);
|
|
1693
|
+
lower.colliders.push(createPhysics2DCollider({ kind: 'capsule', x0: -1, y0: 0, x1: 1, y1: 0, radius: 0.5 }, STONE));
|
|
1694
|
+
addPhysics2DBody(world, lower);
|
|
1695
|
+
const upper = createRigidBody2D('dynamic', 0, 2.5);
|
|
1696
|
+
upper.colliders.push(createPhysics2DCollider({ kind: 'capsule', x0: -1, y0: 0, x1: 1, y1: 0, radius: 0.5 }, STONE));
|
|
1697
|
+
addPhysics2DBody(world, upper);
|
|
1698
|
+
|
|
1699
|
+
for (let step = 0; step < 1200; step++) stepPhysics2D(world, 1 / 240);
|
|
1700
|
+
|
|
1701
|
+
expect(lower.y).toBeCloseTo(0.5, 1);
|
|
1702
|
+
expect(upper.y).toBeCloseTo(1.5, 1);
|
|
1703
|
+
expect(upper.y).toBeGreaterThan(lower.y + 0.9);
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1614
1707
|
describe('stepPhysics2D with joints', () => {
|
|
1615
1708
|
const DISTANCE = 'Distance';
|
|
1616
1709
|
|