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        } else {
136            self.bounding_box = (Vec3::ZERO, Vec3::ONE);
137            self.length_scale = 1.0;
138        }
139    }
140
141    /// Creates a new group.
142    pub fn create_group(&mut self, name: &str) -> &mut Group {
143        self.groups
144            .entry(name.to_string())
145            .or_insert_with(|| Group::new(name))
146    }
147
148    /// Gets a group by name.
149    #[must_use]
150    pub fn get_group(&self, name: &str) -> Option<&Group> {
151        self.groups.get(name)
152    }
153
154    /// Gets a mutable group by name.
155    pub fn get_group_mut(&mut self, name: &str) -> Option<&mut Group> {
156        self.groups.get_mut(name)
157    }
158
159    /// Removes a group by name.
160    pub fn remove_group(&mut self, name: &str) -> Option<Group> {
161        self.groups.remove(name)
162    }
163
164    /// Returns true if a group with the given name exists.
165    #[must_use]
166    pub fn has_group(&self, name: &str) -> bool {
167        self.groups.contains_key(name)
168    }
169
170    /// Returns all group names.
171    #[must_use]
172    pub fn group_names(&self) -> Vec<&str> {
173        self.groups
174            .keys()
175            .map(std::string::String::as_str)
176            .collect()
177    }
178
179    /// Checks if a structure should be visible, combining its own enabled state
180    /// with group visibility.
181    ///
182    /// A structure is visible if:
183    /// - Its own `is_enabled()` returns true, AND
184    /// - It's not in any disabled group (checked via `is_structure_visible_in_groups`)
185    #[must_use]
186    pub fn is_structure_visible(&self, structure: &dyn crate::Structure) -> bool {
187        structure.is_enabled()
188            && self.is_structure_visible_in_groups(structure.type_name(), structure.name())
189    }
190
191    /// Checks if a structure should be visible based on its group membership.
192    ///
193    /// A structure is visible if:
194    /// - It's not in any group, or
195    /// - All of its ancestor groups are enabled
196    #[must_use]
197    pub fn is_structure_visible_in_groups(&self, type_name: &str, name: &str) -> bool {
198        // Find all groups that contain this structure
199        for group in self.groups.values() {
200            if group.contains_structure(type_name, name) {
201                // Check if this group and all its ancestors are enabled
202                if !self.is_group_and_ancestors_enabled(group.name()) {
203                    return false;
204                }
205            }
206        }
207        true
208    }
209
210    /// Checks if a group and all its ancestors are enabled.
211    fn is_group_and_ancestors_enabled(&self, group_name: &str) -> bool {
212        let mut current = group_name;
213        while let Some(group) = self.groups.get(current) {
214            if !group.is_enabled() {
215                return false;
216            }
217            if let Some(parent) = group.parent_group() {
218                current = parent;
219            } else {
220                break;
221            }
222        }
223        true
224    }
225
226    // ========================================================================
227    // Slice Plane Management
228    // ========================================================================
229
230    /// Adds a new slice plane.
231    pub fn add_slice_plane(&mut self, name: &str) -> &mut SlicePlane {
232        self.slice_planes
233            .entry(name.to_string())
234            .or_insert_with(|| SlicePlane::new(name))
235    }
236
237    /// Gets a slice plane by name.
238    #[must_use]
239    pub fn get_slice_plane(&self, name: &str) -> Option<&SlicePlane> {
240        self.slice_planes.get(name)
241    }
242
243    /// Gets a mutable slice plane by name.
244    pub fn get_slice_plane_mut(&mut self, name: &str) -> Option<&mut SlicePlane> {
245        self.slice_planes.get_mut(name)
246    }
247
248    /// Removes a slice plane by name.
249    pub fn remove_slice_plane(&mut self, name: &str) -> Option<SlicePlane> {
250        self.slice_planes.remove(name)
251    }
252
253    /// Returns true if a slice plane with the given name exists.
254    #[must_use]
255    pub fn has_slice_plane(&self, name: &str) -> bool {
256        self.slice_planes.contains_key(name)
257    }
258
259    /// Returns all slice plane names.
260    #[must_use]
261    pub fn slice_plane_names(&self) -> Vec<&str> {
262        self.slice_planes
263            .keys()
264            .map(std::string::String::as_str)
265            .collect()
266    }
267
268    /// Returns the number of slice planes.
269    #[must_use]
270    pub fn num_slice_planes(&self) -> usize {
271        self.slice_planes.len()
272    }
273
274    /// Returns an iterator over all slice planes.
275    pub fn slice_planes(&self) -> impl Iterator<Item = &SlicePlane> {
276        self.slice_planes.values()
277    }
278
279    /// Returns an iterator over all enabled slice planes.
280    pub fn enabled_slice_planes(&self) -> impl Iterator<Item = &SlicePlane> {
281        self.slice_planes.values().filter(|sp| sp.is_enabled())
282    }
283
284    // ========================================================================
285    // Gizmo and Selection Management
286    // ========================================================================
287
288    /// Selects a structure for gizmo manipulation.
289    /// This deselects any selected slice plane (mutual exclusion).
290    pub fn select_structure(&mut self, type_name: &str, name: &str) {
291        self.selected_slice_plane = None; // Mutual exclusion
292        self.selected_structure = Some((type_name.to_string(), name.to_string()));
293    }
294
295    /// Deselects the current structure.
296    pub fn deselect_structure(&mut self) {
297        self.selected_structure = None;
298    }
299
300    /// Returns the currently selected structure, if any.
301    #[must_use]
302    pub fn selected_structure(&self) -> Option<(&str, &str)> {
303        self.selected_structure
304            .as_ref()
305            .map(|(t, n)| (t.as_str(), n.as_str()))
306    }
307
308    /// Returns whether a structure is selected.
309    #[must_use]
310    pub fn has_selection(&self) -> bool {
311        self.selected_structure.is_some()
312    }
313
314    /// Selects a slice plane for gizmo manipulation.
315    /// This deselects any selected structure (mutual exclusion).
316    pub fn select_slice_plane(&mut self, name: &str) {
317        self.selected_structure = None; // Mutual exclusion
318        self.selected_slice_plane = Some(name.to_string());
319    }
320
321    /// Deselects the current slice plane.
322    pub fn deselect_slice_plane(&mut self) {
323        self.selected_slice_plane = None;
324    }
325
326    /// Returns the currently selected slice plane name, if any.
327    #[must_use]
328    pub fn selected_slice_plane(&self) -> Option<&str> {
329        self.selected_slice_plane.as_deref()
330    }
331
332    /// Returns whether a slice plane is selected.
333    #[must_use]
334    pub fn has_slice_plane_selection(&self) -> bool {
335        self.selected_slice_plane.is_some()
336    }
337
338    /// Returns the gizmo configuration.
339    #[must_use]
340    pub fn gizmo(&self) -> &GizmoConfig {
341        &self.gizmo_config
342    }
343
344    /// Returns the mutable gizmo configuration.
345    pub fn gizmo_mut(&mut self) -> &mut GizmoConfig {
346        &mut self.gizmo_config
347    }
348}
349
350/// Initializes the global context.
351///
352/// This should be called once at the start of the program.
353pub fn init_context() -> Result<()> {
354    let context = RwLock::new(Context::default());
355
356    CONTEXT
357        .set(context)
358        .map_err(|_| PolyscopeError::AlreadyInitialized)?;
359
360    with_context_mut(|ctx| {
361        ctx.initialized = true;
362    });
363
364    Ok(())
365}
366
367/// Returns whether the context has been initialized.
368pub fn is_initialized() -> bool {
369    CONTEXT
370        .get()
371        .and_then(|lock| lock.read().ok())
372        .is_some_and(|ctx| ctx.initialized)
373}
374
375/// Access the global context for reading.
376///
377/// # Panics
378///
379/// Panics if polyscope has not been initialized.
380pub fn with_context<F, R>(f: F) -> R
381where
382    F: FnOnce(&Context) -> R,
383{
384    let lock = CONTEXT.get().expect("polyscope not initialized");
385    let guard = lock.read().expect("context lock poisoned");
386    f(&guard)
387}
388
389/// Access the global context for writing.
390///
391/// # Panics
392///
393/// Panics if polyscope has not been initialized.
394pub fn with_context_mut<F, R>(f: F) -> R
395where
396    F: FnOnce(&mut Context) -> R,
397{
398    let lock = CONTEXT.get().expect("polyscope not initialized");
399    let mut guard = lock.write().expect("context lock poisoned");
400    f(&mut guard)
401}
402
403/// Try to access the global context for reading.
404///
405/// Returns `None` if polyscope has not been initialized.
406pub fn try_with_context<F, R>(f: F) -> Option<R>
407where
408    F: FnOnce(&Context) -> R,
409{
410    let lock = CONTEXT.get()?;
411    let guard = lock.read().ok()?;
412    Some(f(&guard))
413}
414
415/// Try to access the global context for writing.
416///
417/// Returns `None` if polyscope has not been initialized.
418pub fn try_with_context_mut<F, R>(f: F) -> Option<R>
419where
420    F: FnOnce(&mut Context) -> R,
421{
422    let lock = CONTEXT.get()?;
423    let mut guard = lock.write().ok()?;
424    Some(f(&mut guard))
425}
426
427/// Shuts down the global context.
428///
429/// Note: Due to `OnceLock` semantics, the context cannot be re-initialized
430/// after shutdown in the same process.
431pub fn shutdown_context() {
432    if let Some(lock) = CONTEXT.get() {
433        if let Ok(mut ctx) = lock.write() {
434            ctx.initialized = false;
435            ctx.registry.clear();
436            ctx.groups.clear();
437            ctx.slice_planes.clear();
438            ctx.selected_structure = None;
439            ctx.selected_slice_plane = None;
440            ctx.floating_quantities.clear();
441            ctx.material_load_queue.clear();
442        }
443    }
444}