Skip to main content

arcane_core/scripting/
physics_ops.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use deno_core::OpState;
5
6use crate::physics::types::*;
7use crate::physics::world::PhysicsWorld;
8
9/// Wrapper for physics state in OpState.
10pub struct PhysicsState(pub Option<PhysicsWorld>);
11
12#[deno_core::op2(fast)]
13fn op_create_physics_world(state: &mut OpState, gravity_x: f64, gravity_y: f64) {
14    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
15    physics.borrow_mut().0 = Some(PhysicsWorld::new(gravity_x as f32, gravity_y as f32));
16}
17
18#[deno_core::op2(fast)]
19fn op_destroy_physics_world(state: &mut OpState) {
20    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
21    physics.borrow_mut().0 = None;
22}
23
24#[deno_core::op2(fast)]
25fn op_physics_step(state: &mut OpState, dt: f64) {
26    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
27    if let Some(world) = physics.borrow_mut().0.as_mut() {
28        world.step(dt as f32);
29    }
30}
31
32/// Create a body. shape_type: 0=circle, 1=aabb. body_type: 0=static, 1=dynamic, 2=kinematic.
33/// For circle: shape_p1=radius, shape_p2 unused.
34/// For AABB: shape_p1=half_w, shape_p2=half_h.
35#[deno_core::op2(fast)]
36fn op_create_body(
37    state: &mut OpState,
38    body_type: u32,
39    shape_type: u32,
40    shape_p1: f64,
41    shape_p2: f64,
42    x: f64,
43    y: f64,
44    mass: f64,
45    restitution: f64,
46    friction: f64,
47    layer: u32,
48    mask: u32,
49) -> u32 {
50    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
51    let mut ps = physics.borrow_mut();
52    let world = match ps.0.as_mut() {
53        Some(w) => w,
54        None => return u32::MAX,
55    };
56
57    let bt = match body_type {
58        0 => BodyType::Static,
59        1 => BodyType::Dynamic,
60        2 => BodyType::Kinematic,
61        _ => return u32::MAX,
62    };
63
64    let shape = match shape_type {
65        0 => Shape::Circle {
66            radius: shape_p1 as f32,
67        },
68        1 => Shape::AABB {
69            half_w: shape_p1 as f32,
70            half_h: shape_p2 as f32,
71        },
72        _ => return u32::MAX,
73    };
74
75    let material = Material {
76        restitution: restitution as f32,
77        friction: friction as f32,
78    };
79
80    world.add_body(bt, shape, x as f32, y as f32, mass as f32, material, layer as u16, mask as u16)
81}
82
83#[deno_core::op2(fast)]
84fn op_remove_body(state: &mut OpState, id: u32) {
85    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
86    let mut ps = physics.borrow_mut();
87    if let Some(world) = ps.0.as_mut() {
88        world.remove_body(id);
89    }
90}
91
92/// Returns [x, y, angle, vx, vy, angular_velocity] or empty vec.
93#[deno_core::op2]
94#[serde]
95fn op_get_body_state(state: &mut OpState, id: u32) -> Vec<f64> {
96    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
97    let ps = physics.borrow();
98    match ps.0.as_ref().and_then(|w| w.get_body(id)) {
99        Some(body) => vec![
100            body.x as f64,
101            body.y as f64,
102            body.angle as f64,
103            body.vx as f64,
104            body.vy as f64,
105            body.angular_velocity as f64,
106            if body.sleeping { 1.0 } else { 0.0 },
107        ],
108        None => vec![],
109    }
110}
111
112#[deno_core::op2(fast)]
113fn op_set_body_velocity(state: &mut OpState, id: u32, vx: f64, vy: f64) {
114    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
115    let mut ps = physics.borrow_mut();
116    if let Some(world) = ps.0.as_mut() {
117        world.set_velocity(id, vx as f32, vy as f32);
118    }
119}
120
121#[deno_core::op2(fast)]
122fn op_set_body_angular_velocity(state: &mut OpState, id: u32, av: f64) {
123    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
124    let mut ps = physics.borrow_mut();
125    if let Some(world) = ps.0.as_mut() {
126        world.set_angular_velocity(id, av as f32);
127    }
128}
129
130#[deno_core::op2(fast)]
131fn op_apply_force(state: &mut OpState, id: u32, fx: f64, fy: f64) {
132    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
133    let mut ps = physics.borrow_mut();
134    if let Some(world) = ps.0.as_mut() {
135        world.apply_force(id, fx as f32, fy as f32);
136    }
137}
138
139#[deno_core::op2(fast)]
140fn op_apply_impulse(state: &mut OpState, id: u32, ix: f64, iy: f64) {
141    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
142    let mut ps = physics.borrow_mut();
143    if let Some(world) = ps.0.as_mut() {
144        world.apply_impulse(id, ix as f32, iy as f32);
145    }
146}
147
148#[deno_core::op2(fast)]
149fn op_set_body_position(state: &mut OpState, id: u32, x: f64, y: f64) {
150    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
151    let mut ps = physics.borrow_mut();
152    if let Some(world) = ps.0.as_mut() {
153        world.set_position(id, x as f32, y as f32);
154    }
155}
156
157#[deno_core::op2(fast)]
158fn op_set_collision_layers(state: &mut OpState, id: u32, layer: u32, mask: u32) {
159    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
160    let mut ps = physics.borrow_mut();
161    if let Some(world) = ps.0.as_mut() {
162        world.set_collision_layers(id, layer as u16, mask as u16);
163    }
164}
165
166#[deno_core::op2(fast)]
167fn op_create_distance_joint(
168    state: &mut OpState,
169    body_a: u32,
170    body_b: u32,
171    distance: f64,
172) -> u32 {
173    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
174    let mut ps = physics.borrow_mut();
175    match ps.0.as_mut() {
176        Some(world) => world.add_constraint(Constraint::Distance {
177            id: 0,
178            body_a,
179            body_b,
180            distance: distance as f32,
181            anchor_a: (0.0, 0.0),
182            anchor_b: (0.0, 0.0),
183            soft: None,
184            accumulated_impulse: 0.0,
185        }),
186        None => u32::MAX,
187    }
188}
189
190#[deno_core::op2(fast)]
191fn op_create_revolute_joint(
192    state: &mut OpState,
193    body_a: u32,
194    body_b: u32,
195    pivot_x: f64,
196    pivot_y: f64,
197) -> u32 {
198    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
199    let mut ps = physics.borrow_mut();
200    match ps.0.as_mut() {
201        Some(world) => {
202            // Compute local anchor offsets from body centers to pivot
203            let (anchor_a, anchor_b) = {
204                let ba = world.get_body(body_a);
205                let bb = world.get_body(body_b);
206                let pivot = (pivot_x as f32, pivot_y as f32);
207                let anchor_a = match ba {
208                    Some(b) => {
209                        let cos = b.angle.cos();
210                        let sin = b.angle.sin();
211                        let dx = pivot.0 - b.x;
212                        let dy = pivot.1 - b.y;
213                        // Rotate into body-local space
214                        (dx * cos + dy * sin, -dx * sin + dy * cos)
215                    }
216                    None => (0.0, 0.0),
217                };
218                let anchor_b = match bb {
219                    Some(b) => {
220                        let cos = b.angle.cos();
221                        let sin = b.angle.sin();
222                        let dx = pivot.0 - b.x;
223                        let dy = pivot.1 - b.y;
224                        // Rotate into body-local space
225                        (dx * cos + dy * sin, -dx * sin + dy * cos)
226                    }
227                    None => (0.0, 0.0),
228                };
229                (anchor_a, anchor_b)
230            };
231            world.add_constraint(Constraint::Revolute {
232                id: 0,
233                body_a,
234                body_b,
235                anchor_a,
236                anchor_b,
237                soft: None,
238                accumulated_impulse: (0.0, 0.0),
239            })
240        },
241        None => u32::MAX,
242    }
243}
244
245/// Create a soft distance joint with frequency and damping.
246/// frequency_hz: 0 = rigid, typical soft: 1-5 Hz
247/// damping_ratio: 1.0 = critical damping
248#[deno_core::op2(fast)]
249fn op_create_soft_distance_joint(
250    state: &mut OpState,
251    body_a: u32,
252    body_b: u32,
253    distance: f64,
254    frequency_hz: f64,
255    damping_ratio: f64,
256) -> u32 {
257    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
258    let mut ps = physics.borrow_mut();
259    match ps.0.as_mut() {
260        Some(world) => world.add_constraint(Constraint::Distance {
261            id: 0,
262            body_a,
263            body_b,
264            distance: distance as f32,
265            anchor_a: (0.0, 0.0),
266            anchor_b: (0.0, 0.0),
267            soft: Some(SoftConstraintParams::soft(frequency_hz as f32, damping_ratio as f32)),
268            accumulated_impulse: 0.0,
269        }),
270        None => u32::MAX,
271    }
272}
273
274/// Create a soft revolute joint with frequency and damping.
275#[deno_core::op2(fast)]
276fn op_create_soft_revolute_joint(
277    state: &mut OpState,
278    body_a: u32,
279    body_b: u32,
280    pivot_x: f64,
281    pivot_y: f64,
282    frequency_hz: f64,
283    damping_ratio: f64,
284) -> u32 {
285    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
286    let mut ps = physics.borrow_mut();
287    match ps.0.as_mut() {
288        Some(world) => {
289            let (anchor_a, anchor_b) = {
290                let ba = world.get_body(body_a);
291                let bb = world.get_body(body_b);
292                let pivot = (pivot_x as f32, pivot_y as f32);
293                let anchor_a = match ba {
294                    Some(b) => {
295                        let cos = b.angle.cos();
296                        let sin = b.angle.sin();
297                        let dx = pivot.0 - b.x;
298                        let dy = pivot.1 - b.y;
299                        (dx * cos + dy * sin, -dx * sin + dy * cos)
300                    }
301                    None => (0.0, 0.0),
302                };
303                let anchor_b = match bb {
304                    Some(b) => {
305                        let cos = b.angle.cos();
306                        let sin = b.angle.sin();
307                        let dx = pivot.0 - b.x;
308                        let dy = pivot.1 - b.y;
309                        (dx * cos + dy * sin, -dx * sin + dy * cos)
310                    }
311                    None => (0.0, 0.0),
312                };
313                (anchor_a, anchor_b)
314            };
315            world.add_constraint(Constraint::Revolute {
316                id: 0,
317                body_a,
318                body_b,
319                anchor_a,
320                anchor_b,
321                soft: Some(SoftConstraintParams::soft(frequency_hz as f32, damping_ratio as f32)),
322                accumulated_impulse: (0.0, 0.0),
323            })
324        },
325        None => u32::MAX,
326    }
327}
328
329#[deno_core::op2(fast)]
330fn op_remove_constraint(state: &mut OpState, id: u32) {
331    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
332    let mut ps = physics.borrow_mut();
333    if let Some(world) = ps.0.as_mut() {
334        world.remove_constraint(id);
335    }
336}
337
338/// Returns body IDs overlapping the query rectangle.
339#[deno_core::op2]
340#[serde]
341fn op_query_aabb(
342    state: &mut OpState,
343    min_x: f64,
344    min_y: f64,
345    max_x: f64,
346    max_y: f64,
347) -> Vec<u32> {
348    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
349    let ps = physics.borrow();
350    match ps.0.as_ref() {
351        Some(world) => world.query_aabb(min_x as f32, min_y as f32, max_x as f32, max_y as f32),
352        None => vec![],
353    }
354}
355
356/// Returns [] for no hit, [body_id, hit_x, hit_y, distance] for hit.
357#[deno_core::op2]
358#[serde]
359fn op_raycast(
360    state: &mut OpState,
361    origin_x: f64,
362    origin_y: f64,
363    dir_x: f64,
364    dir_y: f64,
365    max_dist: f64,
366) -> Vec<f64> {
367    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
368    let ps = physics.borrow();
369    match ps.0.as_ref() {
370        Some(world) => {
371            match world.raycast(
372                origin_x as f32,
373                origin_y as f32,
374                dir_x as f32,
375                dir_y as f32,
376                max_dist as f32,
377            ) {
378                Some((id, hx, hy, dist)) => {
379                    vec![id as f64, hx as f64, hy as f64, dist as f64]
380                }
381                None => vec![],
382            }
383        }
384        None => vec![],
385    }
386}
387
388/// Create a polygon body. vertices is a flat [x0, y0, x1, y1, ...] array.
389/// body_type: 0=static, 1=dynamic, 2=kinematic.
390#[deno_core::op2]
391fn op_create_polygon_body(
392    state: &mut OpState,
393    body_type: u32,
394    #[serde] vertices: Vec<f64>,
395    x: f64,
396    y: f64,
397    mass: f64,
398    restitution: f64,
399    friction: f64,
400    layer: u32,
401    mask: u32,
402) -> u32 {
403    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
404    let mut ps = physics.borrow_mut();
405    let world = match ps.0.as_mut() {
406        Some(w) => w,
407        None => return u32::MAX,
408    };
409
410    let bt = match body_type {
411        0 => BodyType::Static,
412        1 => BodyType::Dynamic,
413        2 => BodyType::Kinematic,
414        _ => return u32::MAX,
415    };
416
417    // Convert flat vertex array to Vec<(f32, f32)>
418    if vertices.len() < 6 || vertices.len() % 2 != 0 {
419        return u32::MAX; // Need at least 3 vertices (6 values)
420    }
421    let polygon_verts: Vec<(f32, f32)> = vertices
422        .chunks(2)
423        .map(|c| (c[0] as f32, c[1] as f32))
424        .collect();
425
426    let shape = Shape::Polygon { vertices: polygon_verts };
427
428    let material = Material {
429        restitution: restitution as f32,
430        friction: friction as f32,
431    };
432
433    world.add_body(bt, shape, x as f32, y as f32, mass as f32, material, layer as u16, mask as u16)
434}
435
436/// Returns flattened contacts: [bodyA, bodyB, nx, ny, penetration, contactX, contactY, ...].
437#[deno_core::op2]
438#[serde]
439fn op_get_contacts(state: &mut OpState) -> Vec<f64> {
440    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
441    let ps = physics.borrow();
442    match ps.0.as_ref() {
443        Some(world) => {
444            let contacts = world.get_contacts();
445            let mut result = Vec::with_capacity(contacts.len() * 7);
446            for c in contacts {
447                result.push(c.body_a as f64);
448                result.push(c.body_b as f64);
449                result.push(c.normal.0 as f64);
450                result.push(c.normal.1 as f64);
451                result.push(c.penetration as f64);
452                result.push(c.contact_point.0 as f64);
453                result.push(c.contact_point.1 as f64);
454            }
455            result
456        }
457        None => vec![],
458    }
459}
460
461/// Get contact manifolds with all points (TGS Soft Phase 1).
462/// Returns flattened data: [bodyA, bodyB, nx, ny, numPoints,
463///   (localAx, localAy, localBx, localBy, penetration) × numPoints, ...]
464/// This exposes the full 2-point manifold data for visualization.
465#[deno_core::op2]
466#[serde]
467fn op_get_manifolds(state: &mut OpState) -> Vec<f64> {
468    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
469    let ps = physics.borrow();
470    match ps.0.as_ref() {
471        Some(world) => {
472            let manifolds = world.get_manifolds();
473            // Estimate capacity: ~10 values per manifold average
474            let mut result = Vec::with_capacity(manifolds.len() * 10);
475            for m in manifolds {
476                result.push(m.body_a as f64);
477                result.push(m.body_b as f64);
478                result.push(m.normal.0 as f64);
479                result.push(m.normal.1 as f64);
480                result.push(m.points.len() as f64);
481                for point in &m.points {
482                    result.push(point.local_a.0 as f64);
483                    result.push(point.local_a.1 as f64);
484                    result.push(point.local_b.0 as f64);
485                    result.push(point.local_b.1 as f64);
486                    result.push(point.penetration as f64);
487                }
488            }
489            result
490        }
491        None => vec![],
492    }
493}
494
495/// Get all body states as a packed f64 array for bulk readback.
496/// Layout per body: [id, x, y, vx, vy, angle, angular_velocity, is_sleeping(0/1)] = 8 f64s.
497/// Only includes bodies that exist (skips removed/empty slots).
498#[deno_core::op2]
499#[serde]
500fn op_get_all_body_states(state: &mut OpState) -> Vec<f64> {
501    let physics = state.borrow_mut::<Rc<RefCell<PhysicsState>>>();
502    let ps = physics.borrow();
503    match ps.0.as_ref() {
504        Some(world) => {
505            let bodies = world.all_bodies();
506            let mut result = Vec::with_capacity(bodies.len() * 8);
507            for body in bodies {
508                result.push(body.id as f64);
509                result.push(body.x as f64);
510                result.push(body.y as f64);
511                result.push(body.vx as f64);
512                result.push(body.vy as f64);
513                result.push(body.angle as f64);
514                result.push(body.angular_velocity as f64);
515                result.push(if body.sleeping { 1.0 } else { 0.0 });
516            }
517            result
518        }
519        None => vec![],
520    }
521}
522
523deno_core::extension!(
524    physics_ext,
525    ops = [
526        op_create_physics_world,
527        op_destroy_physics_world,
528        op_physics_step,
529        op_create_body,
530        op_create_polygon_body,
531        op_remove_body,
532        op_get_body_state,
533        op_set_body_velocity,
534        op_set_body_angular_velocity,
535        op_apply_force,
536        op_apply_impulse,
537        op_set_body_position,
538        op_set_collision_layers,
539        op_create_distance_joint,
540        op_create_revolute_joint,
541        op_create_soft_distance_joint,
542        op_create_soft_revolute_joint,
543        op_remove_constraint,
544        op_query_aabb,
545        op_raycast,
546        op_get_contacts,
547        op_get_manifolds,
548        op_get_all_body_states,
549    ],
550);