diff --git a/inc/starlet-serializer/parser/mesh/obj_parser.hpp b/inc/starlet-serializer/parser/mesh/obj_parser.hpp new file mode 100644 index 0000000..0803b79 --- /dev/null +++ b/inc/starlet-serializer/parser/mesh/obj_parser.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "mesh_parser_base.hpp" +#include + +namespace Starlet { + +namespace Math { + struct Vertex; + template struct Vec3; + template struct Vec2; +} + +namespace Serializer { + struct MeshData; + + class ObjParser : public MeshParserBase { + public: + bool parse(const std::string& path, MeshData& out); + + private: + struct ObjVertex { + int posI; + int texI; + int normI; + }; + + bool parsePosition(const unsigned char*& p, + std::vector>& positions, + std::vector>& colours); + bool parseTexCoord(const unsigned char*& p, std::vector>& texCoords); + bool parseNormal(const unsigned char*& p, std::vector>& normals); + + void fillMeshData( + MeshData& out, + std::vector& vertices, + std::vector& indices, + bool usedTexCoords, + bool usedNormals + ); + }; +} + +} \ No newline at end of file diff --git a/inc/starlet-serializer/parser/mesh_parser.hpp b/inc/starlet-serializer/parser/mesh_parser.hpp index 46ceb1b..b810747 100644 --- a/inc/starlet-serializer/parser/mesh_parser.hpp +++ b/inc/starlet-serializer/parser/mesh_parser.hpp @@ -13,6 +13,7 @@ class MeshParser : public Parser { private: enum class MeshFormat { PLY, + OBJ, UNKNOWN }; diff --git a/src/parser/mesh/obj_parser.cpp b/src/parser/mesh/obj_parser.cpp new file mode 100644 index 0000000..92d5753 --- /dev/null +++ b/src/parser/mesh/obj_parser.cpp @@ -0,0 +1,304 @@ +#include "starlet-serializer/parser/mesh/obj_parser.hpp" +#include "starlet-serializer/data/mesh_data.hpp" +#include "starlet-logger/logger.hpp" + +#include "starlet-math/vertex.hpp" +#include "starlet-math/vec3.hpp" +#include "starlet-math/vec2.hpp" + +#include +#include +#include +#include +#include + +namespace Starlet::Serializer { + +bool ObjParser::parse(const std::string& path, MeshData& out) { + std::vector file; + if (!loadBinaryFile(file, path)) return false; + + if (file.empty() || file[0] == '\0') + return Logger::error("ObjParser", "parse", "File is empty"); + + std::vector> positions; + std::vector> colours; + + std::vector> texCoords; + std::vector> normals; + + bool usedTexCoords = false; + bool usedNormals = false; + + std::map, unsigned int> vertexMap; + std::vector vertices; + std::vector indices; + + const unsigned char* p = file.data(); + while (*p) { + p = skipWhitespace(p); + if (*p == '\0') break; + + if (*p == '#') { + p = skipToNextLine(p); + continue; + } + + unsigned char cmd[32]{}; + if (!parseToken(p, cmd, sizeof(cmd))) { + p = skipToNextLine(p); + continue; + } + + if (strcmp(reinterpret_cast(cmd), "v") == 0) { + if (!parsePosition(p, positions, colours)) + return Logger::error("ObjParser", "parse", "Failed to parse vertex position at vertex " + std::to_string(positions.size())); + } + else if (strcmp(reinterpret_cast(cmd), "vt") == 0) { + if (!parseTexCoord(p, texCoords)) + return Logger::error("ObjParser", "parse", "Failed to parse texture coordinate at texCoord " + std::to_string(texCoords.size())); + } + else if (strcmp(reinterpret_cast(cmd), "vn") == 0) { + if (!parseNormal(p, normals)) + return Logger::error("ObjParser", "parse", "Failed to parse normal at normal " + std::to_string(normals.size())); + } + else if (strcmp((const char*)cmd, "f") == 0) { + std::vector faceVertices; + + while (true) { + p = skipWhitespace(p); + if (!*p || *p == '\n' || *p == '\r') break; + + ObjVertex fv{ -1, -1, -1 }; + + int posI = 0; + bool negative = (*p == '-'); + if (negative) ++p; + + unsigned int absVal; + if (!parseUInt(p, absVal)) break; + posI = negative ? -static_cast(absVal) : static_cast(absVal); + + if (posI > 0) --posI; + else if (posI < 0) posI = static_cast(positions.size()) + posI; + else return Logger::error("ObjParser", "parse", "Face index cannot be 0"); + + if (posI < 0 || posI >= static_cast(positions.size())) + return Logger::error("ObjParser", "parse", "Face index out of bounds" + std::to_string(posI)); + + fv.posI = posI; + + while (*p == '/') { + ++p; + + if (*p == '/') { + ++p; + + if (*p != ' ' && *p != '\n' && *p != '\r') { + int normI{ 0 }; + negative = (*p == '-'); + if (negative) ++p; + + if (parseUInt(p, absVal)) { + normI = negative ? -static_cast(absVal) : static_cast(absVal); + + if (normI > 0) normI--; + else if (normI < 0) normI = static_cast(normals.size()) + normI; + else return Logger::error("ObjParser", "parse", "Normal index cannot be 0"); + + if (normI < 0 || normI >= static_cast(normals.size())) + return Logger::error("ObjParser", "parse", "Normal index out of bounds: " + std::to_string(normI)); + + fv.normI = normI; + } + } + break; + } + + if (*p == ' ' || *p == '\n' || *p == '\r') + return Logger::error("ObjParser", "parse", "Expected texture coordinate index after '/'"); + + int texCoordI{ 0 }; + negative = (*p == '-'); + if (negative) ++p; + + if (!parseUInt(p, absVal)) + return Logger::error("ObjParser", "parse", "Expected texture coordinate index after '/'"); + + texCoordI = negative ? -static_cast(absVal) : static_cast(absVal); + + if (texCoordI > 0) texCoordI--; + else if (texCoordI < 0) texCoordI = static_cast(texCoords.size()) + texCoordI; + else return Logger::error("ObjParser", "parse", "TexCoord index cannot be 0"); + + if (texCoordI < 0 || texCoordI >= static_cast(texCoords.size())) + return Logger::error("ObjParser", "parse", "TexCoord index out of bounds: " + std::to_string(texCoordI)); + + fv.texI = texCoordI; + + if (*p == '/') { + ++p; + + if (*p != ' ' && *p != '\n' && *p != '\r') { + int normI{ 0 }; + negative = (*p == '-'); + if (negative) ++p; + + if (parseUInt(p, absVal)) { + normI = negative ? -static_cast(absVal) : static_cast(absVal); + + if (normI > 0) normI--; + else if (normI < 0) normI = static_cast(normals.size()) + normI; + else return Logger::error("ObjParser", "parse", "Normal index cannot be 0"); + + if (normI < 0 || normI >= static_cast(normals.size())) + return Logger::error("ObjParser", "parse", "Normal index out of bounds: " + std::to_string(normI)); + + fv.normI = normI; + } + } + } + break; + } + + faceVertices.push_back(fv); + } + + if (faceVertices.size() < 3) + return Logger::error("ObjParser", "parse", "Face has fewer than 3 vertices"); + + std::vector faceIndices; + for (const ObjVertex& fv : faceVertices) { + std::tuple key = std::make_tuple(fv.posI, fv.texI, fv.normI); + + auto it = vertexMap.find(key); + if (it != vertexMap.end()) + faceIndices.push_back(it->second); + else { + Math::Vertex v{}; + v.pos = positions[fv.posI]; + v.col = colours[fv.posI]; + if (fv.texI >= 0) { + v.texCoord = texCoords[fv.texI]; + usedTexCoords = true; + } + if (fv.normI >= 0) { + v.norm = normals[fv.normI]; + usedNormals = true; + } + + unsigned int i = static_cast(vertices.size()); + vertices.push_back(v); + vertexMap[key] = i; + faceIndices.push_back(i); + } + } + + for (size_t i = 2; i < faceIndices.size(); ++i) { + indices.push_back(faceIndices[0]); + indices.push_back(faceIndices[i - 1]); + indices.push_back(faceIndices[i]); + } + } + else p = skipToNextLine(p); + } + + if (vertices.empty() && !positions.empty()) { + for (size_t i = 0; i < positions.size(); ++i) { + Math::Vertex v{}; + + v.pos = positions[i]; + v.col = colours[i]; + + vertices.push_back(v); + } + } + + fillMeshData(out, vertices, indices, usedTexCoords, usedNormals); + return true; +} + +bool ObjParser::parsePosition(const unsigned char*& p, + std::vector>& positions, + std::vector>& colours) { + + Starlet::Math::Vec3 pos; + if (!parseVec3f(p, pos)) + return false; + + Starlet::Math::Vec4 col{ 1.0f, 1.0f, 1.0f, 1.0f }; + float w{ 1.0f }; + + const unsigned char* start = p; + std::vector extra; + while (true) { + float val; + const unsigned char* save = p; + if (parseFloat(p, val)) + extra.push_back(val); + else { + p = save; + break; + } + } + + if (extra.size() == 1) { + w = extra[0]; + } + else if (extra.size() == 3) { + col.x = extra[0]; + col.y = extra[1]; + col.z = extra[2]; + } + else if (extra.size() == 4) { + col.x = extra[0]; + col.y = extra[1]; + col.z = extra[2]; + col.w = extra[3]; + } + + colours.push_back(col); + positions.push_back(pos); + return true; +} + +bool ObjParser::parseTexCoord(const unsigned char*& p, std::vector>& texCoords) { + Starlet::Math::Vec2 tex; + if (!parseVec2f(p, tex)) + return false; + + float w; + parseFloat(p, w); + + texCoords.push_back(tex); + return true; +} + +bool ObjParser::parseNormal(const unsigned char*& p, std::vector>& normals) { + Starlet::Math::Vec3 norm; + if (!parseVec3f(p, norm)) + return false; + + normals.push_back(norm); + return true; +} + +void ObjParser::fillMeshData( + MeshData& out, + std::vector& vertices, + std::vector& indices, + bool usedTexCoords, + bool usedNormals +) { + out.vertices = std::move(vertices); + out.numVertices = out.vertices.size(); + + out.numIndices = indices.size(); + out.numTriangles = out.numIndices / 3; + out.indices = std::move(indices); + + out.hasTexCoords = usedTexCoords; + out.hasNormals = usedNormals; +} + +} \ No newline at end of file diff --git a/src/parser/mesh_parser.cpp b/src/parser/mesh_parser.cpp index c15f724..933562f 100644 --- a/src/parser/mesh_parser.cpp +++ b/src/parser/mesh_parser.cpp @@ -1,6 +1,7 @@ #include "starlet-serializer/parser/mesh_parser.hpp" #include "starlet-serializer/parser/mesh/ply_parser.hpp" +#include "starlet-serializer/parser/mesh/obj_parser.hpp" #include "starlet-logger/logger.hpp" @@ -14,6 +15,9 @@ bool MeshParser::parse(const std::string& path, MeshData& out) { case MeshFormat::PLY: parser = std::make_unique(); break; + case MeshFormat::OBJ: + parser = std::make_unique(); + break; default: Logger::error("MeshParser", "parse", "Unsupported mesh format: " + path); return false; @@ -31,6 +35,7 @@ MeshParser::MeshFormat MeshParser::detectFormat(const std::string& path) { for (char& c : extension) c = static_cast(tolower(c)); if (extension == "ply") return MeshFormat::PLY; + if (extension == "obj") return MeshFormat::OBJ; else return MeshFormat::UNKNOWN; } diff --git a/tests/mesh/obj_parser_test.cpp b/tests/mesh/obj_parser_test.cpp new file mode 100644 index 0000000..05fe543 --- /dev/null +++ b/tests/mesh/obj_parser_test.cpp @@ -0,0 +1,512 @@ +#include "../test_helpers.hpp" + +class ObjParserTest : public MeshParserTest {}; + + + +TEST_F(ObjParserTest, CommentOnly) { + createTestFile("test_data/comments.obj", "# Comment\n# Another comment\n"); + EXPECT_TRUE(parser.parse("test_data/comments.obj", out)); +} + + + +TEST_F(ObjParserTest, SingleVertex) { + createTestFile("test_data/single_vertex.obj", "v 1.0 2.0 3.0\n"); + EXPECT_TRUE(parser.parse("test_data/single_vertex.obj", out)); + EXPECT_EQ(out.numVertices, 1); + EXPECT_FLOAT_EQ(out.vertices[0].pos.x, 1.0f); + EXPECT_FLOAT_EQ(out.vertices[0].pos.y, 2.0f); + EXPECT_FLOAT_EQ(out.vertices[0].pos.z, 3.0f); +} + +TEST_F(ObjParserTest, MultipleVertices) { + createTestFile("test_data/vertices.obj", "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\n"); + EXPECT_TRUE(parser.parse("test_data/vertices.obj", out)); + EXPECT_EQ(out.numVertices, 3); + EXPECT_FLOAT_EQ(out.vertices[2].pos.y, 1.0f); +} + +TEST_F(ObjParserTest, VertexWithW) { + createTestFile("test_data/vertex_w.obj", "v 1.0 2.0 3.0 0.5\n"); + EXPECT_TRUE(parser.parse("test_data/vertex_w.obj", out)); + EXPECT_EQ(out.numVertices, 1); + EXPECT_FLOAT_EQ(out.vertices[0].pos.z, 3.0f); +} + +TEST_F(ObjParserTest, VerticesWithComments) { + createTestFile("test_data/vert_comments.obj", "# Header\nv 1.0 0.0 0.0\n# Mid comment\nv 0.0 1.0 0.0\n"); + EXPECT_TRUE(parser.parse("test_data/vert_comments.obj", out)); + EXPECT_EQ(out.numVertices, 2); +} + +TEST_F(ObjParserTest, VertexWithColour) { + createTestFile("test_data/vertex_color.obj", + "v 1.0 2.0 3.0 0.1 0.2 0.3 0.4\n"); + EXPECT_TRUE(parser.parse("test_data/vertex_color.obj", out)); + EXPECT_EQ(out.numVertices, 1); + EXPECT_FLOAT_EQ(out.vertices[0].pos.x, 1.0f); + EXPECT_FLOAT_EQ(out.vertices[0].pos.y, 2.0f); + EXPECT_FLOAT_EQ(out.vertices[0].pos.z, 3.0f); + EXPECT_FLOAT_EQ(out.vertices[0].col.x, 0.1f); + EXPECT_FLOAT_EQ(out.vertices[0].col.y, 0.2f); + EXPECT_FLOAT_EQ(out.vertices[0].col.z, 0.3f); + EXPECT_FLOAT_EQ(out.vertices[0].col.w, 0.4f); +} + +TEST_F(ObjParserTest, VertexColourOptionalAlpha) { + createTestFile("test_data/vertex_color_alpha.obj", + "v 1.0 2.0 3.0 0.5 0.5 0.5\n"); + EXPECT_TRUE(parser.parse("test_data/vertex_color_alpha.obj", out)); + EXPECT_EQ(out.numVertices, 1); + EXPECT_FLOAT_EQ(out.vertices[0].col.w, 1.0f); +} + +TEST_F(ObjParserTest, VertexColourAndFace) { + createTestFile("test_data/color_face.obj", + "v 0 0 0 1 0 0\nv 1 0 0 0 1 0\nv 0 1 0 0 0 1\n" + "f 1 2 3\n"); + EXPECT_TRUE(parser.parse("test_data/color_face.obj", out)); + EXPECT_EQ(out.numTriangles, 1); + EXPECT_FLOAT_EQ(out.vertices[0].col.x, 1.0f); + EXPECT_FLOAT_EQ(out.vertices[1].col.y, 1.0f); + EXPECT_FLOAT_EQ(out.vertices[2].col.z, 1.0f); +} + + +TEST_F(ObjParserTest, SingleTriangle) { + createTestFile("test_data/triangle.obj", "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n"); + expectValidParse("test_data/triangle.obj", 3, 1); + EXPECT_EQ(out.indices[0], 0); + EXPECT_EQ(out.indices[1], 1); + EXPECT_EQ(out.indices[2], 2); +} +TEST_F(ObjParserTest, QuadTriangulation) { + createTestFile("test_data/quad.obj", "v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4\n"); + expectValidParse("test_data/quad.obj", 4, 2); + EXPECT_EQ(out.indices[0], 0); + EXPECT_EQ(out.indices[1], 1); + EXPECT_EQ(out.indices[2], 2); + EXPECT_EQ(out.indices[3], 0); + EXPECT_EQ(out.indices[4], 2); + EXPECT_EQ(out.indices[5], 3); +} + +TEST_F(ObjParserTest, Pentagon) { + createTestFile("test_data/pentagon.obj", + "v 0 0 0\nv 1 0 0\nv 1.5 0.5 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4 5\n"); + expectValidParse("test_data/pentagon.obj", 5, 3); + EXPECT_EQ(out.indices.size(), 9); +} + +TEST_F(ObjParserTest, Hexagon) { + createTestFile("test_data/hexagon.obj", + "v 0 0 0\nv 1 0 0\nv 1.5 0.5 0\nv 1.5 1.5 0\nv 1 2 0\nv 0 2 0\nf 1 2 3 4 5 6\n"); + expectValidParse("test_data/hexagon.obj", 6, 4); + EXPECT_EQ(out.indices.size(), 12); +} + +TEST_F(ObjParserTest, LargePolygon) { + std::string content; + for (int i = 0; i < 10; i++) + content += "v " + std::to_string(i) + " 0 0\n"; + content += "f 1 2 3 4 5 6 7 8 9 10\n"; + createTestFile("test_data/large_poly.obj", content); + expectValidParse("test_data/large_poly.obj", 10, 8); +} + +TEST_F(ObjParserTest, DegenerateFace) { + createTestFile("test_data/degen.obj", "v 0 0 0\nv 1 0 0\nf 1 1 2\n"); + expectValidParse("test_data/degen.obj", 2, 1); +} + + + +TEST_F(ObjParserTest, TextureCoordinate) { + createTestFile("test_data/texcoord.obj", "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0.5 0.5\nf 1/1 2/1 3/1\n"); + EXPECT_TRUE(parser.parse("test_data/texcoord.obj", out)); + EXPECT_TRUE(out.hasTexCoords); + EXPECT_EQ(out.numVertices, 3); +} + +TEST_F(ObjParserTest, TextureCoordinateWithW) { + createTestFile("test_data/texcoord_w.obj", "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0.5 0.5 1.0\nf 1/1 2/1 3/1\n\n"); + EXPECT_TRUE(parser.parse("test_data/texcoord_w.obj", out)); + EXPECT_TRUE(out.hasTexCoords); + EXPECT_EQ(out.numVertices, 3); +} + +TEST_F(ObjParserTest, TriangleWithTexCoords) { + createTestFile("test_data/tri_tc.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nvt 0 1\nf 1/1 2/2 3/3\n"); + expectValidParse("test_data/tri_tc.obj", 3, 1); + EXPECT_TRUE(out.hasTexCoords); + EXPECT_FALSE(out.hasNormals); + EXPECT_FLOAT_EQ(out.vertices[0].texCoord.x, 0.0f); + EXPECT_FLOAT_EQ(out.vertices[0].texCoord.y, 0.0f); + EXPECT_FLOAT_EQ(out.vertices[2].texCoord.x, 0.0f); + EXPECT_FLOAT_EQ(out.vertices[2].texCoord.y, 1.0f); +} + + + +TEST_F(ObjParserTest, Normal) { + createTestFile("test_data/normal.obj", "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0.0 1.0 0.0\nf 1//1 2//1 3//1\n"); + EXPECT_TRUE(parser.parse("test_data/normal.obj", out)); + EXPECT_TRUE(out.hasNormals); + EXPECT_EQ(out.numVertices, 3); +} + +TEST_F(ObjParserTest, TriangleWithNormals) { + createTestFile("test_data/tri_norm.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0 0 1\nf 1//1 2//1 3//1\n"); + expectValidParse("test_data/tri_norm.obj", 3, 1); + EXPECT_FALSE(out.hasTexCoords); + EXPECT_TRUE(out.hasNormals); + EXPECT_FLOAT_EQ(out.vertices[0].norm.z, 1.0f); + EXPECT_FLOAT_EQ(out.vertices[2].norm.z, 1.0f); +} + +TEST_F(ObjParserTest, FaceMissingTexCoordIndex) { + createTestFile("test_data/missing_tc_idx.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0 0 1\nf 1//1 2//1 3//1\n"); + expectValidParse("test_data/missing_tc_idx.obj", 3, 1); + EXPECT_FALSE(out.hasTexCoords); + EXPECT_TRUE(out.hasNormals); +} + + + +TEST_F(ObjParserTest, TriangleWithTexCoordsAndNormals) { + createTestFile("test_data/tri_full.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nvt 0 1\nvn 0 0 1\nf 1/1/1 2/2/1 3/3/1\n"); + expectValidParse("test_data/tri_full.obj", 3, 1); + EXPECT_TRUE(out.hasTexCoords); + EXPECT_TRUE(out.hasNormals); + EXPECT_FLOAT_EQ(out.vertices[1].texCoord.x, 1.0f); + EXPECT_FLOAT_EQ(out.vertices[1].norm.z, 1.0f); +} + +TEST_F(ObjParserTest, MixedVertexData) { + createTestFile("test_data/mixed.obj", "v 0 0 0\nvt 0.0 0.0\nvn 0 1 0\nv 1 0 0\nv 0 1 0\nf 1/1/1 2/1/1 3/1/1\n"); + EXPECT_TRUE(parser.parse("test_data/mixed.obj", out)); + EXPECT_EQ(out.numVertices, 3); + EXPECT_TRUE(out.hasTexCoords); + EXPECT_TRUE(out.hasNormals); +} + +TEST_F(ObjParserTest, MixedFaceSeparators) { + createTestFile("test_data/mixed_sep.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nvn 0 0 1\nf 1//1 2/2 3\n"); + expectValidParse("test_data/mixed_sep.obj", 3, 1); + EXPECT_TRUE(out.hasNormals); + EXPECT_TRUE(out.hasTexCoords); +} + +TEST_F(ObjParserTest, FaceWithOnlyPositions) { + createTestFile("test_data/pos_only.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0.5 0.5\nvn 0 0 1\nf 1 2 3\n"); + expectValidParse("test_data/pos_only.obj", 3, 1); + EXPECT_FALSE(out.hasTexCoords); + EXPECT_FALSE(out.hasNormals); +} + +TEST_F(ObjParserTest, FaceUsingOnlySubsetOfDefinedData) { + createTestFile("test_data/subset_data.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvn 0 0 1\nf 1 2 3\n"); + expectValidParse("test_data/subset_data.obj", 3, 1); + EXPECT_FALSE(out.hasTexCoords); + EXPECT_FALSE(out.hasNormals); +} + + + +TEST_F(ObjParserTest, NegativeIndex) { + createTestFile("test_data/negative.obj", "v 0 0 0\nv 1 0 0\nv 0 1 0\nf -3 -2 -1\n"); + expectValidParse("test_data/negative.obj", 3, 1); +} + +TEST_F(ObjParserTest, NegativeIndexForPositionOnlyFace) { + createTestFile("test_data/neg_pos_only.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 -1\n"); + expectValidParse("test_data/neg_pos_only.obj", 3, 1); + EXPECT_EQ(out.indices[2], 2); +} + +TEST_F(ObjParserTest, NegativeTexCoordIndex) { + createTestFile("test_data/neg_tc.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nf 1/-2 1/-1 1/-2\n"); + expectValidParse("test_data/neg_tc.obj", 2, 1); + EXPECT_TRUE(out.hasTexCoords); +} + +TEST_F(ObjParserTest, NegativeNormalIndex) { + createTestFile("test_data/neg_norm.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0 0 1\nf 1//-1 2//-1 3//-1\n"); + expectValidParse("test_data/neg_norm.obj", 3, 1); + EXPECT_TRUE(out.hasNormals); +} + +TEST_F(ObjParserTest, FaceNegativeIndexMixedRefs) { + createTestFile("test_data/mixed_neg_idx.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nf 1/1 2/-2 3/1\n"); + expectValidParse("test_data/mixed_neg_idx.obj", 3, 1); + EXPECT_TRUE(out.hasTexCoords); +} + + + +TEST_F(ObjParserTest, VertexDeduplication) { + createTestFile("test_data/dedup.obj", + "v 0 0 0\nvt 0 0\nvt 1 0\nf 1/1 1/2 1/1\n"); + EXPECT_TRUE(parser.parse("test_data/dedup.obj", out)); + EXPECT_EQ(out.numVertices, 2); + EXPECT_EQ(out.numTriangles, 1); + EXPECT_EQ(out.indices[0], 0); + EXPECT_EQ(out.indices[1], 1); + EXPECT_EQ(out.indices[2], 0); +} + +TEST_F(ObjParserTest, DeduplicationWithDifferentNormals) { + createTestFile("test_data/dedup_diff_norms.obj", + "v 0 0 0\nvt 0 0\nvn 0 0 1\nvn 0 1 0\nf 1/1/1 1/1/2 1/1/1\n"); + EXPECT_TRUE(parser.parse("test_data/dedup_diff_norms.obj", out)); + EXPECT_EQ(out.numVertices, 2); + EXPECT_EQ(out.numTriangles, 1); + EXPECT_EQ(out.indices[0], 0); + EXPECT_EQ(out.indices[1], 1); + EXPECT_EQ(out.indices[2], 0); +} + +TEST_F(ObjParserTest, DeduplicationWithDifferentTexCoords) { + createTestFile("test_data/dedup_diff_tex.obj", + "v 0 0 0\nvt 0 0\nvt 1 1\nvn 0 0 1\nf 1/1/1 1/2/1 1/1/1\n"); + EXPECT_TRUE(parser.parse("test_data/dedup_diff_tex.obj", out)); + EXPECT_EQ(out.numVertices, 2); + EXPECT_EQ(out.numTriangles, 1); + EXPECT_EQ(out.indices[0], 0); + EXPECT_EQ(out.indices[1], 1); + EXPECT_EQ(out.indices[2], 0); +} + + + +TEST_F(ObjParserTest, ExtraWhitespace) { + createTestFile("test_data/whitespace.obj", "v 1.0 2.0 3.0\nvt\t0.5\t0.5\nf 1 1 1\n"); + EXPECT_TRUE(parser.parse("test_data/whitespace.obj", out)); + EXPECT_EQ(out.numVertices, 1); +} + +TEST_F(ObjParserTest, WindowsCRLF) { + createTestFile("test_data/crlf.obj", "v 1 2 3\r\nvt 0.5 0.5\r\nf 1/1 1/1 1/1\r\n"); + EXPECT_TRUE(parser.parse("test_data/crlf.obj", out)); + EXPECT_EQ(out.numVertices, 1); + EXPECT_TRUE(out.hasTexCoords); +} + +TEST_F(ObjParserTest, LeadingTrailingBlankLines) { + createTestFile("test_data/blanks.obj", "\n\nv 0 0 0\nv 1 0 0\nv 0 1 0\n\nf 1 2 3\n\n"); + expectValidParse("test_data/blanks.obj", 3, 1); +} + +TEST_F(ObjParserTest, MultipleSpacesInFace) { + createTestFile("test_data/multi_space.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0 0 1\nf 1//1 2//1 3//1\n"); + expectValidParse("test_data/multi_space.obj", 3, 1); +} + +TEST_F(ObjParserTest, CommentAfterFace) { + createTestFile("test_data/face_comment.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nvt 0 1\nvn 0 0 1\nf 1/1/1 2/2/1 3/3/1 # triangle\n"); + expectValidParse("test_data/face_comment.obj", 3, 1); +} + +TEST_F(ObjParserTest, ScientificNotation) { + createTestFile("test_data/scientific.obj", "v 1e-3 2E+2 -3e0\nvt 5e-1 5e-1\nf 1/1 1/1 1/1\n"); + EXPECT_TRUE(parser.parse("test_data/scientific.obj", out)); + EXPECT_FLOAT_EQ(out.vertices[0].pos.x, 0.001f); + EXPECT_FLOAT_EQ(out.vertices[0].pos.y, 200.0f); + EXPECT_FLOAT_EQ(out.vertices[0].pos.z, -3.0f); +} + +TEST_F(ObjParserTest, TexCoordExtraComponents) { + createTestFile("test_data/tc_extra.obj", "v 0 0 0\nvt 0.5 0.5 1.0 extra junk\nf 1/1 1/1 1/1\n"); + EXPECT_TRUE(parser.parse("test_data/tc_extra.obj", out)); + EXPECT_EQ(out.numVertices, 1); +} + +TEST_F(ObjParserTest, InterleavedCommands) { + createTestFile("test_data/interleaved.obj", + "v 0 0 0\nvt 0 0\nv 1 0 0\nvn 0 0 1\nv 0 1 0\nf 1/1/1 2/1/1 3/1/1\n"); + expectValidParse("test_data/interleaved.obj", 3, 1); + EXPECT_TRUE(out.hasTexCoords); + EXPECT_TRUE(out.hasNormals); +} + +TEST_F(ObjParserTest, UnknownCommands) { + createTestFile("test_data/unknown.obj", + "usemtl Material\ns off\ng groupName\nv 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n"); + expectValidParse("test_data/unknown.obj", 3, 1); +} + +TEST_F(ObjParserTest, ObjectCommand) { + createTestFile("test_data/object.obj", "o Cube\nv 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n"); + expectValidParse("test_data/object.obj", 3, 1); +} + +TEST_F(ObjParserTest, LargeFile) { + std::string content; + for (int i = 0; i < 1000; i++) + content += "v " + std::to_string(i) + " 0 0\n"; + + for (int i = 0; i < 300; i++) { + int v1 = i * 3 + 1; + int v2 = i * 3 + 2; + int v3 = i * 3 + 3; + if (v3 <= 1000) + content += "f " + std::to_string(v1) + " " + std::to_string(v2) + " " + std::to_string(v3) + "\n"; + } + + createTestFile("test_data/large.obj", content); + EXPECT_TRUE(parser.parse("test_data/large.obj", out)); + EXPECT_EQ(out.numVertices, 900); + EXPECT_EQ(out.numTriangles, 300); +} + + + +// Error tests +TEST_F(ObjParserTest, EmptyFile) { + createTestFile("test_data/empty.obj", ""); + + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/empty.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("File is empty"), std::string::npos); +} + + + +TEST_F(ObjParserTest, VertexMissingZ) { + createTestFile("test_data/missing_z.obj", "v 1.0 2.0\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/missing_z.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Failed to parse vertex position at vertex 0"), std::string::npos); +} + +TEST_F(ObjParserTest, VertexInvalidFloat) { + createTestFile("test_data/invalid_float.obj", "v 1.0 abc 3.0\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/invalid_float.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Failed to parse vertex position"), std::string::npos); +} + +TEST_F(ObjParserTest, TextureCoordMissingV) { + createTestFile("test_data/tc_missing.obj", "vt 0.5\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/tc_missing.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Failed to parse texture coordinate at texCoord 0"), std::string::npos); +} + +TEST_F(ObjParserTest, TextureCoordInvalid) { + createTestFile("test_data/tc_invalid.obj", "vt abc 0.5\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/tc_invalid.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Failed to parse texture coordinate"), std::string::npos); +} + +TEST_F(ObjParserTest, NormalMissingZ) { + createTestFile("test_data/norm_missing.obj", "vn 0.0 1.0\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/norm_missing.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Failed to parse normal at normal 0"), std::string::npos); +} + +TEST_F(ObjParserTest, NormalInvalid) { + createTestFile("test_data/norm_invalid.obj", "vn 0.0 abc 1.0\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/norm_invalid.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Failed to parse normal"), std::string::npos); +} + + + +TEST_F(ObjParserTest, FaceIndexZero) { + createTestFile("test_data/zero_index.obj", "v 0 0 0\nf 0 1 2\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/zero_index.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Face index cannot be 0"), std::string::npos); +} + +TEST_F(ObjParserTest, FaceIndexOutOfBounds) { + createTestFile("test_data/bounds.obj", "v 0 0 0\nf 1 2 3\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/bounds.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Face index out of bounds"), std::string::npos); +} + +TEST_F(ObjParserTest, VeryLargeIndex) { + createTestFile("test_data/huge_idx.obj", "v 0 0 0\nf 9999999 1 2\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/huge_idx.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("out of bounds"), std::string::npos); +} + +TEST_F(ObjParserTest, FaceTooFewVertices) { + createTestFile("test_data/few_verts.obj", "v 0 0 0\nv 1 0 0\nf 1 2\n"); + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/few_verts.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Face has fewer than 3 vertices"), std::string::npos); +} + + + +TEST_F(ObjParserTest, TrailingSlashInFace) { + createTestFile("test_data/trail_slash.obj", + "v 0 0 0\nv 1 0 0\nvt 0.5 0.5\nf 1/ 2/1\n"); + + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/trail_slash.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("index"), std::string::npos); +} + +TEST_F(ObjParserTest, FaceMissingNormalIndex) { + createTestFile("test_data/missing_norm_idx.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nf 1/1 2/2 3/\n"); + + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/missing_norm_idx.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Expected texture coordinate index"), std::string::npos); +} + +TEST_F(ObjParserTest, BareSlashMalformed) { + createTestFile("test_data/bare_slash.obj", + "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0 0 1\nf 1// 2/3/ 3//\n"); + + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/bare_slash.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_FALSE(output.empty()); +} + +TEST_F(ObjParserTest, FaceIndexMissingIndexAfterFirstSlash) { + createTestFile("test_data/empty_after_slash.obj", + "v 0 0 0\nvn 0 0 1\nf 1/ /1\n"); + + testing::internal::CaptureStderr(); + EXPECT_FALSE(parser.parse("test_data/empty_after_slash.obj", out)); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_NE(output.find("Expected texture coordinate index"), std::string::npos); +} \ No newline at end of file diff --git a/tests/mesh/ply_parser_test.cpp b/tests/mesh/ply_parser_test.cpp index cd962a2..3171968 100644 --- a/tests/mesh/ply_parser_test.cpp +++ b/tests/mesh/ply_parser_test.cpp @@ -358,7 +358,7 @@ end_header // Error tests -TEST_F(PlyParserTest, PlyEmpty) { +TEST_F(PlyParserTest, EmptyFile) { createTestFile("test_data/empty.ply", ""); testing::internal::CaptureStderr(); diff --git a/tests/mesh_parser_test.cpp b/tests/mesh_parser_test.cpp index dbb76a8..bfdd22e 100644 --- a/tests/mesh_parser_test.cpp +++ b/tests/mesh_parser_test.cpp @@ -2,7 +2,7 @@ // PLY format detection TEST_F(MeshParserTest, DetectFormatPlyLowercase) { - createTestFile("test_data/model.ply", "ply\nformat ascii 1.0\nelement vertex 0\nelement face 0\nend_header\n"); + createTestFile("test_data/model.ply", "ply"); testing::internal::CaptureStderr(); EXPECT_FALSE(parser.parse("test_data/model.ply", out)); @@ -13,7 +13,7 @@ TEST_F(MeshParserTest, DetectFormatPlyLowercase) { } TEST_F(MeshParserTest, DetectFormatPlyUppercase) { - createTestFile("test_data/model.PLY", "ply\nformat ascii 1.0\nelement vertex 0\nelement face 0\nend_header\n"); + createTestFile("test_data/model.PLY", "ply"); testing::internal::CaptureStderr(); EXPECT_FALSE(parser.parse("test_data/model.PLY", out)); @@ -24,7 +24,7 @@ TEST_F(MeshParserTest, DetectFormatPlyUppercase) { } TEST_F(MeshParserTest, DetectFormatPlyMixedCase) { - createTestFile("test_data/model.PlY", "ply\nformat ascii 1.0\nelement vertex 0\nelement face 0\nend_header\n"); + createTestFile("test_data/model.PlY", "ply"); testing::internal::CaptureStderr(); EXPECT_FALSE(parser.parse("test_data/model.PlY", out)); @@ -35,9 +35,26 @@ TEST_F(MeshParserTest, DetectFormatPlyMixedCase) { } +// OBJ format detection +TEST_F(MeshParserTest, DetectFormatObjLowercase) { + createTestFile("test_data/model.obj", "# obj"); + EXPECT_TRUE(parser.parse("test_data/model.obj", out)); +} + +TEST_F(MeshParserTest, DetectFormatObjUppercase) { + createTestFile("test_data/model.OBJ", "# obj"); + EXPECT_TRUE(parser.parse("test_data/model.OBJ", out)); +} + +TEST_F(MeshParserTest, DetectFormatObjMixedCase) { + createTestFile("test_data/model.ObJ", "# obj"); + EXPECT_TRUE(parser.parse("test_data/model.ObJ", out)); +} + + // Edge cases TEST_F(MeshParserTest, DetectFormatNoExtension) { - createTestFile("test_data/model", "ply\nformat ascii 1.0\nelement vertex 0\nelement face 0\nend_header\n"); + createTestFile("test_data/model", ""); testing::internal::CaptureStderr(); EXPECT_FALSE(parser.parse("test_data/model", out)); @@ -47,7 +64,7 @@ TEST_F(MeshParserTest, DetectFormatNoExtension) { } TEST_F(MeshParserTest, DetectFormatUnknownExtension) { - createTestFile("test_data/model.unknown", "ply\nformat ascii 1.0\nelement vertex 0\nelement face 0\nend_header\n"); + createTestFile("test_data/model.unknown", ""); testing::internal::CaptureStderr(); EXPECT_FALSE(parser.parse("test_data/model.unknown", out)); @@ -57,11 +74,11 @@ TEST_F(MeshParserTest, DetectFormatUnknownExtension) { } TEST_F(MeshParserTest, DetectFormatEmptyExtension) { - createTestFile("test_data/model.", "ply\n"); + createTestFile("test_data/model.", ""); testing::internal::CaptureStderr(); EXPECT_FALSE(parser.parse("test_data/model.", out)); std::string output = testing::internal::GetCapturedStderr(); EXPECT_NE(output.find("Unsupported mesh format: test_data/model."), std::string::npos); -} \ No newline at end of file +}