Files
brahman/crates/modules/pineal/mesh/src/spatial_hash.rs
T
sergio dc8554d123 feat(pineal): cierra stub mesh — viz de grafos (núcleo)
Fase F: sexto stub de pineal cerrado (6/6).

mesh resultó ser un módulo de viz de grafos, no un triangle-mesh.
Núcleo implementado:
- buffers — NodeBuffer (stride 3: x,y,radius) + EdgeBuffer (stride 2),
  Vec planos contiguos, raw() para subir a GPU.
- spatial_hash — uniform grid; rebuild + query (nodo bajo un punto,
  revisa celda + 8 vecinas).
- force — layout force-directed Fruchterman-Reingold naïve O(n²):
  repulsión todo-par + atracción por arista + cooling. Jitter
  determinista para nodos coincidentes.
- tree — layout de árbol por ancho de subárbol (post-order, padres
  centrados sobre hijos), soporta bosque, ciclos sin colgar.
- camera — pan/zoom con zoom anclado al cursor (anchor-preserving).

13 tests verdes. cargo check --workspace verde.

Pendiente (follow-up): hierarchical (Sugiyama) + Barnes-Hut para
escalar el force-directed a grafos masivos.

Pineal: 6/6 stubs cerrados.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:09:22 +00:00

81 lines
2.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Uniform grid para hit-test de nodos móviles.
use crate::buffers::NodeBuffer;
use std::collections::HashMap;
/// Grid de celdas cuadradas. `rebuild` lo repuebla; `query` busca el
/// nodo cuyo radio cubre un punto.
pub struct SpatialHash {
cell: f32,
map: HashMap<(i32, i32), Vec<usize>>,
}
impl SpatialHash {
/// `cell_size` conviene ~2× el radio típico de nodo.
pub fn new(cell_size: f32) -> Self {
Self { cell: cell_size.max(1.0), map: HashMap::new() }
}
fn cell_of(&self, x: f32, y: f32) -> (i32, i32) {
((x / self.cell).floor() as i32, (y / self.cell).floor() as i32)
}
/// Repuebla el grid con las posiciones actuales de los nodos.
pub fn rebuild(&mut self, nodes: &NodeBuffer) {
self.map.clear();
for i in 0..nodes.len() {
let (x, y) = nodes.pos(i);
self.map.entry(self.cell_of(x, y)).or_default().push(i);
}
}
/// Devuelve el nodo más cercano a `(x,y)` cuyo radio lo cubre, o
/// `None`. Revisa la celda del punto y sus 8 vecinas.
pub fn query(&self, nodes: &NodeBuffer, x: f32, y: f32) -> Option<usize> {
let (cx, cy) = self.cell_of(x, y);
let mut best: Option<(usize, f32)> = None;
for dy in -1..=1 {
for dx in -1..=1 {
if let Some(bucket) = self.map.get(&(cx + dx, cy + dy)) {
for &i in bucket {
let (nx, ny) = nodes.pos(i);
let r = nodes.radius(i);
let d2 = (nx - x).powi(2) + (ny - y).powi(2);
if d2 <= r * r && best.map(|(_, bd)| d2 < bd).unwrap_or(true) {
best = Some((i, d2));
}
}
}
}
}
best.map(|(i, _)| i)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_hits_node_under_point() {
let mut nb = NodeBuffer::new();
nb.push(10.0, 10.0, 5.0);
nb.push(100.0, 100.0, 8.0);
let mut sh = SpatialHash::new(20.0);
sh.rebuild(&nb);
assert_eq!(sh.query(&nb, 12.0, 11.0), Some(0));
assert_eq!(sh.query(&nb, 103.0, 98.0), Some(1));
}
#[test]
fn query_misses_empty_space() {
let mut nb = NodeBuffer::new();
nb.push(10.0, 10.0, 5.0);
let mut sh = SpatialHash::new(20.0);
sh.rebuild(&nb);
assert_eq!(sh.query(&nb, 500.0, 500.0), None);
// fuera del radio pero misma celda
assert_eq!(sh.query(&nb, 18.0, 18.0), None);
}
}