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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user