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