Skip to main content

arcane_core/scripting/
sdf_ops.rs

1/// SDF rendering ops: SDF shape commands submitted from TypeScript,
2/// queued for the renderer's SDF pipeline.
3///
4/// ## Command format
5/// Each SdfDrawCommand holds the WGSL expression, fill parameters, and
6/// transform data. The frame callback drains the queue and feeds it to
7/// the SDF rendering pipeline.
8
9use std::cell::RefCell;
10use std::rc::Rc;
11
12use deno_core::OpState;
13
14/// A single SDF draw command queued from TypeScript.
15#[derive(Clone, Debug)]
16pub struct SdfDrawCommand {
17    /// WGSL expression evaluating to f32 distance given `p: vec2<f32>`.
18    pub sdf_expr: String,
19    /// Fill type: 0=solid, 1=outline, 2=solid_outline, 3=gradient, 4=glow, 5=cosine_palette.
20    pub fill_type: u32,
21    /// Primary color [r, g, b, a].
22    pub color: [f32; 4],
23    /// Secondary color [r, g, b, a] (for gradient `to`, outline color, etc.).
24    pub color2: [f32; 4],
25    /// Fill parameter (thickness for outline, angle for gradient, intensity for glow).
26    pub fill_param: f32,
27    /// Cosine palette parameters: a, b, c, d as [r, g, b] each — packed into 12 floats.
28    pub palette_params: [f32; 12],
29    /// Gradient scale factor (1.0 = gradient spans full bounds, >1 = tighter).
30    pub gradient_scale: f32,
31    /// World position X.
32    pub x: f32,
33    /// World position Y.
34    pub y: f32,
35    /// Half-size of the rendering quad.
36    pub bounds: f32,
37    /// Render layer for sorting.
38    pub layer: i32,
39    /// Rotation in radians.
40    pub rotation: f32,
41    /// Uniform scale factor.
42    pub scale: f32,
43    /// Opacity 0-1.
44    pub opacity: f32,
45}
46
47/// SDF command queue: collected by TS ops, drained by the frame callback.
48pub struct SdfState {
49    pub commands: Vec<SdfDrawCommand>,
50}
51
52impl SdfState {
53    pub fn new() -> Self {
54        Self { commands: Vec::new() }
55    }
56}
57
58/// Queue an SDF draw command from TypeScript.
59///
60/// Parameters are split across multiple op calls to stay within the fast-op
61/// parameter limit. This op takes the core parameters; fill-specific params
62/// are encoded into the color/color2/fill_param fields.
63#[deno_core::op2(fast)]
64fn op_sdf_draw(
65    state: &mut OpState,
66    #[string] sdf_expr: &str,
67    fill_type: f64,
68    // Primary color
69    r: f64, g: f64, b: f64, a: f64,
70    // Secondary color
71    r2: f64, g2: f64, b2: f64, a2: f64,
72    // Fill param
73    fill_param: f64,
74    // Transform
75    x: f64, y: f64,
76    bounds: f64,
77    layer: f64,
78    rotation: f64,
79    scale: f64,
80    opacity: f64,
81) {
82    let sdf_state = state.borrow::<Rc<RefCell<SdfState>>>();
83    sdf_state.borrow_mut().commands.push(SdfDrawCommand {
84        sdf_expr: sdf_expr.to_string(),
85        fill_type: fill_type as u32,
86        color: [r as f32, g as f32, b as f32, a as f32],
87        color2: [r2 as f32, g2 as f32, b2 as f32, a2 as f32],
88        fill_param: fill_param as f32,
89        palette_params: [0.0; 12], // Set via separate op for cosine palette
90        gradient_scale: 1.0, // Set via separate op for gradient
91        x: x as f32,
92        y: y as f32,
93        bounds: bounds as f32,
94        layer: layer as i32,
95        rotation: rotation as f32,
96        scale: scale as f32,
97        opacity: opacity as f32,
98    });
99}
100
101/// Set cosine palette parameters for the most recently queued SDF command.
102/// Called immediately after op_sdf_draw when fill_type is cosine_palette.
103#[deno_core::op2(fast)]
104fn op_sdf_set_palette(
105    state: &mut OpState,
106    a_r: f64, a_g: f64, a_b: f64,
107    b_r: f64, b_g: f64, b_b: f64,
108    c_r: f64, c_g: f64, c_b: f64,
109    d_r: f64, d_g: f64, d_b: f64,
110) {
111    let sdf_state = state.borrow::<Rc<RefCell<SdfState>>>();
112    let mut borrowed = sdf_state.borrow_mut();
113    if let Some(cmd) = borrowed.commands.last_mut() {
114        cmd.palette_params = [
115            a_r as f32, a_g as f32, a_b as f32,
116            b_r as f32, b_g as f32, b_b as f32,
117            c_r as f32, c_g as f32, c_b as f32,
118            d_r as f32, d_g as f32, d_b as f32,
119        ];
120    }
121}
122
123/// Set gradient scale for the most recently queued SDF command.
124/// Called immediately after op_sdf_draw when fill_type is gradient.
125/// Scale > 1 makes the gradient span a smaller region (tighter fit to shape).
126#[deno_core::op2(fast)]
127fn op_sdf_set_gradient_scale(state: &mut OpState, scale: f64) {
128    let sdf_state = state.borrow::<Rc<RefCell<SdfState>>>();
129    let mut borrowed = sdf_state.borrow_mut();
130    if let Some(cmd) = borrowed.commands.last_mut() {
131        cmd.gradient_scale = scale as f32;
132    }
133}
134
135/// Clear all queued SDF commands (called at start of each frame).
136#[deno_core::op2(fast)]
137fn op_sdf_clear(state: &mut OpState) {
138    let sdf_state = state.borrow::<Rc<RefCell<SdfState>>>();
139    sdf_state.borrow_mut().commands.clear();
140}
141
142deno_core::extension!(
143    sdf_ext,
144    ops = [
145        op_sdf_draw,
146        op_sdf_set_palette,
147        op_sdf_set_gradient_scale,
148        op_sdf_clear,
149    ],
150);
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_sdf_state_new() {
158        let state = SdfState::new();
159        assert!(state.commands.is_empty());
160    }
161
162    #[test]
163    fn test_sdf_draw_command_fields() {
164        let cmd = SdfDrawCommand {
165            sdf_expr: "sd_circle(p, 20.0)".to_string(),
166            fill_type: 0,
167            color: [1.0, 0.0, 0.0, 1.0],
168            color2: [0.0, 0.0, 0.0, 0.0],
169            fill_param: 0.0,
170            palette_params: [0.0; 12],
171            gradient_scale: 1.0,
172            x: 100.0,
173            y: 200.0,
174            bounds: 25.0,
175            layer: 5,
176            rotation: 0.0,
177            scale: 1.0,
178            opacity: 1.0,
179        };
180        assert_eq!(cmd.layer, 5);
181        assert_eq!(cmd.bounds, 25.0);
182        assert_eq!(cmd.fill_type, 0);
183    }
184
185    #[test]
186    fn test_sdf_state_add_and_drain() {
187        let mut state = SdfState::new();
188        state.commands.push(SdfDrawCommand {
189            sdf_expr: "sd_box(p, vec2<f32>(10.0, 5.0))".to_string(),
190            fill_type: 1,
191            color: [1.0, 1.0, 1.0, 1.0],
192            color2: [0.0, 0.0, 0.0, 0.0],
193            fill_param: 2.0,
194            palette_params: [0.0; 12],
195            gradient_scale: 1.0,
196            x: 50.0,
197            y: 75.0,
198            bounds: 15.0,
199            layer: 0,
200            rotation: 0.785,
201            scale: 2.0,
202            opacity: 0.8,
203        });
204        assert_eq!(state.commands.len(), 1);
205
206        let drained: Vec<_> = state.commands.drain(..).collect();
207        assert_eq!(drained.len(), 1);
208        assert!(state.commands.is_empty());
209        assert_eq!(drained[0].sdf_expr, "sd_box(p, vec2<f32>(10.0, 5.0))");
210    }
211
212    #[test]
213    fn test_sdf_state_multiple_commands() {
214        let mut state = SdfState::new();
215        for i in 0..10 {
216            state.commands.push(SdfDrawCommand {
217                sdf_expr: format!("sd_circle(p, {}.0)", i * 5),
218                fill_type: 0,
219                color: [1.0, 1.0, 1.0, 1.0],
220                color2: [0.0; 4],
221                fill_param: 0.0,
222                palette_params: [0.0; 12],
223                gradient_scale: 1.0,
224                x: i as f32 * 10.0,
225                y: 0.0,
226                bounds: (i * 5 + 5) as f32,
227                layer: i as i32,
228                rotation: 0.0,
229                scale: 1.0,
230                opacity: 1.0,
231            });
232        }
233        assert_eq!(state.commands.len(), 10);
234    }
235
236    #[test]
237    fn test_sdf_palette_params() {
238        let mut state = SdfState::new();
239        state.commands.push(SdfDrawCommand {
240            sdf_expr: "sd_circle(p, 30.0)".to_string(),
241            fill_type: 5,
242            color: [0.0; 4],
243            color2: [0.0; 4],
244            fill_param: 0.0,
245            palette_params: [
246                0.5, 0.5, 0.5,  // a
247                0.5, 0.5, 0.5,  // b
248                1.0, 1.0, 1.0,  // c
249                0.0, 0.33, 0.67, // d
250            ],
251            gradient_scale: 1.0,
252            x: 0.0, y: 0.0, bounds: 35.0,
253            layer: 0, rotation: 0.0, scale: 1.0, opacity: 1.0,
254        });
255        let cmd = &state.commands[0];
256        assert_eq!(cmd.palette_params[0], 0.5);
257        assert_eq!(cmd.palette_params[9], 0.0);
258        assert!((cmd.palette_params[10] - 0.33).abs() < 0.001);
259        assert!((cmd.palette_params[11] - 0.67).abs() < 0.001);
260    }
261}