Rapier 2D (@dimforge/rapier2d-compat 0.20) comes in as the physics engine: the editor loads it once in the background right after opening a project from its own chunk, so the main bundle grows by 0.19 KB gzip and the first frame does not wait; loadPhysics, physicsStatus and the 100 px = 1 m conversion live in src/runtime/physics/engine.ts, __gm.physics() reports the state, unit tests pin free fall and a box resting on a box, and e2e module 41 pins that Rapier is ready from its own chunk without console noise (#145)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d7d0005cf
commit
d02bcca266
9 files changed
+143
-1
No files matched your search
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
loadPhysics,
|
||||
PIXELS_PER_METRE,
|
||||
physicsStatus,
|
||||
type Rapier,
|
||||
toMetres,
|
||||
toPixels,
|
||||
} from '../../src/runtime/physics/engine';
|
||||
|
||||
const STEP = 1 / 60;
|
||||
const GRAVITY = 980;
|
||||
|
||||
/** A 400 × 20 px floor whose top is at y = 290, and a 32 px crate dropped from y = 0. */
|
||||
function crateOverFloor(rapier: Rapier) {
|
||||
const world = new rapier.World({ x: 0, y: toMetres(GRAVITY) });
|
||||
world.timestep = STEP;
|
||||
const floor = world.createRigidBody(rapier.RigidBodyDesc.fixed().setTranslation(0, toMetres(300)));
|
||||
world.createCollider(rapier.ColliderDesc.cuboid(toMetres(200), toMetres(10)), floor);
|
||||
const crate = world.createRigidBody(rapier.RigidBodyDesc.dynamic().setTranslation(0, 0));
|
||||
world.createCollider(rapier.ColliderDesc.cuboid(toMetres(16), toMetres(16)), crate);
|
||||
const run = (steps: number) => {
|
||||
for (let i = 0; i < steps; i++) world.step();
|
||||
};
|
||||
const y = () => toPixels(crate.translation().y);
|
||||
return { world, crate, run, y };
|
||||
}
|
||||
|
||||
describe('physics engine', () => {
|
||||
it('loads once and reports the Rapier version', async () => {
|
||||
const first = loadPhysics();
|
||||
expect(loadPhysics()).toBe(first);
|
||||
const rapier = await first;
|
||||
expect(rapier.version()).toBe('0.20.0');
|
||||
expect(physicsStatus()).toMatchObject({ state: 'ready', version: '0.20.0' });
|
||||
});
|
||||
|
||||
it('converts at 100 px to the metre', () => {
|
||||
expect(PIXELS_PER_METRE).toBe(100);
|
||||
expect(toMetres(980)).toBe(9.8);
|
||||
expect(toPixels(toMetres(123.5))).toBeCloseTo(123.5, 10);
|
||||
});
|
||||
|
||||
it('lets a crate fall freely in pixels per second squared', async () => {
|
||||
const { run, y, crate } = crateOverFloor(await loadPhysics());
|
||||
run(30);
|
||||
// ½ g t² = 122.5 px after half a second; the integrator lands about a pixel past it.
|
||||
expect(Math.abs(y() - 0.5 * GRAVITY * 0.25)).toBeLessThan(1.5);
|
||||
expect(toPixels(crate.linvel().y)).toBeCloseTo(GRAVITY * 0.5, 3);
|
||||
});
|
||||
|
||||
it('keeps a crate resting on the floor', async () => {
|
||||
const { run, y, crate } = crateOverFloor(await loadPhysics());
|
||||
run(180);
|
||||
expect(Math.abs(y() - 274)).toBeLessThan(0.5);
|
||||
expect(Math.abs(toPixels(crate.linvel().y))).toBeLessThan(0.5);
|
||||
run(120);
|
||||
expect(Math.abs(y() - 274)).toBeLessThan(0.5);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user