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            entry.geometry_presentation = presentation.clone();
307            entry
308                .renderer
309                .set_geometry_scene_presentation(presentation)
310                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
311            Ok(())
312        })
313    }
314
315    pub(super) fn pick_geometry_scene_region_impl(
316        surface_id: u32,
317        x: f32,
318        y: f32,
319    ) -> BuiltinResult<Option<GeometryScenePickResult>> {
320        SURFACES.with(|slot| {
321            let mut map = slot.borrow_mut();
322            let entry = map.get_mut(&surface_id).ok_or_else(|| {
323                web_error(format!(
324                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
325                ))
326            })?;
327            let Some(BoundSurfaceTarget::GeometryScene(handle)) = entry.bound_target else {
328                return Ok(None);
329            };
330            let rebuild = entry
331                .geometry_pick_index
332                .as_ref()
333                .map(|cached| cached.handle != handle)
334                .unwrap_or(true);
335            if rebuild {
336                let scene =
337                    crate::builtins::plotting::clone_geometry_scene(handle).ok_or_else(|| {
338                        web_error(format!("geometry scene handle {handle} does not exist"))
339                    })?;
340                entry.geometry_pick_index = Some(CachedGeometryPickIndex {
341                    handle,
342                    index: GeometryScenePickIndex::build(&scene),
343                });
344            }
345            Ok(entry.geometry_pick_index.as_ref().and_then(|cached| {
346                entry
347                    .renderer
348                    .pick_geometry_scene_region(&cached.index, [x, y])
349            }))
350        })
351    }
352
353    pub(super) fn present_surface_impl(surface_id: u32) -> BuiltinResult<()> {
354        SURFACES.with(|slot| {
355            let mut map = slot.borrow_mut();
356            let entry = map.get_mut(&surface_id).ok_or_else(|| {
357                web_error(format!(
358                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
359                ))
360            })?;
361            let target = entry
362                .bound_target
363                .ok_or_else(|| web_error("Plotting surface is not bound to a render target."))?;
364            let BoundSurfaceTarget::Figure(handle) = target else {
365                entry
366                    .renderer
367                    .render_current_scene()
368                    .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
369                return Ok(());
370            };
371            // "Better" path: only re-prime render data when the figure revision changed.
372            let current_rev = current_figure_revision(FigureHandle::from(handle));
373            if entry.last_revision != current_rev {
374                let figure = clone_figure(FigureHandle::from(handle))
375                    .ok_or_else(|| web_error(format!("figure handle {handle} does not exist")))?;
376                if !figure.visible {
377                    entry
378                        .renderer
379                        .clear_surface()
380                        .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
381                    entry.last_revision = current_rev;
382                    return Ok(());
383                }
384                entry
385                    .renderer
386                    .render_figure(figure)
387                    .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
388                entry.last_revision = current_rev;
389            }
390            entry
391                .renderer
392                .render_current_scene()
393                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
394            Ok(())
395        })
396    }
397
398    pub(super) fn handle_surface_event_impl(
399        surface_id: u32,
400        event: PlotEvent,
401    ) -> BuiltinResult<()> {
402        SURFACES.with(|slot| {
403            let mut map = slot.borrow_mut();
404            let entry = map.get_mut(&surface_id).ok_or_else(|| {
405                web_error(format!(
406                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
407                ))
408            })?;
409            match &event {
410                PlotEvent::MousePress { .. }
411                | PlotEvent::MouseRelease { .. }
412                | PlotEvent::MouseWheel { .. } => {
413                    debug!("plot-web: surface_event(surface_id={surface_id}, event={event:?})");
414                }
415                PlotEvent::MouseMove { .. } | PlotEvent::Resize { .. } => {}
416                PlotEvent::KeyPress { .. } | PlotEvent::KeyRelease { .. } => {}
417            }
418            // If no figure was ever rendered, there's nothing to manipulate.
419            // Still accept the event (no-op) so the host doesn't have to special-case.
420            let _ = entry.renderer.handle_event(event);
421            // Camera interactions should re-render immediately, without requiring a figure revision bump.
422            entry
423                .renderer
424                .render_current_scene()
425                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
426            Ok(())
427        })
428    }
429
430    pub(super) fn fit_surface_extents_impl(surface_id: u32) -> BuiltinResult<()> {
431        SURFACES.with(|slot| {
432            let mut map = slot.borrow_mut();
433            let entry = map.get_mut(&surface_id).ok_or_else(|| {
434                web_error(format!(
435                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
436                ))
437            })?;
438            entry.renderer.fit_extents();
439            entry
440                .renderer
441                .render_current_scene()
442                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
443            Ok(())
444        })
445    }
446
447    pub(super) fn reset_surface_camera_impl(surface_id: u32) -> BuiltinResult<()> {
448        SURFACES.with(|slot| {
449            let mut map = slot.borrow_mut();
450            let entry = map.get_mut(&surface_id).ok_or_else(|| {
451                web_error(format!(
452                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
453                ))
454            })?;
455            entry.renderer.reset_camera_position();
456            entry
457                .renderer
458                .render_current_scene()
459                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
460            Ok(())
461        })
462    }
463
464    pub(super) fn get_surface_camera_state_impl(
465        surface_id: u32,
466    ) -> BuiltinResult<PlotSurfaceCameraState> {
467        SURFACES.with(|slot| {
468            let map = slot.borrow();
469            let entry = map.get(&surface_id).ok_or_else(|| {
470                web_error(format!(
471                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
472                ))
473            })?;
474            Ok(convert_camera_state(entry.renderer.camera_state()))
475        })
476    }
477
478    pub(super) fn set_surface_camera_state_impl(
479        surface_id: u32,
480        state: PlotSurfaceCameraState,
481    ) -> BuiltinResult<()> {
482        SURFACES.with(|slot| {
483            let mut map = slot.borrow_mut();
484            let entry = map.get_mut(&surface_id).ok_or_else(|| {
485                web_error(format!(
486                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
487                ))
488            })?;
489            entry
490                .renderer
491                .set_camera_state(&convert_camera_state_back(state));
492            entry
493                .renderer
494                .render_current_scene()
495                .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
496            Ok(())
497        })
498    }
499
500    pub(super) fn take_surface_host_actions_impl(
501        surface_id: u32,
502    ) -> BuiltinResult<Vec<PlotSurfaceHostAction>> {
503        SURFACES.with(|slot| {
504            let mut map = slot.borrow_mut();
505            let entry = map.get_mut(&surface_id).ok_or_else(|| {
506                web_error(format!(
507                    "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
508                ))
509            })?;
510            Ok(entry
511                .renderer
512                .take_host_actions()
513                .into_iter()
514                .map(convert_host_action)
515                .collect())
516        })
517    }
518
519    pub fn render_current_scene(handle: u32) -> BuiltinResult<()> {
520        debug!("plot-web: render_current_scene(handle={handle})");
521        // If nothing is currently bound to this handle, try to claim the lowest-id unbound
522        // surface. This ensures `drawnow()` / `pause()` can present even if the host hasn't
523        // explicitly bound a surface yet.
524        let needs_autobind = SURFACES.with(|slot| {
525            let map = slot.borrow();
526            !map.values()
527                .any(|entry| entry.bound_target == Some(BoundSurfaceTarget::Figure(handle)))
528        });
529        if needs_autobind {
530            let maybe_unbound_surface = SURFACES.with(|slot| {
531                let map = slot.borrow();
532                map.iter()
533                    .filter_map(|(surface_id, entry)| {
534                        if entry.bound_target.is_none() {
535                            Some(*surface_id)
536                        } else {
537                            None
538                        }
539                    })
540                    .min()
541            });
542            if let Some(surface_id) = maybe_unbound_surface {
543                // Bind without forcing a full re-prime here; present_surface will set last_revision.
544                let _ = bind_surface_to_figure_impl(surface_id, handle);
545            }
546        }
547
548        // Render any surfaces that are currently bound to this handle.
549        let surface_ids: Vec<u32> = SURFACES.with(|slot| {
550            slot.borrow()
551                .iter()
552                .filter_map(|(surface_id, entry)| {
553                    if entry.bound_target == Some(BoundSurfaceTarget::Figure(handle)) {
554                        Some(*surface_id)
555                    } else {
556                        None
557                    }
558                })
559                .collect()
560        });
561        if surface_ids.is_empty() {
562            // No bound surfaces; nothing to do.
563            return Ok(());
564        }
565        for surface_id in surface_ids {
566            // Use caching logic in present_surface so we avoid re-priming unless revision changed.
567            present_surface_impl(surface_id)?;
568        }
569        Ok(())
570    }
571
572    pub fn invalidate_surface_revisions() {
573        SURFACES.with(|slot| {
574            let mut map = slot.borrow_mut();
575            for entry in map.values_mut() {
576                entry.last_revision = None;
577            }
578        });
579    }
580
581    // expose type to outer module
582    pub(super) use runmat_plot::web::WebRenderer as RendererType;
583
584    fn convert_host_action(
585        action: runmat_plot::web::WebSurfaceHostAction,
586    ) -> PlotSurfaceHostAction {
587        match action {
588            runmat_plot::web::WebSurfaceHostAction::CreateFeaStudy => {
589                PlotSurfaceHostAction::CreateFeaStudy
590            }
591        }
592    }
593}
594
595#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
596pub(crate) mod wasm {
597    use super::*;
598
599    pub struct RendererPlaceholder;
600
601    pub(super) fn install_surface_impl(
602        _surface_id: u32,
603        _renderer: RendererPlaceholder,
604    ) -> BuiltinResult<()> {
605        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
606    }
607
608    pub(super) fn detach_surface_impl(_surface_id: u32) {}
609
610    pub(super) fn clear_closed_figure_surfaces_impl(_handle: u32) -> BuiltinResult<()> {
611        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
612    }
613
614    pub fn web_renderer_ready() -> bool {
615        false
616    }
617
618    pub(super) use RendererPlaceholder as RendererType;
619
620    pub(super) fn resize_surface_impl(
621        _surface_id: u32,
622        _width: u32,
623        _height: u32,
624        _pixels_per_point: f32,
625    ) -> BuiltinResult<()> {
626        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
627    }
628
629    pub fn render_current_scene(_handle: u32) -> BuiltinResult<()> {
630        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
631    }
632
633    pub fn invalidate_surface_revisions() {}
634
635    pub(super) fn bind_surface_to_figure_impl(_surface_id: u32, _handle: u32) -> BuiltinResult<()> {
636        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
637    }
638
639    pub(super) fn present_surface_impl(_surface_id: u32) -> BuiltinResult<()> {
640        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
641    }
642
643    pub(super) fn present_figure_on_surface_impl(
644        _surface_id: u32,
645        _handle: u32,
646    ) -> BuiltinResult<()> {
647        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
648    }
649
650    pub(super) fn present_geometry_scene_on_surface_impl(
651        _surface_id: u32,
652        _handle: u32,
653        _scene: runmat_plot::GeometryScene,
654    ) -> BuiltinResult<()> {
655        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
656    }
657
658    pub(super) fn set_geometry_scene_presentation_impl(
659        _surface_id: u32,
660        _presentation: runmat_plot::GeometryScenePresentation,
661    ) -> BuiltinResult<()> {
662        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
663    }
664
665    pub(super) fn pick_geometry_scene_region_impl(
666        _surface_id: u32,
667        _x: f32,
668        _y: f32,
669    ) -> BuiltinResult<Option<runmat_plot::GeometryScenePickResult>> {
670        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
671    }
672
673    pub(super) fn fit_surface_extents_impl(_surface_id: u32) -> BuiltinResult<()> {
674        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
675    }
676
677    pub(super) fn reset_surface_camera_impl(_surface_id: u32) -> BuiltinResult<()> {
678        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
679    }
680
681    pub(super) fn get_surface_camera_state_impl(
682        _surface_id: u32,
683    ) -> BuiltinResult<PlotSurfaceCameraState> {
684        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
685    }
686
687    pub(super) fn set_surface_camera_state_impl(
688        _surface_id: u32,
689        _state: PlotSurfaceCameraState,
690    ) -> BuiltinResult<()> {
691        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
692    }
693
694    pub(super) fn set_theme_config_impl(
695        _theme: runmat_plot::styling::PlotThemeConfig,
696    ) -> BuiltinResult<()> {
697        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
698    }
699
700    pub(super) fn current_theme_config_impl() -> runmat_plot::styling::PlotThemeConfig {
701        runmat_plot::styling::PlotThemeConfig::default()
702    }
703
704    pub(super) fn take_surface_host_actions_impl(
705        _surface_id: u32,
706    ) -> BuiltinResult<Vec<PlotSurfaceHostAction>> {
707        Err(web_error(ERR_PLOTTING_UNAVAILABLE))
708    }
709}
710
711pub use wasm::invalidate_surface_revisions;
712pub use wasm::render_current_scene;
713pub use wasm::web_renderer_ready;
714
715pub fn install_surface(surface_id: u32, renderer: wasm::RendererType) -> BuiltinResult<()> {
716    wasm::install_surface_impl(surface_id, renderer)
717}
718
719pub fn detach_surface(surface_id: u32) {
720    wasm::detach_surface_impl(surface_id)
721}
722
723pub fn clear_closed_figure_surfaces(handle: u32) -> BuiltinResult<()> {
724    wasm::clear_closed_figure_surfaces_impl(handle)
725}
726
727pub fn resize_surface(
728    surface_id: u32,
729    width: u32,
730    height: u32,
731    pixels_per_point: f32,
732) -> BuiltinResult<()> {
733    wasm::resize_surface_impl(surface_id, width, height, pixels_per_point)
734}
735
736pub fn bind_surface_to_figure(surface_id: u32, handle: u32) -> BuiltinResult<()> {
737    wasm::bind_surface_to_figure_impl(surface_id, handle)
738}
739
740pub fn present_surface(surface_id: u32) -> BuiltinResult<()> {
741    wasm::present_surface_impl(surface_id)
742}
743
744#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
745pub fn handle_plot_surface_event(
746    surface_id: u32,
747    event: runmat_plot::core::PlotEvent,
748) -> BuiltinResult<()> {
749    wasm::handle_surface_event_impl(surface_id, event)
750}
751
752pub fn present_figure_on_surface(surface_id: u32, handle: u32) -> BuiltinResult<()> {
753    wasm::present_figure_on_surface_impl(surface_id, handle)
754}
755
756pub fn present_geometry_scene_on_surface(
757    surface_id: u32,
758    handle: u32,
759    scene: runmat_plot::GeometryScene,
760) -> BuiltinResult<()> {
761    wasm::present_geometry_scene_on_surface_impl(surface_id, handle, scene)
762}
763
764pub fn set_geometry_scene_presentation(
765    surface_id: u32,
766    presentation: runmat_plot::GeometryScenePresentation,
767) -> BuiltinResult<()> {
768    wasm::set_geometry_scene_presentation_impl(surface_id, presentation)
769}
770
771pub fn pick_geometry_scene_region(
772    surface_id: u32,
773    x: f32,
774    y: f32,
775) -> BuiltinResult<Option<runmat_plot::GeometryScenePickResult>> {
776    wasm::pick_geometry_scene_region_impl(surface_id, x, y)
777}
778
779pub fn fit_surface_extents(surface_id: u32) -> BuiltinResult<()> {
780    wasm::fit_surface_extents_impl(surface_id)
781}
782
783pub fn reset_surface_camera(surface_id: u32) -> BuiltinResult<()> {
784    wasm::reset_surface_camera_impl(surface_id)
785}
786
787pub fn get_surface_camera_state(surface_id: u32) -> BuiltinResult<PlotSurfaceCameraState> {
788    wasm::get_surface_camera_state_impl(surface_id)
789}
790
791pub fn set_surface_camera_state(
792    surface_id: u32,
793    state: PlotSurfaceCameraState,
794) -> BuiltinResult<()> {
795    wasm::set_surface_camera_state_impl(surface_id, state)
796}
797
798pub fn take_surface_host_actions(surface_id: u32) -> BuiltinResult<Vec<PlotSurfaceHostAction>> {
799    wasm::take_surface_host_actions_impl(surface_id)
800}
801
802pub fn set_plot_theme_config(theme: runmat_plot::styling::PlotThemeConfig) -> BuiltinResult<()> {
803    wasm::set_theme_config_impl(theme)
804}
805
806pub fn current_plot_theme_config() -> runmat_plot::styling::PlotThemeConfig {
807    wasm::current_theme_config_impl()
808}
809
810#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
811fn convert_camera_state(state: runmat_plot::web::PlotSurfaceCameraState) -> PlotSurfaceCameraState {
812    PlotSurfaceCameraState {
813        active_axes: state.active_axes,
814        axes: state
815            .axes
816            .into_iter()
817            .map(|camera| PlotCameraState {
818                position: camera.position,
819                target: camera.target,
820                up: camera.up,
821                zoom: camera.zoom,
822                aspect_ratio: camera.aspect_ratio,
823                projection: match camera.projection {
824                    runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far } => {
825                        PlotCameraProjection::Perspective { fov, near, far }
826                    }
827                    runmat_plot::web::PlotCameraProjection::Orthographic {
828                        left,
829                        right,
830                        bottom,
831                        top,
832                        near,
833                        far,
834                    } => PlotCameraProjection::Orthographic {
835                        left,
836                        right,
837                        bottom,
838                        top,
839                        near,
840                        far,
841                    },
842                },
843            })
844            .collect(),
845    }
846}
847
848#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
849fn convert_camera_state_back(
850    state: PlotSurfaceCameraState,
851) -> runmat_plot::web::PlotSurfaceCameraState {
852    runmat_plot::web::PlotSurfaceCameraState {
853        active_axes: state.active_axes,
854        axes: state
855            .axes
856            .into_iter()
857            .map(|camera| runmat_plot::web::PlotCameraState {
858                position: camera.position,
859                target: camera.target,
860                up: camera.up,
861                zoom: camera.zoom,
862                aspect_ratio: camera.aspect_ratio,
863                projection: match camera.projection {
864                    PlotCameraProjection::Perspective { fov, near, far } => {
865                        runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far }
866                    }
867                    PlotCameraProjection::Orthographic {
868                        left,
869                        right,
870                        bottom,
871                        top,
872                        near,
873                        far,
874                    } => runmat_plot::web::PlotCameraProjection::Orthographic {
875                        left,
876                        right,
877                        bottom,
878                        top,
879                        near,
880                        far,
881                    },
882                },
883            })
884            .collect(),
885    }
886}
887
888// No render_web_canvas wrapper; web presentation is surface-driven.