---
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<int> 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 |
