Files
茂之钳 d05dc3f7f3
CI / Build & Test (push) Failing after 30s
CI / Release Build (push) Failing after 35s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
fix: 3MF reader support multiple objects in single model file
2026-07-24 11:39:36 +00:00

193 lines
5.3 KiB
C++

#include <gtest/gtest.h>
#include <cstdio>
#include <fstream>
#include "vde/foundation/io_3mf.h"
using namespace vde::foundation;
using namespace vde::mesh;
// Helper: build a tetrahedron
static ThreeMFMesh make_tetrahedron(const std::string& name = "") {
ThreeMFMesh m;
m.name = name;
m.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 m;
}
// Helper: build a cube (8 vertices, 12 triangles)
static ThreeMFMesh make_cube(const std::string& name = "") {
ThreeMFMesh m;
m.name = name;
m.mesh.build_from_triangles(
{
Point3D(0,0,0), Point3D(1,0,0), Point3D(1,1,0), Point3D(0,1,0),
Point3D(0,0,1), Point3D(1,0,1), Point3D(1,1,1), Point3D(0,1,1)
},
{
{0,1,2}, {0,2,3}, // front
{4,5,6}, {4,6,7}, // back
{0,4,5}, {0,5,1}, // bottom
{2,1,5}, {2,5,6}, // right
{3,2,6}, {3,6,7}, // top
{0,3,7}, {0,7,4} // left
}
);
return m;
}
// ── Export / round-trip ─────────────────────────────────────
TEST(Io3mfTest, ExportThenReadTetrahedron) {
auto orig = make_tetrahedron("Tet");
const char* path = "/tmp/test_3mf_tet.3mf";
std::remove(path);
ASSERT_TRUE(write_3mf(path, {orig}, "VDE-Test"));
auto loaded = read_3mf(path);
ASSERT_EQ(loaded.size(), 1u);
EXPECT_EQ(loaded[0].mesh.num_vertices(), 4u);
EXPECT_EQ(loaded[0].mesh.num_faces(), 4u);
EXPECT_EQ(loaded[0].name, "Tet");
std::remove(path);
}
TEST(Io3mfTest, ExportThenReadCube) {
auto orig = make_cube("MyBox");
const char* path = "/tmp/test_3mf_cube.3mf";
std::remove(path);
ASSERT_TRUE(write_3mf(path, {orig}));
auto loaded = read_3mf(path);
ASSERT_EQ(loaded.size(), 1u);
EXPECT_EQ(loaded[0].mesh.num_vertices(), 8u);
EXPECT_EQ(loaded[0].mesh.num_faces(), 12u);
EXPECT_EQ(loaded[0].name, "MyBox");
std::remove(path);
}
TEST(Io3mfTest, NamePreserved) {
auto orig = make_tetrahedron("DesignerPart001");
const char* path = "/tmp/test_3mf_name.3mf";
std::remove(path);
ASSERT_TRUE(write_3mf(path, {orig}));
auto loaded = read_3mf(path);
ASSERT_EQ(loaded.size(), 1u);
EXPECT_EQ(loaded[0].name, "DesignerPart001");
std::remove(path);
}
TEST(Io3mfTest, NameEmptyPreserved) {
auto orig = make_tetrahedron(); // no name
const char* path = "/tmp/test_3mf_noname.3mf";
std::remove(path);
ASSERT_TRUE(write_3mf(path, {orig}));
auto loaded = read_3mf(path);
ASSERT_EQ(loaded.size(), 1u);
EXPECT_TRUE(loaded[0].name.empty());
std::remove(path);
}
TEST(Io3mfTest, ReadNonExistentFile) {
auto result = read_3mf("/tmp/nonexistent_xyz_3mf.3mf");
EXPECT_TRUE(result.empty());
}
TEST(Io3mfTest, EmptyMeshesReturnsFalse) {
const char* path = "/tmp/test_3mf_empty.3mf";
std::remove(path);
EXPECT_FALSE(write_3mf(path, {}));
std::remove(path);
}
TEST(Io3mfTest, MultipleMeshes) {
auto t1 = make_tetrahedron("PartA");
auto t2 = make_cube("PartB");
const char* path = "/tmp/test_3mf_multi.3mf";
std::remove(path);
ASSERT_TRUE(write_3mf(path, {t1, t2}));
auto loaded = read_3mf(path);
EXPECT_EQ(loaded.size(), 2u);
EXPECT_EQ(loaded[0].name, "PartA");
EXPECT_EQ(loaded[1].name, "PartB");
EXPECT_EQ(loaded[0].mesh.num_vertices(), 4u);
EXPECT_EQ(loaded[1].mesh.num_vertices(), 8u);
std::remove(path);
}
TEST(Io3mfTest, CreatorStoredInMetadata) {
auto orig = make_tetrahedron();
const char* path = "/tmp/test_3mf_creator.3mf";
std::remove(path);
ASSERT_TRUE(write_3mf(path, {orig}, "SuperCAD-2025"));
// Read raw XML and verify metadata tag
auto zip_content = read_3mf(path);
ASSERT_FALSE(zip_content.empty());
// Re-read the raw file to check metadata
std::ifstream f(path, std::ios::binary);
ASSERT_TRUE(f.good());
std::string raw((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
f.close();
// Should contain the creator in metadata
EXPECT_NE(raw.find("SuperCAD-2025"), std::string::npos);
std::remove(path);
}
TEST(Io3mfTest, VertexPositionsRoundTrip) {
// Build a mesh with specific coordinates
ThreeMFMesh orig;
orig.name = "PrecisionTest";
orig.mesh.build_from_triangles(
{
Point3D(1.23456789, 9.87654321, -5.5),
Point3D(0.0, 0.0, 0.0),
Point3D(100.0, -50.0, 25.0)
},
{{0, 1, 2}}
);
const char* path = "/tmp/test_3mf_precision.3mf";
std::remove(path);
ASSERT_TRUE(write_3mf(path, {orig}));
auto loaded = read_3mf(path);
ASSERT_EQ(loaded.size(), 1u);
ASSERT_EQ(loaded[0].mesh.num_vertices(), 3u);
// Check coordinates are close (XML text round-trip precision)
auto& v0 = loaded[0].mesh.vertex(0);
auto& v1 = loaded[0].mesh.vertex(1);
auto& v2 = loaded[0].mesh.vertex(2);
EXPECT_NEAR(v0.x(), 1.23456789, 1e-5);
EXPECT_NEAR(v0.y(), 9.87654321, 1e-5);
EXPECT_NEAR(v0.z(), -5.5, 1e-5);
EXPECT_NEAR(v1.x(), 0.0, 1e-12);
EXPECT_NEAR(v2.x(), 100.0, 1e-12);
std::remove(path);
}