Skip to main content

polyscope_core/
slice_plane.rs

1//! Slice plane functionality for cutting through geometry.
2//!
3//! Slice planes allow visualizing the interior of 3D geometry by
4//! discarding fragments on one side of the plane.
5
6use glam::{Mat4, Vec3, Vec4};
7
8/// A slice plane that can cut through geometry.
9///
10/// The plane is defined by a point (origin) and a normal direction.
11/// Geometry on the negative side of the plane (opposite to normal) is discarded.
12#[derive(Debug, Clone)]
13pub struct SlicePlane {
14    /// Unique name of the slice plane.
15    name: String,
16    /// A point on the plane (the origin).
17    origin: Vec3,
18    /// The normal direction of the plane (points toward kept geometry).
19    normal: Vec3,
20    /// Whether the slice plane is active.
21    enabled: bool,
22    /// Whether to draw a visual representation of the plane.
23    draw_plane: bool,
24    /// Whether to draw a widget at the plane origin.
25    draw_widget: bool,
26    /// Color of the plane visualization.
27    color: Vec4,
28    /// Transparency of the plane visualization (0.0 = fully transparent, 1.0 = opaque).
29    transparency: f32,
30    /// Size of the plane visualization (half-extent in each direction).
31    plane_size: f32,
32}
33
34impl SlicePlane {
35    /// Creates a new slice plane with default settings.
36    ///
37    /// By default, the plane is at the origin with +Y normal.
38    pub fn new(name: impl Into<String>) -> Self {
39        Self {
40            name: name.into(),
41            origin: Vec3::ZERO,
42            normal: Vec3::Y,
43            enabled: true,
44            draw_plane: true,
45            draw_widget: true,
46            color: Vec4::new(0.5, 0.5, 0.5, 1.0),
47            transparency: 0.5,
48            plane_size: 0.05,
49        }
50    }
51
52    /// Creates a slice plane with specific pose.
53    pub fn with_pose(name: impl Into<String>, origin: Vec3, normal: Vec3) -> Self {
54        Self {
55            name: name.into(),
56            origin,
57            normal: normal.normalize(),
58            enabled: true,
59            draw_plane: true,
60            draw_widget: true,
61            color: Vec4::new(0.5, 0.5, 0.5, 1.0),
62            transparency: 0.5,
63            plane_size: 0.05,
64        }
65    }
66
67    /// Returns the name of this slice plane.
68    #[must_use]
69    pub fn name(&self) -> &str {
70        &self.name
71    }
72
73    /// Returns the origin point of the plane.
74    #[must_use]
75    pub fn origin(&self) -> Vec3 {
76        self.origin
77    }
78
79    /// Sets the origin point of the plane.
80    pub fn set_origin(&mut self, origin: Vec3) {
81        self.origin = origin;
82    }
83
84    /// Returns the normal direction of the plane.
85    #[must_use]
86    pub fn normal(&self) -> Vec3 {
87        self.normal
88    }
89
90    /// Sets the normal direction of the plane.
91    pub fn set_normal(&mut self, normal: Vec3) {
92        self.normal = normal.normalize();
93    }
94
95    /// Sets both origin and normal at once.
96    pub fn set_pose(&mut self, origin: Vec3, normal: Vec3) {
97        self.origin = origin;
98        self.normal = normal.normalize();
99    }
100
101    /// Returns whether the slice plane is enabled.
102    #[must_use]
103    pub fn is_enabled(&self) -> bool {
104        self.enabled
105    }
106
107    /// Sets whether the slice plane is enabled.
108    pub fn set_enabled(&mut self, enabled: bool) {
109        self.enabled = enabled;
110    }
111
112    /// Returns whether to draw the plane visualization.
113    #[must_use]
114    pub fn draw_plane(&self) -> bool {
115        self.draw_plane
116    }
117
118    /// Sets whether to draw the plane visualization.
119    pub fn set_draw_plane(&mut self, draw: bool) {
120        self.draw_plane = draw;
121    }
122
123    /// Returns whether to draw the widget.
124    #[must_use]
125    pub fn draw_widget(&self) -> bool {
126        self.draw_widget
127    }
128
129    /// Sets whether to draw the widget.
130    pub fn set_draw_widget(&mut self, draw: bool) {
131        self.draw_widget = draw;
132    }
133
134    /// Returns the color of the plane visualization.
135    #[must_use]
136    pub fn color(&self) -> Vec4 {
137        self.color
138    }
139
140    /// Sets the color of the plane visualization.
141    pub fn set_color(&mut self, color: Vec3) {
142        self.color = color.extend(1.0);
143    }
144
145    /// Returns the transparency of the plane visualization.
146    #[must_use]
147    pub fn transparency(&self) -> f32 {
148        self.transparency
149    }
150
151    /// Sets the transparency of the plane visualization.
152    pub fn set_transparency(&mut self, transparency: f32) {
153        self.transparency = transparency.clamp(0.0, 1.0);
154    }
155
156    /// Returns the size of the plane visualization (half-extent in each direction).
157    #[must_use]
158    pub fn plane_size(&self) -> f32 {
159        self.plane_size
160    }
161
162    /// Sets the size of the plane visualization (half-extent in each direction).
163    pub fn set_plane_size(&mut self, size: f32) {
164        self.plane_size = size.max(0.001);
165    }
166
167    /// Returns the signed distance from a point to the plane.
168    ///
169    /// Positive values are on the normal side (kept), negative on the opposite (discarded).
170    #[must_use]
171    pub fn signed_distance(&self, point: Vec3) -> f32 {
172        (point - self.origin).dot(self.normal)
173    }
174
175    /// Returns whether a point is on the kept side of the plane.
176    #[must_use]
177    pub fn is_kept(&self, point: Vec3) -> bool {
178        !self.enabled || self.signed_distance(point) >= 0.0
179    }
180
181    /// Projects a point onto the plane.
182    #[must_use]
183    pub fn project(&self, point: Vec3) -> Vec3 {
184        point - self.signed_distance(point) * self.normal
185    }
186
187    // ========================================================================
188    // Transform Methods for Gizmo Manipulation
189    // ========================================================================
190
191    /// Computes a transform matrix for gizmo manipulation.
192    ///
193    /// The plane normal becomes the local X axis, with Y and Z axes
194    /// forming an orthonormal basis in the plane.
195    #[must_use]
196    pub fn to_transform(&self) -> Mat4 {
197        let x_axis = self.normal.normalize();
198
199        // Choose an "up" direction that's not parallel to the normal
200        let up = if x_axis.dot(Vec3::Y).abs() < 0.99 {
201            Vec3::Y
202        } else {
203            Vec3::Z
204        };
205
206        // Build orthonormal basis
207        let y_axis = up.cross(x_axis).normalize();
208        let z_axis = x_axis.cross(y_axis).normalize();
209
210        Mat4::from_cols(
211            Vec4::new(x_axis.x, x_axis.y, x_axis.z, 0.0),
212            Vec4::new(y_axis.x, y_axis.y, y_axis.z, 0.0),
213            Vec4::new(z_axis.x, z_axis.y, z_axis.z, 0.0),
214            Vec4::new(self.origin.x, self.origin.y, self.origin.z, 1.0),
215        )
216    }
217
218    /// Updates origin and normal from a transform matrix.
219    ///
220    /// Extracts position from column 3 (translation), and normal from
221    /// column 0 (x-axis in local space).
222    pub fn set_from_transform(&mut self, transform: Mat4) {
223        // Extract origin from translation column
224        self.origin = transform.w_axis.truncate();
225        // Extract normal from first column (x-axis in local space)
226        self.normal = transform.x_axis.truncate().normalize();
227    }
228}
229
230impl Default for SlicePlane {
231    fn default() -> Self {
232        Self::new("default")
233    }
234}
235
236/// GPU-compatible slice plane uniforms.
237#[repr(C)]
238#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
239#[allow(clippy::pub_underscore_fields)]
240pub struct SlicePlaneUniforms {
241    /// Origin point of the plane.
242    pub origin: [f32; 3],
243    /// Whether the plane is enabled (1.0) or disabled (0.0).
244    pub enabled: f32,
245    /// Normal direction of the plane.
246    pub normal: [f32; 3],
247    /// Padding for alignment.
248    pub _padding: f32,
249}
250
251impl From<&SlicePlane> for SlicePlaneUniforms {
252    fn from(plane: &SlicePlane) -> Self {
253        Self {
254            origin: plane.origin.to_array(),
255            enabled: if plane.enabled { 1.0 } else { 0.0 },
256            normal: plane.normal.to_array(),
257            _padding: 0.0,
258        }
259    }
260}
261
262impl Default for SlicePlaneUniforms {
263    fn default() -> Self {
264        Self {
265            origin: [0.0; 3],
266            enabled: 0.0,
267            normal: [0.0, 1.0, 0.0],
268            _padding: 0.0,
269        }
270    }
271}
272
273/// Maximum number of slice planes supported.
274pub const MAX_SLICE_PLANES: usize = 4;
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_signed_distance() {
282        let plane = SlicePlane::with_pose("test", Vec3::ZERO, Vec3::Y);
283
284        // Point above the plane (positive Y)
285        assert!(plane.signed_distance(Vec3::new(0.0, 1.0, 0.0)) > 0.0);
286
287        // Point below the plane (negative Y)
288        assert!(plane.signed_distance(Vec3::new(0.0, -1.0, 0.0)) < 0.0);
289
290        // Point on the plane
291        assert!((plane.signed_distance(Vec3::new(1.0, 0.0, 1.0))).abs() < 1e-6);
292    }
293
294    #[test]
295    fn test_is_kept() {
296        let plane = SlicePlane::with_pose("test", Vec3::ZERO, Vec3::Y);
297
298        // Above plane - kept
299        assert!(plane.is_kept(Vec3::new(0.0, 1.0, 0.0)));
300
301        // Below plane - not kept
302        assert!(!plane.is_kept(Vec3::new(0.0, -1.0, 0.0)));
303
304        // Disabled plane - everything is kept
305        let mut disabled_plane = plane.clone();
306        disabled_plane.set_enabled(false);
307        assert!(disabled_plane.is_kept(Vec3::new(0.0, -1.0, 0.0)));
308    }
309
310    #[test]
311    fn test_project() {
312        let plane = SlicePlane::with_pose("test", Vec3::ZERO, Vec3::Y);
313
314        // Project point above plane onto plane
315        let projected = plane.project(Vec3::new(1.0, 5.0, 2.0));
316        assert!((projected - Vec3::new(1.0, 0.0, 2.0)).length() < 1e-6);
317    }
318
319    #[test]
320    fn test_uniforms() {
321        let plane = SlicePlane::with_pose("test", Vec3::new(1.0, 2.0, 3.0), Vec3::Z);
322        let uniforms = SlicePlaneUniforms::from(&plane);
323
324        assert_eq!(uniforms.origin, [1.0, 2.0, 3.0]);
325        assert_eq!(uniforms.normal, [0.0, 0.0, 1.0]);
326        assert_eq!(uniforms.enabled, 1.0);
327    }
328
329    #[test]
330    fn test_to_transform() {
331        let plane = SlicePlane::with_pose("test", Vec3::new(1.0, 2.0, 3.0), Vec3::X);
332        let transform = plane.to_transform();
333
334        // Check that origin is in the translation column
335        let extracted_origin = transform.w_axis.truncate();
336        assert!((extracted_origin - Vec3::new(1.0, 2.0, 3.0)).length() < 1e-6);
337
338        // Check that normal is the x-axis
339        let extracted_normal = transform.x_axis.truncate().normalize();
340        assert!((extracted_normal - Vec3::X).length() < 1e-6);
341    }
342
343    #[test]
344    fn test_transform_roundtrip() {
345        let original =
346            SlicePlane::with_pose("test", Vec3::new(1.0, 2.0, 3.0), Vec3::new(1.0, 1.0, 0.0));
347        let transform = original.to_transform();
348
349        let mut restored = SlicePlane::new("test2");
350        restored.set_from_transform(transform);
351
352        // Origin should match
353        assert!((restored.origin() - original.origin()).length() < 1e-6);
354
355        // Normal should match (normalized)
356        assert!((restored.normal() - original.normal().normalize()).length() < 1e-6);
357    }
358}