Skip to main content

runmat_runtime/builtins/plotting/core/
state.rs

1use glam::Vec4;
2use once_cell::sync::OnceCell;
3use runmat_builtins::Tensor;
4use runmat_plot::plots::{
5    surface::ColorMap, surface::ShadingMode, Figure, LegendStyle, LineStyle, PlotElement, TextStyle,
6};
7use runmat_thread_local::runmat_thread_local;
8use std::cell::RefCell;
9use std::collections::{hash_map::Entry, HashMap, HashSet};
10use std::ops::{Deref, DerefMut};
11#[cfg(not(target_arch = "wasm32"))]
12use std::sync::MutexGuard;
13#[cfg(test)]
14use std::sync::Once;
15use std::sync::{Arc, Mutex};
16use thiserror::Error;
17
18use super::common::{default_figure, ERR_PLOTTING_UNAVAILABLE};
19#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
20use super::engine::render_figure;
21use super::web::current_plot_theme_config;
22use super::{plotting_error, plotting_error_with_source};
23
24use crate::builtins::common::map_control_flow_with_builtin;
25use crate::{BuiltinResult, RuntimeError};
26
27type AxisLimitSnapshot = (Option<(f64, f64)>, Option<(f64, f64)>);
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub struct FigureHandle(u32);
31
32impl FigureHandle {
33    pub fn as_u32(self) -> u32 {
34        self.0
35    }
36
37    fn next(self) -> FigureHandle {
38        FigureHandle(self.0 + 1)
39    }
40}
41
42impl From<u32> for FigureHandle {
43    fn from(value: u32) -> Self {
44        FigureHandle(value.max(1))
45    }
46}
47
48impl Default for FigureHandle {
49    fn default() -> Self {
50        FigureHandle(1)
51    }
52}
53
54const DEFAULT_LINE_STYLE_ORDER: [LineStyle; 1] = [LineStyle::Solid];
55
56#[derive(Clone)]
57struct LineStyleCycle {
58    order: Vec<LineStyle>,
59    cursor: usize,
60}
61
62impl Default for LineStyleCycle {
63    fn default() -> Self {
64        Self {
65            order: DEFAULT_LINE_STYLE_ORDER.to_vec(),
66            cursor: 0,
67        }
68    }
69}
70
71impl LineStyleCycle {
72    fn next(&mut self) -> LineStyle {
73        if self.order.is_empty() {
74            self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
75        }
76        let style = self.order[self.cursor % self.order.len()];
77        self.cursor = (self.cursor + 1) % self.order.len();
78        style
79    }
80
81    fn set_order(&mut self, order: &[LineStyle]) {
82        if order.is_empty() {
83            self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
84        } else {
85            self.order = order.to_vec();
86        }
87        self.cursor = 0;
88    }
89
90    fn reset_cursor(&mut self) {
91        self.cursor = 0;
92    }
93}
94
95#[derive(Clone, Default)]
96struct LineColorCycle {
97    cursor: usize,
98}
99
100impl LineColorCycle {
101    fn next(&mut self) -> Vec4 {
102        let color = line_color_for_series_index(self.cursor);
103        self.cursor = self.cursor.saturating_add(1);
104        color
105    }
106
107    fn reset_cursor(&mut self) {
108        self.cursor = 0;
109    }
110}
111
112#[derive(Default)]
113struct FigureState {
114    figure: Figure,
115    active_axes: usize,
116    hold_per_axes: HashMap<usize, bool>,
117    line_style_cycles: HashMap<usize, LineStyleCycle>,
118    line_color_cycles: HashMap<usize, LineColorCycle>,
119    revision: u64,
120}
121
122impl FigureState {
123    fn new(handle: FigureHandle) -> Self {
124        let title = format!("Figure {}", handle.as_u32());
125        let figure = default_figure(&title, "X", "Y");
126        Self {
127            figure,
128            active_axes: 0,
129            hold_per_axes: HashMap::new(),
130            line_style_cycles: HashMap::new(),
131            line_color_cycles: HashMap::new(),
132            revision: 0,
133        }
134    }
135
136    fn hold(&self) -> bool {
137        *self.hold_per_axes.get(&self.active_axes).unwrap_or(&false)
138    }
139
140    fn set_hold(&mut self, hold: bool) {
141        self.hold_per_axes.insert(self.active_axes, hold);
142    }
143
144    fn cycle_for_axes_mut(&mut self, axes_index: usize) -> &mut LineStyleCycle {
145        self.line_style_cycles.entry(axes_index).or_default()
146    }
147
148    fn color_cycle_for_axes_mut(&mut self, axes_index: usize) -> &mut LineColorCycle {
149        self.line_color_cycles.entry(axes_index).or_default()
150    }
151
152    fn reset_cycle(&mut self, axes_index: usize) {
153        if let Some(cycle) = self.line_style_cycles.get_mut(&axes_index) {
154            cycle.reset_cursor();
155        }
156        if let Some(cycle) = self.line_color_cycles.get_mut(&axes_index) {
157            cycle.reset_cursor();
158        }
159    }
160}
161
162struct ActiveAxesContext {
163    axes_index: usize,
164    style_cycle_ptr: *mut LineStyleCycle,
165    color_cycle_ptr: *mut LineColorCycle,
166}
167
168struct AxesContextGuard {
169    _private: (),
170}
171
172impl AxesContextGuard {
173    fn install(state: &mut FigureState, axes_index: usize) -> Self {
174        let style_cycle_ptr = state.cycle_for_axes_mut(axes_index) as *mut LineStyleCycle;
175        let color_cycle_ptr = state.color_cycle_for_axes_mut(axes_index) as *mut LineColorCycle;
176        ACTIVE_AXES_CONTEXT.with(|ctx| {
177            debug_assert!(
178                ctx.borrow().is_none(),
179                "plot axes context already installed"
180            );
181            ctx.borrow_mut().replace(ActiveAxesContext {
182                axes_index,
183                style_cycle_ptr,
184                color_cycle_ptr,
185            });
186        });
187        Self { _private: () }
188    }
189}
190
191impl Drop for AxesContextGuard {
192    fn drop(&mut self) {
193        ACTIVE_AXES_CONTEXT.with(|ctx| {
194            ctx.borrow_mut().take();
195        });
196    }
197}
198
199fn with_active_style_cycle<R>(
200    axes_index: usize,
201    f: impl FnOnce(&mut LineStyleCycle) -> R,
202) -> Option<R> {
203    ACTIVE_AXES_CONTEXT.with(|ctx| {
204        let guard = ctx.borrow();
205        let active = guard.as_ref()?;
206        if active.axes_index != axes_index {
207            return None;
208        }
209        let cycle = unsafe { &mut *active.style_cycle_ptr };
210        Some(f(cycle))
211    })
212}
213
214fn with_active_color_cycle<R>(
215    axes_index: usize,
216    f: impl FnOnce(&mut LineColorCycle) -> R,
217) -> Option<R> {
218    ACTIVE_AXES_CONTEXT.with(|ctx| {
219        let guard = ctx.borrow();
220        let active = guard.as_ref()?;
221        if active.axes_index != axes_index {
222            return None;
223        }
224        let cycle = unsafe { &mut *active.color_cycle_ptr };
225        Some(f(cycle))
226    })
227}
228
229struct PlotRegistry {
230    current: FigureHandle,
231    next_handle: FigureHandle,
232    figures: HashMap<FigureHandle, FigureState>,
233    next_plot_child_handle: u64,
234    plot_children: HashMap<u64, PlotChildHandleState>,
235}
236
237#[derive(Clone, Debug)]
238pub struct HistogramHandleState {
239    pub figure: FigureHandle,
240    pub axes_index: usize,
241    pub plot_index: usize,
242    pub bin_edges: Vec<f64>,
243    pub raw_counts: Vec<f64>,
244    pub normalization: String,
245    pub display_name: Option<String>,
246}
247
248#[derive(Clone, Debug)]
249pub struct StemHandleState {
250    pub figure: FigureHandle,
251    pub axes_index: usize,
252    pub plot_index: usize,
253}
254
255#[derive(Clone, Debug)]
256pub struct SimplePlotHandleState {
257    pub figure: FigureHandle,
258    pub axes_index: usize,
259    pub plot_index: usize,
260}
261
262#[derive(Clone, Debug)]
263pub struct ErrorBarHandleState {
264    pub figure: FigureHandle,
265    pub axes_index: usize,
266    pub plot_index: usize,
267}
268
269#[derive(Clone, Debug)]
270pub struct QuiverHandleState {
271    pub figure: FigureHandle,
272    pub axes_index: usize,
273    pub plot_index: usize,
274}
275
276#[derive(Clone, Debug)]
277pub struct ImageHandleState {
278    pub figure: FigureHandle,
279    pub axes_index: usize,
280    pub plot_index: usize,
281}
282
283#[derive(Clone, Debug)]
284pub struct HeatmapHandleState {
285    pub figure: FigureHandle,
286    pub axes_index: usize,
287    pub plot_index: usize,
288    pub x_labels: Vec<String>,
289    pub y_labels: Vec<String>,
290    pub color_data: Tensor,
291}
292
293#[derive(Clone, Debug)]
294pub struct AreaHandleState {
295    pub figure: FigureHandle,
296    pub axes_index: usize,
297    pub plot_index: usize,
298}
299
300#[derive(Clone, Debug)]
301pub struct TextAnnotationHandleState {
302    pub figure: FigureHandle,
303    pub axes_index: usize,
304    pub annotation_index: usize,
305}
306
307#[derive(Clone, Debug)]
308pub enum PlotChildHandleState {
309    Histogram(HistogramHandleState),
310    Line(SimplePlotHandleState),
311    Scatter(SimplePlotHandleState),
312    Bar(SimplePlotHandleState),
313    Stem(StemHandleState),
314    ErrorBar(ErrorBarHandleState),
315    Stairs(SimplePlotHandleState),
316    Quiver(QuiverHandleState),
317    Image(ImageHandleState),
318    Heatmap(HeatmapHandleState),
319    Area(AreaHandleState),
320    Surface(SimplePlotHandleState),
321    Patch(SimplePlotHandleState),
322    Line3(SimplePlotHandleState),
323    Scatter3(SimplePlotHandleState),
324    Contour(SimplePlotHandleState),
325    ContourFill(SimplePlotHandleState),
326    ReferenceLine(SimplePlotHandleState),
327    Pie(SimplePlotHandleState),
328    Text(TextAnnotationHandleState),
329}
330
331impl Default for PlotRegistry {
332    fn default() -> Self {
333        Self {
334            current: FigureHandle::default(),
335            next_handle: FigureHandle::default().next(),
336            figures: HashMap::new(),
337            next_plot_child_handle: 1u64 << 40,
338            plot_children: HashMap::new(),
339        }
340    }
341}
342
343#[cfg(not(target_arch = "wasm32"))]
344static REGISTRY: OnceCell<Mutex<PlotRegistry>> = OnceCell::new();
345
346#[cfg(test)]
347static TEST_PLOT_REGISTRY_LOCK: Mutex<()> = Mutex::new(());
348
349#[cfg(test)]
350thread_local! {
351    static TEST_PLOT_OUTER_LOCK_HELD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
352}
353
354#[cfg(test)]
355pub(crate) struct PlotTestLockGuard {
356    _guard: std::sync::MutexGuard<'static, ()>,
357}
358
359#[cfg(test)]
360impl Drop for PlotTestLockGuard {
361    fn drop(&mut self) {
362        TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(false));
363    }
364}
365
366#[cfg(test)]
367pub(crate) fn lock_plot_test_registry() -> PlotTestLockGuard {
368    let guard = TEST_PLOT_REGISTRY_LOCK
369        .lock()
370        .unwrap_or_else(|e| e.into_inner());
371    TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(true));
372    PlotTestLockGuard { _guard: guard }
373}
374
375#[cfg(target_arch = "wasm32")]
376runmat_thread_local! {
377    static REGISTRY: RefCell<PlotRegistry> = RefCell::new(PlotRegistry::default());
378}
379
380#[cfg(not(target_arch = "wasm32"))]
381type RegistryBackendGuard<'a> = MutexGuard<'a, PlotRegistry>;
382#[cfg(target_arch = "wasm32")]
383type RegistryBackendGuard<'a> = std::cell::RefMut<'a, PlotRegistry>;
384
385struct PlotRegistryGuard<'a> {
386    inner: RegistryBackendGuard<'a>,
387    #[cfg(test)]
388    _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
389}
390
391impl<'a> PlotRegistryGuard<'a> {
392    #[cfg(test)]
393    fn new(
394        inner: RegistryBackendGuard<'a>,
395        _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
396    ) -> Self {
397        Self { inner, _test_lock }
398    }
399
400    #[cfg(not(test))]
401    fn new(inner: RegistryBackendGuard<'a>) -> Self {
402        Self { inner }
403    }
404}
405
406impl<'a> Deref for PlotRegistryGuard<'a> {
407    type Target = PlotRegistry;
408
409    fn deref(&self) -> &Self::Target {
410        &self.inner
411    }
412}
413
414impl<'a> DerefMut for PlotRegistryGuard<'a> {
415    fn deref_mut(&mut self) -> &mut Self::Target {
416        &mut self.inner
417    }
418}
419
420const AXES_INDEX_BITS: u32 = 20;
421const AXES_INDEX_MASK: u64 = (1 << AXES_INDEX_BITS) - 1;
422const OBJECT_KIND_BITS: u32 = 4;
423const OBJECT_KIND_MASK: u64 = (1 << OBJECT_KIND_BITS) - 1;
424
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426pub enum PlotObjectKind {
427    Title = 1,
428    XLabel = 2,
429    YLabel = 3,
430    ZLabel = 4,
431    Legend = 5,
432    SuperTitle = 6,
433}
434
435impl PlotObjectKind {
436    fn from_u64(value: u64) -> Option<Self> {
437        match value {
438            1 => Some(Self::Title),
439            2 => Some(Self::XLabel),
440            3 => Some(Self::YLabel),
441            4 => Some(Self::ZLabel),
442            5 => Some(Self::Legend),
443            6 => Some(Self::SuperTitle),
444            _ => None,
445        }
446    }
447}
448
449#[derive(Debug, Error)]
450pub enum FigureError {
451    #[error("figure handle {0} does not exist")]
452    InvalidHandle(u32),
453    #[error("subplot grid dimensions must be positive (rows={rows}, cols={cols})")]
454    InvalidSubplotGrid { rows: usize, cols: usize },
455    #[error("subplot index {index} is out of range for a {rows}x{cols} grid")]
456    InvalidSubplotIndex {
457        rows: usize,
458        cols: usize,
459        index: usize,
460    },
461    #[error("invalid axes handle")]
462    InvalidAxesHandle,
463    #[error("invalid plot object handle")]
464    InvalidPlotObjectHandle,
465    #[error("failed to render figure snapshot: {source}")]
466    RenderFailure {
467        #[source]
468        source: Box<dyn std::error::Error + Send + Sync>,
469    },
470}
471
472fn map_figure_error(builtin: &'static str, err: FigureError) -> RuntimeError {
473    let message = format!("{builtin}: {err}");
474    plotting_error_with_source(builtin, message, err)
475}
476
477pub(crate) fn clear_figure_with_builtin(
478    builtin: &'static str,
479    target: Option<FigureHandle>,
480) -> BuiltinResult<FigureHandle> {
481    clear_figure(target).map_err(|err| map_figure_error(builtin, err))
482}
483
484pub(crate) fn close_figure_with_builtin(
485    builtin: &'static str,
486    target: Option<FigureHandle>,
487) -> BuiltinResult<FigureHandle> {
488    close_figure(target).map_err(|err| map_figure_error(builtin, err))
489}
490
491pub fn set_grid_enabled(enabled: bool) {
492    let (handle, figure_clone) = {
493        let mut reg = registry();
494        let handle = reg.current;
495        let state = get_state_mut(&mut reg, handle);
496        let axes = state.active_axes;
497        state.figure.set_axes_grid_enabled(axes, enabled);
498        state.revision = state.revision.wrapping_add(1);
499        (handle, state.figure.clone())
500    };
501    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
502}
503
504pub fn set_grid_enabled_for_axes(
505    handle: FigureHandle,
506    axes_index: usize,
507    enabled: bool,
508) -> Result<(), FigureError> {
509    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
510        state.figure.set_axes_grid_enabled(axes_index, enabled);
511    })?;
512    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
513    Ok(())
514}
515
516pub fn toggle_grid() -> bool {
517    let (handle, figure_clone, enabled) = {
518        let mut reg = registry();
519        let handle = reg.current;
520        let state = get_state_mut(&mut reg, handle);
521        let axes = state.active_axes;
522        let next = !state
523            .figure
524            .axes_metadata(axes)
525            .map(|m| m.grid_enabled)
526            .unwrap_or(true);
527        state.figure.set_axes_grid_enabled(axes, next);
528        state.revision = state.revision.wrapping_add(1);
529        (handle, state.figure.clone(), next)
530    };
531    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
532    enabled
533}
534
535pub fn set_box_enabled(enabled: bool) {
536    let (handle, figure_clone) = {
537        let mut reg = registry();
538        let handle = reg.current;
539        let state = get_state_mut(&mut reg, handle);
540        let axes = state.active_axes;
541        state.figure.set_axes_box_enabled(axes, enabled);
542        state.revision = state.revision.wrapping_add(1);
543        (handle, state.figure.clone())
544    };
545    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
546}
547
548pub fn set_box_enabled_for_axes(
549    handle: FigureHandle,
550    axes_index: usize,
551    enabled: bool,
552) -> Result<(), FigureError> {
553    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
554        state.figure.set_axes_box_enabled(axes_index, enabled);
555    })?;
556    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
557    Ok(())
558}
559
560pub fn set_figure_title_for_axes(
561    handle: FigureHandle,
562    axes_index: usize,
563    title: &str,
564    style: TextStyle,
565) -> Result<f64, FigureError> {
566    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
567        state.figure.set_axes_title(axes_index, title.to_string());
568        state.figure.set_axes_title_style(axes_index, style);
569        encode_plot_object_handle(handle, axes_index, PlotObjectKind::Title)
570    })?;
571    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
572    Ok(object_handle)
573}
574
575pub fn set_sg_title_for_figure(
576    handle: FigureHandle,
577    title: &str,
578    style: TextStyle,
579) -> Result<f64, FigureError> {
580    let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
581        state.figure.set_sg_title(title.to_string());
582        state.figure.set_sg_title_style(style);
583        encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
584    })?;
585    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
586    Ok(object_handle)
587}
588
589pub fn set_sg_title_properties_for_figure(
590    handle: FigureHandle,
591    text: Option<String>,
592    style: Option<TextStyle>,
593) -> Result<f64, FigureError> {
594    let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
595        if let Some(text) = text {
596            state.figure.set_sg_title(text);
597        }
598        if let Some(style) = style {
599            state.figure.set_sg_title_style(style);
600        }
601        encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
602    })?;
603    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
604    Ok(object_handle)
605}
606
607pub fn set_text_properties_for_axes(
608    handle: FigureHandle,
609    axes_index: usize,
610    kind: PlotObjectKind,
611    text: Option<String>,
612    style: Option<TextStyle>,
613) -> Result<f64, FigureError> {
614    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
615        if let Some(text) = text {
616            match kind {
617                PlotObjectKind::Title => state.figure.set_axes_title(axes_index, text),
618                PlotObjectKind::XLabel => state.figure.set_axes_xlabel(axes_index, text),
619                PlotObjectKind::YLabel => state.figure.set_axes_ylabel(axes_index, text),
620                PlotObjectKind::ZLabel => state.figure.set_axes_zlabel(axes_index, text),
621                PlotObjectKind::Legend => {}
622                PlotObjectKind::SuperTitle => state.figure.set_sg_title(text),
623            }
624        }
625        if let Some(style) = style {
626            match kind {
627                PlotObjectKind::Title => state.figure.set_axes_title_style(axes_index, style),
628                PlotObjectKind::XLabel => state.figure.set_axes_xlabel_style(axes_index, style),
629                PlotObjectKind::YLabel => state.figure.set_axes_ylabel_style(axes_index, style),
630                PlotObjectKind::ZLabel => state.figure.set_axes_zlabel_style(axes_index, style),
631                PlotObjectKind::Legend => {}
632                PlotObjectKind::SuperTitle => state.figure.set_sg_title_style(style),
633            }
634        }
635        encode_plot_object_handle(handle, axes_index, kind)
636    })?;
637    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
638    Ok(object_handle)
639}
640
641pub fn set_xlabel_for_axes(
642    handle: FigureHandle,
643    axes_index: usize,
644    label: &str,
645    style: TextStyle,
646) -> Result<f64, FigureError> {
647    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
648        state.figure.set_axes_xlabel(axes_index, label.to_string());
649        state.figure.set_axes_xlabel_style(axes_index, style);
650        encode_plot_object_handle(handle, axes_index, PlotObjectKind::XLabel)
651    })?;
652    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
653    Ok(object_handle)
654}
655
656pub fn set_ylabel_for_axes(
657    handle: FigureHandle,
658    axes_index: usize,
659    label: &str,
660    style: TextStyle,
661) -> Result<f64, FigureError> {
662    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
663        state.figure.set_axes_ylabel(axes_index, label.to_string());
664        state.figure.set_axes_ylabel_style(axes_index, style);
665        encode_plot_object_handle(handle, axes_index, PlotObjectKind::YLabel)
666    })?;
667    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
668    Ok(object_handle)
669}
670
671pub fn set_zlabel_for_axes(
672    handle: FigureHandle,
673    axes_index: usize,
674    label: &str,
675    style: TextStyle,
676) -> Result<f64, FigureError> {
677    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
678        state.figure.set_axes_zlabel(axes_index, label.to_string());
679        state.figure.set_axes_zlabel_style(axes_index, style);
680        encode_plot_object_handle(handle, axes_index, PlotObjectKind::ZLabel)
681    })?;
682    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
683    Ok(object_handle)
684}
685
686pub fn add_text_annotation_for_axes(
687    handle: FigureHandle,
688    axes_index: usize,
689    position: glam::Vec3,
690    text: &str,
691    style: TextStyle,
692) -> Result<f64, FigureError> {
693    let (annotation_index, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
694        state
695            .figure
696            .add_axes_text_annotation(axes_index, position, text.to_string(), style)
697    })?;
698    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
699    Ok(register_text_annotation_handle(
700        handle,
701        axes_index,
702        annotation_index,
703    ))
704}
705
706pub fn set_text_annotation_properties_for_axes(
707    handle: FigureHandle,
708    axes_index: usize,
709    annotation_index: usize,
710    text: Option<String>,
711    position: Option<glam::Vec3>,
712    style: Option<TextStyle>,
713) -> Result<f64, FigureError> {
714    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
715        if let Some(text) = text {
716            state
717                .figure
718                .set_axes_text_annotation_text(axes_index, annotation_index, text);
719        }
720        if let Some(position) = position {
721            state
722                .figure
723                .set_axes_text_annotation_position(axes_index, annotation_index, position);
724        }
725        if let Some(style) = style {
726            state
727                .figure
728                .set_axes_text_annotation_style(axes_index, annotation_index, style);
729        }
730        register_text_annotation_handle(handle, axes_index, annotation_index)
731    })?;
732    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
733    Ok(object_handle)
734}
735
736pub fn toggle_box() -> bool {
737    let (handle, figure_clone, enabled) = {
738        let mut reg = registry();
739        let handle = reg.current;
740        let state = get_state_mut(&mut reg, handle);
741        let axes = state.active_axes;
742        let next = !state
743            .figure
744            .axes_metadata(axes)
745            .map(|m| m.box_enabled)
746            .unwrap_or(true);
747        state.figure.set_axes_box_enabled(axes, next);
748        state.revision = state.revision.wrapping_add(1);
749        (handle, state.figure.clone(), next)
750    };
751    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
752    enabled
753}
754
755pub fn set_axis_equal(enabled: bool) {
756    let (handle, figure_clone) = {
757        let mut reg = registry();
758        let handle = reg.current;
759        let state = get_state_mut(&mut reg, handle);
760        let axes = state.active_axes;
761        state.figure.set_axes_axis_equal(axes, enabled);
762        state.revision = state.revision.wrapping_add(1);
763        (handle, state.figure.clone())
764    };
765    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
766}
767
768pub fn set_axis_equal_for_axes(
769    handle: FigureHandle,
770    axes_index: usize,
771    enabled: bool,
772) -> Result<(), FigureError> {
773    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
774        state.figure.set_axes_axis_equal(axes_index, enabled);
775    })?;
776    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
777    Ok(())
778}
779
780pub fn set_axis_limits(x: Option<(f64, f64)>, y: Option<(f64, f64)>) {
781    let (handle, figure_clone) = {
782        let mut reg = registry();
783        let handle = reg.current;
784        let state = get_state_mut(&mut reg, handle);
785        let axes = state.active_axes;
786        state.figure.set_axes_limits(axes, x, y);
787        state.revision = state.revision.wrapping_add(1);
788        (handle, state.figure.clone())
789    };
790    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
791}
792
793pub fn set_axis_limits_for_axes(
794    handle: FigureHandle,
795    axes_index: usize,
796    x: Option<(f64, f64)>,
797    y: Option<(f64, f64)>,
798) -> Result<(), FigureError> {
799    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
800        state.figure.set_axes_limits(axes_index, x, y);
801    })?;
802    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
803    Ok(())
804}
805
806pub fn axis_limits_snapshot() -> AxisLimitSnapshot {
807    let mut reg = registry();
808    let handle = reg.current;
809    let state = get_state_mut(&mut reg, handle);
810    let axes = state.active_axes;
811    let meta = state
812        .figure
813        .axes_metadata(axes)
814        .cloned()
815        .unwrap_or_default();
816    (meta.x_limits, meta.y_limits)
817}
818
819pub fn z_limits_snapshot() -> Option<(f64, f64)> {
820    let mut reg = registry();
821    let handle = reg.current;
822    let state = get_state_mut(&mut reg, handle);
823    let axes = state.active_axes;
824    state.figure.axes_metadata(axes).and_then(|m| m.z_limits)
825}
826
827pub fn color_limits_snapshot() -> Option<(f64, f64)> {
828    let mut reg = registry();
829    let handle = reg.current;
830    let state = get_state_mut(&mut reg, handle);
831    let axes = state.active_axes;
832    state
833        .figure
834        .axes_metadata(axes)
835        .and_then(|m| m.color_limits)
836}
837
838pub fn set_z_limits(limits: Option<(f64, f64)>) {
839    let (handle, figure_clone) = {
840        let mut reg = registry();
841        let handle = reg.current;
842        let state = get_state_mut(&mut reg, handle);
843        let axes = state.active_axes;
844        state.figure.set_axes_z_limits(axes, limits);
845        state.revision = state.revision.wrapping_add(1);
846        (handle, state.figure.clone())
847    };
848    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
849}
850
851pub fn set_z_limits_for_axes(
852    handle: FigureHandle,
853    axes_index: usize,
854    limits: Option<(f64, f64)>,
855) -> Result<(), FigureError> {
856    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
857        state.figure.set_axes_z_limits(axes_index, limits);
858    })?;
859    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
860    Ok(())
861}
862
863pub fn set_color_limits_runtime(limits: Option<(f64, f64)>) {
864    let (handle, figure_clone) = {
865        let mut reg = registry();
866        let handle = reg.current;
867        let state = get_state_mut(&mut reg, handle);
868        let axes = state.active_axes;
869        state.figure.set_axes_color_limits(axes, limits);
870        state.revision = state.revision.wrapping_add(1);
871        (handle, state.figure.clone())
872    };
873    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
874}
875
876pub fn set_color_limits_for_axes(
877    handle: FigureHandle,
878    axes_index: usize,
879    limits: Option<(f64, f64)>,
880) -> Result<(), FigureError> {
881    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
882        state.figure.set_axes_color_limits(axes_index, limits);
883    })?;
884    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
885    Ok(())
886}
887
888pub fn clear_current_axes() {
889    let (handle, figure_clone) = {
890        let mut reg = registry();
891        let handle = reg.current;
892        let axes_index = {
893            let state = get_state_mut(&mut reg, handle);
894            let axes_index = state.active_axes;
895            state.figure.clear_axes(axes_index);
896            state.reset_cycle(axes_index);
897            state.revision = state.revision.wrapping_add(1);
898            axes_index
899        };
900        purge_plot_children_for_axes(&mut reg, handle, axes_index);
901        let figure_clone = reg
902            .figures
903            .get(&handle)
904            .expect("figure exists")
905            .figure
906            .clone();
907        (handle, figure_clone)
908    };
909    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
910}
911
912pub fn set_colorbar_enabled(enabled: bool) {
913    let (handle, figure_clone) = {
914        let mut reg = registry();
915        let handle = reg.current;
916        let state = get_state_mut(&mut reg, handle);
917        let axes = state.active_axes;
918        state.figure.set_axes_colorbar_enabled(axes, enabled);
919        state.revision = state.revision.wrapping_add(1);
920        (handle, state.figure.clone())
921    };
922    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
923}
924
925pub fn set_colorbar_enabled_for_axes(
926    handle: FigureHandle,
927    axes_index: usize,
928    enabled: bool,
929) -> Result<(), FigureError> {
930    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
931        state.figure.set_axes_colorbar_enabled(axes_index, enabled);
932    })?;
933    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
934    Ok(())
935}
936
937pub fn set_legend_for_axes(
938    handle: FigureHandle,
939    axes_index: usize,
940    enabled: bool,
941    labels: Option<&[String]>,
942    style: Option<LegendStyle>,
943) -> Result<f64, FigureError> {
944    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
945        state.figure.set_axes_legend_enabled(axes_index, enabled);
946        if let Some(labels) = labels {
947            state.figure.set_labels_for_axes(axes_index, labels);
948        }
949        if let Some(style) = style {
950            state.figure.set_axes_legend_style(axes_index, style);
951        }
952        encode_plot_object_handle(handle, axes_index, PlotObjectKind::Legend)
953    })?;
954    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
955    Ok(object_handle)
956}
957
958pub fn set_log_modes_for_axes(
959    handle: FigureHandle,
960    axes_index: usize,
961    x_log: bool,
962    y_log: bool,
963) -> Result<(), FigureError> {
964    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
965        state.figure.set_axes_log_modes(axes_index, x_log, y_log);
966    })?;
967    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
968    Ok(())
969}
970
971pub fn set_view_for_axes(
972    handle: FigureHandle,
973    axes_index: usize,
974    azimuth_deg: f32,
975    elevation_deg: f32,
976) -> Result<(), FigureError> {
977    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
978        state
979            .figure
980            .set_axes_view(axes_index, azimuth_deg, elevation_deg);
981    })?;
982    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
983    Ok(())
984}
985
986pub fn legend_entries_snapshot(
987    handle: FigureHandle,
988    axes_index: usize,
989) -> Result<Vec<runmat_plot::plots::LegendEntry>, FigureError> {
990    let mut reg = registry();
991    let state = get_state_mut(&mut reg, handle);
992    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
993    if axes_index >= total_axes {
994        return Err(FigureError::InvalidSubplotIndex {
995            rows: state.figure.axes_rows.max(1),
996            cols: state.figure.axes_cols.max(1),
997            index: axes_index,
998        });
999    }
1000    Ok(state.figure.legend_entries_for_axes(axes_index))
1001}
1002
1003pub fn toggle_colorbar() -> bool {
1004    let (handle, figure_clone, enabled) = {
1005        let mut reg = registry();
1006        let handle = reg.current;
1007        let state = get_state_mut(&mut reg, handle);
1008        let axes = state.active_axes;
1009        let next = !state
1010            .figure
1011            .axes_metadata(axes)
1012            .map(|m| m.colorbar_enabled)
1013            .unwrap_or(false);
1014        state.figure.set_axes_colorbar_enabled(axes, next);
1015        state.revision = state.revision.wrapping_add(1);
1016        (handle, state.figure.clone(), next)
1017    };
1018    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1019    enabled
1020}
1021
1022pub fn set_colormap(colormap: ColorMap) {
1023    let (handle, figure_clone) = {
1024        let mut reg = registry();
1025        let handle = reg.current;
1026        let state = get_state_mut(&mut reg, handle);
1027        let axes = state.active_axes;
1028        state.figure.set_axes_colormap(axes, colormap);
1029        state.revision = state.revision.wrapping_add(1);
1030        (handle, state.figure.clone())
1031    };
1032    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1033}
1034
1035pub fn set_colormap_for_axes(
1036    handle: FigureHandle,
1037    axes_index: usize,
1038    colormap: ColorMap,
1039) -> Result<(), FigureError> {
1040    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1041        state.figure.set_axes_colormap(axes_index, colormap);
1042    })?;
1043    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1044    Ok(())
1045}
1046
1047pub fn set_surface_shading(mode: ShadingMode) {
1048    let (handle, figure_clone) = {
1049        let mut reg = registry();
1050        let handle = reg.current;
1051        let state = get_state_mut(&mut reg, handle);
1052        let plot_count = state.figure.len();
1053        for idx in 0..plot_count {
1054            if let Some(PlotElement::Surface(surface)) = state.figure.get_plot_mut(idx) {
1055                *surface = surface.clone().with_shading(mode);
1056            }
1057        }
1058        state.revision = state.revision.wrapping_add(1);
1059        (handle, state.figure.clone())
1060    };
1061    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1062}
1063
1064#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1065pub enum FigureEventKind {
1066    Created,
1067    Updated,
1068    Cleared,
1069    Closed,
1070}
1071
1072#[derive(Clone, Copy)]
1073pub struct FigureEventView<'a> {
1074    pub handle: FigureHandle,
1075    pub kind: FigureEventKind,
1076    pub revision: Option<u64>,
1077    pub figure: Option<&'a Figure>,
1078}
1079
1080type FigureObserver = dyn for<'a> Fn(FigureEventView<'a>) + Send + Sync + 'static;
1081
1082struct FigureObserverRegistry {
1083    observers: Mutex<Vec<Arc<FigureObserver>>>,
1084}
1085
1086impl FigureObserverRegistry {
1087    fn new() -> Self {
1088        Self {
1089            observers: Mutex::new(Vec::new()),
1090        }
1091    }
1092
1093    fn install(&self, observer: Arc<FigureObserver>) {
1094        let mut guard = self.observers.lock().expect("figure observers poisoned");
1095        guard.push(observer);
1096    }
1097
1098    fn notify(&self, view: FigureEventView<'_>) {
1099        let snapshot = {
1100            let guard = self.observers.lock().expect("figure observers poisoned");
1101            guard.clone()
1102        };
1103        for observer in snapshot {
1104            observer(view);
1105        }
1106    }
1107
1108    fn is_empty(&self) -> bool {
1109        self.observers
1110            .lock()
1111            .map(|guard| guard.is_empty())
1112            .unwrap_or(true)
1113    }
1114}
1115
1116static FIGURE_OBSERVERS: OnceCell<FigureObserverRegistry> = OnceCell::new();
1117
1118runmat_thread_local! {
1119    static RECENT_FIGURES: RefCell<HashSet<FigureHandle>> = RefCell::new(HashSet::new());
1120    static ACTIVE_AXES_CONTEXT: RefCell<Option<ActiveAxesContext>> = const { RefCell::new(None) };
1121}
1122
1123#[derive(Clone, Copy, Debug)]
1124pub struct FigureAxesState {
1125    pub handle: FigureHandle,
1126    pub rows: usize,
1127    pub cols: usize,
1128    pub active_index: usize,
1129}
1130
1131pub fn encode_axes_handle(handle: FigureHandle, axes_index: usize) -> f64 {
1132    let encoded =
1133        ((handle.as_u32() as u64) << AXES_INDEX_BITS) | ((axes_index as u64) & AXES_INDEX_MASK);
1134    encoded as f64
1135}
1136
1137pub fn encode_plot_object_handle(
1138    handle: FigureHandle,
1139    axes_index: usize,
1140    kind: PlotObjectKind,
1141) -> f64 {
1142    let encoded = (((handle.as_u32() as u64) << AXES_INDEX_BITS)
1143        | ((axes_index as u64) & AXES_INDEX_MASK))
1144        << OBJECT_KIND_BITS
1145        | ((kind as u64) & OBJECT_KIND_MASK);
1146    encoded as f64
1147}
1148
1149pub fn decode_plot_object_handle(
1150    value: f64,
1151) -> Result<(FigureHandle, usize, PlotObjectKind), FigureError> {
1152    if !value.is_finite() || value <= 0.0 {
1153        return Err(FigureError::InvalidPlotObjectHandle);
1154    }
1155    let encoded = value.round() as u64;
1156    let kind = PlotObjectKind::from_u64(encoded & OBJECT_KIND_MASK)
1157        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1158    let base = encoded >> OBJECT_KIND_BITS;
1159    let figure_id = base >> AXES_INDEX_BITS;
1160    if figure_id == 0 {
1161        return Err(FigureError::InvalidPlotObjectHandle);
1162    }
1163    let axes_index = (base & AXES_INDEX_MASK) as usize;
1164    Ok((FigureHandle::from(figure_id as u32), axes_index, kind))
1165}
1166
1167pub fn register_histogram_handle(
1168    figure: FigureHandle,
1169    axes_index: usize,
1170    plot_index: usize,
1171    bin_edges: Vec<f64>,
1172    raw_counts: Vec<f64>,
1173    normalization: String,
1174) -> f64 {
1175    let mut reg = registry();
1176    let id = reg.next_plot_child_handle;
1177    reg.next_plot_child_handle += 1;
1178    reg.plot_children.insert(
1179        id,
1180        PlotChildHandleState::Histogram(HistogramHandleState {
1181            figure,
1182            axes_index,
1183            plot_index,
1184            bin_edges,
1185            raw_counts,
1186            normalization,
1187            display_name: None,
1188        }),
1189    );
1190    id as f64
1191}
1192
1193fn register_simple_plot_handle(
1194    figure: FigureHandle,
1195    axes_index: usize,
1196    plot_index: usize,
1197    constructor: fn(SimplePlotHandleState) -> PlotChildHandleState,
1198) -> f64 {
1199    let mut reg = registry();
1200    let id = reg.next_plot_child_handle;
1201    reg.next_plot_child_handle += 1;
1202    reg.plot_children.insert(
1203        id,
1204        constructor(SimplePlotHandleState {
1205            figure,
1206            axes_index,
1207            plot_index,
1208        }),
1209    );
1210    id as f64
1211}
1212
1213pub fn register_line_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1214    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line)
1215}
1216
1217pub fn register_reference_line_handle(
1218    figure: FigureHandle,
1219    axes_index: usize,
1220    plot_index: usize,
1221) -> f64 {
1222    register_simple_plot_handle(
1223        figure,
1224        axes_index,
1225        plot_index,
1226        PlotChildHandleState::ReferenceLine,
1227    )
1228}
1229
1230pub fn register_scatter_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1231    register_simple_plot_handle(
1232        figure,
1233        axes_index,
1234        plot_index,
1235        PlotChildHandleState::Scatter,
1236    )
1237}
1238
1239pub fn register_bar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1240    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Bar)
1241}
1242
1243pub fn register_stem_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1244    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1245        PlotChildHandleState::Stem(StemHandleState {
1246            figure: state.figure,
1247            axes_index: state.axes_index,
1248            plot_index: state.plot_index,
1249        })
1250    })
1251}
1252
1253pub fn register_errorbar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1254    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1255        PlotChildHandleState::ErrorBar(ErrorBarHandleState {
1256            figure: state.figure,
1257            axes_index: state.axes_index,
1258            plot_index: state.plot_index,
1259        })
1260    })
1261}
1262
1263pub fn register_stairs_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1264    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Stairs)
1265}
1266
1267pub fn register_quiver_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1268    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1269        PlotChildHandleState::Quiver(QuiverHandleState {
1270            figure: state.figure,
1271            axes_index: state.axes_index,
1272            plot_index: state.plot_index,
1273        })
1274    })
1275}
1276
1277pub fn register_image_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1278    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1279        PlotChildHandleState::Image(ImageHandleState {
1280            figure: state.figure,
1281            axes_index: state.axes_index,
1282            plot_index: state.plot_index,
1283        })
1284    })
1285}
1286
1287pub fn register_heatmap_handle(
1288    figure: FigureHandle,
1289    axes_index: usize,
1290    plot_index: usize,
1291    x_labels: Vec<String>,
1292    y_labels: Vec<String>,
1293    color_data: Tensor,
1294) -> f64 {
1295    let mut reg = registry();
1296    let id = reg.next_plot_child_handle;
1297    reg.next_plot_child_handle += 1;
1298    reg.plot_children.insert(
1299        id,
1300        PlotChildHandleState::Heatmap(HeatmapHandleState {
1301            figure,
1302            axes_index,
1303            plot_index,
1304            x_labels,
1305            y_labels,
1306            color_data,
1307        }),
1308    );
1309    id as f64
1310}
1311
1312pub fn register_area_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1313    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1314        PlotChildHandleState::Area(AreaHandleState {
1315            figure: state.figure,
1316            axes_index: state.axes_index,
1317            plot_index: state.plot_index,
1318        })
1319    })
1320}
1321
1322pub fn register_surface_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1323    register_simple_plot_handle(
1324        figure,
1325        axes_index,
1326        plot_index,
1327        PlotChildHandleState::Surface,
1328    )
1329}
1330
1331pub fn register_patch_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1332    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Patch)
1333}
1334
1335pub fn register_line3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1336    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line3)
1337}
1338
1339pub fn register_scatter3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1340    register_simple_plot_handle(
1341        figure,
1342        axes_index,
1343        plot_index,
1344        PlotChildHandleState::Scatter3,
1345    )
1346}
1347
1348pub fn register_contour_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1349    register_simple_plot_handle(
1350        figure,
1351        axes_index,
1352        plot_index,
1353        PlotChildHandleState::Contour,
1354    )
1355}
1356
1357pub fn register_contour_fill_handle(
1358    figure: FigureHandle,
1359    axes_index: usize,
1360    plot_index: usize,
1361) -> f64 {
1362    register_simple_plot_handle(
1363        figure,
1364        axes_index,
1365        plot_index,
1366        PlotChildHandleState::ContourFill,
1367    )
1368}
1369
1370pub fn register_pie_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1371    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Pie)
1372}
1373
1374pub fn register_text_annotation_handle(
1375    figure: FigureHandle,
1376    axes_index: usize,
1377    annotation_index: usize,
1378) -> f64 {
1379    let mut reg = registry();
1380    let id = reg.next_plot_child_handle;
1381    reg.next_plot_child_handle += 1;
1382    reg.plot_children.insert(
1383        id,
1384        PlotChildHandleState::Text(TextAnnotationHandleState {
1385            figure,
1386            axes_index,
1387            annotation_index,
1388        }),
1389    );
1390    id as f64
1391}
1392
1393pub fn plot_child_handle_snapshot(handle: f64) -> Result<PlotChildHandleState, FigureError> {
1394    if !handle.is_finite() || handle <= 0.0 {
1395        return Err(FigureError::InvalidPlotObjectHandle);
1396    }
1397    let reg = registry();
1398    reg.plot_children
1399        .get(&(handle.round() as u64))
1400        .cloned()
1401        .ok_or(FigureError::InvalidPlotObjectHandle)
1402}
1403
1404pub fn set_heatmap_display_labels(
1405    figure: FigureHandle,
1406    axes_index: usize,
1407    plot_index: usize,
1408    x_labels: Option<Vec<String>>,
1409    y_labels: Option<Vec<String>>,
1410) -> Result<(), FigureError> {
1411    let figure_clone = {
1412        let mut reg = registry();
1413        let (current_x_labels, current_y_labels) = {
1414            let state = reg.plot_children.values_mut().find(|state| match state {
1415                PlotChildHandleState::Heatmap(heatmap) => {
1416                    heatmap.figure == figure
1417                        && heatmap.axes_index == axes_index
1418                        && heatmap.plot_index == plot_index
1419                }
1420                _ => false,
1421            });
1422            let PlotChildHandleState::Heatmap(heatmap) =
1423                state.ok_or(FigureError::InvalidPlotObjectHandle)?
1424            else {
1425                return Err(FigureError::InvalidPlotObjectHandle);
1426            };
1427            if let Some(labels) = x_labels {
1428                heatmap.x_labels = labels;
1429            }
1430            if let Some(labels) = y_labels {
1431                heatmap.y_labels = labels;
1432            }
1433            (heatmap.x_labels.clone(), heatmap.y_labels.clone())
1434        };
1435
1436        let state = get_state_mut(&mut reg, figure);
1437        let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1438        if axes_index >= total_axes {
1439            return Err(FigureError::InvalidSubplotIndex {
1440                rows: state.figure.axes_rows.max(1),
1441                cols: state.figure.axes_cols.max(1),
1442                index: axes_index,
1443            });
1444        }
1445        state.active_axes = axes_index;
1446        state.figure.set_active_axes_index(axes_index);
1447        state.figure.set_axes_tick_labels(
1448            axes_index,
1449            Some(current_x_labels),
1450            Some(current_y_labels),
1451        );
1452        state.revision = state.revision.wrapping_add(1);
1453        state.figure.clone()
1454    };
1455    notify_with_figure(figure, &figure_clone, FigureEventKind::Updated);
1456    Ok(())
1457}
1458
1459pub fn update_histogram_handle_for_plot(
1460    figure: FigureHandle,
1461    axes_index: usize,
1462    plot_index: usize,
1463    normalization: String,
1464    raw_counts: Vec<f64>,
1465) -> Result<(), FigureError> {
1466    let mut reg = registry();
1467    let state = reg.plot_children.values_mut().find(|state| match state {
1468        PlotChildHandleState::Histogram(hist) => {
1469            hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
1470        }
1471        _ => false,
1472    });
1473    match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
1474        PlotChildHandleState::Histogram(hist) => {
1475            hist.normalization = normalization;
1476            hist.raw_counts = raw_counts;
1477            Ok(())
1478        }
1479        _ => Err(FigureError::InvalidPlotObjectHandle),
1480    }
1481}
1482
1483pub fn set_histogram_handle_display_name(
1484    figure: FigureHandle,
1485    axes_index: usize,
1486    plot_index: usize,
1487    display_name: Option<String>,
1488) -> Result<(), FigureError> {
1489    let mut reg = registry();
1490    let state = reg.plot_children.values_mut().find(|state| match state {
1491        PlotChildHandleState::Histogram(hist) => {
1492            hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
1493        }
1494        _ => false,
1495    });
1496    match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
1497        PlotChildHandleState::Histogram(hist) => {
1498            hist.display_name = display_name;
1499            Ok(())
1500        }
1501        _ => Err(FigureError::InvalidPlotObjectHandle),
1502    }
1503}
1504
1505pub fn update_errorbar_plot(
1506    figure_handle: FigureHandle,
1507    plot_index: usize,
1508    updater: impl FnOnce(&mut runmat_plot::plots::ErrorBar),
1509) -> Result<(), FigureError> {
1510    let mut reg = registry();
1511    let state = get_state_mut(&mut reg, figure_handle);
1512    let plot = state
1513        .figure
1514        .get_plot_mut(plot_index)
1515        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1516    match plot {
1517        runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) => {
1518            updater(errorbar);
1519            Ok(())
1520        }
1521        _ => Err(FigureError::InvalidPlotObjectHandle),
1522    }
1523}
1524
1525pub fn update_histogram_plot_data(
1526    figure_handle: FigureHandle,
1527    plot_index: usize,
1528    labels: Vec<String>,
1529    values: Vec<f64>,
1530) -> Result<(), FigureError> {
1531    let mut reg = registry();
1532    let state = get_state_mut(&mut reg, figure_handle);
1533    let plot = state
1534        .figure
1535        .get_plot_mut(plot_index)
1536        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1537    match plot {
1538        runmat_plot::plots::figure::PlotElement::Bar(bar) => {
1539            bar.set_data(labels, values)
1540                .map_err(|_| FigureError::InvalidPlotObjectHandle)?;
1541            Ok(())
1542        }
1543        _ => Err(FigureError::InvalidPlotObjectHandle),
1544    }
1545}
1546
1547pub fn update_stem_plot(
1548    figure_handle: FigureHandle,
1549    plot_index: usize,
1550    updater: impl FnOnce(&mut runmat_plot::plots::StemPlot),
1551) -> Result<(), FigureError> {
1552    let mut reg = registry();
1553    let state = get_state_mut(&mut reg, figure_handle);
1554    let plot = state
1555        .figure
1556        .get_plot_mut(plot_index)
1557        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1558    match plot {
1559        runmat_plot::plots::figure::PlotElement::Stem(stem) => {
1560            updater(stem);
1561            Ok(())
1562        }
1563        _ => Err(FigureError::InvalidPlotObjectHandle),
1564    }
1565}
1566
1567pub fn update_quiver_plot(
1568    figure_handle: FigureHandle,
1569    plot_index: usize,
1570    updater: impl FnOnce(&mut runmat_plot::plots::QuiverPlot),
1571) -> Result<(), FigureError> {
1572    let mut reg = registry();
1573    let state = get_state_mut(&mut reg, figure_handle);
1574    let plot = state
1575        .figure
1576        .get_plot_mut(plot_index)
1577        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1578    match plot {
1579        runmat_plot::plots::figure::PlotElement::Quiver(quiver) => {
1580            updater(quiver);
1581            Ok(())
1582        }
1583        _ => Err(FigureError::InvalidPlotObjectHandle),
1584    }
1585}
1586
1587pub fn update_image_plot(
1588    figure_handle: FigureHandle,
1589    plot_index: usize,
1590    updater: impl FnOnce(&mut runmat_plot::plots::SurfacePlot),
1591) -> Result<(), FigureError> {
1592    let mut reg = registry();
1593    let state = get_state_mut(&mut reg, figure_handle);
1594    let plot = state
1595        .figure
1596        .get_plot_mut(plot_index)
1597        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1598    match plot {
1599        runmat_plot::plots::figure::PlotElement::Surface(surface) if surface.image_mode => {
1600            updater(surface);
1601            Ok(())
1602        }
1603        _ => Err(FigureError::InvalidPlotObjectHandle),
1604    }
1605}
1606
1607pub fn update_area_plot(
1608    figure_handle: FigureHandle,
1609    plot_index: usize,
1610    updater: impl FnOnce(&mut runmat_plot::plots::AreaPlot),
1611) -> Result<(), FigureError> {
1612    let mut reg = registry();
1613    let state = get_state_mut(&mut reg, figure_handle);
1614    let plot = state
1615        .figure
1616        .get_plot_mut(plot_index)
1617        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1618    match plot {
1619        runmat_plot::plots::figure::PlotElement::Area(area) => {
1620            updater(area);
1621            Ok(())
1622        }
1623        _ => Err(FigureError::InvalidPlotObjectHandle),
1624    }
1625}
1626
1627pub fn update_plot_element(
1628    figure_handle: FigureHandle,
1629    plot_index: usize,
1630    updater: impl FnOnce(&mut runmat_plot::plots::figure::PlotElement),
1631) -> Result<(), FigureError> {
1632    let mut reg = registry();
1633    let state = get_state_mut(&mut reg, figure_handle);
1634    let plot = state
1635        .figure
1636        .get_plot_mut(plot_index)
1637        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1638    updater(plot);
1639    Ok(())
1640}
1641
1642fn purge_plot_children_for_figure(reg: &mut PlotRegistry, handle: FigureHandle) {
1643    reg.plot_children.retain(|_, state| match state {
1644        PlotChildHandleState::Histogram(hist) => hist.figure != handle,
1645        PlotChildHandleState::Line(plot)
1646        | PlotChildHandleState::Scatter(plot)
1647        | PlotChildHandleState::Bar(plot)
1648        | PlotChildHandleState::Stairs(plot)
1649        | PlotChildHandleState::Surface(plot)
1650        | PlotChildHandleState::Patch(plot)
1651        | PlotChildHandleState::Line3(plot)
1652        | PlotChildHandleState::Scatter3(plot)
1653        | PlotChildHandleState::Contour(plot)
1654        | PlotChildHandleState::ContourFill(plot)
1655        | PlotChildHandleState::ReferenceLine(plot)
1656        | PlotChildHandleState::Pie(plot) => plot.figure != handle,
1657        PlotChildHandleState::Stem(stem) => stem.figure != handle,
1658        PlotChildHandleState::ErrorBar(err) => err.figure != handle,
1659        PlotChildHandleState::Quiver(quiver) => quiver.figure != handle,
1660        PlotChildHandleState::Image(image) => image.figure != handle,
1661        PlotChildHandleState::Heatmap(heatmap) => heatmap.figure != handle,
1662        PlotChildHandleState::Area(area) => area.figure != handle,
1663        PlotChildHandleState::Text(text) => text.figure != handle,
1664    });
1665}
1666
1667fn purge_plot_children_for_axes(reg: &mut PlotRegistry, handle: FigureHandle, axes_index: usize) {
1668    reg.plot_children.retain(|_, state| match state {
1669        PlotChildHandleState::Histogram(hist) => {
1670            !(hist.figure == handle && hist.axes_index == axes_index)
1671        }
1672        PlotChildHandleState::Line(plot)
1673        | PlotChildHandleState::Scatter(plot)
1674        | PlotChildHandleState::Bar(plot)
1675        | PlotChildHandleState::Stairs(plot)
1676        | PlotChildHandleState::Surface(plot)
1677        | PlotChildHandleState::Patch(plot)
1678        | PlotChildHandleState::Line3(plot)
1679        | PlotChildHandleState::Scatter3(plot)
1680        | PlotChildHandleState::Contour(plot)
1681        | PlotChildHandleState::ContourFill(plot)
1682        | PlotChildHandleState::ReferenceLine(plot)
1683        | PlotChildHandleState::Pie(plot) => {
1684            !(plot.figure == handle && plot.axes_index == axes_index)
1685        }
1686        PlotChildHandleState::Stem(stem) => {
1687            !(stem.figure == handle && stem.axes_index == axes_index)
1688        }
1689        PlotChildHandleState::ErrorBar(err) => {
1690            !(err.figure == handle && err.axes_index == axes_index)
1691        }
1692        PlotChildHandleState::Quiver(quiver) => {
1693            !(quiver.figure == handle && quiver.axes_index == axes_index)
1694        }
1695        PlotChildHandleState::Image(image) => {
1696            !(image.figure == handle && image.axes_index == axes_index)
1697        }
1698        PlotChildHandleState::Heatmap(heatmap) => {
1699            !(heatmap.figure == handle && heatmap.axes_index == axes_index)
1700        }
1701        PlotChildHandleState::Area(area) => {
1702            !(area.figure == handle && area.axes_index == axes_index)
1703        }
1704        PlotChildHandleState::Text(text) => {
1705            !(text.figure == handle && text.axes_index == axes_index)
1706        }
1707    });
1708}
1709
1710#[allow(dead_code)]
1711pub fn decode_axes_handle(value: f64) -> Result<(FigureHandle, usize), FigureError> {
1712    if !value.is_finite() || value <= 0.0 {
1713        return Err(FigureError::InvalidAxesHandle);
1714    }
1715    let encoded = value.round() as u64;
1716    let figure_id = encoded >> AXES_INDEX_BITS;
1717    if figure_id == 0 {
1718        return Err(FigureError::InvalidAxesHandle);
1719    }
1720    let axes_index = (encoded & AXES_INDEX_MASK) as usize;
1721    Ok((FigureHandle::from(figure_id as u32), axes_index))
1722}
1723
1724#[cfg(not(target_arch = "wasm32"))]
1725fn registry() -> PlotRegistryGuard<'static> {
1726    #[cfg(test)]
1727    let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
1728        if flag.get() {
1729            None
1730        } else {
1731            Some(
1732                TEST_PLOT_REGISTRY_LOCK
1733                    .lock()
1734                    .unwrap_or_else(|e| e.into_inner()),
1735            )
1736        }
1737    });
1738    let guard = REGISTRY
1739        .get_or_init(|| Mutex::new(PlotRegistry::default()))
1740        .lock()
1741        .expect("plot registry poisoned");
1742    #[cfg(test)]
1743    {
1744        PlotRegistryGuard::new(guard, test_lock)
1745    }
1746    #[cfg(not(test))]
1747    {
1748        PlotRegistryGuard::new(guard)
1749    }
1750}
1751
1752#[cfg(target_arch = "wasm32")]
1753fn registry() -> PlotRegistryGuard<'static> {
1754    REGISTRY.with(|cell| {
1755        let guard = cell.borrow_mut();
1756        // SAFETY: the thread-local RefCell lives for the program lifetime and the borrow
1757        // guard is dropped when PlotRegistryGuard is dropped, so extending the lifetime
1758        // to 'static is sound.
1759        let guard_static: std::cell::RefMut<'static, PlotRegistry> =
1760            unsafe { std::mem::transmute::<std::cell::RefMut<'_, PlotRegistry>, _>(guard) };
1761        #[cfg(test)]
1762        {
1763            let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
1764                if flag.get() {
1765                    None
1766                } else {
1767                    Some(
1768                        TEST_PLOT_REGISTRY_LOCK
1769                            .lock()
1770                            .unwrap_or_else(|e| e.into_inner()),
1771                    )
1772                }
1773            });
1774            PlotRegistryGuard::new(guard_static, test_lock)
1775        }
1776        #[cfg(not(test))]
1777        {
1778            PlotRegistryGuard::new(guard_static)
1779        }
1780    })
1781}
1782
1783fn get_state_mut(registry: &mut PlotRegistry, handle: FigureHandle) -> &mut FigureState {
1784    registry
1785        .figures
1786        .entry(handle)
1787        .or_insert_with(|| FigureState::new(handle))
1788}
1789
1790fn observer_registry() -> &'static FigureObserverRegistry {
1791    FIGURE_OBSERVERS.get_or_init(FigureObserverRegistry::new)
1792}
1793
1794pub fn install_figure_observer(observer: Arc<FigureObserver>) -> BuiltinResult<()> {
1795    observer_registry().install(observer);
1796    Ok(())
1797}
1798
1799fn notify_event<'a>(view: FigureEventView<'a>) {
1800    note_recent_figure(view.handle);
1801    if let Some(registry) = FIGURE_OBSERVERS.get() {
1802        if registry.is_empty() {
1803            return;
1804        }
1805        registry.notify(view);
1806    }
1807}
1808
1809fn notify_with_figure(handle: FigureHandle, figure: &Figure, kind: FigureEventKind) {
1810    notify_event(FigureEventView {
1811        handle,
1812        kind,
1813        revision: current_figure_revision(handle),
1814        figure: Some(figure),
1815    });
1816}
1817
1818fn notify_without_figure(handle: FigureHandle, kind: FigureEventKind) {
1819    notify_event(FigureEventView {
1820        handle,
1821        kind,
1822        revision: current_figure_revision(handle),
1823        figure: None,
1824    });
1825}
1826
1827fn note_recent_figure(handle: FigureHandle) {
1828    RECENT_FIGURES.with(|set| {
1829        set.borrow_mut().insert(handle);
1830    });
1831}
1832
1833pub fn reset_recent_figures() {
1834    RECENT_FIGURES.with(|set| set.borrow_mut().clear());
1835}
1836
1837pub fn reset_plot_state() {
1838    {
1839        let mut reg = registry();
1840        *reg = PlotRegistry::default();
1841    }
1842    reset_recent_figures();
1843}
1844
1845pub fn take_recent_figures() -> Vec<FigureHandle> {
1846    RECENT_FIGURES.with(|set| set.borrow_mut().drain().collect())
1847}
1848
1849pub fn select_figure(handle: FigureHandle) {
1850    let mut reg = registry();
1851    reg.current = handle;
1852    let maybe_new = match reg.figures.entry(handle) {
1853        Entry::Occupied(entry) => {
1854            let _ = entry.into_mut();
1855            None
1856        }
1857        Entry::Vacant(vacant) => {
1858            let state = vacant.insert(FigureState::new(handle));
1859            Some(state.figure.clone())
1860        }
1861    };
1862    drop(reg);
1863    if let Some(figure_clone) = maybe_new {
1864        notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
1865    }
1866}
1867
1868pub fn new_figure_handle() -> FigureHandle {
1869    let mut reg = registry();
1870    let handle = reg.next_handle;
1871    reg.next_handle = reg.next_handle.next();
1872    reg.current = handle;
1873    let figure_clone = {
1874        let state = get_state_mut(&mut reg, handle);
1875        state.figure.clone()
1876    };
1877    drop(reg);
1878    notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
1879    handle
1880}
1881
1882pub fn current_figure_handle() -> FigureHandle {
1883    registry().current
1884}
1885
1886pub fn current_axes_state() -> FigureAxesState {
1887    let mut reg = registry();
1888    let handle = reg.current;
1889    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
1890    let state = get_state_mut(&mut reg, handle);
1891    FigureAxesState {
1892        handle,
1893        rows: state.figure.axes_rows.max(1),
1894        cols: state.figure.axes_cols.max(1),
1895        active_index: state.active_axes,
1896    }
1897}
1898
1899pub fn axes_handle_exists(handle: FigureHandle, axes_index: usize) -> bool {
1900    let mut reg = registry();
1901    let state = get_state_mut(&mut reg, handle);
1902    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1903    axes_index < total_axes
1904}
1905
1906pub fn figure_handle_exists(handle: FigureHandle) -> bool {
1907    let reg = registry();
1908    reg.figures.contains_key(&handle)
1909}
1910
1911pub fn axes_metadata_snapshot(
1912    handle: FigureHandle,
1913    axes_index: usize,
1914) -> Result<runmat_plot::plots::AxesMetadata, FigureError> {
1915    let mut reg = registry();
1916    let state = get_state_mut(&mut reg, handle);
1917    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1918    if axes_index >= total_axes {
1919        return Err(FigureError::InvalidSubplotIndex {
1920            rows: state.figure.axes_rows.max(1),
1921            cols: state.figure.axes_cols.max(1),
1922            index: axes_index,
1923        });
1924    }
1925    state
1926        .figure
1927        .axes_metadata(axes_index)
1928        .cloned()
1929        .ok_or(FigureError::InvalidAxesHandle)
1930}
1931
1932pub fn axes_state_snapshot(
1933    handle: FigureHandle,
1934    axes_index: usize,
1935) -> Result<FigureAxesState, FigureError> {
1936    let mut reg = registry();
1937    let state = get_state_mut(&mut reg, handle);
1938    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1939    if axes_index >= total_axes {
1940        return Err(FigureError::InvalidSubplotIndex {
1941            rows: state.figure.axes_rows.max(1),
1942            cols: state.figure.axes_cols.max(1),
1943            index: axes_index,
1944        });
1945    }
1946    Ok(FigureAxesState {
1947        handle,
1948        rows: state.figure.axes_rows.max(1),
1949        cols: state.figure.axes_cols.max(1),
1950        active_index: axes_index,
1951    })
1952}
1953
1954pub fn current_axes_handle_for_figure(handle: FigureHandle) -> Result<f64, FigureError> {
1955    let mut reg = registry();
1956    let state = get_state_mut(&mut reg, handle);
1957    Ok(encode_axes_handle(handle, state.active_axes))
1958}
1959
1960pub fn axes_handles_for_figure(handle: FigureHandle) -> Result<Vec<f64>, FigureError> {
1961    let mut reg = registry();
1962    let state = get_state_mut(&mut reg, handle);
1963    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1964    Ok((0..total_axes)
1965        .map(|idx| encode_axes_handle(handle, idx))
1966        .collect())
1967}
1968
1969pub fn select_axes_for_figure(handle: FigureHandle, axes_index: usize) -> Result<(), FigureError> {
1970    let mut reg = registry();
1971    let state = get_state_mut(&mut reg, handle);
1972    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1973    if axes_index >= total_axes {
1974        return Err(FigureError::InvalidSubplotIndex {
1975            rows: state.figure.axes_rows.max(1),
1976            cols: state.figure.axes_cols.max(1),
1977            index: axes_index,
1978        });
1979    }
1980    reg.current = handle;
1981    let state = get_state_mut(&mut reg, handle);
1982    state.active_axes = axes_index;
1983    state.figure.set_active_axes_index(axes_index);
1984    Ok(())
1985}
1986
1987fn with_axes_target_mut<R>(
1988    handle: FigureHandle,
1989    axes_index: usize,
1990    f: impl FnOnce(&mut FigureState) -> R,
1991) -> Result<(R, Figure), FigureError> {
1992    let mut reg = registry();
1993    let state = get_state_mut(&mut reg, handle);
1994    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1995    if axes_index >= total_axes {
1996        return Err(FigureError::InvalidSubplotIndex {
1997            rows: state.figure.axes_rows.max(1),
1998            cols: state.figure.axes_cols.max(1),
1999            index: axes_index,
2000        });
2001    }
2002    state.active_axes = axes_index;
2003    state.figure.set_active_axes_index(axes_index);
2004    let result = f(state);
2005    state.revision = state.revision.wrapping_add(1);
2006    Ok((result, state.figure.clone()))
2007}
2008
2009fn with_figure_mut<R>(
2010    handle: FigureHandle,
2011    f: impl FnOnce(&mut FigureState) -> R,
2012) -> Result<(R, Figure), FigureError> {
2013    let mut reg = registry();
2014    let state = get_state_mut(&mut reg, handle);
2015    let result = f(state);
2016    state.revision = state.revision.wrapping_add(1);
2017    Ok((result, state.figure.clone()))
2018}
2019
2020pub fn current_hold_enabled() -> bool {
2021    let mut reg = registry();
2022    let handle = reg.current;
2023    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
2024    let state = get_state_mut(&mut reg, handle);
2025    *state
2026        .hold_per_axes
2027        .get(&state.active_axes)
2028        .unwrap_or(&false)
2029}
2030
2031/// Reset hold state for all figures/axes.
2032///
2033/// In the IDE, we want re-running code to behave like a fresh plotting run unless the code
2034/// explicitly enables `hold on` again. Without this, a prior `hold on` will cause subsequent
2035/// runs to keep appending to the same axes (surprising for typical "Run" workflows).
2036pub fn reset_hold_state_for_run() {
2037    let mut reg = registry();
2038    for state in reg.figures.values_mut() {
2039        state.hold_per_axes.clear();
2040    }
2041}
2042
2043pub fn figure_handles() -> Vec<FigureHandle> {
2044    let reg = registry();
2045    reg.figures.keys().copied().collect()
2046}
2047
2048pub fn clone_figure(handle: FigureHandle) -> Option<Figure> {
2049    let reg = registry();
2050    reg.figures.get(&handle).map(|state| state.figure.clone())
2051}
2052
2053pub fn figure_has_sg_title(handle: FigureHandle) -> bool {
2054    let reg = registry();
2055    reg.figures
2056        .get(&handle)
2057        .map(|state| state.figure.sg_title.is_some())
2058        .unwrap_or(false)
2059}
2060
2061pub fn import_figure(figure: Figure) -> FigureHandle {
2062    let mut reg = registry();
2063    let handle = reg.next_handle;
2064    reg.next_handle = reg.next_handle.next();
2065    reg.current = handle;
2066    let figure_clone = figure.clone();
2067    reg.figures.insert(
2068        handle,
2069        FigureState {
2070            figure,
2071            ..FigureState::new(handle)
2072        },
2073    );
2074    drop(reg);
2075    notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
2076    handle
2077}
2078
2079pub fn clear_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2080    let mut reg = registry();
2081    let handle = target.unwrap_or(reg.current);
2082    {
2083        let state = reg
2084            .figures
2085            .get_mut(&handle)
2086            .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2087        *state = FigureState::new(handle);
2088    }
2089    purge_plot_children_for_figure(&mut reg, handle);
2090    let figure_clone = reg
2091        .figures
2092        .get(&handle)
2093        .expect("figure exists")
2094        .figure
2095        .clone();
2096    drop(reg);
2097    notify_with_figure(handle, &figure_clone, FigureEventKind::Cleared);
2098    Ok(handle)
2099}
2100
2101pub fn close_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2102    let mut reg = registry();
2103    let handle = target.unwrap_or(reg.current);
2104    let existed = reg.figures.remove(&handle);
2105    if existed.is_none() {
2106        return Err(FigureError::InvalidHandle(handle.as_u32()));
2107    }
2108    purge_plot_children_for_figure(&mut reg, handle);
2109
2110    if reg.current == handle {
2111        if let Some((&next_handle, _)) = reg.figures.iter().next() {
2112            reg.current = next_handle;
2113        } else {
2114            let default = FigureHandle::default();
2115            reg.current = default;
2116            reg.next_handle = default.next();
2117            drop(reg);
2118            notify_without_figure(handle, FigureEventKind::Closed);
2119            return Ok(handle);
2120        }
2121    }
2122
2123    drop(reg);
2124    notify_without_figure(handle, FigureEventKind::Closed);
2125    Ok(handle)
2126}
2127
2128#[derive(Clone)]
2129pub struct PlotRenderOptions<'a> {
2130    pub title: &'a str,
2131    pub x_label: &'a str,
2132    pub y_label: &'a str,
2133    pub grid: bool,
2134    pub axis_equal: bool,
2135}
2136
2137impl<'a> Default for PlotRenderOptions<'a> {
2138    fn default() -> Self {
2139        Self {
2140            title: "",
2141            x_label: "X",
2142            y_label: "Y",
2143            grid: true,
2144            axis_equal: false,
2145        }
2146    }
2147}
2148
2149pub enum HoldMode {
2150    On,
2151    Off,
2152    Toggle,
2153}
2154
2155pub fn set_hold(mode: HoldMode) -> bool {
2156    let mut reg = registry();
2157    let handle = reg.current;
2158    let state = get_state_mut(&mut reg, handle);
2159    let current = state.hold();
2160    let new_value = match mode {
2161        HoldMode::On => true,
2162        HoldMode::Off => false,
2163        HoldMode::Toggle => !current,
2164    };
2165    state.set_hold(new_value);
2166    new_value
2167}
2168
2169pub fn configure_subplot(rows: usize, cols: usize, index: usize) -> Result<(), FigureError> {
2170    if rows == 0 || cols == 0 {
2171        return Err(FigureError::InvalidSubplotGrid { rows, cols });
2172    }
2173    let total_axes = rows
2174        .checked_mul(cols)
2175        .ok_or(FigureError::InvalidSubplotGrid { rows, cols })?;
2176    if index >= total_axes {
2177        return Err(FigureError::InvalidSubplotIndex { rows, cols, index });
2178    }
2179    let mut reg = registry();
2180    let handle = reg.current;
2181    let state = get_state_mut(&mut reg, handle);
2182    state.figure.set_subplot_grid(rows, cols);
2183    state.active_axes = index;
2184    state.figure.set_active_axes_index(index);
2185    Ok(())
2186}
2187
2188pub fn render_active_plot<F>(
2189    builtin: &'static str,
2190    opts: PlotRenderOptions<'_>,
2191    mut apply: F,
2192) -> BuiltinResult<String>
2193where
2194    F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2195{
2196    let rendering_disabled = interactive_rendering_disabled();
2197    let host_managed_rendering = host_managed_rendering_enabled();
2198    let (handle, figure_clone) = {
2199        let mut reg = registry();
2200        let handle = reg.current;
2201        let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2202        let should_clear = { !get_state_mut(&mut reg, handle).hold() };
2203        {
2204            let state = get_state_mut(&mut reg, handle);
2205            state.figure.set_active_axes_index(axes_index);
2206            if should_clear {
2207                state.figure.clear_axes(axes_index);
2208                state.reset_cycle(axes_index);
2209            }
2210        }
2211        if should_clear {
2212            purge_plot_children_for_axes(&mut reg, handle, axes_index);
2213        }
2214        {
2215            let state = get_state_mut(&mut reg, handle);
2216            if !opts.title.is_empty() {
2217                state.figure.set_axes_title(axes_index, opts.title);
2218            }
2219            if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2220                state
2221                    .figure
2222                    .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2223            }
2224            state.figure.set_grid(opts.grid);
2225            state.figure.set_axis_equal(opts.axis_equal);
2226
2227            let _axes_context = AxesContextGuard::install(state, axes_index);
2228            apply(&mut state.figure, axes_index)
2229                .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2230
2231            // Increment revision after a successful mutation so surfaces can avoid
2232            // re-rendering unchanged figures when "presenting" an already-loaded handle.
2233            state.revision = state.revision.wrapping_add(1);
2234        }
2235        let figure_clone = reg
2236            .figures
2237            .get(&handle)
2238            .expect("figure exists")
2239            .figure
2240            .clone();
2241        (handle, figure_clone)
2242    };
2243    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2244
2245    if rendering_disabled {
2246        if host_managed_rendering {
2247            return Ok(format!("Figure {} updated", handle.as_u32()));
2248        }
2249        return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
2250    }
2251
2252    if host_managed_rendering {
2253        return Ok(format!("Figure {} updated", handle.as_u32()));
2254    }
2255
2256    // On Web/WASM we deliberately decouple "mutate figure state" from "present pixels".
2257    // The host coalesces figure events and presents on a frame cadence, and `drawnow()` /
2258    // `pause()` provide explicit "flush" boundaries for scripts.
2259    #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2260    {
2261        let _ = figure_clone;
2262        Ok(format!("Figure {} updated", handle.as_u32()))
2263    }
2264
2265    #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2266    {
2267        let rendered = render_figure(handle, figure_clone)
2268            .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2269        Ok(format!("Figure {} updated: {rendered}", handle.as_u32()))
2270    }
2271}
2272
2273pub fn append_active_plot<F>(
2274    builtin: &'static str,
2275    opts: PlotRenderOptions<'_>,
2276    mut apply: F,
2277) -> BuiltinResult<String>
2278where
2279    F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2280{
2281    let rendering_disabled = interactive_rendering_disabled();
2282    let host_managed_rendering = host_managed_rendering_enabled();
2283    let (handle, figure_clone) = {
2284        let mut reg = registry();
2285        let handle = reg.current;
2286        let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2287        {
2288            let state = get_state_mut(&mut reg, handle);
2289            state.figure.set_active_axes_index(axes_index);
2290            if !opts.title.is_empty() {
2291                state.figure.set_axes_title(axes_index, opts.title);
2292            }
2293            if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2294                state
2295                    .figure
2296                    .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2297            }
2298            state.figure.set_grid(opts.grid);
2299            state.figure.set_axis_equal(opts.axis_equal);
2300
2301            let _axes_context = AxesContextGuard::install(state, axes_index);
2302            apply(&mut state.figure, axes_index)
2303                .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2304            state.revision = state.revision.wrapping_add(1);
2305        }
2306        let figure_clone = reg
2307            .figures
2308            .get(&handle)
2309            .expect("figure exists")
2310            .figure
2311            .clone();
2312        (handle, figure_clone)
2313    };
2314    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2315
2316    if rendering_disabled {
2317        if host_managed_rendering {
2318            return Ok(format!("Figure {} updated", handle.as_u32()));
2319        }
2320        return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
2321    }
2322
2323    if host_managed_rendering {
2324        return Ok(format!("Figure {} updated", handle.as_u32()));
2325    }
2326
2327    #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2328    {
2329        let _ = figure_clone;
2330        Ok(format!("Figure {} updated", handle.as_u32()))
2331    }
2332
2333    #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2334    {
2335        let rendered = render_figure(handle, figure_clone)
2336            .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2337        Ok(format!("Figure {} updated: {rendered}", handle.as_u32()))
2338    }
2339}
2340
2341/// Monotonic revision counter that increments on each successful mutation of the figure.
2342/// Used by web surface presentation logic to avoid redundant `render_figure` calls when
2343/// a surface is already up-to-date for a handle.
2344pub fn current_figure_revision(handle: FigureHandle) -> Option<u64> {
2345    let reg = registry();
2346    reg.figures.get(&handle).map(|state| state.revision)
2347}
2348
2349fn interactive_rendering_disabled() -> bool {
2350    std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS").is_some()
2351}
2352
2353fn host_managed_rendering_enabled() -> bool {
2354    std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS").is_some()
2355}
2356
2357#[cfg(test)]
2358pub(crate) fn disable_rendering_for_tests() {
2359    static INIT: Once = Once::new();
2360    INIT.call_once(|| unsafe {
2361        std::env::set_var("RUNMAT_DISABLE_INTERACTIVE_PLOTS", "1");
2362    });
2363}
2364
2365pub fn set_line_style_order_for_axes(axes_index: usize, order: &[LineStyle]) {
2366    if with_active_style_cycle(axes_index, |cycle| cycle.set_order(order)).is_some() {
2367        return;
2368    }
2369    let mut reg = registry();
2370    let handle = reg.current;
2371    let state = get_state_mut(&mut reg, handle);
2372    state.cycle_for_axes_mut(axes_index).set_order(order);
2373}
2374
2375pub fn next_line_style_for_axes(axes_index: usize) -> LineStyle {
2376    if let Some(style) = with_active_style_cycle(axes_index, |cycle| cycle.next()) {
2377        return style;
2378    }
2379    let mut reg = registry();
2380    let handle = reg.current;
2381    let state = get_state_mut(&mut reg, handle);
2382    state.cycle_for_axes_mut(axes_index).next()
2383}
2384
2385pub fn line_color_for_series_index(series_index: usize) -> Vec4 {
2386    let theme = current_plot_theme_config().build_theme();
2387    theme.get_data_color(series_index)
2388}
2389
2390pub fn next_line_color_for_axes(axes_index: usize) -> Vec4 {
2391    if let Some(color) = with_active_color_cycle(axes_index, |cycle| cycle.next()) {
2392        return color;
2393    }
2394    let mut reg = registry();
2395    let handle = reg.current;
2396    let state = get_state_mut(&mut reg, handle);
2397    state.color_cycle_for_axes_mut(axes_index).next()
2398}
2399
2400#[cfg(test)]
2401mod tests {
2402    use super::*;
2403    use crate::builtins::plotting::tests::ensure_plot_test_env;
2404
2405    #[cfg(test)]
2406    pub(crate) fn reset_for_tests() {
2407        let mut reg = registry();
2408        reg.figures.clear();
2409        reg.current = FigureHandle::default();
2410        reg.next_handle = FigureHandle::default().next();
2411    }
2412
2413    #[test]
2414    fn closing_last_figure_leaves_no_visible_figures() {
2415        let _guard = lock_plot_test_registry();
2416        ensure_plot_test_env();
2417        reset_for_tests();
2418
2419        let handle = new_figure_handle();
2420        assert_eq!(figure_handles(), vec![handle]);
2421
2422        close_figure(Some(handle)).expect("close figure");
2423
2424        assert!(
2425            figure_handles().is_empty(),
2426            "closing the last figure should not recreate a default visible figure"
2427        );
2428    }
2429}