feat(cpp/viz): split orphan TUs as separate fn entries (ADR 0003)

Cuando una funcion del registry parte su .cpp en varios TUs por testabilidad
o separacion ImGui-vs-puro, cada TU adicional se registra como entrada propia
con su .md en lugar de extender file_path para listar varios archivos.

Aplicado a:
- graph_labels_select_cpp_viz: helpers puros (compute_degrees + labels_select).
- graph_viewport_selection_cpp_viz: clear/add/toggle/is_selected puros.
- graph_types_cpp_viz: TU de update_bounds + find_node_by_user_data.

graph_labels y graph_viewport actualizados para declarar las nuevas entradas
en uses_functions. Razon detallada en docs/adr/0003 + regla actualizada en
.claude/rules/uses_functions.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 11:51:10 +02:00
parent f1a5e04d4f
commit bf94893032
8 changed files with 364 additions and 15 deletions
+93
View File
@@ -0,0 +1,93 @@
---
name: graph_labels_select
kind: function
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void graph::graph_compute_degrees(const GraphData& g, int* out_degrees); int graph::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)"
description: "Helpers puros (sin ImGui ni OpenGL) que calculan los nodos a etiquetar cada frame: graph_compute_degrees calcula el grado por nodo (solo aristas EF_VISIBLE), graph_labels_select aplica frustum cull + always_for_* + top-N por (size * (degree+1)) sobre min_node_pixel_size."
tags: [graph, labels, pure, culling, top-n, frustum, testable]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: ""
imports: []
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_select.cpp"
framework: imgui
params:
- name: g
desc: "Grafo a procesar (lectura). Se respetan NF_VISIBLE, NF_SELECTED, NF_HOVERED, NF_PINNED, EF_VISIBLE."
- name: out_degrees
desc: "(compute_degrees) Array de tamaño node_count que recibe el grado por nodo. Cuenta solo aristas con EF_VISIBLE."
- name: p
desc: "LabelPolicy: max_visible (top-N), always_for_*, min_zoom_for_all, min_node_pixel_size."
- name: cam_x
desc: "Centro de la camara en world coords (X)."
- name: cam_y
desc: "Centro de la camara en world coords (Y)."
- name: zoom
desc: "Pixels por unidad world. zoom <= 0 → no-op."
- name: widget_w
desc: "Ancho del widget en pixeles."
- name: widget_h
desc: "Alto del widget en pixeles."
- name: degrees
desc: "Array de tamaño node_count con el grado por nodo. NULL → score = size sin factor degree."
- name: out_indices
desc: "Array de salida con los indices de nodos a etiquetar. Capacidad = out_capacity."
- name: out_capacity
desc: "Capacidad maxima de out_indices. La funcion no escribe mas alla."
output: "graph_compute_degrees: void. graph_labels_select: numero de indices escritos en out_indices. Cap duro = max_visible + |always_*|."
notes: "Issue 0049j. TU separado de graph_labels.cpp para que los tests unitarios puedan cubrir la seleccion de candidatos sin pintar nada (graph_labels.cpp depende de ImGui). Estrategia: (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. El degree se puede precalcular (O(E)) y pasar al select para evitar recomputar."
---
# graph_labels_select
Logica pura de seleccion de etiquetas para `graph_labels`. Vive en su propio TU
(`graph_labels_select.cpp`) — separada de `graph_labels.cpp` (que tiene la parte
de pintado con ImGui) — para que los tests unitarios puedan ejercerla sin abrir
una ventana.
## API
```cpp
namespace graph {
// Grado por nodo (cuenta solo aristas EF_VISIBLE). O(E).
void graph_compute_degrees(const GraphData& g, int* out_degrees);
// Devuelve los indices de los nodos a etiquetar este frame.
// O(N) frustum cull + O(K log K) si K candidatos > max_visible.
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);
}
```
## Composabilidad
- Lo usa `graph_labels_draw` y `graph_labels_draw_at` internamente cada frame.
- Una app puede llamarlo directamente para pintar etiquetas con su propio
renderer (no-ImGui) reusando la misma politica.
- Sin dependencias de ImGui ni OpenGL — puro `<algorithm>` + `<vector>`.
## Estrategia (resumen)
1. **Pase A — always_***: nodos selected/hovered/pinned visibles en viewport.
Skip de `min_node_pixel_size`. Off-screen NO entra (decision documentada).
2. **Pase B — top-N**: del resto de nodos visibles en viewport con
`size * zoom >= min_node_pixel_size`, ordena por `score = size * (degree+1)`
y coge los `max_visible` primeros (`std::partial_sort`).
3. **Cap duro**: `max_visible + |always_*|`. Imposible saltarse — si quieres 0
labels excepto always, pon `max_visible = 0`.
## Tests
Cubiertos en `cpp/tests/test_graph_labels.cpp`. La funcion devuelve int sin
tocar ImGui — pasable a `assert(graph_labels_select(...) == N)`.