#include "viz/graph_viewport.h" #include "viz/graph_types.h" #include "viz/graph_renderer.h" #include "viz/graph_force_layout.h" #include "core/graph_spatial_hash.h" #include "imgui.h" #include #include // snprintf #include // memset #include // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- static void viewport_to_graph(float vx, float vy, float widget_x, float widget_y, float widget_w, float widget_h, float cam_x, float cam_y, float zoom, float& gx, float& gy) { gx = (vx - widget_x - widget_w * 0.5f) / zoom + cam_x; gy = (vy - widget_y - widget_h * 0.5f) / zoom + cam_y; } static void graph_to_viewport(float gx, float gy, float widget_x, float widget_y, float widget_w, float widget_h, float cam_x, float cam_y, float zoom, float& vx, float& vy) { vx = (gx - cam_x) * zoom + widget_x + widget_w * 0.5f; vy = (gy - cam_y) * zoom + widget_y + widget_h * 0.5f; } // --------------------------------------------------------------------------- // graph_viewport_fit // --------------------------------------------------------------------------- void graph_viewport_fit(GraphData& graph, GraphViewportState& state) { graph.update_bounds(); if (graph.node_count == 0) { state.cam_x = 0.0f; state.cam_y = 0.0f; state.zoom = 1.0f; return; } float cx = (graph.min_x + graph.max_x) * 0.5f; float cy = (graph.min_y + graph.max_y) * 0.5f; state.cam_x = cx; state.cam_y = cy; float span_x = graph.max_x - graph.min_x; float span_y = graph.max_y - graph.min_y; float span = (span_x > span_y ? span_x : span_y); // Use render dimensions if available; fall back to a safe default. float view_px = (state.render_w > 0 ? (float)state.render_w : 600.0f); float view_py = (state.render_h > 0 ? (float)state.render_h : 400.0f); float view_min = (view_px < view_py ? view_px : view_py); if (span > 0.0f) { state.zoom = (view_min * 0.9f) / span; } else { state.zoom = 1.0f; } // Clamp to allowed range if (state.zoom < state.zoom_min) state.zoom = state.zoom_min; if (state.zoom > state.zoom_max) state.zoom = state.zoom_max; } // --------------------------------------------------------------------------- // graph_viewport_destroy // --------------------------------------------------------------------------- void graph_viewport_destroy(GraphViewportState& state) { if (state.renderer) { graph_renderer_destroy(state.renderer); state.renderer = nullptr; } if (state.spatial) { delete state.spatial; state.spatial = nullptr; } state.initialized = false; } // --------------------------------------------------------------------------- // graph_viewport — main widget // --------------------------------------------------------------------------- bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size, const GraphViewportCallbacks& cb) { bool interacted = false; // Resolve size ImVec2 avail = ImGui::GetContentRegionAvail(); float w = (size.x > 0.0f) ? size.x : avail.x; float h = (size.y > 0.0f) ? size.y : avail.y; if (w < 1.0f) w = 1.0f; if (h < 1.0f) h = 1.0f; int iw = (int)w, ih = (int)h; // ------------------------------------------------------------------- // 1. Lazy init // ------------------------------------------------------------------- if (!state.initialized) { state.renderer = graph_renderer_create(iw, ih); state.spatial = new SpatialHash(20.0f, 4096); state.render_w = iw; state.render_h = ih; state.initialized = true; graph_viewport_fit(graph, state); } // ------------------------------------------------------------------- // 2. Resize // ------------------------------------------------------------------- if (iw != state.render_w || ih != state.render_h) { graph_renderer_resize(state.renderer, iw, ih); state.render_w = iw; state.render_h = ih; } // ------------------------------------------------------------------- // 3. Force layout step // ------------------------------------------------------------------- if (state.layout_running && graph.node_count > 0) { state.layout_energy = graph_force_layout_step(graph); if (state.layout_energy < 0.01f) { state.layout_running = false; } } // ------------------------------------------------------------------- // 4. Build spatial hash // ------------------------------------------------------------------- if (graph.node_count > 0) { static std::vector xs_buf, ys_buf, sz_buf; xs_buf.resize(graph.node_count); ys_buf.resize(graph.node_count); sz_buf.resize(graph.node_count); for (int i = 0; i < graph.node_count; ++i) { xs_buf[i] = graph.nodes[i].x; ys_buf[i] = graph.nodes[i].y; sz_buf[i] = resolve_node_size(graph.nodes[i], graph.types, graph.type_count); } state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count); } // ------------------------------------------------------------------- // 5. Invisible button to capture input // ------------------------------------------------------------------- ImGui::PushID(id); ImVec2 widget_pos = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("canvas", ImVec2(w, h), ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonMiddle| ImGuiButtonFlags_MouseButtonRight); bool hovered = ImGui::IsItemHovered(); bool lm_down = ImGui::IsMouseDown(ImGuiMouseButton_Left); bool lm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Left); bool lm_release= ImGui::IsMouseReleased(ImGuiMouseButton_Left); bool lm_dbl = ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left); bool mm_down = ImGui::IsMouseDown(ImGuiMouseButton_Middle); bool rm_down = ImGui::IsMouseDown(ImGuiMouseButton_Right); bool rm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Right); bool shift = ImGui::GetIO().KeyShift; bool ctrl = ImGui::GetIO().KeyCtrl; ImVec2 mouse_pos = ImGui::GetMousePos(); float mx = mouse_pos.x, my = mouse_pos.y; // Convert mouse to graph space float gx_mouse, gy_mouse; viewport_to_graph(mx, my, widget_pos.x, widget_pos.y, w, h, state.cam_x, state.cam_y, state.zoom, gx_mouse, gy_mouse); // ------------------------------------------------------------------- // 5a. Pan (middle mouse drag, o right-button drag SIN nodo bajo cursor) // ------------------------------------------------------------------- // El pan con boton derecho se inhibe si el right-click cae sobre un // nodo: en ese caso queremos mostrar el menu contextual via callback. if (hovered && (mm_down || (rm_down && state.hovered_node < 0))) { ImVec2 delta = ImGui::GetIO().MouseDelta; if (delta.x != 0.0f || delta.y != 0.0f) { state.cam_x -= delta.x / state.zoom; state.cam_y -= delta.y / state.zoom; interacted = true; } } // ------------------------------------------------------------------- // 5b. Zoom (scroll wheel) // ------------------------------------------------------------------- if (hovered) { float wheel = ImGui::GetIO().MouseWheel; if (wheel != 0.0f) { float old_zoom = state.zoom; float new_zoom = old_zoom * (1.0f + wheel * 0.1f); if (new_zoom < state.zoom_min) new_zoom = state.zoom_min; if (new_zoom > state.zoom_max) new_zoom = state.zoom_max; float rel_x = (mx - widget_pos.x - w * 0.5f); float rel_y = (my - widget_pos.y - h * 0.5f); state.cam_x += rel_x / old_zoom - rel_x / new_zoom; state.cam_y += rel_y / old_zoom - rel_y / new_zoom; state.zoom = new_zoom; interacted = true; ImGui::GetIO().MouseWheel = 0.0f; } } // ------------------------------------------------------------------- // 5c. Hover — query nearest node // ------------------------------------------------------------------- int prev_hovered = state.hovered_node; if (prev_hovered >= 0 && prev_hovered < graph.node_count) { graph.nodes[prev_hovered].flags &= ~NF_HOVERED; } state.hovered_node = -1; if (hovered && graph.node_count > 0) { float hit_radius = 10.0f / state.zoom; int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius); if (nearest >= 0) { state.hovered_node = nearest; graph.nodes[nearest].flags |= NF_HOVERED; interacted = true; } } // ------------------------------------------------------------------- // 5d. Lasso (shift + drag sobre area vacia) // ------------------------------------------------------------------- // Activacion: shift presionado + lm_click (no haciendo click sobre nodo). if (hovered && lm_click && shift && state.hovered_node < 0 && state.drag_node == -1 && !state.lasso_active) { state.lasso_active = true; state.lasso_start = ImVec2(gx_mouse, gy_mouse); state.lasso_end = ImVec2(gx_mouse, gy_mouse); } if (state.lasso_active) { state.lasso_end = ImVec2(gx_mouse, gy_mouse); if (lm_release) { // Calcular AABB en world space y anadir nodos contenidos a la seleccion float x0 = std::min(state.lasso_start.x, state.lasso_end.x); float x1 = std::max(state.lasso_start.x, state.lasso_end.x); float y0 = std::min(state.lasso_start.y, state.lasso_end.y); float y1 = std::max(state.lasso_start.y, state.lasso_end.y); for (int i = 0; i < graph.node_count; ++i) { const GraphNode& n = graph.nodes[i]; if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1) { graph_viewport_add_to_selection(graph, state, i); } } state.lasso_active = false; interacted = true; } } // ------------------------------------------------------------------- // 5e. Node drag (left mouse down on a node) // ------------------------------------------------------------------- // Si el nodo bajo el cursor esta seleccionado, arrastra TODA la seleccion; // en otro caso solo el nodo bajo el cursor. if (hovered && lm_down && !state.lasso_active) { if (state.drag_node == -1 && state.hovered_node >= 0) { state.drag_node = state.hovered_node; state.is_dragging = true; state.drag_anchor_x = gx_mouse; state.drag_anchor_y = gy_mouse; // Si el nodo no esta en la seleccion y no es un toggle, lo // anadimos para que el drag mueva al menos ese nodo. if (!graph_viewport_is_selected(state, state.drag_node)) { if (!ctrl) { // Reemplaza seleccion: drag de nodo no seleccionado graph_viewport_clear_selection(graph, state); } graph_viewport_add_to_selection(graph, state, state.drag_node); } // Fijar todos los nodos seleccionados como pinned mientras dura // el drag — el force layout no los movera. for (int idx : state.selection) { if (idx >= 0 && idx < graph.node_count) { graph.nodes[idx].flags |= NF_PINNED; } } } } else if (state.drag_node >= 0) { // Release: mantener pinned (deja al usuario re-arrastrar). Quien // quiera "soltar" el pin puede hacer Esc o un re-click sobre area // vacia para limpiar seleccion. state.drag_node = -1; state.is_dragging = false; } if (state.drag_node >= 0 && state.drag_node < graph.node_count) { // delta en world space respecto al frame previo float dx = ImGui::GetIO().MouseDelta.x / state.zoom; float dy = ImGui::GetIO().MouseDelta.y / state.zoom; if (dx != 0.0f || dy != 0.0f) { for (int idx : state.selection) { if (idx >= 0 && idx < graph.node_count) { GraphNode& n = graph.nodes[idx]; n.x += dx; n.y += dy; n.vx = 0.0f; n.vy = 0.0f; n.flags |= NF_PINNED; } } interacted = true; } } // ------------------------------------------------------------------- // 5f. Click — seleccion (single / ctrl-toggle) // ------------------------------------------------------------------- // Solo procesar clicks que NO son inicio de drag (es decir: el lm_click // ya pudo haber lanzado un drag arriba). La rama drag arriba ya gestiona // la seleccion en su caso. Aqui nos ocupa el caso de click en area vacia // o click puro sobre nodo sin shift/lasso. if (hovered && lm_click && !shift && state.drag_node == -1 && !state.lasso_active) { if (state.hovered_node >= 0) { if (ctrl) { graph_viewport_toggle_selection(graph, state, state.hovered_node); } else { graph_viewport_clear_selection(graph, state); graph_viewport_add_to_selection(graph, state, state.hovered_node); } } else { graph_viewport_clear_selection(graph, state); } interacted = true; } // ------------------------------------------------------------------- // 5g. Right-click sobre nodo -> on_context_menu // ------------------------------------------------------------------- if (hovered && rm_click && state.hovered_node >= 0 && cb.on_context_menu) { cb.on_context_menu(state.hovered_node, mouse_pos, cb.user); interacted = true; } // ------------------------------------------------------------------- // 5h. Double-click -> on_double_click // ------------------------------------------------------------------- if (hovered && lm_dbl && state.hovered_node >= 0 && cb.on_double_click) { cb.on_double_click(state.hovered_node, cb.user); interacted = true; } // ------------------------------------------------------------------- // 5i. Keyboard shortcuts (only when widget hovered) // ------------------------------------------------------------------- if (hovered) { if (ImGui::IsKeyPressed(ImGuiKey_Space)) { state.layout_running = !state.layout_running; } if (ImGui::IsKeyPressed(ImGuiKey_F)) { graph_viewport_fit(graph, state); interacted = true; } if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { graph_viewport_clear_selection(graph, state); interacted = true; } } // ------------------------------------------------------------------- // 6. Render to GPU texture // ------------------------------------------------------------------- unsigned int tex_id = graph_renderer_draw(state.renderer, graph, state.cam_x, state.cam_y, state.zoom); // ------------------------------------------------------------------- // 7. Display texture (flip UV for OpenGL FBO convention) // ------------------------------------------------------------------- ImDrawList* draw_list = ImGui::GetWindowDrawList(); draw_list->AddImage( (ImTextureID)(intptr_t)tex_id, widget_pos, ImVec2(widget_pos.x + w, widget_pos.y + h), ImVec2(0.0f, 1.0f), ImVec2(1.0f, 0.0f) ); // ------------------------------------------------------------------- // 7b. Lasso visual feedback (rect en pantalla) // ------------------------------------------------------------------- if (state.lasso_active) { float vx0, vy0, vx1, vy1; graph_to_viewport(state.lasso_start.x, state.lasso_start.y, widget_pos.x, widget_pos.y, w, h, state.cam_x, state.cam_y, state.zoom, vx0, vy0); graph_to_viewport(state.lasso_end.x, state.lasso_end.y, widget_pos.x, widget_pos.y, w, h, state.cam_x, state.cam_y, state.zoom, vx1, vy1); ImVec2 a(std::min(vx0, vx1), std::min(vy0, vy1)); ImVec2 b(std::max(vx0, vx1), std::max(vy0, vy1)); draw_list->AddRectFilled(a, b, IM_COL32(80, 140, 220, 40)); draw_list->AddRect (a, b, IM_COL32(120, 180, 240, 200)); } // ------------------------------------------------------------------- // 8. Tooltip on hovered node (suprimido durante drag o lasso para no // tapar la accion) // ------------------------------------------------------------------- if (state.hovered_node >= 0 && state.hovered_node < graph.node_count && !state.is_dragging && !state.lasso_active) { const GraphNode& n = graph.nodes[state.hovered_node]; int degree = 0; for (int i = 0; i < graph.edge_count; ++i) { if ((int)graph.edges[i].source == state.hovered_node || (int)graph.edges[i].target == state.hovered_node) { ++degree; } } ImGui::BeginTooltip(); if (graph.types && n.type_id < (uint16_t)graph.type_count && graph.types[n.type_id].name) { ImGui::Text("Type: %s", graph.types[n.type_id].name); } else { ImGui::Text("Type: %u", (unsigned)n.type_id); } ImGui::Text("Index: %d", state.hovered_node); if (n.user_data) ImGui::Text("user_data: %llu", (unsigned long long)n.user_data); ImGui::Text("Degree: %d", degree); ImGui::EndTooltip(); } // ------------------------------------------------------------------- // 9. Status bar overlay // ------------------------------------------------------------------- { char status[160]; snprintf(status, sizeof(status), "Nodes: %d | Edges: %d | Sel: %zu | Zoom: %.2fx | Energy: %.4f | " "[Space] layout [F] fit [Esc] clear [Shift+drag] lasso [Ctrl+click] toggle", graph.node_count, graph.edge_count, state.selection.size(), state.zoom, state.layout_energy); ImVec2 text_pos = ImVec2(widget_pos.x + 6.0f, widget_pos.y + h - 18.0f); draw_list->AddText(text_pos, IM_COL32(180, 180, 180, 200), status); } ImGui::PopID(); return interacted; }