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:
@@ -5,6 +5,7 @@ add_imgui_app(primitives_gallery
|
|||||||
demos_core.cpp
|
demos_core.cpp
|
||||||
demos_viz.cpp
|
demos_viz.cpp
|
||||||
demos_graph.cpp
|
demos_graph.cpp
|
||||||
|
demos_graph_styles.cpp
|
||||||
demos_gfx.cpp
|
demos_gfx.cpp
|
||||||
demos_3d.cpp
|
demos_3d.cpp
|
||||||
demos_text_editor.cpp
|
demos_text_editor.cpp
|
||||||
@@ -66,6 +67,7 @@ add_imgui_app(primitives_gallery
|
|||||||
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
|
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
|
||||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
|
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
|
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/functions/viz/graph_icons.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp
|
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp
|
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp
|
||||||
${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp
|
${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ void demo_scatter_plot();
|
|||||||
void demo_histogram();
|
void demo_histogram();
|
||||||
void demo_sparkline();
|
void demo_sparkline();
|
||||||
void demo_graph();
|
void demo_graph();
|
||||||
|
void demo_graph_styles(); // issue 0049f
|
||||||
void demo_candlestick();
|
void demo_candlestick();
|
||||||
void demo_gauge();
|
void demo_gauge();
|
||||||
void demo_heatmap();
|
void demo_heatmap();
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -64,6 +64,7 @@ static const DemoEntry k_demos[] = {
|
|||||||
{"histogram", "histogram", "Viz", &gallery::demo_histogram},
|
{"histogram", "histogram", "Viz", &gallery::demo_histogram},
|
||||||
{"sparkline", "sparkline", "Viz", &gallery::demo_sparkline},
|
{"sparkline", "sparkline", "Viz", &gallery::demo_sparkline},
|
||||||
{"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph},
|
{"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph},
|
||||||
|
{"graph_styles", "graph_styles", "Viz", &gallery::demo_graph_styles}, // issue 0049f
|
||||||
{"candlestick", "candlestick", "Viz", &gallery::demo_candlestick},
|
{"candlestick", "candlestick", "Viz", &gallery::demo_candlestick},
|
||||||
{"gauge", "gauge", "Viz", &gallery::demo_gauge},
|
{"gauge", "gauge", "Viz", &gallery::demo_gauge},
|
||||||
{"heatmap", "heatmap", "Viz", &gallery::demo_heatmap},
|
{"heatmap", "heatmap", "Viz", &gallery::demo_heatmap},
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
#include "viz/graph_icons.h"
|
||||||
|
|
||||||
|
#include "gfx/gl_loader.h"
|
||||||
|
|
||||||
|
// stb_truetype esta vendor-ada por ImGui. La declaracion `STBTT_DEF static`
|
||||||
|
// hace que cada TU tenga su propia copia de las funciones — no colisionamos
|
||||||
|
// con el `STB_TRUETYPE_IMPLEMENTATION` que ya esta en `imgui_draw.cpp`.
|
||||||
|
#define STB_TRUETYPE_IMPLEMENTATION
|
||||||
|
#include "imstb_truetype.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef FN_CPP_ROOT
|
||||||
|
#define FN_CPP_ROOT ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Hook para tests sin contexto GL. Se setea via la variable de entorno
|
||||||
|
// `FN_GRAPH_ICONS_SKIP_GL=1` antes de llamar a `graph_icons_build`. Cuando
|
||||||
|
// esta activo, el atlas se construye en CPU pero `gl_tex` queda en 0 (los
|
||||||
|
// tests pueden inspeccionar `pixels` y `regions`/`uv_table` sin GL).
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool skip_gl_upload() {
|
||||||
|
const char* v = std::getenv("FN_GRAPH_ICONS_SKIP_GL");
|
||||||
|
return v && v[0] && v[0] != '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int k_atlas_w = 512;
|
||||||
|
constexpr int k_atlas_h = 512;
|
||||||
|
constexpr int k_grid = 16; // 16x16 celdas
|
||||||
|
constexpr int k_max_icons = k_grid * k_grid;
|
||||||
|
|
||||||
|
bool file_exists(const char* path) {
|
||||||
|
if (!path || !*path) return false;
|
||||||
|
if (FILE* f = std::fopen(path, "rb")) { std::fclose(f); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mismo orden de busqueda que `icon_font.cpp` para que cualquier app del
|
||||||
|
// registry encuentre el TTF tras el script de copy de assets.
|
||||||
|
std::string find_tabler_ttf() {
|
||||||
|
const char* fname = "tabler-icons.ttf";
|
||||||
|
std::string p;
|
||||||
|
p = std::string("./") + fname; if (file_exists(p.c_str())) return p;
|
||||||
|
p = std::string("./assets/") + fname; if (file_exists(p.c_str())) return p;
|
||||||
|
if (const char* env = std::getenv("FN_ASSETS_DIR")) {
|
||||||
|
p = std::string(env) + "/" + fname;
|
||||||
|
if (file_exists(p.c_str())) return p;
|
||||||
|
}
|
||||||
|
if (std::strlen(FN_CPP_ROOT) > 0) {
|
||||||
|
p = std::string(FN_CPP_ROOT) + "/vendor/tabler-icons/tabler-icons.ttf";
|
||||||
|
if (file_exists(p.c_str())) return p;
|
||||||
|
}
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> read_file_bytes(const char* path) {
|
||||||
|
std::vector<unsigned char> out;
|
||||||
|
FILE* f = std::fopen(path, "rb");
|
||||||
|
if (!f) return out;
|
||||||
|
std::fseek(f, 0, SEEK_END);
|
||||||
|
long sz = std::ftell(f);
|
||||||
|
std::fseek(f, 0, SEEK_SET);
|
||||||
|
if (sz > 0) {
|
||||||
|
out.resize((size_t)sz);
|
||||||
|
size_t rd = std::fread(out.data(), 1, (size_t)sz, f);
|
||||||
|
if (rd != (size_t)sz) out.clear();
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct IconAtlas {
|
||||||
|
unsigned int gl_tex = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int icon_px = 32;
|
||||||
|
int count = 0;
|
||||||
|
std::vector<IconRegion> regions; // index 0 dummy (id=0 -> nullptr)
|
||||||
|
std::vector<unsigned char> pixels_rgba; // CPU copy para tests
|
||||||
|
std::vector<float> uv_table; // count*4 floats, 0-indexed
|
||||||
|
};
|
||||||
|
|
||||||
|
IconAtlas* graph_icons_build(const uint16_t* codepoints, int count, int icon_px) {
|
||||||
|
if (!codepoints || count <= 0 || count > k_max_icons) return nullptr;
|
||||||
|
if (icon_px <= 0) icon_px = 32;
|
||||||
|
if (icon_px > k_atlas_w / k_grid) icon_px = k_atlas_w / k_grid;
|
||||||
|
|
||||||
|
std::string ttf_path = find_tabler_ttf();
|
||||||
|
if (ttf_path.empty()) {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[graph_icons] tabler-icons.ttf no encontrado. Buscado en ./, "
|
||||||
|
"./assets/, $FN_ASSETS_DIR, %s/vendor/tabler-icons/\n",
|
||||||
|
FN_CPP_ROOT[0] ? FN_CPP_ROOT : "(FN_CPP_ROOT vacio)");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto ttf_bytes = read_file_bytes(ttf_path.c_str());
|
||||||
|
if (ttf_bytes.empty()) {
|
||||||
|
std::fprintf(stderr, "[graph_icons] no pude leer %s\n", ttf_path.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
stbtt_fontinfo font;
|
||||||
|
if (!stbtt_InitFont(&font, ttf_bytes.data(),
|
||||||
|
stbtt_GetFontOffsetForIndex(ttf_bytes.data(), 0))) {
|
||||||
|
std::fprintf(stderr, "[graph_icons] stbtt_InitFont fallo\n");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
IconAtlas* a = new IconAtlas();
|
||||||
|
a->width = k_atlas_w;
|
||||||
|
a->height = k_atlas_h;
|
||||||
|
a->icon_px = icon_px;
|
||||||
|
a->count = count;
|
||||||
|
a->regions.reserve((size_t)count + 1);
|
||||||
|
a->regions.push_back({0, 0, 0.f, 0.f, 0.f, 0.f}); // id=0 reservado
|
||||||
|
a->pixels_rgba.assign((size_t)k_atlas_w * (size_t)k_atlas_h * 4, 0);
|
||||||
|
a->uv_table.assign((size_t)count * 4, 0.f);
|
||||||
|
|
||||||
|
// Padding 1 px dentro de cada celda para que el filtrado linear no muestre
|
||||||
|
// pixels del icono vecino al sumar `fwidth`.
|
||||||
|
const int cell = k_atlas_w / k_grid; // 32 px
|
||||||
|
const int padding = 1;
|
||||||
|
const int target = cell - 2 * padding; // 30 px dentro de la celda
|
||||||
|
|
||||||
|
const float scale = stbtt_ScaleForPixelHeight(&font, (float)target);
|
||||||
|
|
||||||
|
int ascent = 0, descent = 0, lineGap = 0;
|
||||||
|
stbtt_GetFontVMetrics(&font, &ascent, &descent, &lineGap);
|
||||||
|
const float baseline = (float)ascent * scale;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
const uint16_t cp = codepoints[i];
|
||||||
|
const int row = i / k_grid;
|
||||||
|
const int col = i % k_grid;
|
||||||
|
const int cx0 = col * cell;
|
||||||
|
const int cy0 = row * cell;
|
||||||
|
|
||||||
|
// Bitmap box en pixels (relativo al baseline).
|
||||||
|
int x0, y0, x1, y1;
|
||||||
|
stbtt_GetCodepointBitmapBox(&font, cp, scale, scale, &x0, &y0, &x1, &y1);
|
||||||
|
const int gw = x1 - x0;
|
||||||
|
const int gh = y1 - y0;
|
||||||
|
|
||||||
|
// Alinear glifo dentro de la celda manteniendo el padding y el centrado.
|
||||||
|
// Algunos iconos no caben en `target` exacto (Tabler tiene viewBox uniforme
|
||||||
|
// pero el bitmap puede salirse 1-2 px). Si gw > target lo recortamos al
|
||||||
|
// mismo target — la imagen sale ligeramente comprimida pero no pisa la
|
||||||
|
// celda vecina.
|
||||||
|
const int draw_w = (gw > target) ? target : gw;
|
||||||
|
const int draw_h = (gh > target) ? target : gh;
|
||||||
|
const int dx = cx0 + padding + (target - draw_w) / 2;
|
||||||
|
const int dy = cy0 + padding + (target - draw_h) / 2;
|
||||||
|
|
||||||
|
if (draw_w > 0 && draw_h > 0) {
|
||||||
|
std::vector<unsigned char> mono((size_t)draw_w * (size_t)draw_h, 0);
|
||||||
|
stbtt_MakeCodepointBitmap(&font, mono.data(),
|
||||||
|
draw_w, draw_h, draw_w,
|
||||||
|
scale, scale, cp);
|
||||||
|
|
||||||
|
// Copia mono → RGBA (R=G=B=255, A=mono).
|
||||||
|
for (int yy = 0; yy < draw_h; ++yy) {
|
||||||
|
for (int xx = 0; xx < draw_w; ++xx) {
|
||||||
|
const unsigned char alpha = mono[(size_t)yy * draw_w + xx];
|
||||||
|
if (alpha == 0) continue;
|
||||||
|
const size_t off = ((size_t)(dy + yy) * k_atlas_w + (size_t)(dx + xx)) * 4;
|
||||||
|
a->pixels_rgba[off + 0] = 0xFFu;
|
||||||
|
a->pixels_rgba[off + 1] = 0xFFu;
|
||||||
|
a->pixels_rgba[off + 2] = 0xFFu;
|
||||||
|
a->pixels_rgba[off + 3] = alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UVs: bordes externos de la celda (padding fuera de los UVs para que el
|
||||||
|
// shader pueda hacer un overlay perfectamente cuadrado sin bleed).
|
||||||
|
IconRegion r{};
|
||||||
|
r.id = (uint16_t)(i + 1);
|
||||||
|
r.codepoint = cp;
|
||||||
|
r.u0 = (float)(cx0 + padding) / (float)k_atlas_w;
|
||||||
|
r.v0 = (float)(cy0 + padding) / (float)k_atlas_h;
|
||||||
|
r.u1 = (float)(cx0 + cell - padding) / (float)k_atlas_w;
|
||||||
|
r.v1 = (float)(cy0 + cell - padding) / (float)k_atlas_h;
|
||||||
|
a->regions.push_back(r);
|
||||||
|
a->uv_table[(size_t)i * 4 + 0] = r.u0;
|
||||||
|
a->uv_table[(size_t)i * 4 + 1] = r.v0;
|
||||||
|
a->uv_table[(size_t)i * 4 + 2] = r.u1;
|
||||||
|
a->uv_table[(size_t)i * 4 + 3] = r.v1;
|
||||||
|
(void)baseline; // baseline no se usa en este layout (cada celda absorbe el offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip_gl_upload()) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subir a GPU.
|
||||||
|
glGenTextures(1, &a->gl_tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, a->gl_tex);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, a->width, a->height, 0,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, a->pixels_rgba.data());
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int graph_icons_texture(const IconAtlas* a) {
|
||||||
|
return a ? a->gl_tex : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconRegion* graph_icons_region(const IconAtlas* a, uint16_t icon_id) {
|
||||||
|
if (!a || icon_id == 0) return nullptr;
|
||||||
|
if (icon_id >= (uint16_t)a->regions.size()) return nullptr;
|
||||||
|
return &a->regions[icon_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
int graph_icons_count(const IconAtlas* a) { return a ? a->count : 0; }
|
||||||
|
int graph_icons_width(const IconAtlas* a) { return a ? a->width : 0; }
|
||||||
|
int graph_icons_height(const IconAtlas* a) { return a ? a->height : 0; }
|
||||||
|
const unsigned char* graph_icons_pixels(const IconAtlas* a) {
|
||||||
|
return a ? a->pixels_rgba.data() : nullptr;
|
||||||
|
}
|
||||||
|
const float* graph_icons_uv_table(const IconAtlas* a) {
|
||||||
|
return a ? a->uv_table.data() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void graph_icons_destroy(IconAtlas* a) {
|
||||||
|
if (!a) return;
|
||||||
|
if (a->gl_tex) glDeleteTextures(1, &a->gl_tex);
|
||||||
|
delete a;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Atlas de iconos Tabler para `graph_renderer`. Bakea N codepoints (0xE000-
|
||||||
|
// 0xFCFF) en una textura RGBA 512×512 organizada como grid de 16×16 celdas
|
||||||
|
// de 32 px cada una. Cada icono se rasteriza con `stb_truetype` desde
|
||||||
|
// `tabler-icons.ttf`. La textura se sube a GPU como `GL_RGBA8` con filtrado
|
||||||
|
// linear.
|
||||||
|
//
|
||||||
|
// Convencion de IDs: `icon_id = 0` significa "sin icono". Las regiones
|
||||||
|
// devueltas por `graph_icons_build` tienen `id = i + 1`, donde `i` es la
|
||||||
|
// posicion del codepoint en el array de entrada. De esta forma un nodo o un
|
||||||
|
// EntityType con `icon_id == 0` (default tras `graph_node`/`entity_type`) se
|
||||||
|
// pinta sin icono superpuesto.
|
||||||
|
|
||||||
|
struct IconAtlas;
|
||||||
|
|
||||||
|
struct IconRegion {
|
||||||
|
uint16_t id; // 1-based; 0 reservado para "sin icono"
|
||||||
|
uint16_t codepoint; // codepoint Unicode original (debug)
|
||||||
|
float u0, v0, u1, v1; // UVs en [0,1] dentro del atlas
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construye el atlas. `count` puede ser 1..256 (limite del grid 16×16).
|
||||||
|
// `icon_px` controla el tamano de rasterizacion; tipicamente 32 para grid de
|
||||||
|
// 32 px sin re-escalado.
|
||||||
|
//
|
||||||
|
// Devuelve `nullptr` si no encuentra `tabler-icons.ttf` o si `count` esta
|
||||||
|
// fuera de rango. El TTF se busca en (en orden):
|
||||||
|
// 1. `./tabler-icons.ttf`
|
||||||
|
// 2. `./assets/tabler-icons.ttf`
|
||||||
|
// 3. `$FN_ASSETS_DIR/tabler-icons.ttf`
|
||||||
|
// 4. `${FN_CPP_ROOT}/vendor/tabler-icons/tabler-icons.ttf`
|
||||||
|
//
|
||||||
|
// Requiere un contexto OpenGL valido en el hilo actual (sube la textura).
|
||||||
|
IconAtlas* graph_icons_build(const uint16_t* codepoints, int count, int icon_px = 32);
|
||||||
|
|
||||||
|
// GL texture id (RGBA8) — lo consume `graph_renderer` como `samplerBuffer u_icon_atlas`.
|
||||||
|
unsigned int graph_icons_texture(const IconAtlas*);
|
||||||
|
|
||||||
|
// Devuelve la region por icon_id (1-based). nullptr si fuera de rango.
|
||||||
|
const IconRegion* graph_icons_region(const IconAtlas*, uint16_t icon_id);
|
||||||
|
|
||||||
|
// Numero de iconos cargados.
|
||||||
|
int graph_icons_count(const IconAtlas*);
|
||||||
|
|
||||||
|
// Dimensiones del atlas en pixels (siempre 512×512 actualmente).
|
||||||
|
int graph_icons_width(const IconAtlas*);
|
||||||
|
int graph_icons_height(const IconAtlas*);
|
||||||
|
|
||||||
|
// Acceso al bitmap RGBA en CPU (para tests / debug). Layout: row-major,
|
||||||
|
// `width * height * 4` bytes. NULL si el atlas se construyo sin retener
|
||||||
|
// pixels (por defecto se retienen para tests).
|
||||||
|
const unsigned char* graph_icons_pixels(const IconAtlas*);
|
||||||
|
|
||||||
|
// Tabla plana de UVs lista para subir como uniform array al shader. Layout:
|
||||||
|
// `count * 4` floats consecutivos (u0, v0, u1, v1) en el orden de
|
||||||
|
// codepoints pasado a `_build` (0-indexed: el icono con `icon_id == k`
|
||||||
|
// vive en `uv_table[(k-1)*4 .. (k-1)*4 + 4]`).
|
||||||
|
const float* graph_icons_uv_table(const IconAtlas*);
|
||||||
|
|
||||||
|
void graph_icons_destroy(IconAtlas*);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: graph_icons
|
||||||
|
kind: function
|
||||||
|
lang: cpp
|
||||||
|
domain: viz
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "IconAtlas* graph_icons_build(const uint16_t* codepoints, int count, int icon_px)"
|
||||||
|
description: "Atlas RGBA 512x512 con iconos Tabler bakeados via stb_truetype, consumido por graph_renderer para overlay de iconos en nodos del grafo"
|
||||||
|
tags: [graph, atlas, icons, tabler, opengl, gpu, stb_truetype]
|
||||||
|
uses_functions: ["gl_loader_cpp_gfx"]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [imgui]
|
||||||
|
tested: true
|
||||||
|
tests: ["build with 6 codepoints produces non-empty regions", "icon_id=0 returns nullptr", "icon_id out of range returns nullptr", "atlas dimensions are 512x512"]
|
||||||
|
test_file_path: "cpp/tests/test_graph_icons.cpp"
|
||||||
|
file_path: "cpp/functions/viz/graph_icons.cpp"
|
||||||
|
framework: imgui
|
||||||
|
params:
|
||||||
|
- name: codepoints
|
||||||
|
desc: "Array de codepoints Unicode (uint16_t) en el rango Tabler 0xE000-0xFCFF — los TI_* del header icons_tabler.h apuntan a estos codepoints"
|
||||||
|
- name: count
|
||||||
|
desc: "Numero de iconos a bakear. Limite 256 (grid 16x16 dentro del atlas 512x512)"
|
||||||
|
- name: icon_px
|
||||||
|
desc: "Tamano de rasterizacion en pixels. 32 por defecto — coincide con el tamano de celda y evita re-escalado"
|
||||||
|
output: "Handle opaco al atlas. Texture id GL_RGBA8 accesible via graph_icons_texture(); regiones por icon_id (1-based) via graph_icons_region(). icon_id=0 reservado para significar 'sin icono'"
|
||||||
|
---
|
||||||
|
|
||||||
|
# graph_icons
|
||||||
|
|
||||||
|
Builder de atlas de iconos Tabler para `graph_renderer`. Bakea hasta 256 codepoints en una textura RGBA8 de 512×512 organizada como grid 16×16 de celdas de 32 px.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct IconAtlas;
|
||||||
|
struct IconRegion {
|
||||||
|
uint16_t id; // 1-based; 0 = "sin icono"
|
||||||
|
uint16_t codepoint;
|
||||||
|
float u0, v0, u1, v1; // UVs en [0,1]
|
||||||
|
};
|
||||||
|
|
||||||
|
IconAtlas* graph_icons_build(const uint16_t* codepoints, int count, int icon_px = 32);
|
||||||
|
unsigned int graph_icons_texture(const IconAtlas*);
|
||||||
|
const IconRegion* graph_icons_region(const IconAtlas*, uint16_t icon_id);
|
||||||
|
int graph_icons_count(const IconAtlas*);
|
||||||
|
int graph_icons_width(const IconAtlas*);
|
||||||
|
int graph_icons_height(const IconAtlas*);
|
||||||
|
const unsigned char* graph_icons_pixels(const IconAtlas*); // CPU copy para tests
|
||||||
|
void graph_icons_destroy(IconAtlas*);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
const uint16_t cps[] = {
|
||||||
|
0xEB4Du, // TI_USER
|
||||||
|
0xEAE5u, // TI_MAIL
|
||||||
|
0xEAB9u, // TI_GLOBE
|
||||||
|
0xEB09u, // TI_PHONE
|
||||||
|
0xEA4Fu, // TI_BUILDING
|
||||||
|
0xEA88u, // TI_DATABASE
|
||||||
|
};
|
||||||
|
IconAtlas* atlas = graph_icons_build(cps, 6);
|
||||||
|
|
||||||
|
// EntityType refiere por icon_id (1-based):
|
||||||
|
EntityType person = entity_type(0xFF4CAF50, SHAPE_CIRCLE, 12.0f, "Person", 1);
|
||||||
|
EntityType email = entity_type(0xFFF44336, SHAPE_SQUARE, 12.0f, "Email", 2);
|
||||||
|
|
||||||
|
// Pasar el atlas al renderer:
|
||||||
|
graph_renderer_set_icon_atlas(renderer, atlas);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
- Requiere `tabler-icons.ttf` en `./assets/`, `$FN_ASSETS_DIR/`, o `${FN_CPP_ROOT}/vendor/tabler-icons/`.
|
||||||
|
- Cada celda lleva 1 px de padding interior para evitar bleed entre iconos al filtrar linealmente.
|
||||||
|
- Los pixels CPU se retienen para que tests verifiquen la presencia de glifos en las regiones esperadas sin GPU.
|
||||||
|
- `stb_truetype` se incluye con `STB_TRUETYPE_IMPLEMENTATION` local (cada TU tiene `STBTT_DEF static`, no colisiona con la copia de ImGui).
|
||||||
@@ -21,40 +21,41 @@
|
|||||||
// demos que aun no construyen tablas EntityType.
|
// demos que aun no construyen tablas EntityType.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
static const uint32_t k_fallback_palette[10] = {
|
static const uint32_t k_fallback_palette[10] = {
|
||||||
0xFF4CAF50u, // green
|
0xFF4CAF50u, 0xFFF44336u, 0xFF2196F3u, 0xFFFF9800u, 0xFF9C27B0u,
|
||||||
0xFFF44336u, // red
|
0xFF00BCD4u, 0xFFFFEB3Bu, 0xFFE91E63u, 0xFF795548u, 0xFF607D8Bu,
|
||||||
0xFF2196F3u, // blue
|
|
||||||
0xFFFF9800u, // orange
|
|
||||||
0xFF9C27B0u, // purple
|
|
||||||
0xFF00BCD4u, // cyan
|
|
||||||
0xFFFFEB3Bu, // yellow
|
|
||||||
0xFFE91E63u, // pink
|
|
||||||
0xFF795548u, // brown
|
|
||||||
0xFF607D8Bu, // blue-grey
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Maximo de iconos que cabe en el uniform array del shader. 256 es lo que
|
||||||
|
// genera `graph_icons` (grid 16×16 en 512×512). Subirlo requiere mas budget
|
||||||
|
// de uniforms (vec4×N → 4 floats por entrada) y aun cabe holgado en el
|
||||||
|
// limite GL 3.30 de 1024 vec4 por bloque.
|
||||||
|
static constexpr int k_max_icons = 256;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Per-instance / per-vertex data layouts
|
// Per-instance / per-vertex data layouts
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tier 1 packing: el color va como uint32 unico en lugar de 4 floats. Reduce
|
// 0049f: NodeInstance crece de 16 a 24 bytes para llevar shape + icon_id.
|
||||||
// el bandwidth de upload en 60% para nodos (28 → 16 bytes/instance) y 50%
|
// `shape_icon` empaqueta shape (8 bits bajos) + icon_id (16 bits siguientes);
|
||||||
// para aristas (24 → 12 bytes/vertex), y elimina la conversion ABGR→4floats
|
// los UVs del icono no viajan por instancia — el shader los busca en un
|
||||||
// en CPU (los uint32 ya tienen el layout de unpackUnorm4x8 en little-endian).
|
// `uniform vec4 u_icon_uvs[256]` indexado por icon_id-1. Asi conservamos
|
||||||
struct NodeInstance { // 16 bytes
|
// bandwidth aunque haya muchos nodos con el mismo icono.
|
||||||
float x, y; // world position
|
struct NodeInstance { // 24 bytes (alineado a 4)
|
||||||
float size; // diameter
|
float x, y; // 8
|
||||||
uint32_t color; // packed RGBA8
|
float size; // 4 (= diametro en pixels world-space)
|
||||||
|
uint32_t color; // 4
|
||||||
|
uint32_t shape_icon; // 4 — (shape & 0xFF) | (icon_id << 8)
|
||||||
|
uint32_t pad_; // 4 — relleno explicito; reservado para flags futuros
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tier 2 (issue 0049d): aristas via vertex pulling. El buffer es estatico —
|
// 0049f: EdgeStatic crece de 16 a 20 bytes para llevar style + flags reales.
|
||||||
// solo `(source_idx, target_idx, color, flags)` por arista, 16 bytes — y
|
// `style_flags`: flags (low 8 bits) | style (next 8 bits). El resto sigue
|
||||||
// se reuploads solo cuando cambia el grafo. El vertex shader hace fetch de
|
// siendo source/target/color como en 0049d.
|
||||||
// las posiciones desde un TBO RG32F que SI se actualiza por frame.
|
struct EdgeStatic { // 20 bytes
|
||||||
struct EdgeStatic { // 16 bytes
|
uint32_t source; // index into nodes
|
||||||
uint32_t source; // index into nodes
|
uint32_t target; // index into nodes
|
||||||
uint32_t target; // index into nodes
|
uint32_t color; // packed RGBA8
|
||||||
uint32_t color; // packed RGBA8 (sin pre-multiplicar — el shader aplica edge_alpha)
|
uint32_t style_flags; // (flags & 0xFF) | (style << 8)
|
||||||
uint32_t flags; // reservado para flechas/styles futuros
|
uint32_t pad_; // pad a multiplo de 4 — actualmente sin uso
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -63,17 +64,22 @@ struct EdgeStatic { // 16 bytes
|
|||||||
struct GraphRenderer {
|
struct GraphRenderer {
|
||||||
unsigned int fbo;
|
unsigned int fbo;
|
||||||
unsigned int texture;
|
unsigned int texture;
|
||||||
unsigned int rbo; // depth/stencil renderbuffer
|
unsigned int rbo;
|
||||||
int width, height;
|
int width, height;
|
||||||
|
|
||||||
// Node rendering (instanced quads)
|
// Node rendering (instanced quads)
|
||||||
unsigned int node_vao, node_quad_vbo, node_instance_vbo;
|
unsigned int node_vao, node_quad_vbo, node_instance_vbo;
|
||||||
unsigned int node_shader;
|
unsigned int node_shader;
|
||||||
|
int node_u_viewport_loc;
|
||||||
|
int node_u_scale_loc;
|
||||||
|
int node_u_translate_loc;
|
||||||
|
int node_u_outline_loc;
|
||||||
|
int node_u_node_px_loc;
|
||||||
|
int node_u_icon_atlas_loc;
|
||||||
|
int node_u_has_icons_loc;
|
||||||
|
int node_u_icon_uvs_loc;
|
||||||
|
|
||||||
// Edge rendering (vertex pulling — issue 0049d)
|
// Edge rendering (vertex pulling con 6 vertices/instancia para flecha)
|
||||||
// edge_vao : VAO con atributos por-instancia (divisor=1) leyendo de edge_static_vbo
|
|
||||||
// edge_vbo : buffer estatico (uno por grafo) con (source, target, color, flags)
|
|
||||||
// node_pos_buf / node_pos_tex : TBO RG32F que el vertex shader muestrea via texelFetch
|
|
||||||
unsigned int edge_vao, edge_vbo;
|
unsigned int edge_vao, edge_vbo;
|
||||||
unsigned int edge_shader;
|
unsigned int edge_shader;
|
||||||
unsigned int node_pos_buf;
|
unsigned int node_pos_buf;
|
||||||
@@ -84,31 +90,30 @@ struct GraphRenderer {
|
|||||||
int edge_u_alpha_loc;
|
int edge_u_alpha_loc;
|
||||||
int edge_u_node_pos_loc;
|
int edge_u_node_pos_loc;
|
||||||
|
|
||||||
// Streaming buffer capacities (in bytes). Grow x2 cuando used > capacity.
|
// Streaming buffer capacities (in bytes).
|
||||||
// Mantenemos el VBO orphaned con glBufferData(NULL, capacity) y luego
|
|
||||||
// hacemos glBufferSubData con los bytes realmente usados — evita el
|
|
||||||
// sync stall del driver y reduce las reallocaciones a O(log N).
|
|
||||||
size_t node_vbo_capacity;
|
size_t node_vbo_capacity;
|
||||||
size_t node_pos_capacity; // bytes del TBO RG32F
|
size_t node_pos_capacity;
|
||||||
size_t edge_static_capacity; // bytes del buffer estatico de aristas
|
size_t edge_static_capacity;
|
||||||
|
|
||||||
// CPU staging buffers — se reusan entre frames; crecen igual que el VBO.
|
// CPU staging
|
||||||
NodeInstance* node_staging;
|
NodeInstance* node_staging;
|
||||||
size_t node_staging_cap; // en NodeInstances, no bytes
|
size_t node_staging_cap;
|
||||||
float* node_pos_staging; // 2 floats (x,y) por nodo
|
float* node_pos_staging;
|
||||||
size_t node_pos_staging_cap; // en floats
|
size_t node_pos_staging_cap;
|
||||||
EdgeStatic* edge_static_staging;
|
EdgeStatic* edge_static_staging;
|
||||||
size_t edge_static_staging_cap; // en EdgeStatic
|
size_t edge_static_staging_cap;
|
||||||
|
|
||||||
// Cache para detectar cambios del grafo y reuploadear el edge_vbo
|
// Edge cache (reupload solo cuando cambia el grafo)
|
||||||
// estatico solo entonces. Identificamos el grafo por (puntero, count);
|
|
||||||
// basta para los flujos actuales (graph_viewport recrea el array al
|
|
||||||
// recargar). Cuando GraphData gane un campo `revision` se sustituira.
|
|
||||||
const void* cached_edges_ptr;
|
const void* cached_edges_ptr;
|
||||||
int cached_edge_count; // edges del grafo en el ultimo upload
|
int cached_edge_count;
|
||||||
int cached_edges_drawn; // edges realmente subidos (post-filtro)
|
int cached_edges_drawn;
|
||||||
bool edges_uploaded;
|
bool edges_uploaded;
|
||||||
|
|
||||||
|
// Icon atlas binding (0 = sin iconos)
|
||||||
|
unsigned int icon_atlas_tex;
|
||||||
|
float icon_uvs[k_max_icons * 4];
|
||||||
|
int icon_uv_count;
|
||||||
|
|
||||||
GraphRendererConfig config;
|
GraphRendererConfig config;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,27 +121,28 @@ struct GraphRenderer {
|
|||||||
// Shader sources
|
// Shader sources
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Node vertex shader — instanced unit quad
|
// Node vertex shader — instanced unit quad, ahora con shape + icon_id.
|
||||||
// a_color es uint32 packeado (R,G,B,A) — unpackUnorm4x8 esta en GLSL 4.20+,
|
// Pasamos el texto del shape al fragment para que despache el SDF correcto;
|
||||||
// pero en core 3.30 lo hacemos manualmente con bit shifts. Eso mantiene
|
// los UVs del icono se buscan en `u_icon_uvs[icon_id-1]`.
|
||||||
// compatibilidad con drivers que no exponen GL 4.x sin tener que tocar
|
|
||||||
// fn_framework.
|
|
||||||
static const char* k_node_vert = R"(
|
static const char* k_node_vert = R"(
|
||||||
#version 330 core
|
#version 330 core
|
||||||
// Quad corners [-0.5, 0.5]
|
layout(location = 0) in vec2 a_quad;
|
||||||
layout(location = 0) in vec2 a_quad;
|
|
||||||
|
|
||||||
// Per-instance: world position, size, packed RGBA8 color.
|
|
||||||
layout(location = 1) in vec2 a_pos;
|
layout(location = 1) in vec2 a_pos;
|
||||||
layout(location = 2) in float a_size;
|
layout(location = 2) in float a_size;
|
||||||
layout(location = 3) in uint a_color;
|
layout(location = 3) in uint a_color;
|
||||||
|
layout(location = 4) in uint a_shape_icon;
|
||||||
|
|
||||||
out vec2 v_uv;
|
out vec2 v_uv;
|
||||||
out vec4 v_color;
|
out vec4 v_color;
|
||||||
|
flat out uint v_shape;
|
||||||
|
flat out uint v_icon_id;
|
||||||
|
flat out vec4 v_icon_uv;
|
||||||
|
|
||||||
uniform vec2 u_viewport; // (width, height) in pixels
|
uniform vec2 u_viewport;
|
||||||
uniform float u_scale; // cam_zoom
|
uniform float u_scale;
|
||||||
uniform vec2 u_translate; // (tx, ty) in pixels
|
uniform vec2 u_translate;
|
||||||
|
uniform vec4 u_icon_uvs[256];
|
||||||
|
|
||||||
vec4 unpack_rgba8(uint c) {
|
vec4 unpack_rgba8(uint c) {
|
||||||
return vec4(
|
return vec4(
|
||||||
@@ -153,64 +159,145 @@ void main() {
|
|||||||
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
|
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
|
||||||
ndc.y = -ndc.y;
|
ndc.y = -ndc.y;
|
||||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||||
v_uv = a_quad + 0.5;
|
v_uv = a_quad + 0.5;
|
||||||
v_color = unpack_rgba8(a_color);
|
v_color = unpack_rgba8(a_color);
|
||||||
|
v_shape = a_shape_icon & 0xFFu;
|
||||||
|
v_icon_id = (a_shape_icon >> 8) & 0xFFFFu;
|
||||||
|
if (v_icon_id != 0u) {
|
||||||
|
v_icon_uv = u_icon_uvs[int(v_icon_id) - 1];
|
||||||
|
} else {
|
||||||
|
v_icon_uv = vec4(0.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
// Node fragment shader — SDF circle with outline
|
// Node fragment shader — SDF dispatch + opcional icon overlay.
|
||||||
|
// Para mantener la calidad del AA usamos `fwidth(d)` en lugar del
|
||||||
|
// `1.5/u_node_px` viejo: sirve igual a cualquier zoom y se queda nitido en
|
||||||
|
// los bordes complejos (hexagono, triangulo).
|
||||||
static const char* k_node_frag = R"(
|
static const char* k_node_frag = R"(
|
||||||
#version 330 core
|
#version 330 core
|
||||||
in vec2 v_uv;
|
in vec2 v_uv;
|
||||||
in vec4 v_color;
|
in vec4 v_color;
|
||||||
|
flat in uint v_shape;
|
||||||
|
flat in uint v_icon_id;
|
||||||
|
flat in vec4 v_icon_uv;
|
||||||
|
|
||||||
out vec4 frag_color;
|
out vec4 frag_color;
|
||||||
|
|
||||||
uniform float u_outline_px; // outline width in uv units
|
uniform float u_outline_px;
|
||||||
uniform float u_node_px; // node diameter in pixels (= size * zoom)
|
uniform float u_node_px;
|
||||||
|
uniform sampler2D u_icon_atlas;
|
||||||
|
uniform int u_has_icons;
|
||||||
|
|
||||||
|
float sdf_circle(vec2 uv) {
|
||||||
|
return length(uv - 0.5) - 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdf_square(vec2 uv) {
|
||||||
|
vec2 d = abs(uv - 0.5) - 0.5;
|
||||||
|
return max(d.x, d.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdf_diamond(vec2 uv) {
|
||||||
|
vec2 d = abs(uv - 0.5);
|
||||||
|
return d.x + d.y - 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hexagono regular alineado horizontalmente; SDF derivado del clasico de
|
||||||
|
// Inigo Quilez adaptado al cuadrado [0,1]^2. Inscribimos el hex dentro del
|
||||||
|
// circulo de radio 0.5 para que sus vertices toquen los bordes — asi
|
||||||
|
// area visual ~ a la del circulo del mismo `size`.
|
||||||
|
float sdf_hex(vec2 uv) {
|
||||||
|
vec2 p = abs(uv - 0.5);
|
||||||
|
const vec2 k = vec2(0.866025404, 0.5);
|
||||||
|
p -= 2.0 * min(dot(k, p), 0.0) * k;
|
||||||
|
p -= vec2(clamp(p.x, -k.y * 0.5, k.y * 0.5), 0.5);
|
||||||
|
return length(p) * sign(p.y) - (0.5 * 0.866025404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triangulo equilatero apuntando hacia arriba dentro de [0,1]^2.
|
||||||
|
float sdf_triangle(vec2 uv) {
|
||||||
|
const float k = 1.732050808; // sqrt(3)
|
||||||
|
vec2 p = uv - vec2(0.5, 0.5);
|
||||||
|
p.x = abs(p.x) - 0.5;
|
||||||
|
p.y = p.y + 0.5 / k;
|
||||||
|
if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
|
||||||
|
p.x -= clamp(p.x, -1.0, 0.0);
|
||||||
|
return -length(p) * sign(p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdf_rrect(vec2 uv) {
|
||||||
|
float r = 0.18;
|
||||||
|
vec2 d = abs(uv - 0.5) - (0.5 - r);
|
||||||
|
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
float pick_sdf(uint shape, vec2 uv) {
|
||||||
|
// shape 0 = SHAPE_USE_TYPE: el CPU resuelve antes; aqui no debe llegar.
|
||||||
|
// 1=circle 2=square 3=diamond 4=hex 5=triangle 6=rounded_square
|
||||||
|
if (shape == 1u) return sdf_circle(uv);
|
||||||
|
else if (shape == 2u) return sdf_square(uv);
|
||||||
|
else if (shape == 3u) return sdf_diamond(uv);
|
||||||
|
else if (shape == 4u) return sdf_hex(uv);
|
||||||
|
else if (shape == 5u) return sdf_triangle(uv);
|
||||||
|
else if (shape == 6u) return sdf_rrect(uv);
|
||||||
|
return sdf_circle(uv); // default robusto si shape mal codificado
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
float dist = length(v_uv - 0.5);
|
float d = pick_sdf(v_shape, v_uv);
|
||||||
float r = 0.5;
|
float aa = max(fwidth(d), 0.001);
|
||||||
float fwidth_uv = 1.5 / max(u_node_px, 1.0);
|
float fill_alpha = 1.0 - smoothstep(-aa, 0.0, d);
|
||||||
float alpha = 1.0 - smoothstep(r - fwidth_uv, r, dist);
|
if (fill_alpha < 0.001) discard;
|
||||||
if (alpha < 0.001) discard;
|
|
||||||
|
// Outline: anillo exterior — la anchura en uv viene de outline_px / node_px.
|
||||||
float outline_uv = u_outline_px / max(u_node_px, 1.0);
|
float outline_uv = u_outline_px / max(u_node_px, 1.0);
|
||||||
float outline = smoothstep(r - outline_uv - fwidth_uv, r - outline_uv, dist);
|
float outline = smoothstep(-outline_uv - aa, -outline_uv, d);
|
||||||
vec3 fill = v_color.rgb;
|
vec3 fill = v_color.rgb;
|
||||||
vec3 outline_col = mix(fill, vec3(1.0), 0.6);
|
vec3 outline_col = mix(fill, vec3(1.0), 0.6);
|
||||||
vec3 color = mix(fill, outline_col, outline);
|
vec3 col = mix(fill, outline_col, outline);
|
||||||
frag_color = vec4(color, v_color.a * alpha);
|
|
||||||
|
// Overlay del icono (solo si hay atlas + icon_id != 0). El icono se
|
||||||
|
// tintamos sumando blanco modulado por su alpha — el resultado sigue
|
||||||
|
// siendo legible sobre cualquier color de fondo del nodo.
|
||||||
|
if (u_has_icons != 0 && v_icon_id != 0u) {
|
||||||
|
vec2 atlas_uv = mix(v_icon_uv.xy, v_icon_uv.zw, v_uv);
|
||||||
|
vec4 ic = texture(u_icon_atlas, atlas_uv);
|
||||||
|
col = mix(col, vec3(1.0), ic.a * 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
frag_color = vec4(col, v_color.a * fill_alpha);
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
// Edge vertex shader — vertex pulling (issue 0049d).
|
// Edge vertex shader — vertex pulling, ahora 6 vertices por arista para
|
||||||
// El buffer de aristas es estatico: solo indices y color. Las posiciones
|
// soportar flecha en aristas EF_DIRECTED:
|
||||||
// vienen del TBO `u_node_pos` (RG32F, vec2 por nodo). gl_VertexID indica si
|
// gl_VertexID 0 (line src→tip), 1 (tip)
|
||||||
// dibujamos el endpoint source (0) o target (1). Asi eliminamos el upload
|
// gl_VertexID 2 (tip), 3 (back_left)
|
||||||
// de `12 floats × E` por frame que dominaba el coste de aristas.
|
// gl_VertexID 4 (tip), 5 (back_right)
|
||||||
|
// Si no esta directed, los vertices 2..5 se colapsan al tip (lineas
|
||||||
|
// degeneradas no visibles).
|
||||||
//
|
//
|
||||||
// Nota: usamos divisor=1 en los 4 atributos y `glDrawArraysInstanced(LINES,
|
// Para dashed/dotted: pasamos `arc_length` interpolado (en pixels) al
|
||||||
// 0, 2, edge_count)` — cada instancia rinde una linea de 2 vertices, los
|
// fragment shader; este descarta segun style.
|
||||||
// atributos se mantienen constantes en la instancia y `gl_VertexID` cicla
|
|
||||||
// 0..1 dentro de ella.
|
|
||||||
//
|
|
||||||
// `samplerBuffer` y `texelFetch(samplerBuffer, int)` estan en GLSL 1.40+;
|
|
||||||
// 330 core nos vale (no necesitamos 4.30 — el issue exageraba).
|
|
||||||
static const char* k_edge_vert = R"(
|
static const char* k_edge_vert = R"(
|
||||||
#version 330 core
|
#version 330 core
|
||||||
layout(location = 0) in uint a_source;
|
layout(location = 0) in uint a_source;
|
||||||
layout(location = 1) in uint a_target;
|
layout(location = 1) in uint a_target;
|
||||||
layout(location = 2) in uint a_color;
|
layout(location = 2) in uint a_color;
|
||||||
// location 3 (flags) reservado en el buffer (16B alignment) pero no leido aqui.
|
layout(location = 3) in uint a_style_flags;
|
||||||
|
|
||||||
uniform samplerBuffer u_node_pos;
|
uniform samplerBuffer u_node_pos;
|
||||||
uniform vec2 u_viewport;
|
uniform vec2 u_viewport;
|
||||||
uniform float u_scale;
|
uniform float u_scale;
|
||||||
uniform vec2 u_translate;
|
uniform vec2 u_translate;
|
||||||
uniform float u_alpha; // edge_alpha
|
uniform float u_alpha;
|
||||||
|
|
||||||
out vec4 v_color;
|
out vec4 v_color;
|
||||||
|
flat out uint v_style;
|
||||||
|
flat out uint v_segment; // 0=line, 1=arrow
|
||||||
|
out float v_arc;
|
||||||
|
|
||||||
vec4 unpack_rgba8(uint c) {
|
vec4 unpack_rgba8(uint c) {
|
||||||
return vec4(
|
return vec4(
|
||||||
@@ -221,27 +308,97 @@ vec4 unpack_rgba8(uint c) {
|
|||||||
) * (1.0 / 255.0);
|
) * (1.0 / 255.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vec2 to_screen(vec2 wpos) {
|
||||||
|
return wpos * u_scale + u_translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 to_ndc(vec2 screen) {
|
||||||
|
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
|
||||||
|
return vec2(ndc.x, -ndc.y);
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
int idx = (gl_VertexID & 1) == 0 ? int(a_source) : int(a_target);
|
int vid = gl_VertexID;
|
||||||
vec2 wpos = texelFetch(u_node_pos, idx).xy;
|
uint flags = a_style_flags & 0xFFu;
|
||||||
vec2 screen = wpos * u_scale + u_translate;
|
uint style = (a_style_flags >> 8) & 0xFFu;
|
||||||
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
|
bool directed = (flags & 1u) != 0u; // EF_DIRECTED == 1
|
||||||
ndc.y = -ndc.y;
|
|
||||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
vec2 wsrc = texelFetch(u_node_pos, int(a_source)).xy;
|
||||||
|
vec2 wtgt = texelFetch(u_node_pos, int(a_target)).xy;
|
||||||
|
|
||||||
|
vec2 ssrc = to_screen(wsrc);
|
||||||
|
vec2 stgt = to_screen(wtgt);
|
||||||
|
|
||||||
|
vec2 dir = stgt - ssrc;
|
||||||
|
float seg_len = length(dir);
|
||||||
|
vec2 dir_n = (seg_len > 0.0001) ? dir / seg_len : vec2(1.0, 0.0);
|
||||||
|
vec2 perp = vec2(-dir_n.y, dir_n.x);
|
||||||
|
|
||||||
|
// Acortamos el segmento principal si la arista es directed para que la
|
||||||
|
// flecha no se incruste en el nodo target. Tamano fijo en pixels = 10.
|
||||||
|
float arrow_px = 10.0;
|
||||||
|
vec2 tip = stgt;
|
||||||
|
vec2 line_end = directed ? (stgt - dir_n * arrow_px * 0.5) : stgt;
|
||||||
|
|
||||||
|
vec2 spos;
|
||||||
|
float arc;
|
||||||
|
uint segment;
|
||||||
|
|
||||||
|
if (vid <= 1) {
|
||||||
|
// Linea principal source→line_end.
|
||||||
|
segment = 0u;
|
||||||
|
if (vid == 0) { spos = ssrc; arc = 0.0; }
|
||||||
|
else { spos = line_end; arc = length(line_end - ssrc); }
|
||||||
|
} else {
|
||||||
|
segment = 1u;
|
||||||
|
arc = 0.0;
|
||||||
|
if (!directed) {
|
||||||
|
// Sin flecha: degenerado en el tip — sin pintar.
|
||||||
|
spos = tip;
|
||||||
|
} else {
|
||||||
|
// Triangulo de la flecha en 2 lineas (chevron):
|
||||||
|
// (tip, back_left) y (tip, back_right)
|
||||||
|
vec2 back = tip - dir_n * arrow_px;
|
||||||
|
vec2 left_p = back + perp * arrow_px * 0.5;
|
||||||
|
vec2 right_p = back - perp * arrow_px * 0.5;
|
||||||
|
if (vid == 2) spos = tip;
|
||||||
|
else if (vid == 3) spos = left_p;
|
||||||
|
else if (vid == 4) spos = tip;
|
||||||
|
else spos = right_p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = vec4(to_ndc(spos), 0.0, 1.0);
|
||||||
|
|
||||||
vec4 c = unpack_rgba8(a_color);
|
vec4 c = unpack_rgba8(a_color);
|
||||||
c.a *= u_alpha;
|
c.a *= u_alpha;
|
||||||
v_color = c;
|
v_color = c;
|
||||||
|
v_style = style;
|
||||||
|
v_segment = segment;
|
||||||
|
v_arc = arc;
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
// Edge fragment shader
|
// Edge fragment shader — descarta segun style + arc_length para producir
|
||||||
|
// dashed (period 8 px, duty 0.5) o dotted (period 4 px, duty 0.25). Las
|
||||||
|
// lineas de la flecha (segment==1) se renderizan siempre solidas.
|
||||||
static const char* k_edge_frag = R"(
|
static const char* k_edge_frag = R"(
|
||||||
#version 330 core
|
#version 330 core
|
||||||
in vec4 v_color;
|
in vec4 v_color;
|
||||||
|
flat in uint v_style;
|
||||||
|
flat in uint v_segment;
|
||||||
|
in float v_arc;
|
||||||
out vec4 frag_color;
|
out vec4 frag_color;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
if (v_segment == 0u) {
|
||||||
|
// EDGE_USE_TYPE=0, EDGE_SOLID=1, EDGE_DASHED=2, EDGE_DOTTED=3
|
||||||
|
if (v_style == 2u) {
|
||||||
|
if (mod(v_arc, 8.0) > 4.0) discard;
|
||||||
|
} else if (v_style == 3u) {
|
||||||
|
if (mod(v_arc, 4.0) > 1.0) discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
frag_color = v_color;
|
frag_color = v_color;
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
@@ -256,7 +413,7 @@ static unsigned int compile_shader(GLenum type, const char* src) {
|
|||||||
int ok;
|
int ok;
|
||||||
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
char buf[512];
|
char buf[1024];
|
||||||
glGetShaderInfoLog(s, sizeof(buf), nullptr, buf);
|
glGetShaderInfoLog(s, sizeof(buf), nullptr, buf);
|
||||||
fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf);
|
fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf);
|
||||||
}
|
}
|
||||||
@@ -273,7 +430,7 @@ static unsigned int link_program(const char* vert_src, const char* frag_src) {
|
|||||||
int ok;
|
int ok;
|
||||||
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
char buf[512];
|
char buf[1024];
|
||||||
glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf);
|
glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf);
|
||||||
fprintf(stderr, "[graph_renderer] program link error: %s\n", buf);
|
fprintf(stderr, "[graph_renderer] program link error: %s\n", buf);
|
||||||
}
|
}
|
||||||
@@ -315,10 +472,6 @@ static void destroy_fbo(GraphRenderer* r) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Capacity-tracked streaming helpers
|
// Capacity-tracked streaming helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Doblar la capacidad cada vez que el upload supera el VBO. Asi las
|
|
||||||
// reallocaciones quedan en O(log N) en el peor caso y en >0 en el regimen
|
|
||||||
// estable. Capacidad inicial razonable: 4096 nodos / aristas (segun el .md
|
|
||||||
// del issue) — la primera llamada paga el redimensionado si hay mas.
|
|
||||||
static size_t grow_capacity(size_t current, size_t needed, size_t initial) {
|
static size_t grow_capacity(size_t current, size_t needed, size_t initial) {
|
||||||
size_t cap = current > 0 ? current : initial;
|
size_t cap = current > 0 ? current : initial;
|
||||||
while (cap < needed) cap *= 2;
|
while (cap < needed) cap *= 2;
|
||||||
@@ -348,11 +501,12 @@ GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererC
|
|||||||
r->cached_edge_count = 0;
|
r->cached_edge_count = 0;
|
||||||
r->cached_edges_drawn = 0;
|
r->cached_edges_drawn = 0;
|
||||||
r->edges_uploaded = false;
|
r->edges_uploaded = false;
|
||||||
|
r->icon_atlas_tex = 0;
|
||||||
|
r->icon_uv_count = 0;
|
||||||
|
std::memset(r->icon_uvs, 0, sizeof(r->icon_uvs));
|
||||||
|
|
||||||
// --- FBO ---
|
|
||||||
create_fbo(r);
|
create_fbo(r);
|
||||||
|
|
||||||
// --- Node VAO ---
|
|
||||||
static const float quad_verts[8] = {
|
static const float quad_verts[8] = {
|
||||||
-0.5f, -0.5f,
|
-0.5f, -0.5f,
|
||||||
0.5f, -0.5f,
|
0.5f, -0.5f,
|
||||||
@@ -363,42 +517,39 @@ GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererC
|
|||||||
glGenVertexArrays(1, &r->node_vao);
|
glGenVertexArrays(1, &r->node_vao);
|
||||||
glBindVertexArray(r->node_vao);
|
glBindVertexArray(r->node_vao);
|
||||||
|
|
||||||
// Quad VBO (location 0)
|
|
||||||
glGenBuffers(1, &r->node_quad_vbo);
|
glGenBuffers(1, &r->node_quad_vbo);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo);
|
glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo);
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
|
||||||
glEnableVertexAttribArray(0);
|
glEnableVertexAttribArray(0);
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
|
||||||
|
|
||||||
// Instance VBO — layout: NodeInstance (x, y, size, color_u32)
|
|
||||||
glGenBuffers(1, &r->node_instance_vbo);
|
glGenBuffers(1, &r->node_instance_vbo);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
|
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
|
||||||
glEnableVertexAttribArray(1); // pos (2 float)
|
glEnableVertexAttribArray(1);
|
||||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(NodeInstance),
|
||||||
sizeof(NodeInstance),
|
|
||||||
(void*)offsetof(NodeInstance, x));
|
(void*)offsetof(NodeInstance, x));
|
||||||
glVertexAttribDivisor(1, 1);
|
glVertexAttribDivisor(1, 1);
|
||||||
glEnableVertexAttribArray(2); // size (1 float)
|
glEnableVertexAttribArray(2);
|
||||||
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE,
|
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(NodeInstance),
|
||||||
sizeof(NodeInstance),
|
|
||||||
(void*)offsetof(NodeInstance, size));
|
(void*)offsetof(NodeInstance, size));
|
||||||
glVertexAttribDivisor(2, 1);
|
glVertexAttribDivisor(2, 1);
|
||||||
glEnableVertexAttribArray(3); // color (1 uint32) — IPointer, no normalizado
|
glEnableVertexAttribArray(3);
|
||||||
glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT,
|
glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, sizeof(NodeInstance),
|
||||||
sizeof(NodeInstance),
|
|
||||||
(void*)offsetof(NodeInstance, color));
|
(void*)offsetof(NodeInstance, color));
|
||||||
glVertexAttribDivisor(3, 1);
|
glVertexAttribDivisor(3, 1);
|
||||||
|
glEnableVertexAttribArray(4);
|
||||||
|
glVertexAttribIPointer(4, 1, GL_UNSIGNED_INT, sizeof(NodeInstance),
|
||||||
|
(void*)offsetof(NodeInstance, shape_icon));
|
||||||
|
glVertexAttribDivisor(4, 1);
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
// --- Edge VAO (vertex pulling, divisor=1 sobre el buffer estatico) ---
|
|
||||||
glGenVertexArrays(1, &r->edge_vao);
|
glGenVertexArrays(1, &r->edge_vao);
|
||||||
glBindVertexArray(r->edge_vao);
|
glBindVertexArray(r->edge_vao);
|
||||||
|
|
||||||
glGenBuffers(1, &r->edge_vbo);
|
glGenBuffers(1, &r->edge_vbo);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
|
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
|
||||||
|
|
||||||
// (source, target, color, flags) — los 4 con divisor=1.
|
|
||||||
glEnableVertexAttribArray(0);
|
glEnableVertexAttribArray(0);
|
||||||
glVertexAttribIPointer(0, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
|
glVertexAttribIPointer(0, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
|
||||||
(void*)offsetof(EdgeStatic, source));
|
(void*)offsetof(EdgeStatic, source));
|
||||||
@@ -411,15 +562,15 @@ GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererC
|
|||||||
glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
|
glVertexAttribIPointer(2, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
|
||||||
(void*)offsetof(EdgeStatic, color));
|
(void*)offsetof(EdgeStatic, color));
|
||||||
glVertexAttribDivisor(2, 1);
|
glVertexAttribDivisor(2, 1);
|
||||||
// location 3 reservado en el buffer pero no enabled — el shader actual
|
glEnableVertexAttribArray(3);
|
||||||
// no lo lee. Mantenemos el slot para futuros estilos/flechas.
|
glVertexAttribIPointer(3, 1, GL_UNSIGNED_INT, sizeof(EdgeStatic),
|
||||||
|
(void*)offsetof(EdgeStatic, style_flags));
|
||||||
|
glVertexAttribDivisor(3, 1);
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
// --- TBO de posiciones de nodos (RG32F, vec2 por nodo) ---
|
|
||||||
glGenBuffers(1, &r->node_pos_buf);
|
glGenBuffers(1, &r->node_pos_buf);
|
||||||
glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
|
glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
|
||||||
// Reservamos capacidad inicial; se redimensiona en draw segun N.
|
|
||||||
glBufferData(GL_TEXTURE_BUFFER, 4096 * 2 * sizeof(float), nullptr, GL_STREAM_DRAW);
|
glBufferData(GL_TEXTURE_BUFFER, 4096 * 2 * sizeof(float), nullptr, GL_STREAM_DRAW);
|
||||||
r->node_pos_capacity = 4096 * 2 * sizeof(float);
|
r->node_pos_capacity = 4096 * 2 * sizeof(float);
|
||||||
|
|
||||||
@@ -429,12 +580,18 @@ GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererC
|
|||||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||||
|
|
||||||
// --- Shaders ---
|
|
||||||
r->node_shader = link_program(k_node_vert, k_node_frag);
|
r->node_shader = link_program(k_node_vert, k_node_frag);
|
||||||
r->edge_shader = link_program(k_edge_vert, k_edge_frag);
|
r->edge_shader = link_program(k_edge_vert, k_edge_frag);
|
||||||
|
|
||||||
// Cachear locations de uniforms del edge shader (issue 0049d): se
|
r->node_u_viewport_loc = glGetUniformLocation(r->node_shader, "u_viewport");
|
||||||
// resuelven una vez en lugar de glGetUniformLocation cada frame.
|
r->node_u_scale_loc = glGetUniformLocation(r->node_shader, "u_scale");
|
||||||
|
r->node_u_translate_loc = glGetUniformLocation(r->node_shader, "u_translate");
|
||||||
|
r->node_u_outline_loc = glGetUniformLocation(r->node_shader, "u_outline_px");
|
||||||
|
r->node_u_node_px_loc = glGetUniformLocation(r->node_shader, "u_node_px");
|
||||||
|
r->node_u_icon_atlas_loc = glGetUniformLocation(r->node_shader, "u_icon_atlas");
|
||||||
|
r->node_u_has_icons_loc = glGetUniformLocation(r->node_shader, "u_has_icons");
|
||||||
|
r->node_u_icon_uvs_loc = glGetUniformLocation(r->node_shader, "u_icon_uvs");
|
||||||
|
|
||||||
r->edge_u_viewport_loc = glGetUniformLocation(r->edge_shader, "u_viewport");
|
r->edge_u_viewport_loc = glGetUniformLocation(r->edge_shader, "u_viewport");
|
||||||
r->edge_u_scale_loc = glGetUniformLocation(r->edge_shader, "u_scale");
|
r->edge_u_scale_loc = glGetUniformLocation(r->edge_shader, "u_scale");
|
||||||
r->edge_u_translate_loc = glGetUniformLocation(r->edge_shader, "u_translate");
|
r->edge_u_translate_loc = glGetUniformLocation(r->edge_shader, "u_translate");
|
||||||
@@ -471,21 +628,37 @@ void graph_renderer_resize(GraphRenderer* r, int width, int height) {
|
|||||||
create_fbo(r);
|
create_fbo(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void graph_renderer_set_icon_atlas(GraphRenderer* r,
|
||||||
|
unsigned int texture_id,
|
||||||
|
const float* uv_table,
|
||||||
|
int count) {
|
||||||
|
if (!r) return;
|
||||||
|
r->icon_atlas_tex = texture_id;
|
||||||
|
r->icon_uv_count = (count > k_max_icons) ? k_max_icons : (count < 0 ? 0 : count);
|
||||||
|
if (r->icon_uv_count > 0 && uv_table) {
|
||||||
|
std::memcpy(r->icon_uvs, uv_table,
|
||||||
|
(size_t)r->icon_uv_count * 4 * sizeof(float));
|
||||||
|
}
|
||||||
|
// Limpia las entradas no usadas para evitar UVs basura si el shader las
|
||||||
|
// sample por error. Costo: O(k_max_icons) — irrelevante.
|
||||||
|
if (r->icon_uv_count < k_max_icons) {
|
||||||
|
std::memset(r->icon_uvs + r->icon_uv_count * 4, 0,
|
||||||
|
(size_t)(k_max_icons - r->icon_uv_count) * 4 * sizeof(float));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
||||||
float cam_x, float cam_y, float cam_zoom) {
|
float cam_x, float cam_y, float cam_zoom) {
|
||||||
if (!r) return 0;
|
if (!r) return 0;
|
||||||
|
|
||||||
// --- Save GL state ---
|
|
||||||
GLint prev_fbo;
|
GLint prev_fbo;
|
||||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
|
||||||
GLint prev_viewport[4];
|
GLint prev_viewport[4];
|
||||||
glGetIntegerv(GL_VIEWPORT, prev_viewport);
|
glGetIntegerv(GL_VIEWPORT, prev_viewport);
|
||||||
|
|
||||||
// --- Bind FBO ---
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
|
||||||
glViewport(0, 0, r->width, r->height);
|
glViewport(0, 0, r->width, r->height);
|
||||||
|
|
||||||
// Clear with bg_color (interpreted as RGBA8 packed — same memory layout)
|
|
||||||
uint8_t br, bg, bb, ba;
|
uint8_t br, bg, bb, ba;
|
||||||
unpack_rgba8(r->config.bg_color, br, bg, bb, ba);
|
unpack_rgba8(r->config.bg_color, br, bg, bb, ba);
|
||||||
glClearColor(br / 255.0f, bg / 255.0f, bb / 255.0f, ba / 255.0f);
|
glClearColor(br / 255.0f, bg / 255.0f, bb / 255.0f, ba / 255.0f);
|
||||||
@@ -494,13 +667,10 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
// View transform: world -> screen pixels
|
|
||||||
float scale = cam_zoom;
|
float scale = cam_zoom;
|
||||||
float tx = -cam_x * scale + (float)r->width * 0.5f;
|
float tx = -cam_x * scale + (float)r->width * 0.5f;
|
||||||
float ty = -cam_y * scale + (float)r->height * 0.5f;
|
float ty = -cam_y * scale + (float)r->height * 0.5f;
|
||||||
|
|
||||||
// Frustum cull AABB en world coords. Margen del 10% para que un nodo o
|
|
||||||
// arista a punto de entrar en pantalla no haga pop-in al moverse.
|
|
||||||
float half_w = ((float)r->width * 0.5f) / std::max(scale, 0.0001f);
|
float half_w = ((float)r->width * 0.5f) / std::max(scale, 0.0001f);
|
||||||
float half_h = ((float)r->height * 0.5f) / std::max(scale, 0.0001f);
|
float half_h = ((float)r->height * 0.5f) / std::max(scale, 0.0001f);
|
||||||
const float margin = 0.10f;
|
const float margin = 0.10f;
|
||||||
@@ -510,9 +680,7 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
float vy1 = cam_y + half_h * (1.0f + margin);
|
float vy1 = cam_y + half_h * (1.0f + margin);
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Subir posiciones de nodos al TBO (vec2 por nodo). Lo necesitamos
|
// Subir posiciones de nodos al TBO.
|
||||||
// tanto si dibujamos aristas (vertex pulling) como antes de dibujar
|
|
||||||
// nodos — pero se calcula una sola vez por frame.
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
bool tbo_ready = false;
|
bool tbo_ready = false;
|
||||||
if (graph.node_count > 0 && graph.nodes) {
|
if (graph.node_count > 0 && graph.nodes) {
|
||||||
@@ -532,18 +700,15 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
4096 * 2 * sizeof(float));
|
4096 * 2 * sizeof(float));
|
||||||
}
|
}
|
||||||
glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
|
glBindBuffer(GL_TEXTURE_BUFFER, r->node_pos_buf);
|
||||||
// Orphan + subdata: misma estrategia que en 0049c, evita stall.
|
|
||||||
glBufferData(GL_TEXTURE_BUFFER, (GLsizeiptr)r->node_pos_capacity, nullptr, GL_STREAM_DRAW);
|
glBufferData(GL_TEXTURE_BUFFER, (GLsizeiptr)r->node_pos_capacity, nullptr, GL_STREAM_DRAW);
|
||||||
glBufferSubData(GL_TEXTURE_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_pos_staging);
|
glBufferSubData(GL_TEXTURE_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_pos_staging);
|
||||||
// glTexBuffer ya esta vinculado al buffer en create — el view sigue
|
|
||||||
// valido tras orphan: GL_TEXTURE_BUFFER referencia al BO por nombre.
|
|
||||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
||||||
tbo_ready = true;
|
tbo_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Aristas via vertex pulling. El buffer estatico solo se reupload
|
// Aristas via vertex pulling. 6 vertices por arista (line + arrow).
|
||||||
// cuando el grafo cambia — detectamos con (puntero, count).
|
// El buffer estatico se reupload solo cuando cambia el grafo.
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
if (tbo_ready && graph.edge_count > 0 && graph.edges) {
|
if (tbo_ready && graph.edge_count > 0 && graph.edges) {
|
||||||
const bool graph_changed =
|
const bool graph_changed =
|
||||||
@@ -552,9 +717,6 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
|| r->cached_edge_count != graph.edge_count;
|
|| r->cached_edge_count != graph.edge_count;
|
||||||
|
|
||||||
if (graph_changed) {
|
if (graph_changed) {
|
||||||
// (Re)build el buffer estatico. Skipeamos aristas con indices
|
|
||||||
// fuera de rango — pueden aparecer durante una recarga parcial
|
|
||||||
// del grafo y no queremos que el GPU lea fuera del TBO.
|
|
||||||
if ((size_t)graph.edge_count > r->edge_static_staging_cap) {
|
if ((size_t)graph.edge_count > r->edge_static_staging_cap) {
|
||||||
size_t new_cap = grow_capacity(r->edge_static_staging_cap,
|
size_t new_cap = grow_capacity(r->edge_static_staging_cap,
|
||||||
(size_t)graph.edge_count, 8192);
|
(size_t)graph.edge_count, 8192);
|
||||||
@@ -568,17 +730,17 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
if (e.source >= (uint32_t)graph.node_count) continue;
|
if (e.source >= (uint32_t)graph.node_count) continue;
|
||||||
if (e.target >= (uint32_t)graph.node_count) continue;
|
if (e.target >= (uint32_t)graph.node_count) continue;
|
||||||
if (!(e.flags & EF_VISIBLE)) continue;
|
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.source].flags & NF_VISIBLE)) continue;
|
||||||
if (!(graph.nodes[e.target].flags & NF_VISIBLE)) continue;
|
if (!(graph.nodes[e.target].flags & NF_VISIBLE)) continue;
|
||||||
|
|
||||||
uint32_t col = resolve_edge_color(e, graph.rel_types,
|
uint32_t col = resolve_edge_color(e, graph.rel_types,
|
||||||
graph.rel_type_count);
|
graph.rel_type_count);
|
||||||
if (col == 0u) col = pack_rgba8(0x88, 0x88, 0x88, 0xFF);
|
if (col == 0u) col = pack_rgba8(0x88, 0x88, 0x88, 0xFF);
|
||||||
r->edge_static_staging[out++] = { e.source, e.target, col, 0u };
|
uint8_t style = resolve_edge_style(e, graph.rel_types,
|
||||||
|
graph.rel_type_count);
|
||||||
|
uint32_t style_flags = ((uint32_t)e.flags & 0xFFu)
|
||||||
|
| ((uint32_t)style << 8);
|
||||||
|
r->edge_static_staging[out++] = { e.source, e.target, col, style_flags, 0u };
|
||||||
}
|
}
|
||||||
if (out > 0) {
|
if (out > 0) {
|
||||||
const size_t used_bytes = out * sizeof(EdgeStatic);
|
const size_t used_bytes = out * sizeof(EdgeStatic);
|
||||||
@@ -606,7 +768,6 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
glUniform2f(r->edge_u_translate_loc, tx, ty);
|
glUniform2f(r->edge_u_translate_loc, tx, ty);
|
||||||
glUniform1f(r->edge_u_alpha_loc, r->config.edge_alpha);
|
glUniform1f(r->edge_u_alpha_loc, r->config.edge_alpha);
|
||||||
|
|
||||||
// Bind TBO al sampler u_node_pos en la texture unit 0.
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
glBindTexture(GL_TEXTURE_BUFFER, r->node_pos_tex);
|
glBindTexture(GL_TEXTURE_BUFFER, r->node_pos_tex);
|
||||||
glUniform1i(r->edge_u_node_pos_loc, 0);
|
glUniform1i(r->edge_u_node_pos_loc, 0);
|
||||||
@@ -614,20 +775,18 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
glLineWidth(r->config.edge_width);
|
glLineWidth(r->config.edge_width);
|
||||||
|
|
||||||
glBindVertexArray(r->edge_vao);
|
glBindVertexArray(r->edge_vao);
|
||||||
// Una "instancia" = 1 linea (2 vertices). gl_VertexID dentro
|
// 6 vertices por instancia: 2 linea + 4 chevron de la flecha.
|
||||||
// de la instancia es 0 o 1 → elige endpoint source o target.
|
glDrawArraysInstanced(GL_LINES, 0, 6, (GLsizei)r->cached_edges_drawn);
|
||||||
glDrawArraysInstanced(GL_LINES, 0, 2, (GLsizei)r->cached_edges_drawn);
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
}
|
}
|
||||||
} else if (graph.edge_count == 0) {
|
} else if (graph.edge_count == 0) {
|
||||||
// Si el caller borra todas las aristas, invalidamos el cache para
|
|
||||||
// que el siguiente upload reconstruya el buffer.
|
|
||||||
r->edges_uploaded = false;
|
r->edges_uploaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Draw nodes (instanced quads, frustum-culled)
|
// Draw nodes (instanced quads, frustum-culled). Empaqueta shape e
|
||||||
|
// icon_id por instancia; el shader despacha el SDF y aplica overlay.
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
if (graph.node_count > 0 && graph.nodes) {
|
if (graph.node_count > 0 && graph.nodes) {
|
||||||
if ((size_t)graph.node_count > r->node_staging_cap) {
|
if ((size_t)graph.node_count > r->node_staging_cap) {
|
||||||
@@ -641,16 +800,12 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
const GraphNode& n = graph.nodes[i];
|
const GraphNode& n = graph.nodes[i];
|
||||||
if (!(n.flags & NF_VISIBLE)) continue;
|
if (!(n.flags & NF_VISIBLE)) continue;
|
||||||
|
|
||||||
float sz = resolve_node_size(n, graph.types, graph.type_count);
|
float sz = resolve_node_size(n, graph.types, graph.type_count);
|
||||||
if (sz <= 0.0f) sz = 4.0f;
|
if (sz <= 0.0f) sz = 4.0f;
|
||||||
float half = sz * 0.5f;
|
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.x + half < vx0 || n.x - half > vx1) continue;
|
||||||
if (n.y + half < vy0 || n.y - half > vy1) continue;
|
if (n.y + half < vy0 || n.y - half > vy1) continue;
|
||||||
|
|
||||||
// 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;
|
uint32_t ncol;
|
||||||
if (n.color_override != 0u) {
|
if (n.color_override != 0u) {
|
||||||
ncol = n.color_override;
|
ncol = n.color_override;
|
||||||
@@ -659,8 +814,22 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
} else {
|
} else {
|
||||||
ncol = k_fallback_palette[n.type_id % 10];
|
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 };
|
uint8_t shape = resolve_node_shape(n, graph.types, graph.type_count);
|
||||||
|
if (shape == SHAPE_USE_TYPE) shape = SHAPE_CIRCLE;
|
||||||
|
|
||||||
|
// icon_id solo viene del EntityType (los nodos no tienen override
|
||||||
|
// de icono en el modelo actual). 0 = sin overlay.
|
||||||
|
uint16_t icon_id = 0;
|
||||||
|
if (graph.types && n.type_id < (uint16_t)graph.type_count) {
|
||||||
|
icon_id = graph.types[n.type_id].icon_id;
|
||||||
|
}
|
||||||
|
if (icon_id > r->icon_uv_count) icon_id = 0; // fuera de tabla
|
||||||
|
|
||||||
|
uint32_t shape_icon = ((uint32_t)shape & 0xFFu)
|
||||||
|
| ((uint32_t)icon_id << 8);
|
||||||
|
|
||||||
|
r->node_staging[visible++] = { n.x, n.y, sz, ncol, shape_icon, 0u };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visible > 0) {
|
if (visible > 0) {
|
||||||
@@ -671,26 +840,41 @@ unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
|||||||
}
|
}
|
||||||
|
|
||||||
glUseProgram(r->node_shader);
|
glUseProgram(r->node_shader);
|
||||||
glUniform2f(glGetUniformLocation(r->node_shader, "u_viewport"),
|
glUniform2f(r->node_u_viewport_loc, (float)r->width, (float)r->height);
|
||||||
(float)r->width, (float)r->height);
|
glUniform1f(r->node_u_scale_loc, scale);
|
||||||
glUniform1f(glGetUniformLocation(r->node_shader, "u_scale"), scale);
|
glUniform2f(r->node_u_translate_loc, tx, ty);
|
||||||
glUniform2f(glGetUniformLocation(r->node_shader, "u_translate"), tx, ty);
|
glUniform1f(r->node_u_outline_loc, r->config.node_outline);
|
||||||
glUniform1f(glGetUniformLocation(r->node_shader, "u_outline_px"), r->config.node_outline);
|
|
||||||
|
float avg_px = 8.0f * scale;
|
||||||
|
glUniform1f(r->node_u_node_px_loc, avg_px);
|
||||||
|
|
||||||
|
// Subimos siempre la tabla de UVs — son 256 vec4 = 4KB, peanuts.
|
||||||
|
glUniform4fv(r->node_u_icon_uvs_loc, k_max_icons, r->icon_uvs);
|
||||||
|
|
||||||
|
const int has_icons = (r->icon_atlas_tex != 0 && r->icon_uv_count > 0) ? 1 : 0;
|
||||||
|
glUniform1i(r->node_u_has_icons_loc, has_icons);
|
||||||
|
if (has_icons) {
|
||||||
|
glActiveTexture(GL_TEXTURE1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, r->icon_atlas_tex);
|
||||||
|
glUniform1i(r->node_u_icon_atlas_loc, 1);
|
||||||
|
}
|
||||||
|
|
||||||
glBindVertexArray(r->node_vao);
|
glBindVertexArray(r->node_vao);
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
|
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
|
||||||
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)r->node_vbo_capacity, nullptr, GL_STREAM_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)r->node_vbo_capacity, nullptr, GL_STREAM_DRAW);
|
||||||
glBufferSubData(GL_ARRAY_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_staging);
|
glBufferSubData(GL_ARRAY_BUFFER, 0, (GLsizeiptr)used_bytes, r->node_staging);
|
||||||
|
|
||||||
float avg_px = 8.0f * scale; // estimacion para el AA del SDF
|
|
||||||
glUniform1f(glGetUniformLocation(r->node_shader, "u_node_px"), avg_px);
|
|
||||||
|
|
||||||
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)visible);
|
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)visible);
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
if (has_icons) {
|
||||||
|
glActiveTexture(GL_TEXTURE1);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Restore GL state ---
|
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
|
||||||
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
|
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ void graph_renderer_resize(GraphRenderer* r, int width, int height);
|
|||||||
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
|
||||||
float cam_x, float cam_y, float cam_zoom);
|
float cam_x, float cam_y, float cam_zoom);
|
||||||
|
|
||||||
|
// Optional: bind an icon atlas built by graph_icons. The renderer composes
|
||||||
|
// icons over nodes whose `icon_id` (resolved from override or EntityType)
|
||||||
|
// is non-zero. Pass texture_id=0 to disable icons (default).
|
||||||
|
//
|
||||||
|
// `uv_table`: `count * 4` floats (u0, v0, u1, v1) per icon, 0-indexed
|
||||||
|
// (icon_id k uses uv_table[(k-1)*4..]). Must outlive the renderer or be
|
||||||
|
// re-set whenever changed (the renderer copies it into a uniform array).
|
||||||
|
void graph_renderer_set_icon_atlas(GraphRenderer* r,
|
||||||
|
unsigned int texture_id,
|
||||||
|
const float* uv_table,
|
||||||
|
int count);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// RGBA8 packing helpers
|
// RGBA8 packing helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ name: graph_renderer
|
|||||||
kind: function
|
kind: function
|
||||||
lang: cpp
|
lang: cpp
|
||||||
domain: viz
|
domain: viz
|
||||||
version: "1.4.0"
|
version: "1.5.0"
|
||||||
purity: impure
|
purity: impure
|
||||||
signature: "GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)"
|
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"
|
description: "Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes"
|
||||||
tags: [graph, renderer, opengl, gpu, instanced, fbo, visualization, frustum-cull, rgba8, vertex-pulling, tbo]
|
tags: [graph, renderer, opengl, gpu, instanced, fbo, visualization, frustum-cull, rgba8, vertex-pulling, tbo, sdf, icons, arrows, edge-styles]
|
||||||
uses_functions: ["gl_loader_cpp_gfx"]
|
uses_functions: ["gl_loader_cpp_gfx"]
|
||||||
uses_types: ["GraphData_cpp_viz"]
|
uses_types: ["GraphData_cpp_viz", "EntityType_cpp_viz", "RelationType_cpp_viz"]
|
||||||
returns: []
|
returns: []
|
||||||
returns_optional: false
|
returns_optional: false
|
||||||
error_type: "error_go_core"
|
error_type: "error_go_core"
|
||||||
@@ -88,6 +88,13 @@ ndc = (screen / viewport) * 2 - 1
|
|||||||
|
|
||||||
## Notas
|
## Notas
|
||||||
|
|
||||||
|
- **v1.5** (2026-04-29, issue 0049f): renderer extendido con shapes SDF, atlas de iconos, flechas direccionales y estilos de arista. API publica gana `graph_renderer_set_icon_atlas(r, tex, uv_table, count)`. Cambios internos:
|
||||||
|
1. **6 shapes SDF en el fragment shader**: circle, square, diamond, hex (regular), triangle (equilatero), rounded_square. `pick_sdf(shape)` despacha por valor (1..6). El AA usa `fwidth(d)` para mantener calidad a cualquier zoom; el outline se compone con un `smoothstep` extra del SDF.
|
||||||
|
2. **Icon atlas opcional**: el shader compone un overlay con el icono Tabler bakeado por `graph_icons_build`. Las UVs (4 floats por icono) viven en un `uniform vec4 u_icon_uvs[256]` — solo se sube el icon_id (16 bits) por instancia. La textura se bindea al texture unit 1.
|
||||||
|
3. **Aristas direccionales**: cada arista pasa de 2 a 6 vertices (`glDrawArraysInstanced(GL_LINES, 0, 6, edges)`). Los 4 vertices extra dibujan un chevron (2 lineas) en la cabeza si `flags & EF_DIRECTED`; el segmento principal se acorta 5 px para que la flecha no se incruste en el target.
|
||||||
|
4. **Edge styles**: `style_flags` (32 bits) combina `flags` (low 8) y `style` (next 8). Fragment shader descarta segun `arc_length` interpolado: dashed (mod 8 > 4) y dotted (mod 4 > 1). Las lineas del chevron son siempre solidas.
|
||||||
|
5. **NodeInstance**: 16 → 24 bytes (anade `shape_icon` packeado + 4 bytes pad). EdgeStatic: 16 → 20 bytes (anade `style_flags`). Bandwidth de aristas sigue subiendo solo en cambios de grafo (vertex pulling intacto).
|
||||||
|
|
||||||
- **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.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.
|
- **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.
|
- 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.
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ add_fn_test(test_graph_edge_static test_graph_edge_static.cpp
|
|||||||
add_fn_test(test_graph_types test_graph_types.cpp
|
add_fn_test(test_graph_types test_graph_types.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/viz/graph_types.cpp)
|
${CMAKE_CURRENT_SOURCE_DIR}/../functions/viz/graph_types.cpp)
|
||||||
|
|
||||||
|
# --- Issue 0049f — atlas de iconos Tabler para graph_renderer ---------------
|
||||||
|
# graph_icons.cpp incluye gl_loader.h y referencia gl* — el atlas se puede
|
||||||
|
# construir sin contexto via FN_GRAPH_ICONS_SKIP_GL=1 (set por el test), pero
|
||||||
|
# las funciones GL siguen siendo simbolos a resolver en link. Linkamos contra
|
||||||
|
# OpenGL::GL (Linux) u opengl32 (Win cross) para que el linker quede contento.
|
||||||
|
add_fn_test(test_graph_icons test_graph_icons.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../functions/viz/graph_icons.cpp)
|
||||||
|
target_include_directories(test_graph_icons PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../vendor/imgui)
|
||||||
|
target_compile_definitions(test_graph_icons PRIVATE
|
||||||
|
FN_CPP_ROOT="${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(test_graph_icons PRIVATE opengl32)
|
||||||
|
else()
|
||||||
|
find_package(OpenGL REQUIRED)
|
||||||
|
target_link_libraries(test_graph_icons PRIVATE OpenGL::GL)
|
||||||
|
endif()
|
||||||
|
|
||||||
# --- Visual golden-image diff (issue 0048) ---------------------------------
|
# --- Visual golden-image diff (issue 0048) ---------------------------------
|
||||||
# El binario primitives_gallery se compila con --capture; el test compara los
|
# El binario primitives_gallery se compila con --capture; el test compara los
|
||||||
# PNGs generados con los goldens en cpp/tests/golden/. Si no hay goldens o el
|
# PNGs generados con los goldens en cpp/tests/golden/. Si no hay goldens o el
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Unit tests para `graph_icons` (issue 0049f).
|
||||||
|
// Cubre la parte CPU del builder — bake del atlas, layout en grid 16x16,
|
||||||
|
// regiones 1-indexed, uv_table consistente, y comportamiento ante codepoints
|
||||||
|
// inexistentes en la fuente. La subida a GPU se desactiva con la env
|
||||||
|
// `FN_GRAPH_ICONS_SKIP_GL=1` (set en el setup) para que el test corra sin
|
||||||
|
// contexto GL en CI.
|
||||||
|
|
||||||
|
#define CATCH_CONFIG_MAIN
|
||||||
|
#include "catch_amalgamated.hpp"
|
||||||
|
|
||||||
|
#include "viz/graph_icons.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Tabler 3.41: codepoints que sabemos que existen en la TTF embebida en el
|
||||||
|
// repo (ver cpp/functions/core/icons_tabler.h).
|
||||||
|
const uint16_t k_known[] = {
|
||||||
|
0xEB4Du, // TI_USER
|
||||||
|
0xEAE5u, // TI_MAIL
|
||||||
|
0xEAB9u, // TI_GLOBE
|
||||||
|
0xEB09u, // TI_PHONE
|
||||||
|
0xEA4Fu, // TI_BUILDING
|
||||||
|
0xEA88u, // TI_DATABASE
|
||||||
|
};
|
||||||
|
constexpr int k_known_count = sizeof(k_known) / sizeof(k_known[0]);
|
||||||
|
|
||||||
|
bool any_alpha_in_region(const unsigned char* pixels, int W,
|
||||||
|
float u0, float v0, float u1, float v1)
|
||||||
|
{
|
||||||
|
int x0 = (int)(u0 * W);
|
||||||
|
int y0 = (int)(v0 * W);
|
||||||
|
int x1 = (int)(u1 * W);
|
||||||
|
int y1 = (int)(v1 * W);
|
||||||
|
for (int y = y0; y < y1; ++y) {
|
||||||
|
for (int x = x0; x < x1; ++x) {
|
||||||
|
if (pixels[(y * W + x) * 4 + 3] > 0) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnvSetup {
|
||||||
|
EnvSetup() { setenv("FN_GRAPH_ICONS_SKIP_GL", "1", /*overwrite=*/1); }
|
||||||
|
};
|
||||||
|
EnvSetup _env_setup;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("graph_icons_build con 6 codepoints conocidos", "[viz][graph_icons]") {
|
||||||
|
IconAtlas* a = graph_icons_build(k_known, k_known_count, 32);
|
||||||
|
REQUIRE(a != nullptr);
|
||||||
|
REQUIRE(graph_icons_count(a) == k_known_count);
|
||||||
|
REQUIRE(graph_icons_width(a) == 512);
|
||||||
|
REQUIRE(graph_icons_height(a) == 512);
|
||||||
|
graph_icons_destroy(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("regiones 1-indexed y orden conservado", "[viz][graph_icons]") {
|
||||||
|
IconAtlas* a = graph_icons_build(k_known, k_known_count, 32);
|
||||||
|
REQUIRE(a != nullptr);
|
||||||
|
|
||||||
|
// icon_id = 0 -> nullptr (reservado para "sin icono")
|
||||||
|
REQUIRE(graph_icons_region(a, 0) == nullptr);
|
||||||
|
|
||||||
|
// icon_id = i+1 -> region con codepoint en posicion i del array
|
||||||
|
for (int i = 0; i < k_known_count; ++i) {
|
||||||
|
const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1));
|
||||||
|
REQUIRE(r != nullptr);
|
||||||
|
REQUIRE(r->id == (uint16_t)(i + 1));
|
||||||
|
REQUIRE(r->codepoint == k_known[i]);
|
||||||
|
REQUIRE(r->u0 < r->u1);
|
||||||
|
REQUIRE(r->v0 < r->v1);
|
||||||
|
REQUIRE(r->u0 >= 0.0f);
|
||||||
|
REQUIRE(r->v1 <= 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon_id fuera de rango -> nullptr
|
||||||
|
REQUIRE(graph_icons_region(a, (uint16_t)(k_known_count + 1)) == nullptr);
|
||||||
|
REQUIRE(graph_icons_region(a, 9999) == nullptr);
|
||||||
|
|
||||||
|
graph_icons_destroy(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("uv_table coincide con regions", "[viz][graph_icons]") {
|
||||||
|
IconAtlas* a = graph_icons_build(k_known, k_known_count, 32);
|
||||||
|
REQUIRE(a != nullptr);
|
||||||
|
|
||||||
|
const float* uv = graph_icons_uv_table(a);
|
||||||
|
REQUIRE(uv != nullptr);
|
||||||
|
|
||||||
|
for (int i = 0; i < k_known_count; ++i) {
|
||||||
|
const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1));
|
||||||
|
REQUIRE(uv[i * 4 + 0] == r->u0);
|
||||||
|
REQUIRE(uv[i * 4 + 1] == r->v0);
|
||||||
|
REQUIRE(uv[i * 4 + 2] == r->u1);
|
||||||
|
REQUIRE(uv[i * 4 + 3] == r->v1);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_icons_destroy(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("regiones tienen contenido (alpha != 0)", "[viz][graph_icons]") {
|
||||||
|
// El test no se puede correr si el TTF no esta accesible — en ese caso
|
||||||
|
// graph_icons_build devuelve nullptr y skipeamos.
|
||||||
|
IconAtlas* a = graph_icons_build(k_known, k_known_count, 32);
|
||||||
|
if (!a) {
|
||||||
|
WARN("tabler-icons.ttf no encontrado — test de contenido skipped. "
|
||||||
|
"Defina FN_CPP_ROOT o copie el TTF a ./assets/.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned char* px = graph_icons_pixels(a);
|
||||||
|
REQUIRE(px != nullptr);
|
||||||
|
|
||||||
|
for (int i = 0; i < k_known_count; ++i) {
|
||||||
|
const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1));
|
||||||
|
REQUIRE(any_alpha_in_region(px, graph_icons_width(a),
|
||||||
|
r->u0, r->v0, r->u1, r->v1));
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_icons_destroy(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("count fuera de rango devuelve nullptr", "[viz][graph_icons]") {
|
||||||
|
REQUIRE(graph_icons_build(k_known, 0, 32) == nullptr);
|
||||||
|
REQUIRE(graph_icons_build(k_known, -1, 32) == nullptr);
|
||||||
|
REQUIRE(graph_icons_build(k_known, 257, 32) == nullptr); // max 256
|
||||||
|
REQUIRE(graph_icons_build(nullptr, k_known_count, 32) == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("layout en grid 16x16", "[viz][graph_icons]") {
|
||||||
|
IconAtlas* a = graph_icons_build(k_known, k_known_count, 32);
|
||||||
|
REQUIRE(a != nullptr);
|
||||||
|
|
||||||
|
// Cada celda es 32px = 32/512 = 0.0625 en UV. Con 1px padding, las UVs
|
||||||
|
// van de 1/512 a 31/512 dentro de la celda.
|
||||||
|
const float cell_uv = 32.0f / 512.0f;
|
||||||
|
for (int i = 0; i < k_known_count; ++i) {
|
||||||
|
const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1));
|
||||||
|
int row = i / 16;
|
||||||
|
int col = i % 16;
|
||||||
|
// u0 esta dentro de [col*cell, (col+1)*cell]
|
||||||
|
REQUIRE(r->u0 >= col * cell_uv);
|
||||||
|
REQUIRE(r->u1 <= (col + 1) * cell_uv);
|
||||||
|
REQUIRE(r->v0 >= row * cell_uv);
|
||||||
|
REQUIRE(r->v1 <= (row + 1) * cell_uv);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph_icons_destroy(a);
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
| [0049c](completed/0049c-graph-renderer-tier1.md) | graph_renderer Tier 1: RGBA8, orphan, frustum cull, auto-pause | completado | alta | perf | parte de 0049 |
|
| [0049c](completed/0049c-graph-renderer-tier1.md) | graph_renderer Tier 1: RGBA8, orphan, frustum cull, auto-pause | completado | alta | perf | parte de 0049 |
|
||||||
| [0049d](completed/0049d-graph-edges-vertex-pulling.md) | Aristas via vertex pulling con TBO | completado | alta | perf | parte de 0049 |
|
| [0049d](completed/0049d-graph-edges-vertex-pulling.md) | Aristas via vertex pulling con TBO | completado | alta | perf | parte de 0049 |
|
||||||
| [0049e](completed/0049e-graph-types-extended.md) | graph_types modelo extendido + EntityType/RelationType | completado | alta | feature | parte de 0049 |
|
| [0049e](completed/0049e-graph-types-extended.md) | graph_types modelo extendido + EntityType/RelationType | completado | alta | feature | parte de 0049 |
|
||||||
| [0049f](0049f-graph-renderer-symbols.md) | Renderer extendido: shapes SDF, icon atlas, flechas, edge styles | pendiente | alta | feature | parte de 0049 |
|
| [0049f](completed/0049f-graph-renderer-symbols.md) | Renderer extendido: shapes SDF, icon atlas, flechas, edge styles | completado | alta | feature | parte de 0049 |
|
||||||
| [0049g](0049g-graph-source-operations.md) | graph_sources: lector operations.db + abstraccion funcional | pendiente | alta | feature | parte de 0049 |
|
| [0049g](0049g-graph-source-operations.md) | graph_sources: lector operations.db + abstraccion funcional | pendiente | alta | feature | parte de 0049 |
|
||||||
| [0049h](0049h-graph-force-layout-gpu.md) | graph_force_layout_gpu: compute shader + spatial hash | pendiente | media-alta | feature | parte de 0049 |
|
| [0049h](0049h-graph-force-layout-gpu.md) | graph_force_layout_gpu: compute shader + spatial hash | pendiente | media-alta | feature | parte de 0049 |
|
||||||
| [0049i](0049i-graph-layouts-static.md) | graph_layouts (radial/hierarchical/fixed) + viewport multi-select | pendiente | media | feature | parte de 0049 |
|
| [0049i](0049i-graph-layouts-static.md) | graph_layouts (radial/hierarchical/fixed) + viewport multi-select | pendiente | media | feature | parte de 0049 |
|
||||||
|
|||||||
Reference in New Issue
Block a user