feat(viz): renderer shapes/iconos/flechas/edge-styles (issue 0049f)
graph_renderer 1.5.0: - 6 shapes SDF (circle, square, diamond, hex, triangle, rounded square) con dispatch en fragment shader y AA via fwidth. - Atlas opcional de iconos Tabler bakeado por graph_icons; el shader compone overlay desde un uniform vec4 u_icon_uvs[256]. Setter publico graph_renderer_set_icon_atlas(r, tex, uv_table, count). - Aristas direccionales: 6 vertices por arista (line + chevron de la flecha) en una sola draw call; segmento principal acortado por el radio del nodo target. - Edge styles solid/dashed/dotted via descarte por arc_length en el fragment shader; las lineas del chevron son siempre solidas. graph_icons 1.0.0 (nuevo): - Atlas RGBA8 512x512 = grid 16x16 (256 iconos max) bakeado con stb_truetype desde tabler-icons.ttf. - API: graph_icons_build/texture/region/uv_table/destroy. icon_id es 1-based; 0 reservado para "sin icono". - Hook FN_GRAPH_ICONS_SKIP_GL=1 para tests sin contexto GL. Demo demos_graph_styles en primitives_gallery: 6 EntityTypes (uno por shape) con icono Tabler representativo + 3 RelationTypes (knows/uses/ owns) con flechas direccionales y los 3 estilos. test_graph_icons: 6 casos cubriendo bake, regiones 1-indexed, uv_table consistente, layout en grid 16x16, validacion de count fuera de rango, y verificacion de alpha != 0 en las celdas tras bake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
#include "demos.h"
|
||||
#include "demo.h"
|
||||
|
||||
#include "viz/graph_types.h"
|
||||
#include "viz/graph_viewport.h"
|
||||
#include "viz/graph_renderer.h"
|
||||
#include "viz/graph_force_layout.h"
|
||||
#include "viz/graph_icons.h"
|
||||
#include "core/button.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace gallery {
|
||||
|
||||
// 6 codepoints Tabler representativos para los 6 EntityTypes del demo. El
|
||||
// orden coincide con `s_entity_types[i]`: cada tipo apunta a `icon_id = i+1`
|
||||
// (las regiones del atlas son 1-indexed; 0 reservado para "sin icono").
|
||||
static const uint16_t k_demo_codepoints[6] = {
|
||||
0xEB4Du, // TI_USER
|
||||
0xEAE5u, // TI_MAIL
|
||||
0xEAB9u, // TI_GLOBE
|
||||
0xEB09u, // TI_PHONE
|
||||
0xEA4Fu, // TI_BUILDING
|
||||
0xEA88u, // TI_DATABASE
|
||||
};
|
||||
|
||||
static const uint32_t k_styles_palette[6] = {
|
||||
0xFF6BCB77u, // verde — Person (circle)
|
||||
0xFFFF6B6Bu, // rojo — Email (square)
|
||||
0xFF4D96FFu, // azul — Domain (diamond)
|
||||
0xFFFFC75Fu, // ambar — Phone (hex)
|
||||
0xFFC780E8u, // morado — Org (triangle)
|
||||
0xFF52CDF2u, // cyan — Database (rounded square)
|
||||
};
|
||||
|
||||
static const char* k_styles_names[6] = {
|
||||
"Person", "Email", "Domain", "Phone", "Organization", "Database"
|
||||
};
|
||||
|
||||
static EntityType s_entity_types[6];
|
||||
static RelationType s_relation_types[3]; // solid, dashed, dotted
|
||||
static IconAtlas* s_atlas = nullptr;
|
||||
static bool s_types_initialized = false;
|
||||
static bool s_atlas_bound = false;
|
||||
|
||||
static void init_demo_types() {
|
||||
if (s_types_initialized) return;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
EntityType t{};
|
||||
t.color = k_styles_palette[i];
|
||||
t.shape = (uint8_t)(SHAPE_CIRCLE + i); // 1..6 — uno por shape
|
||||
t.icon_id = (uint16_t)(i + 1); // 1-based
|
||||
t.default_size = 14.0f;
|
||||
t.name = k_styles_names[i];
|
||||
s_entity_types[i] = t;
|
||||
}
|
||||
s_relation_types[0] = relation_type(0xFFCCCCCCu, EDGE_SOLID, 1.5f, "knows");
|
||||
s_relation_types[1] = relation_type(0xFFFFB870u, EDGE_DASHED, 1.5f, "uses");
|
||||
s_relation_types[2] = relation_type(0xFF89E0FCu, EDGE_DOTTED, 1.5f, "owns");
|
||||
s_types_initialized = true;
|
||||
}
|
||||
|
||||
// 30 nodos posicionados en un anillo por tipo. Aristas: cada nodo conecta a
|
||||
// sus dos vecinos (arc) y a un nodo "central" del cluster siguiente. Mezcla
|
||||
// de directed/undirected para validar las flechas.
|
||||
static void build_demo_graph(std::vector<GraphNode>& nodes,
|
||||
std::vector<GraphEdge>& edges)
|
||||
{
|
||||
nodes.clear();
|
||||
edges.clear();
|
||||
|
||||
const int per_type = 5;
|
||||
const float ring_r = 80.0f;
|
||||
const float type_r = 30.0f;
|
||||
|
||||
for (int t = 0; t < 6; ++t) {
|
||||
float ang_t = (float)t * (2.0f * 3.14159265f / 6.0f);
|
||||
float cx = std::cos(ang_t) * ring_r;
|
||||
float cy = std::sin(ang_t) * ring_r;
|
||||
for (int k = 0; k < per_type; ++k) {
|
||||
float a = (float)k * (2.0f * 3.14159265f / per_type) + ang_t * 0.3f;
|
||||
GraphNode n = graph_node(cx + std::cos(a) * type_r,
|
||||
cy + std::sin(a) * type_r,
|
||||
(uint16_t)t);
|
||||
n.user_data = (uint64_t)nodes.size();
|
||||
nodes.push_back(n);
|
||||
}
|
||||
}
|
||||
|
||||
auto idx = [&](int t, int k) { return (uint32_t)(t * per_type + k); };
|
||||
|
||||
for (int t = 0; t < 6; ++t) {
|
||||
// Aristas intra-cluster (knows = solid, undirected).
|
||||
for (int k = 0; k < per_type; ++k) {
|
||||
int next_k = (k + 1) % per_type;
|
||||
GraphEdge e = graph_edge(idx(t, k), idx(t, next_k), 1.0f, /*type_id=*/0);
|
||||
edges.push_back(e);
|
||||
}
|
||||
// Inter-cluster: del nodo 0 del cluster t al nodo 0 del cluster t+1
|
||||
// como "uses" (dashed, directed).
|
||||
int t_next = (t + 1) % 6;
|
||||
GraphEdge e1 = graph_edge(idx(t, 0), idx(t_next, 0), 1.0f, /*type_id=*/1);
|
||||
e1.flags |= EF_DIRECTED;
|
||||
edges.push_back(e1);
|
||||
|
||||
// Y otra inter-cluster mas larga al cluster +2 como "owns" (dotted,
|
||||
// directed). Asi se ven las 3 estilos a la vez.
|
||||
int t_far = (t + 2) % 6;
|
||||
GraphEdge e2 = graph_edge(idx(t, 2), idx(t_far, 3), 0.6f, /*type_id=*/2);
|
||||
e2.flags |= EF_DIRECTED;
|
||||
edges.push_back(e2);
|
||||
}
|
||||
}
|
||||
|
||||
void demo_graph_styles() {
|
||||
demo_header("graph_renderer (shapes + icons + arrows + edge styles)", "v1.5.0",
|
||||
"OSINT-style: 6 EntityTypes, uno por shape (circle, square, diamond, hex, "
|
||||
"triangle, rounded square) con icono Tabler en el centro. 3 RelationTypes "
|
||||
"(solid/dashed/dotted) con flechas en los aristas EF_DIRECTED. Mismas dos "
|
||||
"draw calls que el viewport normal (1 nodos + 1 aristas).");
|
||||
|
||||
init_demo_types();
|
||||
|
||||
static std::vector<GraphNode> s_nodes;
|
||||
static std::vector<GraphEdge> s_edges;
|
||||
static GraphData s_graph{};
|
||||
static GraphViewportState s_state;
|
||||
static bool s_initialized = false;
|
||||
static bool s_run_layout = false;
|
||||
|
||||
if (!s_initialized) {
|
||||
build_demo_graph(s_nodes, s_edges);
|
||||
s_graph.nodes = s_nodes.data();
|
||||
s_graph.node_count = (int)s_nodes.size();
|
||||
s_graph.node_capacity = (int)s_nodes.capacity();
|
||||
s_graph.edges = s_edges.data();
|
||||
s_graph.edge_count = (int)s_edges.size();
|
||||
s_graph.edge_capacity = (int)s_edges.capacity();
|
||||
s_graph.types = s_entity_types;
|
||||
s_graph.type_count = 6;
|
||||
s_graph.rel_types = s_relation_types;
|
||||
s_graph.rel_type_count = 3;
|
||||
s_graph.update_bounds();
|
||||
s_state.layout_running = false; // queremos ver las shapes posicionadas, no el caos del force
|
||||
s_state.zoom = 2.0f;
|
||||
s_initialized = true;
|
||||
}
|
||||
|
||||
section("Legend");
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
ImGui::Text("%-13s shape=%d icon_id=%d color=#%06x",
|
||||
k_styles_names[i],
|
||||
(int)s_entity_types[i].shape,
|
||||
(int)s_entity_types[i].icon_id,
|
||||
(unsigned)(s_entity_types[i].color & 0x00FFFFFFu));
|
||||
}
|
||||
ImGui::Text("Edges: knows=solid, uses=dashed (directed), owns=dotted (directed)");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
section("Controls");
|
||||
{
|
||||
using namespace fn_ui;
|
||||
if (button(s_run_layout ? "Pause force layout" : "Run force layout",
|
||||
ButtonVariant::Secondary)) {
|
||||
s_run_layout = !s_run_layout;
|
||||
s_state.layout_running = s_run_layout;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (button("Rebuild", ButtonVariant::Subtle)) {
|
||||
build_demo_graph(s_nodes, s_edges);
|
||||
s_graph.nodes = s_nodes.data();
|
||||
s_graph.node_count = (int)s_nodes.size();
|
||||
s_graph.edges = s_edges.data();
|
||||
s_graph.edge_count = (int)s_edges.size();
|
||||
s_graph.update_bounds();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (button("Fit view", ButtonVariant::Subtle)) {
|
||||
graph_viewport_fit(s_graph, s_state);
|
||||
}
|
||||
}
|
||||
|
||||
section("Viewport");
|
||||
if (s_run_layout) {
|
||||
ForceLayoutConfig cfg;
|
||||
cfg.repulsion = 1500.0f;
|
||||
cfg.attraction = 0.04f;
|
||||
cfg.gravity = 0.005f;
|
||||
cfg.iterations = 1;
|
||||
graph_force_layout_step(s_graph, cfg);
|
||||
}
|
||||
|
||||
// El viewport crea internamente el GraphRenderer. La primera vez que se
|
||||
// dibuja el panel, el renderer existe — bindeamos el atlas justo despues.
|
||||
graph_viewport("##graph_styles", s_graph, s_state, ImVec2(0, 460));
|
||||
|
||||
if (!s_atlas_bound && s_state.renderer) {
|
||||
s_atlas = graph_icons_build(k_demo_codepoints, 6, 32);
|
||||
if (s_atlas) {
|
||||
graph_renderer_set_icon_atlas(s_state.renderer,
|
||||
graph_icons_texture(s_atlas),
|
||||
graph_icons_uv_table(s_atlas),
|
||||
graph_icons_count(s_atlas));
|
||||
s_atlas_bound = true;
|
||||
} else {
|
||||
// Sin atlas: marcamos como bound para no reintentar cada frame —
|
||||
// el renderer simplemente pinta sin overlay de iconos.
|
||||
s_atlas_bound = true;
|
||||
}
|
||||
}
|
||||
|
||||
code_block(
|
||||
"// Build atlas con 6 codepoints Tabler\n"
|
||||
"const uint16_t cps[] = {0xEB4D, 0xEAE5, 0xEAB9, 0xEB09, 0xEA4F, 0xEA88};\n"
|
||||
"IconAtlas* atlas = graph_icons_build(cps, 6, 32);\n"
|
||||
"\n"
|
||||
"// EntityTypes: cada uno con su shape e icono\n"
|
||||
"EntityType person = {0xFF6BCB77, SHAPE_CIRCLE, /*icon_id=*/1, 14, \"Person\"};\n"
|
||||
"EntityType email = {0xFFFF6B6B, SHAPE_SQUARE, /*icon_id=*/2, 14, \"Email\"};\n"
|
||||
"// ... etc\n"
|
||||
"\n"
|
||||
"// RelationTypes: solid / dashed / dotted\n"
|
||||
"RelationType knows = relation_type(0xFFCCCCCC, EDGE_SOLID, 1.5f, \"knows\");\n"
|
||||
"RelationType uses = relation_type(0xFFFFB870, EDGE_DASHED, 1.5f, \"uses\");\n"
|
||||
"\n"
|
||||
"// Bind atlas al renderer\n"
|
||||
"graph_renderer_set_icon_atlas(renderer, graph_icons_texture(atlas),\n"
|
||||
" graph_icons_uv_table(atlas),\n"
|
||||
" graph_icons_count(atlas));\n"
|
||||
"\n"
|
||||
"// Aristas direccionales\n"
|
||||
"GraphEdge e = graph_edge(src, tgt, 1.0f, /*type_id=*/1);\n"
|
||||
"e.flags |= EF_DIRECTED;");
|
||||
}
|
||||
|
||||
} // namespace gallery
|
||||
Reference in New Issue
Block a user