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:
@@ -4,12 +4,32 @@ Cuando un .cpp llama a otra funcion del registry, el `.md` del CONSUMIDOR
|
|||||||
debe anadir la dependencia a `uses_functions`. El indexer NO lo deduce
|
debe anadir la dependencia a `uses_functions`. El indexer NO lo deduce
|
||||||
automaticamente para C++ (parser no trivial).
|
automaticamente para C++ (parser no trivial).
|
||||||
|
|
||||||
Como auditar:
|
Como auditar (funciones huerfanas):
|
||||||
sqlite3 registry.db "SELECT id FROM functions WHERE lang='cpp' AND uses_functions='[]';"
|
sqlite3 registry.db "SELECT id FROM functions WHERE lang='cpp' AND uses_functions='[]';"
|
||||||
|
|
||||||
|
Como auditar (drift entre `CMakeLists.txt` y `app.md`):
|
||||||
|
- Cruzar los `${CMAKE_SOURCE_DIR}/functions/<dom>/<name>.cpp` listados en el
|
||||||
|
`CMakeLists.txt` con el `uses_functions` del `app.md`. Cada `.cpp` linkado
|
||||||
|
debe aparecer como `<name>_cpp_<dom>` en el `.md`. Excepciones: ver mas abajo.
|
||||||
|
|
||||||
Convencion:
|
Convencion:
|
||||||
- Funciones de framework (cpp/framework/) no estan indexadas — sus consumos
|
- **Framework code** (`cpp/framework/app_base.cpp`) — no esta indexado.
|
||||||
se anotan en `notes:` del huerfano (no en uses_functions).
|
- **Funciones bundled en `fn_framework`** — son funciones del registry cuyo
|
||||||
- Apps (`cpp/apps/`, `projects/*/apps/`) no estan en el grafo de uses_functions
|
`.cpp` se compila dentro del static lib `fn_framework` (lista en
|
||||||
del registry — los huerfanos solo consumidos por apps llevan nota en `notes:`.
|
`cpp/CMakeLists.txt`, target `add_library(fn_framework STATIC ...)`):
|
||||||
- DEMO_ONLY en primitives_gallery se etiqueta `notes: scaffolding/demo`.
|
`tokens`, `icon_font`, `app_settings`, `app_about`, `fps_overlay`,
|
||||||
|
`panel_menu`, `app_menubar`, `layouts_menu`, `logger`, `log_window`,
|
||||||
|
`gl_loader`, `layout_storage`, `selectable_text`. Las apps las usan
|
||||||
|
transitivamente (incluyen `core/logger.h`, llaman `fn_log::log_info`),
|
||||||
|
pero NO listan estos `.cpp` en su `CMakeLists.txt` (multiple-definition)
|
||||||
|
ni los declaran en `uses_functions` del `app.md`. Excepcion: si una app
|
||||||
|
toca una API que no este en fn_framework (raro), declara la dep.
|
||||||
|
- **TU adicional de un parent function** (ej. `graph_labels_select.cpp` que
|
||||||
|
va con `graph_labels.cpp`) — desde 2026-05-04 se registra como entrada
|
||||||
|
propia con su `.md` (ver ADR 0003). El parent declara la nueva entrada
|
||||||
|
en su `uses_functions`. Las apps que enlazan ambos `.cpp` listan ambas
|
||||||
|
IDs en `uses_functions` del `app.md`.
|
||||||
|
- **Apps** (`apps/`, `cpp/apps/`, `projects/*/apps/`) son leaves del grafo:
|
||||||
|
declaran `uses_functions` en `app.md` pero ninguna funcion del registry
|
||||||
|
las cita.
|
||||||
|
- DEMO_ONLY en `primitives_gallery` se etiqueta `notes: scaffolding/demo`.
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ lang: cpp
|
|||||||
domain: viz
|
domain: viz
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
purity: impure
|
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*); int graph::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); void graph::graph_compute_degrees(const GraphData&, int* out_degrees)"
|
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."
|
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]
|
tags: [graph, labels, imdrawlist, viewport, osint, culling, top-n]
|
||||||
uses_functions: []
|
uses_functions: ["graph_labels_select_cpp_viz"]
|
||||||
uses_types: ["GraphData_cpp_viz"]
|
uses_types: ["GraphData_cpp_viz"]
|
||||||
returns: []
|
returns: []
|
||||||
returns_optional: false
|
returns_optional: false
|
||||||
@@ -152,3 +152,24 @@ graph::graph_labels_draw(graph, state, policy, get_label, &ctx);
|
|||||||
- El callback se invoca como mucho una vez por nodo etiquetado por frame.
|
- El callback se invoca como mucho una vez por nodo etiquetado por frame.
|
||||||
- Para reproducibilidad en tests, usar `graph_labels_select` con un buffer
|
- Para reproducibilidad en tests, usar `graph_labels_select` con un buffer
|
||||||
de capacidad conocida — no toca ImGui.
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -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)`.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: graph_types
|
||||||
|
kind: function
|
||||||
|
lang: cpp
|
||||||
|
domain: viz
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "void GraphData::update_bounds(); int GraphData::find_node_by_user_data(uint64_t user_data) const"
|
||||||
|
description: "Implementacion de los metodos miembro de GraphData declarados en graph_types.h: update_bounds recalcula el bounding box (min_x/min_y/max_x/max_y) en O(N) recorriendo todos los nodos; find_node_by_user_data busca linealmente el primer nodo cuyo user_data coincide y devuelve su indice (o -1)."
|
||||||
|
tags: [graph, types, bounds, lookup, pure]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: ["GraphData_cpp_viz", "GraphNode_cpp_viz"]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: []
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "cpp/functions/viz/graph_types.cpp"
|
||||||
|
framework: imgui
|
||||||
|
params:
|
||||||
|
- name: user_data
|
||||||
|
desc: "(find_node_by_user_data) Identificador opaco del nodo. La app suele usarlo como FK al backend (ej: rowid de operations.db). Devuelve -1 si no encuentra coincidencia."
|
||||||
|
output: "update_bounds: void (mutates min_x, min_y, max_x, max_y in place; con node_count==0 los pone todos a 0). find_node_by_user_data: indice del primer nodo coincidente, o -1."
|
||||||
|
notes: "TU de implementacion del tipo GraphData (declarado en cpp/functions/viz/graph_types.h, indexado como tipo en types/viz/graph_types.md). Toda app que use GraphData debe enlazar este .cpp ademas de los .cpp de las funciones que opera sobre el grafo. find_node_by_user_data es O(N): para hot paths (lookup repetido por user_data) la app deberia mantener su propio std::unordered_map<uint64_t,int> en paralelo."
|
||||||
|
---
|
||||||
|
|
||||||
|
# graph_types (impl)
|
||||||
|
|
||||||
|
Implementaciones de los metodos miembro de `GraphData` declarados en
|
||||||
|
`graph_types.h`. Pareja obligatoria del tipo — toda app que linkee con
|
||||||
|
funciones que aceptan `GraphData&` debe incluir este `.cpp` en su build.
|
||||||
|
|
||||||
|
## Por que existe como entrada del registry
|
||||||
|
|
||||||
|
El header `graph_types.h` se indexa como tipo (`GraphData_cpp_viz`,
|
||||||
|
`GraphNode_cpp_viz`, etc. en `types/viz/`). Pero las definiciones de los
|
||||||
|
metodos miembro viven en `.cpp` aparte para evitar inlinear la implementacion
|
||||||
|
en cada TU que incluye el header. Sin un `.md` que lo registre, una app nueva
|
||||||
|
que consultara el registry recibiria solo `graph_types.h` y al compilar fallaria
|
||||||
|
con "undefined reference to `GraphData::update_bounds()`".
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct GraphData {
|
||||||
|
void update_bounds(); // O(N) sobre todos los nodos
|
||||||
|
int find_node_by_user_data(uint64_t ud) const; // O(N), -1 si no esta
|
||||||
|
// ... resto de campos en graph_types.h
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composabilidad
|
||||||
|
|
||||||
|
Toda funcion que muta nodos y necesita bounds frescas (force layouts, viewport
|
||||||
|
con auto-fit) llama `update_bounds()` despues. Toda app que mapea filas de DB
|
||||||
|
a nodos usa `find_node_by_user_data` para resolver IDs.
|
||||||
@@ -233,12 +233,22 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
|
|||||||
}
|
}
|
||||||
state.hovered_node = -1;
|
state.hovered_node = -1;
|
||||||
if (hovered && graph.node_count > 0) {
|
if (hovered && graph.node_count > 0) {
|
||||||
float hit_radius = 10.0f / state.zoom;
|
// Spatial query con un radio amplio para descartar lo lejano; despues
|
||||||
int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius);
|
// refinamos comparando con el tamaño real (en world units) del nodo
|
||||||
if (nearest >= 0) {
|
// candidato + un pequeño margen, asi el hover solo se activa cuando el
|
||||||
state.hovered_node = nearest;
|
// raton esta efectivamente sobre el nodo.
|
||||||
graph.nodes[nearest].flags |= NF_HOVERED;
|
float coarse_radius = 24.0f / state.zoom;
|
||||||
interacted = true;
|
float dist = 0.0f;
|
||||||
|
int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, coarse_radius, &dist);
|
||||||
|
if (nearest >= 0 && nearest < graph.node_count) {
|
||||||
|
float node_r_px = resolve_node_size(graph.nodes[nearest],
|
||||||
|
graph.types, graph.type_count);
|
||||||
|
float hit_r_world = (node_r_px + 2.0f) / state.zoom; // 2 px margen
|
||||||
|
if (dist <= hit_r_world) {
|
||||||
|
state.hovered_node = nearest;
|
||||||
|
graph.nodes[nearest].flags |= NF_HOVERED;
|
||||||
|
interacted = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ purity: impure
|
|||||||
signature: "bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size)"
|
signature: "bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size)"
|
||||||
description: "Widget ImGui completo para visualizacion interactiva de grafos con pan, zoom, hover, seleccion y layout en vivo"
|
description: "Widget ImGui completo para visualizacion interactiva de grafos con pan, zoom, hover, seleccion y layout en vivo"
|
||||||
tags: [graph, viewport, imgui, interactive, pan, zoom, dashboard, lasso, multi-select]
|
tags: [graph, viewport, imgui, interactive, pan, zoom, dashboard, lasso, multi-select]
|
||||||
uses_functions: ["graph_force_layout_cpp_viz", "graph_renderer_cpp_viz", "graph_spatial_hash_cpp_core"]
|
uses_functions: ["graph_force_layout_cpp_viz", "graph_renderer_cpp_viz", "graph_spatial_hash_cpp_core", "graph_viewport_selection_cpp_viz"]
|
||||||
tested: true
|
tested: true
|
||||||
tests: ["selection add/clear/toggle/is_selected", "out-of-range indices ignored"]
|
tests: ["selection add/clear/toggle/is_selected", "out-of-range indices ignored"]
|
||||||
test_file_path: "cpp/tests/test_graph_viewport.cpp"
|
test_file_path: "cpp/tests/test_graph_viewport.cpp"
|
||||||
@@ -133,3 +133,15 @@ El renderer OpenGL y el spatial hash se crean en el primer frame. La camara se a
|
|||||||
- La textura del renderer se muestra con UV volteado en Y (`ImVec2(0,1)` a `ImVec2(1,0)`) para corregir la convencion de coordenadas de OpenGL vs ImGui.
|
- La textura del renderer se muestra con UV volteado en Y (`ImVec2(0,1)` a `ImVec2(1,0)`) para corregir la convencion de coordenadas de OpenGL vs ImGui.
|
||||||
- El spatial hash se reconstruye cada frame desde las posiciones actuales de los nodos, garantizando hit-testing correcto despues de drag o layout.
|
- El spatial hash se reconstruye cada frame desde las posiciones actuales de los nodos, garantizando hit-testing correcto despues de drag o layout.
|
||||||
- El zoom hacia el cursor mantiene el punto del grafo bajo el cursor fijo en pantalla ajustando `cam_x`/`cam_y`.
|
- El zoom hacia el cursor mantiene el punto del grafo bajo el cursor fijo en pantalla ajustando `cam_x`/`cam_y`.
|
||||||
|
|
||||||
|
## Split de TU (2026-05-04, ADR 0003)
|
||||||
|
|
||||||
|
Los helpers puros `graph_viewport_clear_selection`, `graph_viewport_is_selected`,
|
||||||
|
`graph_viewport_add_to_selection` y `graph_viewport_toggle_selection` viven en
|
||||||
|
`graph_viewport_selection.cpp` y se indexan como entrada propia
|
||||||
|
`graph_viewport_selection_cpp_viz`. Esta entrada solo cubre el widget completo
|
||||||
|
con pan/zoom/click (impuro, ImGui).
|
||||||
|
|
||||||
|
Apps que reusan `graph_viewport` enlazan ambos `.cpp` y declaran ambas IDs en
|
||||||
|
su `app.md`. Apps que solo necesitan la maquinaria de seleccion (poco probable)
|
||||||
|
pueden declarar solo `graph_viewport_selection_cpp_viz`.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: graph_viewport_selection
|
||||||
|
kind: function
|
||||||
|
lang: cpp
|
||||||
|
domain: viz
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "void graph_viewport_clear_selection(GraphData& graph, GraphViewportState& state); bool graph_viewport_is_selected(const GraphViewportState& state, int node_idx); void graph_viewport_add_to_selection(GraphData& graph, GraphViewportState& state, int node_idx); void graph_viewport_toggle_selection(GraphData& graph, GraphViewportState& state, int node_idx)"
|
||||||
|
description: "Helpers puros (sin ImGui ni OpenGL) para gestionar la multi-seleccion del viewport: clear, add, toggle, is_selected. Mantienen sincronizados state.selection (vector de indices) y nodes[i].flags (NF_SELECTED bit), ademas de state.selected_node como ultimo focus."
|
||||||
|
tags: [graph, viewport, selection, pure, multi-select, testable]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: ["GraphData_cpp_viz"]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: []
|
||||||
|
tested: true
|
||||||
|
tests: ["selection add/clear/toggle/is_selected", "out-of-range indices ignored"]
|
||||||
|
test_file_path: "cpp/tests/test_graph_viewport.cpp"
|
||||||
|
file_path: "cpp/functions/viz/graph_viewport_selection.cpp"
|
||||||
|
framework: imgui
|
||||||
|
params:
|
||||||
|
- name: graph
|
||||||
|
desc: "GraphData. Se modifica para setear/limpiar el bit NF_SELECTED en nodes[i].flags."
|
||||||
|
- name: state
|
||||||
|
desc: "GraphViewportState. Se modifica state.selection (vector de indices) y state.selected_node."
|
||||||
|
- name: node_idx
|
||||||
|
desc: "Indice del nodo. Si esta fuera de rango (negativo o >= node_count) la funcion es no-op."
|
||||||
|
output: "graph_viewport_is_selected: bool (true si el indice esta en state.selection). El resto: void. selected_node se actualiza al ultimo indice añadido, o al ultimo de la lista al togglear, o -1 si la lista queda vacia."
|
||||||
|
notes: "Issue 0049i. TU separado de graph_viewport.cpp para que los tests unitarios cubran la maquinaria de seleccion sin abrir ventana ImGui. Operaciones idempotentes: añadir un indice ya seleccionado es no-op; togglear lo elimina. Las apps que necesiten seleccion programatica (ej: 'select all of type X') deben usar estas funciones en lugar de tocar state.selection a mano."
|
||||||
|
---
|
||||||
|
|
||||||
|
# graph_viewport_selection
|
||||||
|
|
||||||
|
Helpers puros para multi-seleccion en `graph_viewport`. Viven en su propio TU
|
||||||
|
para que los tests unitarios puedan verificar invariantes (clear deja todo a
|
||||||
|
NF_SELECTED=0, toggle es involutivo, etc.) sin abrir ventana ImGui.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void graph_viewport_clear_selection (GraphData& graph, GraphViewportState& state);
|
||||||
|
bool graph_viewport_is_selected (const GraphViewportState& state, int node_idx);
|
||||||
|
void graph_viewport_add_to_selection(GraphData& graph, GraphViewportState& state, int node_idx);
|
||||||
|
void graph_viewport_toggle_selection(GraphData& graph, GraphViewportState& state, int node_idx);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invariantes
|
||||||
|
|
||||||
|
- `state.selection` y `nodes[i].flags & NF_SELECTED` siempre coherentes — no
|
||||||
|
modificar uno sin el otro. Estas funciones lo garantizan.
|
||||||
|
- `state.selected_node` es el "ultimo focus": el ultimo añadido o, al togglear
|
||||||
|
fuera, el ultimo de la lista; `-1` si la lista esta vacia.
|
||||||
|
- Indices fuera de rango son no-op (no escriben memoria, no añaden basura).
|
||||||
|
|
||||||
|
## Composabilidad
|
||||||
|
|
||||||
|
`graph_viewport` los usa internamente al click/Ctrl+click/Esc. Apps externas
|
||||||
|
los usan para seleccion programatica:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Seleccionar todos los nodos de tipo Person
|
||||||
|
graph_viewport_clear_selection(g, state);
|
||||||
|
for (int i = 0; i < g.node_count; ++i) {
|
||||||
|
if (g.nodes[i].type_id == person_type_id) {
|
||||||
|
graph_viewport_add_to_selection(g, state, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Cubiertos en `cpp/tests/test_graph_viewport.cpp` — cubren add/clear/toggle/
|
||||||
|
is_selected y out-of-range.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# ADR 0003 — TUs adicionales de una funcion C++ se registran como entrada propia
|
||||||
|
|
||||||
|
- **Fecha:** 2026-05-04
|
||||||
|
- **Estado:** accepted
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
Algunas funciones C++ del registry necesitan dividir su implementacion en varios `.cpp` por motivos legitimos:
|
||||||
|
|
||||||
|
- **Testabilidad sin ImGui**: `graph_labels` tiene `graph_labels_draw` (depende de `ImDrawList`) y `graph_labels_select` (logica pura). Mantenerlos en el mismo `.cpp` obliga a los tests unitarios a linkear ImGui solo para ejercer la seleccion de candidatos.
|
||||||
|
- **Separacion impl/header**: `graph_types.h` declara `GraphData` (tipo del registry, vive en `types/viz/graph_types.md`). Sus metodos miembro (`update_bounds`, `find_node_by_user_data`) se implementan en `graph_types.cpp` para no inlinear en cada TU consumidor.
|
||||||
|
- **Separacion logica vs render**: `graph_viewport` tiene la mecanica de pan/zoom/click (ImGui) y la mecanica de seleccion (vectores + flags, sin ImGui). Tests unitarios de seleccion no deberian abrir ventana.
|
||||||
|
|
||||||
|
El indexer asume `1 .cpp = 1 .md` — el campo `file_path` del frontmatter es un solo path. Si una funcion partia su impl en varios `.cpp`, los `.cpp` adicionales quedaban como huerfanos: el `CMakeLists.txt` los listaba para enlazar, pero no aparecian en `registry.db`. Resultado: una app nueva que consultaba el registry para reusar `graph_labels` recibia `file_path: graph_labels.cpp` y al compilar fallaba con `undefined reference to graph_labels_select`.
|
||||||
|
|
||||||
|
Esta sesion (2026-05-04) detecto 3 huerfanos asi:
|
||||||
|
|
||||||
|
- `cpp/functions/viz/graph_labels_select.cpp`
|
||||||
|
- `cpp/functions/viz/graph_viewport_selection.cpp`
|
||||||
|
- `cpp/functions/viz/graph_types.cpp`
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Cada TU adicional se registra como una entrada propia del registry con su `.md`**. El parent declara la nueva entrada en `uses_functions`.
|
||||||
|
|
||||||
|
Estructura tras aplicar la decision:
|
||||||
|
|
||||||
|
```
|
||||||
|
cpp/functions/viz/
|
||||||
|
graph_labels.cpp ← parent: graph_labels_draw, graph_labels_draw_at (impure, ImGui)
|
||||||
|
graph_labels.md ← uses_functions: ["graph_labels_select_cpp_viz"]
|
||||||
|
graph_labels_select.cpp ← TU split: graph_compute_degrees, graph_labels_select (pure)
|
||||||
|
graph_labels_select.md ← entrada propia, purity: pure
|
||||||
|
|
||||||
|
graph_viewport.cpp ← parent: widget completo (impure, ImGui)
|
||||||
|
graph_viewport.md ← uses_functions: [..., "graph_viewport_selection_cpp_viz"]
|
||||||
|
graph_viewport_selection.cpp ← TU split: helpers de seleccion (pure)
|
||||||
|
graph_viewport_selection.md ← entrada propia, purity: pure
|
||||||
|
|
||||||
|
graph_types.h ← header del tipo (indexado en types/viz/graph_types.md)
|
||||||
|
graph_types.cpp ← TU impl de metodos miembro
|
||||||
|
graph_types.md ← entrada propia kind: function, purity: pure
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternativas descartadas
|
||||||
|
|
||||||
|
- **Extender `file_path` a `[paths]`** (lista). Romperia la convencion vigente y obligaria a tocar parser, store, schema FTS, sync API y vista del dashboard. Ganancia: ninguna, las apps siguen necesitando la informacion para enlazar.
|
||||||
|
- **`notes:` con instruccion de enlazar el TU adicional**. Frágil: ningun parser lo enforce, las apps lo olvidan.
|
||||||
|
- **Un solo `.cpp` macro con flags de compilacion**. Mantiene los tests acoplados a ImGui, no resuelve el problema raiz.
|
||||||
|
- **Mover el TU puro a `core/`**. Rompe la cohesion del modulo (los helpers son intimos a `graph_labels` / `graph_viewport`) y crea ciclos de includes.
|
||||||
|
|
||||||
|
## Consecuencias
|
||||||
|
|
||||||
|
- Nuevas funciones registradas: `graph_labels_select_cpp_viz`, `graph_viewport_selection_cpp_viz`, `graph_types_cpp_viz`.
|
||||||
|
- `cpp/functions/viz/graph_labels.md` y `cpp/functions/viz/graph_viewport.md` declaran las nuevas entradas en `uses_functions`.
|
||||||
|
- `graph_explorer/app.md` declara explicitamente las 3 entradas en su `uses_functions` — antes solo declaraba el parent y el `.cpp` adicional pasaba "de matute" via `CMakeLists.txt`.
|
||||||
|
- Convencion para futuros splits: si un parent añade un TU `<name>_<sufijo>.cpp` y exporta funciones nuevas, crear `<name>_<sufijo>.md` con `kind: function`, su propia firma, `purity` real (normalmente `pure` ya que ese es el motivo del split), y dependencias correctas. El parent enlaza via `uses_functions`.
|
||||||
|
|
||||||
|
## Aprendizaje
|
||||||
|
|
||||||
|
El registry funciona como API publica: lo que esta indexado define lo que las apps externas pueden consumir. Un `.cpp` que existe en disco pero no en la BD es invisible — y la unica garantia de "una app nueva sabra que enlazar" es que toda dependencia este declarada formalmente. El TU split no es un problema de organizacion del codigo: es un problema de superficie de API que tiene que reflejarse en el contrato del registry.
|
||||||
Reference in New Issue
Block a user