#include "demos.h" #include "demo.h" #include "viz/graph_types.h" #include "viz/graph_viewport.h" #include "viz/graph_force_layout.h" #include "viz/graph_force_layout_gpu.h" #include "core/button.h" #include "core/tokens.h" #include #include #include #include namespace gallery { // Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la // tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo // queda demostrado tal cual lo van a usar las apps reales (osint_graph, // fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id. static const uint32_t k_demo_palette[] = { 0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u, 0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u, }; static constexpr int k_demo_palette_n = sizeof(k_demo_palette) / sizeof(k_demo_palette[0]); // Tabla compartida entre regeneraciones — las apariencias no cambian aunque // el usuario regenere el grafo, asi que vive como `static`. static EntityType s_demo_entity_types[k_demo_palette_n]; static RelationType s_demo_relation_types[1]; static bool s_demo_types_initialized = false; static void init_demo_types() { if (s_demo_types_initialized) return; for (int k = 0; k < k_demo_palette_n; ++k) { s_demo_entity_types[k] = entity_type(k_demo_palette[k], SHAPE_CIRCLE, 4.0f, "cluster"); } s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default"); s_demo_types_initialized = true; } // 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((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u); unsigned seed = 0x1234abcd; auto rnd = [&]() { seed = seed * 1664525u + 1013904223u; return static_cast((seed >> 8) & 0xffffff) / 16777216.0f; }; // 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) * cluster_r; cluster_cy[k] = std::sin(angle) * cluster_r; } for (int i = 0; i < N; i++) { int k = i % K; // type_id mapea al EntityType (k % k_demo_palette_n) que define // color y shape. size_override = 3..5 px para conservar la // variacion sutil del demo v1 — apariencia visual identica. uint16_t tid = static_cast(k % k_demo_palette_n); GraphNode n = graph_node( cluster_cx[k] + (rnd() - 0.5f) * scatter, cluster_cy[k] + (rnd() - 0.5f) * scatter, tid); n.size_override = 3.0f + rnd() * 2.0f; n.user_data = static_cast(i); nodes_out.push_back(n); } auto add_edge = [&](uint32_t a, uint32_t b, float w) { if (a == b) return; edges_out.push_back(graph_edge(a, b, w)); }; int per_cluster = N / K; for (int k = 0; k < K; k++) { int base = k * per_cluster; int end = (k == K - 1) ? N : (base + per_cluster); int size = end - base; if (size < 2) continue; for (int i = base; i < end; i++) { 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: 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); } } void demo_graph() { demo_header("graph_viewport", "v1.0.0", "Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) " "+ 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 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{}; static GraphViewportState s_state; static bool s_initialized = false; static bool s_needs_regen = true; // GPU layout (issue 0049h): toggle CPU/GPU. ctx se crea perezosamente al // primer frame en GPU mode; max_nodes/max_edges se dimensionan al maximo // que ofrece el slider (1M nodos x 10 edges/nodo = 10M edges) — los SSBOs // ocupan ~80 MB en ese tope, suficientemente barato para no // recrear el ctx cada Regenerate. Si compute no esta disponible, el // toggle queda deshabilitado. static bool s_use_gpu = false; static ForceLayoutGPU* s_gpu_ctx = nullptr; static bool s_gpu_dirty = true; // re-upload tras regen / cambio if (s_needs_regen) { init_demo_types(); 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.node_capacity = static_cast(s_nodes.capacity()); s_graph.edges = s_edges.data(); s_graph.edge_count = static_cast(s_edges.size()); s_graph.edge_capacity = static_cast(s_edges.capacity()); s_graph.types = s_demo_entity_types; s_graph.type_count = k_demo_palette_n; s_graph.rel_types = s_demo_relation_types; s_graph.rel_type_count = 1; s_graph.update_bounds(); s_state.layout_running = true; s_state.layout_energy = 0.0f; s_needs_regen = false; s_initialized = true; s_gpu_dirty = true; } section("Controls"); { using namespace fn_ui; ImGui::PushItemWidth(180); // 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"); ImGui::SameLine(); ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f"); ImGui::PopItemWidth(); if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true; ImGui::SameLine(); if (button(s_state.layout_running ? "Pause layout" : "Resume layout", ButtonVariant::Secondary)) { s_state.layout_running = !s_state.layout_running; } ImGui::SameLine(); if (button("Fit view", ButtonVariant::Subtle)) { graph_viewport_fit(s_graph, s_state); } ImGui::SameLine(); // Toggle GPU layout. Si compute no esta disponible (Mesa software o // driver < 4.3), deshabilitamos visualmente el checkbox. bool prev_gpu = s_use_gpu; if (s_gpu_ctx == nullptr && s_use_gpu == false) { // primera oportunidad: intentar crear el ctx para detectar soporte. // Lazy init solo si el usuario lo activa. } ImGui::Checkbox("GPU layout", &s_use_gpu); if (s_use_gpu != prev_gpu) { s_gpu_dirty = true; // re-upload al cambiar de modo } } section("Stats"); { // Una sola linea fija — sin secciones condicionales que cambien la // altura del panel (eso provocaba que el viewport saltara al hacer // hover/select). char hover_buf[32]; char sel_buf[32]; if (s_state.hovered_node >= 0) { std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u", s_state.hovered_node, (unsigned)s_nodes[s_state.hovered_node].type_id); } else { std::snprintf(hover_buf, sizeof(hover_buf), "-"); } if (s_state.selected_node >= 0) { std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node); } else { std::snprintf(sel_buf, sizeof(sel_buf), "-"); } ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s", s_graph.node_count, s_graph.edge_count, s_state.layout_energy, ImGui::GetIO().Framerate, hover_buf, sel_buf); ImGui::PopStyleColor(); } section("Viewport (drag = pan, wheel = zoom, click = select)"); if (s_initialized) { // Avanzamos 1 paso de force layout cada frame mientras layout_running. // Auto-pause: si la energia por nodo cae bajo el umbral durante N // frames consecutivos, paramos la simulacion automaticamente — el // grafo ya esta estable. El usuario lo retoma con "Resume layout" // o "Regenerate". static int s_low_energy_frames = 0; const int k_pause_after_frames = 30; const float k_pause_per_node = 0.001f; // umbral de energia/nodo if (s_state.layout_running) { ForceLayoutConfig cfg; cfg.repulsion = s_repulsion; cfg.attraction = s_attraction; cfg.gravity = s_gravity; cfg.iterations = 1; if (s_use_gpu) { if (!s_gpu_ctx) { s_gpu_ctx = graph_force_layout_gpu_create(s_graph.node_count + 1024, s_graph.edge_count + 1024); s_gpu_dirty = true; } if (s_gpu_ctx) { if (s_gpu_dirty) { graph_force_layout_gpu_upload(s_gpu_ctx, s_graph); s_gpu_dirty = false; } s_state.layout_energy = graph_force_layout_gpu_step(s_gpu_ctx, cfg); graph_force_layout_gpu_readback(s_gpu_ctx, s_graph, /*include_velocities=*/true); } else { // GPU no disponible: caer a CPU silenciosamente. s_use_gpu = false; s_state.layout_energy = graph_force_layout_step(s_graph, cfg); } } else { s_state.layout_energy = graph_force_layout_step(s_graph, cfg); } const float per_node = s_graph.node_count > 0 ? s_state.layout_energy / (float)s_graph.node_count : 0.0f; if (per_node < k_pause_per_node) ++s_low_energy_frames; else s_low_energy_frames = 0; if (graph_force_layout_should_pause(s_low_energy_frames, k_pause_after_frames)) { s_state.layout_running = false; s_low_energy_frames = 0; } } else { s_low_energy_frames = 0; } graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460)); } code_block( "static GraphData graph;\n" "static GraphViewportState state;\n" "// ... rellenar graph.nodes / graph.edges ...\n" "graph.update_bounds();\n" "\n" "// Por frame:\n" "if (state.layout_running) {\n" " ForceLayoutConfig cfg;\n" " cfg.repulsion = 3500; cfg.gravity = 0.001f;\n" " graph_force_layout_step(graph, cfg);\n" "}\n" "graph_viewport(\"##g\", graph, state, ImVec2(0, 460));" ); } } // namespace gallery