Files
fn_registry/dev/issues/completed/0049i-graph-layouts-static.md

6.5 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0049i `graph_layouts` (radial, hierarchical, fixed) + viewport extendido completado feature
multi-app media
2026-05-17 2026-05-17

0049i — graph_layouts (radial, hierarchical, fixed) + viewport extendido

Metadata

Campo Valor
ID 0049i
Estado pendiente
Prioridad media
Tipo feature — parte de #0049

Dependencias

Bloqueada por: 0049e (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

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

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