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.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# 0049j — `graph_labels`: render de etiquetas con `LabelPolicy`
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0049j |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | media |
|
||||
| **Tipo** | feature — parte de [#0049](0049-osint-graph-viewer.md) |
|
||||
|
||||
## Dependencias
|
||||
|
||||
**Bloqueada por:** [0049e](0049e-graph-types-extended.md) (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
|
||||
|
||||
```cpp
|
||||
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 |
|
||||
Reference in New Issue
Block a user