use wasm_bindgen::prelude::*;
use crate::collider::Sphere;
use crate::math::{Fix128, QuatFix, Vec3Fix};
use crate::raycast::{ray_sphere, Ray};
use crate::solver::{PhysicsConfig, PhysicsWorld, RigidBody};
#[cfg(test)]
use crate::solver::BodyType;
#[wasm_bindgen]
pub struct WasmPhysicsWorld {
inner: PhysicsWorld,
}
#[wasm_bindgen]
impl WasmPhysicsWorld {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: PhysicsWorld::new(PhysicsConfig::default()),
}
}
#[wasm_bindgen(js_name = "withConfig")]
pub fn with_config(
gravity_x: f64,
gravity_y: f64,
gravity_z: f64,
substeps: u32,
iterations: u32,
) -> Self {
let config = PhysicsConfig {
substeps: substeps as usize,
iterations: iterations as usize,
gravity: Vec3Fix::new(
Fix128::from_f64(gravity_x),
Fix128::from_f64(gravity_y),
Fix128::from_f64(gravity_z),
),
damping: Fix128::from_ratio(99, 100),
..Default::default()
};
Self {
inner: PhysicsWorld::new(config),
}
}
#[wasm_bindgen(js_name = "addDynamicBody")]
pub fn add_dynamic_body(&mut self, x: f64, y: f64, z: f64, mass: f64) -> usize {
let body = RigidBody::new_dynamic(
Vec3Fix::new(
Fix128::from_f64(x),
Fix128::from_f64(y),
Fix128::from_f64(z),
),
Fix128::from_f64(mass),
);
self.inner.add_body(body)
}
#[wasm_bindgen(js_name = "addStaticBody")]
pub fn add_static_body(&mut self, x: f64, y: f64, z: f64) -> usize {
let body = RigidBody::new_static(Vec3Fix::new(
Fix128::from_f64(x),
Fix128::from_f64(y),
Fix128::from_f64(z),
));
self.inner.add_body(body)
}
#[wasm_bindgen(js_name = "addKinematicBody")]
pub fn add_kinematic_body(&mut self, x: f64, y: f64, z: f64) -> usize {
let body = RigidBody::new_kinematic(Vec3Fix::new(
Fix128::from_f64(x),
Fix128::from_f64(y),
Fix128::from_f64(z),
));
self.inner.add_body(body)
}
#[wasm_bindgen(js_name = "bodyCount")]
pub fn body_count(&self) -> usize {
self.inner.bodies.len()
}
pub fn step(&mut self, dt: f64) {
self.inner.step(Fix128::from_f64(dt));
}
#[wasm_bindgen(js_name = "stepN")]
pub fn step_n(&mut self, dt: f64, steps: u32) {
let dt_fix = Fix128::from_f64(dt);
for _ in 0..steps {
self.inner.step(dt_fix);
}
}
#[wasm_bindgen(js_name = "getPositions")]
pub fn get_positions(&self) -> Vec<f64> {
let n = self.inner.bodies.len();
let mut data = Vec::with_capacity(n * 3);
for body in &self.inner.bodies {
data.push(body.position.x.to_f64());
data.push(body.position.y.to_f64());
data.push(body.position.z.to_f64());
}
data
}
#[wasm_bindgen(js_name = "getVelocities")]
pub fn get_velocities(&self) -> Vec<f64> {
let n = self.inner.bodies.len();
let mut data = Vec::with_capacity(n * 3);
for body in &self.inner.bodies {
data.push(body.velocity.x.to_f64());
data.push(body.velocity.y.to_f64());
data.push(body.velocity.z.to_f64());
}
data
}
#[wasm_bindgen(js_name = "getRotations")]
pub fn get_rotations(&self) -> Vec<f64> {
let n = self.inner.bodies.len();
let mut data = Vec::with_capacity(n * 4);
for body in &self.inner.bodies {
data.push(body.rotation.x.to_f64());
data.push(body.rotation.y.to_f64());
data.push(body.rotation.z.to_f64());
data.push(body.rotation.w.to_f64());
}
data
}
#[wasm_bindgen(js_name = "getStates")]
pub fn get_states(&self) -> Vec<f64> {
let n = self.inner.bodies.len();
let mut data = Vec::with_capacity(n * 10);
for body in &self.inner.bodies {
data.push(body.position.x.to_f64());
data.push(body.position.y.to_f64());
data.push(body.position.z.to_f64());
data.push(body.velocity.x.to_f64());
data.push(body.velocity.y.to_f64());
data.push(body.velocity.z.to_f64());
data.push(body.rotation.x.to_f64());
data.push(body.rotation.y.to_f64());
data.push(body.rotation.z.to_f64());
data.push(body.rotation.w.to_f64());
}
data
}
#[wasm_bindgen(js_name = "getBodyType")]
pub fn get_body_type(&self, body_id: usize) -> u8 {
self.inner
.bodies
.get(body_id)
.map_or(255, |b| b.body_type as u8)
}
#[wasm_bindgen(js_name = "setPosition")]
pub fn set_position(&mut self, body_id: usize, x: f64, y: f64, z: f64) {
if let Some(body) = self.inner.bodies.get_mut(body_id) {
body.position = Vec3Fix::new(
Fix128::from_f64(x),
Fix128::from_f64(y),
Fix128::from_f64(z),
);
}
}
#[wasm_bindgen(js_name = "setVelocity")]
pub fn set_velocity(&mut self, body_id: usize, vx: f64, vy: f64, vz: f64) {
if let Some(body) = self.inner.bodies.get_mut(body_id) {
body.velocity = Vec3Fix::new(
Fix128::from_f64(vx),
Fix128::from_f64(vy),
Fix128::from_f64(vz),
);
}
}
#[wasm_bindgen(js_name = "applyImpulse")]
pub fn apply_impulse(&mut self, body_id: usize, ix: f64, iy: f64, iz: f64) {
if let Some(body) = self.inner.bodies.get_mut(body_id) {
body.apply_impulse(Vec3Fix::new(
Fix128::from_f64(ix),
Fix128::from_f64(iy),
Fix128::from_f64(iz),
));
}
}
#[wasm_bindgen(js_name = "setRestitution")]
pub fn set_restitution(&mut self, body_id: usize, restitution: f64) {
if let Some(body) = self.inner.bodies.get_mut(body_id) {
body.restitution = Fix128::from_f64(restitution);
}
}
#[wasm_bindgen(js_name = "setFriction")]
pub fn set_friction(&mut self, body_id: usize, friction: f64) {
if let Some(body) = self.inner.bodies.get_mut(body_id) {
body.friction = Fix128::from_f64(friction);
}
}
#[wasm_bindgen(js_name = "setGravityScale")]
pub fn set_gravity_scale(&mut self, body_id: usize, scale: f64) {
if let Some(body) = self.inner.bodies.get_mut(body_id) {
body.gravity_scale = Fix128::from_f64(scale);
}
}
#[wasm_bindgen(js_name = "setKinematicTarget")]
pub fn set_kinematic_target(
&mut self,
body_id: usize,
x: f64,
y: f64,
z: f64,
qx: f64,
qy: f64,
qz: f64,
qw: f64,
) {
if let Some(body) = self.inner.bodies.get_mut(body_id) {
body.set_kinematic_target(
Vec3Fix::new(
Fix128::from_f64(x),
Fix128::from_f64(y),
Fix128::from_f64(z),
),
QuatFix {
x: Fix128::from_f64(qx),
y: Fix128::from_f64(qy),
z: Fix128::from_f64(qz),
w: Fix128::from_f64(qw),
},
);
}
}
#[wasm_bindgen(js_name = "setVelocitiesBatch")]
pub fn set_velocities_batch(&mut self, data: &[f64]) {
let n = self.inner.bodies.len();
if data.len() < n * 3 {
return;
}
for (i, body) in self.inner.bodies.iter_mut().enumerate() {
body.velocity = Vec3Fix::new(
Fix128::from_f64(data[i * 3]),
Fix128::from_f64(data[i * 3 + 1]),
Fix128::from_f64(data[i * 3 + 2]),
);
}
}
#[wasm_bindgen(js_name = "applyImpulsesBatch")]
pub fn apply_impulses_batch(&mut self, data: &[f64]) {
let n_bodies = self.inner.bodies.len();
for chunk in data.chunks_exact(4) {
let body_id = chunk[0] as usize;
if body_id >= n_bodies {
continue;
}
let impulse = Vec3Fix::new(
Fix128::from_f64(chunk[1]),
Fix128::from_f64(chunk[2]),
Fix128::from_f64(chunk[3]),
);
self.inner.bodies[body_id].apply_impulse(impulse);
}
}
#[wasm_bindgen(js_name = "setGravity")]
pub fn set_gravity(&mut self, x: f64, y: f64, z: f64) {
self.inner.config.gravity = Vec3Fix::new(
Fix128::from_f64(x),
Fix128::from_f64(y),
Fix128::from_f64(z),
);
}
#[wasm_bindgen(js_name = "setSubsteps")]
pub fn set_substeps(&mut self, substeps: u32) {
self.inner.config.substeps = substeps as usize;
}
#[wasm_bindgen(js_name = "serializeState")]
pub fn serialize_state(&self) -> Vec<u8> {
self.inner.serialize_state()
}
#[wasm_bindgen(js_name = "deserializeState")]
pub fn deserialize_state(&mut self, data: &[u8]) -> bool {
self.inner.deserialize_state(data)
}
#[wasm_bindgen(js_name = "raycast")]
pub fn raycast(
&self,
ox: f64,
oy: f64,
oz: f64,
dx: f64,
dy: f64,
dz: f64,
max_distance: f64,
body_radius: f64,
) -> Vec<f64> {
let origin = Vec3Fix::new(
Fix128::from_f64(ox),
Fix128::from_f64(oy),
Fix128::from_f64(oz),
);
let direction = Vec3Fix::new(
Fix128::from_f64(dx),
Fix128::from_f64(dy),
Fix128::from_f64(dz),
);
let dir_len = direction.length();
if dir_len.is_zero() {
return Vec::new();
}
let dir_norm = direction / dir_len;
let ray = Ray::new(origin, dir_norm);
let max_t = Fix128::from_f64(max_distance);
let br = Fix128::from_f64(body_radius);
let mut best_t = max_t;
let mut result = Vec::new();
for (i, body) in self.inner.bodies.iter().enumerate() {
let expanded = Sphere::new(body.position, br);
if let Some(hit) = ray_sphere(&ray, &expanded, best_t) {
best_t = hit.t;
result = vec![
hit.t.to_f64(),
hit.point.x.to_f64(),
hit.point.y.to_f64(),
hit.point.z.to_f64(),
hit.normal.x.to_f64(),
hit.normal.y.to_f64(),
hit.normal.z.to_f64(),
i as f64,
];
}
}
result
}
}
#[wasm_bindgen(js_name = "alicePhysicsVersion")]
pub fn alice_physics_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_world_creation() {
let world = WasmPhysicsWorld::new();
assert_eq!(world.body_count(), 0);
}
#[test]
fn test_world_with_config() {
let world = WasmPhysicsWorld::with_config(0.0, -9.81, 0.0, 4, 8);
assert_eq!(world.body_count(), 0);
}
#[test]
fn test_add_bodies() {
let mut world = WasmPhysicsWorld::new();
let d = world.add_dynamic_body(0.0, 10.0, 0.0, 1.0);
let s = world.add_static_body(0.0, 0.0, 0.0);
let k = world.add_kinematic_body(5.0, 0.0, 0.0);
assert_eq!(d, 0);
assert_eq!(s, 1);
assert_eq!(k, 2);
assert_eq!(world.body_count(), 3);
}
#[test]
fn test_body_type() {
let mut world = WasmPhysicsWorld::new();
world.add_dynamic_body(0.0, 0.0, 0.0, 1.0);
world.add_static_body(0.0, 0.0, 0.0);
world.add_kinematic_body(0.0, 0.0, 0.0);
assert_eq!(world.get_body_type(0), BodyType::Dynamic as u8);
assert_eq!(world.get_body_type(1), BodyType::Static as u8);
assert_eq!(world.get_body_type(2), BodyType::Kinematic as u8);
assert_eq!(world.get_body_type(999), 255);
}
#[test]
fn test_step_gravity() {
let mut world = WasmPhysicsWorld::new();
world.add_dynamic_body(0.0, 10.0, 0.0, 1.0);
for _ in 0..60 {
world.step(1.0 / 60.0);
}
let positions = world.get_positions();
assert!(positions[1] < 10.0, "Body should fall, y={}", positions[1]);
}
#[test]
fn test_batch_getters() {
let mut world = WasmPhysicsWorld::new();
world.add_static_body(1.0, 2.0, 3.0);
world.add_static_body(4.0, 5.0, 6.0);
let pos = world.get_positions();
assert_eq!(pos.len(), 6);
assert!((pos[0] - 1.0).abs() < 1e-10);
assert!((pos[4] - 5.0).abs() < 1e-10);
let vel = world.get_velocities();
assert_eq!(vel.len(), 6);
let rot = world.get_rotations();
assert_eq!(rot.len(), 8);
let states = world.get_states();
assert_eq!(states.len(), 20); }
#[test]
fn test_set_position_velocity() {
let mut world = WasmPhysicsWorld::new();
world.set_gravity(0.0, 0.0, 0.0);
world.add_dynamic_body(0.0, 0.0, 0.0, 1.0);
world.set_position(0, 5.0, 6.0, 7.0);
let pos = world.get_positions();
assert!((pos[0] - 5.0).abs() < 1e-10);
assert!((pos[1] - 6.0).abs() < 1e-10);
world.set_velocity(0, 1.0, 0.0, 0.0);
let vel = world.get_velocities();
assert!((vel[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_state_serialization() {
let mut world = WasmPhysicsWorld::new();
world.add_dynamic_body(0.0, 10.0, 0.0, 1.0);
for _ in 0..30 {
world.step(1.0 / 60.0);
}
let state = world.serialize_state();
assert!(!state.is_empty());
for _ in 0..30 {
world.step(1.0 / 60.0);
}
let pos_after = world.get_positions();
world.deserialize_state(&state);
for _ in 0..30 {
world.step(1.0 / 60.0);
}
let pos_restored = world.get_positions();
assert!(
(pos_after[1] - pos_restored[1]).abs() < 1e-10,
"State restore should be deterministic"
);
}
#[test]
fn test_impulse_batch() {
let mut world = WasmPhysicsWorld::new();
world.set_gravity(0.0, 0.0, 0.0);
world.add_dynamic_body(0.0, 0.0, 0.0, 1.0);
world.apply_impulses_batch(&[0.0, 10.0, 0.0, 0.0]);
world.step(1.0 / 60.0);
let pos = world.get_positions();
assert!(pos[0] > 0.0, "Impulse should move body, x={}", pos[0]);
}
#[test]
fn test_raycast_miss() {
let mut world = WasmPhysicsWorld::new();
world.add_static_body(100.0, 0.0, 0.0);
let result = world.raycast(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 50.0, 1.0);
assert!(result.is_empty(), "Should miss body at x=100");
}
}