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