diff --git a/cpp/apps/primitives_gallery/demos_graph.cpp b/cpp/apps/primitives_gallery/demos_graph.cpp index 196e4ca6..6285605c 100644 --- a/cpp/apps/primitives_gallery/demos_graph.cpp +++ b/cpp/apps/primitives_gallery/demos_graph.cpp @@ -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& nodes_out, std::vector& 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(std::max(N, 1))); + const float cluster_r = 12.0f * scale; + const float scatter = 4.0f * scale; + std::vector 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(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(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(rnd() * size); add_edge(static_cast(i), static_cast(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(rnd() * N); uint32_t b = static_cast(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 s_nodes; static std::vector 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(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"); diff --git a/cpp/functions/viz/graph_force_layout.cpp b/cpp/functions/viz/graph_force_layout.cpp index f7a99289..586452d9 100644 --- a/cpp/functions/viz/graph_force_layout.cpp +++ b/cpp/functions/viz/graph_force_layout.cpp @@ -4,6 +4,7 @@ #include #include #include +#include // --------------------------------------------------------------------------- // 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 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); diff --git a/cpp/functions/viz/graph_viewport.cpp b/cpp/functions/viz/graph_viewport.cpp index b754f55d..713c1b2c 100644 --- a/cpp/functions/viz/graph_viewport.cpp +++ b/cpp/functions/viz/graph_viewport.cpp @@ -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; } } diff --git a/cpp/tests/golden/graph_viewport.png b/cpp/tests/golden/graph_viewport.png index fa8161a2..4d523f6f 100644 Binary files a/cpp/tests/golden/graph_viewport.png and b/cpp/tests/golden/graph_viewport.png differ