371 lines
13 KiB
C++
371 lines
13 KiB
C++
|
|
#include <gtest/gtest.h>
|
||
|
|
#include <fstream>
|
||
|
|
#include <cstdio>
|
||
|
|
#include <cmath>
|
||
|
|
#include "vde/foundation/vde_format.h"
|
||
|
|
#include "vde/brep/modeling.h"
|
||
|
|
#include "vde/brep/feature_tree.h"
|
||
|
|
|
||
|
|
using namespace vde::foundation;
|
||
|
|
using namespace vde::brep;
|
||
|
|
using namespace vde::core;
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 辅助函数
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
static BrepModel make_test_box() {
|
||
|
|
return make_box(2.0, 3.0, 4.0);
|
||
|
|
}
|
||
|
|
|
||
|
|
static BrepModel make_test_cylinder() {
|
||
|
|
return make_cylinder(1.0, 5.0);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void clean(const char* path) {
|
||
|
|
std::remove(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void expect_valid_body(const BrepModel& m) {
|
||
|
|
EXPECT_GT(m.num_vertices(), 0);
|
||
|
|
EXPECT_GT(m.num_edges(), 0);
|
||
|
|
EXPECT_GT(m.num_faces(), 0);
|
||
|
|
EXPECT_GT(m.num_bodies(), 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 1. VdeHeader 测试
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeHeaderTest, DefaultHeader) {
|
||
|
|
VdeHeader hdr;
|
||
|
|
EXPECT_EQ(hdr.magic, VDE_MAGIC_FOURCC);
|
||
|
|
EXPECT_EQ(hdr.version, VDE_FILE_VERSION);
|
||
|
|
EXPECT_EQ(hdr.flags, 0u);
|
||
|
|
EXPECT_EQ(hdr.data_offset, 32u);
|
||
|
|
EXPECT_TRUE(hdr.is_valid());
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeHeaderTest, EncodeDecodeRoundtrip) {
|
||
|
|
VdeHeader hdr;
|
||
|
|
hdr.flags = VDE_FLAG_HAS_ATTRIBUTES | VDE_FLAG_HAS_HISTORY;
|
||
|
|
hdr.data_offset = 64;
|
||
|
|
|
||
|
|
std::vector<uint8_t> buf;
|
||
|
|
hdr.encode(buf);
|
||
|
|
EXPECT_EQ(buf.size(), 32u);
|
||
|
|
|
||
|
|
VdeHeader parsed = VdeHeader::parse(buf.data(), buf.size());
|
||
|
|
EXPECT_EQ(parsed.magic, VDE_MAGIC_FOURCC);
|
||
|
|
EXPECT_EQ(parsed.version, VDE_FILE_VERSION);
|
||
|
|
EXPECT_EQ(parsed.flags, VDE_FLAG_HAS_ATTRIBUTES | VDE_FLAG_HAS_HISTORY);
|
||
|
|
EXPECT_EQ(parsed.data_offset, 64u);
|
||
|
|
EXPECT_TRUE(parsed.is_valid());
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeHeaderTest, InvalidMagicThrows) {
|
||
|
|
uint8_t bad[32] = {};
|
||
|
|
bad[0] = 'B'; bad[1] = 'A'; bad[2] = 'D'; bad[3] = '!';
|
||
|
|
VdeHeader hdr = VdeHeader::parse(bad, 32);
|
||
|
|
EXPECT_FALSE(hdr.is_valid());
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeHeaderTest, TooSmallThrows) {
|
||
|
|
uint8_t tiny[16] = {};
|
||
|
|
EXPECT_THROW(VdeHeader::parse(tiny, 16), std::runtime_error);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 2. 基本保存与加载
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeFormat, SaveBoxToVde) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
const char* path = "/tmp/test_box.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
ASSERT_TRUE(save_vde(box, path));
|
||
|
|
|
||
|
|
std::ifstream f(path, std::ios::binary);
|
||
|
|
ASSERT_TRUE(f.good());
|
||
|
|
|
||
|
|
// 检查 magic bytes "VDE1" (little-endian uint32 0x31454456)
|
||
|
|
char magic[4];
|
||
|
|
f.read(magic, 4);
|
||
|
|
EXPECT_EQ(magic[0], 'V');
|
||
|
|
EXPECT_EQ(magic[1], 'D');
|
||
|
|
EXPECT_EQ(magic[2], 'E');
|
||
|
|
EXPECT_EQ(magic[3], '1');
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeFormat, LoadBoxFromVde) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
const char* path = "/tmp/test_load.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
ASSERT_TRUE(save_vde(box, path));
|
||
|
|
auto loaded = load_vde(path);
|
||
|
|
expect_valid_body(loaded);
|
||
|
|
|
||
|
|
// 拓扑计数应一致
|
||
|
|
EXPECT_EQ(loaded.num_vertices(), box.num_vertices());
|
||
|
|
EXPECT_EQ(loaded.num_edges(), box.num_edges());
|
||
|
|
EXPECT_EQ(loaded.num_faces(), box.num_faces());
|
||
|
|
EXPECT_EQ(loaded.num_bodies(), box.num_bodies());
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeFormat, CylinderRoundtrip) {
|
||
|
|
auto cyl = make_test_cylinder();
|
||
|
|
const char* path = "/tmp/test_cyl.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
ASSERT_TRUE(save_vde(cyl, path));
|
||
|
|
auto loaded = load_vde(path);
|
||
|
|
expect_valid_body(loaded);
|
||
|
|
|
||
|
|
EXPECT_EQ(loaded.num_vertices(), cyl.num_vertices());
|
||
|
|
EXPECT_EQ(loaded.num_faces(), cyl.num_faces());
|
||
|
|
EXPECT_EQ(loaded.num_surfaces(), cyl.num_surfaces());
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeFormat, NonexistentFileThrows) {
|
||
|
|
EXPECT_THROW(load_vde("/tmp/nonexistent_vde_file.vde"), std::runtime_error);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 3. JSON 导出测试
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeJson, SaveJsonFile) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
const char* path = "/tmp/test_box.vde.json";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
ASSERT_TRUE(save_vde_json(box, path));
|
||
|
|
|
||
|
|
std::ifstream f(path);
|
||
|
|
ASSERT_TRUE(f.good());
|
||
|
|
std::string content((std::istreambuf_iterator<char>(f)), {});
|
||
|
|
|
||
|
|
// JSON 应包含关键字段
|
||
|
|
EXPECT_NE(content.find("\"format\": \"VDE1\""), std::string::npos);
|
||
|
|
EXPECT_NE(content.find("\"vertices\""), std::string::npos);
|
||
|
|
EXPECT_NE(content.find("\"edges\""), std::string::npos);
|
||
|
|
EXPECT_NE(content.find("\"faces\""), std::string::npos);
|
||
|
|
EXPECT_NE(content.find("\"loops\""), std::string::npos);
|
||
|
|
EXPECT_NE(content.find("\"surfaces\""), std::string::npos);
|
||
|
|
EXPECT_NE(content.find("\"tolerance\""), std::string::npos);
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeJson, JsonContainsVertexData) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
const char* path = "/tmp/test_json_vertices.vde.json";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
save_vde_json(box, path);
|
||
|
|
std::ifstream f(path);
|
||
|
|
std::string content((std::istreambuf_iterator<char>(f)), {});
|
||
|
|
|
||
|
|
// 正方体应有 8 个顶点
|
||
|
|
EXPECT_NE(content.find("\"point\""), std::string::npos);
|
||
|
|
EXPECT_NE(content.find("\"tolerance\""), std::string::npos);
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 4. 容差测试
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeTolerance, ToleranceConfigRoundtrip) {
|
||
|
|
// 设置非默认容差
|
||
|
|
ToleranceConfig orig;
|
||
|
|
orig.vertex_merge = 1e-5;
|
||
|
|
orig.edge_merge = 2e-5;
|
||
|
|
orig.boolean = 3e-5;
|
||
|
|
ToleranceConfig::set_global(orig);
|
||
|
|
|
||
|
|
auto box = make_test_box();
|
||
|
|
const char* path = "/tmp/test_tol.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
save_vde(box, path);
|
||
|
|
auto loaded = load_vde(path);
|
||
|
|
|
||
|
|
const auto& restored = ToleranceConfig::global();
|
||
|
|
EXPECT_DOUBLE_EQ(restored.vertex_merge, orig.vertex_merge);
|
||
|
|
EXPECT_DOUBLE_EQ(restored.edge_merge, orig.edge_merge);
|
||
|
|
EXPECT_DOUBLE_EQ(restored.boolean, orig.boolean);
|
||
|
|
|
||
|
|
// 恢复默认
|
||
|
|
ToleranceConfig::set_global(ToleranceConfig{});
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 5. 属性测试
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeAttributes, SaveWithVertexAttributes) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
VdeAttributes attrs;
|
||
|
|
attrs.vertex_attrs.push_back({0, "corner_0", 0xFF0000FF, "key=value"});
|
||
|
|
attrs.vertex_attrs.push_back({1, "corner_1", 0x00FF00FF, ""});
|
||
|
|
|
||
|
|
const char* path = "/tmp/test_attrs.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
ASSERT_TRUE(save_vde(box, path, attrs));
|
||
|
|
|
||
|
|
// 重新加载并检查属性
|
||
|
|
VdeAttributes loaded_attrs;
|
||
|
|
load_vde_with_attrs(path, loaded_attrs);
|
||
|
|
|
||
|
|
ASSERT_EQ(loaded_attrs.vertex_attrs.size(), 2u);
|
||
|
|
EXPECT_EQ(loaded_attrs.vertex_attrs[0].vertex_id, 0);
|
||
|
|
EXPECT_EQ(loaded_attrs.vertex_attrs[0].name, "corner_0");
|
||
|
|
EXPECT_EQ(loaded_attrs.vertex_attrs[0].color_rgba, 0xFF0000FFu);
|
||
|
|
EXPECT_EQ(loaded_attrs.vertex_attrs[0].custom_data, "key=value");
|
||
|
|
EXPECT_EQ(loaded_attrs.vertex_attrs[1].vertex_id, 1);
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeAttributes, SaveWithBodyAttributes) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
VdeAttributes attrs;
|
||
|
|
attrs.body_attrs.push_back({0, "MyPart", "material=steel"});
|
||
|
|
|
||
|
|
const char* path = "/tmp/test_body_attrs.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
save_vde(box, path, attrs);
|
||
|
|
VdeAttributes loaded_attrs;
|
||
|
|
load_vde_with_attrs(path, loaded_attrs);
|
||
|
|
|
||
|
|
ASSERT_EQ(loaded_attrs.body_attrs.size(), 1u);
|
||
|
|
EXPECT_EQ(loaded_attrs.body_attrs[0].name, "MyPart");
|
||
|
|
EXPECT_EQ(loaded_attrs.body_attrs[0].custom_data, "material=steel");
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 6. 建模历史测试
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeHistory, SaveWithFeatureHistory) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
|
||
|
|
std::vector<FeatureNode> history;
|
||
|
|
FeatureNode root;
|
||
|
|
root.type = FeatureType::PrimitiveBox;
|
||
|
|
root.params.values = {2.0, 3.0, 4.0};
|
||
|
|
root.params.name = "Box";
|
||
|
|
history.push_back(std::move(root));
|
||
|
|
|
||
|
|
const char* path = "/tmp/test_hist.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
ASSERT_TRUE(save_vde(box, path, {}, &history));
|
||
|
|
|
||
|
|
// 检查文件大小合理
|
||
|
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||
|
|
ASSERT_TRUE(f.good());
|
||
|
|
EXPECT_GT(f.tellg(), std::streampos(64));
|
||
|
|
|
||
|
|
// 加载应该成功(历史在 load_vde 中被静默跳过)
|
||
|
|
auto loaded = load_vde(path);
|
||
|
|
expect_valid_body(loaded);
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 7. 序列化缓冲区往返测试
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeSerialize, BufferRoundtrip) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
auto data = serialize_vde(box);
|
||
|
|
EXPECT_GT(data.size(), 32u);
|
||
|
|
|
||
|
|
auto restored = deserialize_vde(data);
|
||
|
|
expect_valid_body(restored);
|
||
|
|
EXPECT_EQ(restored.num_vertices(), box.num_vertices());
|
||
|
|
EXPECT_EQ(restored.num_faces(), box.num_faces());
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeSerialize, EmptyModelBuffer) {
|
||
|
|
BrepModel empty;
|
||
|
|
auto data = serialize_vde(empty);
|
||
|
|
EXPECT_GE(data.size(), 32u);
|
||
|
|
|
||
|
|
auto restored = deserialize_vde(data);
|
||
|
|
EXPECT_EQ(restored.num_vertices(), 0u);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 8. 文件格式验证
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeFormatValidation, FileHasCorrectMagic) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
const char* path = "/tmp/test_magic.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
save_vde(box, path);
|
||
|
|
|
||
|
|
std::ifstream f(path, std::ios::binary);
|
||
|
|
char magic[4];
|
||
|
|
f.read(magic, 4);
|
||
|
|
|
||
|
|
EXPECT_EQ(std::string(magic, 4), "VDE1");
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeFormatValidation, FileHasSections) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
const char* path = "/tmp/test_sections.vde";
|
||
|
|
clean(path);
|
||
|
|
|
||
|
|
save_vde(box, path);
|
||
|
|
|
||
|
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||
|
|
auto sz = f.tellg();
|
||
|
|
f.seekg(0);
|
||
|
|
std::vector<uint8_t> data(static_cast<size_t>(sz));
|
||
|
|
f.read(reinterpret_cast<char*>(data.data()), sz);
|
||
|
|
|
||
|
|
// 头部后应至少有 拓扑 + 几何 + 容差 三个段
|
||
|
|
EXPECT_GT(data.size(), 32u + 30u); // header + 3 section headers minimum
|
||
|
|
|
||
|
|
clean(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
// 9. 边界情况
|
||
|
|
// ═══════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
TEST(VdeEdgeCase, VerySmallFileThrows) {
|
||
|
|
std::vector<uint8_t> tiny(8, 0);
|
||
|
|
EXPECT_THROW(deserialize_vde(tiny), std::runtime_error);
|
||
|
|
}
|
||
|
|
|
||
|
|
TEST(VdeEdgeCase, CorruptedMagicThrows) {
|
||
|
|
auto box = make_test_box();
|
||
|
|
auto data = serialize_vde(box);
|
||
|
|
// 损坏 magic
|
||
|
|
data[0] = 'X'; data[1] = 'X'; data[2] = 'X'; data[3] = 'X';
|
||
|
|
EXPECT_THROW(deserialize_vde(data), std::runtime_error);
|
||
|
|
}
|