Skip to main content

runmat_runtime/builtins/plotting/
mod.rs

1//! Plotting builtins backed by the runmat-plot renderer.
2
3#[path = "core/common.rs"]
4pub(crate) mod common;
5#[path = "core/context.rs"]
6pub mod context;
7#[path = "core/engine.rs"]
8pub(crate) mod engine;
9#[path = "core/gpu_helpers.rs"]
10pub(crate) mod gpu_helpers;
11#[path = "core/perf.rs"]
12pub(crate) mod perf;
13#[path = "core/point.rs"]
14pub(crate) mod point;
15#[path = "core/properties.rs"]
16pub(crate) mod properties;
17#[path = "core/state.rs"]
18pub(crate) mod state;
19#[path = "core/style.rs"]
20pub(crate) mod style;
21#[path = "core/web.rs"]
22pub mod web;
23
24#[path = "type_resolvers.rs"]
25pub(crate) mod type_resolvers;
26
27#[path = "ops/area.rs"]
28pub(crate) mod area;
29#[path = "ops/bar.rs"]
30pub(crate) mod bar;
31#[path = "ops/caxis.rs"]
32pub(crate) mod caxis;
33#[path = "ops/clf.rs"]
34pub(crate) mod clf;
35#[path = "ops/clim.rs"]
36pub(crate) mod clim;
37#[path = "ops/close.rs"]
38pub(crate) mod close;
39#[path = "ops/cmds.rs"]
40pub(crate) mod cmds;
41#[path = "ops/contour.rs"]
42pub(crate) mod contour;
43#[path = "ops/contour3.rs"]
44pub(crate) mod contour3;
45#[path = "ops/contourf.rs"]
46pub(crate) mod contourf;
47#[path = "ops/drawnow.rs"]
48pub(crate) mod drawnow;
49#[path = "ops/errorbar.rs"]
50pub(crate) mod errorbar;
51#[path = "ops/figure.rs"]
52pub(crate) mod figure;
53#[path = "ops/fill3.rs"]
54pub(crate) mod fill3;
55#[path = "ops/gca.rs"]
56pub(crate) mod gca;
57#[path = "ops/gcf.rs"]
58pub(crate) mod gcf;
59#[path = "ops/get.rs"]
60pub(crate) mod get;
61#[path = "ops/heatmap.rs"]
62pub(crate) mod heatmap;
63#[path = "ops/hist.rs"]
64pub mod hist;
65#[path = "ops/histogram.rs"]
66pub(crate) mod histogram;
67#[path = "ops/hold.rs"]
68pub(crate) mod hold;
69#[path = "ops/image.rs"]
70pub(crate) mod image;
71#[path = "ops/imagesc.rs"]
72pub(crate) mod imagesc;
73#[path = "ops/imshow.rs"]
74pub(crate) mod imshow;
75#[path = "ops/isgraphics.rs"]
76pub(crate) mod isgraphics;
77#[path = "ops/ishandle.rs"]
78pub(crate) mod ishandle;
79#[path = "ops/legend.rs"]
80pub(crate) mod legend;
81#[path = "ops/loglog.rs"]
82pub(crate) mod loglog;
83#[path = "ops/mesh.rs"]
84pub(crate) mod mesh;
85#[path = "ops/meshc.rs"]
86pub(crate) mod meshc;
87#[path = "ops/common/mod.rs"]
88pub(crate) mod op_common;
89#[path = "ops/patch.rs"]
90pub(crate) mod patch;
91#[path = "ops/pie.rs"]
92pub(crate) mod pie;
93#[path = "ops/plot.rs"]
94pub(crate) mod plot;
95#[path = "ops/plot3.rs"]
96pub(crate) mod plot3;
97#[path = "ops/polarplot.rs"]
98pub(crate) mod polarplot;
99#[path = "ops/print.rs"]
100pub(crate) mod print;
101#[path = "ops/quiver.rs"]
102pub(crate) mod quiver;
103#[path = "ops/scatter.rs"]
104pub(crate) mod scatter;
105#[path = "ops/scatter3.rs"]
106pub(crate) mod scatter3;
107#[path = "ops/scatterplot.rs"]
108pub(crate) mod scatterplot;
109#[path = "ops/semilogx.rs"]
110pub(crate) mod semilogx;
111#[path = "ops/semilogy.rs"]
112pub(crate) mod semilogy;
113#[path = "ops/set.rs"]
114pub(crate) mod set;
115#[path = "ops/sgtitle.rs"]
116pub(crate) mod sgtitle;
117#[path = "ops/stairs.rs"]
118pub(crate) mod stairs;
119#[path = "ops/stem.rs"]
120pub(crate) mod stem;
121#[path = "ops/subplot.rs"]
122pub(crate) mod subplot;
123#[path = "ops/suptitle.rs"]
124pub(crate) mod suptitle;
125#[path = "ops/surf.rs"]
126pub(crate) mod surf;
127#[path = "ops/surfc.rs"]
128pub(crate) mod surfc;
129#[path = "ops/text.rs"]
130pub(crate) mod text;
131#[path = "ops/title.rs"]
132pub(crate) mod title;
133#[path = "ops/view.rs"]
134pub(crate) mod view;
135#[path = "ops/xlabel.rs"]
136pub(crate) mod xlabel;
137#[path = "ops/xlim.rs"]
138pub(crate) mod xlim;
139#[path = "ops/xline.rs"]
140pub(crate) mod xline;
141#[path = "ops/ylabel.rs"]
142pub(crate) mod ylabel;
143#[path = "ops/ylim.rs"]
144pub(crate) mod ylim;
145#[path = "ops/yline.rs"]
146pub(crate) mod yline;
147#[path = "ops/zlabel.rs"]
148pub(crate) mod zlabel;
149#[path = "ops/zlim.rs"]
150pub(crate) mod zlim;
151
152pub use perf::{set_scatter_target_points, set_surface_vertex_budget};
153pub use properties::resolve_plot_handle;
154pub use state::{
155    clear_figure, clone_figure, close_figure, configure_subplot, current_axes_state,
156    current_figure_handle, figure_handles, import_figure, install_figure_observer,
157    new_figure_handle, record_recent_figure, reset_hold_state_for_run, reset_plot_state,
158    reset_recent_figures, select_axes_for_figure, select_figure, set_hold, take_recent_figures,
159    FigureAxesState, FigureError, FigureEventKind, FigureEventView, FigureHandle, HoldMode,
160};
161#[cfg(all(feature = "plot-core", target_arch = "wasm32"))]
162use std::cell::RefCell;
163use std::collections::HashMap;
164#[cfg(feature = "plot-core")]
165use std::sync::atomic::{AtomicU64, Ordering};
166use std::sync::{Mutex, OnceLock};
167use web::present_figure_on_surface as web_present_figure_on_surface;
168use web::present_geometry_scene_on_surface as web_present_geometry_scene_on_surface;
169pub use web::{
170    bind_surface_to_figure, clear_closed_figure_surfaces, detach_surface, fit_surface_extents,
171    get_surface_camera_state, install_surface, invalidate_surface_revisions,
172    pick_geometry_scene_region, present_surface, render_current_scene, reset_surface_camera,
173    resize_surface, set_geometry_scene_presentation, set_plot_theme_config,
174    set_surface_camera_state, web_renderer_ready, PlotCameraProjection, PlotCameraState,
175    PlotSurfaceCameraState,
176};
177
178#[doc(hidden)]
179pub use state::PlotTestLockGuard;
180
181#[doc(hidden)]
182pub fn lock_plot_test_context() -> PlotTestLockGuard {
183    state::lock_plot_test_registry()
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum RuntimePlottingMode {
188    Auto,
189    Interactive,
190    Static,
191}
192
193#[cfg(feature = "gui")]
194pub fn set_runtime_plotting_mode(mode: RuntimePlottingMode) {
195    let mapped = match mode {
196        RuntimePlottingMode::Auto => engine::native::RuntimePlottingMode::Auto,
197        RuntimePlottingMode::Interactive => engine::native::RuntimePlottingMode::Interactive,
198        RuntimePlottingMode::Static => engine::native::RuntimePlottingMode::Static,
199    };
200    engine::native::set_runtime_plotting_mode(mapped);
201}
202
203#[cfg(not(feature = "gui"))]
204pub fn set_runtime_plotting_mode(_mode: RuntimePlottingMode) {}
205
206#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
207pub use web::{handle_plot_surface_event, take_surface_host_actions, PlotSurfaceHostAction};
208
209pub(crate) fn plotting_error(builtin: &str, message: impl Into<String>) -> crate::RuntimeError {
210    crate::build_runtime_error(message)
211        .with_builtin(builtin)
212        .build()
213}
214
215pub(crate) fn plotting_error_with_source(
216    builtin: &str,
217    message: impl Into<String>,
218    source: impl std::error::Error + Send + Sync + 'static,
219) -> crate::RuntimeError {
220    crate::build_runtime_error(message)
221        .with_builtin(builtin)
222        .with_source(source)
223        .build()
224}
225
226#[cfg(feature = "plot-core")]
227pub fn export_figure_scene(handle: FigureHandle) -> crate::BuiltinResult<Option<Vec<u8>>> {
228    let Some(figure) = clone_figure(handle) else {
229        return Ok(None);
230    };
231    let scene = runmat_plot::event::FigureScene::capture(&figure);
232    crate::replay::export_figure_scene_payload(&scene).map(Some)
233}
234
235#[cfg(feature = "plot-core")]
236pub fn import_figure_scene(bytes: &[u8]) -> crate::BuiltinResult<Option<FigureHandle>> {
237    let scene = crate::replay::import_figure_scene_payload(bytes)?;
238    let figure = scene.into_figure().map_err(|err| {
239        crate::replay_error_with_source(
240            crate::ReplayErrorKind::ImportRejected,
241            "invalid figure scene content",
242            std::io::Error::new(std::io::ErrorKind::InvalidData, err),
243        )
244    })?;
245    let handle = import_figure(figure);
246    register_imported_figure(handle.as_u32());
247    Ok(Some(handle))
248}
249
250#[cfg(feature = "plot-core")]
251pub fn import_geometry_scene_payload(bytes: &[u8]) -> crate::BuiltinResult<Option<u32>> {
252    let scene = crate::replay::import_figure_scene_payload(bytes)?;
253    let hash = geometry_scene_payload_hash(bytes);
254    let scene_id = format!("geometry-scene-payload:{hash:016x}");
255    let scene = scene.into_geometry_scene(scene_id, hash).map_err(|err| {
256        crate::replay_error_with_source(
257            crate::ReplayErrorKind::ImportRejected,
258            "invalid geometry scene content",
259            std::io::Error::new(std::io::ErrorKind::InvalidData, err),
260        )
261    })?;
262    import_geometry_scene(scene).map(Some)
263}
264
265#[cfg(feature = "plot-core")]
266pub async fn import_figure_scene_async(bytes: &[u8]) -> crate::BuiltinResult<Option<FigureHandle>> {
267    let scene = crate::replay::import_figure_scene_payload_async(bytes).await?;
268    let figure = scene.into_figure().map_err(|err| {
269        crate::replay_error_with_source(
270            crate::ReplayErrorKind::ImportRejected,
271            "invalid figure scene content",
272            std::io::Error::new(std::io::ErrorKind::InvalidData, err),
273        )
274    })?;
275    let handle = import_figure(figure);
276    register_imported_figure(handle.as_u32());
277    Ok(Some(handle))
278}
279
280#[cfg(feature = "plot-core")]
281pub async fn import_figure_scene_from_path_async(
282    path: &str,
283) -> crate::BuiltinResult<Option<FigureHandle>> {
284    let bytes = runmat_filesystem::read_async(path).await.map_err(|err| {
285        crate::replay_error_with_source(
286            crate::ReplayErrorKind::ImportRejected,
287            format!("failed to read figure scene payload '{path}'"),
288            err,
289        )
290    })?;
291    import_figure_scene_async(&bytes).await
292}
293
294#[cfg(feature = "plot-core")]
295pub fn import_geometry_scene(scene: runmat_plot::GeometryScene) -> crate::BuiltinResult<u32> {
296    let handle = NEXT_GEOMETRY_SCENE_HANDLE.fetch_add(1, Ordering::Relaxed) as u32;
297    insert_geometry_scene(handle, scene)?;
298    register_imported_geometry_scene(handle);
299    Ok(handle)
300}
301
302#[cfg(feature = "plot-core")]
303pub fn clone_geometry_scene(handle: u32) -> Option<runmat_plot::GeometryScene> {
304    get_geometry_scene(handle)
305}
306
307#[cfg(feature = "plot-core")]
308pub fn append_geometry_scene_chunks(
309    handle: u32,
310    chunks: Vec<runmat_plot::GeometrySceneChunk>,
311    overlay: Option<runmat_plot::GeometrySceneOverlay>,
312) -> crate::BuiltinResult<()> {
313    with_geometry_scene_mut(handle, |scene| {
314        if !chunks.is_empty() {
315            scene.append_chunks(chunks);
316        }
317        if let Some(overlay) = overlay {
318            let merged = merge_geometry_scene_overlay(scene, overlay);
319            scene.set_overlay(merged);
320        }
321    })
322}
323
324#[cfg(feature = "plot-core")]
325pub fn close_geometry_scene(handle: u32) -> bool {
326    remove_geometry_scene(handle)
327}
328
329#[cfg(feature = "plot-core")]
330pub fn export_geometry_scene(handle: u32) -> crate::BuiltinResult<Option<Vec<u8>>> {
331    let Some(scene) = clone_geometry_scene(handle) else {
332        return Ok(None);
333    };
334    let scene = runmat_plot::event::FigureScene::from_geometry_scene(&scene);
335    crate::replay::export_figure_scene_payload(&scene).map(Some)
336}
337
338#[cfg(feature = "plot-core")]
339pub fn present_geometry_scene_on_surface(surface_id: u32, handle: u32) -> crate::BuiltinResult<()> {
340    let Some(scene) = clone_geometry_scene(handle) else {
341        return Err(crate::build_runtime_error(format!(
342            "geometry scene handle {handle} does not exist"
343        ))
344        .with_builtin("plot")
345        .build());
346    };
347    web_present_geometry_scene_on_surface(surface_id, handle, scene)?;
348    if take_imported_geometry_scene(handle) {
349        let _ = reset_surface_camera(surface_id);
350    }
351    Ok(())
352}
353
354pub fn present_figure_on_surface(surface_id: u32, handle: u32) -> crate::BuiltinResult<()> {
355    web_present_figure_on_surface(surface_id, handle)?;
356    if take_imported_figure(handle) {
357        let _ = reset_surface_camera(surface_id);
358    }
359    Ok(())
360}
361
362#[cfg(feature = "plot-core")]
363pub fn import_runtime_figure(figure: runmat_plot::plots::Figure) -> u32 {
364    let handle = state::import_figure(figure);
365    register_imported_figure(handle.as_u32());
366    handle.as_u32()
367}
368
369type ImportedFigureRegistry = Mutex<HashMap<u32, ()>>;
370#[cfg(all(feature = "plot-core", not(target_arch = "wasm32")))]
371type GeometrySceneRegistry = Mutex<HashMap<u32, runmat_plot::GeometryScene>>;
372#[cfg(feature = "plot-core")]
373type ImportedGeometrySceneRegistry = Mutex<HashMap<u32, ()>>;
374
375#[cfg(feature = "plot-core")]
376static NEXT_GEOMETRY_SCENE_HANDLE: AtomicU64 = AtomicU64::new(1);
377
378#[cfg(all(feature = "plot-core", target_arch = "wasm32"))]
379thread_local! {
380    static GEOMETRY_SCENE_REGISTRY: RefCell<HashMap<u32, runmat_plot::GeometryScene>> =
381        RefCell::new(HashMap::new());
382}
383
384#[cfg(all(feature = "plot-core", not(target_arch = "wasm32")))]
385fn geometry_scene_registry() -> &'static GeometrySceneRegistry {
386    static REGISTRY: OnceLock<GeometrySceneRegistry> = OnceLock::new();
387    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
388}
389
390#[cfg(all(feature = "plot-core", not(target_arch = "wasm32")))]
391fn insert_geometry_scene(
392    handle: u32,
393    scene: runmat_plot::GeometryScene,
394) -> crate::BuiltinResult<()> {
395    let mut guard = geometry_scene_registry().lock().map_err(|_| {
396        crate::build_runtime_error("geometry scene registry lock poisoned")
397            .with_builtin("plot")
398            .build()
399    })?;
400    guard.insert(handle, scene);
401    Ok(())
402}
403
404#[cfg(all(feature = "plot-core", target_arch = "wasm32"))]
405fn insert_geometry_scene(
406    handle: u32,
407    scene: runmat_plot::GeometryScene,
408) -> crate::BuiltinResult<()> {
409    GEOMETRY_SCENE_REGISTRY.with(|registry| {
410        registry.borrow_mut().insert(handle, scene);
411    });
412    Ok(())
413}
414
415#[cfg(all(feature = "plot-core", not(target_arch = "wasm32")))]
416fn get_geometry_scene(handle: u32) -> Option<runmat_plot::GeometryScene> {
417    geometry_scene_registry().lock().ok()?.get(&handle).cloned()
418}
419
420#[cfg(all(feature = "plot-core", target_arch = "wasm32"))]
421fn get_geometry_scene(handle: u32) -> Option<runmat_plot::GeometryScene> {
422    GEOMETRY_SCENE_REGISTRY.with(|registry| registry.borrow().get(&handle).cloned())
423}
424
425#[cfg(all(feature = "plot-core", not(target_arch = "wasm32")))]
426fn with_geometry_scene_mut(
427    handle: u32,
428    update: impl FnOnce(&mut runmat_plot::GeometryScene),
429) -> crate::BuiltinResult<()> {
430    let mut guard = geometry_scene_registry().lock().map_err(|_| {
431        crate::build_runtime_error("geometry scene registry lock poisoned")
432            .with_builtin("plot")
433            .build()
434    })?;
435    let scene = guard.get_mut(&handle).ok_or_else(|| {
436        crate::build_runtime_error(format!("geometry scene handle {handle} does not exist"))
437            .with_builtin("plot")
438            .build()
439    })?;
440    update(scene);
441    Ok(())
442}
443
444#[cfg(all(feature = "plot-core", target_arch = "wasm32"))]
445fn with_geometry_scene_mut(
446    handle: u32,
447    update: impl FnOnce(&mut runmat_plot::GeometryScene),
448) -> crate::BuiltinResult<()> {
449    GEOMETRY_SCENE_REGISTRY.with(|registry| {
450        let mut registry = registry.borrow_mut();
451        let scene = registry.get_mut(&handle).ok_or_else(|| {
452            crate::build_runtime_error(format!("geometry scene handle {handle} does not exist"))
453                .with_builtin("plot")
454                .build()
455        })?;
456        update(scene);
457        Ok(())
458    })
459}
460
461#[cfg(all(feature = "plot-core", not(target_arch = "wasm32")))]
462fn remove_geometry_scene(handle: u32) -> bool {
463    geometry_scene_registry()
464        .lock()
465        .ok()
466        .and_then(|mut guard| guard.remove(&handle))
467        .is_some()
468}
469
470#[cfg(all(feature = "plot-core", target_arch = "wasm32"))]
471fn remove_geometry_scene(handle: u32) -> bool {
472    GEOMETRY_SCENE_REGISTRY.with(|registry| registry.borrow_mut().remove(&handle).is_some())
473}
474
475#[cfg(feature = "plot-core")]
476fn geometry_scene_payload_hash(bytes: &[u8]) -> u64 {
477    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
478    const FNV_PRIME: u64 = 0x100000001b3;
479    let mut hash = FNV_OFFSET_BASIS;
480    for byte in bytes {
481        hash ^= u64::from(*byte);
482        hash = hash.wrapping_mul(FNV_PRIME);
483    }
484    hash
485}
486
487#[cfg(feature = "plot-core")]
488fn merge_geometry_scene_overlay(
489    scene: &runmat_plot::GeometryScene,
490    incoming: runmat_plot::GeometrySceneOverlay,
491) -> runmat_plot::GeometrySceneOverlay {
492    let Some(mut current) = scene.overlay.clone() else {
493        let mut overlay = incoming;
494        overlay.vertex_count = scene.vertex_count();
495        overlay.triangle_count = scene.triangle_count();
496        return overlay;
497    };
498
499    current.status = incoming.status;
500    current.quality_label = incoming.quality_label.or(current.quality_label);
501    current.format = incoming.format.or(current.format);
502    current.source_label = incoming.source_label.or(current.source_label);
503    current.allow_create_fea_study =
504        current.allow_create_fea_study || incoming.allow_create_fea_study;
505    current.byte_count = incoming.byte_count.or(current.byte_count);
506    current.mesh_count = current.mesh_count.max(incoming.mesh_count);
507    current.vertex_count = scene.vertex_count();
508    current.triangle_count = scene.triangle_count();
509    current.progress_percent = incoming.progress_percent;
510
511    if current.assembly_nodes.is_empty() {
512        current.assembly_nodes = incoming.assembly_nodes;
513    }
514
515    merge_region_summaries(&mut current.regions, incoming.regions);
516    current.region_count = current.regions.len();
517    current.mapped_region_count = current.region_count;
518    merge_warnings(&mut current.warnings, incoming.warnings);
519    current
520}
521
522#[cfg(feature = "plot-core")]
523fn merge_region_summaries(
524    current: &mut Vec<runmat_plot::GeometrySceneRegionSummary>,
525    incoming: Vec<runmat_plot::GeometrySceneRegionSummary>,
526) {
527    let mut positions = HashMap::<String, usize>::with_capacity(current.len() + incoming.len());
528    for (index, region) in current.iter().enumerate() {
529        positions.insert(region.region_id.clone(), index);
530    }
531    for region in incoming {
532        if let Some(index) = positions.get(&region.region_id).copied() {
533            current[index].triangle_count = current[index]
534                .triangle_count
535                .saturating_add(region.triangle_count);
536        } else {
537            positions.insert(region.region_id.clone(), current.len());
538            current.push(region);
539        }
540    }
541}
542
543#[cfg(feature = "plot-core")]
544fn merge_warnings(current: &mut Vec<String>, incoming: Vec<String>) {
545    for warning in incoming {
546        if !current.iter().any(|item| item == &warning) {
547            current.push(warning);
548        }
549    }
550}
551
552fn imported_figure_registry() -> &'static ImportedFigureRegistry {
553    static REGISTRY: OnceLock<ImportedFigureRegistry> = OnceLock::new();
554    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
555}
556
557fn register_imported_figure(handle: u32) {
558    if let Ok(mut map) = imported_figure_registry().lock() {
559        map.insert(handle, ());
560    }
561}
562
563fn take_imported_figure(handle: u32) -> bool {
564    imported_figure_registry()
565        .lock()
566        .ok()
567        .and_then(|mut map| map.remove(&handle))
568        .is_some()
569}
570
571#[cfg(feature = "plot-core")]
572fn imported_geometry_scene_registry() -> &'static ImportedGeometrySceneRegistry {
573    static REGISTRY: OnceLock<ImportedGeometrySceneRegistry> = OnceLock::new();
574    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
575}
576
577#[cfg(feature = "plot-core")]
578pub fn register_imported_geometry_scene(handle: u32) {
579    if let Ok(mut map) = imported_geometry_scene_registry().lock() {
580        map.insert(handle, ());
581    }
582}
583
584#[cfg(feature = "plot-core")]
585fn take_imported_geometry_scene(handle: u32) -> bool {
586    imported_geometry_scene_registry()
587        .lock()
588        .ok()
589        .and_then(|mut map| map.remove(&handle))
590        .is_some()
591}
592
593#[cfg(feature = "plot-core")]
594pub use engine::{
595    render_figure_png_bytes, render_figure_png_bytes_with_axes_cameras,
596    render_figure_png_bytes_with_camera, render_figure_rgba_bytes,
597    render_figure_rgba_bytes_with_axes_cameras, render_figure_rgba_bytes_with_camera,
598    render_figure_snapshot, render_figure_snapshot_with_camera_state,
599    render_geometry_scene_snapshot,
600};
601
602pub mod ops {
603    pub use super::hist;
604}
605
606#[cfg(test)]
607pub(crate) mod tests {
608    use super::state;
609
610    pub(crate) fn ensure_plot_test_env() {
611        state::disable_rendering_for_tests();
612    }
613
614    pub(crate) fn lock_plot_registry() -> state::PlotTestLockGuard {
615        state::lock_plot_test_registry()
616    }
617}