npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nexus-physics/core

v0.2.1

Published

A headless, domain-agnostic, server-authoritative 3D physics engine (isomorphic Wasm)

Readme

@nexus-physics/core

High-performance, headless 3D physics engine for JavaScript/TypeScript. Powered by Rust + Rapier3D, compiled to WebAssembly with zero-copy memory access and automatic TypeScript definitions.

🚀 Quick Start

npm install @nexus-physics/core
import init, { WasmPhysicsWorld } from '@nexus-physics/core';

// Initialize the Wasm module
await init();

// Create world with gravity (3 separate args: gx, gy, gz)
const world = new WasmPhysicsWorld(0, -9.81, 0);

// Add static floor (7 args: entity_id, body_type, shape_type, dims, px, py, pz)
world.add_body('floor', 'static', 'cuboid', [10, 0.5, 10], 0, -1, 0);

// Add dynamic ball
world.add_body('ball', 'dynamic', 'ball', [0.5], 0, 5, 0);

// Step the simulation
world.step(1 / 60);

// Get zero-copy snapshot (Float32Array)
const buffer = world.get_snapshot_view();
console.log(`Ball position: (${buffer[1]}, ${buffer[2]}, ${buffer[3]})`);

📦 What You Get

  • Deterministic Physics: Fixed timestep accumulator ensures reproducible behavior
  • Zero-Copy Snapshots: Direct Float32Array views into Wasm memory—no garbage collection pressure
  • Type-Safe API: Full TypeScript definitions auto-generated from Rust
  • Isomorphic: Works in browser and Node.js from the same package
  • Domain-Agnostic: Generic geometry and forces, not tied to games or robotics
  • Server-Authoritative Ready: Perfect for multiplayer simulations with client prediction

📚 Core Concepts

Body Types

Pass as lowercase strings to add_body():

  • "dynamic": Affected by forces and gravity. For moving objects.
  • "static": Immovable terrain, walls, platforms.
  • "kinematic": Moved programmatically. For elevators, moving platforms.

Shape Types

Pass as lowercase strings with dimension arrays:

  • "cuboid": Box shape. Dims: [hx, hy, hz] (half-extents)
  • "ball": Sphere shape. Dims: [radius]
  • "cylinder": Cylinder shape. Dims: [radius, half_height]

Snapshot Buffer Layout

Each body in a snapshot occupies exactly 8 floats:

[index, x, y, z, qx, qy, qz, qw, index, x, y, z, ...]

Access directly without parsing:

const buffer = world.get_snapshot_view();
for (let i = 0; i < buffer.length; i += 8) {
  const id = buffer[i + 0]; // Snapshot entry index
  const x = buffer[i + 1]; // Position X
  const y = buffer[i + 2]; // Position Y
  const z = buffer[i + 3]; // Position Z
  const qx = buffer[i + 4]; // Quaternion X
  const qy = buffer[i + 5]; // Quaternion Y
  const qz = buffer[i + 6]; // Quaternion Z
  const qw = buffer[i + 7]; // Quaternion W
}

🎮 Common Patterns

Multiplayer Game with Client Prediction

const world = new WasmPhysicsWorld(0, -9.81, 0);

// Game loop
function gameLoop(deltaTime) {
  // Apply local player input (linear + angular velocity combined)
  world.set_velocity('player', vx, vy, vz, ax, ay, az);

  // Predict locally
  world.step(deltaTime);

  // Render from prediction
  const buffer = world.get_snapshot_view();
  updatePlayerMesh(buffer);

  // Periodically reconciliate with server
  if (shouldReconciliate()) {
    const serverSnapshot = await fetchServerSnapshot();
    const pos = serverSnapshot.position;
    const rot = serverSnapshot.rotation;
    world.apply_impulse('other_player', pos[0], pos[1], pos[2]);
  }
}

Three.js Integration

import * as THREE from 'three';
import init, { WasmPhysicsWorld } from '@nexus-physics/core';

const world = new WasmPhysicsWorld(0, -9.81, 0);
const meshes = new Map<string, THREE.Mesh>();

function render() {
  world.step(1 / 60);

  const buffer = world.get_snapshot_view();

  // Update all meshes directly from physics buffer
  for (let i = 0; i < buffer.length; i += 8) {
    const id = buffer[i];
    const mesh = meshes.get(String(id));
    if (mesh) {
      mesh.position.set(buffer[i + 1], buffer[i + 2], buffer[i + 3]);
      mesh.quaternion.set(
        buffer[i + 4],
        buffer[i + 5],
        buffer[i + 6],
        buffer[i + 7],
      );
    }
  }

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

Robotics Simulation

import { WasmPhysicsWorld } from '@nexus-physics/core';

const world = new WasmPhysicsWorld(0, -9.81, 0);

// Add robot base and wheels
world.add_body('robot_base', 'dynamic', 'cuboid', [0.3, 0.1, 0.3], 0, 0.5, 0);

// Control via velocity commands (linear + angular)
function applyMotorCommand(lx, ly, lz, ax, ay, az) {
  world.set_velocity('robot_base', lx, ly, lz, ax, ay, az);
}

// Simulate sensor reading (e.g., LiDAR)
function readSensors() {
  world.step(1 / 60);
  const snapshot = world.get_snapshot_view();
  // Process snapshot as needed
  return snapshot;
}

Deterministic Replay

const world = new WasmPhysicsWorld(0, -9.81, 0);
const timeline: Array<{
  velocities: Float32Array;
}> = [];

// Record phase
function recordFrame(
  lx: number,
  ly: number,
  lz: number,
  ax: number,
  ay: number,
  az: number,
) {
  world.set_velocity('player', lx, ly, lz, ax, ay, az);
  world.step(1 / 60);
  timeline.push({
    velocities: new Float32Array([lx, ly, lz, ax, ay, az]),
  });
}

// Replay phase
function replay() {
  const newWorld = new WasmPhysicsWorld(0, -9.81, 0);
  newWorld.add_body('player', 'dynamic', 'ball', [0.5], 0, 5, 0);

  for (const frame of timeline) {
    const v = frame.velocities;
    newWorld.set_velocity('player', v[0], v[1], v[2], v[3], v[4], v[5]);
    newWorld.step(1 / 60);
  }
}

📖 API Reference

World

class WasmPhysicsWorld {
  // Constructor takes 3 separate gravity components (not an array)
  constructor(gx: number, gy: number, gz: number);

  step(deltaTime: number): void;
  body_count(): number;
  update_query_pipeline(): void;

  // Add body with 7 arguments (not a config object)
  add_body(
    entity_id: string,
    body_type: 'dynamic' | 'static' | 'kinematic', // lowercase!
    shape_type: 'cuboid' | 'ball' | 'cylinder', // lowercase!
    dims: number[], // [hx,hy,hz] or [radius] or [radius,half_height]
    px: number,
    py: number,
    pz: number,
  ): void;

  remove_body(entity_id: string): void;

  // Only snapshot method - returns Float32Array view
  get_snapshot_view(): Float32Array;

  // Set both linear AND angular velocity in one call
  set_velocity(
    entity_id: string,
    lx: number,
    ly: number,
    lz: number, // linear velocity
    ax: number,
    ay: number,
    az: number, // angular velocity
  ): void;

  apply_impulse(entity_id: string, ix: number, iy: number, iz: number): void;

  get_position(entity_id: string): Float32Array; // length 3
  get_rotation(entity_id: string): Float32Array; // length 4 (quaternion)

  cast_ray(
    ox: number,
    oy: number,
    oz: number, // origin
    dx: number,
    dy: number,
    dz: number, // direction
    max_toi: number,
  ): number; // Distance to hit, or NaN if no hit

  cast_ray_batch(
    origins: Float32Array, // [x, y, z, x, y, z, ...]
    directions: Float32Array, // [x, y, z, x, y, z, ...]
    max_toi: number,
  ): Float32Array; // [distance, distance, ...] - max_toi on miss
}

🎯 When to Use

Ideal for:

  • Multiplayer games with server-authoritative physics
  • 3D web applications (Three.js, Babylon.js)
  • Robotics and physics simulations
  • Deterministic replay systems
  • Real-time collision detection
  • Networked motion synthesis

Not ideal for:

  • Softbody physics (cloth, hair)
  • Deformable terrain
  • Particle systems (use GPU instead)
  • Passive rigid body stacking (CPU-bound)

⚡ Performance Tips

  1. Reuse Float32Array views: Don't create new buffers every frame
  2. Batch updates: Apply multiple forces before stepping
  3. Limit body count: 500-1000 bodies per world on modern hardware
  4. Use appropriate shapes: Spheres are faster than boxes
  5. Profile first: Use Chrome DevTools to measure bottlenecks

🔧 Advanced: Node.js Usage

The package exports separate distributions for browser and Node.js. Import detection is automatic:

// Node.js automatically gets the Node.js Wasm build
import init, { WasmPhysicsWorld } from '@nexus-physics/core';

// Or explicitly:
import initNode from '@nexus-physics/core/dist/node/nexus_physics_wasm.js';

// Browser sees the browser build via conditional exports

🐛 Troubleshooting

Q: "Module not found" errors in bundler

A: Ensure your bundler is configured for Wasm. Vite, Webpack 5+, and esbuild handle this automatically.

Q: Physics feels jittery between frames

A: You're likely stepping with variable delta times. The engine uses a fixed 60Hz timestep internally—pass the actual elapsed time and let the accumulator handle it.

Q: Why is my snapshot empty?

A: Only Dynamic and Kinematic bodies appear in snapshots. Static bodies are implicit in the world geometry.

📚 Further Reading

📄 License

MIT License. See project repository for full license text.

🤝 Contributing

Found a bug? Have a feature request? Open an issue on the GitHub repository.


Built with ❤️ in Rust & WebAssembly