Skip to main content

arcane_core/scripting/
render_ops.rs

1use std::cell::RefCell;
2use std::path::PathBuf;
3use std::rc::Rc;
4
5use deno_core::OpState;
6
7use crate::renderer::SpriteCommand;
8use crate::renderer::TilemapStore;
9use crate::renderer::PointLight;
10use crate::renderer::camera::CameraBounds;
11use crate::renderer::msdf::MsdfFontStore;
12
13/// Audio command queued from TS ops, drained by the frame callback.
14#[derive(Clone, Debug)]
15pub enum BridgeAudioCommand {
16    LoadSound { id: u32, path: String },
17    StopAll,
18    SetMasterVolume { volume: f32 },
19
20    // Phase 20: New instance-based commands
21    PlaySoundEx {
22        sound_id: u32,
23        instance_id: u64,
24        volume: f32,
25        looping: bool,
26        bus: u32,
27        pan: f32,
28        pitch: f32,
29        low_pass_freq: u32,
30        reverb_mix: f32,
31        reverb_delay_ms: u32,
32    },
33    PlaySoundSpatial {
34        sound_id: u32,
35        instance_id: u64,
36        volume: f32,
37        looping: bool,
38        bus: u32,
39        pitch: f32,
40        source_x: f32,
41        source_y: f32,
42        listener_x: f32,
43        listener_y: f32,
44    },
45    StopInstance { instance_id: u64 },
46    SetInstanceVolume { instance_id: u64, volume: f32 },
47    SetInstancePitch { instance_id: u64, pitch: f32 },
48    UpdateSpatialPositions {
49        updates: Vec<(u64, f32, f32)>, // (instance_id, source_x, source_y)
50        listener_x: f32,
51        listener_y: f32,
52    },
53    SetBusVolume { bus: u32, volume: f32 },
54}
55
56/// Shared state between render ops and the main loop.
57/// This is placed into `OpState` when running in renderer mode.
58#[derive(Clone)]
59pub struct RenderBridgeState {
60    pub sprite_commands: Vec<SpriteCommand>,
61    pub camera_x: f32,
62    pub camera_y: f32,
63    pub camera_zoom: f32,
64    /// True when TS called setCamera() this frame (prevents sync-back from overwriting it).
65    pub camera_dirty: bool,
66    pub delta_time: f64,
67    /// Accumulated elapsed time in seconds (reset on hot-reload).
68    pub elapsed_time: f64,
69    /// Input state snapshot (updated each frame by the event loop).
70    pub keys_down: std::collections::HashSet<String>,
71    pub keys_pressed: std::collections::HashSet<String>,
72    pub mouse_x: f32,
73    pub mouse_y: f32,
74    pub mouse_buttons_down: std::collections::HashSet<u8>,
75    pub mouse_buttons_pressed: std::collections::HashSet<u8>,
76    /// Gamepad state: buttons down per button name string.
77    pub gamepad_buttons_down: std::collections::HashSet<String>,
78    /// Gamepad buttons pressed this frame.
79    pub gamepad_buttons_pressed: std::collections::HashSet<String>,
80    /// Gamepad axis values: axis name -> value.
81    pub gamepad_axes: std::collections::HashMap<String, f32>,
82    /// Number of connected gamepads.
83    pub gamepad_count: u32,
84    /// Name of the primary gamepad.
85    pub gamepad_name: String,
86    /// Touch state: active touch points as (id, x, y).
87    pub touch_points: Vec<(u64, f32, f32)>,
88    /// Number of active touches.
89    pub touch_count: u32,
90    /// Pending texture load requests (path → result channel).
91    pub texture_load_queue: Vec<(String, u32)>,
92    /// Pending texture load requests with linear filtering.
93    pub texture_load_queue_linear: Vec<(String, u32)>,
94    /// Base directory for resolving relative texture paths.
95    pub base_dir: PathBuf,
96    /// Next texture ID to assign (for pre-registration before GPU load).
97    pub next_texture_id: u32,
98    /// Map of path → already-assigned texture ID.
99    pub texture_path_to_id: std::collections::HashMap<String, u32>,
100    /// Tilemap storage (managed by tilemap ops).
101    pub tilemaps: TilemapStore,
102    /// Lighting: ambient color (0-1 per channel). Default white = no darkening.
103    pub ambient_light: [f32; 3],
104    /// Lighting: point lights for this frame.
105    pub point_lights: Vec<PointLight>,
106    /// Audio commands queued by TS, drained each frame.
107    pub audio_commands: Vec<BridgeAudioCommand>,
108    /// Next sound ID to assign.
109    pub next_sound_id: u32,
110    /// Map of sound path → assigned sound ID.
111    pub sound_path_to_id: std::collections::HashMap<String, u32>,
112    /// Font texture creation queue (texture IDs to create as built-in font).
113    pub font_texture_queue: Vec<u32>,
114    /// Current viewport dimensions in logical pixels (synced from renderer each frame).
115    pub viewport_width: f32,
116    pub viewport_height: f32,
117    /// Display scale factor (e.g. 2.0 on Retina).
118    pub scale_factor: f32,
119    /// Clear/background color [r, g, b, a] in 0.0-1.0 range.
120    pub clear_color: [f32; 4],
121    /// Directory for save files (.arcane/saves/ relative to game entry file).
122    pub save_dir: PathBuf,
123    /// Custom shader creation queue: (id, name, wgsl_source).
124    pub shader_create_queue: Vec<(u32, String, String)>,
125    /// Custom shader param updates: (shader_id, index, [x, y, z, w]).
126    pub shader_param_queue: Vec<(u32, u32, [f32; 4])>,
127    /// Next shader ID to assign.
128    pub next_shader_id: u32,
129    /// Post-process effect creation queue: (id, effect_type_name).
130    pub effect_create_queue: Vec<(u32, String)>,
131    /// Post-process effect param updates: (effect_id, index, [x, y, z, w]).
132    pub effect_param_queue: Vec<(u32, u32, [f32; 4])>,
133    /// Post-process effect removal queue.
134    pub effect_remove_queue: Vec<u32>,
135    /// Flag to clear all post-process effects.
136    pub effect_clear: bool,
137    /// Next effect ID to assign.
138    pub next_effect_id: u32,
139    /// Camera bounds (world-space limits).
140    pub camera_bounds: Option<CameraBounds>,
141    /// Whether global illumination (radiance cascades) is enabled.
142    pub gi_enabled: bool,
143    /// GI intensity multiplier.
144    pub gi_intensity: f32,
145    /// GI probe spacing override (None = default 8).
146    pub gi_probe_spacing: Option<f32>,
147    /// GI interval override (None = default 4).
148    pub gi_interval: Option<f32>,
149    /// GI cascade count override (None = default 4).
150    pub gi_cascade_count: Option<u32>,
151    /// Emissive surfaces for GI: (x, y, w, h, r, g, b, intensity).
152    pub emissives: Vec<[f32; 8]>,
153    /// Occluders for GI: (x, y, w, h).
154    pub occluders: Vec<[f32; 4]>,
155    /// Directional lights: (angle, r, g, b, intensity).
156    pub directional_lights: Vec<[f32; 5]>,
157    /// Spot lights: (x, y, angle, spread, range, r, g, b, intensity).
158    pub spot_lights: Vec<[f32; 9]>,
159    /// MSDF font storage.
160    pub msdf_fonts: MsdfFontStore,
161    /// Queue for creating built-in MSDF font: (font_id, texture_id).
162    pub msdf_builtin_queue: Vec<(u32, u32)>,
163    /// Queue for creating MSDF shader: (shader_id, wgsl_source).
164    pub msdf_shader_queue: Vec<(u32, String)>,
165    /// Pool of MSDF shader IDs (same WGSL, separate uniform buffers for per-draw-call params).
166    pub msdf_shader_pool: Vec<u32>,
167    /// Pending MSDF texture loads (needs linear sampling, not sRGB).
168    pub msdf_texture_load_queue: Vec<(String, u32)>,
169    /// Raw RGBA texture upload queue: (texture_id, width, height, pixels).
170    pub raw_texture_upload_queue: Vec<(u32, u32, u32, Vec<u8>)>,
171    /// Frame timing: milliseconds elapsed during the last frame's script execution.
172    pub frame_time_ms: f64,
173    /// Frame timing: number of draw calls (sprite commands) queued last frame.
174    pub draw_call_count: usize,
175}
176
177impl RenderBridgeState {
178    pub fn new(base_dir: PathBuf) -> Self {
179        let save_dir = base_dir.join(".arcane").join("saves");
180        Self {
181            sprite_commands: Vec::new(),
182            camera_x: 0.0,
183            camera_y: 0.0,
184            camera_zoom: 1.0,
185            camera_dirty: false,
186            delta_time: 0.0,
187            elapsed_time: 0.0,
188            keys_down: std::collections::HashSet::new(),
189            keys_pressed: std::collections::HashSet::new(),
190            mouse_x: 0.0,
191            mouse_y: 0.0,
192            mouse_buttons_down: std::collections::HashSet::new(),
193            mouse_buttons_pressed: std::collections::HashSet::new(),
194            gamepad_buttons_down: std::collections::HashSet::new(),
195            gamepad_buttons_pressed: std::collections::HashSet::new(),
196            gamepad_axes: std::collections::HashMap::new(),
197            gamepad_count: 0,
198            gamepad_name: String::new(),
199            touch_points: Vec::new(),
200            touch_count: 0,
201            texture_load_queue: Vec::new(),
202            texture_load_queue_linear: Vec::new(),
203            base_dir,
204            next_texture_id: 1,
205            texture_path_to_id: std::collections::HashMap::new(),
206            tilemaps: TilemapStore::new(),
207            ambient_light: [1.0, 1.0, 1.0],
208            point_lights: Vec::new(),
209            audio_commands: Vec::new(),
210            next_sound_id: 1,
211            sound_path_to_id: std::collections::HashMap::new(),
212            font_texture_queue: Vec::new(),
213            viewport_width: 800.0,
214            viewport_height: 600.0,
215            scale_factor: 1.0,
216            clear_color: [0.1, 0.1, 0.15, 1.0],
217            save_dir,
218            shader_create_queue: Vec::new(),
219            shader_param_queue: Vec::new(),
220            next_shader_id: 1,
221            effect_create_queue: Vec::new(),
222            effect_param_queue: Vec::new(),
223            effect_remove_queue: Vec::new(),
224            effect_clear: false,
225            next_effect_id: 1,
226            camera_bounds: None,
227            gi_enabled: false,
228            gi_intensity: 1.0,
229            gi_probe_spacing: None,
230            gi_interval: None,
231            gi_cascade_count: None,
232            emissives: Vec::new(),
233            occluders: Vec::new(),
234            directional_lights: Vec::new(),
235            spot_lights: Vec::new(),
236            msdf_fonts: MsdfFontStore::new(),
237            msdf_builtin_queue: Vec::new(),
238            msdf_shader_queue: Vec::new(),
239            msdf_shader_pool: Vec::new(),
240            msdf_texture_load_queue: Vec::new(),
241            raw_texture_upload_queue: Vec::new(),
242            frame_time_ms: 0.0,
243            draw_call_count: 0,
244        }
245    }
246}
247
248/// Clear all queued sprite commands for this frame.
249#[deno_core::op2(fast)]
250pub fn op_clear_sprites(state: &mut OpState) {
251    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
252    bridge.borrow_mut().sprite_commands.clear();
253}
254
255/// Number of f32 values per sprite in the batch buffer.
256/// Layout: [texture_id, x, y, w, h, layer, uv_x, uv_y, uv_w, uv_h,
257///          tint_r, tint_g, tint_b, tint_a, rotation, origin_x, origin_y,
258///          flip_x, flip_y, opacity, blend_mode, shader_id]
259pub const SPRITE_STRIDE: usize = 22;
260
261/// Submit a batch of sprites from a packed Float32Array.
262/// Each sprite is SPRITE_STRIDE (22) f32 values. See layout above.
263/// Called from TS sprites.ts flush path for bulk submission.
264#[deno_core::op2(fast)]
265pub fn op_submit_sprite_batch(state: &mut OpState, #[buffer] data: &[u8]) {
266    let floats: &[f32] = bytemuck::cast_slice(data);
267    let sprite_count = floats.len() / SPRITE_STRIDE;
268
269    // Check if a render target is active — batch goes to target or main surface
270    let active_target = {
271        use super::target_ops::TargetState;
272        let ts = state.borrow::<Rc<RefCell<TargetState>>>();
273        ts.borrow().active_target
274    };
275
276    let parse_cmd = |s: &[f32]| SpriteCommand {
277        texture_id: s[0].to_bits(),
278        x: s[1],
279        y: s[2],
280        w: s[3],
281        h: s[4],
282        layer: s[5].to_bits() as i32,
283        uv_x: s[6],
284        uv_y: s[7],
285        uv_w: s[8],
286        uv_h: s[9],
287        tint_r: s[10],
288        tint_g: s[11],
289        tint_b: s[12],
290        tint_a: s[13],
291        rotation: s[14],
292        origin_x: s[15],
293        origin_y: s[16],
294        flip_x: s[17] != 0.0,
295        flip_y: s[18] != 0.0,
296        opacity: s[19],
297        blend_mode: (s[20] as u8).min(3),
298        shader_id: s[21].to_bits(),
299    };
300
301    if let Some(target_id) = active_target {
302        use super::target_ops::TargetState;
303        let ts = state.borrow::<Rc<RefCell<TargetState>>>();
304        let mut ts = ts.borrow_mut();
305        let queue = ts.target_sprite_queues.entry(target_id).or_default();
306        queue.reserve(sprite_count);
307        for i in 0..sprite_count {
308            let base = i * SPRITE_STRIDE;
309            queue.push(parse_cmd(&floats[base..base + SPRITE_STRIDE]));
310        }
311    } else {
312        let bridge = state.borrow::<Rc<RefCell<RenderBridgeState>>>();
313        let mut b = bridge.borrow_mut();
314        b.sprite_commands.reserve(sprite_count);
315        for i in 0..sprite_count {
316            let base = i * SPRITE_STRIDE;
317            b.sprite_commands.push(parse_cmd(&floats[base..base + SPRITE_STRIDE]));
318        }
319    }
320}
321
322/// Update the camera position and zoom.
323/// Accepts f64 (JavaScript's native number type), converts to f32 for GPU.
324#[deno_core::op2(fast)]
325pub fn op_set_camera(state: &mut OpState, x: f64, y: f64, zoom: f64) {
326    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
327    let mut b = bridge.borrow_mut();
328    b.camera_x = x as f32;
329    b.camera_y = y as f32;
330    b.camera_zoom = zoom as f32;
331    b.camera_dirty = true;
332}
333
334/// Get camera state as [x, y, zoom].
335#[deno_core::op2]
336#[serde]
337pub fn op_get_camera(state: &mut OpState) -> Vec<f64> {
338    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
339    let b = bridge.borrow();
340    vec![b.camera_x as f64, b.camera_y as f64, b.camera_zoom as f64]
341}
342
343/// Register a texture to be loaded. Returns a texture ID immediately.
344/// The actual GPU upload happens on the main thread before the next render.
345#[deno_core::op2(fast)]
346pub fn op_load_texture(state: &mut OpState, #[string] path: &str) -> u32 {
347    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
348    let mut b = bridge.borrow_mut();
349
350    // Resolve relative paths against base_dir
351    let resolved = if std::path::Path::new(path).is_absolute() {
352        path.to_string()
353    } else {
354        b.base_dir.join(path).to_string_lossy().to_string()
355    };
356
357    // Check cache
358    if let Some(&id) = b.texture_path_to_id.get(&resolved) {
359        return id;
360    }
361
362    let id = b.next_texture_id;
363    b.next_texture_id += 1;
364    b.texture_path_to_id.insert(resolved.clone(), id);
365    b.texture_load_queue.push((resolved, id));
366    id
367}
368
369/// Load a texture with linear filtering (smooth, blended).
370/// Use for gradients, photos, or pre-rendered 3D sprites.
371/// For pixel art, use op_load_texture (nearest filtering).
372#[deno_core::op2(fast)]
373pub fn op_load_texture_linear(state: &mut OpState, #[string] path: &str) -> u32 {
374    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
375    let mut b = bridge.borrow_mut();
376
377    // Resolve relative paths against base_dir
378    let resolved = if std::path::Path::new(path).is_absolute() {
379        path.to_string()
380    } else {
381        b.base_dir.join(path).to_string_lossy().to_string()
382    };
383
384    // Check cache (note: linear textures share the same ID space but use separate queue)
385    if let Some(&id) = b.texture_path_to_id.get(&resolved) {
386        return id;
387    }
388
389    let id = b.next_texture_id;
390    b.next_texture_id += 1;
391    b.texture_path_to_id.insert(resolved.clone(), id);
392    b.texture_load_queue_linear.push((resolved, id));
393    id
394}
395
396/// Check if a key is currently held down.
397#[deno_core::op2(fast)]
398pub fn op_is_key_down(state: &mut OpState, #[string] key: &str) -> bool {
399    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
400    bridge.borrow().keys_down.contains(key)
401}
402
403/// Check if a key was pressed this frame.
404#[deno_core::op2(fast)]
405pub fn op_is_key_pressed(state: &mut OpState, #[string] key: &str) -> bool {
406    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
407    bridge.borrow().keys_pressed.contains(key)
408}
409
410/// Get mouse position as [x, y].
411#[deno_core::op2]
412#[serde]
413pub fn op_get_mouse_position(state: &mut OpState) -> Vec<f64> {
414    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
415    let b = bridge.borrow();
416    vec![b.mouse_x as f64, b.mouse_y as f64]
417}
418
419/// Check if a mouse button is currently held down.
420/// Button 0 = left, 1 = right, 2 = middle.
421#[deno_core::op2(fast)]
422pub fn op_is_mouse_button_down(state: &mut OpState, button: u8) -> bool {
423    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
424    bridge.borrow().mouse_buttons_down.contains(&button)
425}
426
427/// Check if a mouse button was pressed this frame.
428/// Button 0 = left, 1 = right, 2 = middle.
429#[deno_core::op2(fast)]
430pub fn op_is_mouse_button_pressed(state: &mut OpState, button: u8) -> bool {
431    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
432    bridge.borrow().mouse_buttons_pressed.contains(&button)
433}
434
435/// Get the delta time (seconds since last frame).
436#[deno_core::op2(fast)]
437pub fn op_get_delta_time(state: &mut OpState) -> f64 {
438    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
439    bridge.borrow().delta_time
440}
441
442/// Create a solid-color texture from TS. Returns texture ID.
443/// The actual GPU upload happens on the main thread.
444#[deno_core::op2(fast)]
445pub fn op_create_solid_texture(
446    state: &mut OpState,
447    #[string] name: &str,
448    r: u32,
449    g: u32,
450    b: u32,
451    a: u32,
452) -> u32 {
453    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
454    let mut br = bridge.borrow_mut();
455
456    let key = format!("__solid__{name}");
457    if let Some(&id) = br.texture_path_to_id.get(&key) {
458        return id;
459    }
460
461    let id = br.next_texture_id;
462    br.next_texture_id += 1;
463    br.texture_path_to_id.insert(key.clone(), id);
464    // Encode color in the path as a signal to the loader
465    br.texture_load_queue
466        .push((format!("__solid__:{name}:{r}:{g}:{b}:{a}"), id));
467    id
468}
469
470/// Upload a raw RGBA texture from a pixel buffer. Cached by name.
471/// Returns existing texture ID if a texture with the same name was already uploaded.
472#[deno_core::op2(fast)]
473pub fn op_upload_rgba_texture(
474    state: &mut OpState,
475    #[string] name: &str,
476    width: f64,
477    height: f64,
478    #[buffer] pixels: &[u8],
479) -> u32 {
480    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
481    let mut b = bridge.borrow_mut();
482
483    let key = format!("__raw__:{name}");
484    if let Some(&id) = b.texture_path_to_id.get(&key) {
485        return id;
486    }
487
488    let id = b.next_texture_id;
489    b.next_texture_id += 1;
490    b.texture_path_to_id.insert(key, id);
491    b.raw_texture_upload_queue.push((
492        id,
493        width as u32,
494        height as u32,
495        pixels.to_vec(),
496    ));
497    id
498}
499
500/// Create a tilemap. Returns tilemap ID.
501#[deno_core::op2(fast)]
502pub fn op_create_tilemap(
503    state: &mut OpState,
504    texture_id: u32,
505    width: u32,
506    height: u32,
507    tile_size: f64,
508    atlas_columns: u32,
509    atlas_rows: u32,
510) -> u32 {
511    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
512    bridge
513        .borrow_mut()
514        .tilemaps
515        .create(texture_id, width, height, tile_size as f32, atlas_columns, atlas_rows)
516}
517
518/// Set a tile in a tilemap.
519#[deno_core::op2(fast)]
520pub fn op_set_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32, tile_id: u32) {
521    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
522    if let Some(tm) = bridge.borrow_mut().tilemaps.get_mut(tilemap_id) {
523        tm.set_tile(gx, gy, tile_id as u16);
524    }
525}
526
527/// Get a tile from a tilemap.
528#[deno_core::op2(fast)]
529pub fn op_get_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32) -> u32 {
530    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
531    bridge
532        .borrow()
533        .tilemaps
534        .get(tilemap_id)
535        .map(|tm| tm.get_tile(gx, gy) as u32)
536        .unwrap_or(0)
537}
538
539/// Draw a tilemap's visible tiles as sprite commands (camera-culled).
540/// Accepts f64 (JavaScript's native number type), converts to f32 for GPU.
541#[deno_core::op2(fast)]
542pub fn op_draw_tilemap(state: &mut OpState, tilemap_id: u32, world_x: f64, world_y: f64, layer: i32) {
543    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
544    let mut b = bridge.borrow_mut();
545    let cam_x = b.camera_x;
546    let cam_y = b.camera_y;
547    let cam_zoom = b.camera_zoom;
548    // Default viewport for culling; actual viewport is synced by renderer
549    let vp_w = 800.0;
550    let vp_h = 600.0;
551
552    if let Some(tm) = b.tilemaps.get(tilemap_id) {
553        let cmds = tm.bake_visible(world_x as f32, world_y as f32, layer, cam_x, cam_y, cam_zoom, vp_w, vp_h);
554        b.sprite_commands.extend(cmds);
555    }
556}
557
558// --- Lighting ops ---
559
560/// Set the ambient light color (0-1 per channel).
561/// Accepts f64 (JavaScript's native number type), converts to f32 for GPU.
562#[deno_core::op2(fast)]
563pub fn op_set_ambient_light(state: &mut OpState, r: f64, g: f64, b: f64) {
564    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
565    bridge.borrow_mut().ambient_light = [r as f32, g as f32, b as f32];
566}
567
568/// Add a point light at world position (x,y) with radius, color, and intensity.
569/// Accepts f64 (JavaScript's native number type), converts to f32 for GPU.
570#[deno_core::op2(fast)]
571pub fn op_add_point_light(
572    state: &mut OpState,
573    x: f64,
574    y: f64,
575    radius: f64,
576    r: f64,
577    g: f64,
578    b: f64,
579    intensity: f64,
580) {
581    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
582    bridge.borrow_mut().point_lights.push(PointLight {
583        x: x as f32,
584        y: y as f32,
585        radius: radius as f32,
586        r: r as f32,
587        g: g as f32,
588        b: b as f32,
589        intensity: intensity as f32,
590    });
591}
592
593/// Clear all point lights for this frame.
594#[deno_core::op2(fast)]
595pub fn op_clear_lights(state: &mut OpState) {
596    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
597    bridge.borrow_mut().point_lights.clear();
598}
599
600// --- Audio ops ---
601
602/// Load a sound file. Returns a sound ID.
603#[deno_core::op2(fast)]
604pub fn op_load_sound(state: &mut OpState, #[string] path: &str) -> u32 {
605    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
606    let mut b = bridge.borrow_mut();
607
608    let resolved = if std::path::Path::new(path).is_absolute() {
609        path.to_string()
610    } else {
611        b.base_dir.join(path).to_string_lossy().to_string()
612    };
613
614    if let Some(&id) = b.sound_path_to_id.get(&resolved) {
615        return id;
616    }
617
618    let id = b.next_sound_id;
619    b.next_sound_id += 1;
620    b.sound_path_to_id.insert(resolved.clone(), id);
621    b.audio_commands.push(BridgeAudioCommand::LoadSound { id, path: resolved });
622    id
623}
624
625/// Stop all sounds.
626#[deno_core::op2(fast)]
627pub fn op_stop_all_sounds(state: &mut OpState) {
628    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
629    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopAll);
630}
631
632/// Set the master volume.
633/// Accepts f64 (JavaScript's native number type), converts to f32 for audio.
634#[deno_core::op2(fast)]
635pub fn op_set_master_volume(state: &mut OpState, volume: f64) {
636    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
637    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetMasterVolume { volume: volume as f32 });
638}
639
640// --- Font ops ---
641
642/// Create the built-in font texture. Returns a texture ID.
643#[deno_core::op2(fast)]
644pub fn op_create_font_texture(state: &mut OpState) -> u32 {
645    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
646    let mut b = bridge.borrow_mut();
647
648    let key = "__builtin_font__".to_string();
649    if let Some(&id) = b.texture_path_to_id.get(&key) {
650        return id;
651    }
652
653    let id = b.next_texture_id;
654    b.next_texture_id += 1;
655    b.texture_path_to_id.insert(key, id);
656    b.font_texture_queue.push(id);
657    id
658}
659
660// --- Viewport ops ---
661
662/// Get the current viewport size as [width, height].
663#[deno_core::op2]
664#[serde]
665pub fn op_get_viewport_size(state: &mut OpState) -> Vec<f64> {
666    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
667    let b = bridge.borrow();
668    vec![b.viewport_width as f64, b.viewport_height as f64]
669}
670
671/// Get the display scale factor (e.g. 2.0 on Retina).
672#[deno_core::op2(fast)]
673pub fn op_get_scale_factor(state: &mut OpState) -> f64 {
674    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
675    bridge.borrow().scale_factor as f64
676}
677
678/// Set the background/clear color (r, g, b in 0.0-1.0 range).
679#[deno_core::op2(fast)]
680pub fn op_set_background_color(state: &mut OpState, r: f64, g: f64, b: f64) {
681    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
682    let mut br = bridge.borrow_mut();
683    br.clear_color = [r as f32, g as f32, b as f32, 1.0];
684}
685
686// --- File I/O ops (save/load) ---
687
688/// Write a save file. Returns true on success.
689#[deno_core::op2(fast)]
690pub fn op_save_file(state: &mut OpState, #[string] key: &str, #[string] value: &str) -> bool {
691    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
692    let save_dir = bridge.borrow().save_dir.clone();
693
694    // Sanitize key: only allow alphanumeric, underscore, dash
695    if !key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
696        return false;
697    }
698
699    // Ensure save directory exists
700    if std::fs::create_dir_all(&save_dir).is_err() {
701        return false;
702    }
703
704    let path = save_dir.join(format!("{key}.json"));
705    std::fs::write(path, value).is_ok()
706}
707
708/// Load a save file. Returns the contents or empty string if not found.
709#[deno_core::op2]
710#[string]
711pub fn op_load_file(state: &mut OpState, #[string] key: &str) -> String {
712    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
713    let save_dir = bridge.borrow().save_dir.clone();
714
715    let path = save_dir.join(format!("{key}.json"));
716    std::fs::read_to_string(path).unwrap_or_default()
717}
718
719/// Delete a save file. Returns true on success.
720#[deno_core::op2(fast)]
721pub fn op_delete_file(state: &mut OpState, #[string] key: &str) -> bool {
722    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
723    let save_dir = bridge.borrow().save_dir.clone();
724
725    let path = save_dir.join(format!("{key}.json"));
726    std::fs::remove_file(path).is_ok()
727}
728
729/// List all save file keys (filenames without .json extension).
730#[deno_core::op2]
731#[serde]
732pub fn op_list_save_files(state: &mut OpState) -> Vec<String> {
733    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
734    let save_dir = bridge.borrow().save_dir.clone();
735
736    let mut keys = Vec::new();
737    if let Ok(entries) = std::fs::read_dir(&save_dir) {
738        for entry in entries.flatten() {
739            let path = entry.path();
740            if path.extension().map_or(false, |ext| ext == "json") {
741                if let Some(stem) = path.file_stem() {
742                    keys.push(stem.to_string_lossy().to_string());
743                }
744            }
745        }
746    }
747    keys.sort();
748    keys
749}
750
751// --- Shader ops ---
752
753/// Create a custom fragment shader from WGSL source. Returns a shader ID.
754#[deno_core::op2(fast)]
755pub fn op_create_shader(state: &mut OpState, #[string] name: &str, #[string] source: &str) -> u32 {
756    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
757    let mut b = bridge.borrow_mut();
758    let id = b.next_shader_id;
759    b.next_shader_id += 1;
760    b.shader_create_queue
761        .push((id, name.to_string(), source.to_string()));
762    id
763}
764
765/// Set a vec4 parameter slot on a custom shader. Index 0-15.
766#[deno_core::op2(fast)]
767pub fn op_set_shader_param(
768    state: &mut OpState,
769    shader_id: u32,
770    index: u32,
771    x: f64,
772    y: f64,
773    z: f64,
774    w: f64,
775) {
776    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
777    bridge.borrow_mut().shader_param_queue.push((
778        shader_id,
779        index,
780        [x as f32, y as f32, z as f32, w as f32],
781    ));
782}
783
784// --- Post-process effect ops ---
785
786/// Add a post-process effect. Returns an effect ID.
787#[deno_core::op2(fast)]
788pub fn op_add_effect(state: &mut OpState, #[string] effect_type: &str) -> u32 {
789    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
790    let mut b = bridge.borrow_mut();
791    let id = b.next_effect_id;
792    b.next_effect_id += 1;
793    b.effect_create_queue
794        .push((id, effect_type.to_string()));
795    id
796}
797
798/// Set a vec4 parameter slot on a post-process effect. Index 0-3.
799#[deno_core::op2(fast)]
800pub fn op_set_effect_param(
801    state: &mut OpState,
802    effect_id: u32,
803    index: u32,
804    x: f64,
805    y: f64,
806    z: f64,
807    w: f64,
808) {
809    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
810    bridge.borrow_mut().effect_param_queue.push((
811        effect_id,
812        index,
813        [x as f32, y as f32, z as f32, w as f32],
814    ));
815}
816
817/// Remove a single post-process effect by ID.
818#[deno_core::op2(fast)]
819pub fn op_remove_effect(state: &mut OpState, effect_id: u32) {
820    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
821    bridge.borrow_mut().effect_remove_queue.push(effect_id);
822}
823
824/// Remove all post-process effects.
825#[deno_core::op2(fast)]
826pub fn op_clear_effects(state: &mut OpState) {
827    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
828    bridge.borrow_mut().effect_clear = true;
829}
830
831// --- Camera bounds ops ---
832
833/// Set camera bounds (world-space limits).
834#[deno_core::op2(fast)]
835pub fn op_set_camera_bounds(state: &mut OpState, min_x: f64, min_y: f64, max_x: f64, max_y: f64) {
836    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
837    bridge.borrow_mut().camera_bounds = Some(CameraBounds {
838        min_x: min_x as f32,
839        min_y: min_y as f32,
840        max_x: max_x as f32,
841        max_y: max_y as f32,
842    });
843}
844
845/// Clear camera bounds (no limits).
846#[deno_core::op2(fast)]
847pub fn op_clear_camera_bounds(state: &mut OpState) {
848    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
849    bridge.borrow_mut().camera_bounds = None;
850}
851
852/// Get camera bounds as [minX, minY, maxX, maxY] or empty if none.
853#[deno_core::op2]
854#[serde]
855pub fn op_get_camera_bounds(state: &mut OpState) -> Vec<f64> {
856    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
857    let b = bridge.borrow();
858    match b.camera_bounds {
859        Some(bounds) => vec![
860            bounds.min_x as f64,
861            bounds.min_y as f64,
862            bounds.max_x as f64,
863            bounds.max_y as f64,
864        ],
865        None => vec![],
866    }
867}
868
869// --- Global Illumination ops ---
870
871/// Enable radiance cascades global illumination.
872#[deno_core::op2(fast)]
873pub fn op_enable_gi(state: &mut OpState) {
874    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
875    bridge.borrow_mut().gi_enabled = true;
876}
877
878/// Disable radiance cascades global illumination.
879#[deno_core::op2(fast)]
880pub fn op_disable_gi(state: &mut OpState) {
881    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
882    bridge.borrow_mut().gi_enabled = false;
883}
884
885/// Set the GI intensity multiplier.
886#[deno_core::op2(fast)]
887pub fn op_set_gi_intensity(state: &mut OpState, intensity: f64) {
888    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
889    bridge.borrow_mut().gi_intensity = intensity as f32;
890}
891
892/// Set GI quality parameters (probe spacing, interval, cascade count).
893/// Pass 0 for any parameter to keep the current/default value.
894#[deno_core::op2(fast)]
895pub fn op_set_gi_quality(state: &mut OpState, probe_spacing: f64, interval: f64, cascade_count: f64) {
896    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
897    let mut b = bridge.borrow_mut();
898    if probe_spacing > 0.0 {
899        b.gi_probe_spacing = Some(probe_spacing as f32);
900    }
901    if interval > 0.0 {
902        b.gi_interval = Some(interval as f32);
903    }
904    if cascade_count > 0.0 {
905        b.gi_cascade_count = Some(cascade_count as u32);
906    }
907}
908
909/// Add an emissive surface (light source) for GI.
910#[deno_core::op2(fast)]
911pub fn op_add_emissive(
912    state: &mut OpState,
913    x: f64,
914    y: f64,
915    w: f64,
916    h: f64,
917    r: f64,
918    g: f64,
919    b: f64,
920    intensity: f64,
921) {
922    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
923    bridge.borrow_mut().emissives.push([
924        x as f32,
925        y as f32,
926        w as f32,
927        h as f32,
928        r as f32,
929        g as f32,
930        b as f32,
931        intensity as f32,
932    ]);
933}
934
935/// Clear all emissive surfaces.
936#[deno_core::op2(fast)]
937pub fn op_clear_emissives(state: &mut OpState) {
938    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
939    bridge.borrow_mut().emissives.clear();
940}
941
942/// Add a rectangular occluder that blocks light.
943#[deno_core::op2(fast)]
944pub fn op_add_occluder(state: &mut OpState, x: f64, y: f64, w: f64, h: f64) {
945    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
946    bridge.borrow_mut().occluders.push([x as f32, y as f32, w as f32, h as f32]);
947}
948
949/// Clear all occluders.
950#[deno_core::op2(fast)]
951pub fn op_clear_occluders(state: &mut OpState) {
952    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
953    bridge.borrow_mut().occluders.clear();
954}
955
956/// Add a directional light (infinite distance, parallel rays).
957#[deno_core::op2(fast)]
958pub fn op_add_directional_light(
959    state: &mut OpState,
960    angle: f64,
961    r: f64,
962    g: f64,
963    b: f64,
964    intensity: f64,
965) {
966    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
967    bridge.borrow_mut().directional_lights.push([
968        angle as f32,
969        r as f32,
970        g as f32,
971        b as f32,
972        intensity as f32,
973    ]);
974}
975
976/// Add a spot light with position, direction, and spread.
977#[deno_core::op2(fast)]
978pub fn op_add_spot_light(
979    state: &mut OpState,
980    x: f64,
981    y: f64,
982    angle: f64,
983    spread: f64,
984    range: f64,
985    r: f64,
986    g: f64,
987    b: f64,
988    intensity: f64,
989) {
990    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
991    bridge.borrow_mut().spot_lights.push([
992        x as f32,
993        y as f32,
994        angle as f32,
995        spread as f32,
996        range as f32,
997        r as f32,
998        g as f32,
999        b as f32,
1000        intensity as f32,
1001    ]);
1002}
1003
1004// --- Phase 20: New audio ops ---
1005
1006/// Play a sound with extended parameters (pan, pitch, effects, bus).
1007/// Accepts f64 for all numeric params (deno_core convention), converts to f32/u32/u64 internally.
1008#[deno_core::op2(fast)]
1009pub fn op_play_sound_ex(
1010    state: &mut OpState,
1011    sound_id: u32,
1012    instance_id: f64,
1013    volume: f64,
1014    looping: bool,
1015    bus: u32,
1016    pan: f64,
1017    pitch: f64,
1018    low_pass_freq: u32,
1019    reverb_mix: f64,
1020    reverb_delay_ms: u32,
1021) {
1022    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1023    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::PlaySoundEx {
1024        sound_id,
1025        instance_id: instance_id as u64,
1026        volume: volume as f32,
1027        looping,
1028        bus,
1029        pan: pan as f32,
1030        pitch: pitch as f32,
1031        low_pass_freq,
1032        reverb_mix: reverb_mix as f32,
1033        reverb_delay_ms,
1034    });
1035}
1036
1037/// Play a sound with spatial audio (3D positioning).
1038/// Accepts f64 for all numeric params (deno_core convention), converts to f32/u64 internally.
1039#[deno_core::op2(fast)]
1040pub fn op_play_sound_spatial(
1041    state: &mut OpState,
1042    sound_id: u32,
1043    instance_id: f64,
1044    volume: f64,
1045    looping: bool,
1046    bus: u32,
1047    pitch: f64,
1048    source_x: f64,
1049    source_y: f64,
1050    listener_x: f64,
1051    listener_y: f64,
1052) {
1053    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1054    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::PlaySoundSpatial {
1055        sound_id,
1056        instance_id: instance_id as u64,
1057        volume: volume as f32,
1058        looping,
1059        bus,
1060        pitch: pitch as f32,
1061        source_x: source_x as f32,
1062        source_y: source_y as f32,
1063        listener_x: listener_x as f32,
1064        listener_y: listener_y as f32,
1065    });
1066}
1067
1068/// Stop a specific audio instance.
1069/// Accepts f64 (deno_core convention), converts to u64 internally.
1070#[deno_core::op2(fast)]
1071pub fn op_stop_instance(state: &mut OpState, instance_id: f64) {
1072    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1073    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopInstance {
1074        instance_id: instance_id as u64,
1075    });
1076}
1077
1078/// Set the volume of a specific audio instance.
1079/// Accepts f64 (deno_core convention), converts to u64/f32 internally.
1080#[deno_core::op2(fast)]
1081pub fn op_set_instance_volume(state: &mut OpState, instance_id: f64, volume: f64) {
1082    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1083    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetInstanceVolume {
1084        instance_id: instance_id as u64,
1085        volume: volume as f32,
1086    });
1087}
1088
1089/// Set the pitch of a specific audio instance.
1090/// Accepts f64 (deno_core convention), converts to u64/f32 internally.
1091#[deno_core::op2(fast)]
1092pub fn op_set_instance_pitch(state: &mut OpState, instance_id: f64, pitch: f64) {
1093    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1094    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetInstancePitch {
1095        instance_id: instance_id as u64,
1096        pitch: pitch as f32,
1097    });
1098}
1099
1100/// Update positions for multiple spatial audio instances in a batch.
1101/// Uses JSON string for variable-length data (simplest approach with deno_core 0.385.0).
1102/// Format: {"instanceIds": [id1, id2, ...], "sourceXs": [x1, x2, ...], "sourceYs": [y1, y2, ...], "listenerX": x, "listenerY": y}
1103#[deno_core::op2(fast)]
1104pub fn op_update_spatial_positions(state: &mut OpState, #[string] data_json: &str, listener_x: f64, listener_y: f64) {
1105    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1106
1107    // Parse JSON (simple ad-hoc parsing for array data)
1108    // In production, we'd use serde_json, but for minimal dependencies, parse manually
1109    let mut updates = Vec::new();
1110
1111    // Extract arrays from JSON manually (hacky but works for simple structure)
1112    if let Some(ids_start) = data_json.find("\"instanceIds\":[") {
1113        if let Some(xs_start) = data_json.find("\"sourceXs\":[") {
1114            if let Some(ys_start) = data_json.find("\"sourceYs\":[") {
1115                let ids_str = &data_json[ids_start + 15..];
1116                let xs_str = &data_json[xs_start + 12..];
1117                let ys_str = &data_json[ys_start + 12..];
1118
1119                let ids_end = ids_str.find(']').unwrap_or(0);
1120                let xs_end = xs_str.find(']').unwrap_or(0);
1121                let ys_end = ys_str.find(']').unwrap_or(0);
1122
1123                let ids: Vec<u64> = ids_str[..ids_end]
1124                    .split(',')
1125                    .filter_map(|s| s.trim().parse().ok())
1126                    .collect();
1127                let xs: Vec<f32> = xs_str[..xs_end]
1128                    .split(',')
1129                    .filter_map(|s| s.trim().parse().ok())
1130                    .collect();
1131                let ys: Vec<f32> = ys_str[..ys_end]
1132                    .split(',')
1133                    .filter_map(|s| s.trim().parse().ok())
1134                    .collect();
1135
1136                for i in 0..ids.len().min(xs.len()).min(ys.len()) {
1137                    updates.push((ids[i], xs[i], ys[i]));
1138                }
1139            }
1140        }
1141    }
1142
1143    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::UpdateSpatialPositions {
1144        updates,
1145        listener_x: listener_x as f32,
1146        listener_y: listener_y as f32,
1147    });
1148}
1149
1150/// Set the volume for an audio bus (affects all sounds on that bus).
1151/// Accepts f64 (deno_core convention), converts to f32 internally.
1152#[deno_core::op2(fast)]
1153pub fn op_set_bus_volume(state: &mut OpState, bus: u32, volume: f64) {
1154    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1155    bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetBusVolume {
1156        bus,
1157        volume: volume as f32,
1158    });
1159}
1160
1161// --- MSDF text ops ---
1162
1163/// Create the built-in MSDF font (from CP437 bitmap data converted to SDF).
1164/// Returns a JSON string: { "fontId": N, "textureId": M, "shaderId": S }
1165#[deno_core::op2]
1166#[string]
1167pub fn op_create_msdf_builtin_font(state: &mut OpState) -> String {
1168    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1169    let mut b = bridge.borrow_mut();
1170
1171    // Check if already created
1172    let key = "__msdf_builtin__".to_string();
1173    if let Some(&tex_id) = b.texture_path_to_id.get(&key) {
1174        // Already created — find the font ID
1175        // The font was registered with the texture_id as the lookup key
1176        let font_id_key = format!("__msdf_font_{tex_id}__");
1177        if let Some(&font_id) = b.texture_path_to_id.get(&font_id_key) {
1178            let pool = &b.msdf_shader_pool;
1179            let shader_id = pool.first().copied().unwrap_or(0);
1180            let pool_json: Vec<String> = pool.iter().map(|id| id.to_string()).collect();
1181            return format!(
1182                "{{\"fontId\":{},\"textureId\":{},\"shaderId\":{},\"shaderPool\":[{}]}}",
1183                font_id, tex_id, shader_id, pool_json.join(",")
1184            );
1185        }
1186    }
1187
1188    // Assign texture ID
1189    let tex_id = b.next_texture_id;
1190    b.next_texture_id += 1;
1191    b.texture_path_to_id.insert(key, tex_id);
1192
1193    // Generate MSDF atlas data and register font
1194    let (_pixels, _width, _height, mut font) =
1195        crate::renderer::msdf::generate_builtin_msdf_font();
1196    font.texture_id = tex_id;
1197
1198    // Register font in the store
1199    let font_id = b.msdf_fonts.register(font);
1200    b.texture_path_to_id
1201        .insert(format!("__msdf_font_{tex_id}__"), font_id);
1202
1203    // Queue the texture for GPU upload.
1204    // dev.rs will call generate_builtin_msdf_font() again and upload pixels.
1205    b.msdf_builtin_queue.push((font_id, tex_id));
1206
1207    // Ensure MSDF shader pool exists
1208    let pool = ensure_msdf_shader_pool(&mut b);
1209    let shader_id = pool.first().copied().unwrap_or(0);
1210    let pool_json: Vec<String> = pool.iter().map(|id| id.to_string()).collect();
1211
1212    format!(
1213        "{{\"fontId\":{},\"textureId\":{},\"shaderId\":{},\"shaderPool\":[{}]}}",
1214        font_id, tex_id, shader_id, pool_json.join(",")
1215    )
1216}
1217
1218/// Get MSDF glyph metrics for a text string. Returns JSON array of glyph info.
1219/// Each glyph: { "uv": [x, y, w, h], "advance": N, "width": N, "height": N, "offsetX": N, "offsetY": N }
1220#[deno_core::op2]
1221#[string]
1222pub fn op_get_msdf_glyphs(
1223    state: &mut OpState,
1224    font_id: u32,
1225    #[string] text: &str,
1226) -> String {
1227    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1228    let b = bridge.borrow();
1229
1230    let font = match b.msdf_fonts.get(font_id) {
1231        Some(f) => f,
1232        None => return "[]".to_string(),
1233    };
1234
1235    let mut entries = Vec::new();
1236    for ch in text.chars() {
1237        if let Some(glyph) = font.get_glyph(ch) {
1238            entries.push(format!(
1239                "{{\"char\":{},\"uv\":[{},{},{},{}],\"advance\":{},\"width\":{},\"height\":{},\"offsetX\":{},\"offsetY\":{}}}",
1240                ch as u32,
1241                glyph.uv_x, glyph.uv_y, glyph.uv_w, glyph.uv_h,
1242                glyph.advance, glyph.width, glyph.height,
1243                glyph.offset_x, glyph.offset_y,
1244            ));
1245        }
1246    }
1247
1248    format!("[{}]", entries.join(","))
1249}
1250
1251/// Get MSDF font info. Returns JSON: { "fontSize": N, "lineHeight": N, "distanceRange": N, "textureId": N }
1252#[deno_core::op2]
1253#[string]
1254pub fn op_get_msdf_font_info(state: &mut OpState, font_id: u32) -> String {
1255    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1256    let b = bridge.borrow();
1257
1258    match b.msdf_fonts.get(font_id) {
1259        Some(font) => format!(
1260            "{{\"fontSize\":{},\"lineHeight\":{},\"distanceRange\":{},\"textureId\":{}}}",
1261            font.font_size, font.line_height, font.distance_range, font.texture_id,
1262        ),
1263        None => "null".to_string(),
1264    }
1265}
1266
1267/// Load an MSDF font from an atlas image path + metrics JSON (string or file path).
1268/// Returns a JSON string: { "fontId": N, "textureId": M, "shaderId": S }
1269#[deno_core::op2]
1270#[string]
1271pub fn op_load_msdf_font(
1272    state: &mut OpState,
1273    #[string] atlas_path: &str,
1274    #[string] metrics_json_or_path: &str,
1275) -> String {
1276    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1277    let mut b = bridge.borrow_mut();
1278
1279    // Resolve atlas path
1280    let resolved = if std::path::Path::new(atlas_path).is_absolute() {
1281        atlas_path.to_string()
1282    } else {
1283        b.base_dir.join(atlas_path).to_string_lossy().to_string()
1284    };
1285
1286    // Load atlas texture (reuse existing if already loaded)
1287    let tex_id = if let Some(&id) = b.texture_path_to_id.get(&resolved) {
1288        id
1289    } else {
1290        let id = b.next_texture_id;
1291        b.next_texture_id += 1;
1292        b.texture_path_to_id.insert(resolved.clone(), id);
1293        b.msdf_texture_load_queue.push((resolved, id));
1294        id
1295    };
1296
1297    // Determine if metrics_json_or_path is a file path or raw JSON.
1298    // If it ends with .json and doesn't start with '{', treat as file path.
1299    let metrics_json: String = if metrics_json_or_path.trim_start().starts_with('{') {
1300        metrics_json_or_path.to_string()
1301    } else {
1302        // Treat as file path
1303        let json_path = if std::path::Path::new(metrics_json_or_path).is_absolute() {
1304            metrics_json_or_path.to_string()
1305        } else {
1306            b.base_dir
1307                .join(metrics_json_or_path)
1308                .to_string_lossy()
1309                .to_string()
1310        };
1311        match std::fs::read_to_string(&json_path) {
1312            Ok(content) => content,
1313            Err(e) => {
1314                return format!("{{\"error\":\"Failed to read metrics file {}: {}\"}}", json_path, e);
1315            }
1316        }
1317    };
1318
1319    // Parse metrics
1320    let font = match crate::renderer::msdf::parse_msdf_metrics(&metrics_json, tex_id) {
1321        Ok(f) => f,
1322        Err(e) => {
1323            return format!("{{\"error\":\"{}\"}}", e);
1324        }
1325    };
1326
1327    let font_id = b.msdf_fonts.register(font);
1328    let pool = ensure_msdf_shader_pool(&mut b);
1329    let shader_id = pool.first().copied().unwrap_or(0);
1330    let pool_json: Vec<String> = pool.iter().map(|id| id.to_string()).collect();
1331
1332    format!(
1333        "{{\"fontId\":{},\"textureId\":{},\"shaderId\":{},\"shaderPool\":[{}]}}",
1334        font_id, tex_id, shader_id, pool_json.join(",")
1335    )
1336}
1337
1338/// Pool size for MSDF shaders (same WGSL, different uniform buffers).
1339const MSDF_SHADER_POOL_SIZE: usize = 8;
1340
1341/// Ensure the MSDF shader pool exists in the bridge state. Returns the pool of shader IDs.
1342fn ensure_msdf_shader_pool(b: &mut RenderBridgeState) -> Vec<u32> {
1343    if !b.msdf_shader_pool.is_empty() {
1344        return b.msdf_shader_pool.clone();
1345    }
1346
1347    let source = crate::renderer::msdf::MSDF_FRAGMENT_SOURCE.to_string();
1348    let mut pool = Vec::with_capacity(MSDF_SHADER_POOL_SIZE);
1349
1350    for _ in 0..MSDF_SHADER_POOL_SIZE {
1351        let id = b.next_shader_id;
1352        b.next_shader_id += 1;
1353        b.msdf_shader_queue.push((id, source.clone()));
1354        pool.push(id);
1355    }
1356
1357    b.msdf_shader_pool = pool.clone();
1358    pool
1359}
1360
1361// --- Gamepad ops ---
1362
1363/// Get the number of connected gamepads.
1364#[deno_core::op2(fast)]
1365pub fn op_get_gamepad_count(state: &mut OpState) -> u32 {
1366    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1367    bridge.borrow().gamepad_count
1368}
1369
1370/// Get the name of the primary gamepad.
1371#[deno_core::op2]
1372#[string]
1373pub fn op_get_gamepad_name(state: &mut OpState) -> String {
1374    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1375    bridge.borrow().gamepad_name.clone()
1376}
1377
1378/// Check if a gamepad button is currently held down.
1379/// Button name is the canonical string (e.g. "A", "B", "LeftBumper", "DPadUp").
1380#[deno_core::op2(fast)]
1381pub fn op_is_gamepad_button_down(state: &mut OpState, #[string] button: &str) -> bool {
1382    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1383    bridge.borrow().gamepad_buttons_down.contains(button)
1384}
1385
1386/// Check if a gamepad button was pressed this frame.
1387#[deno_core::op2(fast)]
1388pub fn op_is_gamepad_button_pressed(state: &mut OpState, #[string] button: &str) -> bool {
1389    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1390    bridge.borrow().gamepad_buttons_pressed.contains(button)
1391}
1392
1393/// Get a gamepad axis value (-1.0 to 1.0 for sticks, 0.0 to 1.0 for triggers).
1394/// Axis name: "LeftStickX", "LeftStickY", "RightStickX", "RightStickY", "LeftTrigger", "RightTrigger".
1395#[deno_core::op2(fast)]
1396pub fn op_get_gamepad_axis(state: &mut OpState, #[string] axis: &str) -> f64 {
1397    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1398    bridge.borrow().gamepad_axes.get(axis).copied().unwrap_or(0.0) as f64
1399}
1400
1401// --- Touch ops ---
1402
1403/// Get the number of active touch points.
1404#[deno_core::op2(fast)]
1405pub fn op_get_touch_count(state: &mut OpState) -> u32 {
1406    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1407    bridge.borrow().touch_count
1408}
1409
1410/// Get a touch point position by index. Returns [x, y] or empty array if not found.
1411#[deno_core::op2]
1412#[serde]
1413pub fn op_get_touch_position(state: &mut OpState, index: u32) -> Vec<f64> {
1414    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1415    let b = bridge.borrow();
1416    if let Some(&(_, x, y)) = b.touch_points.get(index as usize) {
1417        vec![x as f64, y as f64]
1418    } else {
1419        vec![]
1420    }
1421}
1422
1423/// Check if any touch is currently active.
1424#[deno_core::op2(fast)]
1425pub fn op_is_touch_active(state: &mut OpState) -> bool {
1426    let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
1427    bridge.borrow().touch_count > 0
1428}
1429
1430deno_core::extension!(
1431    render_ext,
1432    ops = [
1433        op_clear_sprites,
1434        op_submit_sprite_batch,
1435        op_set_camera,
1436        op_get_camera,
1437        op_load_texture,
1438        op_load_texture_linear,
1439        op_upload_rgba_texture,
1440        op_is_key_down,
1441        op_is_key_pressed,
1442        op_get_mouse_position,
1443        op_is_mouse_button_down,
1444        op_is_mouse_button_pressed,
1445        op_get_delta_time,
1446        op_create_solid_texture,
1447        op_create_tilemap,
1448        op_set_tile,
1449        op_get_tile,
1450        op_draw_tilemap,
1451        op_set_ambient_light,
1452        op_add_point_light,
1453        op_clear_lights,
1454        op_load_sound,
1455        op_stop_all_sounds,
1456        op_set_master_volume,
1457        op_play_sound_ex,
1458        op_play_sound_spatial,
1459        op_stop_instance,
1460        op_set_instance_volume,
1461        op_set_instance_pitch,
1462        op_update_spatial_positions,
1463        op_set_bus_volume,
1464        op_create_font_texture,
1465        op_get_viewport_size,
1466        op_get_scale_factor,
1467        op_set_background_color,
1468        op_save_file,
1469        op_load_file,
1470        op_delete_file,
1471        op_list_save_files,
1472        op_create_shader,
1473        op_set_shader_param,
1474        op_add_effect,
1475        op_set_effect_param,
1476        op_remove_effect,
1477        op_clear_effects,
1478        op_set_camera_bounds,
1479        op_clear_camera_bounds,
1480        op_get_camera_bounds,
1481        op_enable_gi,
1482        op_disable_gi,
1483        op_set_gi_intensity,
1484        op_set_gi_quality,
1485        op_add_emissive,
1486        op_clear_emissives,
1487        op_add_occluder,
1488        op_clear_occluders,
1489        op_add_directional_light,
1490        op_add_spot_light,
1491        op_create_msdf_builtin_font,
1492        op_get_msdf_glyphs,
1493        op_get_msdf_font_info,
1494        op_load_msdf_font,
1495        op_get_gamepad_count,
1496        op_get_gamepad_name,
1497        op_is_gamepad_button_down,
1498        op_is_gamepad_button_pressed,
1499        op_get_gamepad_axis,
1500        op_get_touch_count,
1501        op_get_touch_position,
1502        op_is_touch_active,
1503    ],
1504);