c967c2edfd
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>
242 lines
9.0 KiB
C++
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;
|
|
}
|