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
+49 -18
View File
@@ -14,6 +14,33 @@
namespace gallery {
// Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la
// tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo
// queda demostrado tal cual lo van a usar las apps reales (osint_graph,
// fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id.
static const uint32_t k_demo_palette[] = {
0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u,
0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u,
};
static constexpr int k_demo_palette_n =
sizeof(k_demo_palette) / sizeof(k_demo_palette[0]);
// Tabla compartida entre regeneraciones — las apariencias no cambian aunque
// el usuario regenere el grafo, asi que vive como `static`.
static EntityType s_demo_entity_types[k_demo_palette_n];
static RelationType s_demo_relation_types[1];
static bool s_demo_types_initialized = false;
static void init_demo_types() {
if (s_demo_types_initialized) return;
for (int k = 0; k < k_demo_palette_n; ++k) {
s_demo_entity_types[k] = entity_type(k_demo_palette[k],
SHAPE_CIRCLE, 4.0f, "cluster");
}
s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default");
s_demo_types_initialized = true;
}
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
@@ -34,13 +61,6 @@ static void generate_synthetic_graph(int N, int K,
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
};
// Paleta por cluster (ABGR)
const uint32_t palette[] = {
0xff5b8def, 0xff58ca8c, 0xfff5973e, 0xffd95150,
0xffb87fe0, 0xff5fcdcc, 0xfff2cd52, 0xff99d161,
};
const int palette_n = sizeof(palette) / sizeof(palette[0]);
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
@@ -57,12 +77,16 @@ static void generate_synthetic_graph(int N, int K,
for (int i = 0; i < N; i++) {
int k = i % K;
GraphNode n = graph_node(static_cast<uint32_t>(i),
// type_id mapea al EntityType (k % k_demo_palette_n) que define
// color y shape. size_override = 3..5 px para conservar la
// variacion sutil del demo v1 — apariencia visual identica.
uint16_t tid = static_cast<uint16_t>(k % k_demo_palette_n);
GraphNode n = graph_node(
cluster_cx[k] + (rnd() - 0.5f) * scatter,
cluster_cy[k] + (rnd() - 0.5f) * scatter);
n.size = 3.0f + rnd() * 2.0f;
n.color = palette[k % palette_n];
n.community = static_cast<uint32_t>(k);
cluster_cy[k] + (rnd() - 0.5f) * scatter,
tid);
n.size_override = 3.0f + rnd() * 2.0f;
n.user_data = static_cast<uint64_t>(i);
nodes_out.push_back(n);
}
@@ -114,13 +138,20 @@ void demo_graph() {
static bool s_needs_regen = true;
if (s_needs_regen) {
init_demo_types();
generate_synthetic_graph(s_n_nodes, s_n_clusters,
s_edges_per_n, s_inter_pct,
s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = static_cast<int>(s_nodes.size());
s_graph.edges = s_edges.data();
s_graph.edge_count = static_cast<int>(s_edges.size());
s_graph.nodes = s_nodes.data();
s_graph.node_count = static_cast<int>(s_nodes.size());
s_graph.node_capacity = static_cast<int>(s_nodes.capacity());
s_graph.edges = s_edges.data();
s_graph.edge_count = static_cast<int>(s_edges.size());
s_graph.edge_capacity = static_cast<int>(s_edges.capacity());
s_graph.types = s_demo_entity_types;
s_graph.type_count = k_demo_palette_n;
s_graph.rel_types = s_demo_relation_types;
s_graph.rel_type_count = 1;
s_graph.update_bounds();
s_state.layout_running = true;
s_state.layout_energy = 0.0f;
@@ -168,9 +199,9 @@ void demo_graph() {
char hover_buf[32];
char sel_buf[32];
if (s_state.hovered_node >= 0) {
std::snprintf(hover_buf, sizeof(hover_buf), "#%d c%u",
std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u",
s_state.hovered_node,
s_nodes[s_state.hovered_node].community);
(unsigned)s_nodes[s_state.hovered_node].type_id);
} else {
std::snprintf(hover_buf, sizeof(hover_buf), "-");
}