runmat_runtime/builtins/plotting/core/
web.rs1#[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
43fn web_error(message: impl Into<String>) -> RuntimeError {
44 build_runtime_error(message)
45 .with_identifier("RunMat:plot:WebError")
46 .build()
47}
48
49#[allow(dead_code)]
50fn web_error_with_source(
51 message: impl Into<String>,
52 source: impl std::error::Error + Send + Sync + 'static,
53) -> RuntimeError {
54 build_runtime_error(message)
55 .with_identifier("RunMat:plot:WebError")
56 .with_source(source)
57 .build()
58}
59
60#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
61pub(crate) mod wasm {
62 use super::*;
63 use crate::builtins::plotting::state::{clone_figure, current_figure_revision, FigureHandle};
64 use log::debug;
65 use runmat_plot::core::PlotEvent;
66 use runmat_plot::styling::PlotThemeConfig;
67 use runmat_plot::web::WebRenderer;
68 use runmat_thread_local::runmat_thread_local;
69 use std::cell::RefCell;
70 use std::collections::HashMap;
71
72 runmat_thread_local! {
73 static SURFACES: RefCell<HashMap<u32, SurfaceEntry>> = RefCell::new(HashMap::new());
74 static ACTIVE_THEME: RefCell<PlotThemeConfig> = RefCell::new(PlotThemeConfig::default());
75 }
76
77 struct SurfaceEntry {
78 renderer: WebRenderer,
79 bound_handle: Option<u32>,
80 last_revision: Option<u64>,
81 }
82
83 pub(super) fn install_surface_impl(
84 surface_id: u32,
85 mut renderer: WebRenderer,
86 ) -> BuiltinResult<()> {
87 ACTIVE_THEME.with(|theme| {
88 renderer.set_theme_config(theme.borrow().clone());
89 });
90 SURFACES.with(|slot| {
91 slot.borrow_mut().insert(
92 surface_id,
93 SurfaceEntry {
94 renderer,
95 bound_handle: None,
96 last_revision: None,
97 },
98 );
99 });
100 SURFACES.with(|slot| {
101 let keys: Vec<u32> = slot.borrow().keys().copied().collect();
102 debug!(
103 "plot-web: installed surface surface_id={surface_id} (active_surfaces={keys:?})"
104 );
105 });
106 Ok(())
107 }
108
109 pub(super) fn detach_surface_impl(surface_id: u32) {
110 SURFACES.with(|slot| {
111 slot.borrow_mut().remove(&surface_id);
112 });
113 SURFACES.with(|slot| {
114 let keys: Vec<u32> = slot.borrow().keys().copied().collect();
115 debug!("plot-web: detached surface surface_id={surface_id} (active_surfaces={keys:?})");
116 });
117 }
118
119 pub(super) fn clear_closed_figure_surfaces_impl(handle: u32) -> BuiltinResult<()> {
120 SURFACES.with(|slot| {
121 let mut map = slot.borrow_mut();
122 for entry in map.values_mut() {
123 if entry.bound_handle == Some(handle) {
124 entry.bound_handle = None;
125 entry.last_revision = None;
126 entry
127 .renderer
128 .clear_surface()
129 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
130 }
131 }
132 Ok(())
133 })
134 }
135
136 pub fn web_renderer_ready() -> bool {
137 SURFACES.with(|slot| !slot.borrow().is_empty())
138 }
139
140 pub(super) fn resize_surface_impl(
141 surface_id: u32,
142 width: u32,
143 height: u32,
144 pixels_per_point: f32,
145 ) -> BuiltinResult<()> {
146 SURFACES.with(|slot| {
147 let mut map = slot.borrow_mut();
148 let entry = map.get_mut(&surface_id).ok_or_else(|| {
149 web_error(format!(
150 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
151 ))
152 })?;
153 entry.renderer.set_pixels_per_point(pixels_per_point);
154 entry
155 .renderer
156 .resize_surface(width, height)
157 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
158 Ok(())
159 })
160 }
161
162 pub(super) fn bind_surface_to_figure_impl(surface_id: u32, handle: u32) -> BuiltinResult<()> {
163 SURFACES.with(|slot| {
164 let mut map = slot.borrow_mut();
165 let entry = map.get_mut(&surface_id).ok_or_else(|| {
166 web_error(format!(
167 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
168 ))
169 })?;
170 entry.bound_handle = Some(handle);
171 entry.last_revision = None;
173 Ok(())
174 })
175 }
176
177 pub(super) fn set_theme_config_impl(theme: PlotThemeConfig) -> BuiltinResult<()> {
178 debug!(
179 "plot-web: runtime set_theme_config_impl variant={:?} custom_colors={}",
180 theme.variant,
181 theme.custom_colors.is_some()
182 );
183 ACTIVE_THEME.with(|slot| {
184 *slot.borrow_mut() = theme.clone();
185 });
186 SURFACES.with(|slot| {
187 let mut map = slot.borrow_mut();
188 debug!("plot-web: applying theme to {} surfaces", map.len());
189 for entry in map.values_mut() {
190 entry.renderer.set_theme_config(theme.clone());
191 if let Some(handle) = entry.bound_handle {
192 if let Some(figure) = clone_figure(FigureHandle::from(handle)) {
193 entry
194 .renderer
195 .render_figure(figure)
196 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
197 }
198 }
199 }
200 Ok(())
201 })
202 }
203
204 pub(super) fn current_theme_config_impl() -> PlotThemeConfig {
205 ACTIVE_THEME.with(|slot| slot.borrow().clone())
206 }
207
208 pub(super) fn present_figure_on_surface_impl(
209 surface_id: u32,
210 handle: u32,
211 ) -> BuiltinResult<()> {
212 SURFACES.with(|slot| {
214 let mut map = slot.borrow_mut();
215 let entry = map.get_mut(&surface_id).ok_or_else(|| {
216 web_error(format!(
217 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
218 ))
219 })?;
220 if entry.bound_handle != Some(handle) {
221 entry.bound_handle = Some(handle);
222 entry.last_revision = None;
223 }
224 Ok::<(), RuntimeError>(())
225 })?;
226 present_surface_impl(surface_id)
227 }
228
229 pub(super) fn present_surface_impl(surface_id: u32) -> BuiltinResult<()> {
230 SURFACES.with(|slot| {
231 let mut map = slot.borrow_mut();
232 let entry = map.get_mut(&surface_id).ok_or_else(|| {
233 web_error(format!(
234 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
235 ))
236 })?;
237 let handle = entry.bound_handle.ok_or_else(|| {
238 web_error(
239 "Plotting surface is not bound to a figure handle. Call bindSurfaceToFigure().",
240 )
241 })?;
242 let current_rev = current_figure_revision(FigureHandle::from(handle));
244 if entry.last_revision != current_rev {
245 let figure = clone_figure(FigureHandle::from(handle))
246 .ok_or_else(|| web_error(format!("figure handle {handle} does not exist")))?;
247 entry
248 .renderer
249 .render_figure(figure)
250 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
251 entry.last_revision = current_rev;
252 }
253 entry
254 .renderer
255 .render_current_scene()
256 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
257 Ok(())
258 })
259 }
260
261 pub(super) fn handle_surface_event_impl(
262 surface_id: u32,
263 event: PlotEvent,
264 ) -> BuiltinResult<()> {
265 SURFACES.with(|slot| {
266 let mut map = slot.borrow_mut();
267 let entry = map.get_mut(&surface_id).ok_or_else(|| {
268 web_error(format!(
269 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
270 ))
271 })?;
272 match &event {
273 PlotEvent::MousePress { .. }
274 | PlotEvent::MouseRelease { .. }
275 | PlotEvent::MouseWheel { .. } => {
276 debug!("plot-web: surface_event(surface_id={surface_id}, event={event:?})");
277 }
278 PlotEvent::MouseMove { .. } | PlotEvent::Resize { .. } => {}
279 PlotEvent::KeyPress { .. } | PlotEvent::KeyRelease { .. } => {}
280 }
281 let _ = entry.renderer.handle_event(event);
284 entry
286 .renderer
287 .render_current_scene()
288 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
289 Ok(())
290 })
291 }
292
293 pub(super) fn fit_surface_extents_impl(surface_id: u32) -> 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 entry.renderer.fit_extents();
302 entry
303 .renderer
304 .render_current_scene()
305 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
306 Ok(())
307 })
308 }
309
310 pub(super) fn reset_surface_camera_impl(surface_id: u32) -> BuiltinResult<()> {
311 SURFACES.with(|slot| {
312 let mut map = slot.borrow_mut();
313 let entry = map.get_mut(&surface_id).ok_or_else(|| {
314 web_error(format!(
315 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
316 ))
317 })?;
318 entry.renderer.reset_camera_position();
319 entry
320 .renderer
321 .render_current_scene()
322 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
323 Ok(())
324 })
325 }
326
327 pub(super) fn get_surface_camera_state_impl(
328 surface_id: u32,
329 ) -> BuiltinResult<PlotSurfaceCameraState> {
330 SURFACES.with(|slot| {
331 let map = slot.borrow();
332 let entry = map.get(&surface_id).ok_or_else(|| {
333 web_error(format!(
334 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
335 ))
336 })?;
337 Ok(convert_camera_state(entry.renderer.camera_state()))
338 })
339 }
340
341 pub(super) fn set_surface_camera_state_impl(
342 surface_id: u32,
343 state: PlotSurfaceCameraState,
344 ) -> BuiltinResult<()> {
345 SURFACES.with(|slot| {
346 let mut map = slot.borrow_mut();
347 let entry = map.get_mut(&surface_id).ok_or_else(|| {
348 web_error(format!(
349 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
350 ))
351 })?;
352 entry
353 .renderer
354 .set_camera_state(&convert_camera_state_back(state));
355 entry
356 .renderer
357 .render_current_scene()
358 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
359 Ok(())
360 })
361 }
362
363 pub fn render_current_scene(handle: u32) -> BuiltinResult<()> {
364 debug!("plot-web: render_current_scene(handle={handle})");
365 let needs_autobind = SURFACES.with(|slot| {
369 let map = slot.borrow();
370 !map.values().any(|entry| entry.bound_handle == Some(handle))
371 });
372 if needs_autobind {
373 let maybe_unbound_surface = SURFACES.with(|slot| {
374 let map = slot.borrow();
375 map.iter()
376 .filter_map(|(surface_id, entry)| {
377 if entry.bound_handle.is_none() {
378 Some(*surface_id)
379 } else {
380 None
381 }
382 })
383 .min()
384 });
385 if let Some(surface_id) = maybe_unbound_surface {
386 let _ = bind_surface_to_figure_impl(surface_id, handle);
388 }
389 }
390
391 let surface_ids: Vec<u32> = SURFACES.with(|slot| {
393 slot.borrow()
394 .iter()
395 .filter_map(|(surface_id, entry)| {
396 if entry.bound_handle == Some(handle) {
397 Some(*surface_id)
398 } else {
399 None
400 }
401 })
402 .collect()
403 });
404 if surface_ids.is_empty() {
405 return Ok(());
407 }
408 for surface_id in surface_ids {
409 present_surface_impl(surface_id)?;
411 }
412 Ok(())
413 }
414
415 pub fn invalidate_surface_revisions() {
416 SURFACES.with(|slot| {
417 let mut map = slot.borrow_mut();
418 for entry in map.values_mut() {
419 entry.last_revision = None;
420 }
421 });
422 }
423
424 pub(super) use runmat_plot::web::WebRenderer as RendererType;
426}
427
428#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
429pub(crate) mod wasm {
430 use super::*;
431
432 pub struct RendererPlaceholder;
433
434 pub(super) fn install_surface_impl(
435 _surface_id: u32,
436 _renderer: RendererPlaceholder,
437 ) -> BuiltinResult<()> {
438 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
439 }
440
441 pub(super) fn detach_surface_impl(_surface_id: u32) {}
442
443 pub(super) fn clear_closed_figure_surfaces_impl(_handle: u32) -> BuiltinResult<()> {
444 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
445 }
446
447 pub fn web_renderer_ready() -> bool {
448 false
449 }
450
451 pub(super) use RendererPlaceholder as RendererType;
452
453 pub(super) fn resize_surface_impl(
454 _surface_id: u32,
455 _width: u32,
456 _height: u32,
457 _pixels_per_point: f32,
458 ) -> BuiltinResult<()> {
459 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
460 }
461
462 pub fn render_current_scene(_handle: u32) -> BuiltinResult<()> {
463 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
464 }
465
466 pub fn invalidate_surface_revisions() {}
467
468 pub(super) fn bind_surface_to_figure_impl(_surface_id: u32, _handle: u32) -> BuiltinResult<()> {
469 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
470 }
471
472 pub(super) fn present_surface_impl(_surface_id: u32) -> BuiltinResult<()> {
473 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
474 }
475
476 pub(super) fn present_figure_on_surface_impl(
477 _surface_id: u32,
478 _handle: u32,
479 ) -> BuiltinResult<()> {
480 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
481 }
482
483 pub(super) fn fit_surface_extents_impl(_surface_id: u32) -> BuiltinResult<()> {
484 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
485 }
486
487 pub(super) fn reset_surface_camera_impl(_surface_id: u32) -> BuiltinResult<()> {
488 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
489 }
490
491 pub(super) fn get_surface_camera_state_impl(
492 _surface_id: u32,
493 ) -> BuiltinResult<PlotSurfaceCameraState> {
494 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
495 }
496
497 pub(super) fn set_surface_camera_state_impl(
498 _surface_id: u32,
499 _state: PlotSurfaceCameraState,
500 ) -> BuiltinResult<()> {
501 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
502 }
503
504 pub(super) fn set_theme_config_impl(
505 _theme: runmat_plot::styling::PlotThemeConfig,
506 ) -> BuiltinResult<()> {
507 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
508 }
509
510 pub(super) fn current_theme_config_impl() -> runmat_plot::styling::PlotThemeConfig {
511 runmat_plot::styling::PlotThemeConfig::default()
512 }
513}
514
515pub use wasm::invalidate_surface_revisions;
516pub use wasm::render_current_scene;
517pub use wasm::web_renderer_ready;
518
519pub fn install_surface(surface_id: u32, renderer: wasm::RendererType) -> BuiltinResult<()> {
520 wasm::install_surface_impl(surface_id, renderer)
521}
522
523pub fn detach_surface(surface_id: u32) {
524 wasm::detach_surface_impl(surface_id)
525}
526
527pub fn clear_closed_figure_surfaces(handle: u32) -> BuiltinResult<()> {
528 wasm::clear_closed_figure_surfaces_impl(handle)
529}
530
531pub fn resize_surface(
532 surface_id: u32,
533 width: u32,
534 height: u32,
535 pixels_per_point: f32,
536) -> BuiltinResult<()> {
537 wasm::resize_surface_impl(surface_id, width, height, pixels_per_point)
538}
539
540pub fn bind_surface_to_figure(surface_id: u32, handle: u32) -> BuiltinResult<()> {
541 wasm::bind_surface_to_figure_impl(surface_id, handle)
542}
543
544pub fn present_surface(surface_id: u32) -> BuiltinResult<()> {
545 wasm::present_surface_impl(surface_id)
546}
547
548#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
549pub fn handle_plot_surface_event(
550 surface_id: u32,
551 event: runmat_plot::core::PlotEvent,
552) -> BuiltinResult<()> {
553 wasm::handle_surface_event_impl(surface_id, event)
554}
555
556pub fn present_figure_on_surface(surface_id: u32, handle: u32) -> BuiltinResult<()> {
557 wasm::present_figure_on_surface_impl(surface_id, handle)
558}
559
560pub fn fit_surface_extents(surface_id: u32) -> BuiltinResult<()> {
561 wasm::fit_surface_extents_impl(surface_id)
562}
563
564pub fn reset_surface_camera(surface_id: u32) -> BuiltinResult<()> {
565 wasm::reset_surface_camera_impl(surface_id)
566}
567
568pub fn get_surface_camera_state(surface_id: u32) -> BuiltinResult<PlotSurfaceCameraState> {
569 wasm::get_surface_camera_state_impl(surface_id)
570}
571
572pub fn set_surface_camera_state(
573 surface_id: u32,
574 state: PlotSurfaceCameraState,
575) -> BuiltinResult<()> {
576 wasm::set_surface_camera_state_impl(surface_id, state)
577}
578
579pub fn set_plot_theme_config(theme: runmat_plot::styling::PlotThemeConfig) -> BuiltinResult<()> {
580 wasm::set_theme_config_impl(theme)
581}
582
583pub fn current_plot_theme_config() -> runmat_plot::styling::PlotThemeConfig {
584 wasm::current_theme_config_impl()
585}
586
587#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
588fn convert_camera_state(state: runmat_plot::web::PlotSurfaceCameraState) -> PlotSurfaceCameraState {
589 PlotSurfaceCameraState {
590 active_axes: state.active_axes,
591 axes: state
592 .axes
593 .into_iter()
594 .map(|camera| PlotCameraState {
595 position: camera.position,
596 target: camera.target,
597 up: camera.up,
598 zoom: camera.zoom,
599 aspect_ratio: camera.aspect_ratio,
600 projection: match camera.projection {
601 runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far } => {
602 PlotCameraProjection::Perspective { fov, near, far }
603 }
604 runmat_plot::web::PlotCameraProjection::Orthographic {
605 left,
606 right,
607 bottom,
608 top,
609 near,
610 far,
611 } => PlotCameraProjection::Orthographic {
612 left,
613 right,
614 bottom,
615 top,
616 near,
617 far,
618 },
619 },
620 })
621 .collect(),
622 }
623}
624
625#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
626fn convert_camera_state_back(
627 state: PlotSurfaceCameraState,
628) -> runmat_plot::web::PlotSurfaceCameraState {
629 runmat_plot::web::PlotSurfaceCameraState {
630 active_axes: state.active_axes,
631 axes: state
632 .axes
633 .into_iter()
634 .map(|camera| runmat_plot::web::PlotCameraState {
635 position: camera.position,
636 target: camera.target,
637 up: camera.up,
638 zoom: camera.zoom,
639 aspect_ratio: camera.aspect_ratio,
640 projection: match camera.projection {
641 PlotCameraProjection::Perspective { fov, near, far } => {
642 runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far }
643 }
644 PlotCameraProjection::Orthographic {
645 left,
646 right,
647 bottom,
648 top,
649 near,
650 far,
651 } => runmat_plot::web::PlotCameraProjection::Orthographic {
652 left,
653 right,
654 bottom,
655 top,
656 near,
657 far,
658 },
659 },
660 })
661 .collect(),
662 }
663}
664
665