#include "app_base.h" #include "imgui.h" #include "core/fullscreen_window.h" #include "core/app_about.h" #include "core/app_settings.h" #include "core/panel_menu.h" #include "core/button.h" #include "core/tokens.h" #include "core/icons_tabler.h" #include "viz/graph_types.h" #include "viz/graph_viewport.h" #include "viz/graph_renderer.h" #include "viz/graph_force_layout.h" #include "viz/graph_force_layout_gpu.h" #include "viz/graph_layouts.h" #include "viz/graph_labels.h" #include "viz/graph_icons.h" #include "viz/graph_sources.h" #include "data.h" #include "views.h" #include "types_registry.h" #include "layout_store.h" #include "entity_ops.h" #include "project_manager.h" #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Estado global de la app // ---------------------------------------------------------------------------- static GraphData g_graph{}; static GraphViewportState g_viewport; static ge::AppState g_app; static ge::InputArgs g_input; static std::string g_input_path; // copia para que .uri sea estable static std::string g_types_path; static std::string g_layout_initial; // --layout flag static uint64_t g_graph_hash = 0; static bool g_loaded = false; // Project state — paths derivados del proyecto activo. En modo legacy // (--input/positional explicito), `g_active_project` queda vacio y los paths // vienen del CLI directamente. static std::string g_active_project; static std::string g_layout_db_path; // ruta de graph_explorer.db // Force layout GPU context (lazy init). static ForceLayoutGPU* g_gpu_ctx = nullptr; static bool g_gpu_dirty = true; // Icon atlas (de types.yaml) static IconAtlas* g_atlas = nullptr; static bool g_atlas_bound = false; // Para detectar primera invocacion de viewport (necesitamos el renderer creado) static bool g_first_render = true; // FPS estimate static auto g_last_frame = std::chrono::steady_clock::now(); static int g_frames_acc = 0; static auto g_fps_timer = std::chrono::steady_clock::now(); // Label policy static graph::LabelPolicy g_label_policy; // Indice user_data -> sql id (rebuild en cada load). static ge::EntityIndex g_idx; // ---------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------- static int layout_name_to_index(const std::string& s) { if (s == "force") return 0; if (s == "grid") return 1; if (s == "circular") return 2; if (s == "radial") return 3; if (s == "hierarchical") return 4; if (s == "fixed") return 5; return -1; } static void apply_static_layout(int mode) { if (g_graph.node_count == 0) return; switch (mode) { case 1: graph::layout_grid(g_graph, 20.0f); break; case 2: graph::layout_circular(g_graph, 200.0f); break; case 3: graph::layout_radial(g_graph, 0, 80.0f); break; case 4: graph::layout_hierarchical(g_graph, 0, 120.0f, 60.0f); break; case 5: graph::layout_fixed(g_graph); break; case 0: default: break; // force: no-op (lo mueve el bucle) } g_gpu_dirty = true; if (mode != 0) { g_graph.update_bounds(); graph_viewport_fit(g_graph, g_viewport); } } // Forward decl — definido mas abajo, lo necesita switch_to_project. static bool load_input(); // ---------------------------------------------------------------------------- // Project lifecycle // ---------------------------------------------------------------------------- // Aplica los paths del proyecto `slug` a las globales (g_input_path, // g_types_path, g_layout_db_path) y actualiza g_active_project. No abre BDs // ni carga el grafo — eso lo hace el caller. static void apply_project_paths(const std::string& slug) { ge::ProjectPaths p = ge::project_paths(slug.c_str()); g_active_project = slug; g_input_path = p.operations_db; g_types_path = p.types_yaml; g_layout_db_path = p.layout_db; g_app.active_project = slug; } // Cambia al proyecto `slug`: cierra layout_store, libera grafo, abre BDs // nuevas, carga grafo, persiste como last_active. Devuelve true en exito. static bool switch_to_project(const std::string& slug) { if (slug.empty()) return false; if (!ge::project_exists(slug.c_str())) { std::fprintf(stderr, "[graph_explorer] project '%s' no existe\n", slug.c_str()); return false; } // Cierra estado del proyecto actual ge::layout_store_close(); if (g_loaded) { graph::graph_free(&g_graph); g_loaded = false; } if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; } g_atlas_bound = false; g_viewport.selection.clear(); g_viewport.hovered_node = -1; g_viewport.selected_node = -1; // Aplica paths nuevos y abre BDs apply_project_paths(slug); if (!ge::layout_store_open(g_layout_db_path.c_str())) { std::fprintf(stderr, "[graph_explorer] layout_store_open failed: %s\n", g_layout_db_path.c_str()); } bool ok = load_input(); if (ok) ge::project_settings_touch(slug.c_str()); return ok; } static bool load_input() { g_input.kind = ge::INPUT_OPERATIONS; g_input.uri = g_input_path.c_str(); graph::GraphLoadStats stats{}; bool ok = ge::load_graph(g_input, &g_graph, &stats); if (!ok) { std::fprintf(stderr, "[graph_explorer] load failed: %s\n", stats.error_msg); return false; } std::fprintf(stdout, "[graph_explorer] loaded %d nodes, %d edges, %d types, %d rel_types from %s\n", stats.nodes_loaded, stats.edges_loaded, stats.types_discovered, stats.rel_types_discovered, g_input.uri); // types.yaml if (!g_types_path.empty()) { ge::ParsedTypes pt; std::string err; if (!ge::types_load_yaml(g_types_path.c_str(), &pt, &err)) { std::fprintf(stderr, "[graph_explorer] types.yaml: %s\n", err.c_str()); } else { std::vector codepoints = ge::apply_types_yaml(g_graph, pt); // Reset atlas — la prox vez que el viewport tenga renderer, se baja g_atlas_bound = false; if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; } g_atlas = ge::build_icon_atlas(codepoints); std::fprintf(stdout, "[graph_explorer] types.yaml: %zu entities, %zu relations, %zu icons\n", pt.entities.size(), pt.relations.size(), codepoints.size()); } } // Restablecer viewport state (preserva camara user-visible) g_viewport.selection.clear(); g_viewport.hovered_node = -1; g_viewport.selected_node = -1; g_viewport.layout_running = true; g_viewport.layout_energy = 0.0f; // Posicionar nodos: si todos tienen (x,y)=0, aplicar layout circular como // arranque (grafos cargados desde operations.db vienen sin posiciones). int zero_pos = 0; for (int i = 0; i < g_graph.node_count; ++i) { if (g_graph.nodes[i].x == 0.0f && g_graph.nodes[i].y == 0.0f) ++zero_pos; } if (zero_pos == g_graph.node_count) { graph::layout_circular(g_graph, 200.0f); } g_graph.update_bounds(); // Indice user_data -> sql id (para CRUD desde menu contextual). ge::entity_index_build(g_input.uri, &g_idx); g_app.input_db_path = g_input.uri ? g_input.uri : ""; // Cargar posiciones guardadas para este graph_hash g_graph_hash = ge::compute_graph_hash(g_input.uri); int restored = ge::layout_store_load(g_graph_hash, g_graph); if (restored > 0) { std::fprintf(stdout, "[graph_explorer] restored %d node positions from layout store\n", restored); g_graph.update_bounds(); } // Vista inicial graph_viewport_fit(g_graph, g_viewport); g_gpu_dirty = true; // App state — visibility por tipo g_app.graph = &g_graph; g_app.viewport = &g_viewport; ge::views_reset_visibility(g_app); ge::views_apply_visibility(g_app); // --layout inicial (si llego del CLI) int idx = layout_name_to_index(g_layout_initial); if (idx >= 0) { g_app.layout_mode = idx; apply_static_layout(idx); } g_loaded = true; return true; } static void run_force_step() { if (!g_viewport.layout_running) return; if (g_app.layout_mode != 0) return; // force solo en mode 0 ForceLayoutConfig cfg; cfg.repulsion = g_app.repulsion; cfg.attraction = g_app.attraction; cfg.gravity = g_app.gravity; cfg.iterations = 1; // Tapa de energia: damping mas agresivo + max_velocity bajo evita que el // grafo "explote" al cargar (nodos que arrancan cerca del origen y se // dispersan con repulsion alta). Valores tuneados para sentir movimiento // suave sin saltos visibles entre frames. cfg.damping = 0.7f; cfg.max_velocity = 8.0f; if (g_app.use_gpu) { if (!g_gpu_ctx) { g_gpu_ctx = graph_force_layout_gpu_create(g_graph.node_count + 1024, g_graph.edge_count + 1024); g_gpu_dirty = true; } if (g_gpu_ctx) { if (g_gpu_dirty) { graph_force_layout_gpu_upload(g_gpu_ctx, g_graph); g_gpu_dirty = false; } g_viewport.layout_energy = graph_force_layout_gpu_step(g_gpu_ctx, cfg); graph_force_layout_gpu_readback(g_gpu_ctx, g_graph, /*include_velocities=*/true); } else { g_app.use_gpu = false; g_viewport.layout_energy = graph_force_layout_step(g_graph, cfg); } } else { g_viewport.layout_energy = graph_force_layout_step(g_graph, cfg); } // Auto-pause heuristica: si energia/nodo es muy baja durante muchos // frames, apagar simulacion. El usuario puede reanudarla con el toggle. static int low = 0; const float k_pause_per_node = 0.001f; const int k_pause_after = 60; float per = g_graph.node_count > 0 ? g_viewport.layout_energy / (float)g_graph.node_count : 0.0f; if (per < k_pause_per_node) ++low; else low = 0; if (graph_force_layout_should_pause(low, k_pause_after)) { g_viewport.layout_running = false; low = 0; } } // FPS estimate sintetico (por segundo). static void update_fps() { using namespace std::chrono; auto now = steady_clock::now(); ++g_frames_acc; if (duration_cast(now - g_fps_timer).count() >= 1000) { g_app.fps_estimate = g_frames_acc; g_frames_acc = 0; g_fps_timer = now; } g_last_frame = now; } // ---------------------------------------------------------------------------- // Context menu callback (right-click sobre nodo) // ---------------------------------------------------------------------------- // Doble click sobre nodo: solicita abrir el panel Note. main.cpp procesa // despues (necesita acceso al EntityIndex para resolver el sql id). static void on_double_click_cb(int node_idx, void* /*user*/) { g_app.want_open_note = true; g_app.open_note_target = node_idx; } static void on_context_menu_cb(int node_idx, ImVec2 /*screen_pos*/, void* /*user*/) { g_app.ctx_node = node_idx; g_app.ctx_open_request = true; if (node_idx >= 0 && node_idx < g_graph.node_count) { const GraphNode& n = g_graph.nodes[node_idx]; if (n.type_id < (uint16_t)g_graph.type_count && g_graph.types[n.type_id].name) { std::snprintf(g_app.ctx_new_type, sizeof(g_app.ctx_new_type), "%s", g_graph.types[n.type_id].name); } else { g_app.ctx_new_type[0] = 0; } } } // Lista de tipos disponibles para "Change type" — se construye desde el grafo // activo. Si esta vacia, se usa una lista por defecto. static const char* k_default_types[] = { "text", "person", "organization", "email", "ip_address", "domain", "url", "phone", "crypto_wallet", "malware", "vulnerability", }; constexpr int k_default_types_n = (int)(sizeof(k_default_types) / sizeof(k_default_types[0])); static void render_context_menu() { if (g_app.ctx_open_request) { ImGui::OpenPopup("##node_ctx"); g_app.ctx_open_request = false; } if (!ImGui::BeginPopup("##node_ctx")) return; int idx = g_app.ctx_node; if (idx < 0 || idx >= g_graph.node_count) { ImGui::TextDisabled("(no node)"); ImGui::EndPopup(); return; } const GraphNode& n = g_graph.nodes[idx]; const char* lbl = graph::graph_label(&g_graph, n.label_idx); ImGui::TextDisabled("%s", lbl && *lbl ? lbl : "(unnamed)"); ImGui::Separator(); if (ImGui::BeginMenu("Change type")) { // Construye un set ordenado y deduplicado: tipos del grafo + defaults. // Asi evitamos colisiones de ID en ImGui ("person" en grafo y default). std::vector all; all.reserve(g_graph.type_count + k_default_types_n); for (int i = 0; i < g_graph.type_count; ++i) { if (g_graph.types[i].name && *g_graph.types[i].name) { all.push_back(g_graph.types[i].name); } } for (int i = 0; i < k_default_types_n; ++i) { const char* d = k_default_types[i]; bool dup = false; for (const char* x : all) { if (std::strcmp(x, d) == 0) { dup = true; break; } } if (!dup) all.push_back(d); } for (size_t i = 0; i < all.size(); ++i) { ImGui::PushID((int)i); if (ImGui::MenuItem(all[i])) { std::snprintf(g_app.ctx_new_type, sizeof(g_app.ctx_new_type), "%s", all[i]); g_app.want_change_type = true; } ImGui::PopID(); } ImGui::EndMenu(); } if (ImGui::MenuItem("Duplicate")) { g_app.want_duplicate_node = true; } if (ImGui::MenuItem("Delete")) { g_app.want_delete_node = true; } ImGui::Separator(); if (ImGui::BeginMenu("Run enricher")) { ImGui::TextDisabled("(coming soon — issues 0001/0002/0003)"); ImGui::EndMenu(); } ImGui::EndPopup(); } // ---------------------------------------------------------------------------- // Label callback // ---------------------------------------------------------------------------- static const char* get_label_cb(int node_idx, void* /*user*/) { if (node_idx < 0 || node_idx >= g_graph.node_count) return ""; const GraphNode& n = g_graph.nodes[node_idx]; return graph::graph_label(&g_graph, n.label_idx); } // ---------------------------------------------------------------------------- // Render // ---------------------------------------------------------------------------- static fn_ui::PanelToggle g_panels[] = { {"Viewport", nullptr, &g_app.panel_viewport}, {"Legend", nullptr, &g_app.panel_legend}, {"Inspector", nullptr, &g_app.panel_inspector}, {"Stats", nullptr, &g_app.panel_stats}, {"Note", nullptr, &g_app.panel_note}, }; static void render() { update_fps(); // No tenemos menu propio — fn::run_app llamara al app_menubar via panels[]. if (!g_loaded) { fullscreen_window_begin("##empty"); ImGui::TextColored(ImVec4(1, 0.7f, 0.3f, 1), "graph_explorer — no input loaded"); ImGui::Spacing(); ImGui::TextWrapped( "Usage: graph_explorer [] [--input operations ] " "[--types ] [--layout ]"); ImGui::Spacing(); ge::views_open_modal(g_app); if (g_app.want_open_file) { g_input_path = g_app.open_buf; g_app.want_open_file = false; load_input(); } if (fn_ui::button("Open file...", fn_ui::ButtonVariant::Primary)) { g_app.show_open_modal = true; } fullscreen_window_end(); return; } // Dockspace host: ocupa el area BAJO la toolbar (44 px) para que las // ventanas dockeadas no queden detras de la barra superior. ImGuiViewport* vp = ImGui::GetMainViewport(); const float k_toolbar_h = 44.0f; { ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x, vp->WorkPos.y + k_toolbar_h)); ImGui::SetNextWindowSize(ImVec2(vp->WorkSize.x, vp->WorkSize.y - k_toolbar_h)); ImGui::SetNextWindowViewport(vp->ID); ImGuiWindowFlags hostFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoSavedSettings; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::Begin("##dock_host", nullptr, hostFlags); ImGui::PopStyleVar(); ImGui::DockSpace(ImGui::GetID("##dockspace"), ImVec2(0, 0), ImGuiDockNodeFlags_PassthruCentralNode); ImGui::End(); } // Toolbar superior — usa una ventana sin scroll y sin titulo ImGui::SetNextWindowPos(vp->WorkPos); ImGui::SetNextWindowSize(ImVec2(vp->WorkSize.x, 44.0f)); ImGui::Begin("##toolbar", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoSavedSettings); ge::views_toolbar(g_app); ImGui::End(); // Modals ge::views_open_modal(g_app); ge::views_filters_modal(g_app); ge::views_new_project_modal(g_app); // Project switch (desde menu Project, o tras crear proyecto nuevo) if (g_app.want_switch_project && !g_app.switch_project_target.empty()) { std::string target = g_app.switch_project_target; g_app.want_switch_project = false; g_app.switch_project_target.clear(); if (!switch_to_project(target)) { std::fprintf(stderr, "[graph_explorer] switch_to_project('%s') failed\n", target.c_str()); } } // Si el usuario aplico nuevo layout en la toolbar if (g_app.apply_layout_tick > 0) { apply_static_layout(g_app.layout_mode); g_app.apply_layout_tick = 0; } // Triggers desde la toolbar if (g_app.want_fit) { graph_viewport_fit(g_graph, g_viewport); g_app.want_fit = false; } if (g_app.want_reload) { g_app.want_reload = false; graph::GraphLoadStats stats{}; if (ge::reload_graph(g_input, &g_graph, &stats)) { ge::views_reset_visibility(g_app); ge::views_apply_visibility(g_app); g_graph.update_bounds(); graph_viewport_fit(g_graph, g_viewport); int restored = ge::layout_store_load(g_graph_hash, g_graph); if (restored > 0) g_graph.update_bounds(); g_atlas_bound = false; // re-bind atlas tras reload g_gpu_dirty = true; } } if (g_app.want_save_layout) { int n = ge::layout_store_save(g_graph_hash, g_graph); std::fprintf(stdout, "[graph_explorer] saved %d node positions\n", n); g_app.want_save_layout = false; } if (g_app.want_open_file) { g_input_path = g_app.open_buf; g_app.want_open_file = false; // Cleanup viejo grafo graph::graph_free(&g_graph); load_input(); } // ---- Mutaciones (add/delete/duplicate/change_type) ---- auto reload_after_mutation = [&]() { graph::GraphLoadStats stats{}; if (!ge::reload_graph(g_input, &g_graph, &stats)) return; ge::entity_index_build(g_input.uri, &g_idx); ge::views_reset_visibility(g_app); ge::views_apply_visibility(g_app); // Restablece posiciones guardadas. Los nodos nuevos no tienen // posicion en el layout_store y caen en (0,0). int restored = ge::layout_store_load(g_graph_hash, g_graph); (void)restored; // Centro del area visible en world coords (para que los nuevos nodos // aparezcan donde el usuario esta mirando, no en el origen). float cx = -g_viewport.cam_x; float cy = -g_viewport.cam_y; float spread_r = 80.0f / (g_viewport.zoom > 0.01f ? g_viewport.zoom : 0.01f); // Reparte los nodos sin posicion en un anillo poisson alrededor del // centro visible. Determinista por user_data para que el mismo nodo // caiga siempre en el mismo sitio entre reloads. for (int i = 0; i < g_graph.node_count; ++i) { GraphNode& n = g_graph.nodes[i]; if (n.x != 0.0f || n.y != 0.0f) continue; uint64_t h = n.user_data ? n.user_data : (uint64_t)i * 2654435761ull; float a = (float)((h >> 0) & 0xFFFF) / 65535.0f * 6.2831853f; float r = spread_r * (0.4f + (float)((h >> 16) & 0xFFFF) / 65535.0f * 0.6f); n.x = cx + std::cos(a) * r; n.y = cy + std::sin(a) * r; n.vx = n.vy = 0.0f; } g_graph.update_bounds(); g_atlas_bound = false; g_gpu_dirty = true; }; if (g_app.want_add_node && g_app.add_buf[0]) { char new_id[80]; if (ge::entity_insert(g_app.input_db_path.c_str(), g_app.add_buf, /*type_ref=*/nullptr, new_id, sizeof(new_id))) { std::fprintf(stdout, "[graph_explorer] added entity %s\n", new_id); g_app.add_buf[0] = 0; reload_after_mutation(); } else { std::fprintf(stderr, "[graph_explorer] add_entity failed\n"); } g_app.want_add_node = false; } auto ctx_id = [&]() -> const char* { if (g_app.ctx_node < 0 || g_app.ctx_node >= g_graph.node_count) return nullptr; return ge::entity_index_lookup(g_idx, g_graph.nodes[g_app.ctx_node].user_data); }; if (g_app.want_delete_node) { if (const char* id = ctx_id()) { if (ge::entity_delete(g_app.input_db_path.c_str(), id)) { std::fprintf(stdout, "[graph_explorer] deleted entity %s\n", id); reload_after_mutation(); } } g_app.want_delete_node = false; g_app.ctx_node = -1; } if (g_app.want_duplicate_node) { if (const char* id = ctx_id()) { char new_id[80]; if (ge::entity_duplicate(g_app.input_db_path.c_str(), id, new_id, sizeof(new_id))) { std::fprintf(stdout, "[graph_explorer] duplicated %s -> %s\n", id, new_id); reload_after_mutation(); } } g_app.want_duplicate_node = false; } if (g_app.want_change_type && g_app.ctx_new_type[0]) { if (const char* id = ctx_id()) { if (ge::entity_update_type(g_app.input_db_path.c_str(), id, g_app.ctx_new_type)) { std::fprintf(stdout, "[graph_explorer] %s -> type %s\n", id, g_app.ctx_new_type); reload_after_mutation(); } } g_app.want_change_type = false; } // Reset layout: limpia NF_PINNED en todos los nodos. El layout activo se // reaplica via apply_layout_tick (la toolbar ya lo incrementa). if (g_app.want_unpin_all) { for (int i = 0; i < g_graph.node_count; ++i) { g_graph.nodes[i].flags &= ~NF_PINNED; g_graph.nodes[i].vx = 0.0f; g_graph.nodes[i].vy = 0.0f; } g_viewport.layout_running = true; g_app.want_unpin_all = false; } // Note editor — abrir / guardar. if (g_app.want_open_note && g_app.open_note_target >= 0 && g_app.open_note_target < g_graph.node_count) { int n = g_app.open_note_target; const char* sql_id = ge::entity_index_lookup(g_idx, g_graph.nodes[n].user_data); if (sql_id) { std::string md; ge::entity_get_notes(g_app.input_db_path.c_str(), sql_id, &md); g_app.note_node = n; g_app.note_entity_id = sql_id; const char* lbl = graph::graph_label(&g_graph, g_graph.nodes[n].label_idx); g_app.note_entity_label = lbl ? lbl : ""; uint16_t tid = g_graph.nodes[n].type_id; g_app.note_entity_type = (tid < (uint16_t)g_graph.type_count && g_graph.types[tid].name) ? g_graph.types[tid].name : ""; // Asegura buffer >= max(64KB, contenido + holgura). size_t need = md.size() + 4096; if (need < 65536) need = 65536; g_app.note_buf.assign(need, 0); std::memcpy(g_app.note_buf.data(), md.data(), md.size()); g_app.note_dirty = false; g_app.panel_note = true; ImGui::SetWindowFocus(TI_FILE_TEXT " Note"); } g_app.want_open_note = false; g_app.open_note_target = -1; } if (g_app.want_save_note && !g_app.note_entity_id.empty()) { if (ge::entity_set_notes(g_app.input_db_path.c_str(), g_app.note_entity_id.c_str(), g_app.note_buf.data())) { g_app.note_dirty = false; std::fprintf(stdout, "[graph_explorer] saved note for %s (%zu bytes)\n", g_app.note_entity_id.c_str(), std::strlen(g_app.note_buf.data())); } else { std::fprintf(stderr, "[graph_explorer] save note failed\n"); } g_app.want_save_note = false; } // Posiciones iniciales razonables; el usuario puede moverlas y se // persiste via imgui.ini. const float top = vp->WorkPos.y + 44.0f; const float W = vp->WorkSize.x; const float H = vp->WorkSize.y - 44.0f; const float lw = 240.0f; // Legend const float rw = 320.0f; // Inspector / Stats const float sh = H * 0.55f; // Inspector altura // Viewport — ventana central if (g_app.panel_viewport) { ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + lw, top), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(W - lw - rw, H), ImGuiCond_FirstUseEver); if (ImGui::Begin("Viewport", &g_app.panel_viewport)) { run_force_step(); GraphViewportCallbacks vp_cb{}; vp_cb.on_context_menu = &on_context_menu_cb; vp_cb.on_double_click = &on_double_click_cb; graph_viewport("##gv", g_graph, g_viewport, ImVec2(0, 0), vp_cb); render_context_menu(); // La primera vez que el viewport se dibuja, el renderer existe — // bind del atlas (si tenemos uno). if (!g_atlas_bound && g_viewport.renderer) { if (g_atlas) { graph_renderer_set_icon_atlas(g_viewport.renderer, graph_icons_texture(g_atlas), graph_icons_uv_table(g_atlas), graph_icons_count(g_atlas)); } g_atlas_bound = true; } if (g_app.labels_enabled) { graph::graph_labels_draw(g_graph, g_viewport, g_label_policy, &get_label_cb, nullptr); } } ImGui::End(); } else { // Sin ventana visible, igual avanzamos la simulacion para que al // reabrirla el grafo este actualizado. run_force_step(); } // Legend — izquierda ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x, top), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(lw, H), ImGuiCond_FirstUseEver); ge::views_legend(g_app); // Inspector / Stats — derecha (apilados) ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W - rw, top), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(rw, sh), ImGuiCond_FirstUseEver); ge::views_inspector(g_app); ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W - rw, top + sh), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(rw, H - sh), ImGuiCond_FirstUseEver); ge::views_stats(g_app); // Note editor — al abrirse por primera vez se posiciona como ventana // centrada. El usuario la puede dockear donde prefiera. ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W * 0.25f, top + 40.0f), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(700.0f, 480.0f), ImGuiCond_FirstUseEver); ge::views_note(g_app); g_first_render = false; } // ---------------------------------------------------------------------------- // CLI parsing // ---------------------------------------------------------------------------- static void usage() { std::fprintf(stderr, "Usage: graph_explorer []\n" " graph_explorer --input operations \n" " graph_explorer --types \n" " graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n" " graph_explorer --project \n"); } int main(int argc, char** argv) { bool legacy_mode = false; // --input / positional dado: NO usar proyecto std::string project_arg; // --project (puede estar vacio) for (int i = 1; i < argc; ++i) { const char* a = argv[i]; if (std::strcmp(a, "--input") == 0 && i + 2 < argc) { const char* kind = argv[++i]; const char* path = argv[++i]; if (std::strcmp(kind, "operations") == 0) { g_input_path = path; legacy_mode = true; } else { std::fprintf(stderr, "[graph_explorer] unsupported input kind: %s\n", kind); return 1; } } else if (std::strcmp(a, "--types") == 0 && i + 1 < argc) { g_types_path = argv[++i]; } else if (std::strcmp(a, "--layout") == 0 && i + 1 < argc) { g_layout_initial = argv[++i]; } else if (std::strcmp(a, "--project") == 0 && i + 1 < argc) { project_arg = argv[++i]; } else if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) { usage(); return 0; } else if (a[0] == '-') { std::fprintf(stderr, "[graph_explorer] unknown flag: %s\n", a); usage(); return 1; } else { // Positional: tratado como operations.db (legacy) if (g_input_path.empty()) { g_input_path = a; legacy_mode = true; } } } if (legacy_mode) { // Modo legacy: paths sueltos junto al exe (compat con flujo anterior) ge::layout_store_open("graph_explorer.db"); g_layout_db_path = "graph_explorer.db"; if (!g_input_path.empty()) { load_input(); } } else { // Modo proyecto: migra layout legacy si aplica, decide proyecto activo, // crea default si no existe ninguno. ge::projects_migrate_legacy_layout(); std::string target = project_arg; if (target.empty()) { ge::ProjectSettings ps; ge::project_settings_load(&ps); target = ps.last_active; } if (target.empty()) target = ge::k_default_project; if (!ge::project_exists(target.c_str())) { std::string err; if (!ge::project_create(target.c_str(), &err)) { std::fprintf(stderr, "[graph_explorer] no se pudo crear el proyecto '%s': %s\n", target.c_str(), err.c_str()); return 1; } std::fprintf(stdout, "[graph_explorer] proyecto creado: projects/%s/\n", target.c_str()); } apply_project_paths(target); ge::layout_store_open(g_layout_db_path.c_str()); ge::project_settings_touch(target.c_str()); load_input(); } fn_ui::about_window_set_info( "graph_explorer", "0.1.0", "Visor de grafos GPU-accelerated agnostico del backend. Lee operations.db de " "cualquier app del registry y permite explorar entidades/relaciones con " "shapes/iconos/layouts/filtros."); int rc = fn::run_app( {.title = "graph_explorer", .width = 1600, .height = 1000, .viewports = true, .panels = g_panels, .panel_count = sizeof(g_panels) / sizeof(g_panels[0]), .init_gl_loader = true}, render); // Cleanup if (g_gpu_ctx) graph_force_layout_gpu_destroy(g_gpu_ctx); if (g_atlas) graph_icons_destroy(g_atlas); graph_viewport_destroy(g_viewport); graph::graph_free(&g_graph); ge::layout_store_close(); return rc; }