Files
fn_registry/cpp/functions/viz/graph_labels_select.cpp
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

108 lines
3.8 KiB
C++

// Helpers puros de graph_labels (issue 0049j). Sin ImGui ni OpenGL — vive en
// su propio TU para que los tests unitarios puedan ejercer la seleccion de
// candidatos sin pintar nada. La parte de pintado (graph_labels_draw /
// graph_labels_draw_at) vive en graph_labels.cpp y depende de ImGui.
#include "viz/graph_labels.h"
#include "viz/graph_types.h"
#include <algorithm>
#include <vector>
namespace graph {
void graph_compute_degrees(const GraphData& g, int* out_degrees) {
if (!out_degrees || g.node_count <= 0) return;
for (int i = 0; i < g.node_count; ++i) out_degrees[i] = 0;
for (int e = 0; e < g.edge_count; ++e) {
const GraphEdge& ed = g.edges[e];
if ((ed.flags & EF_VISIBLE) == 0) continue;
if (ed.source < (uint32_t)g.node_count) ++out_degrees[ed.source];
if (ed.target < (uint32_t)g.node_count) ++out_degrees[ed.target];
}
}
int graph_labels_select(const GraphData& g, const LabelPolicy& p,
float cam_x, float cam_y, float zoom,
float widget_w, float widget_h,
const int* degrees,
int* out_indices, int out_capacity)
{
if (!out_indices || out_capacity <= 0) return 0;
if (g.node_count <= 0) return 0;
if (zoom <= 0.0f) return 0;
if (widget_w <= 0.0f || widget_h <= 0.0f) return 0;
const float half_w = widget_w * 0.5f / zoom;
const float half_h = widget_h * 0.5f / zoom;
const float min_wx = cam_x - half_w;
const float max_wx = cam_x + half_w;
const float min_wy = cam_y - half_h;
const float max_wy = cam_y + half_h;
auto in_view = [&](float x, float y) {
return x >= min_wx && x <= max_wx && y >= min_wy && y <= max_wy;
};
int written = 0;
// Pase A: nodos always_* en viewport. Skip min_node_pixel_size, pero
// off-screen NO se dibuja (decision documentada en el issue).
for (int i = 0; i < g.node_count && written < out_capacity; ++i) {
const GraphNode& n = g.nodes[i];
if ((n.flags & NF_VISIBLE) == 0) continue;
if (!in_view(n.x, n.y)) continue;
const bool sel = (n.flags & NF_SELECTED) && p.always_for_selected;
const bool hov = (n.flags & NF_HOVERED) && p.always_for_hovered;
const bool pin = (n.flags & NF_PINNED) && p.always_for_pinned;
if (sel || hov || pin) {
out_indices[written++] = i;
}
}
const int always_count = written;
if (p.max_visible <= 0) return written;
struct Cand { int idx; float score; };
static thread_local std::vector<Cand> cands;
cands.clear();
cands.reserve((size_t)g.node_count);
for (int i = 0; i < g.node_count; ++i) {
const GraphNode& n = g.nodes[i];
if ((n.flags & NF_VISIBLE) == 0) continue;
if (!in_view(n.x, n.y)) continue;
const float size = resolve_node_size(n, g.types, g.type_count);
if (size * zoom < p.min_node_pixel_size) continue;
bool already = false;
for (int k = 0; k < always_count; ++k) {
if (out_indices[k] == i) { already = true; break; }
}
if (already) continue;
const int deg = degrees ? degrees[i] : 0;
const float sc = size * (float)(deg + 1);
cands.push_back(Cand{ i, sc });
}
int budget = p.max_visible;
if ((int)cands.size() > budget) {
std::partial_sort(
cands.begin(), cands.begin() + budget, cands.end(),
[](const Cand& a, const Cand& b) { return a.score > b.score; });
cands.resize((size_t)budget);
}
(void)p.min_zoom_for_all; // gate suave: a zoom alto el min_pixel filtra menos
for (size_t k = 0; k < cands.size() && written < out_capacity; ++k) {
out_indices[written++] = cands[k].idx;
}
return written;
}
} // namespace graph