subsphere/
basetri.rs

1//! Contains types related to [`BaseTriSphere`].
2use crate::math::vec;
3
4/// A tessellation of the unit sphere constructed by projecting a triangular platonic solid
5/// onto it.
6#[repr(u8)]
7#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
8pub enum BaseTriSphere {
9    /// A tessellation of the unit sphere constructed by projecting an icosahedron onto it.
10    #[default]
11    Icosa = 0,
12    /// A tessellation of the unit sphere constructed by projecting an octohedron onto it.
13    Octo = 1,
14    /// A tessellation of the unit sphere constructed by projecting a tetrahedron onto it.
15    Tetra = 2,
16}
17
18impl BaseTriSphere {
19    /// The degree of the vertices in this base shape.
20    pub const fn vertex_degree(self) -> usize {
21        self.lookup::<5, 4, 3>() as usize
22    }
23
24    /// The length of any edge on this base shape, or equivalently, the angle between any
25    /// two adjacent vertices.
26    pub const fn edge_length(self) -> f64 {
27        [1.1071487177940904, std::f64::consts::FRAC_PI_2][self as usize]
28    }
29
30    /// The cosine of [`edge_length`](BaseTriSphere::edge_length`), or equivalently, the dot
31    /// product between any two adjacent vertices.
32    pub const fn edge_cos_length(self) -> f64 {
33        [C_1, 0.0][self as usize]
34    }
35
36    /// The internal representation of the first face of this base shape.
37    pub(crate) const fn first_face_inner(self) -> u8 {
38        self.lookup::<0, 20, 28>()
39    }
40
41    /// One more than the internal representation of the last face of this base shape.
42    pub(crate) const fn last_face_inner(self) -> u8 {
43        self.lookup::<20, 28, 32>()
44    }
45
46    /// The internal representation of the first vertex of this base shape.
47    pub(crate) const fn first_vertex_inner(self) -> u8 {
48        self.lookup::<0, 12, 18>()
49    }
50
51    /// One more than the internal representation of the last vertex of this base shape.
52    pub(crate) const fn last_vertex_inner(self) -> u8 {
53        self.lookup::<12, 18, 22>()
54    }
55
56    /// Returns the constant value corresponding to this base shape.
57    const fn lookup<const ICOSA: u8, const OCTO: u8, const TETRA: u8>(self) -> u8 {
58        ((((TETRA as u32) << 16) | ((OCTO as u32) << 8) | ICOSA as u32) >> (self as u32 * 8)) as u8
59    }
60
61    /// Determines which face contains the given point on the unit sphere.
62    pub const fn face_at(self, point: [f64; 3]) -> Face {
63        match self {
64            BaseTriSphere::Icosa => {
65                let index = icosa_point_index(point);
66                ICOSA_FACE_AT[index as usize]
67            }
68            BaseTriSphere::Octo => {
69                let index = octo_point_index(point);
70                Face((OCTO_FACE_AT >> (index * 8)) as u8)
71            }
72            BaseTriSphere::Tetra => todo!(),
73        }
74    }
75
76    /// The number of vertices on the sphere.
77    pub const fn num_vertices(self) -> usize {
78        self.lookup::<12, 6, 4>() as usize
79    }
80
81    /// Gets the [`Vertex`] with the specified index.
82    pub const fn vertex(self, index: usize) -> Vertex {
83        assert!(index < self.num_vertices(), "index out of bounds");
84        Vertex(self.first_vertex_inner() + index as u8)
85    }
86}
87
88impl crate::Sphere for BaseTriSphere {
89    type Face = Face;
90    type Vertex = Vertex;
91    type HalfEdge = HalfEdge;
92
93    fn num_faces(&self) -> usize {
94        self.lookup::<20, 8, 4>() as usize
95    }
96
97    fn face(&self, index: usize) -> Face {
98        assert!(index < self.num_faces(), "index out of bounds");
99        Face(self.first_face_inner() + index as u8)
100    }
101
102    fn faces(&self) -> impl Iterator<Item = Face> {
103        (self.first_face_inner()..self.last_face_inner()).map(Face)
104    }
105
106    fn face_at(&self, point: [f64; 3]) -> Face {
107        (*self).face_at(point)
108    }
109
110    fn num_vertices(&self) -> usize {
111        (*self).num_vertices()
112    }
113
114    fn vertex(&self, index: usize) -> Vertex {
115        (*self).vertex(index)
116    }
117
118    fn vertices(&self) -> impl Iterator<Item = Vertex> {
119        (self.first_vertex_inner()..self.last_vertex_inner()).map(Vertex)
120    }
121}
122
123/// A face of a [`BaseTriSphere`].
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
125pub struct Face(pub(crate) u8);
126
127impl Face {
128    /// Gets the [`BaseTriSphere`] this face belongs to.
129    pub const fn sphere(self) -> BaseTriSphere {
130        unsafe { std::mem::transmute(self.0.saturating_sub(12) / 8) }
131    }
132
133    /// Indicates whether this face [owns](OwnershipInfo) its second vertex.
134    pub(crate) fn owns_vertex_1(self) -> bool {
135        OWNERSHIP.owns_vert_1 & (1 << self.0) != 0
136    }
137
138    /// Indicates whether this face [owns](OwnershipInfo) its third edge.
139    pub(crate) fn owns_edge_2(self) -> bool {
140        OWNERSHIP.owns_edge_2 & (1 << self.0) != 0
141    }
142
143    /// Gets the number of vertices that are owned by the faces preceding this face in
144    /// iteration order, not counting the first vertex of the shape.
145    pub(crate) fn num_owned_vertices_before(self) -> usize {
146        let start = self.sphere().first_face_inner();
147        let before_mask = (1u32 << self.0) - 1;
148        ((OWNERSHIP.owns_vert_1 & before_mask) >> start).count_ones() as usize
149    }
150
151    /// Gets the number of edges that are owned by the faces preceding this face in iteration
152    /// order.
153    pub(crate) fn num_owned_edges_before(self) -> usize {
154        let start = self.sphere().first_face_inner();
155        let index = self.0 - start;
156        let before_mask = (1u32 << self.0) - 1;
157        index as usize + ((OWNERSHIP.owns_edge_2 & before_mask) >> start).count_ones() as usize
158    }
159
160    /// Gets the [`HalfEdge`] which has the given [`index`](HalfEdge::side_index) and this face as
161    /// its [`inside`](HalfEdge::inside).
162    pub const fn side(self, index: usize) -> HalfEdge {
163        assert!(index < 3, "index out of bounds");
164        HalfEdge((self.0 << 2) | index as u8)
165    }
166
167    /// Gets the point at the center of this face.
168    pub const fn center(self) -> [f64; 3] {
169        let v_0 = self.side(0).start().pos();
170        let v_1 = self.side(1).start().pos();
171        let v_2 = self.side(2).start().pos();
172        let mul = [0.4194695241216063, 0.5773502691896257][self.sphere() as usize]; // TODO
173        vec::mul(vec::add(vec::add(v_0, v_1), v_2), mul)
174    }
175}
176
177#[test]
178fn test_center_face_at() {
179    use crate::Sphere;
180    // TODO: Extend to other spheres
181    for sphere in [BaseTriSphere::Icosa, BaseTriSphere::Octo] {
182        for face in sphere.faces() {
183            let center = face.center();
184            assert!((vec::dot(center, center) - 1.0).abs() < 1.0e-12);
185            assert_eq!(sphere.face_at(center), face);
186        }
187    }
188}
189
190impl crate::Face for Face {
191    type Vertex = Vertex;
192    type HalfEdge = HalfEdge;
193
194    fn index(&self) -> usize {
195        (self.0 - self.sphere().first_face_inner()) as usize
196    }
197
198    fn area(&self) -> f64 {
199        // TODO: Replace with lookup table
200        let v_0 = self.side(0).start().pos();
201        let v_1 = self.side(1).start().pos();
202        let v_2 = self.side(2).start().pos();
203        crate::util::tri_area([v_0, v_1, v_2])
204    }
205
206    fn num_sides(&self) -> usize {
207        3
208    }
209
210    fn side(&self, index: usize) -> HalfEdge {
211        (*self).side(index)
212    }
213}
214
215/// A vertex of a [`BaseTriSphere`].
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
217pub struct Vertex(u8);
218
219impl Vertex {
220    /// Gets the [`BaseTriSphere`] this vertex belongs to.
221    pub const fn sphere(self) -> BaseTriSphere {
222        unsafe { std::mem::transmute(self.0.saturating_sub(6) / 6) }
223    }
224
225    /// Gets the face which [owns](OwnershipInfo) this vertex.
226    pub(crate) const fn owner(self) -> Face {
227        OWNERSHIP.vert_owner[self.0 as usize]
228    }
229
230    /// The position of this vertex.
231    pub const fn pos(self) -> [f64; 3] {
232        VERTS[self.0 as usize]
233    }
234}
235
236impl crate::Vertex for Vertex {
237    type Face = Face;
238    type HalfEdge = HalfEdge;
239
240    fn index(&self) -> usize {
241        (self.0 - self.sphere().first_vertex_inner()) as usize
242    }
243
244    fn pos(&self) -> [f64; 3] {
245        (*self).pos()
246    }
247
248    fn degree(&self) -> usize {
249        self.sphere().vertex_degree()
250    }
251
252    fn outgoing(&self, index: usize) -> HalfEdge {
253        assert!(index < self.degree(), "index out of bounds");
254        OUTGOING[self.0 as usize][index]
255    }
256}
257
258/// A half-edge of a [`BaseTriSphere`].
259#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
260pub struct HalfEdge(u8);
261
262impl HalfEdge {
263    /// Gets the [`BaseTriSphere`] this face belongs to.
264    pub const fn sphere(self) -> BaseTriSphere {
265        self.inside().sphere()
266    }
267
268    /// The index of this half-edge within the [`sides`](crate::Face::sides) list of its
269    /// [`inside`](HalfEdge::inside).
270    pub const fn side_index(self) -> usize {
271        (self.0 & 0b11) as usize
272    }
273
274    /// Gets the [`Face`] whose interior boundary contains this half-edge.
275    pub const fn inside(self) -> Face {
276        Face(self.0 >> 2)
277    }
278
279    /// Gets the [`Vertex`] at the "start" of this half-edge.
280    pub const fn start(self) -> Vertex {
281        Vertex(INDICES[self.inside().0 as usize][self.side_index()])
282    }
283
284    /// Gets the complementary half-edge on the opposite side of the edge.
285    ///
286    /// The returned half-edge will go in the opposite direction along the same edge.
287    pub const fn complement(self) -> Self {
288        COMPLEMENT[self.inside().0 as usize][self.side_index()]
289    }
290
291    /// Gets the half-edge which shares the [`inside`](HalfEdge::inside) face of this half-edge and
292    /// precedes it in counter-clockwise order around the face.
293    pub const fn prev(self) -> Self {
294        HalfEdge(if self.0 & 0b11 == 0 {
295            self.0 + 2
296        } else {
297            self.0 - 1
298        })
299    }
300
301    /// Gets the half-edge which shares the [`inside`](HalfEdge::inside) face of this half-edge and
302    /// follows it in counter-clockwise order around the face.
303    pub const fn next(self) -> Self {
304        HalfEdge(if self.0 & 0b11 == 2 {
305            self.0 - 2
306        } else {
307            self.0 + 1
308        })
309    }
310}
311
312impl crate::HalfEdge for HalfEdge {
313    type Face = Face;
314    type Vertex = Vertex;
315
316    fn side_index(&self) -> usize {
317        (*self).side_index()
318    }
319
320    fn length(&self) -> f64 {
321        self.sphere().edge_length()
322    }
323
324    fn angle(&self) -> f64 {
325        // TODO: Replace with lookup table
326        let v_a = self.prev().start().pos();
327        let v_b = self.start().pos();
328        let v_c = self.next().start().pos();
329        crate::util::angle(v_a, v_b, v_c)
330    }
331
332    fn inside(&self) -> Face {
333        (*self).inside()
334    }
335
336    fn start(&self) -> Vertex {
337        (*self).start()
338    }
339
340    fn complement(&self) -> Self {
341        (*self).complement()
342    }
343
344    fn prev(&self) -> Self {
345        (*self).prev()
346    }
347
348    fn next(&self) -> Self {
349        (*self).next()
350    }
351}
352
353/// Table used to implement [`crate::Vertex::outgoing`].
354static OUTGOING: [[HalfEdge; 5]; NUM_VERTS] = const {
355    let mut res: [[HalfEdge; 5]; NUM_VERTS] = [[HalfEdge(u8::MAX); 5]; NUM_VERTS];
356    let mut j = 0;
357    while j < NUM_VERTS {
358        let vert = Vertex(j as u8);
359        let owner = vert.owner();
360
361        // Determine a unique first outgoing edge for each vertex
362        let first_outgoing = {
363            let mut k = 0;
364            loop {
365                let edge = owner.side(k);
366                if edge.start().0 == vert.0 {
367                    break edge;
368                }
369                k += 1;
370                if k == 3 {
371                    panic!("owner does not contain vertex");
372                }
373            }
374        };
375
376        // Build outgoing edge list
377        let mut k = 0;
378        let mut outgoing = first_outgoing;
379        loop {
380            res[j][k] = outgoing;
381            k += 1;
382            outgoing = outgoing.prev().complement();
383            assert!(
384                outgoing.start().0 == vert.0,
385                "outgoing edge does not start at vertex"
386            );
387            if outgoing.0 == first_outgoing.0 {
388                break;
389            }
390        }
391        assert!(
392            k == Vertex(j as u8).sphere().vertex_degree(),
393            "degree mismatch"
394        );
395        j += 1;
396    }
397    res
398};
399
400/// Provides information about the "ownership" relationships between faces, edges, and vertices.
401///
402/// The rules for ownership are as follows:
403///  * Every vertex and edge is owned by exactly one face.
404///  * Faces must own their first edge.
405///  * Faces may own their third edge.
406///  * The first face in a [`BaseTriSphere`] must own its first vertex, which must be the first
407///    vertex in the [`BaseTriSphere`].
408///  * Faces may own their second vertex.
409struct OwnershipInfo {
410    vert_owner: [Face; NUM_VERTS],
411    owns_vert_1: u32,
412    owns_edge_2: u32,
413}
414
415/// Provides information about the "ownership" relationships between faces, edges, and vertices.
416static OWNERSHIP: OwnershipInfo = const {
417    // Assign ownership of first vertex in each shape
418    let mut vert_owner: [Face; NUM_VERTS] = [Face(u8::MAX); NUM_VERTS];
419    vert_owner[0] = Face(0);
420    vert_owner[12] = Face(20);
421
422    // Assign ownership of each vertex to the first face that contains it as it's 1 vertex
423    let mut owns_vert_1 = 0;
424    let mut i = 0;
425    while i < NUM_FACES {
426        let v_1 = INDICES[i][1] as usize;
427        if vert_owner[v_1].0 == u8::MAX {
428            vert_owner[v_1] = Face(i as u8);
429            owns_vert_1 |= 1 << i;
430        }
431        i += 1;
432    }
433
434    // Verify that every vertex has an owner
435    let mut j = 0;
436    while j < NUM_VERTS {
437        assert!(vert_owner[j].0 != u8::MAX, "vertex does not have an owner");
438        j += 1;
439    }
440
441    // Assign ownership of each edge. First by edge 0, then optionally by edge 2.
442    let mut edge_has_owner: [[bool; NUM_VERTS]; NUM_VERTS] = [[false; NUM_VERTS]; NUM_VERTS];
443    let mut num_owned_edges = 0;
444    let mut i = 0;
445    while i < NUM_FACES {
446        let v_0 = INDICES[i][0] as usize;
447        let v_1 = INDICES[i][1] as usize;
448        let edge_has_owner = if v_0 < v_1 {
449            &mut edge_has_owner[v_0][v_1]
450        } else {
451            &mut edge_has_owner[v_1][v_0]
452        };
453        assert!(!*edge_has_owner, "edge already has an owner");
454        *edge_has_owner = true;
455        num_owned_edges += 1;
456        i += 1;
457    }
458    let mut owns_edge_2 = 0;
459    let mut i = 0;
460    while i < NUM_FACES {
461        let v_2 = INDICES[i][2] as usize;
462        let v_0 = INDICES[i][0] as usize;
463        let edge_has_owner = if v_0 < v_2 {
464            &mut edge_has_owner[v_0][v_2]
465        } else {
466            &mut edge_has_owner[v_2][v_0]
467        };
468        if !*edge_has_owner {
469            *edge_has_owner = true;
470            num_owned_edges += 1;
471            owns_edge_2 |= 1 << i;
472        }
473        i += 1;
474    }
475
476    // Verify that every edge has an owner
477    assert!(
478        num_owned_edges == NUM_FACES * 3 / 2,
479        "not all edges have an owner"
480    );
481
482    // Finalize ownership info
483    OwnershipInfo {
484        vert_owner,
485        owns_vert_1,
486        owns_edge_2,
487    }
488};
489
490/// Table used to implement [`crate::HalfEdge::complement`].
491static COMPLEMENT: [[HalfEdge; 3]; NUM_FACES] = const {
492    // Build adjacent mapping for potential edges
493    let mut adjacent: [[u8; NUM_VERTS]; NUM_VERTS] = [[u8::MAX; NUM_VERTS]; NUM_VERTS];
494    let mut i = 0;
495    while i < NUM_FACES {
496        let v_0 = INDICES[i][0] as usize;
497        let v_1 = INDICES[i][1] as usize;
498        let v_2 = INDICES[i][2] as usize;
499        assert!(adjacent[v_0][v_1] == u8::MAX, "duplicate edge detected");
500        assert!(adjacent[v_1][v_2] == u8::MAX, "duplicate edge detected");
501        assert!(adjacent[v_2][v_0] == u8::MAX, "duplicate edge detected");
502        adjacent[v_0][v_1] = (i as u8) << 2;
503        adjacent[v_1][v_2] = ((i as u8) << 2) | 1;
504        adjacent[v_2][v_0] = ((i as u8) << 2) | 2;
505        i += 1;
506    }
507
508    // Convert to adjacency table for faces
509    let mut res: [[HalfEdge; 3]; NUM_FACES] = [[HalfEdge(0); 3]; NUM_FACES];
510    i = 0;
511    while i < NUM_FACES {
512        let v_0 = INDICES[i][0] as usize;
513        let v_1 = INDICES[i][1] as usize;
514        let v_2 = INDICES[i][2] as usize;
515        assert!(adjacent[v_1][v_0] != u8::MAX, "hanging edge detected");
516        assert!(adjacent[v_2][v_1] != u8::MAX, "hanging edge detected");
517        assert!(adjacent[v_0][v_2] != u8::MAX, "hanging edge detected");
518        res[i][0] = HalfEdge(adjacent[v_1][v_0]);
519        res[i][1] = HalfEdge(adjacent[v_2][v_1]);
520        res[i][2] = HalfEdge(adjacent[v_0][v_2]);
521        i += 1;
522    }
523    res
524};
525
526/// Given a point on the unit sphere, gets an index which can be used to identify which
527/// icosahedron [`Face`] contains it.
528/// 
529/// The indexing scheme is arbitrary, but points on different [`Face`]s must have different
530/// indices.
531const fn icosa_point_index(point: [f64; 3]) -> u8 {
532    // The index is constructed by testing the dot product of `point` with 5 distinct (and
533    // non-antipodal) vertices. For each test, we get one of 3 results. This gives us 243
534    // "possible" indices, with each face corresponding to exactly one of these.
535    let mut res = 0;
536    let mut i = 0;
537    while i < 5 {
538        let dot = vec::dot(point, BaseTriSphere::Icosa.vertex(i).pos());
539        let comp = (dot.is_sign_positive() as u8 + 1) * ((dot.abs() > C_1) as u8);
540        res = res * 3 + comp;
541        i += 1;
542    }
543    res
544}
545
546/// A lookup table which identifies which [`Face`] corresponds to a particular result from
547/// [`icosa_point_index`].
548/// 
549/// For indices that don't correspond exactly to a face, this will provide an arbitrary
550/// nearby [`Face`].
551static ICOSA_FACE_AT: [Face; 243] = const {
552    let mut res = [Face(u8::MAX); 243];
553
554    // Assign each face to its proper index
555    let sphere = BaseTriSphere::Icosa;
556    let mut i = sphere.first_face_inner();
557    while i < sphere.last_face_inner() {
558        let face = Face(i);
559        let j = icosa_point_index(face.center());
560        assert!(res[j as usize].0 == u8::MAX, "index already assigned");
561        res[j as usize] = face;
562        i += 1;
563    }
564
565    // Fill remaining indices by iteratively copying the contents of a "nearby" index which is
566    // already filled.
567    let mut next = res;
568    let mut all_filled = false;
569    while !all_filled {
570        let mut index = 0;
571        all_filled = true;
572        while index < 243 {
573            if res[index].0 == u8::MAX {
574                all_filled = false;
575
576                // Try to find a component we can change slightly to get a filled index.
577                let mut mul = 1;
578                while mul <= 83 {
579                    let comp = (index / mul) % 3;
580                    if comp == 0 {
581                        if res[index + mul].0 != u8::MAX {
582                            next[index] = res[index + mul];
583                            break;
584                        }
585                        if res[index + 2 * mul].0 != u8::MAX {
586                            next[index] = res[index + 2 * mul];
587                            break;
588                        }
589                    } else if res[index - comp * mul].0 != u8::MAX {
590                        next[index] = res[index - comp * mul];
591                        break;
592                    }
593                    mul *= 3;
594                }
595            }
596            index += 1;
597        }
598        res = next;
599    }
600    res
601};
602
603/// Given a point on the unit sphere, gets an index which can be used to identify which
604/// octohedron [`Face`] contains it.
605///
606/// The indexing scheme is arbitrary, but points on different [`Face`]s must have different
607/// indices.
608const fn octo_point_index([x, y, z]: [f64; 3]) -> u8 {
609    (((x >= 0.0) as u8) << 2) | (((y >= 0.0) as u8) << 1) | ((z >= 0.0) as u8)
610}
611
612/// A compact lookup table which identifies which [`Face`] corresponds to a particular result
613/// from [`octo_point_index`].
614const OCTO_FACE_AT: u64 = const {
615    let sphere = BaseTriSphere::Octo;
616    let mut i = sphere.first_face_inner();
617    let mut res = 0;
618    while i < sphere.last_face_inner() {
619        let face = Face(i);
620        let j = octo_point_index(face.center());
621        res |= (i as u64) << (j * 8);
622        i += 1;
623    }
624    res
625};
626
627/// The total number of vertices across all [`BaseTriSphere`]s.
628const NUM_VERTS: usize = 12 + 6;
629
630/// The total number of faces across all [`BaseTriSphere`]s.
631const NUM_FACES: usize = 20 + 8;
632
633/// The vertex position data for all potential vertices on a [`BaseTriSphere`].
634static VERTS: [[f64; 3]; NUM_VERTS] = [
635    // Icosahedron top apex
636    [0.0, 0.0, 1.0],
637    // Icosahedron top ring
638    [C_0, 0.0, C_1],
639    [C_2, C_3, C_1],
640    [-C_4, C_5, C_1],
641    [-C_4, -C_5, C_1],
642    [C_2, -C_3, C_1],
643    // Icosahedron bottom ring
644    [C_4, -C_5, -C_1],
645    [C_4, C_5, -C_1],
646    [-C_2, C_3, -C_1],
647    [-C_0, 0.0, -C_1],
648    [-C_2, -C_3, -C_1],
649    // Icosahedron bottom apex
650    [0.0, 0.0, -1.0],
651    // Octohedron
652    [0.0, 0.0, 1.0],
653    [1.0, 0.0, 0.0],
654    [0.0, 1.0, 0.0],
655    [-1.0, 0.0, 0.0],
656    [0.0, -1.0, 0.0],
657    [0.0, 0.0, -1.0],
658];
659
660/// The face index data for all potential faces on a [`BaseTriSphere`].
661static INDICES: [[u8; 3]; NUM_FACES] = [
662    // Icosahedron top cap
663    [0, 1, 2],
664    [0, 2, 3],
665    [0, 3, 4],
666    [0, 4, 5],
667    [0, 5, 1],
668    // Icosahedron central ring
669    [5, 6, 1],
670    [6, 7, 1],
671    [1, 7, 2],
672    [7, 8, 2],
673    [2, 8, 3],
674    [8, 9, 3],
675    [3, 9, 4],
676    [9, 10, 4],
677    [4, 10, 5],
678    [10, 6, 5],
679    // Icosahedron bottom cap
680    [10, 11, 6],
681    [6, 11, 7],
682    [7, 11, 8],
683    [8, 11, 9],
684    [9, 11, 10],
685    // Octohedron top cap
686    [12, 13, 14],
687    [12, 14, 15],
688    [12, 15, 16],
689    [12, 16, 13],
690    // Octohedron bottom cap
691    [16, 17, 13],
692    [13, 17, 14],
693    [14, 17, 15],
694    [15, 17, 16],
695];
696
697/// `sqrt(4 / 5)`
698const C_0: f64 = 0.8944271909999159;
699
700/// `sqrt(1 / 5)`
701const C_1: f64 = 0.4472135954999579;
702
703/// `(5 - sqrt(5)) / 10`
704const C_2: f64 = 0.276393202250021;
705
706/// `sqrt((5 + sqrt(5)) / 10)`
707const C_3: f64 = 0.8506508083520399;
708
709/// `(5 + sqrt(5)) / 10`
710const C_4: f64 = 0.7236067977499789;
711
712/// `sqrt((5 - sqrt(5)) / 10)`
713const C_5: f64 = 0.5257311121191336;