Skip to main content

loam_shape/
polytope.rs

1use std::sync::LazyLock;
2
3use glam::{Vec3, Vec4};
4
5use crate::polytope_geom::{
6    cell120_vertices, cell16_vertices, cell24_vertices, cell600_vertices, pentatope_vertices,
7    tesseract_vertices,
8};
9
10/// Discriminants match `loam_render::raymarch::SHAPE_*`.
11#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
12#[repr(u32)]
13pub enum Polytope4 {
14    Pentatope = 0,
15    Tesseract = 1,
16    Cell16 = 2,
17    Cell24 = 3,
18    Cell120 = 4,
19    Cell600 = 5,
20}
21
22#[derive(Debug)]
23pub struct Polytope4Topology {
24    /// Canonical (unit-circumradius) coordinates.
25    pub vertices: &'static [Vec4],
26    /// `[lo, hi]` pairs, lexicographically sorted for deterministic iteration.
27    pub edges: &'static [[u32; 2]],
28    /// Ascending index lists, lexicographically sorted for the same reason.
29    pub cells: &'static [&'static [u32]],
30}
31
32impl Polytope4 {
33    /// In `repr(u32)` discriminant order.
34    pub const ALL: [Polytope4; 6] = [
35        Polytope4::Pentatope,
36        Polytope4::Tesseract,
37        Polytope4::Cell16,
38        Polytope4::Cell24,
39        Polytope4::Cell120,
40        Polytope4::Cell600,
41    ];
42
43    pub fn topology(self) -> &'static Polytope4Topology {
44        match self {
45            Polytope4::Pentatope => &PENTATOPE_TOPOLOGY,
46            Polytope4::Tesseract => &TESSERACT_TOPOLOGY,
47            Polytope4::Cell16 => &CELL16_TOPOLOGY,
48            Polytope4::Cell24 => &CELL24_TOPOLOGY,
49            Polytope4::Cell120 => &CELL120_TOPOLOGY,
50            Polytope4::Cell600 => &CELL600_TOPOLOGY,
51        }
52    }
53
54    pub fn vertex_count(self) -> usize {
55        self.topology().vertices.len()
56    }
57
58    /// Undirected edges, each counted once.
59    pub fn edge_count(self) -> usize {
60        self.topology().edges.len()
61    }
62
63    /// Bounding 3-cells (the facets), not the full face lattice.
64    pub fn cell_count(self) -> usize {
65        self.topology().cells.len()
66    }
67
68    /// Length equal to the inradius, along the cell's outward face normal.
69    pub fn cell_centers(self) -> Vec<Vec4> {
70        let topo = self.topology();
71        topo.cells
72            .iter()
73            .map(|cell| {
74                cell.iter()
75                    .map(|&i| topo.vertices[i as usize])
76                    .sum::<Vec4>()
77                    / cell.len() as f32
78            })
79            .collect()
80    }
81
82    /// Outward unit normals and mean facet offset in canonical coordinates.
83    pub fn face_planes(self) -> (Vec<Vec4>, f32) {
84        let topo = self.topology();
85        let mut normals = Vec::with_capacity(topo.cells.len());
86        let mut inradius_sum = 0.0;
87        for cell in topo.cells {
88            let centroid: Vec4 = cell
89                .iter()
90                .map(|&i| topo.vertices[i as usize])
91                .sum::<Vec4>()
92                / cell.len() as f32;
93            let r = centroid.length();
94
95            normals.push(centroid / r);
96            inradius_sum += r;
97        }
98        let inradius = inradius_sum / normals.len() as f32;
99        (normals, inradius)
100    }
101}
102
103const DEFAULT_LINE_COLOR: [f32; 4] = [0.9, 0.9, 0.9, 1.0];
104const DEFAULT_LINE_WIDTH: f32 = 1.5;
105
106impl crate::Visualizable<4> for Polytope4 {
107    fn to_lines(&self) -> Result<crate::LineMesh<4>, crate::NotVisualizable> {
108        let topo = self.topology();
109        let mut mesh = crate::LineMesh::<4>::default();
110        mesh.segments.reserve(topo.edges.len());
111        mesh.colors.reserve(topo.edges.len());
112        mesh.widths.reserve(topo.edges.len());
113        for &[i, j] in topo.edges {
114            let a = topo.vertices[i as usize].to_array();
115            let b = topo.vertices[j as usize].to_array();
116            mesh.segments.push((a, b));
117            mesh.colors.push((DEFAULT_LINE_COLOR, DEFAULT_LINE_COLOR));
118            mesh.widths.push(DEFAULT_LINE_WIDTH);
119        }
120        Ok(mesh)
121    }
122
123    fn to_triangles(&self) -> Result<crate::TriangleMesh<4>, crate::NotVisualizable> {
124        Err(crate::NotVisualizable::Degenerate)
125    }
126
127    fn to_points(&self) -> Result<crate::PointMesh<4>, crate::NotVisualizable> {
128        let topo = self.topology();
129        let mut mesh = crate::PointMesh::<4>::default();
130        mesh.positions.reserve(topo.vertices.len());
131        mesh.colors.reserve(topo.vertices.len());
132        mesh.sizes.reserve(topo.vertices.len());
133        for v in topo.vertices {
134            mesh.positions.push(v.to_array());
135            mesh.colors.push(DEFAULT_LINE_COLOR);
136            mesh.sizes.push(4.0);
137        }
138        Ok(mesh)
139    }
140}
141
142impl Polytope4 {
143    /// Indexes `palette` modulo its length.
144    pub fn lines_colored_by_cell(self, palette: &[[f32; 4]]) -> crate::LineMesh<4> {
145        let topo = self.topology();
146        let mut mesh = crate::LineMesh::<4>::default();
147        let n_palette = palette.len().max(1);
148        for &[i, j] in topo.edges {
149            let cell_idx = topo
150                .cells
151                .iter()
152                .position(|cell| cell.contains(&i) && cell.contains(&j))
153                .unwrap_or(0);
154            let color = palette
155                .get(cell_idx % n_palette)
156                .copied()
157                .unwrap_or(DEFAULT_LINE_COLOR);
158            mesh.segments.push((
159                topo.vertices[i as usize].to_array(),
160                topo.vertices[j as usize].to_array(),
161            ));
162            mesh.colors.push((color, color));
163            mesh.widths.push(DEFAULT_LINE_WIDTH);
164        }
165        mesh
166    }
167
168    pub fn lines_colored_by_position(self) -> crate::LineMesh<4> {
169        let topo = self.topology();
170        let mut mesh = crate::LineMesh::<4>::default();
171        mesh.segments.reserve(topo.edges.len());
172        mesh.colors.reserve(topo.edges.len());
173        mesh.widths.reserve(topo.edges.len());
174        for &[i, j] in topo.edges {
175            let va = topo.vertices[i as usize];
176            let vb = topo.vertices[j as usize];
177            mesh.segments.push((va.to_array(), vb.to_array()));
178            mesh.colors
179                .push((vertex_color_by_position(va), vertex_color_by_position(vb)));
180            mesh.widths.push(DEFAULT_LINE_WIDTH);
181        }
182        mesh
183    }
184}
185
186pub fn vertex_color_by_position(v: Vec4) -> [f32; 4] {
187    let n = v.try_normalize().unwrap_or(Vec4::ZERO);
188    let bias = |c: f32| 0.25 + 0.75 * (0.5 + 0.5 * c);
189    let w_mod = 0.7 + 0.3 * (0.5 + 0.5 * n.w);
190    [bias(n.x) * w_mod, bias(n.y) * w_mod, bias(n.z) * w_mod, 1.0]
191}
192
193#[cfg(test)]
194const SECTION_FILL_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 0.55];
195const SECTION_EDGE_COLOR: [f32; 4] = [0.30, 0.85, 0.95, 1.0];
196const SECTION_EDGE_WIDTH: f32 = 2.0;
197
198#[cfg(test)]
199fn polytope4_section_overlay(
200    polytope: Polytope4,
201    slice: loam_math::WPlane,
202) -> (crate::TriangleMesh<3>, crate::LineMesh<3>) {
203    let topo = polytope.topology();
204    polytope_section_overlay_with_vertices(topo.edges, topo.cells, topo.vertices, slice)
205}
206
207#[cfg(test)]
208fn polytope_section_overlay_with_vertices(
209    edges: &[[u32; 2]],
210    cells: &[&[u32]],
211    vertices: &[Vec4],
212    slice: loam_math::WPlane,
213) -> (crate::TriangleMesh<3>, crate::LineMesh<3>) {
214    let mut tri_mesh = crate::TriangleMesh::<3>::default();
215    let mut edge_mesh = crate::LineMesh::<3>::default();
216    let mut scratch = SectionScratch::default();
217
218    for_each_section_cap(
219        edges,
220        cells,
221        vertices,
222        slice,
223        &mut scratch,
224        |ordered, centroid| {
225            push_cap_fan(ordered, centroid, SECTION_FILL_COLOR, &mut tri_mesh);
226            push_cap_perimeter(ordered, &mut edge_mesh);
227        },
228    );
229
230    (tri_mesh, edge_mesh)
231}
232
233pub fn polytope_section_perimeter_append(
234    edges: &[[u32; 2]],
235    cells: &[&[u32]],
236    vertices: &[Vec4],
237    slice: loam_math::WPlane,
238    scratch: &mut SectionScratch,
239    out: &mut crate::LineMesh<3>,
240) {
241    for_each_section_cap(edges, cells, vertices, slice, scratch, |ordered, _| {
242        push_cap_perimeter(ordered, out);
243    });
244}
245
246fn push_cap_fan(
247    ordered: &[Vec3],
248    centroid: Vec3,
249    color: [f32; 4],
250    out: &mut crate::TriangleMesh<3>,
251) {
252    let cv_base = out.vertices.len() as u32;
253    out.vertices.push(centroid.to_array());
254    out.colors.push(color);
255    for cap_v in ordered {
256        out.vertices.push(cap_v.to_array());
257        out.colors.push(color);
258    }
259    let n = ordered.len() as u32;
260    for k in 0..n {
261        let k_next = (k + 1) % n;
262        out.indices
263            .push([cv_base, cv_base + 1 + k, cv_base + 1 + k_next]);
264    }
265}
266
267fn push_cap_perimeter(ordered: &[Vec3], out: &mut crate::LineMesh<3>) {
268    for k in 0..ordered.len() {
269        let a = ordered[k];
270        let b = ordered[(k + 1) % ordered.len()];
271        out.segments.push((a.to_array(), b.to_array()));
272        out.colors.push((SECTION_EDGE_COLOR, SECTION_EDGE_COLOR));
273        out.widths.push(SECTION_EDGE_WIDTH);
274    }
275}
276
277pub fn polytope_section_faces_append(
278    edges: &[[u32; 2]],
279    cells: &[&[u32]],
280    vertices: &[Vec4],
281    slice: loam_math::WPlane,
282    color: [f32; 4],
283    scratch: &mut SectionScratch,
284    out: &mut crate::TriangleMesh<3>,
285) {
286    for_each_section_cap(
287        edges,
288        cells,
289        vertices,
290        slice,
291        scratch,
292        |ordered, centroid| push_cap_fan(ordered, centroid, color, out),
293    );
294}
295
296#[derive(Debug, Default)]
297pub struct SectionScratch {
298    cap: Vec<Vec3>,
299    keys: Vec<(usize, f32)>,
300    ordered: Vec<Vec3>,
301}
302
303// `emit` runs in topology cell order, so mesh output is deterministic.
304fn for_each_section_cap(
305    edges: &[[u32; 2]],
306    cells: &[&[u32]],
307    vertices: &[Vec4],
308    slice: loam_math::WPlane,
309    scratch: &mut SectionScratch,
310    mut emit: impl FnMut(&[Vec3], Vec3),
311) {
312    let slice = perturb_slice_if_needed(slice, vertices);
313
314    let polytope_center_r3: Vec3 = if vertices.is_empty() {
315        Vec3::ZERO
316    } else {
317        let mean: Vec4 = vertices.iter().copied().sum::<Vec4>() / vertices.len() as f32;
318        Vec3::new(mean.x, mean.y, mean.z)
319    };
320
321    for cell in cells {
322        let (w_min, w_max) = cell_w_range(cell, vertices);
323        if w_max < slice.w_slice - loam_math::SLICE_PERTURBATION_EPSILON
324            || w_min > slice.w_slice + loam_math::SLICE_PERTURBATION_EPSILON
325        {
326            continue;
327        }
328
329        let cap = &mut scratch.cap;
330        cap.clear();
331        for &[i, j] in edges {
332            if !cell.contains(&i) || !cell.contains(&j) {
333                continue;
334            }
335            if let Some((_, p3)) = slice.intersect_edge(vertices[i as usize], vertices[j as usize])
336            {
337                cap.push(p3);
338            }
339        }
340        if cap.len() < 3 {
341            continue;
342        }
343
344        let centroid: Vec3 = cap.iter().copied().sum::<Vec3>() / cap.len() as f32;
345        let Some((basis_u, mut basis_v)) = fit_plane_basis(centroid, cap) else {
346            continue;
347        };
348
349        let outward = centroid - polytope_center_r3;
350        let face_normal = basis_u.cross(basis_v);
351        if outward.length_squared() > 1e-12 && face_normal.dot(outward) < 0.0 {
352            basis_v = -basis_v;
353        }
354
355        order_around_centroid(
356            &scratch.cap,
357            centroid,
358            basis_u,
359            basis_v,
360            &mut scratch.keys,
361            &mut scratch.ordered,
362        );
363
364        emit(&scratch.ordered, centroid);
365    }
366}
367
368fn perturb_slice_if_needed(slice: loam_math::WPlane, vertices: &[Vec4]) -> loam_math::WPlane {
369    let eps = loam_math::SLICE_PERTURBATION_EPSILON;
370    let near = vertices.iter().any(|v| (v.w - slice.w_slice).abs() < eps);
371    if near {
372        loam_math::WPlane::new(slice.w_slice + eps)
373    } else {
374        slice
375    }
376}
377
378fn cell_w_range(cell: &[u32], vertices: &[Vec4]) -> (f32, f32) {
379    let mut w_min = f32::INFINITY;
380    let mut w_max = f32::NEG_INFINITY;
381    for &i in cell {
382        let w = vertices[i as usize].w;
383        if w < w_min {
384            w_min = w;
385        }
386        if w > w_max {
387            w_max = w;
388        }
389    }
390    (w_min, w_max)
391}
392
393fn fit_plane_basis(centroid: Vec3, points: &[Vec3]) -> Option<(Vec3, Vec3)> {
394    let eps = loam_math::EDGE_PARALLEL_EPSILON;
395    let mut basis_u = Vec3::ZERO;
396    for p in points {
397        let off = *p - centroid;
398        if off.length_squared() > eps * eps {
399            basis_u = off.normalize();
400            break;
401        }
402    }
403    if basis_u == Vec3::ZERO {
404        return None;
405    }
406    for p in points {
407        let off = *p - centroid;
408        let cross = basis_u.cross(off);
409        if cross.length_squared() > eps * eps {
410            let normal = cross.normalize();
411            let basis_v = normal.cross(basis_u);
412            return Some((basis_u, basis_v));
413        }
414    }
415    None
416}
417
418fn order_around_centroid(
419    points: &[Vec3],
420    centroid: Vec3,
421    basis_u: Vec3,
422    basis_v: Vec3,
423    keys: &mut Vec<(usize, f32)>,
424    ordered: &mut Vec<Vec3>,
425) {
426    keys.clear();
427    keys.extend(points.iter().enumerate().map(|(i, p)| {
428        let off = *p - centroid;
429        let angle = off.dot(basis_v).atan2(off.dot(basis_u));
430        (i, angle)
431    }));
432
433    keys.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
434    ordered.clear();
435    ordered.extend(keys.iter().map(|&(i, _)| points[i]));
436}
437
438static PENTATOPE_VERTICES: LazyLock<&'static [Vec4]> =
439    LazyLock::new(|| Box::leak(pentatope_vertices(1.0).into_boxed_slice()));
440static TESSERACT_VERTICES: LazyLock<&'static [Vec4]> =
441    LazyLock::new(|| Box::leak(tesseract_vertices(1.0).into_boxed_slice()));
442static CELL16_VERTICES: LazyLock<&'static [Vec4]> =
443    LazyLock::new(|| Box::leak(cell16_vertices(1.0).into_boxed_slice()));
444static CELL24_VERTICES: LazyLock<&'static [Vec4]> =
445    LazyLock::new(|| Box::leak(cell24_vertices(1.0).into_boxed_slice()));
446static CELL120_VERTICES: LazyLock<&'static [Vec4]> =
447    LazyLock::new(|| Box::leak(cell120_vertices(1.0).into_boxed_slice()));
448static CELL600_VERTICES: LazyLock<&'static [Vec4]> =
449    LazyLock::new(|| Box::leak(cell600_vertices(1.0).into_boxed_slice()));
450
451// Coxeter, Regular Polytopes, Table I.
452fn canonical_edge_length(p: Polytope4) -> f32 {
453    let phi = (1.0 + 5.0_f32.sqrt()) * 0.5;
454    let sqrt2 = 2.0_f32.sqrt();
455    match p {
456        Polytope4::Pentatope => (5.0_f32 / 2.0).sqrt(),
457        Polytope4::Tesseract => 1.0,
458        Polytope4::Cell16 => sqrt2,
459        Polytope4::Cell24 => 1.0,
460        Polytope4::Cell120 => 1.0 / (phi * phi * sqrt2),
461        Polytope4::Cell600 => 1.0 / phi,
462    }
463}
464
465const EDGE_TOLERANCE: f32 = 1e-4;
466
467fn derive_edges(vertices: &[Vec4], edge_length: f32) -> Vec<[u32; 2]> {
468    let mut edges = Vec::new();
469    for i in 0..vertices.len() {
470        for j in (i + 1)..vertices.len() {
471            let d = (vertices[i] - vertices[j]).length();
472            if (d - edge_length).abs() < EDGE_TOLERANCE {
473                edges.push([i as u32, j as u32]);
474            }
475        }
476    }
477    edges
478}
479
480fn cache_edges(vertices: &'static [Vec4], edge_length: f32) -> &'static [[u32; 2]] {
481    Box::leak(derive_edges(vertices, edge_length).into_boxed_slice())
482}
483
484static PENTATOPE_EDGES: LazyLock<&'static [[u32; 2]]> = LazyLock::new(|| {
485    cache_edges(
486        *PENTATOPE_VERTICES,
487        canonical_edge_length(Polytope4::Pentatope),
488    )
489});
490static TESSERACT_EDGES: LazyLock<&'static [[u32; 2]]> = LazyLock::new(|| {
491    cache_edges(
492        *TESSERACT_VERTICES,
493        canonical_edge_length(Polytope4::Tesseract),
494    )
495});
496static CELL16_EDGES: LazyLock<&'static [[u32; 2]]> =
497    LazyLock::new(|| cache_edges(*CELL16_VERTICES, canonical_edge_length(Polytope4::Cell16)));
498static CELL24_EDGES: LazyLock<&'static [[u32; 2]]> =
499    LazyLock::new(|| cache_edges(*CELL24_VERTICES, canonical_edge_length(Polytope4::Cell24)));
500static CELL120_EDGES: LazyLock<&'static [[u32; 2]]> =
501    LazyLock::new(|| cache_edges(*CELL120_VERTICES, canonical_edge_length(Polytope4::Cell120)));
502static CELL600_EDGES: LazyLock<&'static [[u32; 2]]> =
503    LazyLock::new(|| cache_edges(*CELL600_VERTICES, canonical_edge_length(Polytope4::Cell600)));
504
505const CELL_TOLERANCE: f32 = 1e-4;
506
507const fn cell_vertex_count(p: Polytope4) -> usize {
508    match p {
509        Polytope4::Pentatope => 4,
510        Polytope4::Tesseract => 8,
511        Polytope4::Cell16 => 4,
512        Polytope4::Cell24 => 6,
513        Polytope4::Cell120 => 20,
514        Polytope4::Cell600 => 4,
515    }
516}
517
518fn cross4(a: Vec4, b: Vec4, c: Vec4) -> Vec4 {
519    let drop_x = |v: Vec4| Vec3::new(v.y, v.z, v.w);
520    let drop_y = |v: Vec4| Vec3::new(v.x, v.z, v.w);
521    let drop_z = |v: Vec4| Vec3::new(v.x, v.y, v.w);
522    let drop_w = |v: Vec4| Vec3::new(v.x, v.y, v.z);
523    Vec4::new(
524        drop_x(a).dot(drop_x(b).cross(drop_x(c))),
525        -drop_y(a).dot(drop_y(b).cross(drop_y(c))),
526        drop_z(a).dot(drop_z(b).cross(drop_z(c))),
527        -drop_w(a).dot(drop_w(b).cross(drop_w(c))),
528    )
529}
530
531fn adjacency(num_vertices: usize, edges: &[[u32; 2]]) -> Vec<Vec<u32>> {
532    let mut adj = vec![Vec::new(); num_vertices];
533    for &[i, j] in edges {
534        adj[i as usize].push(j);
535        adj[j as usize].push(i);
536    }
537    adj
538}
539
540const MIN_CROSS4_LENGTH: f32 = 1e-4;
541
542fn derive_cells(vertices: &[Vec4], edges: &[[u32; 2]], cell_size: usize) -> Vec<Vec<u32>> {
543    use std::collections::BTreeSet;
544
545    let adj = adjacency(vertices.len(), edges);
546    let mut cells_set: BTreeSet<Vec<u32>> = BTreeSet::new();
547    for v_idx in 0..vertices.len() {
548        let v_0 = vertices[v_idx];
549        let neighbors = &adj[v_idx];
550        for i in 0..neighbors.len() {
551            for j in (i + 1)..neighbors.len() {
552                for k in (j + 1)..neighbors.len() {
553                    let n_a = vertices[neighbors[i] as usize];
554                    let n_b = vertices[neighbors[j] as usize];
555                    let n_c = vertices[neighbors[k] as usize];
556                    let normal = cross4(n_a - v_0, n_b - v_0, n_c - v_0);
557                    let mag = normal.length();
558                    if mag < MIN_CROSS4_LENGTH {
559                        continue;
560                    }
561                    let n = normal / mag;
562                    let offset = v_0.dot(n);
563                    let on_plane: Vec<u32> = (0..vertices.len() as u32)
564                        .filter(|&p| (vertices[p as usize].dot(n) - offset).abs() < CELL_TOLERANCE)
565                        .collect();
566                    if on_plane.len() == cell_size {
567                        cells_set.insert(on_plane);
568                    }
569                }
570            }
571        }
572    }
573    cells_set.into_iter().collect()
574}
575
576fn cache_cells(
577    vertices: &'static [Vec4],
578    edges: &'static [[u32; 2]],
579    cell_size: usize,
580) -> &'static [&'static [u32]] {
581    let cells: Vec<&'static [u32]> = derive_cells(vertices, edges, cell_size)
582        .into_iter()
583        .map(|c| &*Box::leak(c.into_boxed_slice()))
584        .collect();
585    Box::leak(cells.into_boxed_slice())
586}
587
588static PENTATOPE_CELLS: LazyLock<&'static [&'static [u32]]> = LazyLock::new(|| {
589    cache_cells(
590        *PENTATOPE_VERTICES,
591        *PENTATOPE_EDGES,
592        cell_vertex_count(Polytope4::Pentatope),
593    )
594});
595static TESSERACT_CELLS: LazyLock<&'static [&'static [u32]]> = LazyLock::new(|| {
596    cache_cells(
597        *TESSERACT_VERTICES,
598        *TESSERACT_EDGES,
599        cell_vertex_count(Polytope4::Tesseract),
600    )
601});
602static CELL16_CELLS: LazyLock<&'static [&'static [u32]]> = LazyLock::new(|| {
603    cache_cells(
604        *CELL16_VERTICES,
605        *CELL16_EDGES,
606        cell_vertex_count(Polytope4::Cell16),
607    )
608});
609static CELL24_CELLS: LazyLock<&'static [&'static [u32]]> = LazyLock::new(|| {
610    cache_cells(
611        *CELL24_VERTICES,
612        *CELL24_EDGES,
613        cell_vertex_count(Polytope4::Cell24),
614    )
615});
616static CELL120_CELLS: LazyLock<&'static [&'static [u32]]> = LazyLock::new(|| {
617    cache_cells(
618        *CELL120_VERTICES,
619        *CELL120_EDGES,
620        cell_vertex_count(Polytope4::Cell120),
621    )
622});
623static CELL600_CELLS: LazyLock<&'static [&'static [u32]]> = LazyLock::new(|| {
624    cache_cells(
625        *CELL600_VERTICES,
626        *CELL600_EDGES,
627        cell_vertex_count(Polytope4::Cell600),
628    )
629});
630
631static PENTATOPE_TOPOLOGY: LazyLock<Polytope4Topology> = LazyLock::new(|| Polytope4Topology {
632    vertices: *PENTATOPE_VERTICES,
633    edges: *PENTATOPE_EDGES,
634    cells: *PENTATOPE_CELLS,
635});
636static TESSERACT_TOPOLOGY: LazyLock<Polytope4Topology> = LazyLock::new(|| Polytope4Topology {
637    vertices: *TESSERACT_VERTICES,
638    edges: *TESSERACT_EDGES,
639    cells: *TESSERACT_CELLS,
640});
641static CELL16_TOPOLOGY: LazyLock<Polytope4Topology> = LazyLock::new(|| Polytope4Topology {
642    vertices: *CELL16_VERTICES,
643    edges: *CELL16_EDGES,
644    cells: *CELL16_CELLS,
645});
646static CELL24_TOPOLOGY: LazyLock<Polytope4Topology> = LazyLock::new(|| Polytope4Topology {
647    vertices: *CELL24_VERTICES,
648    edges: *CELL24_EDGES,
649    cells: *CELL24_CELLS,
650});
651static CELL120_TOPOLOGY: LazyLock<Polytope4Topology> = LazyLock::new(|| Polytope4Topology {
652    vertices: *CELL120_VERTICES,
653    edges: *CELL120_EDGES,
654    cells: *CELL120_CELLS,
655});
656static CELL600_TOPOLOGY: LazyLock<Polytope4Topology> = LazyLock::new(|| Polytope4Topology {
657    vertices: *CELL600_VERTICES,
658    edges: *CELL600_EDGES,
659    cells: *CELL600_CELLS,
660});
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    #[test]
667    fn vertex_counts_match_f_vector() {
668        assert_eq!(Polytope4::Pentatope.vertex_count(), 5);
669        assert_eq!(Polytope4::Tesseract.vertex_count(), 16);
670        assert_eq!(Polytope4::Cell16.vertex_count(), 8);
671        assert_eq!(Polytope4::Cell24.vertex_count(), 24);
672        assert_eq!(Polytope4::Cell120.vertex_count(), 600);
673        assert_eq!(Polytope4::Cell600.vertex_count(), 120);
674    }
675
676    #[test]
677    fn vertices_on_unit_circumradius() {
678        for p in Polytope4::ALL {
679            for v in p.topology().vertices {
680                let r = v.length();
681                assert!(
682                    (r - 1.0).abs() < 1e-5,
683                    "{p:?} vertex {v:?} has |v| = {r}, expected 1.0"
684                );
685            }
686        }
687    }
688
689    #[test]
690    fn edge_counts_match_f_vector() {
691        assert_eq!(Polytope4::Pentatope.edge_count(), 10);
692        assert_eq!(Polytope4::Tesseract.edge_count(), 32);
693        assert_eq!(Polytope4::Cell16.edge_count(), 24);
694        assert_eq!(Polytope4::Cell24.edge_count(), 96);
695        assert_eq!(Polytope4::Cell120.edge_count(), 1200);
696        assert_eq!(Polytope4::Cell600.edge_count(), 720);
697    }
698
699    #[test]
700    fn edge_pairs_in_min_max_order() {
701        for p in Polytope4::ALL {
702            for &[i, j] in p.topology().edges {
703                assert!(i < j, "{p:?} edge ({i}, {j}) not in (min, max) order");
704            }
705        }
706    }
707
708    #[test]
709    fn edges_are_unique() {
710        for p in Polytope4::ALL {
711            let edges = p.topology().edges;
712            let mut seen = std::collections::HashSet::new();
713            for &[i, j] in edges {
714                let key = (i.min(j), i.max(j));
715                assert!(seen.insert(key), "{p:?} edge ({i}, {j}) duplicated");
716            }
717        }
718    }
719
720    #[test]
721    fn canonical_edge_length_matches_empirical_min() {
722        for p in Polytope4::ALL {
723            let vs = p.topology().vertices;
724            let mut min_d = f32::INFINITY;
725            for i in 0..vs.len() {
726                for j in (i + 1)..vs.len() {
727                    let d = (vs[i] - vs[j]).length();
728                    if d < min_d {
729                        min_d = d;
730                    }
731                }
732            }
733            let expected = canonical_edge_length(p);
734            assert!(
735                (min_d - expected).abs() < EDGE_TOLERANCE,
736                "{p:?}: empirical min pairwise distance {min_d} != canonical {expected}"
737            );
738        }
739    }
740
741    #[test]
742    fn cell_counts_match_f_vector() {
743        assert_eq!(Polytope4::Pentatope.cell_count(), 5);
744        assert_eq!(Polytope4::Tesseract.cell_count(), 8);
745        assert_eq!(Polytope4::Cell16.cell_count(), 16);
746        assert_eq!(Polytope4::Cell24.cell_count(), 24);
747        assert_eq!(Polytope4::Cell120.cell_count(), 120);
748        assert_eq!(Polytope4::Cell600.cell_count(), 600);
749    }
750
751    #[test]
752    fn cells_lie_on_common_hyperplane() {
753        for p in Polytope4::ALL {
754            let topo = p.topology();
755            for (idx, cell) in topo.cells.iter().enumerate() {
756                let centroid: Vec4 = cell
757                    .iter()
758                    .map(|&i| topo.vertices[i as usize])
759                    .fold(Vec4::ZERO, |acc, v| acc + v)
760                    / cell.len() as f32;
761                assert!(
762                    centroid.length_squared() > 1e-12,
763                    "{p:?} cell {idx} has no normal"
764                );
765                let dots: Vec<f32> = cell
766                    .iter()
767                    .map(|&i| topo.vertices[i as usize].dot(centroid))
768                    .collect();
769                let lo = dots.iter().copied().fold(f32::INFINITY, f32::min);
770                let hi = dots.iter().copied().fold(f32::NEG_INFINITY, f32::max);
771                assert!(
772                    hi - lo < CELL_TOLERANCE,
773                    "{p:?} cell {idx} centroid-projection spread {} > {CELL_TOLERANCE}",
774                    hi - lo
775                );
776            }
777        }
778    }
779
780    #[test]
781    fn cell_internal_edge_counts_match_shape() {
782        let cases: &[(Polytope4, usize)] = &[
783            (Polytope4::Pentatope, 6),
784            (Polytope4::Tesseract, 12),
785            (Polytope4::Cell16, 6),
786            (Polytope4::Cell24, 12),
787            (Polytope4::Cell120, 30),
788            (Polytope4::Cell600, 6),
789        ];
790        for &(p, expected) in cases {
791            let topo = p.topology();
792            let edge_len = canonical_edge_length(p);
793            for (idx, cell) in topo.cells.iter().enumerate() {
794                let mut count = 0;
795                for i in 0..cell.len() {
796                    for j in (i + 1)..cell.len() {
797                        let a = topo.vertices[cell[i] as usize];
798                        let b = topo.vertices[cell[j] as usize];
799                        if ((a - b).length() - edge_len).abs() < EDGE_TOLERANCE {
800                            count += 1;
801                        }
802                    }
803                }
804                assert_eq!(
805                    count, expected,
806                    "{p:?} cell {idx} has {count} internal edges, expected {expected}"
807                );
808            }
809        }
810    }
811
812    #[test]
813    fn edge_cell_incidence_matches_regular_polytope() {
814        for (p, expected) in [
815            (Polytope4::Pentatope, 3),
816            (Polytope4::Tesseract, 3),
817            (Polytope4::Cell16, 4),
818            (Polytope4::Cell24, 3),
819            (Polytope4::Cell120, 3),
820            (Polytope4::Cell600, 5),
821        ] {
822            let topo = p.topology();
823            for &[i, j] in topo.edges {
824                let count = topo
825                    .cells
826                    .iter()
827                    .filter(|cell| cell.contains(&i) && cell.contains(&j))
828                    .count();
829                assert_eq!(count, expected, "{p:?} edge ({i}, {j}) cell incidence");
830            }
831        }
832    }
833
834    #[test]
835    fn cross4_is_orthogonal_to_inputs() {
836        let a = Vec4::new(1.0, 0.5, -0.3, 0.7);
837        let b = Vec4::new(-0.2, 1.0, 0.4, -0.1);
838        let c = Vec4::new(0.6, -0.8, 1.0, 0.3);
839        let n = cross4(a, b, c);
840        assert!(
841            n.length() > 0.1,
842            "cross4 of linearly-independent inputs is near-zero (|n| = {})",
843            n.length()
844        );
845        for (label, v) in [("a", a), ("b", b), ("c", c)] {
846            assert!(
847                n.dot(v).abs() < 1e-5,
848                "cross4 result not orthogonal to {label}: n·{label} = {}",
849                n.dot(v)
850            );
851        }
852    }
853
854    #[test]
855    fn cross4_zero_for_linearly_dependent_inputs() {
856        let a = Vec4::new(1.0, 0.0, 0.0, 0.0);
857        let b = Vec4::new(0.0, 1.0, 0.0, 0.0);
858        let c = a * 2.0 + b * 3.0;
859        let n = cross4(a, b, c);
860        assert!(
861            n.length() < 1e-5,
862            "cross4 of linearly-dependent inputs is not zero: {n:?} (|n| = {})",
863            n.length()
864        );
865    }
866
867    #[test]
868    fn visualizable_line_count_matches_edge_count() {
869        use crate::Visualizable;
870        for p in Polytope4::ALL {
871            let mesh = <Polytope4 as Visualizable<4>>::to_lines(&p)
872                .expect("polytopes always produce line meshes");
873            assert_eq!(
874                mesh.segments.len(),
875                p.edge_count(),
876                "{p:?} line mesh has {} segments, expected {}",
877                mesh.segments.len(),
878                p.edge_count()
879            );
880            assert_eq!(mesh.colors.len(), mesh.segments.len());
881            assert_eq!(mesh.widths.len(), mesh.segments.len());
882        }
883    }
884
885    #[test]
886    fn visualizable_line_endpoints_are_polytope_vertices() {
887        use crate::Visualizable;
888        let topo = Polytope4::Tesseract.topology();
889        let mesh = <Polytope4 as Visualizable<4>>::to_lines(&Polytope4::Tesseract).unwrap();
890        for (i, &[vi, vj]) in topo.edges.iter().enumerate() {
891            let (a, b) = mesh.segments[i];
892            assert_eq!(a, topo.vertices[vi as usize].to_array());
893            assert_eq!(b, topo.vertices[vj as usize].to_array());
894        }
895    }
896
897    #[test]
898    fn lines_colored_by_cell_uses_palette() {
899        let palette: &[[f32; 4]] = &[
900            [1.0, 0.0, 0.0, 1.0],
901            [0.0, 1.0, 0.0, 1.0],
902            [0.0, 0.0, 1.0, 1.0],
903        ];
904        let mesh = Polytope4::Tesseract.lines_colored_by_cell(palette);
905        assert_eq!(mesh.segments.len(), Polytope4::Tesseract.edge_count());
906        for (start_color, end_color) in &mesh.colors {
907            assert!(palette.contains(start_color));
908            assert!(palette.contains(end_color));
909        }
910    }
911
912    #[test]
913    fn pentatope_section_at_midpoint() {
914        let (tri, edges) =
915            polytope4_section_overlay(Polytope4::Pentatope, loam_math::WPlane::new(0.0));
916        assert_eq!(tri.indices.len(), 12, "expected 12 fan triangles");
917        assert_eq!(edges.segments.len(), 12, "expected 12 perimeter segments");
918
919        assert_eq!(tri.vertices.len(), 16);
920    }
921
922    #[test]
923    fn tesseract_section_at_midpoint_has_six_square_caps() {
924        let (tri, edges) =
925            polytope4_section_overlay(Polytope4::Tesseract, loam_math::WPlane::new(0.0));
926        assert_eq!(tri.indices.len(), 24, "6 cubical cells * 4 fan-triangles");
927        assert_eq!(edges.segments.len(), 24, "6 caps * 4 perimeter edges");
928
929        assert_eq!(tri.vertices.len(), 30);
930    }
931
932    #[test]
933    fn section_outside_polytope_is_empty() {
934        for polytope in Polytope4::ALL {
935            let (tri, edges) = polytope4_section_overlay(polytope, loam_math::WPlane::new(2.0));
936            assert!(
937                tri.indices.is_empty(),
938                "{polytope:?} above-vertex slice should yield no triangles"
939            );
940            assert!(
941                edges.segments.is_empty(),
942                "{polytope:?} above-vertex slice should yield no perimeter edges"
943            );
944        }
945    }
946
947    #[test]
948    fn vertex_on_slice_is_perturbed_not_nan() {
949        let (tri, edges) =
950            polytope4_section_overlay(Polytope4::Pentatope, loam_math::WPlane::new(-0.25));
951        assert!(!tri.indices.is_empty());
952        assert!(!edges.segments.is_empty());
953        for v in &tri.vertices {
954            for component in v {
955                assert!(component.is_finite(), "triangle vertex must be finite");
956            }
957        }
958        for (a, b) in &edges.segments {
959            for component in a.iter().chain(b.iter()) {
960                assert!(component.is_finite(), "edge vertex must be finite");
961            }
962        }
963    }
964
965    #[test]
966    fn perimeter_append_concatenates_instead_of_replacing() {
967        let slice = loam_math::WPlane::new(0.1);
968        let topo = Polytope4::Cell24.topology();
969        let mut scratch = SectionScratch::default();
970        let mut mesh = crate::LineMesh::<3>::default();
971        let append = |scratch: &mut SectionScratch, out: &mut crate::LineMesh<3>| {
972            polytope_section_perimeter_append(
973                topo.edges,
974                topo.cells,
975                topo.vertices,
976                slice,
977                scratch,
978                out,
979            );
980        };
981
982        append(&mut scratch, &mut mesh);
983        let single = mesh.segments.len();
984        assert!(single > 0, "the fixture emitted no perimeter");
985        append(&mut scratch, &mut mesh);
986
987        assert_eq!(mesh.segments.len(), 2 * single);
988        assert_eq!(mesh.segments[..single], mesh.segments[single..]);
989        assert_eq!(mesh.widths.len(), mesh.segments.len());
990        assert_eq!(mesh.colors.len(), mesh.segments.len());
991    }
992
993    #[test]
994    fn section_face_normals_point_outward_from_polytope_center() {
995        for polytope in Polytope4::ALL {
996            let center = Vec3::ZERO;
997            for &slice_w in &[-0.5_f32, -0.2, 0.0, 0.2, 0.5] {
998                let (mesh, _) =
999                    polytope4_section_overlay(polytope, loam_math::WPlane::new(slice_w));
1000                for &[a, b, c] in &mesh.indices {
1001                    let va = Vec3::from(mesh.vertices[a as usize]);
1002                    let vb = Vec3::from(mesh.vertices[b as usize]);
1003                    let vc = Vec3::from(mesh.vertices[c as usize]);
1004                    let n = (vb - va).cross(vc - va);
1005                    if n.length_squared() < 1e-10 {
1006                        continue;
1007                    }
1008                    let tri_centroid = (va + vb + vc) / 3.0;
1009                    let outward = tri_centroid - center;
1010                    if outward.length_squared() < 1e-10 {
1011                        continue;
1012                    }
1013                    assert!(
1014                        n.dot(outward) > 0.0,
1015                        "{polytope:?} at w={slice_w}: triangle ({va:?}, {vb:?}, {vc:?}) \
1016                         has inward-facing normal {n:?}"
1017                    );
1018                }
1019            }
1020        }
1021    }
1022
1023    #[test]
1024    fn section_under_random_rotors_stays_well_formed() {
1025        let mut state: u32 = 0x517_C0DE;
1026        let mut rand = || {
1027            state ^= state << 13;
1028            state ^= state >> 17;
1029            state ^= state << 5;
1030            (state as f32 / u32::MAX as f32) * 2.0 - 1.0
1031        };
1032        for polytope in Polytope4::ALL {
1033            let topo = polytope.topology();
1034            for _ in 0..16 {
1035                use loam_math::Bivector as _;
1036                let rotor =
1037                    loam_math::Bivector4::new(rand(), rand(), rand(), rand(), rand(), rand())
1038                        .exp()
1039                        .normalize();
1040                let rotated: Vec<Vec4> = {
1041                    use loam_math::Rotor as _;
1042                    topo.vertices.iter().map(|v| rotor.apply(*v)).collect()
1043                };
1044
1045                let slice_w = rand() * 0.8;
1046                let slice = loam_math::WPlane::new(slice_w);
1047                let (tri, perim) =
1048                    polytope_section_overlay_with_vertices(topo.edges, topo.cells, &rotated, slice);
1049
1050                for v in &tri.vertices {
1051                    for c in v {
1052                        assert!(c.is_finite(), "{polytope:?} tri vertex non-finite: {v:?}");
1053                    }
1054                }
1055                for (a, b) in &perim.segments {
1056                    for c in a.iter().chain(b.iter()) {
1057                        assert!(c.is_finite(), "{polytope:?} perim endpoint non-finite");
1058                    }
1059                }
1060                for &[i0, i1, i2] in &tri.indices {
1061                    let n = tri.vertices.len() as u32;
1062                    assert!(
1063                        i0 < n && i1 < n && i2 < n,
1064                        "{polytope:?} index out of bounds"
1065                    );
1066                }
1067                let (w_min, w_max) = rotated
1068                    .iter()
1069                    .fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), v| {
1070                        (lo.min(v.w), hi.max(v.w))
1071                    });
1072                if slice_w > w_min + 0.05 && slice_w < w_max - 0.05 {
1073                    assert!(
1074                        !perim.segments.is_empty(),
1075                        "{polytope:?} slice w={slice_w} inside [{w_min}, {w_max}] but produced empty section"
1076                    );
1077                }
1078            }
1079        }
1080    }
1081}