feat: GLTF/GLB export with normals + B-Rep tessellation + pipeline example
CI / Build & Test (push) Failing after 32s
CI / Release Build (push) Failing after 31s

This commit is contained in:
茂之钳
2026-07-24 09:06:12 +00:00
parent b3269b4f09
commit 40440fcc3e
8 changed files with 521 additions and 29 deletions
+1
View File
@@ -14,3 +14,4 @@ add_subdirectory(boolean)
add_subdirectory(brep)
add_subdirectory(sdf)
add_subdirectory(sketch)
add_subdirectory(foundation)
+1
View File
@@ -0,0 +1 @@
add_vde_test(test_io_gltf)
+151
View File
@@ -0,0 +1,151 @@
#include <gtest/gtest.h>
#include <fstream>
#include <cstdio>
#include "vde/foundation/io_gltf.h"
using namespace vde::foundation;
using namespace vde::mesh;
using namespace vde::core;
// ── Helper: build a simple tetrahedron mesh ──
static HalfedgeMesh make_tetrahedron() {
HalfedgeMesh mesh;
mesh.build_from_triangles(
{
Point3D(0, 0, 0), Point3D(1, 0, 0), Point3D(0.5, 0.866, 0), Point3D(0.5, 0.289, 0.816)
},
{
{0, 1, 2}, {0, 3, 1}, {1, 3, 2}, {0, 2, 3}
}
);
return mesh;
}
// ── Tests ──────────────────────────────────────────────────────────
TEST(IoGltfTest, ExportTetToGltfJson) {
auto mesh = make_tetrahedron();
GltfOptions opt;
opt.binary = false;
opt.include_normals = false;
const char* path = "/tmp/test_tet.gltf";
std::remove(path); // clean up from previous run
ASSERT_TRUE(write_gltf(path, mesh, opt));
std::ifstream f(path);
ASSERT_TRUE(f.good());
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
EXPECT_NE(content.find("\"asset\""), std::string::npos);
EXPECT_NE(content.find("\"meshes\""), std::string::npos);
EXPECT_NE(content.find("ViewDesignEngine"), std::string::npos);
std::remove(path);
std::remove("/tmp/test_tet.bin");
}
TEST(IoGltfTest, ExportTetToGlbBinary) {
auto mesh = make_tetrahedron();
GltfOptions opt;
opt.binary = true;
opt.include_normals = false;
const char* path = "/tmp/test_tet.glb";
std::remove(path);
ASSERT_TRUE(write_gltf(path, mesh, opt));
// Verify GLB magic bytes
std::ifstream f(path, std::ios::binary);
ASSERT_TRUE(f.good());
uint32_t magic = 0, version = 0;
f.read(reinterpret_cast<char*>(&magic), 4);
f.read(reinterpret_cast<char*>(&version), 4);
EXPECT_EQ(magic, 0x46546C67u) << "GLB magic should be 'glTF'";
EXPECT_EQ(version, 2u) << "GLB version should be 2";
// Should contain "JSON" chunk marker
uint32_t chunk_len = 0, chunk_type = 0;
f.read(reinterpret_cast<char*>(&chunk_len), 4);
f.read(reinterpret_cast<char*>(&chunk_type), 4);
EXPECT_EQ(chunk_type, 0x4E4F534Au) << "First chunk should be JSON";
std::remove(path);
}
TEST(IoGltfTest, ExportWithNormals) {
auto mesh = make_tetrahedron();
GltfOptions opt;
opt.binary = true;
opt.include_normals = true;
const char* path = "/tmp/test_tet_normals.glb";
std::remove(path);
ASSERT_TRUE(write_gltf(path, mesh, opt));
// Read GLB and verify it contains JSON with NORMAL attribute
std::ifstream f(path, std::ios::binary);
ASSERT_TRUE(f.good());
// Skip header (12 bytes)
f.seekg(12);
// Read JSON chunk
uint32_t chunk_len = 0, chunk_type = 0;
f.read(reinterpret_cast<char*>(&chunk_len), 4);
f.read(reinterpret_cast<char*>(&chunk_type), 4);
std::string json(chunk_len, '\0');
f.read(&json[0], chunk_len);
EXPECT_NE(json.find("\"NORMAL\""), std::string::npos)
<< "JSON should include NORMAL attribute when normals enabled";
// File should also have a BIN chunk (larger because of normals)
uint32_t bin_len = 0, bin_type = 0;
if (f.read(reinterpret_cast<char*>(&bin_len), 4)) {
f.read(reinterpret_cast<char*>(&bin_type), 4);
EXPECT_EQ(bin_type, 0x004E4942u) << "Second chunk should be BIN";
EXPECT_GT(bin_len, 0u) << "BIN chunk should have data";
}
std::remove(path);
}
TEST(IoGltfTest, ExportBrepBoxToGlb) {
auto box = vde::brep::make_box(2.0, 2.0, 2.0);
const char* path = "/tmp/test_brep_box.glb";
std::remove(path);
ASSERT_TRUE(write_brep_gltf(path, box, 32));
// Verify file exists and has magic
std::ifstream f(path, std::ios::binary);
ASSERT_TRUE(f.good());
uint32_t magic = 0;
f.read(reinterpret_cast<char*>(&magic), 4);
EXPECT_EQ(magic, 0x46546C67u);
std::remove(path);
}
TEST(IoGltfTest, EmptyMeshFailsGracefully) {
HalfedgeMesh empty;
GltfOptions opt;
opt.binary = true;
const char* path = "/tmp/test_empty.glb";
std::remove(path);
EXPECT_FALSE(write_gltf(path, empty, opt))
<< "Exporting empty mesh should return false";
}
TEST(IoGltfTest, EmptyBrepFailsGracefully) {
vde::brep::BrepModel empty;
const char* path = "/tmp/test_empty_brep.glb";
std::remove(path);
EXPECT_FALSE(write_brep_gltf(path, empty, 32))
<< "Exporting empty B-Rep should return false";
}