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#[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 pub vertices: &'static [Vec4],
26 pub edges: &'static [[u32; 2]],
28 pub cells: &'static [&'static [u32]],
30}
31
32impl Polytope4 {
33 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 pub fn edge_count(self) -> usize {
60 self.topology().edges.len()
61 }
62
63 pub fn cell_count(self) -> usize {
65 self.topology().cells.len()
66 }
67
68 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 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 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
303fn 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
451fn 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}