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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 22:44:40 +02:00
parent a6e3298f1b
commit b9ffc13caf
19 changed files with 756 additions and 177 deletions
+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();
}