Skip to main content

runmat_runtime/builtins/plotting/core/
web.rs

1#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2use super::common::ERR_PLOTTING_UNAVAILABLE;
3
4use crate::{build_runtime_error, BuiltinResult, RuntimeError};
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub struct PlotSurfaceCameraState {
10    pub active_axes: usize,
11    pub axes: Vec<PlotCameraState>,
12}
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct PlotCameraState {
17    pub position: [f32; 3],
18    pub target: [f32; 3],
19    pub up: [f32; 3],
20    pub zoom: f32,
21    pub aspect_ratio: f32,
22    pub projection: PlotCameraProjection,
23}
24
25#[derive(Clone, Debug, Serialize, Deserialize)]
26#[serde(tag = "kind", rename_all = "camelCase")]
27pub enum PlotCameraProjection {
28    Perspective {
29        fov: f32,
30        near: f32,
31        far: f32,
32    },
33    Orthographic {
34        left: f32,
35        right: f32,
36        bottom: f32,
37        top: f32,
38        near: f32,
39        far: f32,
40    },
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(tag = "kind", rename_all = "camelCase")]
45pub enum PlotSurfaceHostAction {
46    CreateFeaStudy,
47}
48
49fn web_error(message: impl Into<String>) -> RuntimeError {
50    build_runtime_error(message)
51        .with_identifier("RunMat:plot:WebError")
52        .build()
53}
54
55#[allow(dead_code)]
56fn web_error_with_source(
57    message: impl Into<String>,
58    source: impl std::error::Error + Send + Sync + 'static,
59) -> RuntimeError {
60    build_runtime_error(message)
61        .with_identifier("RunMat:plot:WebError")
62        .with_source(source)
63        .build()
64}
65
66#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
67pub(crate) mod wasm {
68    use super::*;
69    use crate::builtins::plotting::state::{clone_figure, current_figure_revision, FigureHandle};
70    use log::debug;
71    use runmat_plot::core::PlotEvent;
72    use runmat_plot::styling::PlotThemeConfig;
73    use runmat_plot::web::WebRenderer;
74    use runmat_plot::{GeometryScenePickIndex, GeometryScenePickResult, GeometryScenePresentation};
75    use runmat_thread_local::runmat_thread_local;
76    use std::cell::RefCell;
77    use std::collections::HashMap;
78
79    runmat_thread_local! {
80        static SURFACES: RefCell<HashMap<u32, SurfaceEntry>> = RefCell::new(HashMap::new());
81        static ACTIVE_THEME: RefCell<PlotThemeConfig> = RefCell::new(PlotThemeConfig::default());
82    }
83
84    struct SurfaceEntry {
85        renderer: WebRenderer,
86        bound_target: Option<BoundSurfaceTarget>,
87        last_revision: Option<u64>,
88        geometry_presentation: GeometryScenePresentation,
89        geometry_pick_index: Option<CachedGeometryPickIndex>,
90    }
91
92    struct CachedGeometryPickIndex {
93        handle: u32,
94        index: GeometryScenePickIndex,
95    }
96
97    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
98    enum BoundSurfaceTarget {
99        Figure(u32),
100        GeometryScene(u32),
101    }
102
103    pub(super) fn install_surface_impl(
104        surface_id: u32,
105        mut renderer: WebRenderer,
106    ) -> BuiltinResult<()> {
107        ACTIVE_THEME.with(|theme| {
108            renderer.set_theme_config(theme.borrow().clone());
109        });
110        SURFACES.with(|slot| {
111            slot.borrow_mut().insert(
112                surface_id,
113                SurfaceEntry {
114                    renderer,
115                    bound_target: None,
116                    last_revision: None,
117                    geometry_presentation: GeometryScenePresentation::default(),
118                    geometry_pick_index: None,
119                },
120            );
121        });
122        SURFACES.with(|slot| {
123            let keys: Vec<u32> = slot.borrow().keys().copied().collect();
124            debug!(
125                "plot-web: installed surface surface_id={surface_id} (active_surfaces={keys:?})"
126            );
127        });
128        Ok(())
129    }
130
131    pub(super) fn detach_surface_impl(surface_id: u32) {
132        SURFACES.with(|slot| {
133            slot.borrow_mut().remove(&surface_id);
134        });
135        SURFACES.with(|slot| {
136            let keys: Vec<u32> = slot.borrow().keys().copied().collect();
137            debug!("plot-web: detached surface surface_id={surface_id} (active_surfaces={keys:?})");
138        });
139    }
140
141    pub(super) fn clear_closed_figure_surfaces_impl(handle: u32) -> BuiltinResult<()> {
142        SURFACES.with(|slot| {
143            let mut map = slot.borrow_mut();
144            for entry in map.values_mut() {
145                if entry.bound_target == Some(BoundSurfaceTarget::Figure(handle)) {
146                    entry.bound_target = None;
147                    entry.last_revision = None;
148                    entry.geometry_pick_index = None;
149                    entry
150                        .renderer
151                        .clear_surface()
152                        .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
153                }
154            }
155            Ok(())
156        })
157    }
158
159    pub fn web_renderer_ready() -> bool {
160        SURFACES.with(|slot| !slot.borrow().is_empty())
161    }
162
163    pub(super) fn resize_surface_impl(
164        surface_id: u32,
165        width: u32,
166        height: u32,
167        pixels_per_point: f32,
168    ) -> BuiltinResult<()> {
169        SURFACES.with(|slot| {
170            let mut map = slot.borrow_mut();
171            let entry = map.get_mut(&surface_id).ok_or_else(|| {
172                web_error(format!(
173                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
174                ))
175            })?;
176            entry.renderer.set_pixels_per_point(pixels_per_point);
177            entry
178                .renderer
179                .resize_surface(width, height)
180                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
181            Ok(())
182        })
183    }
184
185    pub(super) fn bind_surface_to_figure_impl(surface_id: u32, handle: u32) -> BuiltinResult<()> {
186        SURFACES.with(|slot| {
187            let mut map = slot.borrow_mut();
188            let entry = map.get_mut(&surface_id).ok_or_else(|| {
189                web_error(format!(
190                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
191                ))
192            })?;
193            entry.bound_target = Some(BoundSurfaceTarget::Figure(handle));
194            // Force a re-prime on next present.
195            entry.last_revision = None;
196            Ok(())
197        })
198    }
199
200    pub(super) fn set_theme_config_impl(theme: PlotThemeConfig) -> BuiltinResult<()> {
201        debug!(
202            "plot-web: runtime set_theme_config_impl variant={:?} custom_colors={}",
203            theme.variant,
204            theme.custom_colors.is_some()
205        );
206        ACTIVE_THEME.with(|slot| {
207            *slot.borrow_mut() = theme.clone();
208        });
209        SURFACES.with(|slot| {
210            let mut map = slot.borrow_mut();
211            debug!("plot-web: applying theme to {} surfaces", map.len());
212            for entry in map.values_mut() {
213                entry.renderer.set_theme_config(theme.clone());
214                match entry.bound_target {
215                    Some(BoundSurfaceTarget::Figure(handle)) => {
216                        if let Some(figure) = clone_figure(FigureHandle::from(handle)) {
217                            entry
218                                .renderer
219                                .render_figure(figure)
220                                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
221                        }
222                    }
223                    Some(BoundSurfaceTarget::GeometryScene(_)) => {
224                        entry
225                            .renderer
226                            .render_current_scene()
227                            .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
228                    }
229                    None => {}
230                }
231            }
232            Ok(())
233        })
234    }
235
236    pub(super) fn current_theme_config_impl() -> PlotThemeConfig {
237        ACTIVE_THEME.with(|slot| slot.borrow().clone())
238    }
239
240    pub(super) fn present_figure_on_surface_impl(
241        surface_id: u32,
242        handle: u32,
243    ) -> BuiltinResult<()> {
244        // "Better" path: only invalidate cached render data if the handle actually changes.
245        SURFACES.with(|slot| {
246            let mut map = slot.borrow_mut();
247            let entry = map.get_mut(&surface_id).ok_or_else(|| {
248                web_error(format!(
249                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
250                ))
251            })?;
252            if entry.bound_target != Some(BoundSurfaceTarget::Figure(handle)) {
253                entry.bound_target = Some(BoundSurfaceTarget::Figure(handle));
254                entry.last_revision = None;
255                entry.geometry_pick_index = None;
256            }
257            Ok::<(), RuntimeError>(())
258        })?;
259        present_surface_impl(surface_id)
260    }
261
262    pub(super) fn present_geometry_scene_on_surface_impl(
263        surface_id: u32,
264        handle: u32,
265        scene: runmat_plot::GeometryScene,
266    ) -> BuiltinResult<()> {
267        SURFACES.with(|slot| {
268            let mut map = slot.borrow_mut();
269            let entry = map.get_mut(&surface_id).ok_or_else(|| {
270                web_error(format!(
271                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
272                ))
273            })?;
274            if entry.bound_target != Some(BoundSurfaceTarget::GeometryScene(handle))
275                || entry.last_revision != Some(scene.revision)
276            {
277                entry.geometry_pick_index = None;
278            }
279            entry.bound_target = Some(BoundSurfaceTarget::GeometryScene(handle));
280            entry.last_revision = Some(scene.revision);
281            let presentation = entry.geometry_presentation.clone();
282            entry
283                .renderer
284                .render_geometry_scene_with_presentation(scene, Some(presentation))
285                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
286            Ok(())
287        })
288    }
289
290    pub(super) fn set_geometry_scene_presentation_impl(
291        surface_id: u32,
292        presentation: GeometryScenePresentation,
293    ) -> BuiltinResult<()> {
294        SURFACES.with(|slot| {
295            let mut map = slot.borrow_mut();
296            let entry = map.get_mut(&surface_id).ok_or_else(|| {
297                web_error(format!(
298                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
299                ))
300            })?;
301            let Some(BoundSurfaceTarget::GeometryScene(_)) = entry.bound_target else {
302                return Err(web_error(
303                    "Plotting surface is not bound to a geometry scene.",
304                ));
305            };
306            log::info!(
307                "geometry_scene.presentation_runtime surface_id={} selected_region_id={} selected_region_count={} display_mode={:?}",
308                surface_id,
309                presentation.selected_region_id.as_deref().unwrap_or("none"),
310                presentation.selected_region_ids.len(),
311                presentation.display_mode,
312            );
313            entry.geometry_presentation = presentation.clone();
314            entry
315                .renderer
316                .set_geometry_scene_presentation(presentation)
317                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
318            Ok(())
319        })
320    }
321
322    pub(super) fn pick_geometry_scene_region_impl(
323        surface_id: u32,
324        x: f32,
325        y: f32,
326    ) -> BuiltinResult<Option<GeometryScenePickResult>> {
327        SURFACES.with(|slot| {
328            let mut map = slot.borrow_mut();
329            let entry = map.get_mut(&surface_id).ok_or_else(|| {
330                web_error(format!(
331                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
332                ))
333            })?;
334            let Some(BoundSurfaceTarget::GeometryScene(handle)) = entry.bound_target else {
335                return Ok(None);
336            };
337            let rebuild = entry
338                .geometry_pick_index
339                .as_ref()
340                .map(|cached| cached.handle != handle)
341                .unwrap_or(true);
342            if rebuild {
343                let scene =
344                    crate::builtins::plotting::clone_geometry_scene(handle).ok_or_else(|| {
345                        web_error(format!("geometry scene handle {handle} does not exist"))
346                    })?;
347                entry.geometry_pick_index = Some(CachedGeometryPickIndex {
348                    handle,
349                    index: GeometryScenePickIndex::build(&scene),
350                });
351            }
352            Ok(entry.geometry_pick_index.as_ref().and_then(|cached| {
353                entry
354                    .renderer
355                    .pick_geometry_scene_region(&cached.index, [x, y])
356            }))
357        })
358    }
359
360    pub(super) fn present_surface_impl(surface_id: u32) -> BuiltinResult<()> {
361        SURFACES.with(|slot| {
362            let mut map = slot.borrow_mut();
363            let entry = map.get_mut(&surface_id).ok_or_else(|| {
364                web_error(format!(
365                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
366                ))
367            })?;
368            let target = entry
369                .bound_target
370                .ok_or_else(|| web_error("Plotting surface is not bound to a render target."))?;
371            let BoundSurfaceTarget::Figure(handle) = target else {
372                entry
373                    .renderer
374                    .render_current_scene()
375                    .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
376                return Ok(());
377            };
378            // "Better" path: only re-prime render data when the figure revision changed.
379            let current_rev = current_figure_revision(FigureHandle::from(handle));
380            if entry.last_revision != current_rev {
381                let figure = clone_figure(FigureHandle::from(handle))
382                    .ok_or_else(|| web_error(format!("figure handle {handle} does not exist")))?;
383                if !figure.visible {
384                    entry
385                        .renderer
386                        .clear_surface()
387                        .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
388                    entry.last_revision = current_rev;
389                    return Ok(());
390                }
391                entry
392                    .renderer
393                    .render_figure(figure)
394                    .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
395                entry.last_revision = current_rev;
396            }
397            entry
398                .renderer
399                .render_current_scene()
400                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
401            Ok(())
402        })
403    }
404
405    pub(super) fn handle_surface_event_impl(
406        surface_id: u32,
407        event: PlotEvent,
408    ) -> BuiltinResult<()> {
409        SURFACES.with(|slot| {
410            let mut map = slot.borrow_mut();
411            let entry = map.get_mut(&surface_id).ok_or_else(|| {
412                web_error(format!(
413                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
414                ))
415            })?;
416            match &event {
417                PlotEvent::MousePress { .. }
418                | PlotEvent::MouseRelease { .. }
419                | PlotEvent::MouseWheel { .. } => {
420                    debug!("plot-web: surface_event(surface_id={surface_id}, event={event:?})");
421                }
422                PlotEvent::MouseMove { .. } | PlotEvent::Resize { .. } => {}
423                PlotEvent::KeyPress { .. } | PlotEvent::KeyRelease { .. } => {}
424            }
425            // If no figure was ever rendered, there's nothing to manipulate.
426            // Still accept the event (no-op) so the host doesn't have to special-case.
427            let _ = entry.renderer.handle_event(event);
428            // Camera interactions should re-render immediately, without requiring a figure revision bump.
429            entry
430                .renderer
431                .render_current_scene()
432                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
433            Ok(())
434        })
435    }
436
437    pub(super) fn fit_surface_extents_impl(surface_id: u32) -> BuiltinResult<()> {
438        SURFACES.with(|slot| {
439            let mut map = slot.borrow_mut();
440            let entry = map.get_mut(&surface_id).ok_or_else(|| {
441                web_error(format!(
442                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
443                ))
444            })?;
445            entry.renderer.fit_extents();
446            entry
447                .renderer
448                .render_current_scene()
449                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
450            Ok(())
451        })
452    }
453
454    pub(super) fn reset_surface_camera_impl(surface_id: u32) -> BuiltinResult<()> {
455        SURFACES.with(|slot| {
456            let mut map = slot.borrow_mut();
457            let entry = map.get_mut(&surface_id).ok_or_else(|| {
458                web_error(format!(
459                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
460                ))
461            })?;
462            entry.renderer.reset_camera_position();
463            entry
464                .renderer
465                .render_current_scene()
466                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
467            Ok(())
468        })
469    }
470
471    pub(super) fn get_surface_camera_state_impl(
472        surface_id: u32,
473    ) -> BuiltinResult<PlotSurfaceCameraState> {
474        SURFACES.with(|slot| {
475            let map = slot.borrow();
476            let entry = map.get(&surface_id).ok_or_else(|| {
477                web_error(format!(
478                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
479                ))
480            })?;
481            Ok(convert_camera_state(entry.renderer.camera_state()))
482        })
483    }
484
485    pub(super) fn set_surface_camera_state_impl(
486        surface_id: u32,
487        state: PlotSurfaceCameraState,
488    ) -> BuiltinResult<()> {
489        SURFACES.with(|slot| {
490            let mut map = slot.borrow_mut();
491            let entry = map.get_mut(&surface_id).ok_or_else(|| {
492                web_error(format!(
493                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
494                ))
495            })?;
496            entry
497                .renderer
498                .set_camera_state(&convert_camera_state_back(state));
499            entry
500                .renderer
501                .render_current_scene()
502                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
503            Ok(())
504        })
505    }
506
507    pub(super) fn take_surface_host_actions_impl(
508        surface_id: u32,
509    ) -> BuiltinResult<Vec<PlotSurfaceHostAction>> {
510        SURFACES.with(|slot| {
511            let mut map = slot.borrow_mut();
512            let entry = map.get_mut(&surface_id).ok_or_else(|| {
513                web_error(format!(
514                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
515                ))
516            })?;
517            Ok(entry
518                .renderer
519                .take_host_actions()
520                .into_iter()
521                .map(convert_host_action)
522                .collect())
523        })
524    }
525
526    pub fn render_current_scene(handle: u32) -> BuiltinResult<()> {
527        debug!("plot-web: render_current_scene(handle={handle})");
528        // If nothing is currently bound to this handle, try to claim the lowest-id unbound
529        // surface. This ensures `drawnow()` / `pause()` can present even if the host hasn't
530        // explicitly bound a surface yet.
531        let needs_autobind = SURFACES.with(|slot| {
532            let map = slot.borrow();
533            !map.values()
534                .any(|entry| entry.bound_target == Some(BoundSurfaceTarget::Figure(handle)))
535        });
536        if needs_autobind {
537            let maybe_unbound_surface = SURFACES.with(|slot| {
538                let map = slot.borrow();
539                map.iter()
540                    .filter_map(|(surface_id, entry)| {
541                        if entry.bound_target.is_none() {
542                            Some(*surface_id)
543                        } else {
544                            None
545                        }
546                    })
547                    .min()
548            });
549            if let Some(surface_id) = maybe_unbound_surface {
550                // Bind without forcing a full re-prime here; present_surface will set last_revision.
551                let _ = bind_surface_to_figure_impl(surface_id, handle);
552            }
553        }
554
555        // Render any surfaces that are currently bound to this handle.
556        let surface_ids: Vec<u32> = SURFACES.with(|slot| {
557            slot.borrow()
558                .iter()
559                .filter_map(|(surface_id, entry)| {
560                    if entry.bound_target == Some(BoundSurfaceTarget::Figure(handle)) {
561                        Some(*surface_id)
562                    } else {
563                        None
564                    }
565                })
566                .collect()
567        });
568        if surface_ids.is_empty() {
569            // No bound surfaces; nothing to do.
570            return Ok(());
571        }
572        for surface_id in surface_ids {
573            // Use caching logic in present_surface so we avoid re-priming unless revision changed.
574            present_surface_impl(surface_id)?;
575        }
576        Ok(())
577    }
578
579    pub fn invalidate_surface_revisions() {
580        SURFACES.with(|slot| {
581            let mut map = slot.borrow_mut();
582            for entry in map.values_mut() {
583                entry.last_revision = None;
584            }
585        });
586    }
587
588    // expose type to outer module
589    pub(super) use runmat_plot::web::WebRenderer as RendererType;
590
591    fn convert_host_action(
592        action: runmat_plot::web::WebSurfaceHostAction,
593    ) -> PlotSurfaceHostAction {
594        match action {
595            runmat_plot::web::WebSurfaceHostAction::CreateFeaStudy => {
596                PlotSurfaceHostAction::CreateFeaStudy
597            }
598        }
599    }
600}
601
602#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
603pub(crate) mod wasm {
604    use super::*;
605
606    pub struct RendererPlaceholder;
607
608    pub(super) fn install_surface_impl(
609        _surface_id: u32,
610        _renderer: RendererPlaceholder,
611    ) -> BuiltinResult<()> {
612        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
613    }
614
615    pub(super) fn detach_surface_impl(_surface_id: u32) {}
616
617    pub(super) fn clear_closed_figure_surfaces_impl(_handle: u32) -> BuiltinResult<()> {
618        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
619    }
620
621    pub fn web_renderer_ready() -> bool {
622        false
623    }
624
625    pub(super) use RendererPlaceholder as RendererType;
626
627    pub(super) fn resize_surface_impl(
628        _surface_id: u32,
629        _width: u32,
630        _height: u32,
631        _pixels_per_point: f32,
632    ) -> BuiltinResult<()> {
633        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
634    }
635
636    pub fn render_current_scene(_handle: u32) -> BuiltinResult<()> {
637        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
638    }
639
640    pub fn invalidate_surface_revisions() {}
641
642    pub(super) fn bind_surface_to_figure_impl(_surface_id: u32, _handle: u32) -> BuiltinResult<()> {
643        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
644    }
645
646    pub(super) fn present_surface_impl(_surface_id: u32) -> BuiltinResult<()> {
647        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
648    }
649
650    pub(super) fn present_figure_on_surface_impl(
651        _surface_id: u32,
652        _handle: u32,
653    ) -> BuiltinResult<()> {
654        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
655    }
656
657    pub(super) fn present_geometry_scene_on_surface_impl(
658        _surface_id: u32,
659        _handle: u32,
660        _scene: runmat_plot::GeometryScene,
661    ) -> BuiltinResult<()> {
662        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
663    }
664
665    pub(super) fn set_geometry_scene_presentation_impl(
666        _surface_id: u32,
667        _presentation: runmat_plot::GeometryScenePresentation,
668    ) -> BuiltinResult<()> {
669        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
670    }
671
672    pub(super) fn pick_geometry_scene_region_impl(
673        _surface_id: u32,
674        _x: f32,
675        _y: f32,
676    ) -> BuiltinResult<Option<runmat_plot::GeometryScenePickResult>> {
677        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
678    }
679
680    pub(super) fn fit_surface_extents_impl(_surface_id: u32) -> BuiltinResult<()> {
681        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
682    }
683
684    pub(super) fn reset_surface_camera_impl(_surface_id: u32) -> BuiltinResult<()> {
685        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
686    }
687
688    pub(super) fn get_surface_camera_state_impl(
689        _surface_id: u32,
690    ) -> BuiltinResult<PlotSurfaceCameraState> {
691        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
692    }
693
694    pub(super) fn set_surface_camera_state_impl(
695        _surface_id: u32,
696        _state: PlotSurfaceCameraState,
697    ) -> BuiltinResult<()> {
698        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
699    }
700
701    pub(super) fn set_theme_config_impl(
702        _theme: runmat_plot::styling::PlotThemeConfig,
703    ) -> BuiltinResult<()> {
704        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
705    }
706
707    pub(super) fn current_theme_config_impl() -> runmat_plot::styling::PlotThemeConfig {
708        runmat_plot::styling::PlotThemeConfig::default()
709    }
710
711    pub(super) fn take_surface_host_actions_impl(
712        _surface_id: u32,
713    ) -> BuiltinResult<Vec<PlotSurfaceHostAction>> {
714        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
715    }
716}
717
718pub use wasm::invalidate_surface_revisions;
719pub use wasm::render_current_scene;
720pub use wasm::web_renderer_ready;
721
722pub fn install_surface(surface_id: u32, renderer: wasm::RendererType) -> BuiltinResult<()> {
723    wasm::install_surface_impl(surface_id, renderer)
724}
725
726pub fn detach_surface(surface_id: u32) {
727    wasm::detach_surface_impl(surface_id)
728}
729
730pub fn clear_closed_figure_surfaces(handle: u32) -> BuiltinResult<()> {
731    wasm::clear_closed_figure_surfaces_impl(handle)
732}
733
734pub fn resize_surface(
735    surface_id: u32,
736    width: u32,
737    height: u32,
738    pixels_per_point: f32,
739) -> BuiltinResult<()> {
740    wasm::resize_surface_impl(surface_id, width, height, pixels_per_point)
741}
742
743pub fn bind_surface_to_figure(surface_id: u32, handle: u32) -> BuiltinResult<()> {
744    wasm::bind_surface_to_figure_impl(surface_id, handle)
745}
746
747pub fn present_surface(surface_id: u32) -> BuiltinResult<()> {
748    wasm::present_surface_impl(surface_id)
749}
750
751#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
752pub fn handle_plot_surface_event(
753    surface_id: u32,
754    event: runmat_plot::core::PlotEvent,
755) -> BuiltinResult<()> {
756    wasm::handle_surface_event_impl(surface_id, event)
757}
758
759pub fn present_figure_on_surface(surface_id: u32, handle: u32) -> BuiltinResult<()> {
760    wasm::present_figure_on_surface_impl(surface_id, handle)
761}
762
763pub fn present_geometry_scene_on_surface(
764    surface_id: u32,
765    handle: u32,
766    scene: runmat_plot::GeometryScene,
767) -> BuiltinResult<()> {
768    wasm::present_geometry_scene_on_surface_impl(surface_id, handle, scene)
769}
770
771pub fn set_geometry_scene_presentation(
772    surface_id: u32,
773    presentation: runmat_plot::GeometryScenePresentation,
774) -> BuiltinResult<()> {
775    wasm::set_geometry_scene_presentation_impl(surface_id, presentation)
776}
777
778pub fn pick_geometry_scene_region(
779    surface_id: u32,
780    x: f32,
781    y: f32,
782) -> BuiltinResult<Option<runmat_plot::GeometryScenePickResult>> {
783    wasm::pick_geometry_scene_region_impl(surface_id, x, y)
784}
785
786pub fn fit_surface_extents(surface_id: u32) -> BuiltinResult<()> {
787    wasm::fit_surface_extents_impl(surface_id)
788}
789
790pub fn reset_surface_camera(surface_id: u32) -> BuiltinResult<()> {
791    wasm::reset_surface_camera_impl(surface_id)
792}
793
794pub fn get_surface_camera_state(surface_id: u32) -> BuiltinResult<PlotSurfaceCameraState> {
795    wasm::get_surface_camera_state_impl(surface_id)
796}
797
798pub fn set_surface_camera_state(
799    surface_id: u32,
800    state: PlotSurfaceCameraState,
801) -> BuiltinResult<()> {
802    wasm::set_surface_camera_state_impl(surface_id, state)
803}
804
805pub fn take_surface_host_actions(surface_id: u32) -> BuiltinResult<Vec<PlotSurfaceHostAction>> {
806    wasm::take_surface_host_actions_impl(surface_id)
807}
808
809pub fn set_plot_theme_config(theme: runmat_plot::styling::PlotThemeConfig) -> BuiltinResult<()> {
810    wasm::set_theme_config_impl(theme)
811}
812
813pub fn current_plot_theme_config() -> runmat_plot::styling::PlotThemeConfig {
814    wasm::current_theme_config_impl()
815}
816
817#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
818fn convert_camera_state(state: runmat_plot::web::PlotSurfaceCameraState) -> PlotSurfaceCameraState {
819    PlotSurfaceCameraState {
820        active_axes: state.active_axes,
821        axes: state
822            .axes
823            .into_iter()
824            .map(|camera| PlotCameraState {
825                position: camera.position,
826                target: camera.target,
827                up: camera.up,
828                zoom: camera.zoom,
829                aspect_ratio: camera.aspect_ratio,
830                projection: match camera.projection {
831                    runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far } => {
832                        PlotCameraProjection::Perspective { fov, near, far }
833                    }
834                    runmat_plot::web::PlotCameraProjection::Orthographic {
835                        left,
836                        right,
837                        bottom,
838                        top,
839                        near,
840                        far,
841                    } => PlotCameraProjection::Orthographic {
842                        left,
843                        right,
844                        bottom,
845                        top,
846                        near,
847                        far,
848                    },
849                },
850            })
851            .collect(),
852    }
853}
854
855#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
856fn convert_camera_state_back(
857    state: PlotSurfaceCameraState,
858) -> runmat_plot::web::PlotSurfaceCameraState {
859    runmat_plot::web::PlotSurfaceCameraState {
860        active_axes: state.active_axes,
861        axes: state
862            .axes
863            .into_iter()
864            .map(|camera| runmat_plot::web::PlotCameraState {
865                position: camera.position,
866                target: camera.target,
867                up: camera.up,
868                zoom: camera.zoom,
869                aspect_ratio: camera.aspect_ratio,
870                projection: match camera.projection {
871                    PlotCameraProjection::Perspective { fov, near, far } => {
872                        runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far }
873                    }
874                    PlotCameraProjection::Orthographic {
875                        left,
876                        right,
877                        bottom,
878                        top,
879                        near,
880                        far,
881                    } => runmat_plot::web::PlotCameraProjection::Orthographic {
882                        left,
883                        right,
884                        bottom,
885                        top,
886                        near,
887                        far,
888                    },
889                },
890            })
891            .collect(),
892    }
893}
894
895// No render_web_canvas wrapper; web presentation is surface-driven.