use glam::Vec2;
use crate::graph::{CityBlock, EdgeId, NodeId, RoadGraph};
pub fn extract_blocks(graph: &mut RoadGraph) {
graph.blocks.clear();
let mut visited_half_edges: std::collections::HashSet<(NodeId, NodeId)> =
std::collections::HashSet::new();
let directed: Vec<(NodeId, NodeId, EdgeId)> = graph
.edges
.iter()
.enumerate()
.filter(|(_, e)| e.active)
.flat_map(|(i, e)| {
let eid = i as EdgeId;
[(e.start, e.end, eid), (e.end, e.start, eid)]
})
.collect();
let node_count = graph.nodes.len();
let mut adjacency: Vec<Vec<(NodeId, EdgeId)>> = vec![Vec::new(); node_count];
for &(from, to, eid) in &directed {
adjacency[from as usize].push((to, eid));
}
for &(start_from, start_to, _) in &directed {
if visited_half_edges.contains(&(start_from, start_to)) {
continue;
}
let mut cycle: Vec<NodeId> = vec![start_from];
let mut prev = start_from;
let mut curr = start_to;
let max_cycle_len = node_count + 1;
let mut valid = true;
loop {
if visited_half_edges.contains(&(prev, curr)) {
valid = false;
break;
}
visited_half_edges.insert((prev, curr));
cycle.push(curr);
if curr == start_from {
break;
}
if cycle.len() > max_cycle_len {
valid = false;
break;
}
let neighbours = &adjacency[curr as usize];
if neighbours.is_empty() {
valid = false;
break;
}
let incoming_dir =
graph.nodes[prev as usize].position - graph.nodes[curr as usize].position;
let incoming_angle = incoming_dir.y.atan2(incoming_dir.x);
let next = pick_next_face_edge(neighbours, graph, curr, prev, incoming_angle);
match next {
Some(n) => {
prev = curr;
curr = n;
}
None => {
valid = false;
break;
}
}
}
if valid && cycle.len() >= 4 {
cycle.pop();
strip_antennas(&mut cycle);
if cycle.len() >= 3 && signed_area(&cycle, graph) < 0.0 {
graph.blocks.push(CityBlock { perimeter: cycle });
}
}
}
}
fn pick_next_face_edge(
neighbours: &[(NodeId, EdgeId)],
graph: &RoadGraph,
current: NodeId,
prev: NodeId,
incoming_angle: f32,
) -> Option<NodeId> {
let origin = graph.nodes[current as usize].position;
let mut best: Option<NodeId> = None;
let mut best_delta = f32::MAX;
let has_alternatives = neighbours.len() > 1;
for &(to, _) in neighbours {
if to == prev && has_alternatives {
continue;
}
let d = graph.nodes[to as usize].position - origin;
let out_angle = d.y.atan2(d.x);
let mut delta = (out_angle - incoming_angle).rem_euclid(std::f32::consts::TAU);
if delta < 1e-5 {
delta = std::f32::consts::TAU;
}
if delta < best_delta {
best_delta = delta;
best = Some(to);
}
}
best
}
pub(crate) fn signed_area(nodes: &[NodeId], graph: &RoadGraph) -> f32 {
let n = nodes.len();
if n < 3 {
return 0.0;
}
let origin = graph.nodes[nodes[0] as usize].position;
let mut area = 0.0_f32;
for i in 0..n {
let a = graph.nodes[nodes[i] as usize].position - origin;
let b = graph.nodes[nodes[(i + 1) % n] as usize].position - origin;
area += a.x * b.y - b.x * a.y;
}
area * 0.5
}
fn strip_antennas(cycle: &mut Vec<NodeId>) {
loop {
if cycle.len() < 3 {
break;
}
let mut found = false;
let n = cycle.len();
for i in 0..n {
let prev = if i == 0 { n - 1 } else { i - 1 };
let next = (i + 1) % n;
if cycle[prev] == cycle[next] {
if next > i {
cycle.remove(next);
cycle.remove(i);
} else {
cycle.remove(i);
cycle.remove(0);
}
found = true;
break;
}
}
if !found {
break;
}
}
}
pub fn block_centroid(block: &CityBlock, graph: &RoadGraph) -> Vec2 {
let n = block.perimeter.len();
if n == 0 {
return Vec2::ZERO;
}
if n < 3 {
let sum: Vec2 = block
.perimeter
.iter()
.map(|&nid| graph.nodes[nid as usize].position)
.sum();
return sum / n as f32;
}
let origin = graph.nodes[block.perimeter[0] as usize].position;
let mut cx = 0.0_f32;
let mut cy = 0.0_f32;
let mut signed_area_2 = 0.0_f32;
for i in 0..n {
let a = graph.nodes[block.perimeter[i] as usize].position - origin;
let b = graph.nodes[block.perimeter[(i + 1) % n] as usize].position - origin;
let cross = a.x * b.y - b.x * a.y;
cx += (a.x + b.x) * cross;
cy += (a.y + b.y) * cross;
signed_area_2 += cross;
}
if signed_area_2.abs() < 1e-8 {
let sum: Vec2 = block
.perimeter
.iter()
.map(|&nid| graph.nodes[nid as usize].position)
.sum();
return sum / n as f32;
}
let inv = 1.0 / (3.0 * signed_area_2);
Vec2::new(cx * inv, cy * inv) + origin
}