#![allow(dead_code)]
use std::collections::HashMap;
use std::f32::consts::TAU;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Edge(pub u32, pub u32);
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CircularEdgeResult {
pub loops: Vec<Vec<u32>>,
pub open_chains: Vec<Vec<u32>>,
}
#[allow(dead_code)]
pub fn make_edge(a: u32, b: u32) -> Edge {
if a <= b {
Edge(a, b)
} else {
Edge(b, a)
}
}
#[allow(dead_code)]
pub fn build_adjacency(indices: &[u32]) -> HashMap<u32, Vec<u32>> {
let mut adj: HashMap<u32, Vec<u32>> = HashMap::new();
let tri_count = indices.len() / 3;
#[allow(clippy::needless_range_loop)]
for t in 0..tri_count {
let vs = [indices[t * 3], indices[t * 3 + 1], indices[t * 3 + 2]];
for i in 0..3 {
let a = vs[i];
let b = vs[(i + 1) % 3];
adj.entry(a).or_default().push(b);
adj.entry(b).or_default().push(a);
}
}
for v in adj.values_mut() {
v.sort_unstable();
v.dedup();
}
adj
}
#[allow(dead_code)]
pub fn detect_circular_edges(indices: &[u32]) -> CircularEdgeResult {
let mut edge_count: HashMap<Edge, usize> = HashMap::new();
let tri_count = indices.len() / 3;
#[allow(clippy::needless_range_loop)]
for t in 0..tri_count {
let vs = [indices[t * 3], indices[t * 3 + 1], indices[t * 3 + 2]];
for i in 0..3 {
let e = make_edge(vs[i], vs[(i + 1) % 3]);
*edge_count.entry(e).or_default() += 1;
}
}
let boundary: Vec<Edge> = edge_count
.into_iter()
.filter(|&(_, c)| c == 1)
.map(|(e, _)| e)
.collect();
let mut adj: HashMap<u32, Vec<u32>> = HashMap::new();
for e in &boundary {
adj.entry(e.0).or_default().push(e.1);
adj.entry(e.1).or_default().push(e.0);
}
let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut loops = Vec::new();
let mut open_chains = Vec::new();
for &start in adj.keys() {
if visited.contains(&start) {
continue;
}
let mut chain = vec![start];
visited.insert(start);
let mut current = start;
loop {
let neighbors = adj.get(¤t).cloned().unwrap_or_default();
let next = neighbors.iter().find(|n| !visited.contains(n));
match next {
Some(&n) => {
chain.push(n);
visited.insert(n);
current = n;
}
None => break,
}
}
if let Some(neighbors) = adj.get(¤t) {
if neighbors.contains(&start) && chain.len() > 2 {
loops.push(chain);
} else {
open_chains.push(chain);
}
} else {
open_chains.push(chain);
}
}
CircularEdgeResult { loops, open_chains }
}
#[allow(dead_code)]
pub fn loop_count(r: &CircularEdgeResult) -> usize {
r.loops.len()
}
#[allow(dead_code)]
pub fn open_chain_count(r: &CircularEdgeResult) -> usize {
r.open_chains.len()
}
#[allow(dead_code)]
pub fn largest_loop(r: &CircularEdgeResult) -> usize {
r.loops.iter().map(|l| l.len()).max().unwrap_or(0)
}
#[allow(dead_code)]
pub fn circumference(radius: f32) -> f32 {
TAU * radius
}
#[allow(dead_code)]
pub fn circular_edge_to_json(r: &CircularEdgeResult) -> String {
format!(
"{{\"loops\":{},\"open_chains\":{}}}",
loop_count(r),
open_chain_count(r)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_edge() {
assert_eq!(make_edge(5, 3), Edge(3, 5));
}
#[test]
fn test_build_adjacency() {
let adj = build_adjacency(&[0, 1, 2]);
assert_eq!(adj.len(), 3);
}
#[test]
fn test_detect_single_tri() {
let r = detect_circular_edges(&[0, 1, 2]);
assert!(loop_count(&r) + open_chain_count(&r) > 0);
}
#[test]
fn test_detect_empty() {
let r = detect_circular_edges(&[]);
assert_eq!(loop_count(&r), 0);
assert_eq!(open_chain_count(&r), 0);
}
#[test]
fn test_two_tris_shared_edge() {
let r = detect_circular_edges(&[0, 1, 2, 1, 3, 2]);
assert!(loop_count(&r) + open_chain_count(&r) > 0);
}
#[test]
fn test_largest_loop() {
let r = detect_circular_edges(&[0, 1, 2]);
assert!(largest_loop(&r) <= 3);
}
#[test]
fn test_circumference() {
assert!((circumference(1.0) - TAU).abs() < 1e-5);
}
#[test]
fn test_to_json() {
let r = CircularEdgeResult {
loops: vec![],
open_chains: vec![],
};
let j = circular_edge_to_json(&r);
assert!(j.contains("\"loops\":0"));
}
#[test]
fn test_open_chain_count() {
let r = CircularEdgeResult {
loops: vec![],
open_chains: vec![vec![0, 1]],
};
assert_eq!(open_chain_count(&r), 1);
}
#[test]
fn test_loop_count() {
let r = CircularEdgeResult {
loops: vec![vec![0, 1, 2]],
open_chains: vec![],
};
assert_eq!(loop_count(&r), 1);
}
}