1#![allow(clippy::cast_possible_truncation)]
50#![allow(clippy::cast_sign_loss)]
51#![allow(clippy::cast_precision_loss)]
52#![allow(clippy::missing_errors_doc)]
54#![allow(clippy::missing_panics_doc)]
55#![allow(clippy::too_many_lines)]
57#![allow(clippy::items_after_statements)]
59#![allow(clippy::too_many_arguments)]
61#![allow(clippy::unused_self)]
63#![allow(clippy::needless_pass_by_value)]
65#![allow(clippy::many_single_char_names)]
67#![allow(clippy::struct_excessive_bools)]
69#![allow(clippy::must_use_candidate)]
72#![allow(clippy::doc_markdown)]
74
75macro_rules! impl_structure_accessors {
78 (
79 get_fn = $get_fn:ident,
80 with_fn = $with_fn:ident,
81 with_ref_fn = $with_ref_fn:ident,
82 handle = $handle:ident,
83 type_name = $type_name:expr,
84 rust_type = $rust_type:ty,
85 doc_name = $doc_name:expr
86 ) => {
87 #[doc = concat!("Gets a registered ", $doc_name, " by name.")]
88 #[must_use]
89 pub fn $get_fn(name: &str) -> Option<$handle> {
90 crate::with_context(|ctx| {
91 if ctx.registry.contains($type_name, name) {
92 Some($handle {
93 name: name.to_string(),
94 })
95 } else {
96 None
97 }
98 })
99 }
100
101 #[doc = concat!("Executes a closure with mutable access to a registered ", $doc_name, ".\n\nReturns `None` if the ", $doc_name, " does not exist.")]
102 pub fn $with_fn<F, R>(name: &str, f: F) -> Option<R>
103 where
104 F: FnOnce(&mut $rust_type) -> R,
105 {
106 crate::with_context_mut(|ctx| {
107 ctx.registry
108 .get_mut($type_name, name)
109 .and_then(|s| s.as_any_mut().downcast_mut::<$rust_type>())
110 .map(f)
111 })
112 }
113
114 #[doc = concat!("Executes a closure with immutable access to a registered ", $doc_name, ".\n\nReturns `None` if the ", $doc_name, " does not exist.")]
115 pub fn $with_ref_fn<F, R>(name: &str, f: F) -> Option<R>
116 where
117 F: FnOnce(&$rust_type) -> R,
118 {
119 crate::with_context(|ctx| {
120 ctx.registry
121 .get($type_name, name)
122 .and_then(|s| s.as_any().downcast_ref::<$rust_type>())
123 .map(f)
124 })
125 }
126 };
127}
128
129mod app;
130mod camera_view;
131mod curve_network;
132mod floating;
133mod gizmo;
134mod groups;
135mod headless;
136mod init;
137mod point_cloud;
138mod screenshot;
139mod slice_plane;
140mod surface_mesh;
141mod transform;
142mod ui_sync;
143mod volume_grid;
144mod volume_mesh;
145
146pub use polyscope_core::{
148 Mat4, Vec2, Vec3, Vec4,
149 error::{PolyscopeError, Result},
150 gizmo::{GizmoAxis, GizmoConfig, GizmoMode, GizmoSpace, Transform},
151 group::Group,
152 options::Options,
153 pick::{PickResult, Pickable},
154 quantity::{ParamCoordsType, ParamVizStyle, Quantity, QuantityKind},
155 registry::Registry,
156 slice_plane::{MAX_SLICE_PLANES, SlicePlane, SlicePlaneUniforms},
157 state::{Context, with_context, with_context_mut},
158 structure::{HasQuantities, Structure},
159};
160
161pub use polyscope_render::{
163 AxisDirection, Camera, ColorMap, ColorMapRegistry, Material, MaterialRegistry, NavigationStyle,
164 PickElementType, ProjectionMode, RenderContext, RenderEngine, ScreenshotError,
165 ScreenshotOptions,
166};
167
168pub use polyscope_ui::{
170 AppearanceSettings, CameraSettings, GizmoAction, GizmoSettings, GroupSettings, GroupsAction,
171 SceneExtents, SelectionInfo, SlicePlaneGizmoAction, SlicePlaneSelectionInfo,
172 SlicePlaneSettings, SlicePlanesAction, ViewAction,
173};
174
175pub use polyscope_structures::volume_grid::VolumeGridVizMode;
177pub use polyscope_structures::{
178 CameraExtrinsics, CameraIntrinsics, CameraParameters, CameraView, CurveNetwork, PointCloud,
179 SurfaceMesh, VolumeCellType, VolumeGrid, VolumeMesh,
180};
181
182pub use camera_view::*;
184pub use curve_network::*;
185pub use floating::*;
186pub use gizmo::*;
187pub use groups::*;
188pub use headless::*;
189pub use init::*;
190pub use point_cloud::*;
191pub use screenshot::*;
192pub use slice_plane::*;
193pub use surface_mesh::*;
194pub use transform::*;
195pub use ui_sync::*;
196pub use volume_grid::*;
197pub use volume_mesh::*;
198
199pub fn remove_structure(name: &str) {
201 with_context_mut(|ctx| {
202 ctx.registry.remove("PointCloud", name);
204 ctx.registry.remove("SurfaceMesh", name);
205 ctx.registry.remove("CurveNetwork", name);
206 ctx.registry.remove("VolumeMesh", name);
207 ctx.registry.remove("VolumeGrid", name);
208 ctx.registry.remove("CameraView", name);
209 ctx.update_extents();
210 });
211}
212
213pub fn remove_all_structures() {
215 with_context_mut(|ctx| {
216 ctx.registry.clear();
217 ctx.update_extents();
218 });
219}
220
221pub fn set_file_drop_callback(callback: impl FnMut(&[std::path::PathBuf]) + Send + Sync + 'static) {
234 with_context_mut(|ctx| {
235 ctx.file_drop_callback = Some(Box::new(callback));
236 });
237}
238
239pub fn clear_file_drop_callback() {
241 with_context_mut(|ctx| {
242 ctx.file_drop_callback = None;
243 });
244}
245
246pub fn load_blendable_material(name: &str, filenames: [&str; 4]) {
263 with_context_mut(|ctx| {
264 ctx.material_load_queue
265 .push(polyscope_core::state::MaterialLoadRequest::Blendable {
266 name: name.to_string(),
267 filenames: [
268 filenames[0].to_string(),
269 filenames[1].to_string(),
270 filenames[2].to_string(),
271 filenames[3].to_string(),
272 ],
273 });
274 });
275}
276
277pub fn load_blendable_material_ext(name: &str, base: &str, ext: &str) {
284 load_blendable_material(
285 name,
286 [
287 &format!("{base}_r{ext}"),
288 &format!("{base}_g{ext}"),
289 &format!("{base}_b{ext}"),
290 &format!("{base}_k{ext}"),
291 ],
292 );
293}
294
295pub fn load_static_material(name: &str, filename: &str) {
305 with_context_mut(|ctx| {
306 ctx.material_load_queue
307 .push(polyscope_core::state::MaterialLoadRequest::Static {
308 name: name.to_string(),
309 path: filename.to_string(),
310 });
311 });
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use std::sync::atomic::{AtomicU32, Ordering};
318
319 static COUNTER: AtomicU32 = AtomicU32::new(0);
321
322 fn unique_name(prefix: &str) -> String {
323 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
324 format!("{prefix}_{n}")
325 }
326
327 fn setup() {
328 let _ = init();
331 }
332
333 #[test]
334 fn test_register_curve_network() {
335 setup();
336 let name = unique_name("test_cn");
337 let nodes = vec![
338 Vec3::new(0.0, 0.0, 0.0),
339 Vec3::new(1.0, 0.0, 0.0),
340 Vec3::new(1.0, 1.0, 0.0),
341 ];
342 let edges = vec![[0, 1], [1, 2]];
343
344 let handle = register_curve_network(&name, nodes, edges);
345 assert_eq!(handle.name(), name);
346
347 let found = get_curve_network(&name);
349 assert!(found.is_some());
350
351 let not_found = get_curve_network("nonexistent_xyz_123");
353 assert!(not_found.is_none());
354 }
355
356 #[test]
357 fn test_register_curve_network_line() {
358 setup();
359 let name = unique_name("line");
360 let nodes = vec![
361 Vec3::new(0.0, 0.0, 0.0),
362 Vec3::new(1.0, 0.0, 0.0),
363 Vec3::new(2.0, 0.0, 0.0),
364 Vec3::new(3.0, 0.0, 0.0),
365 ];
366
367 register_curve_network_line(&name, nodes);
368
369 let num_edges = with_curve_network_ref(&name, |cn| cn.num_edges());
370 assert_eq!(num_edges, Some(3)); }
372
373 #[test]
374 fn test_register_curve_network_loop() {
375 setup();
376 let name = unique_name("loop");
377 let nodes = vec![
378 Vec3::new(0.0, 0.0, 0.0),
379 Vec3::new(1.0, 0.0, 0.0),
380 Vec3::new(1.0, 1.0, 0.0),
381 ];
382
383 register_curve_network_loop(&name, nodes);
384
385 let num_edges = with_curve_network_ref(&name, |cn| cn.num_edges());
386 assert_eq!(num_edges, Some(3)); }
388
389 #[test]
390 fn test_register_curve_network_segments() {
391 setup();
392 let name = unique_name("segs");
393 let nodes = vec![
394 Vec3::new(0.0, 0.0, 0.0),
395 Vec3::new(1.0, 0.0, 0.0),
396 Vec3::new(2.0, 0.0, 0.0),
397 Vec3::new(3.0, 0.0, 0.0),
398 ];
399
400 register_curve_network_segments(&name, nodes);
401
402 let num_edges = with_curve_network_ref(&name, |cn| cn.num_edges());
403 assert_eq!(num_edges, Some(2)); }
405
406 #[test]
407 fn test_curve_network_handle_methods() {
408 setup();
409 let name = unique_name("handle_test");
410 let nodes = vec![Vec3::ZERO, Vec3::X];
411 let edges = vec![[0, 1]];
412
413 let handle = register_curve_network(&name, nodes, edges);
414
415 handle
417 .set_color(Vec3::new(1.0, 0.0, 0.0))
418 .set_radius(0.1, false)
419 .set_material("clay");
420
421 with_curve_network_ref(&name, |cn| {
423 assert_eq!(cn.color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
424 assert_eq!(cn.radius(), 0.1);
425 assert!(!cn.radius_is_relative());
426 assert_eq!(cn.material(), "clay");
427 });
428 }
429
430 #[test]
431 fn test_with_curve_network() {
432 setup();
433 let name = unique_name("with_test");
434 let nodes = vec![Vec3::ZERO, Vec3::X, Vec3::Y];
435 let edges = vec![[0, 1], [1, 2]];
436
437 register_curve_network(&name, nodes, edges);
438
439 let result = with_curve_network(&name, |cn| {
441 cn.set_color(Vec3::new(0.5, 0.5, 0.5));
442 cn.num_nodes()
443 });
444 assert_eq!(result, Some(3));
445
446 let color = with_curve_network_ref(&name, |cn| cn.color());
448 assert_eq!(color, Some(Vec4::new(0.5, 0.5, 0.5, 1.0)));
449 }
450
451 #[test]
452 fn test_create_group() {
453 setup();
454 let name = unique_name("test_group");
455 let handle = create_group(&name);
456 assert_eq!(handle.name(), name);
457 assert!(handle.is_enabled());
458 }
459
460 #[test]
461 fn test_get_group() {
462 setup();
463 let name = unique_name("get_group");
464 create_group(&name);
465
466 let found = get_group(&name);
467 assert!(found.is_some());
468 assert_eq!(found.unwrap().name(), name);
469
470 let not_found = get_group("nonexistent_group_xyz");
471 assert!(not_found.is_none());
472 }
473
474 #[test]
475 fn test_group_enable_disable() {
476 setup();
477 let name = unique_name("enable_group");
478 let handle = create_group(&name);
479
480 assert!(handle.is_enabled());
481 handle.set_enabled(false);
482 assert!(!handle.is_enabled());
483 handle.set_enabled(true);
484 assert!(handle.is_enabled());
485 }
486
487 #[test]
488 fn test_group_add_structures() {
489 setup();
490 let group_name = unique_name("struct_group");
491 let pc_name = unique_name("pc_in_group");
492
493 register_point_cloud(&pc_name, vec![Vec3::ZERO, Vec3::X]);
495
496 let handle = create_group(&group_name);
498 handle.add_point_cloud(&pc_name);
499
500 assert_eq!(handle.num_structures(), 1);
501 }
502
503 #[test]
504 fn test_group_hierarchy() {
505 setup();
506 let parent_name = unique_name("parent_group");
507 let child_name = unique_name("child_group");
508
509 let parent = create_group(&parent_name);
510 let _child = create_group(&child_name);
511
512 parent.add_child_group(&child_name);
513
514 assert_eq!(parent.num_child_groups(), 1);
515 }
516
517 #[test]
518 fn test_remove_group() {
519 setup();
520 let name = unique_name("remove_group");
521 create_group(&name);
522
523 assert!(get_group(&name).is_some());
524 remove_group(&name);
525 assert!(get_group(&name).is_none());
526 }
527
528 #[test]
529 fn test_add_slice_plane() {
530 setup();
531 let name = unique_name("slice_plane");
532 let handle = add_slice_plane(&name);
533 assert_eq!(handle.name(), name);
534 assert!(handle.is_enabled());
535 }
536
537 #[test]
538 fn test_slice_plane_pose() {
539 setup();
540 let name = unique_name("slice_pose");
541 let handle = add_slice_plane_with_pose(&name, Vec3::new(1.0, 2.0, 3.0), Vec3::X);
542
543 assert_eq!(handle.origin(), Vec3::new(1.0, 2.0, 3.0));
544 assert_eq!(handle.normal(), Vec3::X);
545 }
546
547 #[test]
548 fn test_slice_plane_setters() {
549 setup();
550 let name = unique_name("slice_setters");
551 let handle = add_slice_plane(&name);
552
553 handle
554 .set_origin(Vec3::new(1.0, 0.0, 0.0))
555 .set_normal(Vec3::Z)
556 .set_color(Vec3::new(1.0, 0.0, 0.0))
557 .set_transparency(0.5);
558
559 assert_eq!(handle.origin(), Vec3::new(1.0, 0.0, 0.0));
560 assert_eq!(handle.normal(), Vec3::Z);
561 assert_eq!(handle.color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
562 assert!((handle.transparency() - 0.5).abs() < 0.001);
563 }
564
565 #[test]
566 fn test_slice_plane_enable_disable() {
567 setup();
568 let name = unique_name("slice_enable");
569 let handle = add_slice_plane(&name);
570
571 assert!(handle.is_enabled());
572 handle.set_enabled(false);
573 assert!(!handle.is_enabled());
574 handle.set_enabled(true);
575 assert!(handle.is_enabled());
576 }
577
578 #[test]
579 fn test_remove_slice_plane() {
580 setup();
581 let name = unique_name("slice_remove");
582 add_slice_plane(&name);
583
584 assert!(get_slice_plane(&name).is_some());
585 remove_slice_plane(&name);
586 assert!(get_slice_plane(&name).is_none());
587 }
588
589 #[test]
590 fn test_select_structure() {
591 setup();
592 let name = unique_name("select_pc");
593 register_point_cloud(&name, vec![Vec3::ZERO]);
594
595 assert!(!has_selection());
596
597 select_structure("PointCloud", &name);
598 assert!(has_selection());
599
600 let selected = get_selected_structure();
601 assert!(selected.is_some());
602 let (type_name, struct_name) = selected.unwrap();
603 assert_eq!(type_name, "PointCloud");
604 assert_eq!(struct_name, name);
605
606 deselect_structure();
607 assert!(!has_selection());
608 }
609
610 #[test]
611 fn test_slice_plane_gizmo_selection() {
612 setup();
613 let name = unique_name("slice_gizmo");
614 add_slice_plane(&name);
615
616 let info = get_slice_plane_selection_info();
618 assert!(!info.has_selection);
619
620 select_slice_plane_for_gizmo(&name);
622 let info = get_slice_plane_selection_info();
623 assert!(info.has_selection);
624 assert_eq!(info.name, name);
625
626 deselect_slice_plane_gizmo();
628 let info = get_slice_plane_selection_info();
629 assert!(!info.has_selection);
630 }
631
632 #[test]
633 fn test_slice_plane_structure_mutual_exclusion() {
634 setup();
635 let pc_name = unique_name("mutual_pc");
636 let plane_name = unique_name("mutual_plane");
637
638 register_point_cloud(&pc_name, vec![Vec3::ZERO]);
639 add_slice_plane(&plane_name);
640
641 select_structure("PointCloud", &pc_name);
643 assert!(has_selection());
644
645 select_slice_plane_for_gizmo(&plane_name);
647 assert!(!has_selection()); let info = get_slice_plane_selection_info();
649 assert!(info.has_selection);
650
651 select_structure("PointCloud", &pc_name);
653 assert!(has_selection());
654 let info = get_slice_plane_selection_info();
655 assert!(!info.has_selection); }
657
658 #[test]
659 fn test_structure_transform() {
660 setup();
661 let name = unique_name("transform_pc");
662 register_point_cloud(&name, vec![Vec3::ZERO, Vec3::X]);
663
664 let transform = get_point_cloud_transform(&name);
666 assert!(transform.is_some());
667
668 let new_transform = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
670 set_point_cloud_transform(&name, new_transform);
671
672 let transform = get_point_cloud_transform(&name).unwrap();
673 let translation = transform.w_axis.truncate();
674 assert!((translation - Vec3::new(1.0, 2.0, 3.0)).length() < 0.001);
675 }
676
677 #[test]
678 fn test_get_slice_plane_settings() {
679 setup();
680 let name = unique_name("ui_slice_plane");
681
682 add_slice_plane_with_pose(&name, Vec3::new(1.0, 2.0, 3.0), Vec3::X);
684
685 let settings = get_slice_plane_settings();
687 let found = settings.iter().find(|s| s.name == name);
688 assert!(found.is_some());
689
690 let s = found.unwrap();
691 assert_eq!(s.origin, [1.0, 2.0, 3.0]);
692 assert_eq!(s.normal, [1.0, 0.0, 0.0]);
693 assert!(s.enabled);
694 }
695
696 #[test]
697 fn test_apply_slice_plane_settings() {
698 setup();
699 let name = unique_name("apply_slice_plane");
700
701 add_slice_plane(&name);
703
704 let settings = polyscope_ui::SlicePlaneSettings {
706 name: name.clone(),
707 enabled: false,
708 origin: [5.0, 6.0, 7.0],
709 normal: [0.0, 0.0, 1.0],
710 draw_plane: false,
711 draw_widget: true,
712 color: [1.0, 0.0, 0.0],
713 transparency: 0.8,
714 plane_size: 0.2,
715 is_selected: false,
716 };
717
718 apply_slice_plane_settings(&settings);
720
721 let handle = get_slice_plane(&name).unwrap();
723 assert!(!handle.is_enabled());
724 assert_eq!(handle.origin(), Vec3::new(5.0, 6.0, 7.0));
725 assert_eq!(handle.normal(), Vec3::Z);
726 assert!(!handle.draw_plane());
727 assert!(handle.draw_widget());
728 assert_eq!(handle.color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
729 assert!((handle.transparency() - 0.8).abs() < 0.001);
730 }
731
732 #[test]
733 fn test_handle_slice_plane_action_add() {
734 setup();
735 let name = unique_name("action_add_plane");
736 let mut settings = Vec::new();
737
738 handle_slice_plane_action(
739 polyscope_ui::SlicePlanesAction::Add(name.clone()),
740 &mut settings,
741 );
742
743 assert_eq!(settings.len(), 1);
744 assert_eq!(settings[0].name, name);
745 assert!(get_slice_plane(&name).is_some());
746 }
747
748 #[test]
749 fn test_handle_slice_plane_action_remove() {
750 setup();
751 let name = unique_name("action_remove_plane");
752
753 add_slice_plane(&name);
755 let mut settings = vec![polyscope_ui::SlicePlaneSettings::with_name(&name)];
756
757 handle_slice_plane_action(polyscope_ui::SlicePlanesAction::Remove(0), &mut settings);
759
760 assert!(settings.is_empty());
761 assert!(get_slice_plane(&name).is_none());
762 }
763
764 #[test]
765 fn test_get_group_settings() {
766 setup();
767 let name = unique_name("ui_group");
768 let pc_name = unique_name("pc_in_ui_group");
769
770 let handle = create_group(&name);
772 register_point_cloud(&pc_name, vec![Vec3::ZERO]);
773 handle.add_point_cloud(&pc_name);
774
775 let settings = get_group_settings();
777 let found = settings.iter().find(|s| s.name == name);
778 assert!(found.is_some());
779
780 let s = found.unwrap();
781 assert!(s.enabled);
782 assert!(s.show_child_details);
783 assert_eq!(s.child_structures.len(), 1);
784 assert_eq!(s.child_structures[0], ("PointCloud".to_string(), pc_name));
785 }
786
787 #[test]
788 fn test_apply_group_settings() {
789 setup();
790 let name = unique_name("apply_group");
791
792 create_group(&name);
794
795 let settings = polyscope_ui::GroupSettings {
797 name: name.clone(),
798 enabled: false,
799 show_child_details: false,
800 parent_group: None,
801 child_structures: Vec::new(),
802 child_groups: Vec::new(),
803 };
804
805 apply_group_settings(&settings);
807
808 let handle = get_group(&name).unwrap();
810 assert!(!handle.is_enabled());
811 }
812
813 #[test]
814 fn test_get_gizmo_settings() {
815 setup();
816
817 set_gizmo_space(GizmoSpace::Local);
819 set_gizmo_visible(false);
820 set_gizmo_snap_translate(0.5);
821 set_gizmo_snap_rotate(15.0);
822 set_gizmo_snap_scale(0.1);
823
824 let settings = get_gizmo_settings();
825 assert!(settings.local_space); assert!(!settings.visible);
827 assert!((settings.snap_translate - 0.5).abs() < 0.001);
828 assert!((settings.snap_rotate - 15.0).abs() < 0.001);
829 assert!((settings.snap_scale - 0.1).abs() < 0.001);
830 }
831
832 #[test]
833 fn test_apply_gizmo_settings() {
834 setup();
835
836 let settings = polyscope_ui::GizmoSettings {
837 local_space: false, visible: true,
839 snap_translate: 1.0,
840 snap_rotate: 45.0,
841 snap_scale: 0.25,
842 };
843
844 apply_gizmo_settings(&settings);
845
846 assert_eq!(get_gizmo_space(), GizmoSpace::World);
847 assert!(is_gizmo_visible());
848 }
849
850 #[test]
851 fn test_get_selection_info_with_selection() {
852 setup();
853 let name = unique_name("gizmo_select_pc");
854
855 register_point_cloud(&name, vec![Vec3::ZERO]);
856 select_structure("PointCloud", &name);
857
858 let info = get_selection_info();
859 assert!(info.has_selection);
860 assert_eq!(info.type_name, "PointCloud");
861 assert_eq!(info.name, name);
862
863 deselect_structure();
864 }
865
866 #[test]
867 fn test_apply_selection_transform() {
868 setup();
869 let name = unique_name("gizmo_transform_pc");
870
871 register_point_cloud(&name, vec![Vec3::ZERO]);
872 select_structure("PointCloud", &name);
873
874 let selection = polyscope_ui::SelectionInfo {
875 has_selection: true,
876 type_name: "PointCloud".to_string(),
877 name: name.clone(),
878 translation: [1.0, 2.0, 3.0],
879 rotation_degrees: [0.0, 0.0, 0.0],
880 scale: [1.0, 1.0, 1.0],
881 centroid: [1.0, 2.0, 3.0],
882 };
883
884 apply_selection_transform(&selection);
885
886 let transform = get_point_cloud_transform(&name).unwrap();
887 let translation = transform.w_axis.truncate();
888 assert!((translation - Vec3::new(1.0, 2.0, 3.0)).length() < 0.001);
889
890 deselect_structure();
891 }
892}