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,111 @@
|
||||
//! Buffers planos de nodos y aristas — `Vec` contiguos con stride fijo.
|
||||
|
||||
/// Nodos: stride 3 = `[x, y, radius]` por nodo.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NodeBuffer {
|
||||
data: Vec<f32>,
|
||||
}
|
||||
|
||||
impl NodeBuffer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_capacity(n: usize) -> Self {
|
||||
Self { data: Vec::with_capacity(n * 3) }
|
||||
}
|
||||
|
||||
/// Agrega un nodo y devuelve su índice.
|
||||
pub fn push(&mut self, x: f32, y: f32, radius: f32) -> usize {
|
||||
let idx = self.len();
|
||||
self.data.extend_from_slice(&[x, y, radius]);
|
||||
idx
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len() / 3
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
|
||||
pub fn pos(&self, i: usize) -> (f32, f32) {
|
||||
(self.data[i * 3], self.data[i * 3 + 1])
|
||||
}
|
||||
|
||||
pub fn radius(&self, i: usize) -> f32 {
|
||||
self.data[i * 3 + 2]
|
||||
}
|
||||
|
||||
pub fn set_pos(&mut self, i: usize, x: f32, y: f32) {
|
||||
self.data[i * 3] = x;
|
||||
self.data[i * 3 + 1] = y;
|
||||
}
|
||||
|
||||
/// Acceso crudo al `Vec<f32>` interleaved — para subir como buffer GPU.
|
||||
pub fn raw(&self) -> &[f32] {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
/// Aristas: stride 2 = `[from, to]` (índices de nodo).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EdgeBuffer {
|
||||
data: Vec<u32>,
|
||||
}
|
||||
|
||||
impl EdgeBuffer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, from: usize, to: usize) {
|
||||
self.data.push(from as u32);
|
||||
self.data.push(to as u32);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len() / 2
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
|
||||
pub fn edge(&self, i: usize) -> (usize, usize) {
|
||||
(self.data[i * 2] as usize, self.data[i * 2 + 1] as usize)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
|
||||
(0..self.len()).map(move |i| self.edge(i))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn node_buffer_push_and_access() {
|
||||
let mut nb = NodeBuffer::new();
|
||||
let a = nb.push(1.0, 2.0, 5.0);
|
||||
let b = nb.push(3.0, 4.0, 6.0);
|
||||
assert_eq!((a, b), (0, 1));
|
||||
assert_eq!(nb.len(), 2);
|
||||
assert_eq!(nb.pos(1), (3.0, 4.0));
|
||||
assert_eq!(nb.radius(0), 5.0);
|
||||
nb.set_pos(0, 9.0, 9.0);
|
||||
assert_eq!(nb.pos(0), (9.0, 9.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_buffer_roundtrip() {
|
||||
let mut eb = EdgeBuffer::new();
|
||||
eb.push(0, 1);
|
||||
eb.push(1, 2);
|
||||
assert_eq!(eb.len(), 2);
|
||||
assert_eq!(eb.edge(1), (1, 2));
|
||||
assert_eq!(eb.iter().collect::<Vec<_>>(), vec![(0, 1), (1, 2)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Cámara 2D: pan + zoom con zoom anclado a un punto de pantalla.
|
||||
|
||||
/// Transformación world↔screen. `screen = (world - pan) * zoom`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Camera {
|
||||
pub pan: (f32, f32),
|
||||
pub zoom: f32,
|
||||
}
|
||||
|
||||
impl Default for Camera {
|
||||
fn default() -> Self {
|
||||
Self { pan: (0.0, 0.0), zoom: 1.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn world_to_screen(&self, w: (f32, f32)) -> (f32, f32) {
|
||||
((w.0 - self.pan.0) * self.zoom, (w.1 - self.pan.1) * self.zoom)
|
||||
}
|
||||
|
||||
pub fn screen_to_world(&self, s: (f32, f32)) -> (f32, f32) {
|
||||
(s.0 / self.zoom + self.pan.0, s.1 / self.zoom + self.pan.1)
|
||||
}
|
||||
|
||||
/// Desplaza la cámara `delta` pixels de pantalla.
|
||||
pub fn pan_by(&mut self, dx: f32, dy: f32) {
|
||||
self.pan.0 -= dx / self.zoom;
|
||||
self.pan.1 -= dy / self.zoom;
|
||||
}
|
||||
|
||||
/// Zoom multiplicando por `factor`, manteniendo fijo el punto de
|
||||
/// pantalla `anchor` (el world-point bajo el cursor no se mueve).
|
||||
pub fn zoom_at(&mut self, anchor: (f32, f32), factor: f32) {
|
||||
let before = self.screen_to_world(anchor);
|
||||
self.zoom = (self.zoom * factor).clamp(0.01, 1000.0);
|
||||
let after = self.screen_to_world(anchor);
|
||||
self.pan.0 += before.0 - after.0;
|
||||
self.pan.1 += before.1 - after.1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn close(a: (f32, f32), b: (f32, f32)) -> bool {
|
||||
(a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn world_screen_roundtrip() {
|
||||
let mut cam = Camera::new();
|
||||
cam.pan = (10.0, 20.0);
|
||||
cam.zoom = 2.0;
|
||||
let w = (33.0, 44.0);
|
||||
assert!(close(cam.screen_to_world(cam.world_to_screen(w)), w));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_at_keeps_anchor_world_point_fixed() {
|
||||
let mut cam = Camera::new();
|
||||
let anchor = (100.0, 80.0);
|
||||
let before = cam.screen_to_world(anchor);
|
||||
cam.zoom_at(anchor, 2.5);
|
||||
let after = cam.screen_to_world(anchor);
|
||||
assert!(close(before, after), "el punto bajo el cursor no se mueve");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Layout force-directed (Fruchterman-Reingold).
|
||||
//!
|
||||
//! Repulsión entre todo par de nodos + atracción a lo largo de las
|
||||
//! aristas, integrado con cooling. Implementación naïve O(n²); Barnes-Hut
|
||||
//! es la optimización de escala (millones de nodos) — pendiente.
|
||||
|
||||
use crate::buffers::{EdgeBuffer, NodeBuffer};
|
||||
|
||||
/// Parámetros de la simulación.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ForceParams {
|
||||
/// Distancia ideal entre nodos conectados.
|
||||
pub k: f32,
|
||||
/// Desplazamiento máximo inicial por paso (se enfría).
|
||||
pub temperature: f32,
|
||||
/// Factor de enfriamiento aplicado cada paso (`0 < cooling < 1`).
|
||||
pub cooling: f32,
|
||||
}
|
||||
|
||||
impl Default for ForceParams {
|
||||
fn default() -> Self {
|
||||
Self { k: 50.0, temperature: 50.0, cooling: 0.95 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Estado de una simulación force-directed.
|
||||
pub struct ForceLayout {
|
||||
params: ForceParams,
|
||||
temp: f32,
|
||||
}
|
||||
|
||||
impl ForceLayout {
|
||||
pub fn new(params: ForceParams) -> Self {
|
||||
let temp = params.temperature;
|
||||
Self { params, temp }
|
||||
}
|
||||
|
||||
/// Temperatura actual (baja con cada paso — útil para detectar fin).
|
||||
pub fn temperature(&self) -> f32 {
|
||||
self.temp
|
||||
}
|
||||
|
||||
/// Un paso de simulación. Muta las posiciones de `nodes`. Devuelve el
|
||||
/// desplazamiento total aplicado (converge hacia 0).
|
||||
pub fn step(&mut self, nodes: &mut NodeBuffer, edges: &EdgeBuffer) -> f32 {
|
||||
let n = nodes.len();
|
||||
if n == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let k = self.params.k.max(1e-3);
|
||||
let mut disp = vec![(0.0f32, 0.0f32); n];
|
||||
|
||||
// Repulsión: todo par. f_r = k² / d.
|
||||
for i in 0..n {
|
||||
let (xi, yi) = nodes.pos(i);
|
||||
for j in (i + 1)..n {
|
||||
let (xj, yj) = nodes.pos(j);
|
||||
let mut dx = xi - xj;
|
||||
let mut dy = yi - yj;
|
||||
let mut dist = (dx * dx + dy * dy).sqrt();
|
||||
if dist < 1e-3 {
|
||||
// Jitter determinista para despegar nodos coincidentes.
|
||||
dx = ((i as f32) - (j as f32)) * 0.01 + 0.01;
|
||||
dy = 0.01;
|
||||
dist = (dx * dx + dy * dy).sqrt();
|
||||
}
|
||||
let f = k * k / dist;
|
||||
let (ux, uy) = (dx / dist, dy / dist);
|
||||
disp[i].0 += ux * f;
|
||||
disp[i].1 += uy * f;
|
||||
disp[j].0 -= ux * f;
|
||||
disp[j].1 -= uy * f;
|
||||
}
|
||||
}
|
||||
|
||||
// Atracción: a lo largo de cada arista. f_a = d² / k.
|
||||
for (u, v) in edges.iter() {
|
||||
if u >= n || v >= n || u == v {
|
||||
continue;
|
||||
}
|
||||
let (xu, yu) = nodes.pos(u);
|
||||
let (xv, yv) = nodes.pos(v);
|
||||
let dx = xu - xv;
|
||||
let dy = yu - yv;
|
||||
let dist = (dx * dx + dy * dy).sqrt().max(1e-3);
|
||||
let f = dist * dist / k;
|
||||
let (ux, uy) = (dx / dist, dy / dist);
|
||||
disp[u].0 -= ux * f;
|
||||
disp[u].1 -= uy * f;
|
||||
disp[v].0 += ux * f;
|
||||
disp[v].1 += uy * f;
|
||||
}
|
||||
|
||||
// Integración con cap de temperatura.
|
||||
let mut total = 0.0f32;
|
||||
for i in 0..n {
|
||||
let (dx, dy) = disp[i];
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let capped = len.min(self.temp);
|
||||
let (mx, my) = (dx / len * capped, dy / len * capped);
|
||||
let (x, y) = nodes.pos(i);
|
||||
nodes.set_pos(i, x + mx, y + my);
|
||||
total += capped;
|
||||
}
|
||||
self.temp *= self.params.cooling;
|
||||
total
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn two_connected_nodes_settle_near_k() {
|
||||
let mut nb = NodeBuffer::new();
|
||||
nb.push(0.0, 0.0, 5.0);
|
||||
nb.push(500.0, 0.0, 5.0); // arrancan muy lejos
|
||||
let mut eb = EdgeBuffer::new();
|
||||
eb.push(0, 1);
|
||||
let mut fl = ForceLayout::new(ForceParams::default());
|
||||
for _ in 0..400 {
|
||||
fl.step(&mut nb, &eb);
|
||||
}
|
||||
let (x0, y0) = nb.pos(0);
|
||||
let (x1, y1) = nb.pos(1);
|
||||
let dist = ((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt();
|
||||
// No deberían quedar ni pegados ni a 500 de distancia.
|
||||
assert!(dist > 5.0 && dist < 300.0, "dist tras converger = {dist}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_nodes_do_not_nan() {
|
||||
let mut nb = NodeBuffer::new();
|
||||
nb.push(10.0, 10.0, 5.0);
|
||||
nb.push(10.0, 10.0, 5.0);
|
||||
let eb = EdgeBuffer::new();
|
||||
let mut fl = ForceLayout::new(ForceParams::default());
|
||||
fl.step(&mut nb, &eb);
|
||||
let (x, y) = nb.pos(0);
|
||||
assert!(x.is_finite() && y.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_graph_is_noop() {
|
||||
let mut nb = NodeBuffer::new();
|
||||
let eb = EdgeBuffer::new();
|
||||
let mut fl = ForceLayout::new(ForceParams::default());
|
||||
assert_eq!(fl.step(&mut nb, &eb), 0.0);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,25 @@
|
||||
//! `pineal-mesh` — visualización de grafos.
|
||||
//!
|
||||
//! Módulos:
|
||||
//! - **`node_buffer`** / **`edge_buffer`** — `Vec<f32>` planos con
|
||||
//! stride fijo (3 floats por nodo: `[x, y, radius]`).
|
||||
//! - **`spatial_hash`** — uniform grid para hit-test de nodos
|
||||
//! móviles (sección 5.1).
|
||||
//! - **`force_directed`** — layout con Barnes-Hut delegado a
|
||||
//! `pineal_core::barnes_hut` (cuando se implemente).
|
||||
//! - **`hierarchical`** — Sugiyama-lite, delegado a
|
||||
//! `pineal_core::sugiyama`.
|
||||
//! - **`tree`** — subtree-width layout, delegado a
|
||||
//! `pineal_core::tree_layout`.
|
||||
//! - **`camera`** — pan/zoom con anchor-preserving zoom de la
|
||||
//! sección 5.3.
|
||||
//! - **`element`** — `Element` GPUI.
|
||||
//! - [`buffers`] — `NodeBuffer` / `EdgeBuffer`: `Vec` planos con stride
|
||||
//! fijo (3 floats por nodo `[x,y,radius]`, 2 por arista).
|
||||
//! - [`spatial_hash`] — uniform grid para hit-test de nodos móviles.
|
||||
//! - [`force`] — layout force-directed (Fruchterman-Reingold naïve).
|
||||
//! - [`tree`] — layout de árbol por ancho de subárbol.
|
||||
//! - [`camera`] — pan/zoom con zoom anclado al cursor.
|
||||
//!
|
||||
//! Pendiente: `hierarchical` (Sugiyama, layered graph drawing) y la
|
||||
//! optimización Barnes-Hut del force-directed para grafos masivos.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod node_buffer {}
|
||||
pub mod edge_buffer {}
|
||||
pub mod spatial_hash {}
|
||||
pub mod force_directed {}
|
||||
pub mod hierarchical {}
|
||||
pub mod tree {}
|
||||
pub mod camera {}
|
||||
pub mod element {}
|
||||
pub mod buffers;
|
||||
pub mod camera;
|
||||
pub mod force;
|
||||
pub mod spatial_hash;
|
||||
pub mod tree;
|
||||
|
||||
pub use buffers::{EdgeBuffer, NodeBuffer};
|
||||
pub use camera::Camera;
|
||||
pub use force::{ForceLayout, ForceParams};
|
||||
pub use spatial_hash::SpatialHash;
|
||||
pub use tree::tree_layout;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -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