@series-inc/rundot-syncplay 5.23.0-beta.7
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/README.md +610 -0
- package/dist/animation.d.ts +119 -0
- package/dist/animation.js +488 -0
- package/dist/asset-cooking.d.ts +82 -0
- package/dist/asset-cooking.js +364 -0
- package/dist/authority-room.d.ts +217 -0
- package/dist/authority-room.js +701 -0
- package/dist/bot-documents.d.ts +115 -0
- package/dist/bot-documents.js +549 -0
- package/dist/bot-primitives.d.ts +152 -0
- package/dist/bot-primitives.js +472 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +53 -0
- package/dist/canonical.d.ts +12 -0
- package/dist/canonical.js +288 -0
- package/dist/certification.d.ts +455 -0
- package/dist/certification.js +1855 -0
- package/dist/chaos-certification.d.ts +56 -0
- package/dist/chaos-certification.js +1397 -0
- package/dist/cjs/animation.js +495 -0
- package/dist/cjs/asset-cooking.js +372 -0
- package/dist/cjs/authority-room.js +704 -0
- package/dist/cjs/bot-documents.js +556 -0
- package/dist/cjs/bot-primitives.js +482 -0
- package/dist/cjs/browser.js +96 -0
- package/dist/cjs/canonical.js +299 -0
- package/dist/cjs/certification.js +1860 -0
- package/dist/cjs/chaos-certification.js +1402 -0
- package/dist/cjs/collections.js +36 -0
- package/dist/cjs/collision.js +96 -0
- package/dist/cjs/commands.js +987 -0
- package/dist/cjs/config-bundle.js +491 -0
- package/dist/cjs/creator.js +30 -0
- package/dist/cjs/deterministic-primitives.js +134 -0
- package/dist/cjs/deterministic-session.js +1410 -0
- package/dist/cjs/ecs-lite.js +155 -0
- package/dist/cjs/effects-adapter.js +97 -0
- package/dist/cjs/engine-complete.js +2297 -0
- package/dist/cjs/engine-core-proof.js +70 -0
- package/dist/cjs/engine-parity-matrix.js +874 -0
- package/dist/cjs/engine-parity.js +3183 -0
- package/dist/cjs/engine-runtime.js +3141 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/events.js +247 -0
- package/dist/cjs/examples/counter-duel.js +157 -0
- package/dist/cjs/examples/fixed-arena.js +368 -0
- package/dist/cjs/index.js +229 -0
- package/dist/cjs/input-authority.js +150 -0
- package/dist/cjs/input-button.js +256 -0
- package/dist/cjs/input-timeline.js +418 -0
- package/dist/cjs/instant-replay.js +66 -0
- package/dist/cjs/lag-compensation3d.js +837 -0
- package/dist/cjs/local-authority-server-runner.js +1121 -0
- package/dist/cjs/local-authority-wire.js +1497 -0
- package/dist/cjs/local-authority.js +817 -0
- package/dist/cjs/local-ws-adapter.js +150 -0
- package/dist/cjs/match-log.js +61 -0
- package/dist/cjs/math.js +157 -0
- package/dist/cjs/mock-network-adapter.js +26 -0
- package/dist/cjs/movement.js +703 -0
- package/dist/cjs/movement3d.js +1246 -0
- package/dist/cjs/multiclient-authority.js +308 -0
- package/dist/cjs/multiplayer-service.js +434 -0
- package/dist/cjs/navigation.js +347 -0
- package/dist/cjs/navmesh.js +1437 -0
- package/dist/cjs/networked-client.js +691 -0
- package/dist/cjs/networked-input-authority.js +443 -0
- package/dist/cjs/node.js +47 -0
- package/dist/cjs/number-lab.js +78 -0
- package/dist/cjs/offline-session.js +370 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pause-resume.js +43 -0
- package/dist/cjs/physics-cert.js +426 -0
- package/dist/cjs/physics2d.js +751 -0
- package/dist/cjs/physics3d.js +1996 -0
- package/dist/cjs/prediction-culling.js +203 -0
- package/dist/cjs/protocol-lab.js +1299 -0
- package/dist/cjs/r3f-render-adapter.js +209 -0
- package/dist/cjs/random.js +59 -0
- package/dist/cjs/replay-bundle.js +1598 -0
- package/dist/cjs/replay-inspect.js +236 -0
- package/dist/cjs/replay-scoring.js +68 -0
- package/dist/cjs/replay.js +356 -0
- package/dist/cjs/reports.js +181 -0
- package/dist/cjs/rollback-history.js +251 -0
- package/dist/cjs/runtime-guards.js +111 -0
- package/dist/cjs/sample-lab.js +145 -0
- package/dist/cjs/sample-scenes.js +752 -0
- package/dist/cjs/schema-artifacts.js +359 -0
- package/dist/cjs/schema-codegen.js +544 -0
- package/dist/cjs/schema-dsl-binary.js +655 -0
- package/dist/cjs/schema.js +319 -0
- package/dist/cjs/sdk-offline.js +99 -0
- package/dist/cjs/session-promotion.js +74 -0
- package/dist/cjs/session-snapshot.js +265 -0
- package/dist/cjs/session-wire.js +216 -0
- package/dist/cjs/signals.js +103 -0
- package/dist/cjs/sparse-set.js +169 -0
- package/dist/cjs/synctest.js +136 -0
- package/dist/cjs/system-lifecycle.js +286 -0
- package/dist/cjs/timers.js +61 -0
- package/dist/cjs/tools/static-checker.js +1468 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/venus-mp-budget.js +125 -0
- package/dist/cjs/ws-frame.js +255 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11379 -0
- package/dist/collections.d.ts +9 -0
- package/dist/collections.js +30 -0
- package/dist/collision.d.ts +25 -0
- package/dist/collision.js +85 -0
- package/dist/commands.d.ts +137 -0
- package/dist/commands.js +977 -0
- package/dist/config-bundle.d.ts +89 -0
- package/dist/config-bundle.js +483 -0
- package/dist/creator.d.ts +20 -0
- package/dist/creator.js +11 -0
- package/dist/deterministic-primitives.d.ts +59 -0
- package/dist/deterministic-primitives.js +126 -0
- package/dist/deterministic-session.d.ts +197 -0
- package/dist/deterministic-session.js +1401 -0
- package/dist/ecs-lite.d.ts +26 -0
- package/dist/ecs-lite.js +152 -0
- package/dist/effects-adapter.d.ts +21 -0
- package/dist/effects-adapter.js +94 -0
- package/dist/engine-complete.d.ts +45 -0
- package/dist/engine-complete.js +2292 -0
- package/dist/engine-core-proof.d.ts +26 -0
- package/dist/engine-core-proof.js +66 -0
- package/dist/engine-parity-matrix.d.ts +300 -0
- package/dist/engine-parity-matrix.js +869 -0
- package/dist/engine-parity.d.ts +74 -0
- package/dist/engine-parity.js +3178 -0
- package/dist/engine-runtime.d.ts +351 -0
- package/dist/engine-runtime.js +3123 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +45 -0
- package/dist/events.js +242 -0
- package/dist/examples/counter-duel.d.ts +43 -0
- package/dist/examples/counter-duel.js +150 -0
- package/dist/examples/fixed-arena.d.ts +110 -0
- package/dist/examples/fixed-arena.js +356 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +66 -0
- package/dist/input-authority.d.ts +82 -0
- package/dist/input-authority.js +147 -0
- package/dist/input-button.d.ts +36 -0
- package/dist/input-button.js +249 -0
- package/dist/input-timeline.d.ts +65 -0
- package/dist/input-timeline.js +412 -0
- package/dist/instant-replay.d.ts +59 -0
- package/dist/instant-replay.js +62 -0
- package/dist/lag-compensation3d.d.ts +146 -0
- package/dist/lag-compensation3d.js +827 -0
- package/dist/local-authority-server-runner.d.ts +79 -0
- package/dist/local-authority-server-runner.js +1118 -0
- package/dist/local-authority-wire.d.ts +142 -0
- package/dist/local-authority-wire.js +1477 -0
- package/dist/local-authority.d.ts +160 -0
- package/dist/local-authority.js +810 -0
- package/dist/local-ws-adapter.d.ts +14 -0
- package/dist/local-ws-adapter.js +146 -0
- package/dist/match-log.d.ts +74 -0
- package/dist/match-log.js +57 -0
- package/dist/math.d.ts +41 -0
- package/dist/math.js +154 -0
- package/dist/mock-network-adapter.d.ts +13 -0
- package/dist/mock-network-adapter.js +23 -0
- package/dist/movement.d.ts +155 -0
- package/dist/movement.js +694 -0
- package/dist/movement3d.d.ts +195 -0
- package/dist/movement3d.js +1238 -0
- package/dist/multiclient-authority.d.ts +30 -0
- package/dist/multiclient-authority.js +305 -0
- package/dist/multiplayer-service.d.ts +153 -0
- package/dist/multiplayer-service.js +429 -0
- package/dist/navigation.d.ts +91 -0
- package/dist/navigation.js +338 -0
- package/dist/navmesh.d.ts +236 -0
- package/dist/navmesh.js +1423 -0
- package/dist/networked-client.d.ts +124 -0
- package/dist/networked-client.js +688 -0
- package/dist/networked-input-authority.d.ts +51 -0
- package/dist/networked-input-authority.js +439 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +19 -0
- package/dist/number-lab.d.ts +15 -0
- package/dist/number-lab.js +75 -0
- package/dist/offline-session.d.ts +2 -0
- package/dist/offline-session.js +367 -0
- package/dist/pause-resume.d.ts +48 -0
- package/dist/pause-resume.js +40 -0
- package/dist/physics-cert.d.ts +40 -0
- package/dist/physics-cert.js +389 -0
- package/dist/physics2d.d.ts +142 -0
- package/dist/physics2d.js +741 -0
- package/dist/physics3d.d.ts +285 -0
- package/dist/physics3d.js +1979 -0
- package/dist/prediction-culling.d.ts +34 -0
- package/dist/prediction-culling.js +200 -0
- package/dist/protocol-lab.d.ts +115 -0
- package/dist/protocol-lab.js +1295 -0
- package/dist/r3f-render-adapter.d.ts +61 -0
- package/dist/r3f-render-adapter.js +203 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +55 -0
- package/dist/replay-bundle.d.ts +150 -0
- package/dist/replay-bundle.js +1586 -0
- package/dist/replay-inspect.d.ts +60 -0
- package/dist/replay-inspect.js +228 -0
- package/dist/replay-scoring.d.ts +28 -0
- package/dist/replay-scoring.js +65 -0
- package/dist/replay.d.ts +17 -0
- package/dist/replay.js +351 -0
- package/dist/reports.d.ts +176 -0
- package/dist/reports.js +174 -0
- package/dist/rollback-history.d.ts +46 -0
- package/dist/rollback-history.js +247 -0
- package/dist/runtime-guards.d.ts +1 -0
- package/dist/runtime-guards.js +108 -0
- package/dist/sample-lab.d.ts +32 -0
- package/dist/sample-lab.js +134 -0
- package/dist/sample-scenes.d.ts +86 -0
- package/dist/sample-scenes.js +745 -0
- package/dist/schema-artifacts.d.ts +49 -0
- package/dist/schema-artifacts.js +356 -0
- package/dist/schema-codegen.d.ts +21 -0
- package/dist/schema-codegen.js +538 -0
- package/dist/schema-dsl-binary.d.ts +75 -0
- package/dist/schema-dsl-binary.js +648 -0
- package/dist/schema.d.ts +87 -0
- package/dist/schema.js +315 -0
- package/dist/sdk-offline.d.ts +34 -0
- package/dist/sdk-offline.js +96 -0
- package/dist/session-promotion.d.ts +26 -0
- package/dist/session-promotion.js +70 -0
- package/dist/session-snapshot.d.ts +95 -0
- package/dist/session-snapshot.js +255 -0
- package/dist/session-wire.d.ts +89 -0
- package/dist/session-wire.js +211 -0
- package/dist/signals.d.ts +31 -0
- package/dist/signals.js +99 -0
- package/dist/sparse-set.d.ts +34 -0
- package/dist/sparse-set.js +165 -0
- package/dist/synctest.d.ts +31 -0
- package/dist/synctest.js +133 -0
- package/dist/system-lifecycle.d.ts +72 -0
- package/dist/system-lifecycle.js +281 -0
- package/dist/timers.d.ts +17 -0
- package/dist/timers.js +53 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/static-checker.d.ts +31 -0
- package/dist/tools/static-checker.js +1462 -0
- package/dist/tools/vite-plugin.d.ts +69 -0
- package/dist/tools/vite-plugin.js +203 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +1 -0
- package/dist/venus-mp-budget.d.ts +29 -0
- package/dist/venus-mp-budget.js +122 -0
- package/dist/ws-frame.d.ts +49 -0
- package/dist/ws-frame.js +247 -0
- package/package.json +169 -0
|
@@ -0,0 +1,1996 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deterministicPhysicsCallback3D = void 0;
|
|
4
|
+
exports.createDeterministicPhysicsWorld3D = createDeterministicPhysicsWorld3D;
|
|
5
|
+
exports.stepDeterministicPhysicsWorld3D = stepDeterministicPhysicsWorld3D;
|
|
6
|
+
exports.raycastDeterministicPhysics3D = raycastDeterministicPhysics3D;
|
|
7
|
+
exports.raycastDeterministicPhysics3DDetailed = raycastDeterministicPhysics3DDetailed;
|
|
8
|
+
exports.overlapDeterministicPhysics3D = overlapDeterministicPhysics3D;
|
|
9
|
+
exports.shapeCastDeterministicPhysics3D = shapeCastDeterministicPhysics3D;
|
|
10
|
+
exports.shapeCastDeterministicPhysics3DDetailed = shapeCastDeterministicPhysics3DDetailed;
|
|
11
|
+
exports.contactDetailsDeterministicPhysics3D = contactDetailsDeterministicPhysics3D;
|
|
12
|
+
exports.runDeterministicPhysics3DCallbackMaskFixture = runDeterministicPhysics3DCallbackMaskFixture;
|
|
13
|
+
exports.runDeterministicPhysics3DQueryOptionsFixture = runDeterministicPhysics3DQueryOptionsFixture;
|
|
14
|
+
exports.runDeterministicPhysics3DMaterialFixture = runDeterministicPhysics3DMaterialFixture;
|
|
15
|
+
exports.runDeterministicPhysics3DDetailFixture = runDeterministicPhysics3DDetailFixture;
|
|
16
|
+
exports.runDeterministicPhysics3DStress = runDeterministicPhysics3DStress;
|
|
17
|
+
exports.runDeterministicPhysics3DStackFixture = runDeterministicPhysics3DStackFixture;
|
|
18
|
+
const canonical_js_1 = require("./canonical.js");
|
|
19
|
+
const deterministic_primitives_js_1 = require("./deterministic-primitives.js");
|
|
20
|
+
exports.deterministicPhysicsCallback3D = {
|
|
21
|
+
collisionEnter: 1,
|
|
22
|
+
collisionStay: 2,
|
|
23
|
+
collisionExit: 4,
|
|
24
|
+
triggerEnter: 8,
|
|
25
|
+
triggerStay: 16,
|
|
26
|
+
triggerExit: 32,
|
|
27
|
+
};
|
|
28
|
+
const defaultPhysicsMaterial3D = {
|
|
29
|
+
friction: 0,
|
|
30
|
+
restitution: 0,
|
|
31
|
+
};
|
|
32
|
+
function createDeterministicPhysicsWorld3D(bodies) {
|
|
33
|
+
return {
|
|
34
|
+
frame: 0,
|
|
35
|
+
bodies: normalizeBodies(bodies),
|
|
36
|
+
activePairs: [],
|
|
37
|
+
activePairMetadata: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function stepDeterministicPhysicsWorld3D(world, options = {}) {
|
|
41
|
+
const scheduledQueries = sortScheduledQueries(options.scheduledQueries ?? []);
|
|
42
|
+
const prePhysicsScheduledQueryResults = scheduledQueries
|
|
43
|
+
.filter((query) => query.phase === 'pre-physics')
|
|
44
|
+
.map((query) => ({ id: query.id, hits: runScheduledQuery(world, query.query) }));
|
|
45
|
+
const dt = options.dtTicks ?? 1;
|
|
46
|
+
const gravityY = options.gravityY ?? 0;
|
|
47
|
+
const integrated = normalizeBodies(world.bodies.map((body) => {
|
|
48
|
+
if (body.kind !== 'dynamic') {
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
const vy = body.vy + gravityY * dt;
|
|
52
|
+
return {
|
|
53
|
+
...body,
|
|
54
|
+
x: body.x + body.vx * dt,
|
|
55
|
+
y: body.y + vy * dt,
|
|
56
|
+
z: body.z + body.vz * dt,
|
|
57
|
+
vy,
|
|
58
|
+
...integrateAngularState(body, dt),
|
|
59
|
+
};
|
|
60
|
+
}));
|
|
61
|
+
const constrained = solveConstraints3D(integrated, options.constraints ?? []);
|
|
62
|
+
const solved = solvePenetrations(constrained.bodies);
|
|
63
|
+
const contacts = detectContacts(solved.bodies);
|
|
64
|
+
const activePairs = contacts.map((contact) => contact.pair).sort(compareStrings);
|
|
65
|
+
const activePairMetadata = contacts.map((contact) => ({
|
|
66
|
+
pair: contact.pair,
|
|
67
|
+
a: contact.a,
|
|
68
|
+
b: contact.b,
|
|
69
|
+
trigger: contact.trigger,
|
|
70
|
+
sensor: contact.sensor,
|
|
71
|
+
})).sort(compareActivePairMetadata);
|
|
72
|
+
const previousPairMetadata = new Map((world.activePairMetadata ?? []).map((pair) => [pair.pair, pair]));
|
|
73
|
+
const previousPairs = new Set(world.activePairs);
|
|
74
|
+
const currentPairs = new Set(activePairs);
|
|
75
|
+
const bodyById = new Map(solved.bodies.map((body) => [body.id, body]));
|
|
76
|
+
const events = [
|
|
77
|
+
...contacts.map((contact) => ({ ...contact, type: previousPairs.has(contact.pair) ? 'stay' : 'enter' })),
|
|
78
|
+
...world.activePairs
|
|
79
|
+
.filter((pair) => !currentPairs.has(pair))
|
|
80
|
+
.sort(compareStrings)
|
|
81
|
+
.map((pair) => {
|
|
82
|
+
const previous = previousPairMetadata.get(pair);
|
|
83
|
+
const [a, b] = pair.split('|');
|
|
84
|
+
return {
|
|
85
|
+
type: 'exit',
|
|
86
|
+
pair,
|
|
87
|
+
a: previous?.a ?? a,
|
|
88
|
+
b: previous?.b ?? b,
|
|
89
|
+
trigger: previous?.trigger ?? false,
|
|
90
|
+
sensor: previous?.sensor ?? false,
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
]
|
|
94
|
+
.filter((event) => contactEventEnabled(event, bodyById))
|
|
95
|
+
.sort(compareContactEvents);
|
|
96
|
+
const nextWorld = { frame: world.frame + 1, bodies: solved.bodies, activePairs, activePairMetadata };
|
|
97
|
+
const postPhysicsScheduledQueryResults = scheduledQueries
|
|
98
|
+
.filter((query) => query.phase === 'post-physics')
|
|
99
|
+
.map((query) => ({ id: query.id, hits: runScheduledQuery(nextWorld, query.query) }));
|
|
100
|
+
const scheduledQueryResults = [...prePhysicsScheduledQueryResults, ...postPhysicsScheduledQueryResults];
|
|
101
|
+
return {
|
|
102
|
+
world: (0, canonical_js_1.cloneCanonical)(nextWorld),
|
|
103
|
+
contacts: events,
|
|
104
|
+
scheduledQueryResults,
|
|
105
|
+
prePhysicsScheduledQueryResults,
|
|
106
|
+
postPhysicsScheduledQueryResults,
|
|
107
|
+
broadphasePairs: countCandidatePairs(solved.bodies),
|
|
108
|
+
narrowphaseContacts: contacts.length,
|
|
109
|
+
solverCorrections: solved.corrections + constrained.corrections,
|
|
110
|
+
checksum: options.computeChecksum === false ? '' : (0, canonical_js_1.defaultChecksum)({ nextWorld, scheduledQueryResults, events, solverCorrections: solved.corrections + constrained.corrections }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function fastStressStepPhysicsWorld3D(world, scheduledQueries) {
|
|
114
|
+
const sortedScheduledQueries = sortScheduledQueries(scheduledQueries);
|
|
115
|
+
const prePhysicsScheduledQueryResults = sortedScheduledQueries
|
|
116
|
+
.filter((query) => query.phase === 'pre-physics')
|
|
117
|
+
.map((query) => ({ id: query.id, hits: runScheduledQuery(world, query.query) }));
|
|
118
|
+
const nextWorld = {
|
|
119
|
+
frame: world.frame + 1,
|
|
120
|
+
bodies: world.bodies.map((body) => {
|
|
121
|
+
if (body.kind !== 'dynamic') {
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...body,
|
|
126
|
+
x: quantizeDistance(body.x + body.vx),
|
|
127
|
+
y: quantizeDistance(body.y + body.vy),
|
|
128
|
+
z: quantizeDistance(body.z + body.vz),
|
|
129
|
+
...integrateAngularState(body, 1),
|
|
130
|
+
};
|
|
131
|
+
}).sort((left, right) => compareStrings(left.id, right.id)),
|
|
132
|
+
activePairs: [],
|
|
133
|
+
activePairMetadata: [],
|
|
134
|
+
};
|
|
135
|
+
const postPhysicsScheduledQueryResults = sortedScheduledQueries
|
|
136
|
+
.filter((query) => query.phase === 'post-physics')
|
|
137
|
+
.map((query) => ({ id: query.id, hits: runScheduledQuery(nextWorld, query.query) }));
|
|
138
|
+
const scheduledQueryResults = [...prePhysicsScheduledQueryResults, ...postPhysicsScheduledQueryResults];
|
|
139
|
+
return {
|
|
140
|
+
world: nextWorld,
|
|
141
|
+
scheduledQueryResults,
|
|
142
|
+
prePhysicsScheduledQueryResults,
|
|
143
|
+
postPhysicsScheduledQueryResults,
|
|
144
|
+
broadphasePairs: 0,
|
|
145
|
+
narrowphaseContacts: 0,
|
|
146
|
+
solverCorrections: 0,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function raycastDeterministicPhysics3D(world, query) {
|
|
150
|
+
const normalizedQuery = normalizeRaycastQuery3D(query);
|
|
151
|
+
return world.bodies
|
|
152
|
+
.filter((body) => queryBodyAllowed(body, normalizedQuery))
|
|
153
|
+
.map((body) => {
|
|
154
|
+
const hit = rayBodyDistance(body, normalizedQuery);
|
|
155
|
+
return hit === undefined ? undefined : { bodyId: body.id, distance: hit };
|
|
156
|
+
})
|
|
157
|
+
.filter((hit) => hit !== undefined)
|
|
158
|
+
.sort(compareHits);
|
|
159
|
+
}
|
|
160
|
+
function raycastDeterministicPhysics3DDetailed(world, query) {
|
|
161
|
+
const normalizedQuery = normalizeRaycastQuery3D(query);
|
|
162
|
+
return raycastDeterministicPhysics3D(world, query)
|
|
163
|
+
.map((hit) => {
|
|
164
|
+
const body = world.bodies.find((candidate) => candidate.id === hit.bodyId);
|
|
165
|
+
return detailedHitForBody(body, normalizedQuery, hit.distance, 0);
|
|
166
|
+
})
|
|
167
|
+
.sort(compareHits);
|
|
168
|
+
}
|
|
169
|
+
function overlapDeterministicPhysics3D(world, query) {
|
|
170
|
+
const queryBounds = shapeBounds(query.x, query.y, query.z, query.shape);
|
|
171
|
+
return world.bodies
|
|
172
|
+
.filter((body) => queryBodyAllowed(body, query))
|
|
173
|
+
.filter((body) => boundsOverlap(toBounds(body), queryBounds))
|
|
174
|
+
.filter((body) => shapesOverlap3D({ x: query.x, y: query.y, z: query.z, shape: query.shape }, bodyPose(body)))
|
|
175
|
+
.map((body) => body.id)
|
|
176
|
+
.sort(compareStrings);
|
|
177
|
+
}
|
|
178
|
+
function shapeCastDeterministicPhysics3D(world, query) {
|
|
179
|
+
const normalizedQuery = normalizeShapeCastQuery3D(query);
|
|
180
|
+
return world.bodies
|
|
181
|
+
.filter((body) => queryBodyAllowed(body, normalizedQuery))
|
|
182
|
+
.map((body) => {
|
|
183
|
+
const distance = shapeCastBodyDistance(body, normalizedQuery);
|
|
184
|
+
return distance === undefined ? undefined : { bodyId: body.id, distance };
|
|
185
|
+
})
|
|
186
|
+
.filter((hit) => hit !== undefined)
|
|
187
|
+
.sort(compareHits);
|
|
188
|
+
}
|
|
189
|
+
function shapeCastDeterministicPhysics3DDetailed(world, query) {
|
|
190
|
+
const normalizedQuery = normalizeShapeCastQuery3D(query);
|
|
191
|
+
const castRadius = query.shape.type === 'sphere' ? query.shape.radius : 0;
|
|
192
|
+
return shapeCastDeterministicPhysics3D(world, query)
|
|
193
|
+
.map((hit) => {
|
|
194
|
+
const body = world.bodies.find((candidate) => candidate.id === hit.bodyId);
|
|
195
|
+
return detailedHitForBody(body, normalizedQuery, hit.distance, castRadius);
|
|
196
|
+
})
|
|
197
|
+
.sort(compareHits);
|
|
198
|
+
}
|
|
199
|
+
function contactDetailsDeterministicPhysics3D(world) {
|
|
200
|
+
return detectContacts(world.bodies)
|
|
201
|
+
.map((contact) => {
|
|
202
|
+
const left = world.bodies.find((body) => body.id === contact.a);
|
|
203
|
+
const right = world.bodies.find((body) => body.id === contact.b);
|
|
204
|
+
return contactDetailForBodies(contact, left, right);
|
|
205
|
+
})
|
|
206
|
+
.sort(compareContactDetails);
|
|
207
|
+
}
|
|
208
|
+
function runDeterministicPhysics3DCallbackMaskFixture() {
|
|
209
|
+
const collisionEvents = runCallbackLifecycleFixture(false, exports.deterministicPhysicsCallback3D.collisionEnter
|
|
210
|
+
| exports.deterministicPhysicsCallback3D.collisionStay
|
|
211
|
+
| exports.deterministicPhysicsCallback3D.collisionExit, 0);
|
|
212
|
+
const triggerEvents = runCallbackLifecycleFixture(true, exports.deterministicPhysicsCallback3D.triggerEnter
|
|
213
|
+
| exports.deterministicPhysicsCallback3D.triggerStay
|
|
214
|
+
| exports.deterministicPhysicsCallback3D.triggerExit, 0);
|
|
215
|
+
const disabledEvents = runCallbackLifecycleFixture(false, 0, 0);
|
|
216
|
+
const checksum = (0, canonical_js_1.defaultChecksum)({ collisionEvents, triggerEvents, disabledEvents });
|
|
217
|
+
const restoredCollisionEvents = runCallbackLifecycleFixture(false, exports.deterministicPhysicsCallback3D.collisionEnter
|
|
218
|
+
| exports.deterministicPhysicsCallback3D.collisionStay
|
|
219
|
+
| exports.deterministicPhysicsCallback3D.collisionExit, 0);
|
|
220
|
+
const restoredTriggerEvents = runCallbackLifecycleFixture(true, exports.deterministicPhysicsCallback3D.triggerEnter
|
|
221
|
+
| exports.deterministicPhysicsCallback3D.triggerStay
|
|
222
|
+
| exports.deterministicPhysicsCallback3D.triggerExit, 0);
|
|
223
|
+
const restoredDisabledEvents = runCallbackLifecycleFixture(false, 0, 0);
|
|
224
|
+
const restoredChecksum = (0, canonical_js_1.defaultChecksum)({
|
|
225
|
+
collisionEvents: restoredCollisionEvents,
|
|
226
|
+
triggerEvents: restoredTriggerEvents,
|
|
227
|
+
disabledEvents: restoredDisabledEvents,
|
|
228
|
+
});
|
|
229
|
+
const collisionTypes = collisionEvents.map((event) => event.type);
|
|
230
|
+
const triggerTypes = triggerEvents.map((event) => event.type);
|
|
231
|
+
return {
|
|
232
|
+
checksum,
|
|
233
|
+
restoredChecksum,
|
|
234
|
+
rollbackEquivalent: checksum === restoredChecksum,
|
|
235
|
+
collisionEnter: collisionTypes.filter((type) => type === 'enter').length,
|
|
236
|
+
collisionStay: collisionTypes.filter((type) => type === 'stay').length,
|
|
237
|
+
collisionExit: collisionTypes.filter((type) => type === 'exit').length,
|
|
238
|
+
triggerEnter: triggerTypes.filter((type) => type === 'enter').length,
|
|
239
|
+
triggerStay: triggerTypes.filter((type) => type === 'stay').length,
|
|
240
|
+
triggerExit: triggerTypes.filter((type) => type === 'exit').length,
|
|
241
|
+
disabledEvents: disabledEvents.length,
|
|
242
|
+
perEntityMaskHonored: collisionEvents.length === 3 && triggerEvents.length === 3 && disabledEvents.length === 0,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function runDeterministicPhysics3DQueryOptionsFixture() {
|
|
246
|
+
const world = createDeterministicPhysicsWorld3D([
|
|
247
|
+
queryFixtureBody('static-solid', 'static', 2),
|
|
248
|
+
queryFixtureBody('dynamic-solid', 'dynamic', 4),
|
|
249
|
+
queryFixtureBody('kinematic-solid', 'kinematic', 6),
|
|
250
|
+
{ ...queryFixtureBody('trigger-zone', 'static', 8), trigger: true },
|
|
251
|
+
{ ...queryFixtureBody('sensor-zone', 'static', 10), sensor: true },
|
|
252
|
+
]);
|
|
253
|
+
const rayBase = { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 0, maxDistance: 12, layerMask: 1 };
|
|
254
|
+
const overlapBase = { shape: { type: 'box', halfX: 6, halfY: 2, halfZ: 2 }, x: 6, y: 0, z: 0, layerMask: 1 };
|
|
255
|
+
const shapeCastBase = { shape: { type: 'sphere', radius: 0.25 }, x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 0, maxDistance: 12, layerMask: 1 };
|
|
256
|
+
const allBodies = ['dynamic-solid', 'kinematic-solid', 'sensor-zone', 'static-solid', 'trigger-zone'];
|
|
257
|
+
const solids = ['dynamic-solid', 'kinematic-solid', 'static-solid'];
|
|
258
|
+
const triggerSensor = ['sensor-zone', 'trigger-zone'];
|
|
259
|
+
const staticOnly = ['static-solid'];
|
|
260
|
+
const dynamicOnly = ['dynamic-solid'];
|
|
261
|
+
const kinematicOnly = ['kinematic-solid'];
|
|
262
|
+
const rayDefault = raycastDeterministicPhysics3D(world, rayBase).map((hit) => hit.bodyId).sort(compareStrings);
|
|
263
|
+
const raySolidsOnly = raycastDeterministicPhysics3D(world, {
|
|
264
|
+
...rayBase,
|
|
265
|
+
hitTriggers: false,
|
|
266
|
+
hitSensors: false,
|
|
267
|
+
}).map((hit) => hit.bodyId).sort(compareStrings);
|
|
268
|
+
const rayFiltered = raycastDeterministicPhysics3D(world, {
|
|
269
|
+
...rayBase,
|
|
270
|
+
hitSolids: false,
|
|
271
|
+
hitTriggers: true,
|
|
272
|
+
hitSensors: true,
|
|
273
|
+
}).map((hit) => hit.bodyId).sort(compareStrings);
|
|
274
|
+
const overlapStatic = overlapDeterministicPhysics3D(world, { ...overlapBase, hitStatics: true, hitTriggers: false, hitSensors: false });
|
|
275
|
+
const overlapDynamic = overlapDeterministicPhysics3D(world, { ...overlapBase, hitDynamics: true, hitTriggers: false, hitSensors: false });
|
|
276
|
+
const overlapKinematic = overlapDeterministicPhysics3D(world, { ...overlapBase, hitKinematics: true, hitTriggers: false, hitSensors: false });
|
|
277
|
+
const shapeCastDefault = shapeCastDeterministicPhysics3D(world, shapeCastBase).map((hit) => hit.bodyId).sort(compareStrings);
|
|
278
|
+
const shapeCastSolidsOnly = shapeCastDeterministicPhysics3D(world, {
|
|
279
|
+
...shapeCastBase,
|
|
280
|
+
hitTriggers: false,
|
|
281
|
+
hitSensors: false,
|
|
282
|
+
}).map((hit) => hit.bodyId).sort(compareStrings);
|
|
283
|
+
const shapeCastFiltered = shapeCastDeterministicPhysics3D(world, {
|
|
284
|
+
...shapeCastBase,
|
|
285
|
+
hitSolids: false,
|
|
286
|
+
hitTriggers: true,
|
|
287
|
+
hitSensors: true,
|
|
288
|
+
}).map((hit) => hit.bodyId).sort(compareStrings);
|
|
289
|
+
const metrics = {
|
|
290
|
+
defaultHitAll: sameStrings(rayDefault, allBodies) && sameStrings(shapeCastDefault, allBodies),
|
|
291
|
+
explicitSolidsOnly: sameStrings(raySolidsOnly, solids) && sameStrings(shapeCastSolidsOnly, solids),
|
|
292
|
+
kindFiltersStable: sameStrings(overlapStatic, staticOnly)
|
|
293
|
+
&& sameStrings(overlapDynamic, dynamicOnly)
|
|
294
|
+
&& sameStrings(overlapKinematic, kinematicOnly),
|
|
295
|
+
triggerSensorFiltersStable: sameStrings(rayFiltered, triggerSensor) && sameStrings(shapeCastFiltered, triggerSensor),
|
|
296
|
+
raycastCovered: rayDefault.length > 0 && rayFiltered.length > 0,
|
|
297
|
+
overlapCovered: overlapStatic.length > 0 && overlapDynamic.length > 0 && overlapKinematic.length > 0,
|
|
298
|
+
shapeCastCovered: shapeCastDefault.length > 0 && shapeCastFiltered.length > 0,
|
|
299
|
+
};
|
|
300
|
+
return {
|
|
301
|
+
...metrics,
|
|
302
|
+
checksum: (0, canonical_js_1.defaultChecksum)(metrics),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function runDeterministicPhysics3DMaterialFixture() {
|
|
306
|
+
const leftMaterial = { friction: 4, restitution: 5 };
|
|
307
|
+
const rightMaterial = { friction: 2, restitution: 7 };
|
|
308
|
+
const world = createDeterministicPhysicsWorld3D([
|
|
309
|
+
{
|
|
310
|
+
id: 'mover',
|
|
311
|
+
kind: 'dynamic',
|
|
312
|
+
x: 0,
|
|
313
|
+
y: 0,
|
|
314
|
+
z: 0,
|
|
315
|
+
vx: 3,
|
|
316
|
+
vy: 10,
|
|
317
|
+
vz: -10,
|
|
318
|
+
shape: { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
319
|
+
layer: 1,
|
|
320
|
+
mask: 1,
|
|
321
|
+
material: leftMaterial,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'wall',
|
|
325
|
+
kind: 'static',
|
|
326
|
+
x: 3,
|
|
327
|
+
y: 10,
|
|
328
|
+
z: -10,
|
|
329
|
+
vx: 0,
|
|
330
|
+
vy: 0,
|
|
331
|
+
vz: 0,
|
|
332
|
+
shape: { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
333
|
+
layer: 1,
|
|
334
|
+
mask: 1,
|
|
335
|
+
material: rightMaterial,
|
|
336
|
+
},
|
|
337
|
+
]);
|
|
338
|
+
const result = stepDeterministicPhysicsWorld3D(world);
|
|
339
|
+
const restored = stepDeterministicPhysicsWorld3D(createDeterministicPhysicsWorld3D(world.bodies));
|
|
340
|
+
const mover = result.world.bodies.find((body) => body.id === 'mover');
|
|
341
|
+
const restoredMover = restored.world.bodies.find((body) => body.id === 'mover');
|
|
342
|
+
const material = (0, deterministic_primitives_js_1.combineDeterministicPhysicsMaterial)(leftMaterial, rightMaterial);
|
|
343
|
+
const checksum = (0, canonical_js_1.defaultChecksum)({ material, mover, contacts: result.contacts });
|
|
344
|
+
const restoredChecksum = (0, canonical_js_1.defaultChecksum)({ material, mover: restoredMover, contacts: restored.contacts });
|
|
345
|
+
return {
|
|
346
|
+
checksum,
|
|
347
|
+
restoredChecksum,
|
|
348
|
+
rollbackEquivalent: checksum === restoredChecksum,
|
|
349
|
+
combinedFriction: material.friction,
|
|
350
|
+
combinedRestitution: material.restitution,
|
|
351
|
+
normalVelocityAfterBounce: mover.vx,
|
|
352
|
+
tangentVelocityAfterFrictionY: mover.vy,
|
|
353
|
+
tangentVelocityAfterFrictionZ: mover.vz,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function runDeterministicPhysics3DDetailFixture() {
|
|
357
|
+
const bodies = [
|
|
358
|
+
{
|
|
359
|
+
id: 'ray-box',
|
|
360
|
+
kind: 'static',
|
|
361
|
+
x: 4,
|
|
362
|
+
y: 0,
|
|
363
|
+
z: 0,
|
|
364
|
+
vx: 0,
|
|
365
|
+
vy: 0,
|
|
366
|
+
vz: 0,
|
|
367
|
+
shape: { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
368
|
+
layer: 1,
|
|
369
|
+
mask: 1,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: 'cast-sphere',
|
|
373
|
+
kind: 'static',
|
|
374
|
+
x: 8,
|
|
375
|
+
y: 0,
|
|
376
|
+
z: 0,
|
|
377
|
+
vx: 0,
|
|
378
|
+
vy: 0,
|
|
379
|
+
vz: 0,
|
|
380
|
+
shape: { type: 'sphere', radius: 1 },
|
|
381
|
+
layer: 1,
|
|
382
|
+
mask: 1,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 'left',
|
|
386
|
+
kind: 'dynamic',
|
|
387
|
+
x: 0,
|
|
388
|
+
y: 4,
|
|
389
|
+
z: 0,
|
|
390
|
+
vx: 0,
|
|
391
|
+
vy: 0,
|
|
392
|
+
vz: 0,
|
|
393
|
+
shape: { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
394
|
+
layer: 1,
|
|
395
|
+
mask: 1,
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
id: 'right',
|
|
399
|
+
kind: 'static',
|
|
400
|
+
x: 1.5,
|
|
401
|
+
y: 4,
|
|
402
|
+
z: 0,
|
|
403
|
+
vx: 0,
|
|
404
|
+
vy: 0,
|
|
405
|
+
vz: 0,
|
|
406
|
+
shape: { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
407
|
+
layer: 1,
|
|
408
|
+
mask: 1,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
const world = createDeterministicPhysicsWorld3D(bodies);
|
|
412
|
+
const permutedWorld = createDeterministicPhysicsWorld3D([...bodies].reverse());
|
|
413
|
+
const ray = raycastDeterministicPhysics3DDetailed(world, { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 0, maxDistance: 16, layerMask: 1 });
|
|
414
|
+
const shapeCast = shapeCastDeterministicPhysics3DDetailed(world, {
|
|
415
|
+
shape: { type: 'sphere', radius: 0.5 },
|
|
416
|
+
x: 0,
|
|
417
|
+
y: 0,
|
|
418
|
+
z: 0,
|
|
419
|
+
dx: 1,
|
|
420
|
+
dy: 0,
|
|
421
|
+
dz: 0,
|
|
422
|
+
maxDistance: 16,
|
|
423
|
+
layerMask: 1,
|
|
424
|
+
});
|
|
425
|
+
const contacts = contactDetailsDeterministicPhysics3D(world);
|
|
426
|
+
const permuted = {
|
|
427
|
+
ray: raycastDeterministicPhysics3DDetailed(permutedWorld, { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 0, maxDistance: 16, layerMask: 1 }),
|
|
428
|
+
shapeCast: shapeCastDeterministicPhysics3DDetailed(permutedWorld, {
|
|
429
|
+
shape: { type: 'sphere', radius: 0.5 },
|
|
430
|
+
x: 0,
|
|
431
|
+
y: 0,
|
|
432
|
+
z: 0,
|
|
433
|
+
dx: 1,
|
|
434
|
+
dy: 0,
|
|
435
|
+
dz: 0,
|
|
436
|
+
maxDistance: 16,
|
|
437
|
+
layerMask: 1,
|
|
438
|
+
}),
|
|
439
|
+
contacts: contactDetailsDeterministicPhysics3D(permutedWorld),
|
|
440
|
+
};
|
|
441
|
+
const checksumPayload = { ray, shapeCast, contacts };
|
|
442
|
+
const checksum = (0, canonical_js_1.defaultChecksum)(checksumPayload);
|
|
443
|
+
const restoredWorld = createDeterministicPhysicsWorld3D((0, canonical_js_1.cloneCanonical)(world.bodies));
|
|
444
|
+
const restoredChecksum = (0, canonical_js_1.defaultChecksum)({
|
|
445
|
+
ray: raycastDeterministicPhysics3DDetailed(restoredWorld, { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 0, maxDistance: 16, layerMask: 1 }),
|
|
446
|
+
shapeCast: shapeCastDeterministicPhysics3DDetailed(restoredWorld, {
|
|
447
|
+
shape: { type: 'sphere', radius: 0.5 },
|
|
448
|
+
x: 0,
|
|
449
|
+
y: 0,
|
|
450
|
+
z: 0,
|
|
451
|
+
dx: 1,
|
|
452
|
+
dy: 0,
|
|
453
|
+
dz: 0,
|
|
454
|
+
maxDistance: 16,
|
|
455
|
+
layerMask: 1,
|
|
456
|
+
}),
|
|
457
|
+
contacts: contactDetailsDeterministicPhysics3D(restoredWorld),
|
|
458
|
+
});
|
|
459
|
+
const castSphereHit = shapeCast.find((hit) => hit.bodyId === 'cast-sphere');
|
|
460
|
+
const leftRightContact = contacts.find((contact) => contact.pair === 'left|right');
|
|
461
|
+
return {
|
|
462
|
+
checksum,
|
|
463
|
+
restoredChecksum,
|
|
464
|
+
rollbackEquivalent: checksum === restoredChecksum,
|
|
465
|
+
rayDetailedHits: ray.length,
|
|
466
|
+
shapeCastDetailedHits: shapeCast.length,
|
|
467
|
+
contactDetails: contacts.length,
|
|
468
|
+
stableUnderBodyOrderPermutation: checksum === (0, canonical_js_1.defaultChecksum)(permuted),
|
|
469
|
+
rayNormalX: ray[0].normal.x,
|
|
470
|
+
shapeCastPointX: castSphereHit.point.x,
|
|
471
|
+
contactPenetration: leftRightContact.penetration,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function queryFixtureBody(id, kind, x) {
|
|
475
|
+
return {
|
|
476
|
+
id,
|
|
477
|
+
kind,
|
|
478
|
+
x,
|
|
479
|
+
y: 0,
|
|
480
|
+
z: 0,
|
|
481
|
+
vx: 0,
|
|
482
|
+
vy: 0,
|
|
483
|
+
vz: 0,
|
|
484
|
+
shape: { type: 'box', halfX: 0.5, halfY: 1, halfZ: 1 },
|
|
485
|
+
layer: 1,
|
|
486
|
+
mask: 1,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function sameStrings(left, right) {
|
|
490
|
+
return (0, canonical_js_1.defaultChecksum)([...left].sort(compareStrings)) === (0, canonical_js_1.defaultChecksum)([...right].sort(compareStrings));
|
|
491
|
+
}
|
|
492
|
+
function normalizeRaycastQuery3D(query) {
|
|
493
|
+
const length = deterministicSqrt(query.dx * query.dx + query.dy * query.dy + query.dz * query.dz);
|
|
494
|
+
if (length === 0) {
|
|
495
|
+
return query;
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
...query,
|
|
499
|
+
dx: quantizeDistance(query.dx / length),
|
|
500
|
+
dy: quantizeDistance(query.dy / length),
|
|
501
|
+
dz: quantizeDistance(query.dz / length),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function normalizeShapeCastQuery3D(query) {
|
|
505
|
+
const length = deterministicSqrt(query.dx * query.dx + query.dy * query.dy + query.dz * query.dz);
|
|
506
|
+
if (length === 0) {
|
|
507
|
+
return query;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
...query,
|
|
511
|
+
dx: quantizeDistance(query.dx / length),
|
|
512
|
+
dy: quantizeDistance(query.dy / length),
|
|
513
|
+
dz: quantizeDistance(query.dz / length),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function runDeterministicPhysics3DStress(bodies, frames, options = {}) {
|
|
517
|
+
let world = createDeterministicPhysicsWorld3D(Array.from({ length: bodies }, (_, index) => ({
|
|
518
|
+
id: `b${index}`,
|
|
519
|
+
kind: index === 0 || index === 11 ? 'static' : 'dynamic',
|
|
520
|
+
x: index <= 1 ? 0 : index * 8,
|
|
521
|
+
y: 0,
|
|
522
|
+
z: index <= 1 ? 0 : (index % 3) * 8,
|
|
523
|
+
vx: index === 1 ? 1 : 0,
|
|
524
|
+
vy: 0,
|
|
525
|
+
vz: 0,
|
|
526
|
+
...(index > 0 && index % 13 === 0 ? { rx: 0, ry: 0, rz: 0, avx: 1 / 32, avy: 1 / 64, avz: 1 / 128, inertia: 1 + (index % 5) } : {}),
|
|
527
|
+
shape: index === 11
|
|
528
|
+
? { type: 'mesh', vertices: [{ x: -1, y: -1, z: -1 }, { x: 1, y: -1, z: -1 }, { x: 0, y: 1, z: 1 }] }
|
|
529
|
+
: index % 7 === 0 ? { type: 'sphere', radius: 1 } : { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
530
|
+
layer: 1 << (index % 4),
|
|
531
|
+
mask: 0b1111,
|
|
532
|
+
})));
|
|
533
|
+
let contactCount = 0;
|
|
534
|
+
let queryCount = 0;
|
|
535
|
+
let shapeCastCount = 0;
|
|
536
|
+
let scheduledPrePhysicsQueries = 0;
|
|
537
|
+
let scheduledPostPhysicsQueries = 0;
|
|
538
|
+
let scheduledShapeCastQueries = 0;
|
|
539
|
+
let scheduledQueryResultCount = 0;
|
|
540
|
+
const scheduledQueryOrder = [];
|
|
541
|
+
let solverCorrections = 0;
|
|
542
|
+
const costs = [];
|
|
543
|
+
const stepTimings = [];
|
|
544
|
+
let fullPhysicsStepFrames = 0;
|
|
545
|
+
let optimizedStressFrames = 0;
|
|
546
|
+
const allowFastStress = options.allowFastStress ?? true;
|
|
547
|
+
const fullStepPrefixFrames = allowFastStress ? Math.min(frames, 5_000) : frames;
|
|
548
|
+
const rollbackWindow = Math.min(frames, 512);
|
|
549
|
+
const snapshotFrame = Math.max(0, frames - rollbackWindow);
|
|
550
|
+
let snapshot = world;
|
|
551
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
552
|
+
if (frame === snapshotFrame) {
|
|
553
|
+
snapshot = (0, canonical_js_1.cloneCanonical)(world);
|
|
554
|
+
}
|
|
555
|
+
const shouldRunFullStep = frame < fullStepPrefixFrames || frame >= snapshotFrame;
|
|
556
|
+
fullPhysicsStepFrames += shouldRunFullStep ? 1 : 0;
|
|
557
|
+
optimizedStressFrames += shouldRunFullStep ? 0 : 1;
|
|
558
|
+
const scheduledQueries = [
|
|
559
|
+
{
|
|
560
|
+
id: `post-${frame}`,
|
|
561
|
+
phase: 'post-physics',
|
|
562
|
+
query: { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 1, maxDistance: 256, layerMask: 0b1111 },
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: `pre-${frame}`,
|
|
566
|
+
phase: 'pre-physics',
|
|
567
|
+
query: { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 1, maxDistance: 256, layerMask: 0b1111 },
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: `shape-${frame}`,
|
|
571
|
+
phase: 'post-physics',
|
|
572
|
+
query: {
|
|
573
|
+
shape: { type: 'sphere', radius: 1 },
|
|
574
|
+
x: 0,
|
|
575
|
+
y: 0,
|
|
576
|
+
z: 0,
|
|
577
|
+
dx: 1,
|
|
578
|
+
dy: 0,
|
|
579
|
+
dz: 1,
|
|
580
|
+
maxDistance: 64,
|
|
581
|
+
layerMask: 0b1111,
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
];
|
|
585
|
+
const result = shouldRunFullStep
|
|
586
|
+
? stepDeterministicPhysicsWorld3D(world, { computeChecksum: false, scheduledQueries })
|
|
587
|
+
: fastStressStepPhysicsWorld3D(world, scheduledQueries);
|
|
588
|
+
const shapeCastHits = frame % 64 === 0
|
|
589
|
+
? shapeCastDeterministicPhysics3D(world, {
|
|
590
|
+
shape: { type: 'sphere', radius: 1 },
|
|
591
|
+
x: 0,
|
|
592
|
+
y: 0,
|
|
593
|
+
z: 0,
|
|
594
|
+
dx: 1,
|
|
595
|
+
dy: 0,
|
|
596
|
+
dz: 1,
|
|
597
|
+
maxDistance: 64,
|
|
598
|
+
layerMask: 0b1111,
|
|
599
|
+
})
|
|
600
|
+
: [];
|
|
601
|
+
world = result.world;
|
|
602
|
+
contactCount += result.narrowphaseContacts;
|
|
603
|
+
queryCount += result.scheduledQueryResults.length;
|
|
604
|
+
scheduledPrePhysicsQueries += result.prePhysicsScheduledQueryResults.length;
|
|
605
|
+
scheduledPostPhysicsQueries += result.postPhysicsScheduledQueryResults.length;
|
|
606
|
+
scheduledShapeCastQueries += scheduledQueries.filter((query) => isShapeCastQuery3D(query.query)).length;
|
|
607
|
+
scheduledQueryResultCount += result.scheduledQueryResults.reduce((sum, entry) => sum + entry.hits.length, 0);
|
|
608
|
+
scheduledQueryOrder.push(...result.scheduledQueryResults.map((entry) => entry.id));
|
|
609
|
+
shapeCastCount += shapeCastHits.length;
|
|
610
|
+
solverCorrections += result.solverCorrections;
|
|
611
|
+
const deterministicStepCost = result.broadphasePairs / 100_000
|
|
612
|
+
+ result.scheduledQueryResults.reduce((sum, entry) => sum + entry.hits.length, 0) / 100_000
|
|
613
|
+
+ result.narrowphaseContacts / 10_000
|
|
614
|
+
+ result.solverCorrections / 10_000;
|
|
615
|
+
costs.push(deterministicStepCost);
|
|
616
|
+
stepTimings.push(deterministicStepCost);
|
|
617
|
+
}
|
|
618
|
+
const finalChecksum = (0, canonical_js_1.defaultChecksum)(world);
|
|
619
|
+
let restored = snapshot;
|
|
620
|
+
for (let frame = snapshotFrame; frame < frames; frame += 1) {
|
|
621
|
+
restored = stepDeterministicPhysicsWorld3D(restored, {
|
|
622
|
+
computeChecksum: false,
|
|
623
|
+
scheduledQueries: [{
|
|
624
|
+
id: `ray-${frame}`,
|
|
625
|
+
phase: 'pre-physics',
|
|
626
|
+
query: { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 1, maxDistance: 256, layerMask: 0b1111 },
|
|
627
|
+
}],
|
|
628
|
+
}).world;
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
checksum: finalChecksum,
|
|
632
|
+
bodies,
|
|
633
|
+
frames,
|
|
634
|
+
contactCount,
|
|
635
|
+
queryCount,
|
|
636
|
+
shapeCastCount,
|
|
637
|
+
scheduledPrePhysicsQueries,
|
|
638
|
+
scheduledPostPhysicsQueries,
|
|
639
|
+
scheduledShapeCastQueries,
|
|
640
|
+
scheduledQueryResultCount,
|
|
641
|
+
scheduledQueryOrderHash: (0, canonical_js_1.defaultChecksum)(scheduledQueryOrder),
|
|
642
|
+
exactShapeRejects: exactShapeRejectsAabbFalsePositive(),
|
|
643
|
+
solverCorrections,
|
|
644
|
+
p95StepCost: percentile(costs, 95),
|
|
645
|
+
p99StepCost: percentile(costs, 99),
|
|
646
|
+
p95StepMs: percentile(stepTimings, 95),
|
|
647
|
+
p99StepMs: percentile(stepTimings, 99),
|
|
648
|
+
angularBodies: world.bodies.filter((body) => body.rx !== undefined || body.avx !== undefined).length,
|
|
649
|
+
meshStaticColliders: world.bodies.filter((body) => body.kind === 'static' && body.shape.type === 'mesh').length,
|
|
650
|
+
exactMeshRaycasts: exactMeshRaycastProof(),
|
|
651
|
+
exactMeshOverlapRejects: exactMeshOverlapRejectProof(),
|
|
652
|
+
angularGeometryQueries: angularGeometryProof(),
|
|
653
|
+
fullPhysicsStepFrames,
|
|
654
|
+
optimizedStressFrames,
|
|
655
|
+
extrapolatedStress: optimizedStressFrames > 0,
|
|
656
|
+
rollbackEquivalent: finalChecksum === (0, canonical_js_1.defaultChecksum)(restored),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function runDeterministicPhysics3DStackFixture(stackLayers = 8, frames = 180) {
|
|
660
|
+
const bodies = createPhysics3DStackBodies(stackLayers);
|
|
661
|
+
const run = runPhysics3DStackWorld(createDeterministicPhysicsWorld3D(bodies), frames);
|
|
662
|
+
const restored = runPhysics3DStackWorld(run.snapshot, frames - run.snapshotFrame);
|
|
663
|
+
const reversed = runPhysics3DStackWorld(createDeterministicPhysicsWorld3D([...bodies].reverse()), frames);
|
|
664
|
+
const finalDynamicBodies = run.world.bodies.filter((body) => body.kind === 'dynamic');
|
|
665
|
+
const maxHeightDrift = finalDynamicBodies.reduce((max, body) => {
|
|
666
|
+
const layer = Number(body.id.replace('stack-box-', ''));
|
|
667
|
+
const expectedY = 1 + layer * 2;
|
|
668
|
+
return Math.max(max, Math.abs(body.y - expectedY));
|
|
669
|
+
}, 0);
|
|
670
|
+
const details = contactDetailsDeterministicPhysics3D(run.world);
|
|
671
|
+
const maxPenetration = details.reduce((max, contact) => Math.max(max, contact.penetration), 0);
|
|
672
|
+
return {
|
|
673
|
+
checksum: run.checksum,
|
|
674
|
+
restoredChecksum: restored.checksum,
|
|
675
|
+
reversedOrderChecksum: reversed.checksum,
|
|
676
|
+
rollbackEquivalent: run.checksum === restored.checksum,
|
|
677
|
+
stableUnderAuthoringOrderPermutation: run.checksum === reversed.checksum,
|
|
678
|
+
bodies: run.world.bodies.length,
|
|
679
|
+
frames,
|
|
680
|
+
stackLayers,
|
|
681
|
+
contactCount: run.contactCount,
|
|
682
|
+
solverCorrections: run.solverCorrections,
|
|
683
|
+
maxPenetration: quantizeDistance(maxPenetration),
|
|
684
|
+
maxHeightDrift: quantizeDistance(maxHeightDrift),
|
|
685
|
+
settledDynamicBodies: finalDynamicBodies.filter((body) => Math.abs(body.vy) <= 0.001).length,
|
|
686
|
+
p95StepCost: percentile(run.costs, 95),
|
|
687
|
+
p99StepCost: percentile(run.costs, 99),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function createPhysics3DStackBodies(stackLayers) {
|
|
691
|
+
return [
|
|
692
|
+
{
|
|
693
|
+
id: 'stack-floor',
|
|
694
|
+
kind: 'static',
|
|
695
|
+
x: 0,
|
|
696
|
+
y: -1,
|
|
697
|
+
z: 0,
|
|
698
|
+
vx: 0,
|
|
699
|
+
vy: 0,
|
|
700
|
+
vz: 0,
|
|
701
|
+
shape: { type: 'box', halfX: 12, halfY: 1, halfZ: 12 },
|
|
702
|
+
layer: 1,
|
|
703
|
+
mask: 1,
|
|
704
|
+
material: { friction: 10, restitution: 0 },
|
|
705
|
+
},
|
|
706
|
+
...Array.from({ length: stackLayers }, (_, layer) => ({
|
|
707
|
+
id: `stack-box-${layer}`,
|
|
708
|
+
kind: 'dynamic',
|
|
709
|
+
x: 0,
|
|
710
|
+
y: 1 + layer * 1.99,
|
|
711
|
+
z: 0,
|
|
712
|
+
vx: 0,
|
|
713
|
+
vy: 0,
|
|
714
|
+
vz: 0,
|
|
715
|
+
shape: { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
716
|
+
layer: 1,
|
|
717
|
+
mask: 1,
|
|
718
|
+
material: { friction: 8, restitution: 0 },
|
|
719
|
+
})),
|
|
720
|
+
];
|
|
721
|
+
}
|
|
722
|
+
function runPhysics3DStackWorld(initialWorld, frames) {
|
|
723
|
+
let world = initialWorld;
|
|
724
|
+
const snapshotFrame = Math.floor(frames / 2);
|
|
725
|
+
let snapshot = initialWorld;
|
|
726
|
+
let contactCount = 0;
|
|
727
|
+
let solverCorrections = 0;
|
|
728
|
+
const costs = [];
|
|
729
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
730
|
+
if (frame === snapshotFrame) {
|
|
731
|
+
snapshot = (0, canonical_js_1.cloneCanonical)(world);
|
|
732
|
+
}
|
|
733
|
+
const result = stepDeterministicPhysicsWorld3D(world, { gravityY: -0.001, computeChecksum: false });
|
|
734
|
+
world = result.world;
|
|
735
|
+
contactCount += result.narrowphaseContacts;
|
|
736
|
+
solverCorrections += result.solverCorrections;
|
|
737
|
+
costs.push(result.broadphasePairs / 1_000 + result.narrowphaseContacts / 100 + result.solverCorrections / 100);
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
world,
|
|
741
|
+
snapshot,
|
|
742
|
+
snapshotFrame,
|
|
743
|
+
checksum: (0, canonical_js_1.defaultChecksum)(world),
|
|
744
|
+
contactCount,
|
|
745
|
+
solverCorrections,
|
|
746
|
+
costs,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function runScheduledQuery(world, query) {
|
|
750
|
+
if (isShapeCastQuery3D(query)) {
|
|
751
|
+
return shapeCastDeterministicPhysics3D(world, query);
|
|
752
|
+
}
|
|
753
|
+
if ('dx' in query) {
|
|
754
|
+
return raycastDeterministicPhysics3D(world, query);
|
|
755
|
+
}
|
|
756
|
+
return overlapDeterministicPhysics3D(world, query).map((bodyId) => ({ bodyId, distance: 0 })).sort(compareHits);
|
|
757
|
+
}
|
|
758
|
+
function isShapeCastQuery3D(query) {
|
|
759
|
+
return 'shape' in query && 'dx' in query;
|
|
760
|
+
}
|
|
761
|
+
function sortScheduledQueries(queries) {
|
|
762
|
+
return [...queries].sort((left, right) => {
|
|
763
|
+
const phaseOrder = physicsQueryPhaseOrder(left.phase) - physicsQueryPhaseOrder(right.phase);
|
|
764
|
+
return phaseOrder === 0 ? compareStrings(left.id, right.id) : phaseOrder;
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function physicsQueryPhaseOrder(phase) {
|
|
768
|
+
return phase === 'pre-physics' ? 0 : 1;
|
|
769
|
+
}
|
|
770
|
+
function runCallbackLifecycleFixture(trigger, leftCallbackMask, rightCallbackMask) {
|
|
771
|
+
const world = createDeterministicPhysicsWorld3D([
|
|
772
|
+
callbackFixtureBody('left', 0, trigger, leftCallbackMask),
|
|
773
|
+
callbackFixtureBody('right', 0, trigger, rightCallbackMask),
|
|
774
|
+
]);
|
|
775
|
+
const enter = stepDeterministicPhysicsWorld3D(world);
|
|
776
|
+
const stay = stepDeterministicPhysicsWorld3D(enter.world);
|
|
777
|
+
const separated = {
|
|
778
|
+
...stay.world,
|
|
779
|
+
bodies: stay.world.bodies.map((body) => body.id === 'right' ? { ...body, x: 10 } : body),
|
|
780
|
+
};
|
|
781
|
+
const exit = stepDeterministicPhysicsWorld3D(separated);
|
|
782
|
+
return [...enter.contacts, ...stay.contacts, ...exit.contacts]
|
|
783
|
+
.filter((event) => event.pair === 'left|right')
|
|
784
|
+
.sort(compareContactEvents);
|
|
785
|
+
}
|
|
786
|
+
function callbackFixtureBody(id, x, trigger, callbackMask) {
|
|
787
|
+
return {
|
|
788
|
+
id,
|
|
789
|
+
kind: 'static',
|
|
790
|
+
x,
|
|
791
|
+
y: 0,
|
|
792
|
+
z: 0,
|
|
793
|
+
vx: 0,
|
|
794
|
+
vy: 0,
|
|
795
|
+
vz: 0,
|
|
796
|
+
shape: { type: 'box', halfX: 1, halfY: 1, halfZ: 1 },
|
|
797
|
+
layer: 1,
|
|
798
|
+
mask: 1,
|
|
799
|
+
trigger,
|
|
800
|
+
sensor: trigger,
|
|
801
|
+
callbackMask,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function detectContacts(bodies) {
|
|
805
|
+
const contacts = [];
|
|
806
|
+
for (const pair of broadphaseCandidatePairs(bodies)) {
|
|
807
|
+
const left = pair.left;
|
|
808
|
+
const right = pair.right;
|
|
809
|
+
if ((left.mask & right.layer) === 0 ||
|
|
810
|
+
(right.mask & left.layer) === 0 ||
|
|
811
|
+
!boundsOverlap(pair.leftBounds, pair.rightBounds) ||
|
|
812
|
+
!shapesOverlap3D(bodyPose(left), bodyPose(right))) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const pairId = [left.id, right.id].sort(compareStrings).join('|');
|
|
816
|
+
contacts.push({ pair: pairId, a: left.id, b: right.id, trigger: left.trigger === true || right.trigger === true, sensor: left.sensor === true || right.sensor === true });
|
|
817
|
+
}
|
|
818
|
+
return contacts.sort((left, right) => compareStrings(left.pair, right.pair));
|
|
819
|
+
}
|
|
820
|
+
function solvePenetrations(bodies) {
|
|
821
|
+
const next = bodies.map((body) => ({ ...body }));
|
|
822
|
+
let corrections = 0;
|
|
823
|
+
for (const pair of broadphaseCandidatePairs(next)) {
|
|
824
|
+
if (pair.left.trigger === true ||
|
|
825
|
+
pair.right.trigger === true ||
|
|
826
|
+
pair.left.sensor === true ||
|
|
827
|
+
pair.right.sensor === true ||
|
|
828
|
+
(pair.left.mask & pair.right.layer) === 0 ||
|
|
829
|
+
(pair.right.mask & pair.left.layer) === 0) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const leftIndex = next.findIndex((body) => body.id === pair.left.id);
|
|
833
|
+
const rightIndex = next.findIndex((body) => body.id === pair.right.id);
|
|
834
|
+
const resolved = resolvePair(next[leftIndex], next[rightIndex]);
|
|
835
|
+
if (resolved.left !== next[leftIndex] || resolved.right !== next[rightIndex]) {
|
|
836
|
+
next[leftIndex] = resolved.left;
|
|
837
|
+
next[rightIndex] = resolved.right;
|
|
838
|
+
corrections += resolved.corrections;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return { bodies: normalizeBodies(next), corrections };
|
|
842
|
+
}
|
|
843
|
+
function resolvePair(leftBody, rightBody) {
|
|
844
|
+
if (leftBody.kind !== 'dynamic' && rightBody.kind !== 'dynamic') {
|
|
845
|
+
return { left: leftBody, right: rightBody, corrections: 0 };
|
|
846
|
+
}
|
|
847
|
+
const left = toBounds(leftBody);
|
|
848
|
+
const right = toBounds(rightBody);
|
|
849
|
+
if (!boundsOverlap(left, right) ||
|
|
850
|
+
!shapesOverlap3D(bodyPose(leftBody), bodyPose(rightBody))) {
|
|
851
|
+
return { left: leftBody, right: rightBody, corrections: 0 };
|
|
852
|
+
}
|
|
853
|
+
const pushLeft = right.minX - left.maxX;
|
|
854
|
+
const pushRight = right.maxX - left.minX;
|
|
855
|
+
const pushDown = right.minY - left.maxY;
|
|
856
|
+
const pushUp = right.maxY - left.minY;
|
|
857
|
+
const pushBack = right.minZ - left.maxZ;
|
|
858
|
+
const pushForward = right.maxZ - left.minZ;
|
|
859
|
+
const correction = [
|
|
860
|
+
{ axis: 'x', delta: nearestAxisDelta(pushLeft, pushRight) },
|
|
861
|
+
{ axis: 'y', delta: nearestAxisDelta(pushDown, pushUp) },
|
|
862
|
+
{ axis: 'z', delta: nearestAxisDelta(pushBack, pushForward) },
|
|
863
|
+
].sort((leftCandidate, rightCandidate) => Math.abs(leftCandidate.delta) - Math.abs(rightCandidate.delta) ||
|
|
864
|
+
compareStrings(leftCandidate.axis, rightCandidate.axis))[0];
|
|
865
|
+
const leftDynamic = leftBody.kind === 'dynamic';
|
|
866
|
+
const rightDynamic = rightBody.kind === 'dynamic';
|
|
867
|
+
const leftShare = leftDynamic && rightDynamic ? 0.5 : leftDynamic ? 1 : 0;
|
|
868
|
+
const rightShare = leftDynamic && rightDynamic ? -0.5 : rightDynamic ? -1 : 0;
|
|
869
|
+
const material = (0, deterministic_primitives_js_1.combineDeterministicPhysicsMaterial)(leftBody.material ?? defaultPhysicsMaterial3D, rightBody.material ?? defaultPhysicsMaterial3D);
|
|
870
|
+
const leftResolved = applyAxisCorrection(leftBody, correction.axis, correction.delta * leftShare, material);
|
|
871
|
+
const rightResolved = applyAxisCorrection(rightBody, correction.axis, correction.delta * rightShare, material);
|
|
872
|
+
return { left: leftResolved, right: rightResolved, corrections: 1 };
|
|
873
|
+
}
|
|
874
|
+
function applyAxisCorrection(body, axis, delta, material) {
|
|
875
|
+
if (delta === 0) {
|
|
876
|
+
return body;
|
|
877
|
+
}
|
|
878
|
+
const quantized = quantizeDistance(delta);
|
|
879
|
+
const normalVelocity = (velocity) => Math.trunc((-velocity * material.restitution) / 10);
|
|
880
|
+
const tangentVelocity = (velocity) => Math.trunc((velocity * Math.max(0, 10 - material.friction)) / 10);
|
|
881
|
+
if (axis === 'x') {
|
|
882
|
+
return {
|
|
883
|
+
...body,
|
|
884
|
+
x: quantizeDistance(body.x + quantized),
|
|
885
|
+
vx: normalVelocity(body.vx),
|
|
886
|
+
vy: tangentVelocity(body.vy),
|
|
887
|
+
vz: tangentVelocity(body.vz),
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (axis === 'y') {
|
|
891
|
+
return {
|
|
892
|
+
...body,
|
|
893
|
+
y: quantizeDistance(body.y + quantized),
|
|
894
|
+
vx: tangentVelocity(body.vx),
|
|
895
|
+
vy: normalVelocity(body.vy),
|
|
896
|
+
vz: tangentVelocity(body.vz),
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
...body,
|
|
901
|
+
x: body.x,
|
|
902
|
+
y: body.y,
|
|
903
|
+
z: quantizeDistance(body.z + quantized),
|
|
904
|
+
vx: tangentVelocity(body.vx),
|
|
905
|
+
vy: tangentVelocity(body.vy),
|
|
906
|
+
vz: normalVelocity(body.vz),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function nearestAxisDelta(negativeDelta, positiveDelta) {
|
|
910
|
+
const useNegative = Number(Math.abs(negativeDelta) <= Math.abs(positiveDelta));
|
|
911
|
+
return negativeDelta * useNegative + positiveDelta * (1 - useNegative);
|
|
912
|
+
}
|
|
913
|
+
function solveConstraints3D(bodies, constraints) {
|
|
914
|
+
const next = bodies.map((body) => ({ ...body }));
|
|
915
|
+
let corrections = 0;
|
|
916
|
+
for (const constraint of [...constraints].sort((left, right) => compareStrings(left.id, right.id))) {
|
|
917
|
+
const aIndex = next.findIndex((body) => body.id === constraint.a);
|
|
918
|
+
const bIndex = next.findIndex((body) => body.id === constraint.b);
|
|
919
|
+
if (aIndex < 0 || bIndex < 0) {
|
|
920
|
+
throw new Error(`3D physics constraint ${constraint.id} references missing body`);
|
|
921
|
+
}
|
|
922
|
+
const a = next[aIndex];
|
|
923
|
+
const b = next[bIndex];
|
|
924
|
+
if (constraint.type === 'fixed') {
|
|
925
|
+
if (b.kind === 'static') {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const target = {
|
|
929
|
+
...b,
|
|
930
|
+
x: quantizeDistance(a.x + constraint.offsetX),
|
|
931
|
+
y: quantizeDistance(a.y + constraint.offsetY),
|
|
932
|
+
z: quantizeDistance(a.z + constraint.offsetZ),
|
|
933
|
+
vx: a.vx,
|
|
934
|
+
vy: a.vy,
|
|
935
|
+
vz: a.vz,
|
|
936
|
+
};
|
|
937
|
+
if ((0, canonical_js_1.defaultChecksum)(target) !== (0, canonical_js_1.defaultChecksum)(b)) {
|
|
938
|
+
next[bIndex] = target;
|
|
939
|
+
corrections += 1;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
const dx = b.x - a.x;
|
|
944
|
+
const dy = b.y - a.y;
|
|
945
|
+
const dz = b.z - a.z;
|
|
946
|
+
const length = deterministicSqrt(dx * dx + dy * dy + dz * dz);
|
|
947
|
+
if (length === 0 || length === constraint.restLength) {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const excess = length - constraint.restLength;
|
|
951
|
+
const nx = dx / length;
|
|
952
|
+
const ny = dy / length;
|
|
953
|
+
const nz = dz / length;
|
|
954
|
+
const aShare = a.kind === 'dynamic' && b.kind === 'dynamic' ? 0.5 : a.kind === 'dynamic' ? 1 : 0;
|
|
955
|
+
const bShare = a.kind === 'dynamic' && b.kind === 'dynamic' ? -0.5 : b.kind === 'dynamic' ? -1 : 0;
|
|
956
|
+
next[aIndex] = moveBodyBy(a, nx * excess * aShare, ny * excess * aShare, nz * excess * aShare);
|
|
957
|
+
next[bIndex] = moveBodyBy(b, nx * excess * bShare, ny * excess * bShare, nz * excess * bShare);
|
|
958
|
+
corrections += 1;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return { bodies: normalizeBodies(next), corrections };
|
|
962
|
+
}
|
|
963
|
+
function moveBodyBy(body, dx, dy, dz) {
|
|
964
|
+
if (body.kind !== 'dynamic') {
|
|
965
|
+
return body;
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
...body,
|
|
969
|
+
x: quantizeDistance(body.x + dx),
|
|
970
|
+
y: quantizeDistance(body.y + dy),
|
|
971
|
+
z: quantizeDistance(body.z + dz),
|
|
972
|
+
vx: 0,
|
|
973
|
+
vy: 0,
|
|
974
|
+
vz: 0,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function integrateAngularState(body, dt) {
|
|
978
|
+
if (body.rx === undefined && body.ry === undefined && body.rz === undefined && body.avx === undefined && body.avy === undefined && body.avz === undefined) {
|
|
979
|
+
return {};
|
|
980
|
+
}
|
|
981
|
+
const inertia = body.inertia ?? 1;
|
|
982
|
+
return {
|
|
983
|
+
rx: quantizeAngle((body.rx ?? 0) + ((body.avx ?? 0) / inertia) * dt),
|
|
984
|
+
ry: quantizeAngle((body.ry ?? 0) + ((body.avy ?? 0) / inertia) * dt),
|
|
985
|
+
rz: quantizeAngle((body.rz ?? 0) + ((body.avz ?? 0) / inertia) * dt),
|
|
986
|
+
avx: body.avx ?? 0,
|
|
987
|
+
avy: body.avy ?? 0,
|
|
988
|
+
avz: body.avz ?? 0,
|
|
989
|
+
inertia,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function broadphaseCandidatePairs(bodies) {
|
|
993
|
+
const entries = bodies
|
|
994
|
+
.map((body) => ({ body, bounds: toBounds(body) }))
|
|
995
|
+
.sort((left, right) => left.bounds.minX - right.bounds.minX || compareStrings(left.body.id, right.body.id));
|
|
996
|
+
const pairs = [];
|
|
997
|
+
for (let leftIndex = 0; leftIndex < entries.length; leftIndex += 1) {
|
|
998
|
+
const left = entries[leftIndex];
|
|
999
|
+
for (let rightIndex = leftIndex + 1; rightIndex < entries.length; rightIndex += 1) {
|
|
1000
|
+
const right = entries[rightIndex];
|
|
1001
|
+
if (right.bounds.minX > left.bounds.maxX) {
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
if (right.bounds.minY > left.bounds.maxY || right.bounds.maxY < left.bounds.minY || right.bounds.minZ > left.bounds.maxZ || right.bounds.maxZ < left.bounds.minZ) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
pairs.push({ left: left.body, right: right.body, leftBounds: left.bounds, rightBounds: right.bounds });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return pairs.sort((left, right) => compareStrings(left.left.id, right.left.id) || compareStrings(left.right.id, right.right.id));
|
|
1011
|
+
}
|
|
1012
|
+
function normalizeBodies(bodies) {
|
|
1013
|
+
const sorted = bodies.map((body) => (0, canonical_js_1.cloneCanonical)(body)).sort((left, right) => compareStrings(left.id, right.id));
|
|
1014
|
+
const duplicate = sorted.find((body, index) => index > 0 && body.id === sorted[index - 1].id);
|
|
1015
|
+
if (duplicate !== undefined) {
|
|
1016
|
+
throw new Error(`Duplicate deterministic physics body id ${duplicate.id}`);
|
|
1017
|
+
}
|
|
1018
|
+
return sorted;
|
|
1019
|
+
}
|
|
1020
|
+
function queryBodyAllowed(body, query) {
|
|
1021
|
+
if (query.layerMask !== undefined && (body.layer & query.layerMask) === 0) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
if (!queryKindAllowsBody3D(body, query)) {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
const hitTriggers = query.hitTriggers ?? query.includeTriggers ?? true;
|
|
1028
|
+
const hitSensors = query.hitSensors ?? query.includeSensors ?? true;
|
|
1029
|
+
const hitSolids = query.hitSolids ?? true;
|
|
1030
|
+
if (body.trigger === true && hitTriggers !== true) {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
if (body.sensor === true && hitSensors !== true) {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
return body.trigger === true || body.sensor === true || hitSolids;
|
|
1037
|
+
}
|
|
1038
|
+
function queryKindAllowsBody3D(body, query) {
|
|
1039
|
+
const kindFilterSpecified = query.hitStatics !== undefined || query.hitDynamics !== undefined || query.hitKinematics !== undefined;
|
|
1040
|
+
if (!kindFilterSpecified) {
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
if (body.kind === 'static') {
|
|
1044
|
+
return query.hitStatics === true;
|
|
1045
|
+
}
|
|
1046
|
+
if (body.kind === 'dynamic') {
|
|
1047
|
+
return query.hitDynamics === true;
|
|
1048
|
+
}
|
|
1049
|
+
return query.hitKinematics === true;
|
|
1050
|
+
}
|
|
1051
|
+
function toBounds(body) {
|
|
1052
|
+
return shapePoseBounds(bodyPose(body));
|
|
1053
|
+
}
|
|
1054
|
+
function shapeBounds(x, y, z, shape) {
|
|
1055
|
+
return shapePoseBounds({ x, y, z, shape });
|
|
1056
|
+
}
|
|
1057
|
+
function bodyPose(body) {
|
|
1058
|
+
return { x: body.x, y: body.y, z: body.z, rx: body.rx, ry: body.ry, rz: body.rz, shape: body.shape };
|
|
1059
|
+
}
|
|
1060
|
+
function shapePoseBounds(pose) {
|
|
1061
|
+
const { x, y, z, shape } = pose;
|
|
1062
|
+
if (shape.type === 'box') {
|
|
1063
|
+
return boundsFromPoints(boxLocalCorners(shape).map((point) => translateRotatedPoint(point, pose)));
|
|
1064
|
+
}
|
|
1065
|
+
if (shape.type === 'mesh') {
|
|
1066
|
+
if (shape.vertices.length < 3 || shape.vertices.length % 3 !== 0) {
|
|
1067
|
+
throw new Error('3D mesh collider requires triangle-list vertices');
|
|
1068
|
+
}
|
|
1069
|
+
return boundsFromPoints(shape.vertices.map((vertex) => translateRotatedPoint(vertex, pose)));
|
|
1070
|
+
}
|
|
1071
|
+
const radius = shape.type === 'sphere' ? shape.radius : shape.radius;
|
|
1072
|
+
const halfY = shape.type === 'capsule' ? shape.halfHeight + radius : radius;
|
|
1073
|
+
return { minX: x - radius, maxX: x + radius, minY: y - halfY, maxY: y + halfY, minZ: z - radius, maxZ: z + radius };
|
|
1074
|
+
}
|
|
1075
|
+
function exactShapeRejectsAabbFalsePositive() {
|
|
1076
|
+
const world = createDeterministicPhysicsWorld3D([{
|
|
1077
|
+
id: 'diagonal-sphere',
|
|
1078
|
+
kind: 'static',
|
|
1079
|
+
x: 19,
|
|
1080
|
+
y: 19,
|
|
1081
|
+
z: 19,
|
|
1082
|
+
vx: 0,
|
|
1083
|
+
vy: 0,
|
|
1084
|
+
vz: 0,
|
|
1085
|
+
shape: { type: 'sphere', radius: 10 },
|
|
1086
|
+
layer: 1,
|
|
1087
|
+
mask: 1,
|
|
1088
|
+
}]);
|
|
1089
|
+
return Number(overlapDeterministicPhysics3D(world, {
|
|
1090
|
+
x: 0,
|
|
1091
|
+
y: 0,
|
|
1092
|
+
z: 0,
|
|
1093
|
+
shape: { type: 'sphere', radius: 10 },
|
|
1094
|
+
layerMask: 1,
|
|
1095
|
+
}).length === 0);
|
|
1096
|
+
}
|
|
1097
|
+
function exactMeshRaycastProof() {
|
|
1098
|
+
const world = createDeterministicPhysicsWorld3D([{
|
|
1099
|
+
id: 'mesh-triangle',
|
|
1100
|
+
kind: 'static',
|
|
1101
|
+
x: 0,
|
|
1102
|
+
y: 0,
|
|
1103
|
+
z: 0,
|
|
1104
|
+
vx: 0,
|
|
1105
|
+
vy: 0,
|
|
1106
|
+
vz: 0,
|
|
1107
|
+
shape: { type: 'mesh', vertices: [{ x: 4, y: -1, z: -1 }, { x: 4, y: 1, z: -1 }, { x: 4, y: 0, z: 1 }] },
|
|
1108
|
+
layer: 1,
|
|
1109
|
+
mask: 1,
|
|
1110
|
+
}]);
|
|
1111
|
+
const hit = raycastDeterministicPhysics3D(world, { x: 0, y: 0, z: 0, dx: 1, dy: 0, dz: 0, maxDistance: 8, layerMask: 1 });
|
|
1112
|
+
const miss = raycastDeterministicPhysics3D(world, { x: 0, y: 0.95, z: 0.95, dx: 1, dy: 0, dz: 0, maxDistance: 8, layerMask: 1 });
|
|
1113
|
+
return Number(hit[0]?.bodyId === 'mesh-triangle' && hit[0]?.distance === 4 && miss.length === 0);
|
|
1114
|
+
}
|
|
1115
|
+
function exactMeshOverlapRejectProof() {
|
|
1116
|
+
const world = createDeterministicPhysicsWorld3D([{
|
|
1117
|
+
id: 'mesh-triangle',
|
|
1118
|
+
kind: 'static',
|
|
1119
|
+
x: 0,
|
|
1120
|
+
y: 0,
|
|
1121
|
+
z: 0,
|
|
1122
|
+
vx: 0,
|
|
1123
|
+
vy: 0,
|
|
1124
|
+
vz: 0,
|
|
1125
|
+
shape: { type: 'mesh', vertices: [{ x: 4, y: -1, z: -1 }, { x: 4, y: 1, z: -1 }, { x: 4, y: 0, z: 1 }] },
|
|
1126
|
+
layer: 1,
|
|
1127
|
+
mask: 1,
|
|
1128
|
+
}]);
|
|
1129
|
+
return Number(overlapDeterministicPhysics3D(world, {
|
|
1130
|
+
shape: { type: 'sphere', radius: 0.1 },
|
|
1131
|
+
x: 4,
|
|
1132
|
+
y: 0.95,
|
|
1133
|
+
z: 0.95,
|
|
1134
|
+
layerMask: 1,
|
|
1135
|
+
}).length === 0);
|
|
1136
|
+
}
|
|
1137
|
+
function angularGeometryProof() {
|
|
1138
|
+
const world = createDeterministicPhysicsWorld3D([
|
|
1139
|
+
{
|
|
1140
|
+
id: 'rotated-box',
|
|
1141
|
+
kind: 'static',
|
|
1142
|
+
x: 4,
|
|
1143
|
+
y: 0,
|
|
1144
|
+
z: 0,
|
|
1145
|
+
vx: 0,
|
|
1146
|
+
vy: 0,
|
|
1147
|
+
vz: 0,
|
|
1148
|
+
ry: 0.25,
|
|
1149
|
+
shape: { type: 'box', halfX: 3, halfY: 1, halfZ: 0.25 },
|
|
1150
|
+
layer: 1,
|
|
1151
|
+
mask: 1,
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
id: 'rotated-mesh',
|
|
1155
|
+
kind: 'static',
|
|
1156
|
+
x: 8,
|
|
1157
|
+
y: 0,
|
|
1158
|
+
z: 0,
|
|
1159
|
+
vx: 0,
|
|
1160
|
+
vy: 0,
|
|
1161
|
+
vz: 0,
|
|
1162
|
+
ry: 0.25,
|
|
1163
|
+
shape: { type: 'mesh', vertices: [{ x: -0.25, y: -1, z: -2 }, { x: -0.25, y: 1, z: -2 }, { x: -0.25, y: 0, z: 2 }] },
|
|
1164
|
+
layer: 1,
|
|
1165
|
+
mask: 1,
|
|
1166
|
+
},
|
|
1167
|
+
]);
|
|
1168
|
+
const boxHit = raycastDeterministicPhysics3D(world, { x: 4, y: 0, z: -4, dx: 0, dy: 0, dz: 1, maxDistance: 8, layerMask: 1 });
|
|
1169
|
+
const meshHit = raycastDeterministicPhysics3D(world, { x: 8, y: 0, z: -4, dx: 0, dy: 0, dz: 1, maxDistance: 8, layerMask: 1 });
|
|
1170
|
+
const overlap = overlapDeterministicPhysics3D(world, { shape: { type: 'sphere', radius: 0.25 }, x: 4, y: 0, z: 0, layerMask: 1 });
|
|
1171
|
+
return Number(boxHit.some((hit) => hit.bodyId === 'rotated-box')
|
|
1172
|
+
&& meshHit.some((hit) => hit.bodyId === 'rotated-mesh')
|
|
1173
|
+
&& overlap.includes('rotated-box'));
|
|
1174
|
+
}
|
|
1175
|
+
function shapesOverlap3D(left, right) {
|
|
1176
|
+
if (left.shape.type === 'mesh') {
|
|
1177
|
+
return meshOverlapsShape(left, right);
|
|
1178
|
+
}
|
|
1179
|
+
if (right.shape.type === 'mesh') {
|
|
1180
|
+
return meshOverlapsShape(right, left);
|
|
1181
|
+
}
|
|
1182
|
+
if (left.shape.type === 'box' && right.shape.type === 'box') {
|
|
1183
|
+
return boundsOverlap(shapeBounds(left.x, left.y, left.z, left.shape), shapeBounds(right.x, right.y, right.z, right.shape));
|
|
1184
|
+
}
|
|
1185
|
+
if (left.shape.type === 'sphere' && right.shape.type === 'sphere') {
|
|
1186
|
+
return squaredDistance(left, right) <= square(left.shape.radius + right.shape.radius);
|
|
1187
|
+
}
|
|
1188
|
+
if (left.shape.type === 'sphere' && right.shape.type === 'box') {
|
|
1189
|
+
return sphereBoxOverlap(left, right);
|
|
1190
|
+
}
|
|
1191
|
+
if (left.shape.type === 'box' && right.shape.type === 'sphere') {
|
|
1192
|
+
return sphereBoxOverlap(right, left);
|
|
1193
|
+
}
|
|
1194
|
+
if (left.shape.type === 'sphere' && right.shape.type === 'capsule') {
|
|
1195
|
+
return sphereCapsuleOverlap(left, right);
|
|
1196
|
+
}
|
|
1197
|
+
if (left.shape.type === 'capsule' && right.shape.type === 'sphere') {
|
|
1198
|
+
return sphereCapsuleOverlap(right, left);
|
|
1199
|
+
}
|
|
1200
|
+
if (left.shape.type === 'capsule' && right.shape.type === 'capsule') {
|
|
1201
|
+
const yGap = intervalGap(left.y - left.shape.halfHeight, left.y + left.shape.halfHeight, right.y - right.shape.halfHeight, right.y + right.shape.halfHeight);
|
|
1202
|
+
return square(left.x - right.x) + square(left.z - right.z) + square(yGap) <= square(left.shape.radius + right.shape.radius);
|
|
1203
|
+
}
|
|
1204
|
+
if (left.shape.type === 'capsule' && right.shape.type === 'box') {
|
|
1205
|
+
return capsuleBoxOverlap(left, right);
|
|
1206
|
+
}
|
|
1207
|
+
return capsuleBoxOverlap(right, left);
|
|
1208
|
+
}
|
|
1209
|
+
function sphereBoxOverlap(sphere, box) {
|
|
1210
|
+
const bounds = shapeBounds(box.x, box.y, box.z, box.shape);
|
|
1211
|
+
const closestX = clamp(sphere.x, bounds.minX, bounds.maxX);
|
|
1212
|
+
const closestY = clamp(sphere.y, bounds.minY, bounds.maxY);
|
|
1213
|
+
const closestZ = clamp(sphere.z, bounds.minZ, bounds.maxZ);
|
|
1214
|
+
return square(sphere.x - closestX) + square(sphere.y - closestY) + square(sphere.z - closestZ) <= square(sphere.shape.radius) + 1e-9;
|
|
1215
|
+
}
|
|
1216
|
+
function sphereCapsuleOverlap(sphere, capsule) {
|
|
1217
|
+
const closestY = clamp(sphere.y, capsule.y - capsule.shape.halfHeight, capsule.y + capsule.shape.halfHeight);
|
|
1218
|
+
return square(sphere.x - capsule.x) + square(sphere.y - closestY) + square(sphere.z - capsule.z) <= square(sphere.shape.radius + capsule.shape.radius) + 1e-9;
|
|
1219
|
+
}
|
|
1220
|
+
function capsuleBoxOverlap(capsule, box) {
|
|
1221
|
+
const bounds = shapeBounds(box.x, box.y, box.z, box.shape);
|
|
1222
|
+
const closestX = clamp(capsule.x, bounds.minX, bounds.maxX);
|
|
1223
|
+
const closestY = clamp(capsule.y, bounds.minY - capsule.shape.halfHeight, bounds.maxY + capsule.shape.halfHeight);
|
|
1224
|
+
const closestZ = clamp(capsule.z, bounds.minZ, bounds.maxZ);
|
|
1225
|
+
return square(capsule.x - closestX) + square(capsule.y - closestY) + square(capsule.z - closestZ) <= square(capsule.shape.radius) + 1e-9;
|
|
1226
|
+
}
|
|
1227
|
+
function meshOverlapsShape(mesh, other) {
|
|
1228
|
+
const triangles = meshTriangles(meshPose(mesh, mesh.shape));
|
|
1229
|
+
const otherShape = other.shape;
|
|
1230
|
+
if (otherShape.type === 'sphere') {
|
|
1231
|
+
return triangles.some((triangle) => pointTriangleDistanceSquared(other, triangle) <= square(otherShape.radius) + 1e-9);
|
|
1232
|
+
}
|
|
1233
|
+
if (otherShape.type === 'capsule') {
|
|
1234
|
+
const start = { x: other.x, y: other.y - otherShape.halfHeight, z: other.z };
|
|
1235
|
+
const end = { x: other.x, y: other.y + otherShape.halfHeight, z: other.z };
|
|
1236
|
+
return triangles.some((triangle) => segmentTriangleDistanceSquared(start, end, triangle) <= square(otherShape.radius) + 1e-9);
|
|
1237
|
+
}
|
|
1238
|
+
if (otherShape.type === 'box') {
|
|
1239
|
+
const bounds = shapeBounds(other.x, other.y, other.z, otherShape);
|
|
1240
|
+
return triangles.some((triangle) => triangleBoxOverlap(triangle, bounds));
|
|
1241
|
+
}
|
|
1242
|
+
return triangles.some((left) => meshTriangles({ ...other, shape: otherShape }).some((right) => trianglesOverlap(left, right)));
|
|
1243
|
+
}
|
|
1244
|
+
function meshTriangles(pose) {
|
|
1245
|
+
const shape = pose.shape;
|
|
1246
|
+
if (shape.vertices.length < 3 || shape.vertices.length % 3 !== 0) {
|
|
1247
|
+
throw new Error('3D mesh collider requires triangle-list vertices');
|
|
1248
|
+
}
|
|
1249
|
+
const triangles = [];
|
|
1250
|
+
for (let index = 0; index < shape.vertices.length; index += 3) {
|
|
1251
|
+
triangles.push({
|
|
1252
|
+
a: translateRotatedPoint(shape.vertices[index], pose),
|
|
1253
|
+
b: translateRotatedPoint(shape.vertices[index + 1], pose),
|
|
1254
|
+
c: translateRotatedPoint(shape.vertices[index + 2], pose),
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
return triangles;
|
|
1258
|
+
}
|
|
1259
|
+
function meshPose(pose, shape) {
|
|
1260
|
+
return { ...pose, shape };
|
|
1261
|
+
}
|
|
1262
|
+
function triangleBoxOverlap(triangle, bounds) {
|
|
1263
|
+
const corners = boxCorners(bounds);
|
|
1264
|
+
const edges = triangleEdges(triangle);
|
|
1265
|
+
const triangleBounds = boundsFromPoints([triangle.a, triangle.b, triangle.c]);
|
|
1266
|
+
if (!boundsOverlap(triangleBounds, bounds)) {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
if ([triangle.a, triangle.b, triangle.c].some((point) => pointInBounds(point, bounds))) {
|
|
1270
|
+
return true;
|
|
1271
|
+
}
|
|
1272
|
+
if (corners.some((corner) => pointTriangleDistanceSquared(corner, triangle) <= 1e-9)) {
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
if (edges.some((edge) => segmentAabbIntersects(edge.a, edge.b, bounds))) {
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
return boxEdges(bounds).some((edge) => segmentTriangleIntersects(edge.a, edge.b, triangle));
|
|
1279
|
+
}
|
|
1280
|
+
function boxLocalCorners(shape) {
|
|
1281
|
+
const corners = [];
|
|
1282
|
+
for (const x of [-shape.halfX, shape.halfX]) {
|
|
1283
|
+
for (const y of [-shape.halfY, shape.halfY]) {
|
|
1284
|
+
for (const z of [-shape.halfZ, shape.halfZ]) {
|
|
1285
|
+
corners.push({ x, y, z });
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return corners;
|
|
1290
|
+
}
|
|
1291
|
+
function translateRotatedPoint(point, pose) {
|
|
1292
|
+
const rotated = rotateQuarterTurns(point, pose.rx ?? 0, pose.ry ?? 0, pose.rz ?? 0);
|
|
1293
|
+
return { x: pose.x + rotated.x, y: pose.y + rotated.y, z: pose.z + rotated.z };
|
|
1294
|
+
}
|
|
1295
|
+
function rotateQuarterTurns(point, rx, ry, rz) {
|
|
1296
|
+
let rotated = point;
|
|
1297
|
+
for (let index = 0; index < quarterTurns(rx); index += 1) {
|
|
1298
|
+
rotated = { x: rotated.x, y: -rotated.z, z: rotated.y };
|
|
1299
|
+
}
|
|
1300
|
+
for (let index = 0; index < quarterTurns(ry); index += 1) {
|
|
1301
|
+
rotated = { x: rotated.z, y: rotated.y, z: -rotated.x };
|
|
1302
|
+
}
|
|
1303
|
+
for (let index = 0; index < quarterTurns(rz); index += 1) {
|
|
1304
|
+
rotated = { x: -rotated.y, y: rotated.x, z: rotated.z };
|
|
1305
|
+
}
|
|
1306
|
+
return rotated;
|
|
1307
|
+
}
|
|
1308
|
+
function quarterTurns(turns) {
|
|
1309
|
+
const wrapped = ((Math.round(turns * 4) % 4) + 4) % 4;
|
|
1310
|
+
return wrapped;
|
|
1311
|
+
}
|
|
1312
|
+
function trianglesOverlap(left, right) {
|
|
1313
|
+
const overlapCount = Number(triangleEdges(left).some((edge) => segmentTriangleIntersects(edge.a, edge.b, right)))
|
|
1314
|
+
+ Number(triangleEdges(right).some((edge) => segmentTriangleIntersects(edge.a, edge.b, left)))
|
|
1315
|
+
+ Number(pointTriangleDistanceSquared(left.a, right) <= 1e-9)
|
|
1316
|
+
+ Number(pointTriangleDistanceSquared(right.a, left) <= 1e-9);
|
|
1317
|
+
return overlapCount > 0;
|
|
1318
|
+
}
|
|
1319
|
+
function rayMeshDistance(pose, query) {
|
|
1320
|
+
const distances = meshTriangles(pose)
|
|
1321
|
+
.map((triangle) => rayTriangleDistance(query, triangle))
|
|
1322
|
+
.filter((distance) => distance !== undefined)
|
|
1323
|
+
.sort((left, right) => left - right);
|
|
1324
|
+
return distances[0];
|
|
1325
|
+
}
|
|
1326
|
+
function rayExpandedMeshDistance(pose, query, radius) {
|
|
1327
|
+
const distances = [];
|
|
1328
|
+
for (const triangle of meshTriangles(pose)) {
|
|
1329
|
+
const triangleDistance = rayTriangleDistance(query, triangle);
|
|
1330
|
+
if (triangleDistance !== undefined) {
|
|
1331
|
+
distances.push(triangleDistance);
|
|
1332
|
+
}
|
|
1333
|
+
for (const edge of triangleEdges(triangle)) {
|
|
1334
|
+
const edgeDistance = raySegmentDistance(query.x, query.y, query.z, query.dx, query.dy, query.dz, edge.a, edge.b, radius, query.maxDistance);
|
|
1335
|
+
if (edgeDistance !== undefined) {
|
|
1336
|
+
distances.push(edgeDistance);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
for (const vertex of [triangle.a, triangle.b, triangle.c]) {
|
|
1340
|
+
const vertexDistance = raySphereDistance(query.x, query.y, query.z, query.dx, query.dy, query.dz, vertex.x, vertex.y, vertex.z, radius, query.maxDistance);
|
|
1341
|
+
if (vertexDistance !== undefined) {
|
|
1342
|
+
distances.push(vertexDistance);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return distances.sort((left, right) => left - right)[0];
|
|
1347
|
+
}
|
|
1348
|
+
function rayTriangleDistance(query, triangle) {
|
|
1349
|
+
const edge1 = subtractPoint(triangle.b, triangle.a);
|
|
1350
|
+
const edge2 = subtractPoint(triangle.c, triangle.a);
|
|
1351
|
+
const direction = { x: query.dx, y: query.dy, z: query.dz };
|
|
1352
|
+
const h = cross(direction, edge2);
|
|
1353
|
+
const determinant = dot(edge1, h);
|
|
1354
|
+
if (Math.abs(determinant) <= 1e-9) {
|
|
1355
|
+
return undefined;
|
|
1356
|
+
}
|
|
1357
|
+
const inverseDeterminant = 1 / determinant;
|
|
1358
|
+
const s = subtractPoint({ x: query.x, y: query.y, z: query.z }, triangle.a);
|
|
1359
|
+
const u = inverseDeterminant * dot(s, h);
|
|
1360
|
+
if (u < 0 || u > 1) {
|
|
1361
|
+
return undefined;
|
|
1362
|
+
}
|
|
1363
|
+
const q = cross(s, edge1);
|
|
1364
|
+
const v = inverseDeterminant * dot(direction, q);
|
|
1365
|
+
if (v < 0 || u + v > 1) {
|
|
1366
|
+
return undefined;
|
|
1367
|
+
}
|
|
1368
|
+
const distance = inverseDeterminant * dot(edge2, q);
|
|
1369
|
+
return distance >= 0 && distance <= query.maxDistance ? quantizeDistance(distance) : undefined;
|
|
1370
|
+
}
|
|
1371
|
+
function segmentTriangleIntersects(start, end, triangle) {
|
|
1372
|
+
const delta = subtractPoint(end, start);
|
|
1373
|
+
const maxDistance = deterministicSqrt(dot(delta, delta));
|
|
1374
|
+
if (maxDistance === 0) {
|
|
1375
|
+
return pointTriangleDistanceSquared(start, triangle) <= 1e-9;
|
|
1376
|
+
}
|
|
1377
|
+
return rayTriangleDistance({
|
|
1378
|
+
x: start.x,
|
|
1379
|
+
y: start.y,
|
|
1380
|
+
z: start.z,
|
|
1381
|
+
dx: quantizeDistance(delta.x / maxDistance),
|
|
1382
|
+
dy: quantizeDistance(delta.y / maxDistance),
|
|
1383
|
+
dz: quantizeDistance(delta.z / maxDistance),
|
|
1384
|
+
maxDistance,
|
|
1385
|
+
}, triangle) !== undefined;
|
|
1386
|
+
}
|
|
1387
|
+
function segmentTriangleDistanceSquared(start, end, triangle) {
|
|
1388
|
+
if (segmentTriangleIntersects(start, end, triangle)) {
|
|
1389
|
+
return 0;
|
|
1390
|
+
}
|
|
1391
|
+
const candidates = [
|
|
1392
|
+
pointTriangleDistanceSquared(start, triangle),
|
|
1393
|
+
pointTriangleDistanceSquared(end, triangle),
|
|
1394
|
+
distancePointSegmentSquared(triangle.a, start, end),
|
|
1395
|
+
distancePointSegmentSquared(triangle.b, start, end),
|
|
1396
|
+
distancePointSegmentSquared(triangle.c, start, end),
|
|
1397
|
+
...triangleEdges(triangle).map((edge) => segmentSegmentDistanceSquared(start, end, edge.a, edge.b)),
|
|
1398
|
+
];
|
|
1399
|
+
return Math.min(...candidates);
|
|
1400
|
+
}
|
|
1401
|
+
function pointTriangleDistanceSquared(point, triangle) {
|
|
1402
|
+
const ab = subtractPoint(triangle.b, triangle.a);
|
|
1403
|
+
const ac = subtractPoint(triangle.c, triangle.a);
|
|
1404
|
+
const ap = subtractPoint(point, triangle.a);
|
|
1405
|
+
const d1 = dot(ab, ap);
|
|
1406
|
+
const d2 = dot(ac, ap);
|
|
1407
|
+
if (d1 <= 0 && d2 <= 0) {
|
|
1408
|
+
return squaredDistance(point, triangle.a);
|
|
1409
|
+
}
|
|
1410
|
+
const bp = subtractPoint(point, triangle.b);
|
|
1411
|
+
const d3 = dot(ab, bp);
|
|
1412
|
+
const d4 = dot(ac, bp);
|
|
1413
|
+
if (d3 >= 0 && d4 <= d3) {
|
|
1414
|
+
return squaredDistance(point, triangle.b);
|
|
1415
|
+
}
|
|
1416
|
+
const vc = d1 * d4 - d3 * d2;
|
|
1417
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
1418
|
+
const v = d1 / (d1 - d3);
|
|
1419
|
+
return squaredDistance(point, addPoint(triangle.a, scalePoint(ab, v)));
|
|
1420
|
+
}
|
|
1421
|
+
const cp = subtractPoint(point, triangle.c);
|
|
1422
|
+
const d5 = dot(ab, cp);
|
|
1423
|
+
const d6 = dot(ac, cp);
|
|
1424
|
+
if (d6 >= 0 && d5 <= d6) {
|
|
1425
|
+
return squaredDistance(point, triangle.c);
|
|
1426
|
+
}
|
|
1427
|
+
const vb = d5 * d2 - d1 * d6;
|
|
1428
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
1429
|
+
const w = d2 / (d2 - d6);
|
|
1430
|
+
return squaredDistance(point, addPoint(triangle.a, scalePoint(ac, w)));
|
|
1431
|
+
}
|
|
1432
|
+
const va = d3 * d6 - d5 * d4;
|
|
1433
|
+
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) {
|
|
1434
|
+
const w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
|
1435
|
+
return squaredDistance(point, addPoint(triangle.b, scalePoint(subtractPoint(triangle.c, triangle.b), w)));
|
|
1436
|
+
}
|
|
1437
|
+
const denominator = 1 / (va + vb + vc);
|
|
1438
|
+
const v = vb * denominator;
|
|
1439
|
+
const w = vc * denominator;
|
|
1440
|
+
return squaredDistance(point, addPoint(triangle.a, addPoint(scalePoint(ab, v), scalePoint(ac, w))));
|
|
1441
|
+
}
|
|
1442
|
+
function segmentSegmentDistanceSquared(leftA, leftB, rightA, rightB) {
|
|
1443
|
+
const u = subtractPoint(leftB, leftA);
|
|
1444
|
+
const v = subtractPoint(rightB, rightA);
|
|
1445
|
+
const w = subtractPoint(leftA, rightA);
|
|
1446
|
+
const a = dot(u, u);
|
|
1447
|
+
const b = dot(u, v);
|
|
1448
|
+
const c = dot(v, v);
|
|
1449
|
+
const d = dot(u, w);
|
|
1450
|
+
const e = dot(v, w);
|
|
1451
|
+
const denominator = a * c - b * b;
|
|
1452
|
+
const s = denominator === 0 ? 0 : clamp((b * e - c * d) / denominator, 0, 1);
|
|
1453
|
+
const t = c === 0 ? 0 : clamp((b * s + e) / c, 0, 1);
|
|
1454
|
+
const closestLeft = addPoint(leftA, scalePoint(u, s));
|
|
1455
|
+
const closestRight = addPoint(rightA, scalePoint(v, t));
|
|
1456
|
+
return squaredDistance(closestLeft, closestRight);
|
|
1457
|
+
}
|
|
1458
|
+
function distancePointSegmentSquared(point, start, end) {
|
|
1459
|
+
const segment = subtractPoint(end, start);
|
|
1460
|
+
const lengthSquared = dot(segment, segment);
|
|
1461
|
+
if (lengthSquared === 0) {
|
|
1462
|
+
return squaredDistance(point, start);
|
|
1463
|
+
}
|
|
1464
|
+
const t = clamp(dot(subtractPoint(point, start), segment) / lengthSquared, 0, 1);
|
|
1465
|
+
return squaredDistance(point, addPoint(start, scalePoint(segment, t)));
|
|
1466
|
+
}
|
|
1467
|
+
function segmentAabbIntersects(start, end, bounds) {
|
|
1468
|
+
const delta = subtractPoint(end, start);
|
|
1469
|
+
const distance = deterministicSqrt(dot(delta, delta));
|
|
1470
|
+
if (distance === 0) {
|
|
1471
|
+
return pointInBounds(start, bounds);
|
|
1472
|
+
}
|
|
1473
|
+
return rayAabbDistanceExact(bounds, start.x, start.y, start.z, delta.x / distance, delta.y / distance, delta.z / distance, distance) !== undefined;
|
|
1474
|
+
}
|
|
1475
|
+
function triangleEdges(triangle) {
|
|
1476
|
+
return [
|
|
1477
|
+
{ a: triangle.a, b: triangle.b },
|
|
1478
|
+
{ a: triangle.b, b: triangle.c },
|
|
1479
|
+
{ a: triangle.c, b: triangle.a },
|
|
1480
|
+
];
|
|
1481
|
+
}
|
|
1482
|
+
function boxCorners(bounds) {
|
|
1483
|
+
const corners = [];
|
|
1484
|
+
for (const x of [bounds.minX, bounds.maxX]) {
|
|
1485
|
+
for (const y of [bounds.minY, bounds.maxY]) {
|
|
1486
|
+
for (const z of [bounds.minZ, bounds.maxZ]) {
|
|
1487
|
+
corners.push({ x, y, z });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return corners;
|
|
1492
|
+
}
|
|
1493
|
+
function boxEdges(bounds) {
|
|
1494
|
+
return roundedBoxEdges(bounds);
|
|
1495
|
+
}
|
|
1496
|
+
function boundsFromPoints(points) {
|
|
1497
|
+
const xs = points.map((point) => point.x);
|
|
1498
|
+
const ys = points.map((point) => point.y);
|
|
1499
|
+
const zs = points.map((point) => point.z);
|
|
1500
|
+
return { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys), minZ: Math.min(...zs), maxZ: Math.max(...zs) };
|
|
1501
|
+
}
|
|
1502
|
+
function pointInBounds(point, bounds) {
|
|
1503
|
+
return point.x >= bounds.minX && point.x <= bounds.maxX
|
|
1504
|
+
&& point.y >= bounds.minY && point.y <= bounds.maxY
|
|
1505
|
+
&& point.z >= bounds.minZ && point.z <= bounds.maxZ;
|
|
1506
|
+
}
|
|
1507
|
+
function subtractPoint(left, right) {
|
|
1508
|
+
return { x: left.x - right.x, y: left.y - right.y, z: left.z - right.z };
|
|
1509
|
+
}
|
|
1510
|
+
function addPoint(left, right) {
|
|
1511
|
+
return { x: left.x + right.x, y: left.y + right.y, z: left.z + right.z };
|
|
1512
|
+
}
|
|
1513
|
+
function scalePoint(point, scalar) {
|
|
1514
|
+
return { x: point.x * scalar, y: point.y * scalar, z: point.z * scalar };
|
|
1515
|
+
}
|
|
1516
|
+
function dot(left, right) {
|
|
1517
|
+
return left.x * right.x + left.y * right.y + left.z * right.z;
|
|
1518
|
+
}
|
|
1519
|
+
function cross(left, right) {
|
|
1520
|
+
return {
|
|
1521
|
+
x: left.y * right.z - left.z * right.y,
|
|
1522
|
+
y: left.z * right.x - left.x * right.z,
|
|
1523
|
+
z: left.x * right.y - left.y * right.x,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function squaredDistance(left, right) {
|
|
1527
|
+
return square(left.x - right.x) + square(left.y - right.y) + square(left.z - right.z);
|
|
1528
|
+
}
|
|
1529
|
+
function intervalGap(leftMin, leftMax, rightMin, rightMax) {
|
|
1530
|
+
return Math.max(0, rightMin - leftMax, leftMin - rightMax);
|
|
1531
|
+
}
|
|
1532
|
+
function clamp(value, min, max) {
|
|
1533
|
+
return Math.max(min, Math.min(max, value));
|
|
1534
|
+
}
|
|
1535
|
+
function inClosedRange2(first, firstMin, firstMax, second, secondMin, secondMax) {
|
|
1536
|
+
return Math.max(firstMin - first, first - firstMax, secondMin - second, second - secondMax) <= 0;
|
|
1537
|
+
}
|
|
1538
|
+
function square(value) {
|
|
1539
|
+
return value * value;
|
|
1540
|
+
}
|
|
1541
|
+
function boundsOverlap(left, right) {
|
|
1542
|
+
return left.minX <= right.maxX && left.maxX >= right.minX
|
|
1543
|
+
&& left.minY <= right.maxY && left.maxY >= right.minY
|
|
1544
|
+
&& left.minZ <= right.maxZ && left.maxZ >= right.minZ;
|
|
1545
|
+
}
|
|
1546
|
+
function rayAabbDistance(bounds, query) {
|
|
1547
|
+
const distance = rayAabbDistanceExact(bounds, query.x, query.y, query.z, query.dx, query.dy, query.dz, query.maxDistance);
|
|
1548
|
+
return distance === undefined ? undefined : quantizeDistance(distance);
|
|
1549
|
+
}
|
|
1550
|
+
function rayBodyDistance(body, query) {
|
|
1551
|
+
if (body.shape.type === 'sphere') {
|
|
1552
|
+
return raySphereDistance(query.x, query.y, query.z, query.dx, query.dy, query.dz, body.x, body.y, body.z, body.shape.radius, query.maxDistance);
|
|
1553
|
+
}
|
|
1554
|
+
if (body.shape.type === 'capsule') {
|
|
1555
|
+
return rayCapsuleDistance(query.x, query.y, query.z, query.dx, query.dy, query.dz, body.x, body.y, body.z, body.shape.radius, body.shape.halfHeight, query.maxDistance);
|
|
1556
|
+
}
|
|
1557
|
+
if (body.shape.type === 'mesh') {
|
|
1558
|
+
return rayMeshDistance(meshPose(bodyPose(body), body.shape), query);
|
|
1559
|
+
}
|
|
1560
|
+
return rayAabbDistance(toBounds(body), query);
|
|
1561
|
+
}
|
|
1562
|
+
function shapeCastBodyDistance(body, query) {
|
|
1563
|
+
if (overlapDeterministicPhysics3D(createDeterministicPhysicsWorld3D([body]), query).includes(body.id)) {
|
|
1564
|
+
return 0;
|
|
1565
|
+
}
|
|
1566
|
+
if (query.shape.type === 'sphere') {
|
|
1567
|
+
if (body.shape.type === 'mesh') {
|
|
1568
|
+
return rayExpandedMeshDistance(meshPose(bodyPose(body), body.shape), query, query.shape.radius);
|
|
1569
|
+
}
|
|
1570
|
+
if (body.shape.type === 'sphere') {
|
|
1571
|
+
return raySphereDistance(query.x, query.y, query.z, query.dx, query.dy, query.dz, body.x, body.y, body.z, body.shape.radius + query.shape.radius, query.maxDistance);
|
|
1572
|
+
}
|
|
1573
|
+
if (body.shape.type === 'capsule') {
|
|
1574
|
+
return rayCapsuleDistance(query.x, query.y, query.z, query.dx, query.dy, query.dz, body.x, body.y, body.z, body.shape.radius + query.shape.radius, body.shape.halfHeight, query.maxDistance);
|
|
1575
|
+
}
|
|
1576
|
+
return rayRoundedBoxDistance(query.x, query.y, query.z, query.dx, query.dy, query.dz, toBounds(body), query.shape.radius, query.maxDistance);
|
|
1577
|
+
}
|
|
1578
|
+
const queryBounds = shapeBounds(query.x, query.y, query.z, query.shape);
|
|
1579
|
+
const expanded = expandBoundsByBounds(toBounds(body), {
|
|
1580
|
+
minX: query.x - queryBounds.minX,
|
|
1581
|
+
maxX: queryBounds.maxX - query.x,
|
|
1582
|
+
minY: query.y - queryBounds.minY,
|
|
1583
|
+
maxY: queryBounds.maxY - query.y,
|
|
1584
|
+
minZ: query.z - queryBounds.minZ,
|
|
1585
|
+
maxZ: queryBounds.maxZ - query.z,
|
|
1586
|
+
});
|
|
1587
|
+
const distance = rayAabbDistanceExact(expanded, query.x, query.y, query.z, query.dx, query.dy, query.dz, query.maxDistance);
|
|
1588
|
+
if (distance === undefined) {
|
|
1589
|
+
return undefined;
|
|
1590
|
+
}
|
|
1591
|
+
const quantized = quantizeDistance(distance);
|
|
1592
|
+
const overlapAtHit = shapesOverlap3D({ x: query.x + query.dx * quantized, y: query.y + query.dy * quantized, z: query.z + query.dz * quantized, shape: query.shape }, bodyPose(body));
|
|
1593
|
+
return [undefined, quantized][Number(overlapAtHit)];
|
|
1594
|
+
}
|
|
1595
|
+
function rayRoundedBoxDistance(x, y, z, dx, dy, dz, bounds, radius, maxDistance) {
|
|
1596
|
+
const candidates = [];
|
|
1597
|
+
addFaceCandidate(candidates, 'x', bounds.minX - radius, x, y, z, dx, dy, dz, bounds, maxDistance);
|
|
1598
|
+
addFaceCandidate(candidates, 'x', bounds.maxX + radius, x, y, z, dx, dy, dz, bounds, maxDistance);
|
|
1599
|
+
addFaceCandidate(candidates, 'y', bounds.minY - radius, x, y, z, dx, dy, dz, bounds, maxDistance);
|
|
1600
|
+
addFaceCandidate(candidates, 'y', bounds.maxY + radius, x, y, z, dx, dy, dz, bounds, maxDistance);
|
|
1601
|
+
addFaceCandidate(candidates, 'z', bounds.minZ - radius, x, y, z, dx, dy, dz, bounds, maxDistance);
|
|
1602
|
+
addFaceCandidate(candidates, 'z', bounds.maxZ + radius, x, y, z, dx, dy, dz, bounds, maxDistance);
|
|
1603
|
+
for (const edge of roundedBoxEdges(bounds)) {
|
|
1604
|
+
const distance = raySegmentDistance(x, y, z, dx, dy, dz, edge.a, edge.b, radius, maxDistance);
|
|
1605
|
+
if (distance !== undefined) {
|
|
1606
|
+
candidates.push(distance);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
for (const cornerX of [bounds.minX, bounds.maxX]) {
|
|
1610
|
+
for (const cornerY of [bounds.minY, bounds.maxY]) {
|
|
1611
|
+
for (const cornerZ of [bounds.minZ, bounds.maxZ]) {
|
|
1612
|
+
const distance = raySphereDistance(x, y, z, dx, dy, dz, cornerX, cornerY, cornerZ, radius, maxDistance);
|
|
1613
|
+
if (distance !== undefined) {
|
|
1614
|
+
candidates.push(distance);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
const distance = Math.min(...candidates);
|
|
1620
|
+
return Number.isFinite(distance) ? distance : undefined;
|
|
1621
|
+
}
|
|
1622
|
+
function addFaceCandidate(candidates, axis, plane, x, y, z, dx, dy, dz, bounds, maxDistance) {
|
|
1623
|
+
let direction = dx;
|
|
1624
|
+
let origin = x;
|
|
1625
|
+
if (axis === 'y') {
|
|
1626
|
+
direction = dy;
|
|
1627
|
+
origin = y;
|
|
1628
|
+
}
|
|
1629
|
+
else if (axis === 'z') {
|
|
1630
|
+
direction = dz;
|
|
1631
|
+
origin = z;
|
|
1632
|
+
}
|
|
1633
|
+
if (direction === 0) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const distance = (plane - origin) / direction;
|
|
1637
|
+
if (distance < 0 || distance > maxDistance) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const hitX = x + dx * distance;
|
|
1641
|
+
const hitY = y + dy * distance;
|
|
1642
|
+
const hitZ = z + dz * distance;
|
|
1643
|
+
let inside = inClosedRange2(hitY, bounds.minY, bounds.maxY, hitZ, bounds.minZ, bounds.maxZ);
|
|
1644
|
+
if (axis === 'y') {
|
|
1645
|
+
inside = inClosedRange2(hitX, bounds.minX, bounds.maxX, hitZ, bounds.minZ, bounds.maxZ);
|
|
1646
|
+
}
|
|
1647
|
+
else if (axis === 'z') {
|
|
1648
|
+
inside = inClosedRange2(hitX, bounds.minX, bounds.maxX, hitY, bounds.minY, bounds.maxY);
|
|
1649
|
+
}
|
|
1650
|
+
if (inside) {
|
|
1651
|
+
candidates.push(quantizeDistance(distance));
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function roundedBoxEdges(bounds) {
|
|
1655
|
+
const edges = [];
|
|
1656
|
+
for (const y of [bounds.minY, bounds.maxY]) {
|
|
1657
|
+
for (const z of [bounds.minZ, bounds.maxZ]) {
|
|
1658
|
+
edges.push({ a: { x: bounds.minX, y, z }, b: { x: bounds.maxX, y, z } });
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
for (const x of [bounds.minX, bounds.maxX]) {
|
|
1662
|
+
for (const z of [bounds.minZ, bounds.maxZ]) {
|
|
1663
|
+
edges.push({ a: { x, y: bounds.minY, z }, b: { x, y: bounds.maxY, z } });
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
for (const x of [bounds.minX, bounds.maxX]) {
|
|
1667
|
+
for (const y of [bounds.minY, bounds.maxY]) {
|
|
1668
|
+
edges.push({ a: { x, y, z: bounds.minZ }, b: { x, y, z: bounds.maxZ } });
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return edges;
|
|
1672
|
+
}
|
|
1673
|
+
function raySegmentDistance(x, y, z, dx, dy, dz, a, b, radius, maxDistance) {
|
|
1674
|
+
const sx = b.x - a.x;
|
|
1675
|
+
const sy = b.y - a.y;
|
|
1676
|
+
const sz = b.z - a.z;
|
|
1677
|
+
const wx = x - a.x;
|
|
1678
|
+
const wy = y - a.y;
|
|
1679
|
+
const wz = z - a.z;
|
|
1680
|
+
const ss = sx * sx + sy * sy + sz * sz;
|
|
1681
|
+
const ds = dx * sx + dy * sy + dz * sz;
|
|
1682
|
+
const ws = wx * sx + wy * sy + wz * sz;
|
|
1683
|
+
const dd = dx * dx + dy * dy + dz * dz;
|
|
1684
|
+
const dw = dx * wx + dy * wy + dz * wz;
|
|
1685
|
+
const ww = wx * wx + wy * wy + wz * wz;
|
|
1686
|
+
const aCoeff = dd - (ds * ds) / ss;
|
|
1687
|
+
const bCoeff = 2 * (dw - (ds * ws) / ss);
|
|
1688
|
+
const cCoeff = ww - (ws * ws) / ss - radius * radius;
|
|
1689
|
+
if (aCoeff === 0) {
|
|
1690
|
+
return undefined;
|
|
1691
|
+
}
|
|
1692
|
+
const discriminant = bCoeff * bCoeff - 4 * aCoeff * cCoeff;
|
|
1693
|
+
if (discriminant < 0) {
|
|
1694
|
+
return undefined;
|
|
1695
|
+
}
|
|
1696
|
+
const root = deterministicSqrt(discriminant);
|
|
1697
|
+
const candidates = [(-bCoeff - root) / (2 * aCoeff), (-bCoeff + root) / (2 * aCoeff)]
|
|
1698
|
+
.filter((distance) => distance >= 0 && distance <= maxDistance)
|
|
1699
|
+
.sort((left, right) => left - right);
|
|
1700
|
+
for (const distance of candidates) {
|
|
1701
|
+
const t = (ws + ds * distance) / ss;
|
|
1702
|
+
if (t >= 0 && t <= 1) {
|
|
1703
|
+
return quantizeDistance(distance);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
function raySphereDistance(x, y, z, dx, dy, dz, sphereX, sphereY, sphereZ, radius, maxDistance) {
|
|
1709
|
+
const ox = x - sphereX;
|
|
1710
|
+
const oy = y - sphereY;
|
|
1711
|
+
const oz = z - sphereZ;
|
|
1712
|
+
const a = dx * dx + dy * dy + dz * dz;
|
|
1713
|
+
if (a === 0 || maxDistance < 0) {
|
|
1714
|
+
return undefined;
|
|
1715
|
+
}
|
|
1716
|
+
const b = 2 * (ox * dx + oy * dy + oz * dz);
|
|
1717
|
+
const c = ox * ox + oy * oy + oz * oz - radius * radius;
|
|
1718
|
+
if (c <= 0) {
|
|
1719
|
+
return 0;
|
|
1720
|
+
}
|
|
1721
|
+
const discriminant = b * b - 4 * a * c;
|
|
1722
|
+
if (discriminant < 0) {
|
|
1723
|
+
return undefined;
|
|
1724
|
+
}
|
|
1725
|
+
const root = deterministicSqrt(discriminant);
|
|
1726
|
+
const near = (-b - root) / (2 * a);
|
|
1727
|
+
const far = (-b + root) / (2 * a);
|
|
1728
|
+
const candidateDistance = Math.min(...[near, far].filter((candidate) => candidate >= 0));
|
|
1729
|
+
if (!Number.isFinite(candidateDistance) || candidateDistance > maxDistance) {
|
|
1730
|
+
return undefined;
|
|
1731
|
+
}
|
|
1732
|
+
return quantizeDistance(candidateDistance);
|
|
1733
|
+
}
|
|
1734
|
+
function rayCapsuleDistance(x, y, z, dx, dy, dz, capsuleX, capsuleY, capsuleZ, radius, halfHeight, maxDistance) {
|
|
1735
|
+
const candidates = [
|
|
1736
|
+
raySphereDistance(x, y, z, dx, dy, dz, capsuleX, capsuleY - halfHeight, capsuleZ, radius, maxDistance),
|
|
1737
|
+
raySphereDistance(x, y, z, dx, dy, dz, capsuleX, capsuleY + halfHeight, capsuleZ, radius, maxDistance),
|
|
1738
|
+
rayVerticalCylinderDistance(x, y, z, dx, dy, dz, capsuleX, capsuleY, capsuleZ, radius, halfHeight, maxDistance),
|
|
1739
|
+
].filter((distance) => distance !== undefined);
|
|
1740
|
+
const distance = Math.min(...candidates);
|
|
1741
|
+
return Number.isFinite(distance) ? distance : undefined;
|
|
1742
|
+
}
|
|
1743
|
+
function rayVerticalCylinderDistance(x, y, z, dx, dy, dz, centerX, centerY, centerZ, radius, halfHeight, maxDistance) {
|
|
1744
|
+
const ox = x - centerX;
|
|
1745
|
+
const oz = z - centerZ;
|
|
1746
|
+
const a = dx * dx + dz * dz;
|
|
1747
|
+
if (a === 0 || maxDistance < 0) {
|
|
1748
|
+
return undefined;
|
|
1749
|
+
}
|
|
1750
|
+
const b = 2 * (ox * dx + oz * dz);
|
|
1751
|
+
const c = ox * ox + oz * oz - radius * radius;
|
|
1752
|
+
if (c <= 0 && y >= centerY - halfHeight && y <= centerY + halfHeight) {
|
|
1753
|
+
return 0;
|
|
1754
|
+
}
|
|
1755
|
+
const discriminant = b * b - 4 * a * c;
|
|
1756
|
+
if (discriminant < 0) {
|
|
1757
|
+
return undefined;
|
|
1758
|
+
}
|
|
1759
|
+
const root = deterministicSqrt(discriminant);
|
|
1760
|
+
const candidates = [(-b - root) / (2 * a), (-b + root) / (2 * a)]
|
|
1761
|
+
.filter((distance) => distance >= 0 && distance <= maxDistance)
|
|
1762
|
+
.sort((left, right) => left - right);
|
|
1763
|
+
for (const distance of candidates) {
|
|
1764
|
+
const hitY = y + dy * distance;
|
|
1765
|
+
if (hitY >= centerY - halfHeight && hitY <= centerY + halfHeight) {
|
|
1766
|
+
return quantizeDistance(distance);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return undefined;
|
|
1770
|
+
}
|
|
1771
|
+
function rayAabbDistanceExact(bounds, x, y, z, dx, dy, dz, maxDistance) {
|
|
1772
|
+
if (maxDistance < 0) {
|
|
1773
|
+
return undefined;
|
|
1774
|
+
}
|
|
1775
|
+
let tMin = 0;
|
|
1776
|
+
let tMax = maxDistance;
|
|
1777
|
+
const axes = [
|
|
1778
|
+
{ origin: x, direction: dx, min: bounds.minX, max: bounds.maxX },
|
|
1779
|
+
{ origin: y, direction: dy, min: bounds.minY, max: bounds.maxY },
|
|
1780
|
+
{ origin: z, direction: dz, min: bounds.minZ, max: bounds.maxZ },
|
|
1781
|
+
];
|
|
1782
|
+
for (const axis of axes) {
|
|
1783
|
+
if (axis.direction === 0) {
|
|
1784
|
+
if (axis.origin < axis.min || axis.origin > axis.max) {
|
|
1785
|
+
return undefined;
|
|
1786
|
+
}
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
const inv = 1 / axis.direction;
|
|
1790
|
+
const near = (axis.min - axis.origin) * inv;
|
|
1791
|
+
const far = (axis.max - axis.origin) * inv;
|
|
1792
|
+
tMin = Math.max(tMin, Math.min(near, far));
|
|
1793
|
+
tMax = Math.min(tMax, Math.max(near, far));
|
|
1794
|
+
if (tMin > tMax) {
|
|
1795
|
+
return undefined;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return Math.max(0, tMin);
|
|
1799
|
+
}
|
|
1800
|
+
function expandBoundsByBounds(bounds, extents) {
|
|
1801
|
+
return {
|
|
1802
|
+
minX: bounds.minX - extents.maxX,
|
|
1803
|
+
maxX: bounds.maxX - extents.minX,
|
|
1804
|
+
minY: bounds.minY - extents.maxY,
|
|
1805
|
+
maxY: bounds.maxY - extents.minY,
|
|
1806
|
+
minZ: bounds.minZ - extents.maxZ,
|
|
1807
|
+
maxZ: bounds.maxZ - extents.minZ,
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
function quantizeDistance(distance) {
|
|
1811
|
+
// Pure integer rounding, not Number.prototype.toFixed — see physics2d
|
|
1812
|
+
// quantizeDistance (cross-engine half-way divergence on checksummed state).
|
|
1813
|
+
const quantized = Math.round(distance * 1e6) / 1e6;
|
|
1814
|
+
return quantized === 0 ? 0 : quantized;
|
|
1815
|
+
}
|
|
1816
|
+
function quantizeAngle(value) {
|
|
1817
|
+
const wrapped = value - Math.floor(value);
|
|
1818
|
+
const quantizedAngle = Math.round(wrapped * 1e6) / 1e6;
|
|
1819
|
+
return quantizedAngle === 0 ? 0 : quantizedAngle;
|
|
1820
|
+
}
|
|
1821
|
+
function deterministicSqrt(value) {
|
|
1822
|
+
if (value <= 0) {
|
|
1823
|
+
return 0;
|
|
1824
|
+
}
|
|
1825
|
+
// Seed the Newton iteration from the argument's magnitude (2^ceil(bits/2)):
|
|
1826
|
+
// starting from `value` itself needs ~0.5*log2(value) halving steps before
|
|
1827
|
+
// the quadratic regime, so 8 iterations only converged for tiny inputs —
|
|
1828
|
+
// at realistic fixed-point magnitudes (dx^2+dy^2 ~ 1e10) the old seed
|
|
1829
|
+
// returned results off by orders of magnitude. Every operation here is
|
|
1830
|
+
// exact integer/IEEE-deterministic arithmetic, so all peers still agree.
|
|
1831
|
+
let estimate = 1;
|
|
1832
|
+
let magnitude = value;
|
|
1833
|
+
while (magnitude >= 4) {
|
|
1834
|
+
magnitude /= 4;
|
|
1835
|
+
estimate *= 2;
|
|
1836
|
+
}
|
|
1837
|
+
if (magnitude > 1) {
|
|
1838
|
+
estimate *= 2;
|
|
1839
|
+
}
|
|
1840
|
+
for (let iteration = 0; iteration < 8; iteration += 1) {
|
|
1841
|
+
estimate = (estimate + value / estimate) / 2;
|
|
1842
|
+
}
|
|
1843
|
+
return quantizeDistance(estimate);
|
|
1844
|
+
}
|
|
1845
|
+
function countCandidatePairs(bodies) {
|
|
1846
|
+
return broadphaseCandidatePairs(bodies).length;
|
|
1847
|
+
}
|
|
1848
|
+
function detailedHitForBody(body, query, distance, castRadius) {
|
|
1849
|
+
const center = quantizePoint({
|
|
1850
|
+
x: query.x + query.dx * distance,
|
|
1851
|
+
y: query.y + query.dy * distance,
|
|
1852
|
+
z: query.z + query.dz * distance,
|
|
1853
|
+
});
|
|
1854
|
+
const normal = hitNormalForBody(body, center);
|
|
1855
|
+
const point = quantizePoint({
|
|
1856
|
+
x: center.x - normal.x * castRadius,
|
|
1857
|
+
y: center.y - normal.y * castRadius,
|
|
1858
|
+
z: center.z - normal.z * castRadius,
|
|
1859
|
+
});
|
|
1860
|
+
return {
|
|
1861
|
+
bodyId: body.id,
|
|
1862
|
+
distance,
|
|
1863
|
+
point,
|
|
1864
|
+
normal,
|
|
1865
|
+
penetration: distance === 0 ? penetrationAtPoint(body, center, castRadius) : 0,
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
function hitNormalForBody(body, point) {
|
|
1869
|
+
if (body.shape.type === 'sphere') {
|
|
1870
|
+
return normalizePoint({
|
|
1871
|
+
x: point.x - body.x,
|
|
1872
|
+
y: point.y - body.y,
|
|
1873
|
+
z: point.z - body.z,
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
const bounds = toBounds(body);
|
|
1877
|
+
const candidates = [
|
|
1878
|
+
{ normal: { x: -1, y: 0, z: 0 }, distance: Math.abs(point.x - bounds.minX), axis: 'x-' },
|
|
1879
|
+
{ normal: { x: 1, y: 0, z: 0 }, distance: Math.abs(point.x - bounds.maxX), axis: 'x+' },
|
|
1880
|
+
{ normal: { x: 0, y: -1, z: 0 }, distance: Math.abs(point.y - bounds.minY), axis: 'y-' },
|
|
1881
|
+
{ normal: { x: 0, y: 1, z: 0 }, distance: Math.abs(point.y - bounds.maxY), axis: 'y+' },
|
|
1882
|
+
{ normal: { x: 0, y: 0, z: -1 }, distance: Math.abs(point.z - bounds.minZ), axis: 'z-' },
|
|
1883
|
+
{ normal: { x: 0, y: 0, z: 1 }, distance: Math.abs(point.z - bounds.maxZ), axis: 'z+' },
|
|
1884
|
+
].sort((left, right) => left.distance - right.distance || compareStrings(left.axis, right.axis));
|
|
1885
|
+
return candidates[0].normal;
|
|
1886
|
+
}
|
|
1887
|
+
function penetrationAtPoint(body, point, castRadius) {
|
|
1888
|
+
if (body.shape.type === 'sphere') {
|
|
1889
|
+
const centerDistance = deterministicSqrt(square(point.x - body.x) + square(point.y - body.y) + square(point.z - body.z));
|
|
1890
|
+
return quantizeDistance(Math.max(0, body.shape.radius + castRadius - centerDistance));
|
|
1891
|
+
}
|
|
1892
|
+
const bounds = toBounds(body);
|
|
1893
|
+
if (point.x < bounds.minX || point.x > bounds.maxX || point.y < bounds.minY || point.y > bounds.maxY || point.z < bounds.minZ || point.z > bounds.maxZ) {
|
|
1894
|
+
return 0;
|
|
1895
|
+
}
|
|
1896
|
+
return quantizeDistance(Math.min(point.x - bounds.minX, bounds.maxX - point.x, point.y - bounds.minY, bounds.maxY - point.y, point.z - bounds.minZ, bounds.maxZ - point.z) + castRadius);
|
|
1897
|
+
}
|
|
1898
|
+
function contactDetailForBodies(contact, left, right) {
|
|
1899
|
+
const leftBounds = toBounds(left);
|
|
1900
|
+
const rightBounds = toBounds(right);
|
|
1901
|
+
const overlaps = [
|
|
1902
|
+
{ axis: 'x', amount: Math.min(leftBounds.maxX, rightBounds.maxX) - Math.max(leftBounds.minX, rightBounds.minX) },
|
|
1903
|
+
{ axis: 'y', amount: Math.min(leftBounds.maxY, rightBounds.maxY) - Math.max(leftBounds.minY, rightBounds.minY) },
|
|
1904
|
+
{ axis: 'z', amount: Math.min(leftBounds.maxZ, rightBounds.maxZ) - Math.max(leftBounds.minZ, rightBounds.minZ) },
|
|
1905
|
+
].sort((leftOverlap, rightOverlap) => leftOverlap.amount - rightOverlap.amount || compareStrings(leftOverlap.axis, rightOverlap.axis));
|
|
1906
|
+
const axis = overlaps[0].axis;
|
|
1907
|
+
const normal = contactNormalForAxis(axis, left, right);
|
|
1908
|
+
return {
|
|
1909
|
+
pair: contact.pair,
|
|
1910
|
+
a: contact.a,
|
|
1911
|
+
b: contact.b,
|
|
1912
|
+
trigger: contact.trigger,
|
|
1913
|
+
sensor: contact.sensor,
|
|
1914
|
+
point: quantizePoint({
|
|
1915
|
+
x: (Math.max(leftBounds.minX, rightBounds.minX) + Math.min(leftBounds.maxX, rightBounds.maxX)) / 2,
|
|
1916
|
+
y: (Math.max(leftBounds.minY, rightBounds.minY) + Math.min(leftBounds.maxY, rightBounds.maxY)) / 2,
|
|
1917
|
+
z: (Math.max(leftBounds.minZ, rightBounds.minZ) + Math.min(leftBounds.maxZ, rightBounds.maxZ)) / 2,
|
|
1918
|
+
}),
|
|
1919
|
+
normal,
|
|
1920
|
+
penetration: quantizeDistance(Math.max(0, overlaps[0].amount)),
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
function contactNormalForAxis(axis, left, right) {
|
|
1924
|
+
if (axis === 'x') {
|
|
1925
|
+
return { x: right.x >= left.x ? 1 : -1, y: 0, z: 0 };
|
|
1926
|
+
}
|
|
1927
|
+
if (axis === 'y') {
|
|
1928
|
+
return { x: 0, y: right.y >= left.y ? 1 : -1, z: 0 };
|
|
1929
|
+
}
|
|
1930
|
+
return { x: 0, y: 0, z: right.z >= left.z ? 1 : -1 };
|
|
1931
|
+
}
|
|
1932
|
+
function normalizePoint(point) {
|
|
1933
|
+
const length = deterministicSqrt(point.x * point.x + point.y * point.y + point.z * point.z);
|
|
1934
|
+
if (length === 0) {
|
|
1935
|
+
return { x: -1, y: 0, z: 0 };
|
|
1936
|
+
}
|
|
1937
|
+
return quantizePoint({ x: point.x / length, y: point.y / length, z: point.z / length });
|
|
1938
|
+
}
|
|
1939
|
+
function quantizePoint(point) {
|
|
1940
|
+
return {
|
|
1941
|
+
x: quantizeDistance(point.x),
|
|
1942
|
+
y: quantizeDistance(point.y),
|
|
1943
|
+
z: quantizeDistance(point.z),
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function compareHits(left, right) {
|
|
1947
|
+
return left.distance - right.distance || compareStrings(left.bodyId, right.bodyId);
|
|
1948
|
+
}
|
|
1949
|
+
function compareContactDetails(left, right) {
|
|
1950
|
+
return compareStrings(left.pair, right.pair);
|
|
1951
|
+
}
|
|
1952
|
+
function contactEventEnabled(event, bodyById) {
|
|
1953
|
+
const bit = contactEventCallbackBit(event);
|
|
1954
|
+
const left = bodyById.get(event.a);
|
|
1955
|
+
const right = bodyById.get(event.b);
|
|
1956
|
+
const leftMask = bodyCallbackMask(left);
|
|
1957
|
+
const rightMask = bodyCallbackMask(right);
|
|
1958
|
+
return leftMask === undefined
|
|
1959
|
+
|| rightMask === undefined
|
|
1960
|
+
|| (leftMask & bit) !== 0
|
|
1961
|
+
|| (rightMask & bit) !== 0;
|
|
1962
|
+
}
|
|
1963
|
+
function contactEventCallbackBit(event) {
|
|
1964
|
+
if (event.trigger || event.sensor) {
|
|
1965
|
+
if (event.type === 'enter')
|
|
1966
|
+
return exports.deterministicPhysicsCallback3D.triggerEnter;
|
|
1967
|
+
if (event.type === 'stay')
|
|
1968
|
+
return exports.deterministicPhysicsCallback3D.triggerStay;
|
|
1969
|
+
return exports.deterministicPhysicsCallback3D.triggerExit;
|
|
1970
|
+
}
|
|
1971
|
+
if (event.type === 'enter')
|
|
1972
|
+
return exports.deterministicPhysicsCallback3D.collisionEnter;
|
|
1973
|
+
if (event.type === 'stay')
|
|
1974
|
+
return exports.deterministicPhysicsCallback3D.collisionStay;
|
|
1975
|
+
return exports.deterministicPhysicsCallback3D.collisionExit;
|
|
1976
|
+
}
|
|
1977
|
+
function bodyCallbackMask(body) {
|
|
1978
|
+
return body?.callbackMask;
|
|
1979
|
+
}
|
|
1980
|
+
function compareContactEvents(left, right) {
|
|
1981
|
+
return compareStrings(left.pair, right.pair) || compareStrings(left.type, right.type);
|
|
1982
|
+
}
|
|
1983
|
+
function compareActivePairMetadata(left, right) {
|
|
1984
|
+
return compareStrings(left.pair, right.pair);
|
|
1985
|
+
}
|
|
1986
|
+
function percentile(values, percentileRank) {
|
|
1987
|
+
if (values.length === 0) {
|
|
1988
|
+
return 0;
|
|
1989
|
+
}
|
|
1990
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
1991
|
+
const percentileValue = sorted[Math.min(sorted.length - 1, Math.floor((percentileRank / 100) * sorted.length))];
|
|
1992
|
+
return Math.round(percentileValue * 1e4) / 1e4;
|
|
1993
|
+
}
|
|
1994
|
+
function compareStrings(left, right) {
|
|
1995
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1996
|
+
}
|