#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 #include #include #include #include #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 read_file_bytes(const char* path) { std::vector 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 regions; // index 0 dummy (id=0 -> nullptr) std::vector pixels_rgba; // CPU copy para tests std::vector 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 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; }