feat(graph): wheel-zoom no scrollea, slider 1M nodos, edges/node configurable

Tres mejoras de UX/escala en el demo de grafos:

1. **Wheel zoom dentro del canvas no scrollea la pagina**
   En graph_viewport.cpp tras procesar MouseWheel para zoom hacemos
   io.MouseWheel = 0 — consume el evento para que el BeginChild padre
   (la galeria) no scrollee a la vez que el grafo se acerca. Antes
   sentia "doble accion" al rodar la rueda sobre el canvas.

2. **graph_force_layout: pool dinamico (soporta 1M nodos)**
   El array static QuadNode[1<<20] (~48MB siempre reservados, tope
   rigido en ~250k nodos por la fan-out) se reemplaza por
   std::vector<QuadNode>. graph_force_layout_step llama a
   quad_pool_reserve(5*N + 1024) ANTES de construir el arbol — asi las
   referencias QuadNode& que mantenemos vivas durante quad_subdivide
   no se invalidan por reallocaciones a mitad del build (resize solo
   ocurre en el reserve inicial). Memoria escala lineal con N: 1M
   nodos ≈ 240MB de pool, una vez por programa.

3. **Demo de grafo: sliders extendidos + cluster_r escala con sqrt(N)**
   - "Nodes" pasa de 100..20k a 100..1M con escala logaritmica
     (ImGuiSliderFlags_Logarithmic) para que el rango medio sea util.
   - Nuevos sliders "Edges/node" (1..10) e "Inter %" (0..30%) — antes
     hardcoded a 3 y 5%.
   - cluster_radius y scatter ahora escalan con sqrt(N): a 1k nodos
     ~370 px de radio, a 1M ~12000 px. Antes era constante a 200/40
     y los nodos quedaban empaquetados al subir N — visualmente "sin
     limite cuadrado", esparcidos sobre un area proporcional al grafo.
   - Golden de graph_viewport regenerado por la nueva fila de sliders.

Notas:
- A 1M nodos sin GPU compute esta limitado por el upload de aristas
  (vertex pulling con TBO llega en 0049d). Render mantenible hasta
  ~200-300k.
- En Linux/Windows ambos builds limpios. 27/27 tests verde.
This commit is contained in:
2026-04-29 21:53:33 +02:00
parent 9a4ff33e68
commit 0e6a013937
4 changed files with 67 additions and 26 deletions
+19 -3
View File
@@ -4,6 +4,7 @@
#include <cmath>
#include <cstdlib>
#include <algorithm>
#include <vector>
// ---------------------------------------------------------------------------
// Quadtree for Barnes-Hut approximation
@@ -18,12 +19,17 @@ struct QuadNode {
int body; // node index if leaf (-1 if internal)
};
static constexpr int MAX_QUAD_NODES = 1 << 20; // supports graphs up to ~1M nodes
static QuadNode quad_pool[MAX_QUAD_NODES];
// Pool dinamico — antes era un array static QuadNode[1<<20] (~48MB siempre
// reservados, tope rigido en ~250k nodos por la fan-out del subdivide).
// Ahora se redimensiona UNA VEZ al inicio de cada step segun el N del grafo
// (5*N + 1024 celdas como cota holgada para subdivisiones). Despues de eso
// quad_new solo incrementa quad_count, asi que las referencias QuadNode& que
// se mantienen vivas durante la construccion del arbol son seguras.
static std::vector<QuadNode> quad_pool;
static int quad_count = 0;
static int quad_new(float x0, float y0, float x1, float y1) {
if (quad_count >= MAX_QUAD_NODES) return -1;
if (quad_count >= (int)quad_pool.size()) return -1; // pool agotado
int idx = quad_count++;
QuadNode& q = quad_pool[idx];
q.cx = 0; q.cy = 0; q.mass = 0;
@@ -33,6 +39,13 @@ static int quad_new(float x0, float y0, float x1, float y1) {
return idx;
}
// Garantiza que el pool tenga al menos `need` celdas disponibles. Llamar
// ANTES de empezar a construir el arbol para evitar invalidar referencias
// QuadNode& durante quad_subdivide / quad_insert_body.
static void quad_pool_reserve(size_t need) {
if (quad_pool.size() < need) quad_pool.resize(need);
}
// Determine quadrant index for point (px,py) relative to cell midpoint.
// 0=NW, 1=NE, 2=SW, 3=SE
static int quad_child_idx(const QuadNode& q, float px, float py) {
@@ -235,6 +248,9 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
bx0 = cx - side * 0.5f; bx1 = cx + side * 0.5f;
by0 = cy - side * 0.5f; by1 = cy + side * 0.5f;
// Reserva el pool antes de construir: 5*N + 1024 es cota holgada
// para quadtrees de 2D (worst case ~4N celdas internas+hojas).
quad_pool_reserve((size_t)graph.node_count * 5 + 1024);
quad_count = 0;
g_nodes = graph.nodes;
int root = quad_new(bx0, by0, bx1, by1);