use std::collections::HashMap;
use nalgebra::Point2;
use super::quads::Quad;
use super::walk::{build_edge_index, connected_components};
use super::TopologicalParams;
#[derive(Clone, Debug)]
pub(crate) struct QuadFilterDecision {
pub(crate) quad: Quad,
pub(crate) illegal_vertices: Vec<usize>,
pub(crate) topology_pass: bool,
pub(crate) geometry_pass: bool,
pub(crate) max_opposing_edge_ratio: f32,
pub(crate) kept: bool,
}
fn edge_len(positions: &[Point2<f32>], u: usize, v: usize) -> f32 {
let pu = positions[u];
let pv = positions[v];
((pv.x - pu.x).powi(2) + (pv.y - pu.y).powi(2)).sqrt()
}
fn max_opposing_edge_ratio(quad: &Quad, positions: &[Point2<f32>]) -> f32 {
let v = quad.vertices;
let l01 = edge_len(positions, v[0], v[1]);
let l12 = edge_len(positions, v[1], v[2]);
let l23 = edge_len(positions, v[2], v[3]);
let l30 = edge_len(positions, v[3], v[0]);
let r1 = l01.max(l23) / l01.min(l23).max(1e-6);
let r2 = l12.max(l30) / l12.min(l30).max(1e-6);
r1.max(r2)
}
fn quad_degrees(quads: &[Quad]) -> HashMap<usize, u32> {
let mut degree: HashMap<usize, u32> = HashMap::new();
for q in quads {
for (u, v) in q.perimeter_edges() {
*degree.entry(u).or_default() += 1;
*degree.entry(v).or_default() += 1;
}
}
degree
}
fn evaluate_quad(
quad: Quad,
positions: &[Point2<f32>],
params: &TopologicalParams,
degree: &HashMap<usize, u32>,
) -> QuadFilterDecision {
let illegal_vertices: Vec<usize> = quad
.vertices
.iter()
.copied()
.filter(|v| degree.get(v).copied().unwrap_or(0) > 8)
.collect();
let topology_pass = illegal_vertices.len() < 2;
let max_opposing_edge_ratio = max_opposing_edge_ratio(&quad, positions);
let geometry_pass = max_opposing_edge_ratio <= params.edge_ratio_max;
let kept = topology_pass && geometry_pass;
QuadFilterDecision {
quad,
illegal_vertices,
topology_pass,
geometry_pass,
max_opposing_edge_ratio,
kept,
}
}
#[cfg(feature = "tracing")]
fn illegal_vertices_for_quad(quad: &Quad, degree: &HashMap<usize, u32>) -> Vec<usize> {
quad.vertices
.iter()
.copied()
.filter(|v| degree.get(v).copied().unwrap_or(0) > 8)
.collect()
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "debug",
skip_all,
fields(num_quads_in = quads.len()),
)
)]
pub(crate) fn filter_quad_decisions(
quads: &[Quad],
positions: &[Point2<f32>],
params: &TopologicalParams,
) -> Vec<QuadFilterDecision> {
let degree = quad_degrees(quads);
let mut decisions: Vec<QuadFilterDecision> = quads
.iter()
.copied()
.map(|q| evaluate_quad(q, positions, params, °ree))
.collect();
let candidate_quads: Vec<Quad> = decisions
.iter()
.filter(|d| d.kept)
.map(|d| d.quad)
.collect();
let after_cell_size = apply_per_component_cell_size_filter(candidate_quads, positions, params);
let surviving: std::collections::HashSet<[usize; 4]> =
after_cell_size.into_iter().map(|q| q.vertices).collect();
for d in decisions.iter_mut() {
if d.kept && !surviving.contains(&d.quad.vertices) {
d.kept = false;
}
}
decisions
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "debug",
skip_all,
fields(num_quads_in = quads.len()),
)
)]
#[cfg(not(feature = "tracing"))]
pub(crate) fn filter_quads(
quads: &[Quad],
positions: &[Point2<f32>],
params: &TopologicalParams,
) -> Vec<Quad> {
let degree = quad_degrees(quads);
let initial: Vec<Quad> = quads
.iter()
.copied()
.map(|q| evaluate_quad(q, positions, params, °ree))
.filter(|d| d.kept)
.map(|d| d.quad)
.collect();
apply_per_component_cell_size_filter(initial, positions, params)
}
#[cfg(feature = "tracing")]
pub(crate) fn filter_quads(
quads: &[Quad],
positions: &[Point2<f32>],
params: &TopologicalParams,
) -> Vec<Quad> {
let topology = {
let _span =
tracing::debug_span!("topological_quad_filter", num_quads_in = quads.len()).entered();
let degree = quad_degrees(quads);
quads
.iter()
.copied()
.map(|quad| {
let illegal_vertices = illegal_vertices_for_quad(&quad, °ree);
let topology_pass = illegal_vertices.len() < 2;
(quad, topology_pass)
})
.collect::<Vec<_>>()
};
let initial: Vec<Quad> = {
let _span =
tracing::debug_span!("geometry_quad_filter", num_quads_in = topology.len()).entered();
topology
.into_iter()
.filter_map(|(quad, topology_pass)| {
let max_opposing_edge_ratio = max_opposing_edge_ratio(&quad, positions);
let geometry_pass = max_opposing_edge_ratio <= params.edge_ratio_max;
(topology_pass && geometry_pass).then_some(quad)
})
.collect()
};
let _span =
tracing::debug_span!("cell_size_quad_filter", num_quads_in = initial.len()).entered();
apply_per_component_cell_size_filter(initial, positions, params)
}
#[inline]
fn quad_min_max_edge(quad: &Quad, positions: &[Point2<f32>]) -> (f32, f32) {
let mut lo = f32::INFINITY;
let mut hi = 0.0_f32;
for (u, v) in quad.perimeter_edges() {
let l = edge_len(positions, u, v);
if l < lo {
lo = l;
}
if l > hi {
hi = l;
}
}
(lo, hi)
}
fn apply_per_component_cell_size_filter(
quads: Vec<Quad>,
positions: &[Point2<f32>],
params: &TopologicalParams,
) -> Vec<Quad> {
if quads.is_empty() {
return quads;
}
if params.quad_edge_min_rel <= 0.0 && !params.quad_edge_max_rel.is_finite() {
return quads;
}
let edge_index = build_edge_index(&quads);
let (comp_of, n_comps) = connected_components(&quads, &edge_index);
let mut comp_edges: Vec<Vec<f32>> = vec![Vec::new(); n_comps as usize];
for (qi, q) in quads.iter().enumerate() {
let cid = comp_of[qi] as usize;
for (u, v) in q.perimeter_edges() {
comp_edges[cid].push(edge_len(positions, u, v));
}
}
let mut comp_median: Vec<Option<f32>> = Vec::with_capacity(comp_edges.len());
for v in comp_edges.iter_mut() {
if v.is_empty() {
comp_median.push(None);
continue;
}
v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
comp_median.push(Some(v[v.len() / 2]));
}
quads
.into_iter()
.enumerate()
.filter_map(|(qi, q)| {
let median = comp_median[comp_of[qi] as usize]?;
if median <= 0.0 {
return Some(q);
}
let (lo_e, hi_e) = quad_min_max_edge(&q, positions);
let lo_band = params.quad_edge_min_rel * median;
let hi_band = params.quad_edge_max_rel * median;
if lo_e < lo_band || hi_e > hi_band {
None
} else {
Some(q)
}
})
.collect()
}