// 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 #include 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 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