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>
154 lines
5.1 KiB
C++
154 lines
5.1 KiB
C++
// 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);
|
|
}
|