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 a6e3298f1b
commit b9ffc13caf
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), "-");
}
+8 -8
View File
@@ -265,7 +265,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
// stack local en pila, asi que es thread-safe.
#pragma omp parallel for if(graph.node_count >= 1024) schedule(dynamic, 256)
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
if (graph.nodes[i].flags & NF_PINNED) continue;
quad_force(root,
graph.nodes[i].x, graph.nodes[i].y,
config.theta, config.repulsion, config.min_distance,
@@ -290,15 +290,15 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
float fx_e = force * dx / dist;
float fy_e = force * dy / dist;
if (!graph.nodes[s].pinned) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; }
if (!graph.nodes[t].pinned) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; }
if (!(graph.nodes[s].flags & NF_PINNED)) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; }
if (!(graph.nodes[t].flags & NF_PINNED)) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; }
}
// ---- Gravity toward center (0,0) ----
if (config.gravity != 0.0f) {
#pragma omp parallel for if(graph.node_count >= 1024) schedule(static)
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
if (graph.nodes[i].flags & NF_PINNED) continue;
fx_buf[i] -= config.gravity * graph.nodes[i].x;
fy_buf[i] -= config.gravity * graph.nodes[i].y;
}
@@ -309,7 +309,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
#pragma omp parallel for if(graph.node_count >= 1024) schedule(static) reduction(+:total_energy)
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
n.vx = n.vx * config.damping + fx_buf[i];
n.vy = n.vy * config.damping + fy_buf[i];
@@ -332,7 +332,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
void graph_force_layout_reset(GraphData& graph, float spread) {
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
// rand() produces [0, RAND_MAX]; map to [-spread, spread]
n.x = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
n.y = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
@@ -347,7 +347,7 @@ void graph_layout_circular(GraphData& graph, float radius) {
const float two_pi = 6.28318530718f;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
float angle = two_pi * (float)i / (float)graph.node_count;
n.x = radius * std::cos(angle);
n.y = radius * std::sin(angle);
@@ -370,7 +370,7 @@ void graph_layout_grid(GraphData& graph, float spacing) {
float oy = -0.5f * (rows - 1) * spacing;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
int col = i % cols;
int row = i / cols;
n.x = ox + col * spacing;
+2 -1
View File
@@ -3,7 +3,7 @@ name: graph_force_layout
kind: function
lang: cpp
domain: viz
version: "1.1.0"
version: "1.2.0"
purity: pure
signature: "float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)"
description: "Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada"
@@ -94,6 +94,7 @@ if (running) {
## Notas de version
- **v1.2** (2026-04-29, issue 0049e): el campo `pinned` desaparece del modelo de `GraphNode` y se sustituye por `flags & NF_PINNED`. La logica del integrador, atraccion, gravedad y reset usa el bit equivalente. Sin cambios en la API publica ni en el comportamiento.
- **v1.1** (2026-04-29, issue 0049c): añade el helper puro
`graph_force_layout_should_pause(low_frames, min_consecutive)` para que las apps
detecten convergencia sin replicar el contador por todas partes. Sin cambios en
+44 -16
View File
@@ -13,19 +13,24 @@
#include <algorithm>
// ---------------------------------------------------------------------------
// Community palette (ABGR packed, 10 colors)
// Fallback palette (RGBA8 con R en LSB) — usada solo cuando GraphData::types
// esta vacio. En el modelo extendido (issue 0049e) la apariencia de cada
// nodo viene resuelta por `resolve_node_color()`, que mira primero el
// override del nodo, luego el EntityType de la tabla, y finalmente este
// fallback. Mantener 10 colores para nodos sin tipos sigue siendo util en
// demos que aun no construyen tablas EntityType.
// ---------------------------------------------------------------------------
static const uint32_t k_palette[10] = {
0xFF4CAF50, // green
0xFFF44336, // red
0xFF2196F3, // blue
0xFFFF9800, // orange
0xFF9C27B0, // purple
0xFF00BCD4, // cyan
0xFFFFEB3B, // yellow
0xFFE91E63, // pink
0xFF795548, // brown
0xFF607D8B // blue-grey
static const uint32_t k_fallback_palette[10] = {
0xFF4CAF50u, // green
0xFFF44336u, // red
0xFF2196F3u, // blue
0xFFFF9800u, // orange
0xFF9C27B0u, // purple
0xFF00BCD4u, // cyan
0xFFFFEB3Bu, // yellow
0xFFE91E63u, // pink
0xFF795548u, // brown
0xFF607D8Bu, // blue-grey
};
// ---------------------------------------------------------------------------
@@ -562,8 +567,17 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
const GraphEdge& e = graph.edges[i];
if (e.source >= (uint32_t)graph.node_count) continue;
if (e.target >= (uint32_t)graph.node_count) continue;
uint32_t col = e.color != 0 ? e.color
: pack_rgba8(0x88, 0x88, 0x88, 0xFF);
if (!(e.flags & EF_VISIBLE)) continue;
// Saltamos aristas cuyos endpoints no estan visibles —
// el shader las pintaria igualmente (las posiciones siguen
// en el TBO) pero la aristas tendrian apariencia conectando
// "vacios" si los nodos estan ocultos por NF_VISIBLE off.
if (!(graph.nodes[e.source].flags & NF_VISIBLE)) continue;
if (!(graph.nodes[e.target].flags & NF_VISIBLE)) continue;
uint32_t col = resolve_edge_color(e, graph.rel_types,
graph.rel_type_count);
if (col == 0u) col = pack_rgba8(0x88, 0x88, 0x88, 0xFF);
r->edge_static_staging[out++] = { e.source, e.target, col, 0u };
}
if (out > 0) {
@@ -625,13 +639,27 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
size_t visible = 0;
for (int i = 0; i < graph.node_count; ++i) {
const GraphNode& n = graph.nodes[i];
float sz = n.size > 0.0f ? n.size : 4.0f;
if (!(n.flags & NF_VISIBLE)) continue;
float sz = resolve_node_size(n, graph.types, graph.type_count);
if (sz <= 0.0f) sz = 4.0f;
float half = sz * 0.5f;
// AABB del nodo: centro ± half. Skip si fuera del viewport.
if (n.x + half < vx0 || n.x - half > vx1) continue;
if (n.y + half < vy0 || n.y - half > vy1) continue;
uint32_t ncol = n.color != 0 ? n.color : k_palette[n.community % 10];
// Apariencia: 1) override del nodo, 2) EntityType, 3) fallback
// indexado por type_id (paleta de 10 — sustituye al community
// del modelo v1).
uint32_t ncol;
if (n.color_override != 0u) {
ncol = n.color_override;
} else if (graph.types && n.type_id < (uint16_t)graph.type_count) {
ncol = graph.types[n.type_id].color;
} else {
ncol = k_fallback_palette[n.type_id % 10];
}
// 0049f anadira el dispatch de shape; por ahora todos circulos.
r->node_staging[visible++] = { n.x, n.y, sz, ncol };
}
+2 -1
View File
@@ -3,7 +3,7 @@ name: graph_renderer
kind: function
lang: cpp
domain: viz
version: "1.3.0"
version: "1.4.0"
purity: impure
signature: "GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)"
description: "Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes"
@@ -88,6 +88,7 @@ ndc = (screen / viewport) * 2 - 1
## Notas
- **v1.4** (2026-04-29, issue 0049e): adapta el renderer al modelo extendido de `GraphData`. Lee `n.flags & NF_VISIBLE` para skipear nodos invisibles, resuelve color via `n.color_override``EntityType` → fallback indexado por `type_id`. Aristas: skip si `!(EF_VISIBLE)` o si los endpoints no son visibles, color via `RelationType`. Shapes/iconos/dashed-style siguen como circulo solido — el dispatch real llega en 0049f.
- **v1.3** (2026-04-29, issue 0049d): aristas via vertex pulling. API publica intacta.
- El buffer de aristas pasa a ser estatico (`source_idx, target_idx, color, flags` × E, 16 bytes/arista) y solo se reupload cuando el grafo cambia (detectado por `(edges_ptr, edge_count)` — heuristica suficiente mientras `GraphData` no tenga `revision`). Para 100k aristas: 1.6 MB iniciales vs 4.8 MB/frame del esquema anterior — el upload baja a cero en regimen estable.
- Las posiciones de los nodos se suben cada frame a un Texture Buffer Object `RG32F` (`vec2[]`, 8 bytes/nodo). El vertex shader de aristas hace `texelFetch(u_node_pos, idx)` con `idx` derivado de `gl_VertexID & 1` (0=source, 1=target).
+2 -3
View File
@@ -1,5 +1,4 @@
#include "graph_types.h"
#include <cfloat>
void GraphData::update_bounds() {
if (node_count == 0) {
@@ -16,9 +15,9 @@ void GraphData::update_bounds() {
}
}
int GraphData::find_node(uint32_t id) const {
int GraphData::find_node_by_user_data(uint64_t user_data) const {
for (int i = 0; i < node_count; ++i) {
if (nodes[i].id == id) return i;
if (nodes[i].user_data == user_data) return i;
}
return -1;
}
+177 -27
View File
@@ -1,50 +1,200 @@
#pragma once
#include <cstdint>
// Modelo de grafo extendido (issue 0049e). El sentinel `0` se usa en cada
// campo `*_override` para significar "usar el valor del EntityType/RelationType
// indicado por type_id". Asi un nodo nuevo creado con todos los campos a 0
// hereda automaticamente la apariencia del tipo, sin tener que volver a
// escribir color/size/shape.
// --- Bitmask de flags por nodo. NF_VISIBLE es default tras `graph_node()`.
enum NodeFlags : uint8_t {
NF_NONE = 0,
NF_PINNED = 1 << 0, // el force layout no mueve este nodo
NF_VISIBLE = 1 << 1, // si esta a 0, el renderer salta el nodo
NF_SELECTED = 1 << 2, // marcado por el viewport (click)
NF_HOVERED = 1 << 3, // marcado por el viewport (mouse over)
};
// --- Bitmask de flags por arista. EF_VISIBLE es default tras `graph_edge()`.
enum EdgeFlags : uint8_t {
EF_NONE = 0,
EF_DIRECTED = 1 << 0, // arista con direccion (futuro: dibujar flecha)
EF_VISIBLE = 1 << 1,
};
// --- Shapes per-nodo. Convencion: `shape_override == 0` significa "usa el
// shape del EntityType". Por eso SHAPE_CIRCLE no es 0 — un override que
// exigiera circulo cuando el tipo dice cuadrado seria indistinguible de
// "no override". Issue 0049f anadira el dispatch real; por ahora todos los
// nodos se pintan como circulo.
enum Shape : uint8_t {
SHAPE_USE_TYPE = 0, // sentinel: heredar del EntityType
SHAPE_CIRCLE = 1,
SHAPE_SQUARE = 2,
SHAPE_DIAMOND = 3,
SHAPE_HEX = 4,
SHAPE_TRIANGLE = 5,
SHAPE_ROUNDED_SQUARE = 6,
};
// --- Estilos de arista. Misma convencion: 0 = "usa el estilo del
// RelationType". 0049f introducira dashed/dotted reales; ahora EDGE_SOLID
// es lo que sale por la GPU sea cual sea el valor.
enum EdgeStyle : uint8_t {
EDGE_USE_TYPE = 0,
EDGE_SOLID = 1,
EDGE_DASHED = 2,
EDGE_DOTTED = 3,
};
// --- Graph node ---
// 40 bytes en x86_64. Layout pensado para vertex-pulling: x/y/vx/vy primero
// (4 floats consecutivos = 16 bytes que el TBO RG32F lee tal cual), seguidos
// del paquete denso de overrides + flags + indices.
struct GraphNode {
uint32_t id;
float x, y; // position in layout space
float vx, vy; // velocity (used by force layout)
float size; // visual radius (default 4.0)
uint32_t color; // ABGR packed (0 = use default palette)
const char* label; // optional display label (nullptr = none)
uint32_t community; // group/cluster ID (for auto-coloring)
float value; // arbitrary metric (for sizing)
bool pinned; // if true, force layout won't move this node
float x, y, vx, vy;
uint16_t type_id; // index into GraphData::types
uint8_t shape_override; // SHAPE_*; 0 = usar EntityType
uint8_t flags; // NF_* mask; default NF_VISIBLE tras graph_node()
uint32_t color_override; // RGBA8 (R en LSB); 0 = usar EntityType
float size_override; // pixels; <= 0 = usar EntityType
uint32_t label_idx; // index en el string pool del consumer (0 = no label)
uint64_t user_data; // opaco, app-specific (e.g. id de la BD origen)
};
// --- Graph edge ---
// 24 bytes. weight separado del paquete de uint8_t para mantenerlo alineado.
struct GraphEdge {
uint32_t source; // index into GraphData::nodes
uint32_t target; // index into GraphData::nodes
float weight; // edge weight (affects attraction force)
uint32_t color; // ABGR packed (0 = default gray)
uint32_t source; // index into GraphData::nodes
uint32_t target; // index into GraphData::nodes
uint16_t type_id; // index into GraphData::rel_types
uint8_t style_override; // EDGE_*; 0 = usar RelationType
uint8_t flags; // EF_* mask; default EF_VISIBLE tras graph_edge()
float weight;
uint32_t label_idx;
};
// --- Entity type (per-node visual & semantic class) ---
// La tabla EntityType[] vive en GraphData::types. Los nodos referencian via
// `type_id`. El `name` es solo para UI/debug; el renderer no lo lee.
struct EntityType {
uint32_t color; // RGBA8 (R en LSB)
uint8_t shape; // SHAPE_* (no SHAPE_USE_TYPE — es el valor base)
uint16_t icon_id; // 0 = sin icono (issue 0049f)
float default_size; // pixels
const char* name; // C-string propiedad del caller
};
// --- Relation type (per-edge visual & semantic class) ---
struct RelationType {
uint32_t color; // RGBA8
uint8_t style; // EDGE_* (no EDGE_USE_TYPE)
float width; // pixels
const char* name;
};
// --- Graph container ---
// Memoria de `nodes`, `edges`, `types`, `rel_types` es propiedad del caller.
struct GraphData {
GraphNode* nodes;
int node_count;
GraphEdge* edges;
int edge_count;
GraphNode* nodes; int node_count; int node_capacity;
GraphEdge* edges; int edge_count; int edge_capacity;
EntityType* types; int type_count;
RelationType* rel_types; int rel_type_count;
// Bounding box (updated by layout)
// Bounding box en world space (actualizado por update_bounds / layout)
float min_x, min_y, max_x, max_y;
// Recompute bounding box from node positions
void update_bounds();
// Find node index by id. Returns -1 if not found.
int find_node(uint32_t id) const;
// Busqueda lineal por user_data. -1 si no existe. O(n).
int find_node_by_user_data(uint64_t user_data) const;
};
// --- Helper: create a default node ---
inline GraphNode graph_node(uint32_t id, float x = 0, float y = 0) {
return {id, x, y, 0, 0, 4.0f, 0, nullptr, 0, 0, false};
// --- Resoluciones de override (puras, exportadas para tests y consumers) ---
// Devuelven el valor "efectivo" combinando el override del nodo/arista con la
// tabla de tipos. Las funciones son seguras frente a tablas vacias o type_id
// fuera de rango: degradan a un default razonable (gris para color, circulo
// para shape, 4 px para size, solid para style, 1 px para width).
inline uint32_t resolve_node_color(const GraphNode& n,
const EntityType* types, int type_count) {
if (n.color_override != 0) return n.color_override;
if (types && n.type_id < (uint16_t)type_count) return types[n.type_id].color;
return 0xFF888888u;
}
// --- Helper: create an edge ---
inline GraphEdge graph_edge(uint32_t source, uint32_t target, float weight = 1.0f) {
return {source, target, weight, 0};
inline uint8_t resolve_node_shape(const GraphNode& n,
const EntityType* types, int type_count) {
if (n.shape_override != SHAPE_USE_TYPE) return n.shape_override;
if (types && n.type_id < (uint16_t)type_count) return types[n.type_id].shape;
return SHAPE_CIRCLE;
}
inline float resolve_node_size(const GraphNode& n,
const EntityType* types, int type_count) {
if (n.size_override > 0.0f) return n.size_override;
if (types && n.type_id < (uint16_t)type_count) return types[n.type_id].default_size;
return 4.0f;
}
inline uint32_t resolve_edge_color(const GraphEdge& e,
const RelationType* rel_types, int rel_type_count) {
if (rel_types && e.type_id < (uint16_t)rel_type_count) return rel_types[e.type_id].color;
return 0xFF888888u;
}
inline uint8_t resolve_edge_style(const GraphEdge& e,
const RelationType* rel_types, int rel_type_count) {
if (e.style_override != EDGE_USE_TYPE) return e.style_override;
if (rel_types && e.type_id < (uint16_t)rel_type_count) return rel_types[e.type_id].style;
return EDGE_SOLID;
}
inline float resolve_edge_width(const GraphEdge& e,
const RelationType* rel_types, int rel_type_count) {
if (rel_types && e.type_id < (uint16_t)rel_type_count) return rel_types[e.type_id].width;
return 1.0f;
}
// --- Helpers de construccion ergonomicos ---
// Defaults: NF_VISIBLE / EF_VISIBLE encendidos, type_id = 0, sin overrides.
// Asi un nodo recien creado se renderiza con la apariencia del primer
// EntityType (que el caller tiene que registrar) sin tocar nada mas.
inline GraphNode graph_node(float x = 0.0f, float y = 0.0f, uint16_t type_id = 0) {
GraphNode n{};
n.x = x; n.y = y;
n.vx = 0.0f; n.vy = 0.0f;
n.type_id = type_id;
n.shape_override = SHAPE_USE_TYPE;
n.flags = NF_VISIBLE;
n.color_override = 0u;
n.size_override = 0.0f;
n.label_idx = 0u;
n.user_data = 0ull;
return n;
}
inline GraphEdge graph_edge(uint32_t source, uint32_t target,
float weight = 1.0f, uint16_t type_id = 0) {
GraphEdge e{};
e.source = source;
e.target = target;
e.type_id = type_id;
e.style_override = EDGE_USE_TYPE;
e.flags = EF_VISIBLE;
e.weight = weight;
e.label_idx = 0u;
return e;
}
inline EntityType entity_type(uint32_t color, uint8_t shape = SHAPE_CIRCLE,
float default_size = 4.0f, const char* name = nullptr,
uint16_t icon_id = 0) {
return EntityType{ color, shape, icon_id, default_size, name };
}
inline RelationType relation_type(uint32_t color, uint8_t style = EDGE_SOLID,
float width = 1.0f, const char* name = nullptr) {
return RelationType{ color, style, width, name };
}
+27 -8
View File
@@ -139,7 +139,8 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
for (int i = 0; i < graph.node_count; ++i) {
xs_buf[i] = graph.nodes[i].x;
ys_buf[i] = graph.nodes[i].y;
sz_buf[i] = graph.nodes[i].size;
sz_buf[i] = resolve_node_size(graph.nodes[i],
graph.types, graph.type_count);
}
state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count);
}
@@ -216,12 +217,20 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
// -------------------------------------------------------------------
// 5c. Hover — query nearest node
// -------------------------------------------------------------------
// Clear-then-set: limpiamos NF_HOVERED del nodo previo aunque el cursor
// ya no este sobre el viewport (cambio de target). Esto evita que el
// flag persista cuando el usuario sale del widget.
int prev_hovered = state.hovered_node;
if (prev_hovered >= 0 && prev_hovered < graph.node_count) {
graph.nodes[prev_hovered].flags &= ~NF_HOVERED;
}
state.hovered_node = -1;
if (hovered && graph.node_count > 0) {
float hit_radius = 10.0f / state.zoom;
int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius);
if (nearest >= 0) {
state.hovered_node = nearest;
graph.nodes[nearest].flags |= NF_HOVERED;
interacted = true;
}
}
@@ -237,7 +246,7 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
} else {
// Release drag
if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
graph.nodes[state.drag_node].pinned = false;
graph.nodes[state.drag_node].flags &= ~NF_PINNED;
}
state.drag_node = -1;
state.is_dragging = false;
@@ -249,15 +258,21 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
n.y = gy_mouse;
n.vx = 0.0f;
n.vy = 0.0f;
n.pinned = true;
n.flags |= NF_PINNED;
interacted = true;
}
// -------------------------------------------------------------------
// 5e. Click — select node
// 5e. Click — select node (clear-then-set NF_SELECTED)
// -------------------------------------------------------------------
if (hovered && lm_click && state.drag_node == -1) {
if (state.selected_node >= 0 && state.selected_node < graph.node_count) {
graph.nodes[state.selected_node].flags &= ~NF_SELECTED;
}
state.selected_node = state.hovered_node;
if (state.selected_node >= 0 && state.selected_node < graph.node_count) {
graph.nodes[state.selected_node].flags |= NF_SELECTED;
}
interacted = true;
}
@@ -309,11 +324,15 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
}
ImGui::BeginTooltip();
if (n.label) ImGui::TextUnformatted(n.label);
ImGui::Text("ID: %u", n.id);
ImGui::Text("Community: %u", n.community);
if (graph.types && n.type_id < (uint16_t)graph.type_count
&& graph.types[n.type_id].name) {
ImGui::Text("Type: %s", graph.types[n.type_id].name);
} else {
ImGui::Text("Type: %u", (unsigned)n.type_id);
}
ImGui::Text("Index: %d", state.hovered_node);
if (n.user_data) ImGui::Text("user_data: %llu", (unsigned long long)n.user_data);
ImGui::Text("Degree: %d", degree);
ImGui::Text("Value: %.3f", n.value);
ImGui::EndTooltip();
}
+5 -1
View File
@@ -3,7 +3,7 @@ name: graph_viewport
kind: component
lang: cpp
domain: viz
version: "1.0.0"
version: "1.1.0"
purity: impure
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"
@@ -111,6 +111,10 @@ En la parte inferior del widget aparece: numero de nodos, aristas, zoom actual,
El renderer OpenGL y el spatial hash se crean en el primer frame. La camara se ajusta automaticamente con `graph_viewport_fit` en la inicializacion.
## Notas de version
- **v1.1** (2026-04-29, issue 0049e): adapta el viewport al modelo extendido. Hover/seleccion ahora se publican tambien como `flags |= NF_HOVERED` / `NF_SELECTED` en el grafo (clear-then-set) — los `state.hovered_node` / `selected_node` siguen siendo la API estable. El drag usa `flags |= NF_PINNED` en lugar del campo `pinned` desaparecido. El tooltip muestra `Type` (nombre del EntityType si esta) y `user_data` en lugar de `community`/`value`/`label`/`id`.
## Notas de implementacion
- Usa `ImGui::InvisibleButton` con flags para los tres botones del raton, capturando input sin dibujar ningun boton visible.
+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);
}
+52
View File
@@ -0,0 +1,52 @@
---
name: EntityType
lang: cpp
domain: viz
version: "1.0.0"
algebraic: product
definition: |
struct EntityType {
uint32_t color;
uint8_t shape;
uint16_t icon_id;
float default_size;
const char* name;
};
description: "Clase visual de un nodo. La tabla EntityType[] vive en GraphData::types y los nodos referencian por indice (GraphNode::type_id). Define color (RGBA8), shape (SHAPE_*), icon_id (issue 0049f), default_size en pixels y nombre para UI/debug."
tags: [graph, type, entity, visual, icon, shape, color]
uses_types: []
file_path: "cpp/functions/viz/graph_types.h"
---
## Campos
| Campo | Descripcion |
|---|---|
| `color` | RGBA8 (R en LSB, mismo formato que `pack_rgba8`) |
| `shape` | `SHAPE_*` (CIRCLE, SQUARE, DIAMOND, HEX, TRIANGLE, ROUNDED_SQUARE). Issue 0049f anadira el dispatch real al GPU |
| `icon_id` | 0 = sin icono. Issue 0049f popula los iconos via Tabler atlas |
| `default_size` | Diametro en pixels para nodos que no tienen `size_override > 0` |
| `name` | C-string propiedad del caller. Solo para UI/debug, no se renderiza |
## Helper
```cpp
EntityType person = entity_type(0xFF5B8DEFu, SHAPE_CIRCLE, 5.0f, "person");
EntityType org = entity_type(0xFFF5973Eu, SHAPE_SQUARE, 6.0f, "org");
```
## Uso tipico
La app construye un array fijo de tipos y lo asigna a `GraphData::types`:
```cpp
EntityType types[] = {
entity_type(0xFF5B8DEFu, SHAPE_CIRCLE, 5.0f, "person"),
entity_type(0xFFF5973Eu, SHAPE_SQUARE, 6.0f, "org"),
};
graph.types = types;
graph.type_count = 2;
```
`GraphNode::type_id` es uint16_t — soporta 65k tipos distintos, suficiente
para cualquier ontologia razonable.
+51
View File
@@ -0,0 +1,51 @@
---
name: GraphEdge
lang: cpp
domain: viz
version: "2.0.0"
algebraic: product
definition: |
struct GraphEdge {
uint32_t source;
uint32_t target;
uint16_t type_id;
uint8_t style_override;
uint8_t flags;
float weight;
uint32_t label_idx;
};
description: "Arista del grafo: indices de nodos source/target, type_id (apariencia via RelationType), style_override (EDGE_*), flags (EF_DIRECTED/VISIBLE), weight (para attraction del force layout) y label_idx."
tags: [graph, edge, relation, type-id, flags, override]
uses_types: []
file_path: "cpp/functions/viz/graph_types.h"
---
## Campos
| Campo | Descripcion |
|---|---|
| `source` / `target` | Indices en `GraphData::nodes` |
| `type_id` | Indice en `GraphData::rel_types`. Determina color/style/width |
| `style_override` | `EDGE_*` (`SOLID`/`DASHED`/`DOTTED`). `EDGE_USE_TYPE` = heredar |
| `flags` | `EdgeFlags` mask: `EF_DIRECTED`, `EF_VISIBLE` |
| `weight` | Multiplica la fuerza de atraccion en el force layout |
| `label_idx` | Indice en el string pool. 0 = sin label |
## Helper
```cpp
GraphEdge e = graph_edge(/*src*/0, /*tgt*/5, /*weight*/2.0f, /*type_id*/1);
e.flags |= EF_DIRECTED;
```
`graph_edge()` deja `flags = EF_VISIBLE` y `style_override = EDGE_USE_TYPE`.
## Notas de diseño
- No tiene `color` directo: la apariencia siempre se deriva de `RelationType`.
Las apps que necesiten variabilidad por arista lo modelan creando varios
RelationType y asignando `type_id` distintos.
- El renderer (issue 0049d) reservaba 16 bytes alineados con un campo
`flags` de 32-bit; en este modelo el campo de flags es 8-bit pero el
buffer estatico del shader puede empacarse de forma distinta — la
conversion ocurre en `graph_renderer.cpp`.
+55
View File
@@ -0,0 +1,55 @@
---
name: GraphNode
lang: cpp
domain: viz
version: "2.0.0"
algebraic: product
definition: |
struct GraphNode {
float x, y, vx, vy;
uint16_t type_id;
uint8_t shape_override;
uint8_t flags;
uint32_t color_override;
float size_override;
uint32_t label_idx;
uint64_t user_data;
};
description: "Vertice del grafo: posicion, velocidad, type_id (apariencia per tipo), overrides per-nodo (color/shape/size), flags (NF_PINNED/VISIBLE/SELECTED/HOVERED), label_idx y user_data opaco para mapear a la entidad real del backend."
tags: [graph, node, vertex, type-id, flags, override, user-data]
uses_types: []
file_path: "cpp/functions/viz/graph_types.h"
---
## Campos
| Campo | Descripcion |
|---|---|
| `x, y` | Posicion en world space, modificada por el force layout |
| `vx, vy` | Velocidad usada por el integrador del layout |
| `type_id` | Indice en `GraphData::types`. Determina color/shape/size por defecto |
| `shape_override` | `SHAPE_*`. `SHAPE_USE_TYPE` (0) = heredar del EntityType |
| `flags` | Mascara `NodeFlags`: `NF_PINNED`, `NF_VISIBLE`, `NF_SELECTED`, `NF_HOVERED` |
| `color_override` | RGBA8. 0 = usar EntityType |
| `size_override` | Pixels. <= 0 = usar EntityType |
| `label_idx` | Indice en el string pool del consumer. 0 = sin label |
| `user_data` | Opaco. La app lo usa para volver a la entidad del backend (ej: id de la BD) |
## Helper
```cpp
GraphNode n = graph_node(/*x*/100.f, /*y*/200.f, /*type_id*/3);
n.flags |= NF_PINNED;
```
`graph_node()` deja `flags = NF_VISIBLE` y todos los `*_override` a 0 — el nodo
hereda apariencia del EntityType correspondiente.
## Notas de diseño
- El campo `id` (v1) desaparece. Las apps que necesiten un identificador
estable usan `user_data` (uint64_t) — encaja IDs SQLite, hashes o
punteros opaco-cast.
- Los overrides son sentinel-based para que un nodo creado con `graph_node()`
herede apariencia automaticamente sin tocar mas campos.
- Tamaño: 40 bytes en x86_64. Para 100k nodos = ~4 MB.
+26 -88
View File
@@ -2,105 +2,43 @@
name: GraphData
lang: cpp
domain: viz
version: "1.0.0"
version: "2.0.0"
algebraic: product
definition: |
struct GraphNode {
uint32_t id;
float x, y;
float vx, vy;
float size;
uint32_t color;
const char* label;
uint32_t community;
float value;
bool pinned;
};
struct GraphEdge {
uint32_t source;
uint32_t target;
float weight;
uint32_t color;
};
struct GraphData {
GraphNode* nodes;
int node_count;
GraphEdge* edges;
int edge_count;
GraphNode* nodes; int node_count; int node_capacity;
GraphEdge* edges; int edge_count; int edge_capacity;
EntityType* types; int type_count;
RelationType* rel_types; int rel_type_count;
float min_x, min_y, max_x, max_y;
void update_bounds();
int find_node(uint32_t id) const;
int find_node_by_user_data(uint64_t user_data) const;
};
description: "Tipos de datos base para el sistema de grafos GPU del registry. GraphNode modela un vertice con posicion, velocidad, apariencia y metadatos de layout. GraphEdge modela una arista con peso y color. GraphData es el contenedor principal que agrupa nodos y aristas con bounding box y metodos de consulta. Disenado para integrarse con force-directed layout y renderizado GPU via ImGui/ImPlot."
tags: [graph, network, visualization, gpu, force-layout, node, edge, imgui]
uses_types: []
description: "Contenedor del grafo: nodos, aristas y tablas de tipos (EntityType / RelationType) que definen apariencia per-tipo, ademas de bounding box y busqueda por user_data. v2.0 incorpora capacities, tablas de tipos y find_node_by_user_data tras issue 0049e."
tags: [graph, container, viewport, gpu, types-table, entity-type, relation-type]
uses_types: ["GraphNode_cpp_viz", "GraphEdge_cpp_viz", "EntityType_cpp_viz", "RelationType_cpp_viz"]
file_path: "cpp/functions/viz/graph_types.h"
---
## Structs
## Estructura
### GraphNode
Vertice del grafo. Contiene todos los datos necesarios para el layout y el renderizado.
| Campo | Tipo | Descripcion |
|---|---|---|
| `id` | `uint32_t` | Identificador unico del nodo |
| `x, y` | `float` | Posicion en el espacio de layout |
| `vx, vy` | `float` | Velocidad del nodo, usada por el algoritmo force-directed |
| `size` | `float` | Radio visual en pixels (por defecto 4.0) |
| `color` | `uint32_t` | Color en formato ABGR packed. 0 = usar paleta automatica por community |
| `label` | `const char*` | Etiqueta visible. `nullptr` = sin etiqueta |
| `community` | `uint32_t` | ID de grupo/cluster para auto-coloreo. 0 = sin grupo |
| `value` | `float` | Metrica arbitraria (puede usarse para escalar el tamaño del nodo) |
| `pinned` | `bool` | Si es `true`, el force layout no mueve este nodo |
### GraphEdge
Arista del grafo. Referencia nodos por indice (no por id) para acceso O(1) en el loop de simulacion.
| Campo | Tipo | Descripcion |
|---|---|---|
| `source` | `uint32_t` | Indice en `GraphData::nodes` del nodo origen |
| `target` | `uint32_t` | Indice en `GraphData::nodes` del nodo destino |
| `weight` | `float` | Peso de la arista. Afecta la fuerza de atraccion en el layout |
| `color` | `uint32_t` | Color ABGR packed. 0 = gris por defecto |
### GraphData
Contenedor principal. Posee los arrays de nodos y aristas (memoria gestionada externamente). Mantiene un bounding box actualizable para proyeccion de coordenadas a pantalla.
| Campo/Metodo | Descripcion |
| Campo | Descripcion |
|---|---|
| `nodes` / `node_count` | Array de nodos y su longitud |
| `edges` / `edge_count` | Array de aristas y su longitud |
| `min_x, min_y, max_x, max_y` | Bounding box calculado sobre las posiciones actuales |
| `update_bounds()` | Recalcula el bounding box iterando todos los nodos |
| `find_node(id)` | Busqueda lineal por `GraphNode::id`. Retorna -1 si no existe |
| `nodes` / `node_count` / `node_capacity` | Array de nodos. `node_capacity >= node_count` y la app gestiona la memoria |
| `edges` / `edge_count` / `edge_capacity` | Array de aristas |
| `types` / `type_count` | Tabla de `EntityType`. Indexada por `GraphNode::type_id` |
| `rel_types` / `rel_type_count` | Tabla de `RelationType`. Indexada por `GraphEdge::type_id` |
| `min_x, min_y, max_x, max_y` | AABB de las posiciones de nodos. Se recalcula con `update_bounds()` |
| `update_bounds()` | O(n) — usar tras cada step del layout |
| `find_node_by_user_data(uid)` | Busqueda lineal O(n). -1 si no existe. Para indices ad-hoc el caller puede mantener un `unordered_map` propio |
## Helpers
## Notas de version
```cpp
// Crear un nodo con valores por defecto
GraphNode n = graph_node(42, 100.0f, 200.0f);
- **v2.0** (2026-04-29, issue 0049e): Modelo extendido. `GraphData` gana
`node_capacity`, `edge_capacity`, las tablas `types[]` / `rel_types[]` y
el helper `find_node_by_user_data` (sustituye a `find_node(id)` — ahora
los nodos no tienen `id` propio, el mapeo a la entidad real va en
`user_data`). Apparencia per-nodo se resuelve combinando overrides con la
tabla de tipos: ver `resolve_node_*` / `resolve_edge_*` en el .h.
// Crear una arista con peso por defecto 1.0
GraphEdge e = graph_edge(0, 1, 2.5f);
```
## Implementacion
Los metodos `update_bounds()` y `find_node()` estan implementados en `cpp/functions/viz/graph_types.cpp`.
`update_bounds()` es O(n) sobre `node_count`. Llamar despues de cada step del layout para mantener el bounding box fresco.
`find_node()` es O(n) por busqueda lineal. Para grafos grandes (>10k nodos) considerar mantener un `unordered_map<uint32_t, int>` externo como indice.
## Notas de diseño
- La memoria de `nodes` y `edges` es propiedad del caller. `GraphData` no hace `new`/`delete`.
- `color` usa formato ABGR packed (compatible con ImGui `ImU32`): `0xAABBGGRR`.
- Las aristas referencian por indice, no por id, para que el loop de simulacion sea cache-friendly.
- `community` con valor 0 se interpreta como "sin grupo" — los colores de comunidad empiezan desde 1.
- **v1.0**: modelo inicial con id, color directo, community.
+44
View File
@@ -0,0 +1,44 @@
---
name: RelationType
lang: cpp
domain: viz
version: "1.0.0"
algebraic: product
definition: |
struct RelationType {
uint32_t color;
uint8_t style;
float width;
const char* name;
};
description: "Clase visual de una arista. La tabla RelationType[] vive en GraphData::rel_types y las aristas referencian via GraphEdge::type_id. Define color (RGBA8), style (EDGE_SOLID/DASHED/DOTTED), width en pixels y nombre."
tags: [graph, type, relation, edge, visual, style]
uses_types: []
file_path: "cpp/functions/viz/graph_types.h"
---
## Campos
| Campo | Descripcion |
|---|---|
| `color` | RGBA8 (R en LSB) |
| `style` | `EDGE_*` (`SOLID`, `DASHED`, `DOTTED`). Issue 0049f anadira los estilos no-solid a la GPU |
| `width` | Pixels en pantalla. Aplicado via `glLineWidth` (clamp del driver al maximo soportado) |
| `name` | C-string propiedad del caller. Solo UI/debug |
## Helper
```cpp
RelationType knows = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "knows");
RelationType owns = relation_type(0xFFEAB308u, EDGE_DASHED, 1.5f, "owns");
```
## Uso tipico
```cpp
RelationType rels[] = {
relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default"),
};
graph.rel_types = rels;
graph.rel_type_count = 1;
```