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