#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::items_after_statements)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::unused_self)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::many_single_char_names)]
#![allow(clippy::struct_excessive_bools)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::doc_markdown)]
macro_rules! impl_structure_accessors {
(
get_fn = $get_fn:ident,
with_fn = $with_fn:ident,
with_ref_fn = $with_ref_fn:ident,
handle = $handle:ident,
type_name = $type_name:expr,
rust_type = $rust_type:ty,
doc_name = $doc_name:expr
) => {
#[doc = concat!("Gets a registered ", $doc_name, " by name.")]
#[must_use]
pub fn $get_fn(name: &str) -> Option<$handle> {
crate::with_context(|ctx| {
if ctx.registry.contains($type_name, name) {
Some($handle {
name: name.to_string(),
})
} else {
None
}
})
}
#[doc = concat!("Executes a closure with mutable access to a registered ", $doc_name, ".\n\nReturns `None` if the ", $doc_name, " does not exist.")]
pub fn $with_fn<F, R>(name: &str, f: F) -> Option<R>
where
F: FnOnce(&mut $rust_type) -> R,
{
crate::with_context_mut(|ctx| {
ctx.registry
.get_mut($type_name, name)
.and_then(|s| s.as_any_mut().downcast_mut::<$rust_type>())
.map(f)
})
}
#[doc = concat!("Executes a closure with immutable access to a registered ", $doc_name, ".\n\nReturns `None` if the ", $doc_name, " does not exist.")]
pub fn $with_ref_fn<F, R>(name: &str, f: F) -> Option<R>
where
F: FnOnce(&$rust_type) -> R,
{
crate::with_context(|ctx| {
ctx.registry
.get($type_name, name)
.and_then(|s| s.as_any().downcast_ref::<$rust_type>())
.map(f)
})
}
};
}
mod app;
mod camera_view;
mod curve_network;
mod floating;
mod gizmo;
mod groups;
mod headless;
mod init;
mod point_cloud;
mod screenshot;
mod slice_plane;
mod surface_mesh;
mod transform;
mod ui_sync;
mod volume_grid;
mod volume_mesh;
pub use polyscope_core::{
Mat4, Vec2, Vec3, Vec4,
error::{PolyscopeError, Result},
gizmo::{GizmoAxis, GizmoConfig, GizmoMode, GizmoSpace, Transform},
group::Group,
options::Options,
pick::{PickResult, Pickable},
quantity::{ParamCoordsType, ParamVizStyle, Quantity, QuantityKind},
registry::Registry,
slice_plane::{MAX_SLICE_PLANES, SlicePlane, SlicePlaneUniforms},
state::{Context, with_context, with_context_mut},
structure::{HasQuantities, Structure},
};
pub use polyscope_render::{
AxisDirection, Camera, ColorMap, ColorMapRegistry, Material, MaterialRegistry, NavigationStyle,
PickElementType, ProjectionMode, RenderContext, RenderEngine, ScreenshotError,
ScreenshotOptions,
};
pub use polyscope_ui::{
AppearanceSettings, CameraSettings, GizmoAction, GizmoSettings, GroupSettings, GroupsAction,
SceneExtents, SelectionInfo, SlicePlaneGizmoAction, SlicePlaneSelectionInfo,
SlicePlaneSettings, SlicePlanesAction, ViewAction,
};
pub use polyscope_structures::volume_grid::VolumeGridVizMode;
pub use polyscope_structures::{
CameraExtrinsics, CameraIntrinsics, CameraParameters, CameraView, CurveNetwork, PointCloud,
SurfaceMesh, VolumeCellType, VolumeGrid, VolumeMesh,
};
pub use camera_view::*;
pub use curve_network::*;
pub use floating::*;
pub use gizmo::*;
pub use groups::*;
pub use headless::*;
pub use init::*;
pub use point_cloud::*;
pub use screenshot::*;
pub use slice_plane::*;
pub use surface_mesh::*;
pub use transform::*;
pub use ui_sync::*;
pub use volume_grid::*;
pub use volume_mesh::*;
pub fn remove_structure(name: &str) {
with_context_mut(|ctx| {
ctx.registry.remove("PointCloud", name);
ctx.registry.remove("SurfaceMesh", name);
ctx.registry.remove("CurveNetwork", name);
ctx.registry.remove("VolumeMesh", name);
ctx.registry.remove("VolumeGrid", name);
ctx.registry.remove("CameraView", name);
ctx.update_extents();
});
}
pub fn remove_all_structures() {
with_context_mut(|ctx| {
ctx.registry.clear();
ctx.update_extents();
});
}
pub fn remove_everything() {
remove_all_structures();
remove_all_groups();
remove_all_slice_planes();
remove_all_floating_quantities();
clear_file_drop_callback();
}
pub fn set_file_drop_callback(callback: impl FnMut(&[std::path::PathBuf]) + Send + Sync + 'static) {
with_context_mut(|ctx| {
ctx.file_drop_callback = Some(Box::new(callback));
});
}
pub fn clear_file_drop_callback() {
with_context_mut(|ctx| {
ctx.file_drop_callback = None;
});
}
pub fn load_blendable_material(name: &str, filenames: [&str; 4]) {
with_context_mut(|ctx| {
ctx.material_load_queue
.push(polyscope_core::state::MaterialLoadRequest::Blendable {
name: name.to_string(),
filenames: [
filenames[0].to_string(),
filenames[1].to_string(),
filenames[2].to_string(),
filenames[3].to_string(),
],
});
});
}
pub fn load_blendable_material_ext(name: &str, base: &str, ext: &str) {
load_blendable_material(
name,
[
&format!("{base}_r{ext}"),
&format!("{base}_g{ext}"),
&format!("{base}_b{ext}"),
&format!("{base}_k{ext}"),
],
);
}
pub fn load_static_material(name: &str, filename: &str) {
with_context_mut(|ctx| {
ctx.material_load_queue
.push(polyscope_core::state::MaterialLoadRequest::Static {
name: name.to_string(),
path: filename.to_string(),
});
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn unique_name(prefix: &str) -> String {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
format!("{prefix}_{n}")
}
fn setup() {
let _ = init();
}
#[test]
fn test_register_curve_network() {
setup();
let name = unique_name("test_cn");
let nodes = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 0.0),
];
let edges = vec![[0, 1], [1, 2]];
let handle = register_curve_network(&name, nodes, edges);
assert_eq!(handle.name(), name);
let found = get_curve_network(&name);
assert!(found.is_some());
let not_found = get_curve_network("nonexistent_xyz_123");
assert!(not_found.is_none());
}
#[test]
fn test_register_curve_network_line() {
setup();
let name = unique_name("line");
let nodes = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(3.0, 0.0, 0.0),
];
register_curve_network_line(&name, nodes);
let num_edges = with_curve_network_ref(&name, |cn| cn.num_edges());
assert_eq!(num_edges, Some(3)); }
#[test]
fn test_register_curve_network_loop() {
setup();
let name = unique_name("loop");
let nodes = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 0.0),
];
register_curve_network_loop(&name, nodes);
let num_edges = with_curve_network_ref(&name, |cn| cn.num_edges());
assert_eq!(num_edges, Some(3)); }
#[test]
fn test_register_curve_network_segments() {
setup();
let name = unique_name("segs");
let nodes = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(3.0, 0.0, 0.0),
];
register_curve_network_segments(&name, nodes);
let num_edges = with_curve_network_ref(&name, |cn| cn.num_edges());
assert_eq!(num_edges, Some(2)); }
#[test]
fn test_curve_network_handle_methods() {
setup();
let name = unique_name("handle_test");
let nodes = vec![Vec3::ZERO, Vec3::X];
let edges = vec![[0, 1]];
let handle = register_curve_network(&name, nodes, edges);
handle
.set_color(Vec3::new(1.0, 0.0, 0.0))
.set_radius(0.1, false)
.set_material("clay");
with_curve_network_ref(&name, |cn| {
assert_eq!(cn.color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
assert_eq!(cn.radius(), 0.1);
assert!(!cn.radius_is_relative());
assert_eq!(cn.material(), "clay");
});
}
#[test]
fn test_with_curve_network() {
setup();
let name = unique_name("with_test");
let nodes = vec![Vec3::ZERO, Vec3::X, Vec3::Y];
let edges = vec![[0, 1], [1, 2]];
register_curve_network(&name, nodes, edges);
let result = with_curve_network(&name, |cn| {
cn.set_color(Vec3::new(0.5, 0.5, 0.5));
cn.num_nodes()
});
assert_eq!(result, Some(3));
let color = with_curve_network_ref(&name, |cn| cn.color());
assert_eq!(color, Some(Vec4::new(0.5, 0.5, 0.5, 1.0)));
}
#[test]
fn test_create_group() {
setup();
let name = unique_name("test_group");
let handle = create_group(&name);
assert_eq!(handle.name(), name);
assert!(handle.is_enabled());
}
#[test]
fn test_get_group() {
setup();
let name = unique_name("get_group");
create_group(&name);
let found = get_group(&name);
assert!(found.is_some());
assert_eq!(found.unwrap().name(), name);
let not_found = get_group("nonexistent_group_xyz");
assert!(not_found.is_none());
}
#[test]
fn test_group_enable_disable() {
setup();
let name = unique_name("enable_group");
let handle = create_group(&name);
assert!(handle.is_enabled());
handle.set_enabled(false);
assert!(!handle.is_enabled());
handle.set_enabled(true);
assert!(handle.is_enabled());
}
#[test]
fn test_group_add_structures() {
setup();
let group_name = unique_name("struct_group");
let pc_name = unique_name("pc_in_group");
register_point_cloud(&pc_name, vec![Vec3::ZERO, Vec3::X]);
let handle = create_group(&group_name);
handle.add_point_cloud(&pc_name);
assert_eq!(handle.num_structures(), 1);
}
#[test]
fn test_group_hierarchy() {
setup();
let parent_name = unique_name("parent_group");
let child_name = unique_name("child_group");
let parent = create_group(&parent_name);
let _child = create_group(&child_name);
parent.add_child_group(&child_name);
assert_eq!(parent.num_child_groups(), 1);
}
#[test]
fn test_remove_group() {
setup();
let name = unique_name("remove_group");
create_group(&name);
assert!(get_group(&name).is_some());
remove_group(&name);
assert!(get_group(&name).is_none());
}
#[test]
fn test_add_slice_plane() {
setup();
let name = unique_name("slice_plane");
let handle = add_slice_plane(&name);
assert_eq!(handle.name(), name);
assert!(handle.is_enabled());
}
#[test]
fn test_slice_plane_pose() {
setup();
let name = unique_name("slice_pose");
let handle = add_slice_plane_with_pose(&name, Vec3::new(1.0, 2.0, 3.0), Vec3::X);
assert_eq!(handle.origin(), Vec3::new(1.0, 2.0, 3.0));
assert_eq!(handle.normal(), Vec3::X);
}
#[test]
fn test_slice_plane_setters() {
setup();
let name = unique_name("slice_setters");
let handle = add_slice_plane(&name);
handle
.set_origin(Vec3::new(1.0, 0.0, 0.0))
.set_normal(Vec3::Z)
.set_color(Vec3::new(1.0, 0.0, 0.0))
.set_transparency(0.5);
assert_eq!(handle.origin(), Vec3::new(1.0, 0.0, 0.0));
assert_eq!(handle.normal(), Vec3::Z);
assert_eq!(handle.color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
assert!((handle.transparency() - 0.5).abs() < 0.001);
}
#[test]
fn test_slice_plane_enable_disable() {
setup();
let name = unique_name("slice_enable");
let handle = add_slice_plane(&name);
assert!(handle.is_enabled());
handle.set_enabled(false);
assert!(!handle.is_enabled());
handle.set_enabled(true);
assert!(handle.is_enabled());
}
#[test]
fn test_remove_slice_plane() {
setup();
let name = unique_name("slice_remove");
add_slice_plane(&name);
assert!(get_slice_plane(&name).is_some());
remove_slice_plane(&name);
assert!(get_slice_plane(&name).is_none());
}
#[test]
fn test_select_structure() {
setup();
let name = unique_name("select_pc");
register_point_cloud(&name, vec![Vec3::ZERO]);
assert!(!has_selection());
select_structure("PointCloud", &name);
assert!(has_selection());
let selected = get_selected_structure();
assert!(selected.is_some());
let (type_name, struct_name) = selected.unwrap();
assert_eq!(type_name, "PointCloud");
assert_eq!(struct_name, name);
deselect_structure();
assert!(!has_selection());
}
#[test]
fn test_slice_plane_gizmo_selection() {
setup();
let name = unique_name("slice_gizmo");
add_slice_plane(&name);
let info = get_slice_plane_selection_info();
assert!(!info.has_selection);
select_slice_plane_for_gizmo(&name);
let info = get_slice_plane_selection_info();
assert!(info.has_selection);
assert_eq!(info.name, name);
deselect_slice_plane_gizmo();
let info = get_slice_plane_selection_info();
assert!(!info.has_selection);
}
#[test]
fn test_slice_plane_structure_mutual_exclusion() {
setup();
let pc_name = unique_name("mutual_pc");
let plane_name = unique_name("mutual_plane");
register_point_cloud(&pc_name, vec![Vec3::ZERO]);
add_slice_plane(&plane_name);
select_structure("PointCloud", &pc_name);
assert!(has_selection());
select_slice_plane_for_gizmo(&plane_name);
assert!(!has_selection()); let info = get_slice_plane_selection_info();
assert!(info.has_selection);
select_structure("PointCloud", &pc_name);
assert!(has_selection());
let info = get_slice_plane_selection_info();
assert!(!info.has_selection); }
#[test]
fn test_structure_transform() {
setup();
let name = unique_name("transform_pc");
register_point_cloud(&name, vec![Vec3::ZERO, Vec3::X]);
let transform = get_point_cloud_transform(&name);
assert!(transform.is_some());
let new_transform = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0));
set_point_cloud_transform(&name, new_transform);
let transform = get_point_cloud_transform(&name).unwrap();
let translation = transform.w_axis.truncate();
assert!((translation - Vec3::new(1.0, 2.0, 3.0)).length() < 0.001);
}
#[test]
fn test_get_slice_plane_settings() {
setup();
let name = unique_name("ui_slice_plane");
add_slice_plane_with_pose(&name, Vec3::new(1.0, 2.0, 3.0), Vec3::X);
let settings = get_slice_plane_settings();
let found = settings.iter().find(|s| s.name == name);
assert!(found.is_some());
let s = found.unwrap();
assert_eq!(s.origin, [1.0, 2.0, 3.0]);
assert_eq!(s.normal, [1.0, 0.0, 0.0]);
assert!(s.enabled);
}
#[test]
fn test_apply_slice_plane_settings() {
setup();
let name = unique_name("apply_slice_plane");
add_slice_plane(&name);
let settings = polyscope_ui::SlicePlaneSettings {
name: name.clone(),
enabled: false,
origin: [5.0, 6.0, 7.0],
normal: [0.0, 0.0, 1.0],
draw_plane: false,
draw_widget: true,
color: [1.0, 0.0, 0.0],
transparency: 0.8,
plane_size: 0.2,
is_selected: false,
};
apply_slice_plane_settings(&settings);
let handle = get_slice_plane(&name).unwrap();
assert!(!handle.is_enabled());
assert_eq!(handle.origin(), Vec3::new(5.0, 6.0, 7.0));
assert_eq!(handle.normal(), Vec3::Z);
assert!(!handle.draw_plane());
assert!(handle.draw_widget());
assert_eq!(handle.color(), Vec4::new(1.0, 0.0, 0.0, 1.0));
assert!((handle.transparency() - 0.8).abs() < 0.001);
}
#[test]
fn test_handle_slice_plane_action_add() {
setup();
let name = unique_name("action_add_plane");
let mut settings = Vec::new();
handle_slice_plane_action(
polyscope_ui::SlicePlanesAction::Add(name.clone()),
&mut settings,
);
assert_eq!(settings.len(), 1);
assert_eq!(settings[0].name, name);
assert!(get_slice_plane(&name).is_some());
}
#[test]
fn test_handle_slice_plane_action_remove() {
setup();
let name = unique_name("action_remove_plane");
add_slice_plane(&name);
let mut settings = vec![polyscope_ui::SlicePlaneSettings::with_name(&name)];
handle_slice_plane_action(polyscope_ui::SlicePlanesAction::Remove(0), &mut settings);
assert!(settings.is_empty());
assert!(get_slice_plane(&name).is_none());
}
#[test]
fn test_get_group_settings() {
setup();
let name = unique_name("ui_group");
let pc_name = unique_name("pc_in_ui_group");
let handle = create_group(&name);
register_point_cloud(&pc_name, vec![Vec3::ZERO]);
handle.add_point_cloud(&pc_name);
let settings = get_group_settings();
let found = settings.iter().find(|s| s.name == name);
assert!(found.is_some());
let s = found.unwrap();
assert!(s.enabled);
assert!(s.show_child_details);
assert_eq!(s.child_structures.len(), 1);
assert_eq!(s.child_structures[0], ("PointCloud".to_string(), pc_name));
}
#[test]
fn test_apply_group_settings() {
setup();
let name = unique_name("apply_group");
create_group(&name);
let settings = polyscope_ui::GroupSettings {
name: name.clone(),
enabled: false,
show_child_details: false,
parent_group: None,
child_structures: Vec::new(),
child_groups: Vec::new(),
};
apply_group_settings(&settings);
let handle = get_group(&name).unwrap();
assert!(!handle.is_enabled());
}
#[test]
fn test_get_gizmo_settings() {
setup();
set_gizmo_space(GizmoSpace::Local);
set_gizmo_visible(false);
set_gizmo_snap_translate(0.5);
set_gizmo_snap_rotate(15.0);
set_gizmo_snap_scale(0.1);
let settings = get_gizmo_settings();
assert!(settings.local_space); assert!(!settings.visible);
assert!((settings.snap_translate - 0.5).abs() < 0.001);
assert!((settings.snap_rotate - 15.0).abs() < 0.001);
assert!((settings.snap_scale - 0.1).abs() < 0.001);
}
#[test]
fn test_apply_gizmo_settings() {
setup();
let settings = polyscope_ui::GizmoSettings {
local_space: false, visible: true,
snap_translate: 1.0,
snap_rotate: 45.0,
snap_scale: 0.25,
};
apply_gizmo_settings(&settings);
assert_eq!(get_gizmo_space(), GizmoSpace::World);
assert!(is_gizmo_visible());
}
#[test]
fn test_get_selection_info_with_selection() {
setup();
let name = unique_name("gizmo_select_pc");
register_point_cloud(&name, vec![Vec3::ZERO]);
select_structure("PointCloud", &name);
let info = get_selection_info();
assert!(info.has_selection);
assert_eq!(info.type_name, "PointCloud");
assert_eq!(info.name, name);
deselect_structure();
}
#[test]
fn test_apply_selection_transform() {
setup();
let name = unique_name("gizmo_transform_pc");
register_point_cloud(&name, vec![Vec3::ZERO]);
select_structure("PointCloud", &name);
let selection = polyscope_ui::SelectionInfo {
has_selection: true,
type_name: "PointCloud".to_string(),
name: name.clone(),
translation: [1.0, 2.0, 3.0],
rotation_degrees: [0.0, 0.0, 0.0],
scale: [1.0, 1.0, 1.0],
centroid: [1.0, 2.0, 3.0],
};
apply_selection_transform(&selection);
let transform = get_point_cloud_transform(&name).unwrap();
let translation = transform.w_axis.truncate();
assert!((translation - Vec3::new(1.0, 2.0, 3.0)).length() < 0.001);
deselect_structure();
}
#[test]
fn test_remove_all_groups() {
setup();
let g1 = unique_name("rag_group1");
let g2 = unique_name("rag_group2");
create_group(&g1);
create_group(&g2);
assert!(get_group(&g1).is_some());
assert!(get_group(&g2).is_some());
remove_all_groups();
assert!(get_group(&g1).is_none());
assert!(get_group(&g2).is_none());
}
#[test]
fn test_remove_everything() {
setup();
let pc_name = unique_name("re_pc");
let group_name = unique_name("re_group");
let sp_name = unique_name("re_slice");
register_point_cloud(&pc_name, vec![Vec3::ZERO]);
create_group(&group_name);
add_slice_plane(&sp_name);
remove_everything();
assert!(get_point_cloud(&pc_name).is_none());
assert!(get_group(&group_name).is_none());
assert!(get_all_slice_planes().is_empty());
}
#[test]
fn test_degenerate_bounding_box() {
setup();
remove_all_structures();
let name = unique_name("degen_bbox");
register_point_cloud(&name, vec![Vec3::ONE, Vec3::ONE, Vec3::ONE]);
let (bb_min, bb_max) = with_context(|ctx| ctx.bounding_box);
assert!(
bb_max.x > bb_min.x,
"degenerate bbox not perturbed: min={bb_min}, max={bb_max}"
);
assert!(bb_max.y > bb_min.y);
assert!(bb_max.z > bb_min.z);
}
}