Skip to main content

arcane_core/scripting/
geometry_ops.rs

1/// Geometry pipeline ops: triangles and line segments submitted from TS,
2/// rendered in a dedicated GPU pass after the sprite batch.
3///
4/// Stream A owns this file. Add ops here; wire into renderer/mod.rs in Phase 2.
5///
6/// ## Command format
7/// Each GeoCommand is a tagged enum collected into GeoState.commands per frame.
8/// The frame callback in dev.rs drains GeoState and passes to GeometryBatch::flush().
9
10use std::cell::RefCell;
11use std::rc::Rc;
12
13use deno_core::OpState;
14
15/// A single geometry draw command queued from TS.
16#[derive(Clone, Debug)]
17pub enum GeoCommand {
18    Triangle {
19        x1: f32, y1: f32,
20        x2: f32, y2: f32,
21        x3: f32, y3: f32,
22        r: f32, g: f32, b: f32, a: f32,
23        layer: i32,
24    },
25    LineSeg {
26        x1: f32, y1: f32,
27        x2: f32, y2: f32,
28        thickness: f32,
29        r: f32, g: f32, b: f32, a: f32,
30        layer: i32,
31    },
32}
33
34impl GeoCommand {
35    pub fn layer(&self) -> i32 {
36        match self {
37            GeoCommand::Triangle { layer, .. } => *layer,
38            GeoCommand::LineSeg { layer, .. } => *layer,
39        }
40    }
41}
42
43/// Geometry command queue: collected by TS ops, drained by the frame callback.
44pub struct GeoState {
45    pub commands: Vec<GeoCommand>,
46}
47
48impl GeoState {
49    pub fn new() -> Self {
50        Self { commands: Vec::new() }
51    }
52}
53
54/// Push a filled triangle to the geometry command queue.
55/// All params are f64 (V8 number boundary), converted to f32 internally.
56#[deno_core::op2(fast)]
57fn op_geo_triangle(
58    state: &mut OpState,
59    x1: f64, y1: f64,
60    x2: f64, y2: f64,
61    x3: f64, y3: f64,
62    r: f64, g: f64, b: f64, a: f64,
63    layer: f64,
64) {
65    let geo = state.borrow::<Rc<RefCell<GeoState>>>();
66    geo.borrow_mut().commands.push(GeoCommand::Triangle {
67        x1: x1 as f32, y1: y1 as f32,
68        x2: x2 as f32, y2: y2 as f32,
69        x3: x3 as f32, y3: y3 as f32,
70        r: r as f32, g: g as f32, b: b as f32, a: a as f32,
71        layer: layer as i32,
72    });
73}
74
75/// Push a thick line segment to the geometry command queue.
76/// The line is rendered as a quad (2 triangles) with the given thickness.
77/// All params are f64 (V8 number boundary), converted to f32 internally.
78#[deno_core::op2(fast)]
79fn op_geo_line(
80    state: &mut OpState,
81    x1: f64, y1: f64,
82    x2: f64, y2: f64,
83    thickness: f64,
84    r: f64, g: f64, b: f64, a: f64,
85    layer: f64,
86) {
87    let geo = state.borrow::<Rc<RefCell<GeoState>>>();
88    geo.borrow_mut().commands.push(GeoCommand::LineSeg {
89        x1: x1 as f32, y1: y1 as f32,
90        x2: x2 as f32, y2: y2 as f32,
91        thickness: thickness as f32,
92        r: r as f32, g: g as f32, b: b as f32, a: a as f32,
93        layer: layer as i32,
94    });
95}
96
97deno_core::extension!(
98    geometry_ext,
99    ops = [
100        op_geo_triangle,
101        op_geo_line,
102    ],
103);
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_geo_command_triangle_layer() {
111        let cmd = GeoCommand::Triangle {
112            x1: 0.0, y1: 0.0,
113            x2: 10.0, y2: 0.0,
114            x3: 5.0, y3: 10.0,
115            r: 1.0, g: 0.0, b: 0.0, a: 1.0,
116            layer: 5,
117        };
118        assert_eq!(cmd.layer(), 5);
119    }
120
121    #[test]
122    fn test_geo_command_line_layer() {
123        let cmd = GeoCommand::LineSeg {
124            x1: 0.0, y1: 0.0,
125            x2: 100.0, y2: 100.0,
126            thickness: 2.0,
127            r: 0.0, g: 1.0, b: 0.0, a: 0.5,
128            layer: 10,
129        };
130        assert_eq!(cmd.layer(), 10);
131    }
132
133    #[test]
134    fn test_geo_state_new() {
135        let state = GeoState::new();
136        assert!(state.commands.is_empty());
137    }
138
139    #[test]
140    fn test_geo_state_add_commands() {
141        let mut state = GeoState::new();
142
143        state.commands.push(GeoCommand::Triangle {
144            x1: 0.0, y1: 0.0, x2: 10.0, y2: 0.0, x3: 5.0, y3: 10.0,
145            r: 1.0, g: 1.0, b: 1.0, a: 1.0, layer: 0,
146        });
147
148        state.commands.push(GeoCommand::LineSeg {
149            x1: 0.0, y1: 0.0, x2: 50.0, y2: 50.0,
150            thickness: 3.0, r: 0.5, g: 0.5, b: 0.5, a: 1.0, layer: 1,
151        });
152
153        assert_eq!(state.commands.len(), 2);
154        assert_eq!(state.commands[0].layer(), 0);
155        assert_eq!(state.commands[1].layer(), 1);
156    }
157
158    #[test]
159    fn test_geo_state_drain() {
160        let mut state = GeoState::new();
161
162        state.commands.push(GeoCommand::Triangle {
163            x1: 0.0, y1: 0.0, x2: 10.0, y2: 0.0, x3: 5.0, y3: 10.0,
164            r: 1.0, g: 0.0, b: 0.0, a: 1.0, layer: 0,
165        });
166
167        let drained: Vec<_> = state.commands.drain(..).collect();
168        assert_eq!(drained.len(), 1);
169        assert!(state.commands.is_empty());
170    }
171}