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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user