Skip to main content

runmat_runtime/builtins/plotting/core/
state.rs

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