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 if !figure.visible {
248 entry
249 .renderer
250 .clear_surface()
251 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
252 entry.last_revision = current_rev;
253 return Ok(());
254 }
255 entry
256 .renderer
257 .render_figure(figure)
258 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
259 entry.last_revision = current_rev;
260 }
261 entry
262 .renderer
263 .render_current_scene()
264 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
265 Ok(())
266 })
267 }
268
269 pub(super) fn handle_surface_event_impl(
270 surface_id: u32,
271 event: PlotEvent,
272 ) -> BuiltinResult<()> {
273 SURFACES.with(|slot| {
274 let mut map = slot.borrow_mut();
275 let entry = map.get_mut(&surface_id).ok_or_else(|| {
276 web_error(format!(
277 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
278 ))
279 })?;
280 match &event {
281 PlotEvent::MousePress { .. }
282 | PlotEvent::MouseRelease { .. }
283 | PlotEvent::MouseWheel { .. } => {
284 debug!("plot-web: surface_event(surface_id={surface_id}, event={event:?})");
285 }
286 PlotEvent::MouseMove { .. } | PlotEvent::Resize { .. } => {}
287 PlotEvent::KeyPress { .. } | PlotEvent::KeyRelease { .. } => {}
288 }
289 let _ = entry.renderer.handle_event(event);
292 entry
294 .renderer
295 .render_current_scene()
296 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
297 Ok(())
298 })
299 }
300
301 pub(super) fn fit_surface_extents_impl(surface_id: u32) -> BuiltinResult<()> {
302 SURFACES.with(|slot| {
303 let mut map = slot.borrow_mut();
304 let entry = map.get_mut(&surface_id).ok_or_else(|| {
305 web_error(format!(
306 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
307 ))
308 })?;
309 entry.renderer.fit_extents();
310 entry
311 .renderer
312 .render_current_scene()
313 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
314 Ok(())
315 })
316 }
317
318 pub(super) fn reset_surface_camera_impl(surface_id: u32) -> BuiltinResult<()> {
319 SURFACES.with(|slot| {
320 let mut map = slot.borrow_mut();
321 let entry = map.get_mut(&surface_id).ok_or_else(|| {
322 web_error(format!(
323 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
324 ))
325 })?;
326 entry.renderer.reset_camera_position();
327 entry
328 .renderer
329 .render_current_scene()
330 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
331 Ok(())
332 })
333 }
334
335 pub(super) fn get_surface_camera_state_impl(
336 surface_id: u32,
337 ) -> BuiltinResult<PlotSurfaceCameraState> {
338 SURFACES.with(|slot| {
339 let map = slot.borrow();
340 let entry = map.get(&surface_id).ok_or_else(|| {
341 web_error(format!(
342 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
343 ))
344 })?;
345 Ok(convert_camera_state(entry.renderer.camera_state()))
346 })
347 }
348
349 pub(super) fn set_surface_camera_state_impl(
350 surface_id: u32,
351 state: PlotSurfaceCameraState,
352 ) -> BuiltinResult<()> {
353 SURFACES.with(|slot| {
354 let mut map = slot.borrow_mut();
355 let entry = map.get_mut(&surface_id).ok_or_else(|| {
356 web_error(format!(
357 "Plotting surface {surface_id} not registered. Call createPlotSurface() first."
358 ))
359 })?;
360 entry
361 .renderer
362 .set_camera_state(&convert_camera_state_back(state));
363 entry
364 .renderer
365 .render_current_scene()
366 .map_err(|err| web_error(format!("Plotting failed: {err}")))?;
367 Ok(())
368 })
369 }
370
371 pub fn render_current_scene(handle: u32) -> BuiltinResult<()> {
372 debug!("plot-web: render_current_scene(handle={handle})");
373 let needs_autobind = SURFACES.with(|slot| {
377 let map = slot.borrow();
378 !map.values().any(|entry| entry.bound_handle == Some(handle))
379 });
380 if needs_autobind {
381 let maybe_unbound_surface = SURFACES.with(|slot| {
382 let map = slot.borrow();
383 map.iter()
384 .filter_map(|(surface_id, entry)| {
385 if entry.bound_handle.is_none() {
386 Some(*surface_id)
387 } else {
388 None
389 }
390 })
391 .min()
392 });
393 if let Some(surface_id) = maybe_unbound_surface {
394 let _ = bind_surface_to_figure_impl(surface_id, handle);
396 }
397 }
398
399 let surface_ids: Vec<u32> = SURFACES.with(|slot| {
401 slot.borrow()
402 .iter()
403 .filter_map(|(surface_id, entry)| {
404 if entry.bound_handle == Some(handle) {
405 Some(*surface_id)
406 } else {
407 None
408 }
409 })
410 .collect()
411 });
412 if surface_ids.is_empty() {
413 return Ok(());
415 }
416 for surface_id in surface_ids {
417 present_surface_impl(surface_id)?;
419 }
420 Ok(())
421 }
422
423 pub fn invalidate_surface_revisions() {
424 SURFACES.with(|slot| {
425 let mut map = slot.borrow_mut();
426 for entry in map.values_mut() {
427 entry.last_revision = None;
428 }
429 });
430 }
431
432 pub(super) use runmat_plot::web::WebRenderer as RendererType;
434}
435
436#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
437pub(crate) mod wasm {
438 use super::*;
439
440 pub struct RendererPlaceholder;
441
442 pub(super) fn install_surface_impl(
443 _surface_id: u32,
444 _renderer: RendererPlaceholder,
445 ) -> BuiltinResult<()> {
446 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
447 }
448
449 pub(super) fn detach_surface_impl(_surface_id: u32) {}
450
451 pub(super) fn clear_closed_figure_surfaces_impl(_handle: u32) -> BuiltinResult<()> {
452 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
453 }
454
455 pub fn web_renderer_ready() -> bool {
456 false
457 }
458
459 pub(super) use RendererPlaceholder as RendererType;
460
461 pub(super) fn resize_surface_impl(
462 _surface_id: u32,
463 _width: u32,
464 _height: u32,
465 _pixels_per_point: f32,
466 ) -> BuiltinResult<()> {
467 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
468 }
469
470 pub fn render_current_scene(_handle: u32) -> BuiltinResult<()> {
471 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
472 }
473
474 pub fn invalidate_surface_revisions() {}
475
476 pub(super) fn bind_surface_to_figure_impl(_surface_id: u32, _handle: u32) -> BuiltinResult<()> {
477 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
478 }
479
480 pub(super) fn present_surface_impl(_surface_id: u32) -> BuiltinResult<()> {
481 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
482 }
483
484 pub(super) fn present_figure_on_surface_impl(
485 _surface_id: u32,
486 _handle: u32,
487 ) -> BuiltinResult<()> {
488 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
489 }
490
491 pub(super) fn fit_surface_extents_impl(_surface_id: u32) -> BuiltinResult<()> {
492 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
493 }
494
495 pub(super) fn reset_surface_camera_impl(_surface_id: u32) -> BuiltinResult<()> {
496 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
497 }
498
499 pub(super) fn get_surface_camera_state_impl(
500 _surface_id: u32,
501 ) -> BuiltinResult<PlotSurfaceCameraState> {
502 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
503 }
504
505 pub(super) fn set_surface_camera_state_impl(
506 _surface_id: u32,
507 _state: PlotSurfaceCameraState,
508 ) -> BuiltinResult<()> {
509 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
510 }
511
512 pub(super) fn set_theme_config_impl(
513 _theme: runmat_plot::styling::PlotThemeConfig,
514 ) -> BuiltinResult<()> {
515 Err(web_error(ERR_PLOTTING_UNAVAILABLE))
516 }
517
518 pub(super) fn current_theme_config_impl() -> runmat_plot::styling::PlotThemeConfig {
519 runmat_plot::styling::PlotThemeConfig::default()
520 }
521}
522
523pub use wasm::invalidate_surface_revisions;
524pub use wasm::render_current_scene;
525pub use wasm::web_renderer_ready;
526
527pub fn install_surface(surface_id: u32, renderer: wasm::RendererType) -> BuiltinResult<()> {
528 wasm::install_surface_impl(surface_id, renderer)
529}
530
531pub fn detach_surface(surface_id: u32) {
532 wasm::detach_surface_impl(surface_id)
533}
534
535pub fn clear_closed_figure_surfaces(handle: u32) -> BuiltinResult<()> {
536 wasm::clear_closed_figure_surfaces_impl(handle)
537}
538
539pub fn resize_surface(
540 surface_id: u32,
541 width: u32,
542 height: u32,
543 pixels_per_point: f32,
544) -> BuiltinResult<()> {
545 wasm::resize_surface_impl(surface_id, width, height, pixels_per_point)
546}
547
548pub fn bind_surface_to_figure(surface_id: u32, handle: u32) -> BuiltinResult<()> {
549 wasm::bind_surface_to_figure_impl(surface_id, handle)
550}
551
552pub fn present_surface(surface_id: u32) -> BuiltinResult<()> {
553 wasm::present_surface_impl(surface_id)
554}
555
556#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
557pub fn handle_plot_surface_event(
558 surface_id: u32,
559 event: runmat_plot::core::PlotEvent,
560) -> BuiltinResult<()> {
561 wasm::handle_surface_event_impl(surface_id, event)
562}
563
564pub fn present_figure_on_surface(surface_id: u32, handle: u32) -> BuiltinResult<()> {
565 wasm::present_figure_on_surface_impl(surface_id, handle)
566}
567
568pub fn fit_surface_extents(surface_id: u32) -> BuiltinResult<()> {
569 wasm::fit_surface_extents_impl(surface_id)
570}
571
572pub fn reset_surface_camera(surface_id: u32) -> BuiltinResult<()> {
573 wasm::reset_surface_camera_impl(surface_id)
574}
575
576pub fn get_surface_camera_state(surface_id: u32) -> BuiltinResult<PlotSurfaceCameraState> {
577 wasm::get_surface_camera_state_impl(surface_id)
578}
579
580pub fn set_surface_camera_state(
581 surface_id: u32,
582 state: PlotSurfaceCameraState,
583) -> BuiltinResult<()> {
584 wasm::set_surface_camera_state_impl(surface_id, state)
585}
586
587pub fn set_plot_theme_config(theme: runmat_plot::styling::PlotThemeConfig) -> BuiltinResult<()> {
588 wasm::set_theme_config_impl(theme)
589}
590
591pub fn current_plot_theme_config() -> runmat_plot::styling::PlotThemeConfig {
592 wasm::current_theme_config_impl()
593}
594
595#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
596fn convert_camera_state(state: runmat_plot::web::PlotSurfaceCameraState) -> PlotSurfaceCameraState {
597 PlotSurfaceCameraState {
598 active_axes: state.active_axes,
599 axes: state
600 .axes
601 .into_iter()
602 .map(|camera| PlotCameraState {
603 position: camera.position,
604 target: camera.target,
605 up: camera.up,
606 zoom: camera.zoom,
607 aspect_ratio: camera.aspect_ratio,
608 projection: match camera.projection {
609 runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far } => {
610 PlotCameraProjection::Perspective { fov, near, far }
611 }
612 runmat_plot::web::PlotCameraProjection::Orthographic {
613 left,
614 right,
615 bottom,
616 top,
617 near,
618 far,
619 } => PlotCameraProjection::Orthographic {
620 left,
621 right,
622 bottom,
623 top,
624 near,
625 far,
626 },
627 },
628 })
629 .collect(),
630 }
631}
632
633#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
634fn convert_camera_state_back(
635 state: PlotSurfaceCameraState,
636) -> runmat_plot::web::PlotSurfaceCameraState {
637 runmat_plot::web::PlotSurfaceCameraState {
638 active_axes: state.active_axes,
639 axes: state
640 .axes
641 .into_iter()
642 .map(|camera| runmat_plot::web::PlotCameraState {
643 position: camera.position,
644 target: camera.target,
645 up: camera.up,
646 zoom: camera.zoom,
647 aspect_ratio: camera.aspect_ratio,
648 projection: match camera.projection {
649 PlotCameraProjection::Perspective { fov, near, far } => {
650 runmat_plot::web::PlotCameraProjection::Perspective { fov, near, far }
651 }
652 PlotCameraProjection::Orthographic {
653 left,
654 right,
655 bottom,
656 top,
657 near,
658 far,
659 } => runmat_plot::web::PlotCameraProjection::Orthographic {
660 left,
661 right,
662 bottom,
663 top,
664 near,
665 far,
666 },
667 },
668 })
669 .collect(),
670 }
671}
672
673