--- id: "0049i" title: "`graph_layouts` (radial, hierarchical, fixed) + viewport extendido" status: completado type: feature domain: [] scope: multi-app priority: media depends: [] blocks: [] related: [] created: 2026-05-17 updated: 2026-05-17 tags: [] --- # 0049i — `graph_layouts` (radial, hierarchical, fixed) + viewport extendido ## Metadata | Campo | Valor | |-------|-------| | **ID** | 0049i | | **Estado** | pendiente | | **Prioridad** | media | | **Tipo** | feature — parte de [#0049](0049-osint-graph-viewer.md) | ## Dependencias **Bloqueada por:** [0049e](0049e-graph-types-extended.md) (necesita `flags`). --- ## Objetivo Consolidar las estrategias de layout estatico en una sola funcion `graph_layouts` (anadiendo `radial`, `hierarchical`, `fixed`), y extender `graph_viewport` con lasso, multi-select acumulativo, drag de seleccion entera y callbacks de menu contextual / double-click. ## Contexto Hoy `graph_force_layout.cpp` incluye `graph_layout_circular` y `graph_layout_grid` como helpers. Para OSINT son utiles: - **Radial**: arbol con un nodo raiz seleccionado y sus vecinos en circulos concentricos por hop. - **Hierarchical** (Sugiyama-style): niveles por tipo o por dependencia (Person → Email → Domain). - **Fixed**: no-op, las posiciones las pone el caller. `graph_viewport` ya soporta pan/zoom/click + hit-test. Falta el resto de UX para Maltego. ## Arquitectura ``` cpp/functions/viz/ ├── graph_layouts.{h,cpp} # NEW (mueve circular/grid + nuevos) ├── graph_layouts.md # NEW ├── graph_viewport.{h,cpp} # MOD: lasso, multi-select, callbacks └── graph_viewport.md # MOD: bump cpp/tests/ ├── test_graph_layouts.cpp # NEW └── test_graph_viewport.cpp # NEW (smoke) ``` ### `graph_layouts` API ```cpp namespace graph { // Estaticos. Mutan posiciones, respetan NF_PINNED. void layout_grid (GraphData&, float spacing); void layout_circular (GraphData&, float radius); void layout_random (GraphData&, float spread); void layout_radial (GraphData&, int root_node, float ring_spacing); void layout_hierarchical(GraphData&, int direction); // 0=LR, 1=RL, 2=TB, 3=BT void layout_fixed (GraphData&); // no-op } // namespace graph ``` `graph_force_layout.cpp` deja de exportar `_circular`/`_grid` (delegan a `graph_layouts`). Mantener wrappers deprecados un sub-issue maximo, eliminar antes del cierre de 0049. ### `graph_viewport` extensiones ```cpp struct GraphViewportCallbacks { void (*on_context_menu)(int node_idx, ImVec2 screen_pos, void* user) = nullptr; void (*on_double_click)(int node_idx, void* user) = nullptr; void* user = nullptr; }; struct GraphViewportState { // ... existente int selected_node; // legacy: ultimo seleccionado std::vector selection; // NEW: multi-seleccion bool lasso_active; ImVec2 lasso_start, lasso_end; }; // Igual firma que hoy, mas un parametro opcional de callbacks. void graph_viewport(const char* id, GraphData&, GraphViewportState&, ImVec2 size, const GraphViewportCallbacks& cb = {}); ``` Comportamiento: - **Click**: limpia seleccion, anade nodo bajo cursor. - **Ctrl+Click**: toggle nodo en seleccion. - **Shift+Drag (sin nodo bajo cursor)**: lasso. Al soltar, anade los nodos dentro del rect a la seleccion. - **Drag con un nodo seleccionado bajo el cursor**: arrastra todos los seleccionados como pinned (set `NF_PINNED` mientras se arrastra; mantener pinned al soltar). - **Right-click sobre un nodo**: invoca `on_context_menu(idx, screen_pos, user)` si esta seteado. - **Double-click sobre un nodo**: invoca `on_double_click(idx, user)`. - **Esc**: limpia seleccion. ## Tareas ### Fase 1 — `graph_layouts` - [ ] **1.1** Crear `graph_layouts.{h,cpp,md}`. Mover impl de `circular`/`grid` desde `graph_force_layout.cpp`. - [ ] **1.2** Implementar `layout_radial`: BFS desde `root_node`, posicionar cada hop k en un circulo de radio `k * ring_spacing`, distribuir uniformemente. - [ ] **1.3** Implementar `layout_hierarchical`: BFS levels por longest-path desde nodos sin in-edges; dentro de cada nivel ordenar por minimo cruce (greedy heuristico — no optimo, pero bueno para la UX OSINT). - [ ] **1.4** Implementar `layout_fixed`: no-op (recordar que existe la funcion). - [ ] **1.5** Todas respetan `NF_PINNED`. ### Fase 2 — Viewport multi-select + lasso - [ ] **2.1** En `graph_viewport.cpp`, implementar el comportamiento de la tabla anterior. - [ ] **2.2** Lasso: `ImDrawList::AddRect` para feedback visual + AABB hit-test al soltar. - [ ] **2.3** Drag de seleccion: pin todos los nodos seleccionados al inicio del drag, aplicar el delta a todos, mantener pinned al soltar. ### Fase 3 — Callbacks - [ ] **3.1** Anadir `GraphViewportCallbacks` y wirear `on_context_menu` (right-click) + `on_double_click`. - [ ] **3.2** Documentar en el `.md` que el callback se invoca dentro del frame ImGui — el caller puede abrir un popup. ### Fase 4 — Tests - [ ] **4.1** `test_graph_layouts`: smoke de cada layout sobre un grafo pequeño; verificar que `NF_PINNED` no se mueve; que `radial` distribuye correctamente. - [ ] **4.2** `test_graph_viewport`: setup de un grafo, simular hit-test programatico (no test interactivo, solo helpers puros). ### Fase 5 — Demo - [ ] **5.1** Anadir toggle de layout en `demos_graph` (`force | grid | circular | radial | hierarchical | fixed`). - [ ] **5.2** Anadir lasso + multi-select visible en el demo (text overlay con count seleccionados). ### Fase 6 — Cleanup - [ ] Bump versions: `graph_layouts` 1.0.0 (nuevo), `graph_viewport` 1.x → 1.x+1. - [ ] Documentar `params`/`output` en el `.md` para FTS5 search. - [ ] `fn index`. - [ ] Commit `feat(viz): graph_layouts (radial/hierarchical/fixed) + viewport multi-select+lasso`. ## Criterio de done - [ ] Switch entre layouts en el demo es instantaneo. - [ ] Lasso visible, multi-seleccion acumulativa funcional. - [ ] Drag de N nodos seleccionados los mueve juntos como pinned. - [ ] Right-click invoca callback si esta seteado. - [ ] Tests verdes. ## Riesgos | Riesgo | Mitigacion | |---|---| | Hierarchical layout se ve mal en grafos densamente cruzados | Aceptable — Sugiyama optimo es un campo entero; el heuristico es para visualizacion OSINT, no publicacion | | Multi-select state en GraphViewportState rompe ABI | Es un cambio interno; `selection` es campo nuevo, ok | | Drag de seleccion gigante (10k nodos) lagueva | Desactivar fuerzas en pinned ya implica que la GPU no los toca. Drag solo aplica delta — O(N seleccionados) trivial |