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>
This commit is contained in:
sergio
2026-05-20 15:09:22 +00:00
parent 0042fe3f1f
commit dc8554d123
6 changed files with 542 additions and 23 deletions
+105
View File
@@ -0,0 +1,105 @@
//! Layout de árbol por ancho de subárbol.
//!
//! Post-order: las hojas se ubican en columnas consecutivas; cada nodo
//! interno se centra sobre sus hijos. `y` es la profundidad. Soporta
//! bosque (múltiples raíces). Ciclos en los punteros `parent` se ignoran
//! con gracia (esos nodos quedan en el origen).
/// Calcula `(x, y)` por nodo. `parent[i] = None` marca una raíz.
pub fn tree_layout(parent: &[Option<usize>], x_gap: f32, y_gap: f32) -> Vec<(f32, f32)> {
let n = parent.len();
let mut pos = vec![(0.0f32, 0.0f32); n];
if n == 0 {
return pos;
}
let mut children: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut roots: Vec<usize> = Vec::new();
for i in 0..n {
match parent[i] {
Some(p) if p < n && p != i => children[p].push(i),
_ => roots.push(i),
}
}
// Profundidad por BFS desde las raíces.
let mut depth = vec![0usize; n];
let mut visited = vec![false; n];
let mut queue: Vec<usize> = roots.clone();
for &r in &roots {
visited[r] = true;
}
let mut head = 0;
while head < queue.len() {
let u = queue[head];
head += 1;
for &c in &children[u] {
if !visited[c] {
visited[c] = true;
depth[c] = depth[u] + 1;
queue.push(c);
}
}
}
// Asignación de `x` por post-order iterativo, raíz por raíz.
let mut next_leaf = 0.0f32;
for &root in &roots {
let mut stack: Vec<(usize, usize)> = vec![(root, 0)];
while let Some(&mut (u, ref mut ci)) = stack.last_mut() {
if *ci < children[u].len() {
let c = children[u][*ci];
*ci += 1;
stack.push((c, 0));
} else {
if children[u].is_empty() {
pos[u].0 = next_leaf;
next_leaf += x_gap;
} else {
let sum: f32 = children[u].iter().map(|&c| pos[c].0).sum();
pos[u].0 = sum / children[u].len() as f32;
}
pos[u].1 = depth[u] as f32 * y_gap;
stack.pop();
}
}
}
pos
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_node_at_origin() {
let pos = tree_layout(&[None], 10.0, 20.0);
assert_eq!(pos, vec![(0.0, 0.0)]);
}
#[test]
fn parent_centered_over_two_children() {
// 0 raíz; 1 y 2 hijos.
let pos = tree_layout(&[None, Some(0), Some(0)], 10.0, 20.0);
assert_eq!(pos[1], (0.0, 20.0));
assert_eq!(pos[2], (10.0, 20.0));
// padre centrado en x = (0+10)/2 = 5, depth 0.
assert_eq!(pos[0], (5.0, 0.0));
}
#[test]
fn depth_increases_down_the_tree() {
// cadena 0 → 1 → 2
let pos = tree_layout(&[None, Some(0), Some(1)], 10.0, 20.0);
assert_eq!(pos[0].1, 0.0);
assert_eq!(pos[1].1, 20.0);
assert_eq!(pos[2].1, 40.0);
}
#[test]
fn cycle_in_parents_does_not_hang() {
// 0 ↔ 1 sin raíz: no debe colgar.
let pos = tree_layout(&[Some(1), Some(0)], 10.0, 20.0);
assert_eq!(pos.len(), 2);
}
}