feat(v3.2): 3MF format import/export
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdio>
|
||||
#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-6);
|
||||
EXPECT_NEAR(v0.y(), 9.87654321, 1e-6);
|
||||
EXPECT_NEAR(v0.z(), -5.5, 1e-6);
|
||||
EXPECT_NEAR(v1.x(), 0.0, 1e-12);
|
||||
EXPECT_NEAR(v2.x(), 100.0, 1e-12);
|
||||
|
||||
std::remove(path);
|
||||
}
|
||||
Reference in New Issue
Block a user