feat(viz): graph_types modelo extendido + EntityType/RelationType + flags (issue 0049e)

Extiende el modelo agnostico de graph_types.h para soportar shapes/iconos/
filtros/labels/streaming sin acoplar a backend. Migra el unico consumer
(demos_graph) en el mismo cambio.

- GraphNode v2: type_id + shape_override/color_override/size_override +
  flags (NF_PINNED/VISIBLE/SELECTED/HOVERED) + label_idx + user_data.
- GraphEdge v2: type_id + style_override + flags (EF_DIRECTED/VISIBLE).
- EntityType / RelationType: tablas en GraphData (types, rel_types).
- Helpers de resolucion (resolve_node_color/shape/size, resolve_edge_*)
  y constructores ergonomicos (graph_node, graph_edge, entity_type,
  relation_type) — sentinel-based para herencia automatica del tipo.
- graph_renderer v1.4: lee NF_VISIBLE / EF_VISIBLE, resuelve apariencia
  via override → EntityType → fallback indexado por type_id. Skipea
  aristas con endpoints invisibles. Shapes siguen pintandose como
  circulo (0049f cableara el dispatch real).
- graph_force_layout v1.2: pinned ahora vive en flags & NF_PINNED.
- graph_viewport v1.1: hover/seleccion publican NF_HOVERED/SELECTED en
  el grafo (clear-then-set). Drag usa NF_PINNED. Tooltip muestra Type/
  user_data en lugar de community/value/label.
- demos_graph: 8 EntityType (paleta antigua) + 1 RelationType. type_id
  por cluster. user_data = indice numerico del nodo. Apariencia visual
  identica al pre-cambio.
- test_graph_types.cpp: 12 casos cubriendo helpers, defaults, bitmask
  manipulation y resoluciones override-vs-EntityType. test_graph_edge_
  static actualizado al nuevo modelo (ya no tiene .color directo).
- 4 .md de tipos nuevos (graph_node, graph_edge, entity_type,
  relation_type) + GraphData v2.0 actualizado.

Tests: 31/31 ctest verdes (incluye test_visual golden).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 22:44:40 +02:00
parent ae47b76d0c
commit c29428a187
19 changed files with 756 additions and 177 deletions
+8 -8
View File
@@ -265,7 +265,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
// stack local en pila, asi que es thread-safe.
#pragma omp parallel for if(graph.node_count >= 1024) schedule(dynamic, 256)
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
if (graph.nodes[i].flags & NF_PINNED) continue;
quad_force(root,
graph.nodes[i].x, graph.nodes[i].y,
config.theta, config.repulsion, config.min_distance,
@@ -290,15 +290,15 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
float fx_e = force * dx / dist;
float fy_e = force * dy / dist;
if (!graph.nodes[s].pinned) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; }
if (!graph.nodes[t].pinned) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; }
if (!(graph.nodes[s].flags & NF_PINNED)) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; }
if (!(graph.nodes[t].flags & NF_PINNED)) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; }
}
// ---- Gravity toward center (0,0) ----
if (config.gravity != 0.0f) {
#pragma omp parallel for if(graph.node_count >= 1024) schedule(static)
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
if (graph.nodes[i].flags & NF_PINNED) continue;
fx_buf[i] -= config.gravity * graph.nodes[i].x;
fy_buf[i] -= config.gravity * graph.nodes[i].y;
}
@@ -309,7 +309,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
#pragma omp parallel for if(graph.node_count >= 1024) schedule(static) reduction(+:total_energy)
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
n.vx = n.vx * config.damping + fx_buf[i];
n.vy = n.vy * config.damping + fy_buf[i];
@@ -332,7 +332,7 @@ float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)
void graph_force_layout_reset(GraphData& graph, float spread) {
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
// rand() produces [0, RAND_MAX]; map to [-spread, spread]
n.x = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
n.y = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
@@ -347,7 +347,7 @@ void graph_layout_circular(GraphData& graph, float radius) {
const float two_pi = 6.28318530718f;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
float angle = two_pi * (float)i / (float)graph.node_count;
n.x = radius * std::cos(angle);
n.y = radius * std::sin(angle);
@@ -370,7 +370,7 @@ void graph_layout_grid(GraphData& graph, float spacing) {
float oy = -0.5f * (rows - 1) * spacing;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
if (n.flags & NF_PINNED) continue;
int col = i % cols;
int row = i / cols;
n.x = ox + col * spacing;
+2 -1
View File
@@ -3,7 +3,7 @@ name: graph_force_layout
kind: function
lang: cpp
domain: viz
version: "1.1.0"
version: "1.2.0"
purity: pure
signature: "float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)"
description: "Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada"
@@ -94,6 +94,7 @@ if (running) {
## Notas de version
- **v1.2** (2026-04-29, issue 0049e): el campo `pinned` desaparece del modelo de `GraphNode` y se sustituye por `flags & NF_PINNED`. La logica del integrador, atraccion, gravedad y reset usa el bit equivalente. Sin cambios en la API publica ni en el comportamiento.
- **v1.1** (2026-04-29, issue 0049c): añade el helper puro
`graph_force_layout_should_pause(low_frames, min_consecutive)` para que las apps
detecten convergencia sin replicar el contador por todas partes. Sin cambios en
+44 -16
View File
@@ -13,19 +13,24 @@
#include <algorithm>
// ---------------------------------------------------------------------------
// Community palette (ABGR packed, 10 colors)
// Fallback palette (RGBA8 con R en LSB) — usada solo cuando GraphData::types
// esta vacio. En el modelo extendido (issue 0049e) la apariencia de cada
// nodo viene resuelta por `resolve_node_color()`, que mira primero el
// override del nodo, luego el EntityType de la tabla, y finalmente este
// fallback. Mantener 10 colores para nodos sin tipos sigue siendo util en
// demos que aun no construyen tablas EntityType.
// ---------------------------------------------------------------------------
static const uint32_t k_palette[10] = {
0xFF4CAF50, // green
0xFFF44336, // red
0xFF2196F3, // blue
0xFFFF9800, // orange
0xFF9C27B0, // purple
0xFF00BCD4, // cyan
0xFFFFEB3B, // yellow
0xFFE91E63, // pink
0xFF795548, // brown
0xFF607D8B // blue-grey
static const uint32_t k_fallback_palette[10] = {
0xFF4CAF50u, // green
0xFFF44336u, // red
0xFF2196F3u, // blue
0xFFFF9800u, // orange
0xFF9C27B0u, // purple
0xFF00BCD4u, // cyan
0xFFFFEB3Bu, // yellow
0xFFE91E63u, // pink
0xFF795548u, // brown
0xFF607D8Bu, // blue-grey
};
// ---------------------------------------------------------------------------
@@ -562,8 +567,17 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
const GraphEdge& e = graph.edges[i];
if (e.source >= (uint32_t)graph.node_count) continue;
if (e.target >= (uint32_t)graph.node_count) continue;
uint32_t col = e.color != 0 ? e.color
: pack_rgba8(0x88, 0x88, 0x88, 0xFF);
if (!(e.flags & EF_VISIBLE)) continue;
// Saltamos aristas cuyos endpoints no estan visibles —
// el shader las pintaria igualmente (las posiciones siguen
// en el TBO) pero la aristas tendrian apariencia conectando
// "vacios" si los nodos estan ocultos por NF_VISIBLE off.
if (!(graph.nodes[e.source].flags & NF_VISIBLE)) continue;
if (!(graph.nodes[e.target].flags & NF_VISIBLE)) continue;
uint32_t col = resolve_edge_color(e, graph.rel_types,
graph.rel_type_count);
if (col == 0u) col = pack_rgba8(0x88, 0x88, 0x88, 0xFF);
r->edge_static_staging[out++] = { e.source, e.target, col, 0u };
}
if (out > 0) {
@@ -625,13 +639,27 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
size_t visible = 0;
for (int i = 0; i < graph.node_count; ++i) {
const GraphNode& n = graph.nodes[i];
float sz = n.size > 0.0f ? n.size : 4.0f;
if (!(n.flags & NF_VISIBLE)) continue;
float sz = resolve_node_size(n, graph.types, graph.type_count);
if (sz <= 0.0f) sz = 4.0f;
float half = sz * 0.5f;
// AABB del nodo: centro ± half. Skip si fuera del viewport.
if (n.x + half < vx0 || n.x - half > vx1) continue;
if (n.y + half < vy0 || n.y - half > vy1) continue;
uint32_t ncol = n.color != 0 ? n.color : k_palette[n.community % 10];
// Apariencia: 1) override del nodo, 2) EntityType, 3) fallback
// indexado por type_id (paleta de 10 — sustituye al community
// del modelo v1).
uint32_t ncol;
if (n.color_override != 0u) {
ncol = n.color_override;
} else if (graph.types && n.type_id < (uint16_t)graph.type_count) {
ncol = graph.types[n.type_id].color;
} else {
ncol = k_fallback_palette[n.type_id % 10];
}
// 0049f anadira el dispatch de shape; por ahora todos circulos.
r->node_staging[visible++] = { n.x, n.y, sz, ncol };
}
+2 -1
View File
@@ -3,7 +3,7 @@ name: graph_renderer
kind: function
lang: cpp
domain: viz
version: "1.3.0"
version: "1.4.0"
purity: impure
signature: "GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)"
description: "Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes"
@@ -88,6 +88,7 @@ ndc = (screen / viewport) * 2 - 1
## Notas
- **v1.4** (2026-04-29, issue 0049e): adapta el renderer al modelo extendido de `GraphData`. Lee `n.flags & NF_VISIBLE` para skipear nodos invisibles, resuelve color via `n.color_override``EntityType` → fallback indexado por `type_id`. Aristas: skip si `!(EF_VISIBLE)` o si los endpoints no son visibles, color via `RelationType`. Shapes/iconos/dashed-style siguen como circulo solido — el dispatch real llega en 0049f.
- **v1.3** (2026-04-29, issue 0049d): aristas via vertex pulling. API publica intacta.
- El buffer de aristas pasa a ser estatico (`source_idx, target_idx, color, flags` × E, 16 bytes/arista) y solo se reupload cuando el grafo cambia (detectado por `(edges_ptr, edge_count)` — heuristica suficiente mientras `GraphData` no tenga `revision`). Para 100k aristas: 1.6 MB iniciales vs 4.8 MB/frame del esquema anterior — el upload baja a cero en regimen estable.
- Las posiciones de los nodos se suben cada frame a un Texture Buffer Object `RG32F` (`vec2[]`, 8 bytes/nodo). El vertex shader de aristas hace `texelFetch(u_node_pos, idx)` con `idx` derivado de `gl_VertexID & 1` (0=source, 1=target).
+2 -3
View File
@@ -1,5 +1,4 @@
#include "graph_types.h"
#include <cfloat>
void GraphData::update_bounds() {
if (node_count == 0) {
@@ -16,9 +15,9 @@ void GraphData::update_bounds() {
}
}
int GraphData::find_node(uint32_t id) const {
int GraphData::find_node_by_user_data(uint64_t user_data) const {
for (int i = 0; i < node_count; ++i) {
if (nodes[i].id == id) return i;
if (nodes[i].user_data == user_data) return i;
}
return -1;
}
+177 -27
View File
@@ -1,50 +1,200 @@
#pragma once
#include <cstdint>
// Modelo de grafo extendido (issue 0049e). El sentinel `0` se usa en cada
// campo `*_override` para significar "usar el valor del EntityType/RelationType
// indicado por type_id". Asi un nodo nuevo creado con todos los campos a 0
// hereda automaticamente la apariencia del tipo, sin tener que volver a
// escribir color/size/shape.
// --- Bitmask de flags por nodo. NF_VISIBLE es default tras `graph_node()`.
enum NodeFlags : uint8_t {
NF_NONE = 0,
NF_PINNED = 1 << 0, // el force layout no mueve este nodo
NF_VISIBLE = 1 << 1, // si esta a 0, el renderer salta el nodo
NF_SELECTED = 1 << 2, // marcado por el viewport (click)
NF_HOVERED = 1 << 3, // marcado por el viewport (mouse over)
};
// --- Bitmask de flags por arista. EF_VISIBLE es default tras `graph_edge()`.
enum EdgeFlags : uint8_t {
EF_NONE = 0,
EF_DIRECTED = 1 << 0, // arista con direccion (futuro: dibujar flecha)
EF_VISIBLE = 1 << 1,
};
// --- Shapes per-nodo. Convencion: `shape_override == 0` significa "usa el
// shape del EntityType". Por eso SHAPE_CIRCLE no es 0 — un override que
// exigiera circulo cuando el tipo dice cuadrado seria indistinguible de
// "no override". Issue 0049f anadira el dispatch real; por ahora todos los
// nodos se pintan como circulo.
enum Shape : uint8_t {
SHAPE_USE_TYPE = 0, // sentinel: heredar del EntityType
SHAPE_CIRCLE = 1,
SHAPE_SQUARE = 2,
SHAPE_DIAMOND = 3,
SHAPE_HEX = 4,
SHAPE_TRIANGLE = 5,
SHAPE_ROUNDED_SQUARE = 6,
};
// --- Estilos de arista. Misma convencion: 0 = "usa el estilo del
// RelationType". 0049f introducira dashed/dotted reales; ahora EDGE_SOLID
// es lo que sale por la GPU sea cual sea el valor.
enum EdgeStyle : uint8_t {
EDGE_USE_TYPE = 0,
EDGE_SOLID = 1,
EDGE_DASHED = 2,
EDGE_DOTTED = 3,
};
// --- Graph node ---
// 40 bytes en x86_64. Layout pensado para vertex-pulling: x/y/vx/vy primero
// (4 floats consecutivos = 16 bytes que el TBO RG32F lee tal cual), seguidos
// del paquete denso de overrides + flags + indices.
struct GraphNode {
uint32_t id;
float x, y; // position in layout space
float vx, vy; // velocity (used by force layout)
float size; // visual radius (default 4.0)
uint32_t color; // ABGR packed (0 = use default palette)
const char* label; // optional display label (nullptr = none)
uint32_t community; // group/cluster ID (for auto-coloring)
float value; // arbitrary metric (for sizing)
bool pinned; // if true, force layout won't move this node
float x, y, vx, vy;
uint16_t type_id; // index into GraphData::types
uint8_t shape_override; // SHAPE_*; 0 = usar EntityType
uint8_t flags; // NF_* mask; default NF_VISIBLE tras graph_node()
uint32_t color_override; // RGBA8 (R en LSB); 0 = usar EntityType
float size_override; // pixels; <= 0 = usar EntityType
uint32_t label_idx; // index en el string pool del consumer (0 = no label)
uint64_t user_data; // opaco, app-specific (e.g. id de la BD origen)
};
// --- Graph edge ---
// 24 bytes. weight separado del paquete de uint8_t para mantenerlo alineado.
struct GraphEdge {
uint32_t source; // index into GraphData::nodes
uint32_t target; // index into GraphData::nodes
float weight; // edge weight (affects attraction force)
uint32_t color; // ABGR packed (0 = default gray)
uint32_t source; // index into GraphData::nodes
uint32_t target; // index into GraphData::nodes
uint16_t type_id; // index into GraphData::rel_types
uint8_t style_override; // EDGE_*; 0 = usar RelationType
uint8_t flags; // EF_* mask; default EF_VISIBLE tras graph_edge()
float weight;
uint32_t label_idx;
};
// --- Entity type (per-node visual & semantic class) ---
// La tabla EntityType[] vive en GraphData::types. Los nodos referencian via
// `type_id`. El `name` es solo para UI/debug; el renderer no lo lee.
struct EntityType {
uint32_t color; // RGBA8 (R en LSB)
uint8_t shape; // SHAPE_* (no SHAPE_USE_TYPE — es el valor base)
uint16_t icon_id; // 0 = sin icono (issue 0049f)
float default_size; // pixels
const char* name; // C-string propiedad del caller
};
// --- Relation type (per-edge visual & semantic class) ---
struct RelationType {
uint32_t color; // RGBA8
uint8_t style; // EDGE_* (no EDGE_USE_TYPE)
float width; // pixels
const char* name;
};
// --- Graph container ---
// Memoria de `nodes`, `edges`, `types`, `rel_types` es propiedad del caller.
struct GraphData {
GraphNode* nodes;
int node_count;
GraphEdge* edges;
int edge_count;
GraphNode* nodes; int node_count; int node_capacity;
GraphEdge* edges; int edge_count; int edge_capacity;
EntityType* types; int type_count;
RelationType* rel_types; int rel_type_count;
// Bounding box (updated by layout)
// Bounding box en world space (actualizado por update_bounds / layout)
float min_x, min_y, max_x, max_y;
// Recompute bounding box from node positions
void update_bounds();
// Find node index by id. Returns -1 if not found.
int find_node(uint32_t id) const;
// Busqueda lineal por user_data. -1 si no existe. O(n).
int find_node_by_user_data(uint64_t user_data) const;
};
// --- Helper: create a default node ---
inline GraphNode graph_node(uint32_t id, float x = 0, float y = 0) {
return {id, x, y, 0, 0, 4.0f, 0, nullptr, 0, 0, false};
// --- Resoluciones de override (puras, exportadas para tests y consumers) ---
// Devuelven el valor "efectivo" combinando el override del nodo/arista con la
// tabla de tipos. Las funciones son seguras frente a tablas vacias o type_id
// fuera de rango: degradan a un default razonable (gris para color, circulo
// para shape, 4 px para size, solid para style, 1 px para width).
inline uint32_t resolve_node_color(const GraphNode& n,
const EntityType* types, int type_count) {
if (n.color_override != 0) return n.color_override;
if (types && n.type_id < (uint16_t)type_count) return types[n.type_id].color;
return 0xFF888888u;
}
// --- Helper: create an edge ---
inline GraphEdge graph_edge(uint32_t source, uint32_t target, float weight = 1.0f) {
return {source, target, weight, 0};
inline uint8_t resolve_node_shape(const GraphNode& n,
const EntityType* types, int type_count) {
if (n.shape_override != SHAPE_USE_TYPE) return n.shape_override;
if (types && n.type_id < (uint16_t)type_count) return types[n.type_id].shape;
return SHAPE_CIRCLE;
}
inline float resolve_node_size(const GraphNode& n,
const EntityType* types, int type_count) {
if (n.size_override > 0.0f) return n.size_override;
if (types && n.type_id < (uint16_t)type_count) return types[n.type_id].default_size;
return 4.0f;
}
inline uint32_t resolve_edge_color(const GraphEdge& e,
const RelationType* rel_types, int rel_type_count) {
if (rel_types && e.type_id < (uint16_t)rel_type_count) return rel_types[e.type_id].color;
return 0xFF888888u;
}
inline uint8_t resolve_edge_style(const GraphEdge& e,
const RelationType* rel_types, int rel_type_count) {
if (e.style_override != EDGE_USE_TYPE) return e.style_override;
if (rel_types && e.type_id < (uint16_t)rel_type_count) return rel_types[e.type_id].style;
return EDGE_SOLID;
}
inline float resolve_edge_width(const GraphEdge& e,
const RelationType* rel_types, int rel_type_count) {
if (rel_types && e.type_id < (uint16_t)rel_type_count) return rel_types[e.type_id].width;
return 1.0f;
}
// --- Helpers de construccion ergonomicos ---
// Defaults: NF_VISIBLE / EF_VISIBLE encendidos, type_id = 0, sin overrides.
// Asi un nodo recien creado se renderiza con la apariencia del primer
// EntityType (que el caller tiene que registrar) sin tocar nada mas.
inline GraphNode graph_node(float x = 0.0f, float y = 0.0f, uint16_t type_id = 0) {
GraphNode n{};
n.x = x; n.y = y;
n.vx = 0.0f; n.vy = 0.0f;
n.type_id = type_id;
n.shape_override = SHAPE_USE_TYPE;
n.flags = NF_VISIBLE;
n.color_override = 0u;
n.size_override = 0.0f;
n.label_idx = 0u;
n.user_data = 0ull;
return n;
}
inline GraphEdge graph_edge(uint32_t source, uint32_t target,
float weight = 1.0f, uint16_t type_id = 0) {
GraphEdge e{};
e.source = source;
e.target = target;
e.type_id = type_id;
e.style_override = EDGE_USE_TYPE;
e.flags = EF_VISIBLE;
e.weight = weight;
e.label_idx = 0u;
return e;
}
inline EntityType entity_type(uint32_t color, uint8_t shape = SHAPE_CIRCLE,
float default_size = 4.0f, const char* name = nullptr,
uint16_t icon_id = 0) {
return EntityType{ color, shape, icon_id, default_size, name };
}
inline RelationType relation_type(uint32_t color, uint8_t style = EDGE_SOLID,
float width = 1.0f, const char* name = nullptr) {
return RelationType{ color, style, width, name };
}
+27 -8
View File
@@ -139,7 +139,8 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
for (int i = 0; i < graph.node_count; ++i) {
xs_buf[i] = graph.nodes[i].x;
ys_buf[i] = graph.nodes[i].y;
sz_buf[i] = graph.nodes[i].size;
sz_buf[i] = resolve_node_size(graph.nodes[i],
graph.types, graph.type_count);
}
state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count);
}
@@ -216,12 +217,20 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
// -------------------------------------------------------------------
// 5c. Hover — query nearest node
// -------------------------------------------------------------------
// Clear-then-set: limpiamos NF_HOVERED del nodo previo aunque el cursor
// ya no este sobre el viewport (cambio de target). Esto evita que el
// flag persista cuando el usuario sale del widget.
int prev_hovered = state.hovered_node;
if (prev_hovered >= 0 && prev_hovered < graph.node_count) {
graph.nodes[prev_hovered].flags &= ~NF_HOVERED;
}
state.hovered_node = -1;
if (hovered && graph.node_count > 0) {
float hit_radius = 10.0f / state.zoom;
int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius);
if (nearest >= 0) {
state.hovered_node = nearest;
graph.nodes[nearest].flags |= NF_HOVERED;
interacted = true;
}
}
@@ -237,7 +246,7 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
} else {
// Release drag
if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
graph.nodes[state.drag_node].pinned = false;
graph.nodes[state.drag_node].flags &= ~NF_PINNED;
}
state.drag_node = -1;
state.is_dragging = false;
@@ -249,15 +258,21 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
n.y = gy_mouse;
n.vx = 0.0f;
n.vy = 0.0f;
n.pinned = true;
n.flags |= NF_PINNED;
interacted = true;
}
// -------------------------------------------------------------------
// 5e. Click — select node
// 5e. Click — select node (clear-then-set NF_SELECTED)
// -------------------------------------------------------------------
if (hovered && lm_click && state.drag_node == -1) {
if (state.selected_node >= 0 && state.selected_node < graph.node_count) {
graph.nodes[state.selected_node].flags &= ~NF_SELECTED;
}
state.selected_node = state.hovered_node;
if (state.selected_node >= 0 && state.selected_node < graph.node_count) {
graph.nodes[state.selected_node].flags |= NF_SELECTED;
}
interacted = true;
}
@@ -309,11 +324,15 @@ bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
}
ImGui::BeginTooltip();
if (n.label) ImGui::TextUnformatted(n.label);
ImGui::Text("ID: %u", n.id);
ImGui::Text("Community: %u", n.community);
if (graph.types && n.type_id < (uint16_t)graph.type_count
&& graph.types[n.type_id].name) {
ImGui::Text("Type: %s", graph.types[n.type_id].name);
} else {
ImGui::Text("Type: %u", (unsigned)n.type_id);
}
ImGui::Text("Index: %d", state.hovered_node);
if (n.user_data) ImGui::Text("user_data: %llu", (unsigned long long)n.user_data);
ImGui::Text("Degree: %d", degree);
ImGui::Text("Value: %.3f", n.value);
ImGui::EndTooltip();
}
+5 -1
View File
@@ -3,7 +3,7 @@ name: graph_viewport
kind: component
lang: cpp
domain: viz
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size)"
description: "Widget ImGui completo para visualizacion interactiva de grafos con pan, zoom, hover, seleccion y layout en vivo"
@@ -111,6 +111,10 @@ En la parte inferior del widget aparece: numero de nodos, aristas, zoom actual,
El renderer OpenGL y el spatial hash se crean en el primer frame. La camara se ajusta automaticamente con `graph_viewport_fit` en la inicializacion.
## Notas de version
- **v1.1** (2026-04-29, issue 0049e): adapta el viewport al modelo extendido. Hover/seleccion ahora se publican tambien como `flags |= NF_HOVERED` / `NF_SELECTED` en el grafo (clear-then-set) — los `state.hovered_node` / `selected_node` siguen siendo la API estable. El drag usa `flags |= NF_PINNED` en lugar del campo `pinned` desaparecido. El tooltip muestra `Type` (nombre del EntityType si esta) y `user_data` en lugar de `community`/`value`/`label`/`id`.
## Notas de implementacion
- Usa `ImGui::InvisibleButton` con flags para los tres botones del raton, capturando input sin dibujar ningun boton visible.