From b9ffc13caf33bacf6665366e8972e205387b784e Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 29 Apr 2026 22:44:40 +0200 Subject: [PATCH] feat(viz): graph_types modelo extendido + EntityType/RelationType + flags (issue 0049e) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- cpp/apps/primitives_gallery/demos_graph.cpp | 67 ++++-- cpp/functions/viz/graph_force_layout.cpp | 16 +- cpp/functions/viz/graph_force_layout.md | 3 +- cpp/functions/viz/graph_renderer.cpp | 60 ++++-- cpp/functions/viz/graph_renderer.md | 3 +- cpp/functions/viz/graph_types.cpp | 5 +- cpp/functions/viz/graph_types.h | 204 +++++++++++++++--- cpp/functions/viz/graph_viewport.cpp | 35 ++- cpp/functions/viz/graph_viewport.md | 6 +- cpp/tests/CMakeLists.txt | 4 + cpp/tests/test_graph_edge_static.cpp | 15 +- cpp/tests/test_graph_types.cpp | 197 +++++++++++++++++ cpp/types/viz/entity_type.md | 52 +++++ cpp/types/viz/graph_edge.md | 51 +++++ cpp/types/viz/graph_node.md | 55 +++++ cpp/types/viz/graph_types.md | 114 +++------- cpp/types/viz/relation_type.md | 44 ++++ dev/issues/README.md | 2 +- .../0049e-graph-types-extended.md | 0 19 files changed, 756 insertions(+), 177 deletions(-) create mode 100644 cpp/tests/test_graph_types.cpp create mode 100644 cpp/types/viz/entity_type.md create mode 100644 cpp/types/viz/graph_edge.md create mode 100644 cpp/types/viz/graph_node.md create mode 100644 cpp/types/viz/relation_type.md rename dev/issues/{ => completed}/0049e-graph-types-extended.md (100%) diff --git a/cpp/apps/primitives_gallery/demos_graph.cpp b/cpp/apps/primitives_gallery/demos_graph.cpp index 5b2d6131..21efeb45 100644 --- a/cpp/apps/primitives_gallery/demos_graph.cpp +++ b/cpp/apps/primitives_gallery/demos_graph.cpp @@ -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((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(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(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(k); + cluster_cy[k] + (rnd() - 0.5f) * scatter, + tid); + n.size_override = 3.0f + rnd() * 2.0f; + n.user_data = static_cast(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(s_nodes.size()); - s_graph.edges = s_edges.data(); - s_graph.edge_count = static_cast(s_edges.size()); + s_graph.nodes = s_nodes.data(); + s_graph.node_count = static_cast(s_nodes.size()); + s_graph.node_capacity = static_cast(s_nodes.capacity()); + s_graph.edges = s_edges.data(); + s_graph.edge_count = static_cast(s_edges.size()); + s_graph.edge_capacity = static_cast(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), "-"); } diff --git a/cpp/functions/viz/graph_force_layout.cpp b/cpp/functions/viz/graph_force_layout.cpp index 5159ff24..67db16c7 100644 --- a/cpp/functions/viz/graph_force_layout.cpp +++ b/cpp/functions/viz/graph_force_layout.cpp @@ -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; diff --git a/cpp/functions/viz/graph_force_layout.md b/cpp/functions/viz/graph_force_layout.md index 020ea242..f68f3fe7 100644 --- a/cpp/functions/viz/graph_force_layout.md +++ b/cpp/functions/viz/graph_force_layout.md @@ -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 diff --git a/cpp/functions/viz/graph_renderer.cpp b/cpp/functions/viz/graph_renderer.cpp index 5a62eee6..1c8d5794 100644 --- a/cpp/functions/viz/graph_renderer.cpp +++ b/cpp/functions/viz/graph_renderer.cpp @@ -13,19 +13,24 @@ #include // --------------------------------------------------------------------------- -// 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 }; } diff --git a/cpp/functions/viz/graph_renderer.md b/cpp/functions/viz/graph_renderer.md index 4ddea3dd..367de392 100644 --- a/cpp/functions/viz/graph_renderer.md +++ b/cpp/functions/viz/graph_renderer.md @@ -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). diff --git a/cpp/functions/viz/graph_types.cpp b/cpp/functions/viz/graph_types.cpp index 4a3c7e9b..4a6cddf8 100644 --- a/cpp/functions/viz/graph_types.cpp +++ b/cpp/functions/viz/graph_types.cpp @@ -1,5 +1,4 @@ #include "graph_types.h" -#include 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; } diff --git a/cpp/functions/viz/graph_types.h b/cpp/functions/viz/graph_types.h index 672ff1cf..7dfb3bc9 100644 --- a/cpp/functions/viz/graph_types.h +++ b/cpp/functions/viz/graph_types.h @@ -1,50 +1,200 @@ #pragma once #include +// 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 }; } diff --git a/cpp/functions/viz/graph_viewport.cpp b/cpp/functions/viz/graph_viewport.cpp index 713c1b2c..021f987a 100644 --- a/cpp/functions/viz/graph_viewport.cpp +++ b/cpp/functions/viz/graph_viewport.cpp @@ -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(); } diff --git a/cpp/functions/viz/graph_viewport.md b/cpp/functions/viz/graph_viewport.md index 3cd5046e..217175cb 100644 --- a/cpp/functions/viz/graph_viewport.md +++ b/cpp/functions/viz/graph_viewport.md @@ -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. diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index d341e287..e6b1724b 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -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 diff --git a/cpp/tests/test_graph_edge_static.cpp b/cpp/tests/test_graph_edge_static.cpp index 16247196..0fbceec9 100644 --- a/cpp/tests/test_graph_edge_static.cpp +++ b/cpp/tests/test_graph_edge_static.cpp @@ -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); } diff --git a/cpp/tests/test_graph_types.cpp b/cpp/tests/test_graph_types.cpp new file mode 100644 index 00000000..dd4c271e --- /dev/null +++ b/cpp/tests/test_graph_types.cpp @@ -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 + +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); +} diff --git a/cpp/types/viz/entity_type.md b/cpp/types/viz/entity_type.md new file mode 100644 index 00000000..d866b695 --- /dev/null +++ b/cpp/types/viz/entity_type.md @@ -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. diff --git a/cpp/types/viz/graph_edge.md b/cpp/types/viz/graph_edge.md new file mode 100644 index 00000000..63cea16f --- /dev/null +++ b/cpp/types/viz/graph_edge.md @@ -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`. diff --git a/cpp/types/viz/graph_node.md b/cpp/types/viz/graph_node.md new file mode 100644 index 00000000..af1fd1cd --- /dev/null +++ b/cpp/types/viz/graph_node.md @@ -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. diff --git a/cpp/types/viz/graph_types.md b/cpp/types/viz/graph_types.md index aaf50313..0ed12c09 100644 --- a/cpp/types/viz/graph_types.md +++ b/cpp/types/viz/graph_types.md @@ -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` 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. diff --git a/cpp/types/viz/relation_type.md b/cpp/types/viz/relation_type.md new file mode 100644 index 00000000..a8b5034c --- /dev/null +++ b/cpp/types/viz/relation_type.md @@ -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; +``` diff --git a/dev/issues/README.md b/dev/issues/README.md index 980fec4c..361c4b51 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -59,7 +59,7 @@ | [0049b](completed/0049b-cpp-bump-gl-43.md) | Bump OpenGL 3.3 → 4.3 core en cpp/framework | completado | alta | infra | parte de 0049 | | [0049c](completed/0049c-graph-renderer-tier1.md) | graph_renderer Tier 1: RGBA8, orphan, frustum cull, auto-pause | completado | alta | perf | parte de 0049 | | [0049d](completed/0049d-graph-edges-vertex-pulling.md) | Aristas via vertex pulling con TBO | completado | alta | perf | parte de 0049 | -| [0049e](0049e-graph-types-extended.md) | graph_types modelo extendido + EntityType/RelationType | pendiente | alta | feature | parte de 0049 | +| [0049e](completed/0049e-graph-types-extended.md) | graph_types modelo extendido + EntityType/RelationType | completado | alta | feature | parte de 0049 | | [0049f](0049f-graph-renderer-symbols.md) | Renderer extendido: shapes SDF, icon atlas, flechas, edge styles | pendiente | alta | feature | parte de 0049 | | [0049g](0049g-graph-source-operations.md) | graph_sources: lector operations.db + abstraccion funcional | pendiente | alta | feature | parte de 0049 | | [0049h](0049h-graph-force-layout-gpu.md) | graph_force_layout_gpu: compute shader + spatial hash | pendiente | media-alta | feature | parte de 0049 | diff --git a/dev/issues/0049e-graph-types-extended.md b/dev/issues/completed/0049e-graph-types-extended.md similarity index 100% rename from dev/issues/0049e-graph-types-extended.md rename to dev/issues/completed/0049e-graph-types-extended.md