@aura3d/physics 1.3.3 → 1.4.1

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.
@@ -1,394 +0,0 @@
1
- import { PhysicsWorld } from "./PhysicsWorld.js";
2
- import type { RigidBody } from "./RigidBody.js";
3
- import { Shape, type PhysicsShape, type Vec3 } from "./Shape.js";
4
-
5
- export type PhysicsSandboxSpawnerPreset =
6
- | "box"
7
- | "sphere"
8
- | "capsule"
9
- | "cylinder-proxy"
10
- | "tower"
11
- | "pyramid"
12
- | "wall"
13
- | "chain"
14
- | "newtons-cradle"
15
- | "dominoes"
16
- | "wrecking-ball"
17
- | "vehicle"
18
- | "ragdoll";
19
-
20
- export type PhysicsSandboxTool = "grab" | "push" | "freeze" | "delete" | "explode" | "slice";
21
-
22
- export interface PhysicsSandboxFixtureOptions {
23
- readonly seed?: number;
24
- readonly steps?: number;
25
- }
26
-
27
- export interface PhysicsSandboxSpawnSummary {
28
- readonly preset: PhysicsSandboxSpawnerPreset;
29
- readonly bodies: number;
30
- readonly dynamicBodies: number;
31
- readonly staticBodies: number;
32
- readonly colliders: number;
33
- readonly constraints: number;
34
- readonly visualKinds: readonly string[];
35
- readonly colliderKinds: readonly string[];
36
- }
37
-
38
- export interface PhysicsSandboxToolSummary {
39
- readonly tool: PhysicsSandboxTool;
40
- readonly supported: boolean;
41
- readonly affectedBodies: number;
42
- readonly selectedBodyId?: number;
43
- readonly impulseMagnitude?: number;
44
- readonly deletedBodies?: number;
45
- readonly frozenBodies?: number;
46
- readonly blocker?: string;
47
- }
48
-
49
- export interface PhysicsSandboxFixture {
50
- readonly id: "external-parity-old-branch-physics-sandbox-fixture";
51
- readonly source: "origin-master-physics-sandbox-tools-spawners-adapted";
52
- readonly spawners: readonly PhysicsSandboxSpawnSummary[];
53
- readonly tools: readonly PhysicsSandboxToolSummary[];
54
- readonly metrics: {
55
- readonly bodyCountBeforeTools: number;
56
- readonly bodyCountAfterTools: number;
57
- readonly colliderCountAfterTools: number;
58
- readonly constraintCountAfterTools: number;
59
- readonly dynamicBodiesAfterTools: number;
60
- readonly sleepingBodiesAfterTools: number;
61
- readonly contactsAfterSteps: number;
62
- readonly broadphasePairsAfterSteps: number;
63
- readonly kineticEnergyAfterSteps: number;
64
- readonly maxContactPenetrationAfterSteps: number;
65
- readonly totalSpawnedBodies: number;
66
- readonly totalSpawnerConstraints: number;
67
- readonly supportedToolCount: number;
68
- readonly blockedToolCount: number;
69
- };
70
- readonly unsupportedAdvancedSimulations: readonly ["cloth", "soft-body", "fluid", "fracture"];
71
- readonly hash: string;
72
- readonly claimBoundary: string;
73
- }
74
-
75
- type SpawnRecord = {
76
- readonly preset: PhysicsSandboxSpawnerPreset;
77
- readonly body: RigidBody;
78
- readonly visualKind: string;
79
- readonly colliderKind: PhysicsShape["kind"] | "cylinder-proxy";
80
- };
81
-
82
- type ConstraintRecord = {
83
- readonly preset: PhysicsSandboxSpawnerPreset;
84
- };
85
-
86
- export function samplePhysicsSandboxFixture(options: PhysicsSandboxFixtureOptions = {}): PhysicsSandboxFixture {
87
- const seed = integer(options.seed ?? 0x3d2025, "seed");
88
- const steps = integer(options.steps ?? 24, "steps");
89
- if (steps < 0 || steps > 240) throw new RangeError("Physics sandbox fixture steps must be in the [0, 240] range.");
90
-
91
- const world = new PhysicsWorld({ gravity: [0, -9.81, 0], fixedDelta: 1 / 60, solverIterations: 6 });
92
- const records: SpawnRecord[] = [];
93
- const constraints: ConstraintRecord[] = [];
94
- const ground = world.createRigidBody({ type: "static", position: [0, -0.15, 0] });
95
- world.createCollider(ground, { shape: Shape.box(8, 0.15, 2), material: { friction: 0.9, restitution: 0 } });
96
-
97
- spawnBasic(world, records, "box", [-4.2, 1.2, 0], Shape.box(0.32, 0.32, 0.32), "box", 1);
98
- spawnBasic(world, records, "sphere", [-3.45, 1.25, 0], Shape.sphere(0.32), "sphere", 1);
99
- spawnBasic(world, records, "capsule", [-2.7, 1.35, 0], Shape.capsule(0.22, 0.38), "capsule", 1);
100
- spawnBasic(world, records, "cylinder-proxy", [-1.95, 1.3, 0], Shape.capsule(0.28, 0.26), "cylinder", 1);
101
- spawnTower(world, records, [-4.5, 0.25, 0], 5);
102
- spawnPyramid(world, records, [-2.7, 0.25, 0], 4);
103
- spawnWall(world, records, [-0.6, 0.25, 0], 5, 3);
104
- spawnChain(world, records, constraints, [1.5, 3.1, 0], [3.0, 2.2, 0], 6);
105
- spawnNewtonsCradle(world, records, constraints, [0.9, 4.2, 0], 4);
106
- spawnDominoes(world, records, [2.1, 0.45, 0], 8);
107
- spawnWreckingBall(world, records, constraints, [4.0, 3.5, 0], 2.4);
108
- spawnVehicle(world, records, constraints, [-0.2, 2.0, 0]);
109
- spawnRagdoll(world, records, constraints, [3.7, 1.6, 0]);
110
-
111
- const bodyCountBeforeTools = world.snapshot().stats.bodies;
112
- const tools = applyToolSequence(world, seed);
113
- for (let step = 0; step < steps; step += 1) world.step(1 / 60);
114
- const snapshot = world.snapshot();
115
- const spawners = summarizeSpawners(records, constraints, world);
116
- const supportedToolCount = tools.filter((tool) => tool.supported).length;
117
- const blockedToolCount = tools.length - supportedToolCount;
118
- const metrics = {
119
- bodyCountBeforeTools,
120
- bodyCountAfterTools: snapshot.stats.bodies,
121
- colliderCountAfterTools: snapshot.stats.colliders,
122
- constraintCountAfterTools: snapshot.stats.constraints,
123
- dynamicBodiesAfterTools: snapshot.bodies.filter((body) => body.type === "dynamic").length,
124
- sleepingBodiesAfterTools: snapshot.stats.sleepingBodies,
125
- contactsAfterSteps: snapshot.stats.contacts,
126
- broadphasePairsAfterSteps: snapshot.stats.broadphasePairs,
127
- kineticEnergyAfterSteps: Number(snapshot.stats.kineticEnergy.toFixed(4)),
128
- maxContactPenetrationAfterSteps: Number(snapshot.stats.maxContactPenetration.toFixed(5)),
129
- totalSpawnedBodies: records.length,
130
- totalSpawnerConstraints: constraints.length,
131
- supportedToolCount,
132
- blockedToolCount
133
- };
134
- return {
135
- id: "external-parity-old-branch-physics-sandbox-fixture",
136
- source: "origin-master-physics-sandbox-tools-spawners-adapted",
137
- spawners,
138
- tools,
139
- metrics,
140
- unsupportedAdvancedSimulations: ["cloth", "soft-body", "fluid", "fracture"],
141
- hash: hashFixture(spawners, tools, metrics),
142
- claimBoundary: "Deterministic current-engine fixture adapted from old physics sandbox spawners and tools. It proves bounded rigid-body spawn presets, hinge/spring constraints, and supported tool telemetry; it does not claim soft body, cloth, fluid, fracture, full interactive editor tools, or Unity/Unreal physics-sandbox parity."
143
- };
144
- }
145
-
146
- function spawnBasic(
147
- world: PhysicsWorld,
148
- records: SpawnRecord[],
149
- preset: PhysicsSandboxSpawnerPreset,
150
- position: Vec3,
151
- shape: PhysicsShape,
152
- visualKind: string,
153
- mass: number
154
- ): RigidBody {
155
- const body = world.createRigidBody({ position, mass, linearDamping: 0.02, angularDamping: 0.02 });
156
- world.createCollider(body, { shape, material: { friction: 0.72, restitution: 0.05 } });
157
- records.push({ preset, body, visualKind, colliderKind: preset === "cylinder-proxy" ? "cylinder-proxy" : shape.kind });
158
- return body;
159
- }
160
-
161
- function spawnTower(world: PhysicsWorld, records: SpawnRecord[], origin: Vec3, height: number): void {
162
- for (let index = 0; index < height; index += 1) {
163
- spawnBasic(world, records, "tower", [origin[0], origin[1] + index * 0.46, origin[2]], Shape.box(0.36, 0.22, 0.32), "box", 1);
164
- }
165
- }
166
-
167
- function spawnPyramid(world: PhysicsWorld, records: SpawnRecord[], origin: Vec3, levels: number): void {
168
- for (let level = 0; level < levels; level += 1) {
169
- const width = levels - level;
170
- for (let x = 0; x < width; x += 1) {
171
- spawnBasic(world, records, "pyramid", [
172
- origin[0] + (x - (width - 1) / 2) * 0.44,
173
- origin[1] + level * 0.46,
174
- origin[2]
175
- ], Shape.box(0.2, 0.22, 0.25), "box", 1);
176
- }
177
- }
178
- }
179
-
180
- function spawnWall(world: PhysicsWorld, records: SpawnRecord[], origin: Vec3, width: number, height: number): void {
181
- for (let y = 0; y < height; y += 1) {
182
- const boxesInRow = width - (y % 2);
183
- for (let x = 0; x < boxesInRow; x += 1) {
184
- spawnBasic(world, records, "wall", [
185
- origin[0] + (x - (boxesInRow - 1) / 2) * 0.42 + (y % 2) * 0.21,
186
- origin[1] + y * 0.42,
187
- origin[2]
188
- ], Shape.box(0.2, 0.2, 0.2), "brick", 1);
189
- }
190
- }
191
- }
192
-
193
- function spawnChain(world: PhysicsWorld, records: SpawnRecord[], constraints: ConstraintRecord[], start: Vec3, end: Vec3, segments: number): void {
194
- let previous: RigidBody | undefined;
195
- for (let index = 0; index < segments; index += 1) {
196
- const t = (index + 0.5) / segments;
197
- const body = spawnBasic(world, records, "chain", lerpVec3(start, end, t), Shape.box(0.12, 0.12, 0.32), "chain-link", 0.5);
198
- if (previous) {
199
- world.createConstraint({ type: "hinge", bodyA: previous, bodyB: body, stiffness: 0.85 });
200
- constraints.push({ preset: "chain" });
201
- }
202
- previous = body;
203
- }
204
- }
205
-
206
- function spawnNewtonsCradle(world: PhysicsWorld, records: SpawnRecord[], constraints: ConstraintRecord[], origin: Vec3, balls: number): void {
207
- for (let index = 0; index < balls; index += 1) {
208
- const x = origin[0] + (index - (balls - 1) / 2) * 0.42;
209
- const anchor = world.createRigidBody({ type: "static", position: [x, origin[1], origin[2]] });
210
- world.createCollider(anchor, { shape: Shape.box(0.06, 0.06, 0.06) });
211
- records.push({ preset: "newtons-cradle", body: anchor, visualKind: "anchor", colliderKind: "box" });
212
- const ball = spawnBasic(world, records, "newtons-cradle", [x, origin[1] - 1.1, origin[2]], Shape.sphere(0.18), "sphere", 1);
213
- world.createConstraint({ type: "hinge", bodyA: anchor, bodyB: ball, stiffness: 0.9 });
214
- constraints.push({ preset: "newtons-cradle" });
215
- }
216
- }
217
-
218
- function spawnDominoes(world: PhysicsWorld, records: SpawnRecord[], start: Vec3, count: number): void {
219
- for (let index = 0; index < count; index += 1) {
220
- spawnBasic(world, records, "dominoes", [start[0] + index * 0.22, start[1], start[2]], Shape.box(0.05, 0.34, 0.18), "domino", 0.5);
221
- }
222
- }
223
-
224
- function spawnWreckingBall(world: PhysicsWorld, records: SpawnRecord[], constraints: ConstraintRecord[], position: Vec3, chainLength: number): void {
225
- const anchor = world.createRigidBody({ type: "static", position });
226
- world.createCollider(anchor, { shape: Shape.box(0.08, 0.08, 0.08) });
227
- records.push({ preset: "wrecking-ball", body: anchor, visualKind: "anchor", colliderKind: "box" });
228
- const ball = spawnBasic(world, records, "wrecking-ball", [position[0] + 0.55, position[1] - chainLength, position[2]], Shape.sphere(0.34), "sphere", 8);
229
- world.createConstraint({ type: "spring", bodyA: anchor, bodyB: ball, restLength: chainLength, stiffness: 0.55 });
230
- constraints.push({ preset: "wrecking-ball" });
231
- }
232
-
233
- function spawnVehicle(world: PhysicsWorld, records: SpawnRecord[], constraints: ConstraintRecord[], position: Vec3): void {
234
- const chassis = spawnBasic(world, records, "vehicle", position, Shape.box(0.56, 0.16, 0.32), "chassis", 5);
235
- for (const offset of [[-0.38, -0.24, -0.32], [-0.38, -0.24, 0.32], [0.38, -0.24, -0.32], [0.38, -0.24, 0.32]] as const) {
236
- const wheel = spawnBasic(world, records, "vehicle", [position[0] + offset[0], position[1] + offset[1], position[2] + offset[2]], Shape.sphere(0.15), "wheel", 0.5);
237
- world.createConstraint({ type: "hinge", bodyA: chassis, bodyB: wheel, stiffness: 0.75 });
238
- constraints.push({ preset: "vehicle" });
239
- }
240
- }
241
-
242
- function spawnRagdoll(world: PhysicsWorld, records: SpawnRecord[], constraints: ConstraintRecord[], position: Vec3): void {
243
- const torso = spawnBasic(world, records, "ragdoll", position, Shape.box(0.22, 0.36, 0.16), "torso", 5);
244
- const parts: readonly [string, Vec3, PhysicsShape, number][] = [
245
- ["head", [0, 0.58, 0], Shape.sphere(0.18), 1],
246
- ["left-arm", [-0.36, 0.14, 0], Shape.capsule(0.08, 0.28), 1],
247
- ["right-arm", [0.36, 0.14, 0], Shape.capsule(0.08, 0.28), 1],
248
- ["left-leg", [-0.16, -0.56, 0], Shape.capsule(0.09, 0.34), 1],
249
- ["right-leg", [0.16, -0.56, 0], Shape.capsule(0.09, 0.34), 1]
250
- ];
251
- for (const [visualKind, offset, shape, mass] of parts) {
252
- const body = spawnBasic(world, records, "ragdoll", [position[0] + offset[0], position[1] + offset[1], position[2] + offset[2]], shape, visualKind, mass);
253
- world.createConstraint({ type: "hinge", bodyA: torso, bodyB: body, stiffness: 0.72 });
254
- constraints.push({ preset: "ragdoll" });
255
- }
256
- }
257
-
258
- function applyToolSequence(world: PhysicsWorld, seed: number): readonly PhysicsSandboxToolSummary[] {
259
- const tools: PhysicsSandboxToolSummary[] = [];
260
- const dynamicBodies = () => world.bodies().filter((body) => body.type === "dynamic");
261
- const grabTarget: Vec3 = [0.25 + hash01(seed, 1) * 0.25, 2.15, 0];
262
- const grabbed = nearestBody(dynamicBodies(), [0, 2, 0]);
263
- if (grabbed) {
264
- grabbed.setVelocity([
265
- (grabTarget[0] - grabbed.position[0]) * 3.5,
266
- (grabTarget[1] - grabbed.position[1]) * 3.5,
267
- 0
268
- ]);
269
- }
270
- tools.push({
271
- tool: "grab",
272
- supported: grabbed !== undefined,
273
- affectedBodies: grabbed ? 1 : 0,
274
- selectedBodyId: grabbed?.id,
275
- blocker: "Current RigidBody type is immutable, so this fixture proves velocity-targeted grab telemetry rather than old kinematic type switching."
276
- });
277
-
278
- const pushPoint: Vec3 = [0, 1.2, 0];
279
- let pushed = 0;
280
- let impulseMagnitude = 0;
281
- for (const body of dynamicBodies()) {
282
- const distance = distanceVec3(body.position, pushPoint);
283
- if (distance <= 2.5 && distance > 1e-6) {
284
- const direction = scaleVec3(subVec3(body.position, pushPoint), 1 / distance);
285
- const impulse = scaleVec3(direction, Number(((2.5 - distance) * 0.38).toFixed(4)));
286
- body.applyImpulse(impulse);
287
- impulseMagnitude += Math.hypot(...impulse);
288
- pushed += 1;
289
- }
290
- }
291
- tools.push({ tool: "push", supported: true, affectedBodies: pushed, impulseMagnitude: Number(impulseMagnitude.toFixed(4)) });
292
-
293
- const freezeCandidates = dynamicBodies().slice(0, 3);
294
- for (const body of freezeCandidates) {
295
- body.setVelocity([0, 0, 0]);
296
- body.setAngularVelocity([0, 0, 0]);
297
- body.sleeping = true;
298
- }
299
- tools.push({ tool: "freeze", supported: true, affectedBodies: freezeCandidates.length, frozenBodies: freezeCandidates.length });
300
-
301
- const deleteTarget = dynamicBodies().at(-1);
302
- if (deleteTarget) world.removeRigidBody(deleteTarget.id);
303
- tools.push({ tool: "delete", supported: deleteTarget !== undefined, affectedBodies: deleteTarget ? 1 : 0, selectedBodyId: deleteTarget?.id, deletedBodies: deleteTarget ? 1 : 0 });
304
-
305
- const explodePoint: Vec3 = [1.2, 1.2, 0];
306
- let exploded = 0;
307
- let explosionImpulse = 0;
308
- for (const body of dynamicBodies()) {
309
- const distance = distanceVec3(body.position, explodePoint);
310
- if (distance <= 3.25 && distance > 1e-6) {
311
- const direction = scaleVec3(subVec3(body.position, explodePoint), 1 / distance);
312
- const impulse = scaleVec3(direction, Number(((3.25 - distance) * 0.52).toFixed(4)));
313
- body.applyImpulse(impulse);
314
- explosionImpulse += Math.hypot(...impulse);
315
- exploded += 1;
316
- }
317
- }
318
- tools.push({ tool: "explode", supported: true, affectedBodies: exploded, impulseMagnitude: Number(explosionImpulse.toFixed(4)) });
319
- tools.push({
320
- tool: "slice",
321
- supported: false,
322
- affectedBodies: 0,
323
- blocker: "Old slice tool only drew/specified a cut gesture; current physics has no convex decomposition or fracture pipeline."
324
- });
325
- return tools;
326
- }
327
-
328
- function summarizeSpawners(records: readonly SpawnRecord[], constraints: readonly ConstraintRecord[], world: PhysicsWorld): readonly PhysicsSandboxSpawnSummary[] {
329
- const summaries: PhysicsSandboxSpawnSummary[] = [];
330
- const liveBodyIds = new Set(world.bodies().map((body) => body.id));
331
- for (const preset of unique(records.map((record) => record.preset))) {
332
- const presetRecords = records.filter((record) => record.preset === preset && liveBodyIds.has(record.body.id));
333
- summaries.push({
334
- preset,
335
- bodies: presetRecords.length,
336
- dynamicBodies: presetRecords.filter((record) => record.body.type === "dynamic").length,
337
- staticBodies: presetRecords.filter((record) => record.body.type === "static").length,
338
- colliders: presetRecords.length,
339
- constraints: constraints.filter((constraint) => constraint.preset === preset).length,
340
- visualKinds: unique(presetRecords.map((record) => record.visualKind)).sort(),
341
- colliderKinds: unique(presetRecords.map((record) => record.colliderKind)).sort()
342
- });
343
- }
344
- return summaries;
345
- }
346
-
347
- function nearestBody(bodies: readonly RigidBody[], target: Vec3): RigidBody | undefined {
348
- return [...bodies].sort((left, right) => distanceVec3(left.position, target) - distanceVec3(right.position, target) || left.id - right.id)[0];
349
- }
350
-
351
- function lerpVec3(start: Vec3, end: Vec3, t: number): Vec3 {
352
- return [
353
- start[0] + (end[0] - start[0]) * t,
354
- start[1] + (end[1] - start[1]) * t,
355
- start[2] + (end[2] - start[2]) * t
356
- ];
357
- }
358
-
359
- function distanceVec3(left: Vec3, right: Vec3): number {
360
- return Math.hypot(left[0] - right[0], left[1] - right[1], left[2] - right[2]);
361
- }
362
-
363
- function subVec3(left: Vec3, right: Vec3): Vec3 {
364
- return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
365
- }
366
-
367
- function scaleVec3(value: Vec3, scale: number): Vec3 {
368
- return [value[0] * scale, value[1] * scale, value[2] * scale];
369
- }
370
-
371
- function unique<T extends string>(values: readonly T[]): T[] {
372
- return [...new Set(values)];
373
- }
374
-
375
- function hashFixture(...parts: readonly unknown[]): string {
376
- const text = JSON.stringify(parts);
377
- let hash = 0x811c9dc5;
378
- for (let index = 0; index < text.length; index += 1) {
379
- hash ^= text.charCodeAt(index);
380
- hash = Math.imul(hash, 0x01000193) >>> 0;
381
- }
382
- return hash.toString(16).padStart(8, "0");
383
- }
384
-
385
- function hash01(seed: number, salt: number): number {
386
- let value = Math.imul(seed + salt * 1013904223, 1664525) + 1013904223;
387
- value = Math.imul(value ^ (value >>> 13), 1274126177);
388
- return ((value ^ (value >>> 16)) >>> 0) / 4294967296;
389
- }
390
-
391
- function integer(value: number, label: string): number {
392
- if (!Number.isInteger(value)) throw new RangeError(`Physics sandbox fixture ${label} must be an integer.`);
393
- return value;
394
- }
@@ -1,47 +0,0 @@
1
- import type { PhysicsWorld } from "./PhysicsWorld.js";
2
-
3
- export type PhysicsStepperResult = {
4
- readonly steps: number;
5
- readonly alpha: number;
6
- readonly droppedTime: number;
7
- };
8
-
9
- export class PhysicsStepper {
10
- readonly fixedDelta: number;
11
- readonly maxSubSteps: number;
12
- private accumulator = 0;
13
-
14
- constructor(fixedDelta: number, maxSubSteps = 5) {
15
- if (!Number.isFinite(fixedDelta) || fixedDelta <= 0) {
16
- throw new Error("fixedDelta must be a finite positive number.");
17
- }
18
- if (!Number.isInteger(maxSubSteps) || maxSubSteps <= 0) {
19
- throw new Error("maxSubSteps must be a positive integer.");
20
- }
21
- this.fixedDelta = fixedDelta;
22
- this.maxSubSteps = maxSubSteps;
23
- }
24
-
25
- advance(deltaSeconds: number, world: Pick<PhysicsWorld, "step">): PhysicsStepperResult {
26
- if (!Number.isFinite(deltaSeconds) || deltaSeconds < 0) {
27
- throw new Error("deltaSeconds must be finite and non-negative.");
28
- }
29
- this.accumulator += deltaSeconds;
30
- let steps = 0;
31
- while (this.accumulator + 1e-12 >= this.fixedDelta && steps < this.maxSubSteps) {
32
- world.step(this.fixedDelta);
33
- this.accumulator -= this.fixedDelta;
34
- steps += 1;
35
- }
36
- let droppedTime = 0;
37
- if (this.accumulator >= this.fixedDelta) {
38
- droppedTime = this.accumulator;
39
- this.accumulator = 0;
40
- }
41
- return { steps, alpha: this.accumulator / this.fixedDelta, droppedTime };
42
- }
43
-
44
- reset(): void {
45
- this.accumulator = 0;
46
- }
47
- }