Files
fn_registry/dev/issues/completed/0049j-graph-labels.md
T
egutierrez 4b28ef6774 feat(viz): graph_labels render con LabelPolicy + ImDrawList (issue 0049j)
graph_labels_draw pinta etiquetas de nodos sobre el FBO del graph_renderer
via ImDrawList. Politica configurable: always-on para selected/hovered/
pinned, top-N por size*(degree+1), culling por viewport AABB y
min_node_pixel_size. Cap duro = max_visible + |always_*|.

API:
- graph_labels_draw(graph, viewport_state, policy, cb, user)
- graph_labels_draw_at(...)  — variante con rect explicito
- graph_labels_select(...)   — helper puro testeable
- graph_compute_degrees(...) — O(E)

Splitting en dos TUs:
- graph_labels.cpp          — funciones draw (depende de ImGui)
- graph_labels_select.cpp   — helpers puros para tests sin ImGui

12 tests en test_graph_labels (culling, max_visible cap, min_pixel_size,
always_* gating por viewport, top-N por score, edge cases). Todos verdes.

Integrado en demos_graph con UI: toggle Labels, sliders Max visible /
Font / Min px, checkboxes Selected/Hovered/Pinned. Golden de
graph_viewport regenerado.

Cierra issue 0049j.
2026-04-29 23:53:32 +02:00

5.3 KiB

0049j — graph_labels: render de etiquetas con LabelPolicy

Metadata

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

Dependencias

Bloqueada por: 0049e (necesita label_idx + flags).


Objetivo

Funcion graph_labels_draw que renderiza etiquetas de nodos seleccionados/hover/pinned + top-N por importancia, con politica configurable y culling por viewport. Independiente del renderer GPU — usa ImDrawList sobre el FBO.

Contexto

Maltego/OSINT necesita leer "juan@x.com", IBAN, etc. No se pueden mostrar 20k labels — pero se puede mostrar:

  • Siempre los selected/hovered/pinned (suelen ser pocos).
  • Top-N por tamaño de nodo o grado (configurable).
  • Todos cuando el zoom es alto y el nodo mide > X pixels en pantalla.

Esto se decide cada frame. ImDrawList es eficiente y se compone sobre la imagen del FBO ya pintada.

Arquitectura

cpp/functions/viz/
├── graph_labels.h                # NEW
├── graph_labels.cpp              # NEW
└── graph_labels.md                # NEW

cpp/tests/
└── test_graph_labels.cpp         # NEW (smoke + culling logic)

API

namespace graph {

struct LabelPolicy {
    bool  always_for_selected   = true;
    bool  always_for_hovered    = true;
    bool  always_for_pinned     = false;
    int   max_visible           = 200;     // top-N por size + degree
    float min_zoom_for_all      = 4.0f;    // a este zoom, mostrar todos los visibles del viewport
    float min_node_pixel_size   = 12.0f;   // skip si en pantalla mide menos
    float font_size             = 13.0f;   // pixels
    uint32_t color              = 0xFFFFFFFF; // ABGR
    uint32_t bg_color           = 0xC8000000; // semi-transparente
    float padding_x             = 4.0f;
    float padding_y             = 2.0f;
};

// Callback que devuelve el texto del label dado un node_idx.
// El consumer maneja su propio string pool / metadata.
typedef const char* (*GetLabelFn)(int node_idx, void* user);

// Llamar tras ImGui::Image(...) del FBO. Usa el ImDrawList del current window.
void graph_labels_draw(const GraphData&, const GraphViewportState&,
                       const LabelPolicy&, GetLabelFn cb, void* user);

} // namespace graph

Algoritmo (cada frame)

  1. Determinar AABB visible en world coords desde camera+zoom.
  2. Colectar nodos visibles + nodos con flags & (NF_SELECTED|NF_HOVERED|NF_PINNED).
  3. Si zoom >= min_zoom_for_all: candidatos = todos los visibles del viewport. Else: top-N por (size * degree).
  4. Filtrar: node_pixel_size = node.size * zoom; skip si < min_node_pixel_size (excepto los always_*).
  5. Para cada candidato superviviente:
    • World → screen.
    • text = cb(idx, user).
    • ImDrawList::AddRectFilled(bg) + AddText(color) con padding.
  6. Limit hard: nunca dibujar mas de max_visible + |selected| + |hovered| + |pinned|.

Tareas

Fase 1 — Funcion + helpers

  • 1.1 Crear graph_labels.{h,cpp,md}. Implementar _draw segun el algoritmo.
  • 1.2 Helper interno score(node) = size * (degree+1) calculado tras frustum cull para top-N.
  • 1.3 Cache opcional del degree por nodo si el consumer la quiere precalcular y pasarsela (parametro avanzado en LabelPolicy o helper aparte). Para v1, calcular o-fly desde edges en O(E) y guardar en un thread_local vector — no critico.

Fase 2 — Tests

  • 2.1 Test culling: setup grafo de 100 nodos, viewport pequeño, verificar que el numero de labels devuelto (mock callback que cuenta) respeta max_visible.
  • 2.2 Test always_for_selected: setear NF_SELECTED en uno fuera del viewport, verificar que NO se dibuja (selected pero off-screen — segun politica). Decision: documentar comportamiento (default: no, para no spamear).
  • 2.3 Test min_node_pixel_size: zoom bajo, nodo pequeño, no se dibuja.

Fase 3 — Integrar en demos_graph

  • 3.1 Tras la ImGui::Image(...) del viewport, llamar graph_labels_draw con un callback que devuelve "#" + node_idx.
  • 3.2 Anadir controles en demo para variar LabelPolicy: max_visible slider, font_size slider, toggle always_*.

Fase 4 — Cleanup

  • params/output documentados en .md.
  • fn index.
  • Commit feat(viz): graph_labels con LabelPolicy + ImDrawList.

Criterio de done

  • En demos_graph con 20k nodos: labels visibles para selected/hovered + top-N a fps estable.
  • Zoom alto muestra todos los visibles, zoom bajo solo los importantes — sin saltos bruscos.
  • Tests verdes.
  • No rompe perf: con LabelPolicy.max_visible = 0 y todos los always_* off, la funcion es practicamente gratis.

Riesgos

Riesgo Mitigacion
ImDrawList con miles de AddText degrada fps max_visible = 200 por default; cap es duro
Texto recortado por el clip rect del child window Si el FBO esta dentro de un BeginChild/EndChild, usar el draw list correcto (probablemente el del window padre con clip ajustado)
Cambios de zoom hacen aparecer/desaparecer labels en avalancha Hysteresis opcional en min_zoom_for_all (umbral on != umbral off). Para v1, simple
Costo de calcular degree cada frame Aceptable a 100k aristas (un pase O(E)); cachear si se vuelve hot path