lib_curveball/map/
geometry.rs

1// Copyright 2025 Jordan Johnson
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Structures defining the geometry of a map.
5
6const TEX_DEFAULT: &str = "mtrl/invisible";
7const ALMOST_EQUAL_DELTA: f64 = 0.000000001;
8use core::fmt;
9use glam::DVec3;
10use std::fmt::{Display, Formatter};
11
12/// A face represented as three points in space.
13#[derive(Debug, Default, Clone, Copy, PartialEq)]
14pub struct SideGeom(pub [DVec3; 3]);
15impl SideGeom {
16    pub fn normal(self) -> Option<DVec3> {
17        let Self([p0, p1, p2]) = self;
18        ((p0 - p1).cross(p2 - p1)).try_normalize()
19    }
20    pub fn dist(self) -> Option<f64> {
21        let Self([_p0, p1, _p2]) = self;
22        Some(self.normal()?.dot(p1))
23    }
24    pub fn equivalent(self, other: SideGeom) -> bool {
25        let Some(normal) = self.normal() else {
26            return false;
27        };
28        let Some(other_normal) = other.normal() else {
29            return false;
30        };
31        if normal.dot(other_normal) < 1.0 - ALMOST_EQUAL_DELTA {
32            return false;
33        }
34
35        let Some(dist) = self.dist() else {
36            return false;
37        };
38        let Some(other_dist) = other.dist() else {
39            return false;
40        };
41        if (dist - other_dist) > ALMOST_EQUAL_DELTA {
42            return false;
43        }
44        true
45    }
46}
47
48// TODO: Add texture offset, scale, rotation
49
50/// A struct defining the texture of a face.
51///
52/// This struct is missing important properties, like the texture offset, scale, and rotation. It
53/// is currently unused.
54#[derive(Debug, Clone, PartialEq)]
55pub struct SideMtrl {
56    pub texture: String,
57}
58
59impl Default for SideMtrl {
60    fn default() -> Self {
61        Self {
62            texture: String::from(TEX_DEFAULT),
63        }
64    }
65}
66
67/// A face, consisting of the face's geometry and the face's texture.
68#[derive(Debug, Default, Clone, PartialEq)]
69pub struct Side {
70    pub geom: SideGeom,
71    pub mtrl: SideMtrl,
72}
73
74impl Side {
75    pub(crate) fn bake(&self) -> impl Display + use<'_> {
76        struct SideDisplay<'a>(&'a Side);
77        impl Display for SideDisplay<'_> {
78            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
79                write!(
80                    f,
81                    "( {:.6} {:.6} {:.6} ) ( {:.6} {:.6} {:.6} ) ( {:.6} {:.6} {:.6} ) {} 0 0 0 0.5 0.5 0 0 0",
82                    self.0.geom.0[0][0],
83                    self.0.geom.0[0][1],
84                    self.0.geom.0[0][2],
85                    self.0.geom.0[1][0],
86                    self.0.geom.0[1][1],
87                    self.0.geom.0[1][2],
88                    self.0.geom.0[2][0],
89                    self.0.geom.0[2][1],
90                    self.0.geom.0[2][2],
91                    self.0.mtrl.texture
92                )
93            }
94        }
95        SideDisplay(self)
96    }
97}
98
99use chull::ConvexHullWrapper;
100/// A brush, representing a convex polyhedron that can be instantiated in a Neverball level. Curves
101/// consist of multiple Brushes.
102#[derive(Debug, Clone)]
103pub struct Brush {
104    vertices: Vec<DVec3>,
105    sides: Vec<([usize; 3], SideMtrl)>, // the [usize; 3] contains indices into the vertices vector
106}
107
108impl Brush {
109    pub fn try_from_vertices(
110        vertices: &[DVec3],
111        max_iter: Option<usize>,
112    ) -> Result<Self, chull::convex::ErrorKind> {
113        let vertices: Vec<Vec<f64>> = vertices
114            .iter()
115            .map(|vertex| vec![vertex.x, vertex.y, vertex.z])
116            .collect();
117
118        let hull = ConvexHullWrapper::try_new(&vertices, max_iter)?;
119
120        Ok(hull.into())
121    }
122
123    pub fn side_vertex_indices(&self) -> (&Vec<DVec3>, &Vec<([usize; 3], SideMtrl)>) {
124        (&self.vertices, &self.sides)
125    }
126
127    pub fn triangles(&self) -> impl Iterator<Item = Side> + use<'_> {
128        self.sides.iter().map(|([idx0, idx1, idx2], mtrl)| Side {
129            geom: SideGeom([
130                self.vertices[*idx0],
131                self.vertices[*idx1],
132                self.vertices[*idx2],
133            ]),
134            mtrl: mtrl.clone(),
135        })
136    }
137
138    pub fn to_sides_unique(&self) -> Vec<Side> {
139        let mut result: Vec<_> = self.triangles().collect();
140
141        let keep: Vec<_> = result
142            .iter()
143            .enumerate()
144            .map(|(i, candidate)| {
145                !result[0..i]
146                    .iter()
147                    .any(|so_far| SideGeom::equivalent(so_far.geom, candidate.geom))
148            })
149            .collect();
150
151        let mut keep_iter = keep.iter();
152
153        // SideGeom::equivalent(*a, *b)
154        result.retain(|_| *keep_iter.next().unwrap());
155        result
156    }
157
158    pub fn vertices(&self) -> &Vec<DVec3> {
159        &self.vertices
160    }
161}
162
163#[allow(clippy::get_first)]
164impl From<ConvexHullWrapper<f64>> for Brush {
165    fn from(hull: ConvexHullWrapper<f64>) -> Self {
166        let (vertices, side_indices) = hull.vertices_indices();
167
168        let vertices: Vec<DVec3> = vertices
169            .into_iter()
170            .map(|vertex| DVec3 {
171                x: *vertex
172                    .get(0)
173                    .expect("vertices expected to have three components"),
174                y: *vertex
175                    .get(1)
176                    .expect("vertices expected to have three components"),
177                z: *vertex
178                    .get(2)
179                    .expect("vertices expected to have three components"),
180            })
181            .collect();
182
183        // Just a check.
184        #[cfg(debug_assertions)]
185        assert_eq!(side_indices.len() % 3, 0);
186
187        use itertools::Itertools;
188        let sides = side_indices
189            .into_iter()
190            .tuples()
191            .map(|(i0, i1, i2)| (i1, i0, i2)) // Reorder to (p1-p0; p2-p0) order
192            .map(|(i0, i1, i2)| ([i0, i1, i2], SideMtrl::default()))
193            .collect();
194
195        Self { vertices, sides }
196    }
197}
198
199impl Brush {
200    pub(crate) fn bake(&self) -> impl Display + use<'_> {
201        struct BrushDisp<'a>(&'a Brush);
202        impl Display for BrushDisp<'_> {
203            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
204                writeln!(f, "{{",)?;
205                for side in self.0.to_sides_unique().iter() {
206                    writeln!(f, "{}", side.bake())?;
207                }
208                writeln!(f, "}}")?;
209                Ok(())
210            }
211        }
212        BrushDisp(self)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    fn almost_equals(a: &DVec3, b: &DVec3) -> bool {
221        const EPSILON: f64 = 0.000000001;
222        (a.x - b.x).abs() < EPSILON && (a.y - b.y).abs() < EPSILON && (a.z - b.z).abs() < EPSILON
223    }
224
225    #[test]
226    fn test_brush_cube() {
227        let vertices = vec![
228            DVec3::from([0.0, 0.0, 0.0]),
229            DVec3::from([0.0, 0.0, 1.0]),
230            DVec3::from([0.0, 1.0, 0.0]),
231            DVec3::from([0.0, 1.0, 1.0]),
232            DVec3::from([1.0, 0.0, 0.0]),
233            DVec3::from([1.0, 0.0, 1.0]),
234            DVec3::from([1.0, 1.0, 0.0]),
235            DVec3::from([1.0, 1.0, 1.0]),
236            DVec3::from([0.5, 0.5, 0.5]),
237        ];
238
239        let brush = Brush::try_from_vertices(&vertices, Some(1000)).unwrap();
240
241        let extracted_vertices: &Vec<DVec3> = brush.vertices();
242        let extracted_sides: Vec<Side> = brush.to_sides_unique();
243
244        assert_eq!(extracted_vertices.len(), 8);
245        assert_eq!(extracted_sides.len(), 6);
246        assert!(
247            extracted_vertices
248                .iter()
249                .any(|vertex| almost_equals(vertex, &DVec3::from([0.0, 0.0, 0.0])))
250        );
251        assert!(
252            extracted_vertices
253                .iter()
254                .any(|vertex| almost_equals(vertex, &DVec3::from([0.0, 0.0, 1.0])))
255        );
256        assert!(
257            extracted_vertices
258                .iter()
259                .any(|vertex| almost_equals(vertex, &DVec3::from([0.0, 1.0, 0.0])))
260        );
261        assert!(
262            extracted_vertices
263                .iter()
264                .any(|vertex| almost_equals(vertex, &DVec3::from([0.0, 1.0, 1.0])))
265        );
266        assert!(
267            extracted_vertices
268                .iter()
269                .any(|vertex| almost_equals(vertex, &DVec3::from([1.0, 0.0, 0.0])))
270        );
271        assert!(
272            extracted_vertices
273                .iter()
274                .any(|vertex| almost_equals(vertex, &DVec3::from([1.0, 0.0, 1.0])))
275        );
276        assert!(
277            extracted_vertices
278                .iter()
279                .any(|vertex| almost_equals(vertex, &DVec3::from([1.0, 1.0, 0.0])))
280        );
281        assert!(
282            extracted_vertices
283                .iter()
284                .any(|vertex| almost_equals(vertex, &DVec3::from([1.0, 1.0, 1.0])))
285        );
286    }
287
288    #[test]
289    fn test_brush_pyramid() {
290        let vertices = vec![
291            DVec3::from([0.0, 0.0, 0.0]),
292            DVec3::from([0.0, 0.0, 1.0]),
293            DVec3::from([0.0, 1.0, 0.0]),
294            DVec3::from([1.0, 0.0, 0.0]),
295            DVec3::from([0.3, 0.3, 0.3]),
296        ];
297
298        let brush = Brush::try_from_vertices(&vertices, Some(1000)).unwrap();
299
300        let extracted_vertices: &Vec<DVec3> = brush.vertices();
301        let extracted_sides: Vec<Side> = brush.to_sides_unique();
302
303        assert_eq!(extracted_vertices.len(), 4);
304        assert_eq!(extracted_sides.len(), 4);
305
306        assert!(
307            extracted_vertices
308                .iter()
309                .any(|vertex| almost_equals(vertex, &DVec3::from([0.0, 0.0, 0.0])))
310        );
311        assert!(
312            extracted_vertices
313                .iter()
314                .any(|vertex| almost_equals(vertex, &DVec3::from([0.0, 0.0, 1.0])))
315        );
316        assert!(
317            extracted_vertices
318                .iter()
319                .any(|vertex| almost_equals(vertex, &DVec3::from([0.0, 1.0, 0.0])))
320        );
321        assert!(
322            extracted_vertices
323                .iter()
324                .any(|vertex| almost_equals(vertex, &DVec3::from([1.0, 0.0, 0.0])))
325        );
326    }
327
328    #[test]
329    fn bake_side() {
330        let testvertex1 = DVec3 {
331            x: 1.0,
332            y: 2.0,
333            z: 3.0,
334        };
335
336        let testvertex2 = DVec3 {
337            x: 10.0,
338            y: 20.0,
339            z: 30.0,
340        };
341
342        let testvertex3 = DVec3 {
343            x: 100.0,
344            y: 200.0,
345            z: 300.0,
346        };
347
348        let side = Side {
349            geom: SideGeom([testvertex1, testvertex2, testvertex3]),
350            mtrl: SideMtrl::default(),
351        };
352
353        assert_eq!(
354            format!("{}", side.bake()),
355            "( 1.000000 2.000000 3.000000 ) ( 10.000000 20.000000 30.000000 ) ( 100.000000 200.000000 300.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0"
356        );
357    }
358
359    #[test]
360    fn bake_brush_pyramid() {
361        let vertices = vec![
362            DVec3::from([0.0, 0.0, 0.0]),
363            DVec3::from([0.0, 0.0, 1.0]),
364            DVec3::from([0.0, 1.0, 0.0]),
365            DVec3::from([1.0, 0.0, 0.0]),
366            DVec3::from([0.3, 0.3, 0.3]),
367        ];
368
369        let brush = Brush::try_from_vertices(&vertices, Some(1000)).unwrap();
370
371        let should_eq_str = r"{
372( 0.000000 0.000000 0.000000 ) ( 0.000000 1.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
373( 0.000000 1.000000 0.000000 ) ( 1.000000 0.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
374( 1.000000 0.000000 0.000000 ) ( 0.000000 0.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
375( 0.000000 0.000000 0.000000 ) ( 1.000000 0.000000 0.000000 ) ( 0.000000 1.000000 0.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
376}
377";
378        println!("{}", brush.bake());
379        println!("{}", should_eq_str);
380        assert_eq!(format!("{}", brush.bake()), should_eq_str);
381    }
382}