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:
@@ -14,16 +14,19 @@
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// Genera un grafo sintetico con N nodos en K clusters + aristas intra-cluster
|
||||
// y unas pocas inter-cluster. Pensado para demostrar el rendimiento del
|
||||
// pipeline graph_renderer + graph_force_layout + graph_viewport.
|
||||
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
|
||||
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
|
||||
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
|
||||
// mismo cuadrado de 200 px — a 1M nodos crece a ~6 km de radio en graph
|
||||
// space y los nodos pueden esparcirse libremente sin caja artificial.
|
||||
static void generate_synthetic_graph(int N, int K,
|
||||
int edges_per_node, int inter_pct,
|
||||
std::vector<GraphNode>& nodes_out,
|
||||
std::vector<GraphEdge>& edges_out) {
|
||||
nodes_out.clear();
|
||||
edges_out.clear();
|
||||
nodes_out.reserve(N);
|
||||
edges_out.reserve(N * 3);
|
||||
edges_out.reserve((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u);
|
||||
|
||||
unsigned seed = 0x1234abcd;
|
||||
auto rnd = [&]() {
|
||||
@@ -38,26 +41,31 @@ static void generate_synthetic_graph(int N, int K,
|
||||
};
|
||||
const int palette_n = sizeof(palette) / sizeof(palette[0]);
|
||||
|
||||
// Asignar cluster + posicion inicial cerca del centroide del cluster
|
||||
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
|
||||
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
|
||||
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
|
||||
const float scale = std::sqrt(static_cast<float>(std::max(N, 1)));
|
||||
const float cluster_r = 12.0f * scale;
|
||||
const float scatter = 4.0f * scale;
|
||||
|
||||
std::vector<float> cluster_cx(K), cluster_cy(K);
|
||||
for (int k = 0; k < K; k++) {
|
||||
float angle = 2.0f * 3.14159f * k / K;
|
||||
cluster_cx[k] = std::cos(angle) * 200.0f;
|
||||
cluster_cy[k] = std::sin(angle) * 200.0f;
|
||||
cluster_cx[k] = std::cos(angle) * cluster_r;
|
||||
cluster_cy[k] = std::sin(angle) * cluster_r;
|
||||
}
|
||||
|
||||
for (int i = 0; i < N; i++) {
|
||||
int k = i % K;
|
||||
GraphNode n = graph_node(static_cast<uint32_t>(i),
|
||||
cluster_cx[k] + (rnd() - 0.5f) * 80.0f,
|
||||
cluster_cy[k] + (rnd() - 0.5f) * 80.0f);
|
||||
cluster_cx[k] + (rnd() - 0.5f) * scatter,
|
||||
cluster_cy[k] + (rnd() - 0.5f) * scatter);
|
||||
n.size = 3.0f + rnd() * 2.0f;
|
||||
n.color = palette[k % palette_n];
|
||||
n.community = static_cast<uint32_t>(k);
|
||||
nodes_out.push_back(n);
|
||||
}
|
||||
|
||||
// Aristas: ~3 por nodo dentro del cluster, +5% inter-cluster.
|
||||
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
|
||||
if (a == b) return;
|
||||
edges_out.push_back(graph_edge(a, b, w));
|
||||
@@ -68,18 +76,17 @@ static void generate_synthetic_graph(int N, int K,
|
||||
int end = (k == K - 1) ? N : (base + per_cluster);
|
||||
int size = end - base;
|
||||
if (size < 2) continue;
|
||||
// Dentro del cluster
|
||||
for (int i = base; i < end; i++) {
|
||||
for (int e = 0; e < 3; e++) {
|
||||
for (int e = 0; e < edges_per_node; e++) {
|
||||
int j = base + static_cast<int>(rnd() * size);
|
||||
add_edge(static_cast<uint32_t>(i),
|
||||
static_cast<uint32_t>(j), 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Inter-cluster (5% de los nodos)
|
||||
int inter = N / 20;
|
||||
for (int e = 0; e < inter; e++) {
|
||||
// Inter-cluster: pct% del total de nodos
|
||||
long long inter = (long long)N * (long long)inter_pct / 100LL;
|
||||
for (long long e = 0; e < inter; e++) {
|
||||
uint32_t a = static_cast<uint32_t>(rnd() * N);
|
||||
uint32_t b = static_cast<uint32_t>(rnd() * N);
|
||||
add_edge(a, b, 0.3f);
|
||||
@@ -92,11 +99,13 @@ void demo_graph() {
|
||||
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
|
||||
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
|
||||
|
||||
static int s_n_nodes = 1000;
|
||||
static int s_n_clusters = 6;
|
||||
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
|
||||
static float s_attraction = 0.02f; // muelle entre nodos conectados
|
||||
static float s_gravity = 0.001f; // tiron hacia el centro
|
||||
static int s_n_nodes = 1000;
|
||||
static int s_n_clusters = 6;
|
||||
static int s_edges_per_n = 3; // aristas intra-cluster por nodo
|
||||
static int s_inter_pct = 5; // % de nodos para edges inter-cluster
|
||||
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
|
||||
static float s_attraction = 0.02f; // muelle entre nodos conectados
|
||||
static float s_gravity = 0.001f; // tiron hacia el centro
|
||||
static std::vector<GraphNode> s_nodes;
|
||||
static std::vector<GraphEdge> s_edges;
|
||||
static GraphData s_graph{};
|
||||
@@ -105,7 +114,9 @@ void demo_graph() {
|
||||
static bool s_needs_regen = true;
|
||||
|
||||
if (s_needs_regen) {
|
||||
generate_synthetic_graph(s_n_nodes, s_n_clusters, s_nodes, s_edges);
|
||||
generate_synthetic_graph(s_n_nodes, s_n_clusters,
|
||||
s_edges_per_n, s_inter_pct,
|
||||
s_nodes, s_edges);
|
||||
s_graph.nodes = s_nodes.data();
|
||||
s_graph.node_count = static_cast<int>(s_nodes.size());
|
||||
s_graph.edges = s_edges.data();
|
||||
@@ -120,11 +131,16 @@ void demo_graph() {
|
||||
section("Controls");
|
||||
{
|
||||
using namespace fn_ui;
|
||||
// Sliders en dos filas para que quepan sin scrollbar
|
||||
ImGui::PushItemWidth(180);
|
||||
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 20000);
|
||||
// Slider Nodes con escala logaritmica para que sea util tanto a 100
|
||||
// como a 1M sin tener que arrastrar 10000px.
|
||||
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 1000000, "%d",
|
||||
ImGuiSliderFlags_Logarithmic);
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
|
||||
ImGui::SliderInt("Edges/node", &s_edges_per_n, 1, 10);
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderInt("Inter %", &s_inter_pct, 0, 30, "%d%%");
|
||||
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -186,6 +186,11 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
|
||||
// -------------------------------------------------------------------
|
||||
// 5b. Zoom (scroll wheel)
|
||||
// -------------------------------------------------------------------
|
||||
// Consumimos el wheel cuando esta sobre el canvas para que la ventana
|
||||
// padre (BeginChild de la galeria, p.ej.) NO scrollee a la vez que
|
||||
// hacemos zoom — sin esto la pagina entera se mueve mientras el grafo
|
||||
// se acerca, sensacion incomoda. Tambien marcamos NoNav del item para
|
||||
// que ImGui no intente keyboard-scroll al estar enfocado.
|
||||
if (hovered) {
|
||||
float wheel = ImGui::GetIO().MouseWheel;
|
||||
if (wheel != 0.0f) {
|
||||
@@ -201,6 +206,10 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
|
||||
state.cam_y += rel_y / old_zoom - rel_y / new_zoom;
|
||||
state.zoom = new_zoom;
|
||||
interacted = true;
|
||||
|
||||
// Consumir el evento — ImGui::GetIO().MouseWheel a 0 evita que
|
||||
// el padre lo procese.
|
||||
ImGui::GetIO().MouseWheel = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 96 KiB |
Reference in New Issue
Block a user