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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user