feat(viz): graph_types modelo extendido + EntityType/RelationType + flags (issue 0049e)

Extiende el modelo agnostico de graph_types.h para soportar shapes/iconos/
filtros/labels/streaming sin acoplar a backend. Migra el unico consumer
(demos_graph) en el mismo cambio.

- GraphNode v2: type_id + shape_override/color_override/size_override +
  flags (NF_PINNED/VISIBLE/SELECTED/HOVERED) + label_idx + user_data.
- GraphEdge v2: type_id + style_override + flags (EF_DIRECTED/VISIBLE).
- EntityType / RelationType: tablas en GraphData (types, rel_types).
- Helpers de resolucion (resolve_node_color/shape/size, resolve_edge_*)
  y constructores ergonomicos (graph_node, graph_edge, entity_type,
  relation_type) — sentinel-based para herencia automatica del tipo.
- graph_renderer v1.4: lee NF_VISIBLE / EF_VISIBLE, resuelve apariencia
  via override → EntityType → fallback indexado por type_id. Skipea
  aristas con endpoints invisibles. Shapes siguen pintandose como
  circulo (0049f cableara el dispatch real).
- graph_force_layout v1.2: pinned ahora vive en flags & NF_PINNED.
- graph_viewport v1.1: hover/seleccion publican NF_HOVERED/SELECTED en
  el grafo (clear-then-set). Drag usa NF_PINNED. Tooltip muestra Type/
  user_data en lugar de community/value/label.
- demos_graph: 8 EntityType (paleta antigua) + 1 RelationType. type_id
  por cluster. user_data = indice numerico del nodo. Apariencia visual
  identica al pre-cambio.
- test_graph_types.cpp: 12 casos cubriendo helpers, defaults, bitmask
  manipulation y resoluciones override-vs-EntityType. test_graph_edge_
  static actualizado al nuevo modelo (ya no tiene .color directo).
- 4 .md de tipos nuevos (graph_node, graph_edge, entity_type,
  relation_type) + GraphData v2.0 actualizado.

Tests: 31/31 ctest verdes (incluye test_visual golden).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 22:44:40 +02:00
parent ae47b76d0c
commit c29428a187
19 changed files with 756 additions and 177 deletions
+4
View File
@@ -70,6 +70,10 @@ add_fn_test(test_graph_should_pause test_graph_should_pause.cpp
add_fn_test(test_graph_edge_static test_graph_edge_static.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../functions/viz/graph_types.cpp)
# --- Issue 0049e — modelo de grafo extendido (GraphNode/GraphEdge + tipos) --
add_fn_test(test_graph_types test_graph_types.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../functions/viz/graph_types.cpp)
# --- Visual golden-image diff (issue 0048) ---------------------------------
# El binario primitives_gallery se compila con --capture; el test compara los
# PNGs generados con los goldens en cpp/tests/golden/. Si no hay goldens o el
+10 -5
View File
@@ -50,11 +50,16 @@ TEST_CASE("Fallback gris 0x88 tiene R en el byte LSB", "[viz][edge_static]") {
REQUIRE(((gray >> 24) & 0xFFu) == 0xFFu); // A
}
TEST_CASE("GraphEdge.color = 0 indica fallback al gris por defecto",
TEST_CASE("graph_edge() default deja flags=EF_VISIBLE y type_id=0",
"[viz][edge_static]") {
// Modelo extendido (issue 0049e): GraphEdge ya no tiene `color` directo —
// el color sale de RelationType via type_id. Aqui solo validamos los
// defaults del helper para que el renderer pueda dibujar la arista
// (EF_VISIBLE encendido) y que el fallback al RelationType funcione
// con type_id = 0.
GraphEdge e = graph_edge(0, 1, 1.0f);
REQUIRE(e.color == 0u);
// El renderer interpreta esto como "usa el gris 0x88,0x88,0x88,0xFF".
// Un agente que precarga colores debe usar `pack_rgba8` para evitar
// colisionar con el sentinel 0.
REQUIRE(e.flags == EF_VISIBLE);
REQUIRE(e.type_id == 0);
REQUIRE(e.style_override == EDGE_USE_TYPE);
REQUIRE(e.weight == 1.0f);
}
+197
View File
@@ -0,0 +1,197 @@
// Unit tests para el modelo extendido de graph_types (issue 0049e).
// Cubre: helpers, defaults, manipulacion de flags, lookup color/shape/size
// con y sin override sobre la tabla EntityType / RelationType.
#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"
#include "viz/graph_types.h"
#include <cstdint>
TEST_CASE("graph_node() defaults", "[viz][graph_types]") {
GraphNode n = graph_node();
REQUIRE(n.x == 0.0f);
REQUIRE(n.y == 0.0f);
REQUIRE(n.vx == 0.0f);
REQUIRE(n.vy == 0.0f);
REQUIRE(n.type_id == 0);
REQUIRE(n.shape_override == SHAPE_USE_TYPE);
REQUIRE(n.flags == NF_VISIBLE);
REQUIRE(n.color_override == 0u);
REQUIRE(n.size_override == 0.0f);
REQUIRE(n.label_idx == 0u);
REQUIRE(n.user_data == 0ull);
}
TEST_CASE("graph_node() acepta type_id custom", "[viz][graph_types]") {
GraphNode n = graph_node(10.0f, 20.0f, 7);
REQUIRE(n.x == 10.0f);
REQUIRE(n.y == 20.0f);
REQUIRE(n.type_id == 7);
REQUIRE((n.flags & NF_VISIBLE) != 0);
}
TEST_CASE("graph_edge() defaults", "[viz][graph_types]") {
GraphEdge e = graph_edge(3, 5);
REQUIRE(e.source == 3);
REQUIRE(e.target == 5);
REQUIRE(e.weight == 1.0f);
REQUIRE(e.type_id == 0);
REQUIRE(e.style_override == EDGE_USE_TYPE);
REQUIRE(e.flags == EF_VISIBLE);
REQUIRE(e.label_idx == 0u);
}
TEST_CASE("Manipulacion de NodeFlags es bitmask", "[viz][graph_types][flags]") {
GraphNode n = graph_node();
REQUIRE((n.flags & NF_PINNED) == 0);
n.flags |= NF_PINNED;
REQUIRE((n.flags & NF_PINNED) != 0);
REQUIRE((n.flags & NF_VISIBLE) != 0); // VISIBLE no cambia
n.flags |= NF_HOVERED | NF_SELECTED;
REQUIRE((n.flags & NF_PINNED) != 0);
REQUIRE((n.flags & NF_HOVERED) != 0);
REQUIRE((n.flags & NF_SELECTED) != 0);
n.flags &= ~NF_HOVERED;
REQUIRE((n.flags & NF_HOVERED) == 0);
REQUIRE((n.flags & NF_SELECTED) != 0);
}
TEST_CASE("Manipulacion de EdgeFlags es bitmask", "[viz][graph_types][flags]") {
GraphEdge e = graph_edge(0, 1);
REQUIRE((e.flags & EF_DIRECTED) == 0);
e.flags |= EF_DIRECTED;
REQUIRE((e.flags & EF_DIRECTED) != 0);
REQUIRE((e.flags & EF_VISIBLE) != 0);
e.flags &= ~EF_VISIBLE;
REQUIRE((e.flags & EF_VISIBLE) == 0);
REQUIRE((e.flags & EF_DIRECTED) != 0);
}
TEST_CASE("resolve_node_color: override gana sobre EntityType",
"[viz][graph_types][resolve]") {
EntityType types[] = {
entity_type(0xFFAABBCCu, SHAPE_CIRCLE, 5.0f, "a"),
entity_type(0xFF112233u, SHAPE_SQUARE, 6.0f, "b"),
};
GraphNode n = graph_node(0, 0, 1);
// Sin override: usa el tipo
REQUIRE(resolve_node_color(n, types, 2) == 0xFF112233u);
// Con override: gana siempre
n.color_override = 0xFFFFFFFFu;
REQUIRE(resolve_node_color(n, types, 2) == 0xFFFFFFFFu);
// Sin tabla: cae al fallback gris
GraphNode m = graph_node();
REQUIRE(resolve_node_color(m, nullptr, 0) == 0xFF888888u);
}
TEST_CASE("resolve_node_shape: override y sentinel",
"[viz][graph_types][resolve]") {
EntityType types[] = {
entity_type(0xFFAAAAAAu, SHAPE_HEX, 5.0f),
};
GraphNode n = graph_node(0, 0, 0);
// shape_override = SHAPE_USE_TYPE (0) → usa el tipo
REQUIRE(resolve_node_shape(n, types, 1) == SHAPE_HEX);
// override no-cero → gana
n.shape_override = SHAPE_DIAMOND;
REQUIRE(resolve_node_shape(n, types, 1) == SHAPE_DIAMOND);
// type_id fuera de rango con override → respeta override
n.type_id = 99;
REQUIRE(resolve_node_shape(n, types, 1) == SHAPE_DIAMOND);
// type_id fuera de rango sin override → fallback CIRCLE
n.shape_override = SHAPE_USE_TYPE;
REQUIRE(resolve_node_shape(n, types, 1) == SHAPE_CIRCLE);
}
TEST_CASE("resolve_node_size: override > 0 gana, <=0 hereda",
"[viz][graph_types][resolve]") {
EntityType types[] = { entity_type(0u, SHAPE_CIRCLE, 8.0f) };
GraphNode n = graph_node();
REQUIRE(resolve_node_size(n, types, 1) == 8.0f);
n.size_override = 12.0f;
REQUIRE(resolve_node_size(n, types, 1) == 12.0f);
// valor "negativo" se trata como sentinel → hereda
n.size_override = -1.0f;
REQUIRE(resolve_node_size(n, types, 1) == 8.0f);
// sin tabla y sin override → fallback 4.0
GraphNode m = graph_node();
REQUIRE(resolve_node_size(m, nullptr, 0) == 4.0f);
}
TEST_CASE("resolve_edge_color/style/width siguen al RelationType",
"[viz][graph_types][resolve]") {
RelationType rels[] = {
relation_type(0xFF445566u, EDGE_DASHED, 2.5f, "owns"),
};
GraphEdge e = graph_edge(0, 1);
REQUIRE(resolve_edge_color(e, rels, 1) == 0xFF445566u);
REQUIRE(resolve_edge_style(e, rels, 1) == EDGE_DASHED);
REQUIRE(resolve_edge_width(e, rels, 1) == 2.5f);
e.style_override = EDGE_DOTTED;
REQUIRE(resolve_edge_style(e, rels, 1) == EDGE_DOTTED);
// Sin tabla: defaults razonables
REQUIRE(resolve_edge_color(e, nullptr, 0) == 0xFF888888u);
REQUIRE(resolve_edge_width(e, nullptr, 0) == 1.0f);
}
TEST_CASE("find_node_by_user_data busca lineal", "[viz][graph_types]") {
GraphNode nodes[3];
nodes[0] = graph_node(); nodes[0].user_data = 100ull;
nodes[1] = graph_node(); nodes[1].user_data = 200ull;
nodes[2] = graph_node(); nodes[2].user_data = 300ull;
GraphData g{};
g.nodes = nodes;
g.node_count = 3;
REQUIRE(g.find_node_by_user_data(200ull) == 1);
REQUIRE(g.find_node_by_user_data(300ull) == 2);
REQUIRE(g.find_node_by_user_data(999ull) == -1);
}
TEST_CASE("update_bounds calcula AABB", "[viz][graph_types]") {
GraphNode nodes[3];
nodes[0] = graph_node(-5.0f, -3.0f);
nodes[1] = graph_node( 4.0f, 7.0f);
nodes[2] = graph_node( 1.0f, 0.0f);
GraphData g{};
g.nodes = nodes;
g.node_count = 3;
g.update_bounds();
REQUIRE(g.min_x == -5.0f);
REQUIRE(g.max_x == 4.0f);
REQUIRE(g.min_y == -3.0f);
REQUIRE(g.max_y == 7.0f);
}
TEST_CASE("Tamaños esperados (presupuesto memoria)", "[viz][graph_types]") {
// GraphNode: 40 bytes en x86_64 — para 100k nodos = ~4 MB. Si crece
// accidentalmente, este test alerta antes de mergear.
REQUIRE(sizeof(GraphNode) == 40);
// GraphEdge: 20 bytes — los uint8_t style/flags caben en el hueco de 2
// bytes que dejaria el alineado de uint16_t type_id, sin padding extra.
REQUIRE(sizeof(GraphEdge) == 20);
}