Skip to main content

runmat_runtime/builtins/plotting/core/
state.rs

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