Skip to main content

polyscope_core/
state.rs

1//! Global state management for polyscope.
2
3use std::collections::HashMap;
4use std::sync::{OnceLock, RwLock};
5
6use glam::Vec3;
7
8use crate::error::{PolyscopeError, Result};
9use crate::gizmo::GizmoConfig;
10use crate::group::Group;
11use crate::options::Options;
12use crate::quantity::Quantity;
13use crate::registry::Registry;
14use crate::slice_plane::SlicePlane;
15
16/// Callback type for file drop events.
17pub type FileDropCallback = Box<dyn FnMut(&[std::path::PathBuf]) + Send + Sync>;
18
19/// A deferred request to load a material from disk.
20#[derive(Debug, Clone)]
21pub enum MaterialLoadRequest {
22    /// Load a static material from a single file.
23    Static { name: String, path: String },
24    /// Load a blendable material from 4 files (R, G, B, K channels).
25    Blendable {
26        name: String,
27        filenames: [String; 4],
28    },
29}
30
31/// Global context singleton.
32static CONTEXT: OnceLock<RwLock<Context>> = OnceLock::new();
33
34/// The global context containing all polyscope state.
35pub struct Context {
36    /// Whether polyscope has been initialized.
37    pub initialized: bool,
38
39    /// The structure registry.
40    pub registry: Registry,
41
42    /// Groups for organizing structures.
43    pub groups: HashMap<String, Group>,
44
45    /// Slice planes for cutting through geometry.
46    pub slice_planes: HashMap<String, SlicePlane>,
47
48    /// Gizmo configuration for transformation controls.
49    pub gizmo_config: GizmoConfig,
50
51    /// Currently selected structure (`type_name`, name) for gizmo operations.
52    pub selected_structure: Option<(String, String)>,
53
54    /// Currently selected slice plane for gizmo operations.
55    pub selected_slice_plane: Option<String>,
56
57    /// Global options.
58    pub options: Options,
59
60    /// Representative length scale for all registered structures.
61    pub length_scale: f32,
62
63    /// Axis-aligned bounding box for all registered structures.
64    pub bounding_box: (Vec3, Vec3),
65
66    /// Floating quantities (not attached to any structure).
67    pub floating_quantities: Vec<Box<dyn Quantity>>,
68
69    /// Callback invoked when files are dropped onto the window.
70    pub file_drop_callback: Option<FileDropCallback>,
71
72    /// Deferred material load requests (processed by App each frame).
73    pub material_load_queue: Vec<MaterialLoadRequest>,
74}
75
76impl Default for Context {
77    fn default() -> Self {
78        Self {
79            initialized: false,
80            registry: Registry::new(),
81            groups: HashMap::new(),
82            slice_planes: HashMap::new(),
83            gizmo_config: GizmoConfig::default(),
84            selected_structure: None,
85            selected_slice_plane: None,
86            options: Options::default(),
87            length_scale: 1.0,
88            bounding_box: (Vec3::ZERO, Vec3::ONE),
89            floating_quantities: Vec::new(),
90            file_drop_callback: None,
91            material_load_queue: Vec::new(),
92        }
93    }
94}
95
96impl Context {
97    /// Computes the center of the bounding box.
98    #[must_use]
99    pub fn center(&self) -> Vec3 {
100        (self.bounding_box.0 + self.bounding_box.1) * 0.5
101    }
102
103    /// Updates the global bounding box and length scale from all structures.
104    ///
105    /// Respects the `auto_compute_scene_extents` option: when disabled, this
106    /// is a no-op and the user controls extents manually. Matches C++ Polyscope's
107    /// `updateStructureExtents()` guard behavior.
108    pub fn update_extents(&mut self) {
109        if !self.options.auto_compute_scene_extents {
110            return;
111        }
112        self.recompute_extents();
113    }
114
115    /// Unconditionally recomputes extents from all registered structures.
116    ///
117    /// Called by `update_extents()` when auto-compute is enabled, and also
118    /// called directly when the user re-enables auto-compute.
119    pub fn recompute_extents(&mut self) {
120        let mut min = Vec3::splat(f32::MAX);
121        let mut max = Vec3::splat(f32::MIN);
122        let mut has_extent = false;
123
124        for structure in self.registry.iter() {
125            if let Some((bb_min, bb_max)) = structure.bounding_box() {
126                min = min.min(bb_min);
127                max = max.max(bb_max);
128                has_extent = true;
129            }
130        }
131
132        if has_extent {
133            self.bounding_box = (min, max);
134            self.length_scale = (max - min).length();
135
136            // Handle degenerate bounding box (all points coincide).
137            // Matches C++ Polyscope commit 3198ab5 — tolerance 1e-3.
138            if min == max {
139                let offset_scale = if self.length_scale == 0.0 {
140                    1e-3
141                } else {
142                    self.length_scale * 1e-3
143                };
144                let offset = Vec3::splat(offset_scale / 2.0);
145                self.bounding_box = (min - offset, max + offset);
146            }
147        } else {
148            self.bounding_box = (Vec3::ZERO, Vec3::ONE);
149            self.length_scale = 1.0;
150        }
151    }
152
153    /// Creates a new group.
154    pub fn create_group(&mut self, name: &str) -> &mut Group {
155        self.groups
156            .entry(name.to_string())
157            .or_insert_with(|| Group::new(name))
158    }
159
160    /// Gets a group by name.
161    #[must_use]
162    pub fn get_group(&self, name: &str) -> Option<&Group> {
163        self.groups.get(name)
164    }
165
166    /// Gets a mutable group by name.
167    pub fn get_group_mut(&mut self, name: &str) -> Option<&mut Group> {
168        self.groups.get_mut(name)
169    }
170
171    /// Removes a group by name.
172    pub fn remove_group(&mut self, name: &str) -> Option<Group> {
173        self.groups.remove(name)
174    }
175
176    /// Returns true if a group with the given name exists.
177    #[must_use]
178    pub fn has_group(&self, name: &str) -> bool {
179        self.groups.contains_key(name)
180    }
181
182    /// Returns all group names.
183    #[must_use]
184    pub fn group_names(&self) -> Vec<&str> {
185        self.groups
186            .keys()
187            .map(std::string::String::as_str)
188            .collect()
189    }
190
191    /// Checks if a structure should be visible, combining its own enabled state
192    /// with group visibility.
193    ///
194    /// A structure is visible if:
195    /// - Its own `is_enabled()` returns true, AND
196    /// - It's not in any disabled group (checked via `is_structure_visible_in_groups`)
197    #[must_use]
198    pub fn is_structure_visible(&self, structure: &dyn crate::Structure) -> bool {
199        structure.is_enabled()
200            && self.is_structure_visible_in_groups(structure.type_name(), structure.name())
201    }
202
203    /// Checks if a structure should be visible based on its group membership.
204    ///
205    /// A structure is visible if:
206    /// - It's not in any group, or
207    /// - All of its ancestor groups are enabled
208    #[must_use]
209    pub fn is_structure_visible_in_groups(&self, type_name: &str, name: &str) -> bool {
210        // Find all groups that contain this structure
211        for group in self.groups.values() {
212            if group.contains_structure(type_name, name) {
213                // Check if this group and all its ancestors are enabled
214                if !self.is_group_and_ancestors_enabled(group.name()) {
215                    return false;
216                }
217            }
218        }
219        true
220    }
221
222    /// Checks if a group and all its ancestors are enabled.
223    fn is_group_and_ancestors_enabled(&self, group_name: &str) -> bool {
224        let mut current = group_name;
225        while let Some(group) = self.groups.get(current) {
226            if !group.is_enabled() {
227                return false;
228            }
229            if let Some(parent) = group.parent_group() {
230                current = parent;
231            } else {
232                break;
233            }
234        }
235        true
236    }
237
238    // ========================================================================
239    // Slice Plane Management
240    // ========================================================================
241
242    /// Adds a new slice plane.
243    pub fn add_slice_plane(&mut self, name: &str) -> &mut SlicePlane {
244        self.slice_planes
245            .entry(name.to_string())
246            .or_insert_with(|| SlicePlane::new(name))
247    }
248
249    /// Gets a slice plane by name.
250    #[must_use]
251    pub fn get_slice_plane(&self, name: &str) -> Option<&SlicePlane> {
252        self.slice_planes.get(name)
253    }
254
255    /// Gets a mutable slice plane by name.
256    pub fn get_slice_plane_mut(&mut self, name: &str) -> Option<&mut SlicePlane> {
257        self.slice_planes.get_mut(name)
258    }
259
260    /// Removes a slice plane by name.
261    pub fn remove_slice_plane(&mut self, name: &str) -> Option<SlicePlane> {
262        self.slice_planes.remove(name)
263    }
264
265    /// Returns true if a slice plane with the given name exists.
266    #[must_use]
267    pub fn has_slice_plane(&self, name: &str) -> bool {
268        self.slice_planes.contains_key(name)
269    }
270
271    /// Returns all slice plane names.
272    #[must_use]
273    pub fn slice_plane_names(&self) -> Vec<&str> {
274        self.slice_planes
275            .keys()
276            .map(std::string::String::as_str)
277            .collect()
278    }
279
280    /// Returns the number of slice planes.
281    #[must_use]
282    pub fn num_slice_planes(&self) -> usize {
283        self.slice_planes.len()
284    }
285
286    /// Returns an iterator over all slice planes.
287    pub fn slice_planes(&self) -> impl Iterator<Item = &SlicePlane> {
288        self.slice_planes.values()
289    }
290
291    /// Returns an iterator over all enabled slice planes.
292    pub fn enabled_slice_planes(&self) -> impl Iterator<Item = &SlicePlane> {
293        self.slice_planes.values().filter(|sp| sp.is_enabled())
294    }
295
296    // ========================================================================
297    // Gizmo and Selection Management
298    // ========================================================================
299
300    /// Selects a structure for gizmo manipulation.
301    /// This deselects any selected slice plane (mutual exclusion).
302    pub fn select_structure(&mut self, type_name: &str, name: &str) {
303        self.selected_slice_plane = None; // Mutual exclusion
304        self.selected_structure = Some((type_name.to_string(), name.to_string()));
305    }
306
307    /// Deselects the current structure.
308    pub fn deselect_structure(&mut self) {
309        self.selected_structure = None;
310    }
311
312    /// Returns the currently selected structure, if any.
313    #[must_use]
314    pub fn selected_structure(&self) -> Option<(&str, &str)> {
315        self.selected_structure
316            .as_ref()
317            .map(|(t, n)| (t.as_str(), n.as_str()))
318    }
319
320    /// Returns whether a structure is selected.
321    #[must_use]
322    pub fn has_selection(&self) -> bool {
323        self.selected_structure.is_some()
324    }
325
326    /// Selects a slice plane for gizmo manipulation.
327    /// This deselects any selected structure (mutual exclusion).
328    pub fn select_slice_plane(&mut self, name: &str) {
329        self.selected_structure = None; // Mutual exclusion
330        self.selected_slice_plane = Some(name.to_string());
331    }
332
333    /// Deselects the current slice plane.
334    pub fn deselect_slice_plane(&mut self) {
335        self.selected_slice_plane = None;
336    }
337
338    /// Returns the currently selected slice plane name, if any.
339    #[must_use]
340    pub fn selected_slice_plane(&self) -> Option<&str> {
341        self.selected_slice_plane.as_deref()
342    }
343
344    /// Returns whether a slice plane is selected.
345    #[must_use]
346    pub fn has_slice_plane_selection(&self) -> bool {
347        self.selected_slice_plane.is_some()
348    }
349
350    /// Returns the gizmo configuration.
351    #[must_use]
352    pub fn gizmo(&self) -> &GizmoConfig {
353        &self.gizmo_config
354    }
355
356    /// Returns the mutable gizmo configuration.
357    pub fn gizmo_mut(&mut self) -> &mut GizmoConfig {
358        &mut self.gizmo_config
359    }
360}
361
362/// Initializes the global context.
363///
364/// This should be called once at the start of the program.
365pub fn init_context() -> Result<()> {
366    let context = RwLock::new(Context::default());
367
368    CONTEXT
369        .set(context)
370        .map_err(|_| PolyscopeError::AlreadyInitialized)?;
371
372    with_context_mut(|ctx| {
373        ctx.initialized = true;
374    });
375
376    Ok(())
377}
378
379/// Returns whether the context has been initialized.
380pub fn is_initialized() -> bool {
381    CONTEXT
382        .get()
383        .and_then(|lock| lock.read().ok())
384        .is_some_and(|ctx| ctx.initialized)
385}
386
387/// Access the global context for reading.
388///
389/// # Panics
390///
391/// Panics if polyscope has not been initialized.
392pub fn with_context<F, R>(f: F) -> R
393where
394    F: FnOnce(&Context) -> R,
395{
396    let lock = CONTEXT.get().expect("polyscope not initialized");
397    let guard = lock.read().expect("context lock poisoned");
398    f(&guard)
399}
400
401/// Access the global context for writing.
402///
403/// # Panics
404///
405/// Panics if polyscope has not been initialized.
406pub fn with_context_mut<F, R>(f: F) -> R
407where
408    F: FnOnce(&mut Context) -> R,
409{
410    let lock = CONTEXT.get().expect("polyscope not initialized");
411    let mut guard = lock.write().expect("context lock poisoned");
412    f(&mut guard)
413}
414
415/// Try to access the global context for reading.
416///
417/// Returns `None` if polyscope has not been initialized.
418pub fn try_with_context<F, R>(f: F) -> Option<R>
419where
420    F: FnOnce(&Context) -> R,
421{
422    let lock = CONTEXT.get()?;
423    let guard = lock.read().ok()?;
424    Some(f(&guard))
425}
426
427/// Try to access the global context for writing.
428///
429/// Returns `None` if polyscope has not been initialized.
430pub fn try_with_context_mut<F, R>(f: F) -> Option<R>
431where
432    F: FnOnce(&mut Context) -> R,
433{
434    let lock = CONTEXT.get()?;
435    let mut guard = lock.write().ok()?;
436    Some(f(&mut guard))
437}
438
439/// Shuts down the global context.
440///
441/// Note: Due to `OnceLock` semantics, the context cannot be re-initialized
442/// after shutdown in the same process.
443pub fn shutdown_context() {
444    if let Some(lock) = CONTEXT.get() {
445        if let Ok(mut ctx) = lock.write() {
446            ctx.initialized = false;
447            ctx.registry.clear();
448            ctx.groups.clear();
449            ctx.slice_planes.clear();
450            ctx.selected_structure = None;
451            ctx.selected_slice_plane = None;
452            ctx.floating_quantities.clear();
453            ctx.material_load_queue.clear();
454        }
455    }
456}