--- name: graph_labels kind: function lang: cpp domain: viz version: "1.0.0" purity: impure signature: "void graph::graph_labels_draw(const GraphData&, const GraphViewportState&, const LabelPolicy&, GetLabelFn, void*); void graph::graph_labels_draw_at(const GraphData&, float cam_x, float cam_y, float zoom, float wmin_x, float wmin_y, float w, float h, const LabelPolicy&, GetLabelFn, void*)" description: "Renderiza etiquetas de nodos sobre el viewport del grafo via ImDrawList con politica configurable: always-on para selected/hovered/pinned, top-N por (size * (degree+1)), y culling por viewport + min pixel size. Independiente del renderer GPU." tags: [graph, labels, imdrawlist, viewport, osint, culling, top-n] uses_functions: ["graph_labels_select_cpp_viz"] uses_types: ["GraphData_cpp_viz"] returns: [] returns_optional: false error_type: "error_go_core" imports: ["imgui.h"] tested: true tests: ["select respects max_visible cap", "min_node_pixel_size culling", "always_for_selected gates by viewport", "no callback / empty graph no-op"] test_file_path: "cpp/tests/test_graph_labels.cpp" file_path: "cpp/functions/viz/graph_labels.cpp" framework: imgui params: - name: graph desc: "Grafo a etiquetar (lectura). Se respetan NF_VISIBLE, NF_SELECTED, NF_HOVERED, NF_PINNED." - name: state desc: "GraphViewportState con camara y zoom. graph_labels_draw lo usa para world->screen." - name: cam_x desc: "(draw_at/select) Centro de la camara en world coords (X)." - name: cam_y desc: "(draw_at/select) Centro de la camara en world coords (Y)." - name: zoom desc: "(draw_at/select) Pixels por unidad world. zoom <= 0 → no-op." - name: wmin_x desc: "(draw_at) Esquina superior-izquierda del widget en pixeles de pantalla (X)." - name: wmin_y desc: "(draw_at) Esquina superior-izquierda del widget en pixeles de pantalla (Y)." - name: w desc: "(draw_at/select) Ancho del widget en pixeles." - name: h desc: "(draw_at/select) Alto del widget en pixeles." - name: policy desc: "LabelPolicy: max_visible (top-N), always_for_*, min_zoom_for_all, min_node_pixel_size, font_size, color, bg_color, padding." - name: cb desc: "Callback `const char* (int node_idx, void* user)` que devuelve el texto del label. NULL/'' omite el nodo." - name: user desc: "Puntero opaco que se pasa al callback. Util para pasar el string pool o metadata del consumer." - name: degrees desc: "(select) Array de tamano node_count con el grado por nodo. NULL → score = size sin factor degree." - name: out_indices desc: "(select) Array de salida con los indices de nodos a etiquetar. Capacidad = out_capacity." - name: out_capacity desc: "(select) Capacidad maxima de out_indices. La funcion no escribe mas alla." - name: out_degrees desc: "(compute_degrees) Array de tamano node_count que recibe el grado por nodo. Cuenta solo aristas con EF_VISIBLE." output: "Ninguno (void) salvo graph_labels_select que devuelve int (numero de indices escritos en out_indices). Las funciones draw pintan rectangulos y texto via ImDrawList del current ImGui window." notes: "Issue 0049j. La estrategia de seleccion es: (A) nodos always_* en viewport sin chequeo de pixel size; (B) resto de nodos visibles que cumplen min_node_pixel_size, capados al top-N por (size * (degree+1)). Off-screen always_* se OMITEN intencionadamente para no spamear. El degree se calcula on-the-fly cada frame en draw_at (O(E)); para hot paths el caller puede precalcular y pasarlo a graph_labels_select. Cap duro = max_visible + |always_*|." --- # graph_labels Pintar etiquetas de nodos sobre la imagen del FBO del `graph_renderer`. Pensado para uso OSINT/Maltego: necesitas leer "juan@x.com", IBAN, etc. en los nodos relevantes, pero no puedes mostrar 20k labels — el ojo y la GPU se saturan. ## Estrategia Cada frame el algoritmo decide que nodos etiquetar: 1. **Always-on** — si `LabelPolicy.always_for_{selected,hovered,pinned}` esta activo y el nodo lleva ese flag, se etiqueta. Skip de `min_node_pixel_size`, pero sigue requiriendo estar dentro del viewport (decision: off-screen no se dibuja para no spamear con flechas o etiquetas en los bordes). 2. **Top-N** — del resto de nodos visibles en el viewport que ademas tienen `size * zoom >= min_node_pixel_size`, se ordena por `score = size * (degree+1)` y se cogen los `max_visible` primeros. 3. **Cap duro** — total = `max_visible + |always_*|`. Sin opciones de saltarse esto. Si el caller quiere 0 labels excepto los always, basta poner `max_visible = 0`. `min_zoom_for_all` esta en la struct por compatibilidad con el spec original; en la implementacion v1 actua como un parametro suave: con zoom alto la mayoria de nodos visibles superan `min_node_pixel_size` y entran al top-N por si solos, asi que el efecto deseado (mostrar todos a zoom alto) sale naturalmente sin ramas adicionales. ## API ```cpp namespace graph { struct LabelPolicy { /* ... */ }; typedef const char* (*GetLabelFn)(int node_idx, void* user); // Tras ImGui::Image(...) o graph_viewport(...). Resuelve rect via // GetItemRectMin/Max y usa el ImDrawList del current window. void graph_labels_draw (const GraphData&, const GraphViewportState&, const LabelPolicy&, GetLabelFn, void*); // Variante explicita: el caller pasa cam, zoom y rect del widget. void graph_labels_draw_at(const GraphData&, float cam_x, float cam_y, float zoom, float wmin_x, float wmin_y, float w, float h, const LabelPolicy&, GetLabelFn, void*); // Helpers puros (testables sin ImGui). void graph_compute_degrees(const GraphData&, int* out_degrees); int graph_labels_select (const GraphData&, const LabelPolicy&, float cam_x, float cam_y, float zoom, float w, float h, const int* degrees, int* out_indices, int out_capacity); } ``` ## Uso tipico (en demos_graph) ```cpp graph_viewport("##g", graph, state, ImVec2(0, 460)); graph::LabelPolicy policy; policy.max_visible = 200; policy.always_for_selected = true; policy.always_for_hovered = true; policy.min_node_pixel_size = 12.0f; struct LabelCtx { char buf[32]; }; auto get_label = [](int idx, void* user) -> const char* { auto* ctx = (LabelCtx*)user; std::snprintf(ctx->buf, sizeof(ctx->buf), "#%d", idx); return ctx->buf; }; LabelCtx ctx; graph::graph_labels_draw(graph, state, policy, get_label, &ctx); ``` ## Coste - `graph_compute_degrees`: O(E). - `graph_labels_select`: O(N) para frustum cull + O(K log K) si K candidatos superan `max_visible` (partial_sort). - Por label: 1 `AddRectFilled` + 1 `AddText`. ImDrawList es eficiente; con `max_visible = 200` apenas se nota. ## Notas de integracion - Llamar **despues** de `graph_viewport` (o `ImGui::Image` del FBO). El widget rect se resuelve con `ImGui::GetItemRectMin/Max()`. - Las labels se dibujan en el `ImDrawList` del **current window**. Si el FBO esta dentro de un `BeginChild/EndChild`, el clip rect del child puede recortar etiquetas de nodos pegados a los bordes — usar `draw_at` y pasar el rect que se quiera respetar. - El callback se invoca como mucho una vez por nodo etiquetado por frame. - Para reproducibilidad en tests, usar `graph_labels_select` con un buffer de capacidad conocida — no toca ImGui. ## Split de TU (2026-05-04, ADR 0003) Los helpers puros `graph_compute_degrees` y `graph_labels_select` viven en `graph_labels_select.cpp` y se indexan como entrada propia `graph_labels_select_cpp_viz`. Esta entrada `graph_labels.md` solo cubre `graph_labels_draw` y `graph_labels_draw_at` (impuras, dependen de ImGui). Apps que reusan `graph_labels` deben enlazar AMBOS `.cpp` y declarar AMBAS entradas en su `app.md`: ```cmake ${FN_CPP_ROOT_DIR}/functions/viz/graph_labels.cpp ${FN_CPP_ROOT_DIR}/functions/viz/graph_labels_select.cpp ``` ```yaml uses_functions: - graph_labels_cpp_viz - graph_labels_select_cpp_viz ```