Files
fn_registry/cpp/functions/viz/graph_icons.cpp
T
egutierrez c967c2edfd 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>
2026-04-29 23:01:49 +02:00

242 lines
9.0 KiB
C++

#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;
}