From 31a7db645fc4aac40dbfe910e15a1a4560f36928 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Sun, 2 Jun 2024 23:39:34 +0900 Subject: [PATCH 01/22] w.i.p. --- examples/viewer/viewer.cc | 54 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/examples/viewer/viewer.cc b/examples/viewer/viewer.cc index 059e57fe..2e6bd952 100644 --- a/examples/viewer/viewer.cc +++ b/examples/viewer/viewer.cc @@ -152,6 +152,8 @@ bool mouseRightPressed; float curr_quat[4]; float prev_quat[4]; float eye[3], lookat[3], up[3]; +float g_angleX = 0.0f; // in degree +float g_angleY = 0.0f; // in degree bool g_show_wire = true; bool g_cull_face = false; @@ -235,6 +237,29 @@ struct mat3 { } }; +struct mat4 { + float m[4][4]; + mat4() { + m[0][0] = 1.0f; + m[0][1] = 0.0f; + m[0][2] = 0.0f; + m[0][3] = 0.0f; + m[1][0] = 0.0f; + m[1][1] = 1.0f; + m[1][2] = 0.0f; + m[1][3] = 0.0f; + m[2][0] = 0.0f; + m[2][1] = 0.0f; + m[2][2] = 1.0f; + m[2][3] = 0.0f; + m[3][0] = 0.0f; + m[3][1] = 0.0f; + m[3][2] = 0.0f; + m[3][3] = 1.0f; + } +}; + + void matmul3x3(const mat3 &a, const mat3 &b, mat3 &dst) { for (size_t i = 0; i < 3; i++) { for (size_t j = 0; j < 3; j++) { @@ -247,6 +272,18 @@ void matmul3x3(const mat3 &a, const mat3 &b, mat3 &dst) { } } +void matmul4x4(const mat4 &a, const mat4 &b, mat4 &dst) { + for (size_t i = 0; i < 4; i++) { + for (size_t j = 0; j < 4; j++) { + float v = 0.0f; + for (size_t k = 0; k < 4; k++) { + v += a.m[i][k] * b.m[k][j]; + } + dst.m[i][j] = v; + } + } +} + void normalizeVector(vec3 &v) { float len2 = v.v[0] * v.v[0] + v.v[1] * v.v[1] + v.v[2] * v.v[2]; if (len2 > 0.0f) { @@ -258,12 +295,13 @@ void normalizeVector(vec3 &v) { } } -// Maya-like turntable +// Maya-like turntable // Reference: // https://gamedev.stackexchange.com/questions/204367/implementing-a-maya-like-orbit-camera-in-vulkan-opengl // // angleX, angleY = angle in degree. -static void turntable(float angleX, float angleY, float center[3]) { +// TODO: scale +static void turntable(float angleX, float angleY, float center[3], float dst[4][4]) { float pivot[3]; pivot[0] = center[0]; pivot[1] = center[1]; @@ -299,7 +337,7 @@ static void turntable(float angleX, float angleY, float center[3]) { rotX.m[2][1] = -sinX; rotX.m[2][2] = cosX; - + } @@ -1138,15 +1176,25 @@ int main(int argc, char** argv) { GLfloat mat[4][4]; gluLookAt(eye[0], eye[1], eye[2], lookat[0], lookat[1], lookat[2], up[0], up[1], up[2]); + + float center[3]; + center[0] = 0.5 * (bmax[0] + bmin[0]); + center[1] = 0.5 * (bmax[1] + bmin[1]); + center[2] = 0.5 * (bmax[2] + bmin[2]); + float rotm[4][4]; + turntable(g_angleX, g_angleY, center, rotm); + build_rotmatrix(mat, curr_quat); glMultMatrixf(&mat[0][0]); // Fit to -1, 1 glScalef(1.0f / maxExtent, 1.0f / maxExtent, 1.0f / maxExtent); +#if 0 // Centerize object. glTranslatef(-0.5 * (bmax[0] + bmin[0]), -0.5 * (bmax[1] + bmin[1]), -0.5 * (bmax[2] + bmin[2])); +#endif Draw(gDrawObjects, materials, textures); From 50461d0e0a77c178bb478e9319d7de82f469a848 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Sat, 24 Aug 2024 02:06:11 +0900 Subject: [PATCH 02/22] Create FUNDING.yml --- .github/FUNDING.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..0fd988d9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: syoyo # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From b5346fac929c8a862a1c1d4d89db5477d30b7e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?El=C3=ADas=20Mart=C3=ADnez?= Date: Wed, 23 Oct 2024 19:27:25 +0200 Subject: [PATCH 03/22] Update README.md (#387) Fix wrong C standard --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bf40db1..5ed88291 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Tiny but powerful single file wavefront obj loader written in C++03. No dependen `tinyobjloader` is good for embedding .obj loader to your (global illumination) renderer ;-) -If you are looking for C89 version, please see https://github.com/syoyo/tinyobjloader-c . +If you are looking for C99 version, please see https://github.com/syoyo/tinyobjloader-c . Version notice -------------- From 5cd3842fdca3b06cc993801cff1825fc6d999068 Mon Sep 17 00:00:00 2001 From: Diego Sanz <35377545+ElDigoXD@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:13:01 +0100 Subject: [PATCH 04/22] Update README.md (fix typo) (#388) tri*na*gulation -> tri*an*gulation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5ed88291..8569a352 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ You can enable `double(64bit)` precision by using `TINYOBJLOADER_USE_DOUBLE` def When you enable `triangulation`(default is enabled), TinyObjLoader triangulate polygons(faces with 4 or more vertices). -Built-in trinagulation code may not work well in some polygon shape. +Built-in triangulation code may not work well in some polygon shape. You can define `TINYOBJLOADER_USE_MAPBOX_EARCUT` for robust triangulation using `mapbox/earcut.hpp`. This requires C++11 compiler though. And you need to copy `mapbox/earcut.hpp` to your project. @@ -260,7 +260,7 @@ If you have your own `mapbox/earcut.hpp` file incuded in your project, you can d ```c++ #define TINYOBJLOADER_IMPLEMENTATION // define this in only *one* .cc -// Optional. define TINYOBJLOADER_USE_MAPBOX_EARCUT gives robust trinagulation. Requires C++11 +// Optional. define TINYOBJLOADER_USE_MAPBOX_EARCUT gives robust triangulation. Requires C++11 //#define TINYOBJLOADER_USE_MAPBOX_EARCUT #include "tiny_obj_loader.h" @@ -332,7 +332,7 @@ for (size_t s = 0; s < shapes.size(); s++) { ```c++ #define TINYOBJLOADER_IMPLEMENTATION // define this in only *one* .cc -// Optional. define TINYOBJLOADER_USE_MAPBOX_EARCUT gives robust trinagulation. Requires C++11 +// Optional. define TINYOBJLOADER_USE_MAPBOX_EARCUT gives robust triangulation. Requires C++11 //#define TINYOBJLOADER_USE_MAPBOX_EARCUT #include "tiny_obj_loader.h" From 3201329ba83c244e85a4933f5a08f813740761a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Sch=C3=A4fer?= <38749758+scschaefer@users.noreply.github.com> Date: Sat, 23 Nov 2024 23:20:00 +0100 Subject: [PATCH 05/22] Fix parsing of comments at the end of lines for tokens with variable number of elements. (#389) (#390) --- tiny_obj_loader.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index c23acc0d..829ea6de 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -2708,7 +2708,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, sw.vertex_id = vid; - while (!IS_NEW_LINE(token[0])) { + while (!IS_NEW_LINE(token[0]) && token[0] != '#') { real_t j, w; // joint_id should not be negative, weight may be negative // TODO(syoyo): # of elements check @@ -2749,7 +2749,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, __line_t line; - while (!IS_NEW_LINE(token[0])) { + while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi; if (!parseTriple(&token, static_cast(v.size() / 3), static_cast(vn.size() / 3), @@ -2780,7 +2780,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, __points_t pts; - while (!IS_NEW_LINE(token[0])) { + while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi; if (!parseTriple(&token, static_cast(v.size() / 3), static_cast(vn.size() / 3), @@ -2815,7 +2815,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, face.smoothing_group_id = current_smoothing_id; face.vertex_indices.reserve(3); - while (!IS_NEW_LINE(token[0])) { + while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi; if (!parseTriple(&token, static_cast(v.size() / 3), static_cast(vn.size() / 3), @@ -2951,7 +2951,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, std::vector names; - while (!IS_NEW_LINE(token[0])) { + while (!IS_NEW_LINE(token[0]) && token[0] != '#') { std::string str = parseString(&token); names.push_back(str); token += strspn(token, " \t\r"); // skip tag @@ -3245,7 +3245,7 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, token += strspn(token, " \t"); indices.clear(); - while (!IS_NEW_LINE(token[0])) { + while (!IS_NEW_LINE(token[0]) && token[0] != '#') { vertex_index_t vi = parseRawTriple(&token); index_t idx; @@ -3360,7 +3360,7 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, if (token[0] == 'g' && IS_SPACE((token[1]))) { names.clear(); - while (!IS_NEW_LINE(token[0])) { + while (!IS_NEW_LINE(token[0]) && token[0] != '#') { std::string str = parseString(&token); names.push_back(str); token += strspn(token, " \t\r"); // skip tag From fe9e7130a0eee720a28f39b33852108217114076 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Sun, 24 Nov 2024 07:20:56 +0900 Subject: [PATCH 06/22] add regression test for #389 --- models/issue-389-comment.obj | 44 ++++++++++++++++++++++++++++++++++++ tests/tester.cc | 27 ++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 models/issue-389-comment.obj diff --git a/models/issue-389-comment.obj b/models/issue-389-comment.obj new file mode 100644 index 00000000..cf16d926 --- /dev/null +++ b/models/issue-389-comment.obj @@ -0,0 +1,44 @@ +g Part 1 +v 0.0576127 0.0488792 0.0423 +v 0.0576127 0.0488792 0 +v -0.0483158 0.0488792 0 +v -0.0483158 0.0488792 0.0423 +v -0.0483158 -0.0139454 0 +v -0.0483158 -0.0139454 0.0423 +v 0.0576127 -0.0139454 0 +v 0.0576127 -0.0139454 0.0423 +vn 0 1 0 +vn -1 0 0 +vn 0 -1 0 +vn 1 0 0 +vn 0 0 1 +vn 0 0 -1 +o mesh0 +f 1//1 2//1 3//1 +f 3//1 4//1 1//1 +o mesh1 +f 4//2 3//2 5//2 +f 5//2 6//2 4//2 +o mesh2 +f 6//3 5//3 7//3 +f 7//3 8//3 6//3 +o mesh3 +f 8//4 7//4 2//4 +f 2//4 1//4 8//4 +o mesh4 +f 8//5 1//5 4//5 +f 4//5 6//5 8//5 +o mesh5 +f 5//6 3//6 2//6 +f 2//6 7//6 5//6 + +# Zusätzliche Linien (aus der Oberseite) +o lines +v 0.0576127 0.0488792 0.0423 # Startpunkt Linie 1 (Ecke 1 Oberseite) +v 0.0576127 0.0488792 0.2423 # Endpunkt Linie 1 (2m Höhe) +v -0.0483158 -0.0139454 0.0423 # Startpunkt Linie 2 (Ecke 6 Oberseite) +v -0.0483158 -0.0139454 0.2423 # Endpunkt Linie 2 (2m Höhe) + +# Linien +l 1 9 # Linie 1 +l 6 10 # Linie 2 diff --git a/tests/tester.cc b/tests/tester.cc index 2f336c76..780d9755 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -1408,6 +1408,31 @@ void test_face_missing_issue295() { TEST_CHECK((3 * 28) == shapes[0].mesh.indices.size()); // 28 triangle faces x 3 } +void test_comment_issue389() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + + std::string warn; + std::string err; + bool ret = tinyobj::LoadObj( + &attrib, &shapes, &materials, &warn, &err, + "../models/issue-389-comment.obj", + gMtlBasePath, /* triangualte */false); + + TEST_CHECK(warn.empty()); + + if (!warn.empty()) { + std::cout << "WARN: " << warn << std::endl; + } + + if (!err.empty()) { + std::cerr << "ERR: " << err << std::endl; + } + + TEST_CHECK(true == ret); +} + // Fuzzer test. // Just check if it does not crash. // Disable by default since Windows filesystem can't create filename of afl @@ -1511,6 +1536,8 @@ TEST_LIST = { test_mtl_filename_with_whitespace_issue46}, {"test_face_missing_issue295", test_face_missing_issue295}, + {"test_comment_issue389", + test_comment_issue389}, {"test_invalid_relative_vertex_index", test_invalid_relative_vertex_index}, {"test_invalid_texture_vertex_index", From 3bb554cf74428d7db13418b4aca1b9752a1d2be8 Mon Sep 17 00:00:00 2001 From: Sean Curtis Date: Wed, 29 Jan 2025 05:47:59 -0800 Subject: [PATCH 07/22] Correct multi-material parsing (#392) Each material gets is state of having specified a diffuse color (kd) reset so that one material doesn't infect another. --- models/issue-391.mtl | 4 ++++ models/issue-391.obj | 9 +++++++++ tests/tester.cc | 37 +++++++++++++++++++++++++++++++++++++ tiny_obj_loader.h | 1 + 4 files changed, 51 insertions(+) create mode 100644 models/issue-391.mtl create mode 100644 models/issue-391.obj diff --git a/models/issue-391.mtl b/models/issue-391.mtl new file mode 100644 index 00000000..c23ced4b --- /dev/null +++ b/models/issue-391.mtl @@ -0,0 +1,4 @@ +newmtl has_kd +Kd 1 0 0 +newmtl has_map +map_Kd test.png \ No newline at end of file diff --git a/models/issue-391.obj b/models/issue-391.obj new file mode 100644 index 00000000..06d8774b --- /dev/null +++ b/models/issue-391.obj @@ -0,0 +1,9 @@ +mtllib issue-391.mtl +v 0 0 0 +v 1 0 0 +v 0 1 0 +vn 0 0 1 +usemtl has_map +f 1//1 2//1 3//1 +usemtl has_kd +f 1//1 2//1 3//1 \ No newline at end of file diff --git a/tests/tester.cc b/tests/tester.cc index 780d9755..41d02ed2 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -1433,6 +1433,41 @@ void test_comment_issue389() { TEST_CHECK(true == ret); } +void test_default_kd_for_multiple_materials_issue391() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + + std::string warn; + std::string err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + "../models/issue-391.obj", gMtlBasePath); + if (!warn.empty()) { + std::cout << "WARN: " << warn << std::endl; + } + + if (!err.empty()) { + std::cerr << "ERR: " << err << std::endl; + } + + const tinyobj::real_t kGrey[] = {0.6, 0.6, 0.6}; + const tinyobj::real_t kRed[] = {1.0, 0.0, 0.0}; + + TEST_CHECK(true == ret); + TEST_CHECK(2 == materials.size()); + for (size_t i = 0; i < materials.size(); ++i) { + const tinyobj::material_t& material = materials[i]; + if (material.name == "has_map") { + for (int i = 0; i < 3; ++i) TEST_CHECK(material.diffuse[i] == kGrey[i]); + } else if (material.name == "has_kd") { + for (int i = 0; i < 3; ++i) TEST_CHECK(material.diffuse[i] == kRed[i]); + } else { + std::cerr << "Unexpected material found!" << std::endl; + TEST_CHECK(false); + } + } +} + // Fuzzer test. // Just check if it does not crash. // Disable by default since Windows filesystem can't create filename of afl @@ -1542,4 +1577,6 @@ TEST_LIST = { test_invalid_relative_vertex_index}, {"test_invalid_texture_vertex_index", test_invalid_texture_vertex_index}, + {"default_kd_for_multiple_materials_issue391", + test_default_kd_for_multiple_materials_issue391}, {NULL, NULL}}; diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index 829ea6de..e5ea9470 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -2134,6 +2134,7 @@ void LoadMtl(std::map *material_map, has_d = false; has_tr = false; + has_kd = false; // set new mtl name token += 7; From a4e519b0a0f29c790464fcfeadfe25a7f9fa15ff Mon Sep 17 00:00:00 2001 From: Paul Bauriegel Date: Tue, 15 Jul 2025 19:00:37 +0200 Subject: [PATCH 08/22] Add remove Utf8 BOM (#394) --- models/cube_w_BOM.mtl | 24 +++++++++++++++++++ models/cube_w_BOM.obj | 32 +++++++++++++++++++++++++ tests/tester.cc | 54 +++++++++++++++++++++++++++++++++++++++++++ tiny_obj_loader.h | 17 ++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 models/cube_w_BOM.mtl create mode 100644 models/cube_w_BOM.obj diff --git a/models/cube_w_BOM.mtl b/models/cube_w_BOM.mtl new file mode 100644 index 00000000..96255b54 --- /dev/null +++ b/models/cube_w_BOM.mtl @@ -0,0 +1,24 @@ +newmtl white +Ka 0 0 0 +Kd 1 1 1 +Ks 0 0 0 + +newmtl red +Ka 0 0 0 +Kd 1 0 0 +Ks 0 0 0 + +newmtl green +Ka 0 0 0 +Kd 0 1 0 +Ks 0 0 0 + +newmtl blue +Ka 0 0 0 +Kd 0 0 1 +Ks 0 0 0 + +newmtl light +Ka 20 20 20 +Kd 1 1 1 +Ks 0 0 0 diff --git a/models/cube_w_BOM.obj b/models/cube_w_BOM.obj new file mode 100644 index 00000000..3c395f04 --- /dev/null +++ b/models/cube_w_BOM.obj @@ -0,0 +1,32 @@ +mtllib cube_w_BOM.mtl + +v 0.000000 2.000000 2.000000 +v 0.000000 0.000000 2.000000 +v 2.000000 0.000000 2.000000 +v 2.000000 2.000000 2.000000 +v 0.000000 2.000000 0.000000 +v 0.000000 0.000000 0.000000 +v 2.000000 0.000000 0.000000 +v 2.000000 2.000000 0.000000 +# 8 vertices + +g front cube +usemtl white +f 1 2 3 4 +# two white spaces between 'back' and 'cube' +g back cube +# expects white material +f 8 7 6 5 +g right cube +usemtl red +f 4 3 7 8 +g top cube +usemtl white +f 5 1 4 8 +g left cube +usemtl green +f 5 6 2 1 +g bottom cube +usemtl white +f 2 6 7 3 +# 6 elements diff --git a/tests/tester.cc b/tests/tester.cc index 41d02ed2..b5c4d0db 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -1465,9 +1465,61 @@ void test_default_kd_for_multiple_materials_issue391() { std::cerr << "Unexpected material found!" << std::endl; TEST_CHECK(false); } + } +} + +void test_removeUtf8Bom() { + // Basic input with BOM + std::string withBOM = "\xEF\xBB\xBFhello world"; + TEST_CHECK(tinyobj::removeUtf8Bom(withBOM) == "hello world"); + + // Input without BOM + std::string noBOM = "hello world"; + TEST_CHECK(tinyobj::removeUtf8Bom(noBOM) == "hello world"); + + // Leaves short string unchanged + std::string shortStr = "\xEF"; + TEST_CHECK(tinyobj::removeUtf8Bom(shortStr) == shortStr); + + std::string shortStr2 = "\xEF\xBB"; + TEST_CHECK(tinyobj::removeUtf8Bom(shortStr2) == shortStr2); + + // BOM only returns empty string + std::string justBom = "\xEF\xBB\xBF"; + TEST_CHECK(tinyobj::removeUtf8Bom(justBom) == ""); + + // Empty string + std::string emptyStr = ""; + TEST_CHECK(tinyobj::removeUtf8Bom(emptyStr) == ""); +} + +void test_loadObj_with_BOM() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + + std::string warn; + std::string err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + "../models/cube_w_BOM.obj", gMtlBasePath); + + if (!warn.empty()) { + std::cout << "WARN: " << warn << std::endl; + } + + if (!err.empty()) { + std::cerr << "ERR: " << err << std::endl; } + + TEST_CHECK(true == ret); + TEST_CHECK(6 == shapes.size()); + TEST_CHECK(0 == shapes[0].name.compare("front cube")); + TEST_CHECK(0 == shapes[1].name.compare("back cube")); // multiple whitespaces + // are aggregated as + // single white space. } + // Fuzzer test. // Just check if it does not crash. // Disable by default since Windows filesystem can't create filename of afl @@ -1579,4 +1631,6 @@ TEST_LIST = { test_invalid_texture_vertex_index}, {"default_kd_for_multiple_materials_issue391", test_default_kd_for_multiple_materials_issue391}, + {"test_removeUtf8Bom", test_removeUtf8Bom}, + {"test_loadObj_with_BOM", test_loadObj_with_BOM}, {NULL, NULL}}; diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index e5ea9470..a927864e 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -810,6 +810,17 @@ static inline std::string toString(const T &t) { return ss.str(); } +static inline std::string removeUtf8Bom(const std::string& input) { + // UTF-8 BOM = 0xEF,0xBB,0xBF + if (input.size() >= 3 && + static_cast(input[0]) == 0xEF && + static_cast(input[1]) == 0xBB && + static_cast(input[2]) == 0xBF) { + return input.substr(3); // Skip BOM + } + return input; +} + struct warning_context { std::string *warn; size_t line_number; @@ -2110,6 +2121,9 @@ void LoadMtl(std::map *material_map, if (linebuf.empty()) { continue; } + if (line_no == 1) { + linebuf = removeUtf8Bom(linebuf); + } // Skip leading space. const char *token = linebuf.c_str(); @@ -2637,6 +2651,9 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, if (linebuf.empty()) { continue; } + if (line_num == 1) { + linebuf = removeUtf8Bom(linebuf); + } // Skip leading space. const char *token = linebuf.c_str(); From 1dc289762ee26ce1020cad5bcdb37c88bb1efe28 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Tue, 16 Dec 2025 06:35:29 +0900 Subject: [PATCH 09/22] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8569a352..58a7f5e4 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ TinyObjLoader is successfully used in ... * Supernova Engine - 2D and 3D projects with Lua or C++ in data oriented design: https://github.com/supernovaengine/supernova * AGE (Arc Game Engine) - An open-source engine for building 2D & 3D real-time rendering and interactive contents: https://github.com/MohitSethi99/ArcGameEngine * [Wicked Engine](https://github.com/turanszkij/WickedEngine) - 3D engine with modern graphics -* Your project here! (Letting us know via github issue is welcome!) +* Your project here! (Plese send PR) ### Old version(v0.9.x) From d56555b026c1c7cec0f93f3ec7f1de2ff005c5ad Mon Sep 17 00:00:00 2001 From: DrElliot <133075253+MrDrElliot@users.noreply.github.com> Date: Sat, 20 Dec 2025 10:05:18 -0600 Subject: [PATCH 10/22] Update README.md (#398) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 58a7f5e4..8bfcdc2b 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,8 @@ TinyObjLoader is successfully used in ... * metal-ray-tracer - Writing ray-tracer using Metal Performance Shaders https://github.com/sergeyreznik/metal-ray-tracer https://sergeyreznik.github.io/metal-ray-tracer/index.html * Supernova Engine - 2D and 3D projects with Lua or C++ in data oriented design: https://github.com/supernovaengine/supernova * AGE (Arc Game Engine) - An open-source engine for building 2D & 3D real-time rendering and interactive contents: https://github.com/MohitSethi99/ArcGameEngine -* [Wicked Engine](https://github.com/turanszkij/WickedEngine) - 3D engine with modern graphics +* [Wicked Engine](https://github.com/turanszkij/WickedEngine) - 3D engine with modern graphics +* [Lumina Game Engine](https://github.com/MrDrElliot/LuminaEngine) - A modern, high-performance game engine built with Vulkan * Your project here! (Plese send PR) ### Old version(v0.9.x) From 8a3f8b92d18309e00911bf5cf5e465cfe4eaa1b1 Mon Sep 17 00:00:00 2001 From: Syoyo Fujita Date: Sat, 14 Feb 2026 07:49:31 +0900 Subject: [PATCH 11/22] close inactive issues. --- .github/workflows/cron.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/cron.yml diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml new file mode 100644 index 00000000..77d39d3c --- /dev/null +++ b/.github/workflows/cron.yml @@ -0,0 +1,22 @@ +name: Close inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 14 + days-before-issue-close: 14 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 14 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} From afdd3fa785ac556d12e2e0d99f3bbf6239ab5f31 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Mon, 23 Feb 2026 16:15:52 -0500 Subject: [PATCH 12/22] Test Python + NumPy in CI (#405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test tasks to GitHub Actions which test the generated wheels using different Python and NumPy versions. I've added four tests for issues #400 and they are all failing in the way I expect – surfacing disparities between `num_face_vertices` and `numpy_num_face_vertices()`. I've also added tests for issue #401 which is passing, and #402 is also covered in the #400 tests so those will need further investigation at a later time. Closes #403 --- .github/workflows/python.yml | 203 ++++++++++++ .github/workflows/wheels.yml | 115 ------- azure-pipelines.yml | 149 --------- fuzzer/runner.py | 3 +- pyproject.toml | 11 + setup.py | 31 +- tests/python/README.md | 12 + tests/python/pyproject.toml | 12 + tests/python/tinyobjloader_tests/__init__.py | 0 tests/python/tinyobjloader_tests/loader.py | 33 ++ .../python/tinyobjloader_tests/test_loader.py | 176 ++++++++++ tests/python/uv.lock | 303 ++++++++++++++++++ 12 files changed, 766 insertions(+), 282 deletions(-) create mode 100644 .github/workflows/python.yml delete mode 100644 .github/workflows/wheels.yml create mode 100644 tests/python/README.md create mode 100644 tests/python/pyproject.toml create mode 100644 tests/python/tinyobjloader_tests/__init__.py create mode 100644 tests/python/tinyobjloader_tests/loader.py create mode 100644 tests/python/tinyobjloader_tests/test_loader.py create mode 100644 tests/python/uv.lock diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 00000000..4dbf2b18 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,203 @@ +name: Python + +# Build on every branch push, tag push, and pull request change: +on: [push, pull_request] + +jobs: + check_format: + name: Check Python code format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python and install dependencies + working-directory: tests/python + run: uv sync --project . --python 3.13 + + - name: Check code format + working-directory: tests/python + run: uv run --project . black --check ../.. + + build_wheels_quick: + name: Build wheels for quick testing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true # Optional, use if you use setuptools_scm + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_ARCHS_LINUX: "x86_64" + # We do not need to support Python 3.6–3.8, and we only need + # manylinux for testing. PyPy isn't useful as this is a binary + # extension. + CIBW_SKIP: pp* cp36-* cp37-* cp38-* *-musllinux_* + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-quick + path: ./wheelhouse/*.whl + + test_wheels: + name: Test wheels with Python ${{ matrix.python-version }} and NumPy ${{ matrix.numpy-version }} + needs: [build_wheels_quick] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.9" + numpy-version: "1.25.2" + - python-version: "3.10" + numpy-version: "1.26.4" + - python-version: "3.11" + numpy-version: "1.26.4" + - python-version: "3.12" + numpy-version: "1.26.4" + - python-version: "3.11" + numpy-version: "2.4.2" + - python-version: "3.12" + numpy-version: "2.4.2" + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Download wheel artifacts + uses: actions/download-artifact@v4 + with: + pattern: cibw-wheels-quick + path: dist + merge-multiple: true + + - name: Set up Python ${{ matrix.python-version }} and install dependencies + working-directory: tests/python + run: uv sync --project . --python ${{ matrix.python-version }} + + - name: Install NumPy ${{ matrix.numpy-version }} + working-directory: tests/python + run: | + uv pip install --project . --only-binary :all: numpy==${{ matrix.numpy-version }} + + - name: Install manylinux wheel built for Python ${{ matrix.python-version }} + working-directory: tests/python + run: uv pip install --project . ../../dist/*cp$(echo ${{ matrix.python-version }} | tr -d .)*.whl + + - name: Run tests + working-directory: tests/python + run: uv run --project . pytest + + build_wheels_main: + name: Build remaining wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true # Optional, use if you use setuptools_scm + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_ARCHS_MACOS: "x86_64 universal2 arm64" + CIBW_ARCHS_WINDOWS: "AMD64 x86" + # disable aarm64 build since its too slow to build(docker + qemu) + CIBW_ARCHS_LINUX: "x86_64 i686" + # We do not need to support Python 3.6–3.8, and the quick build + # has already taken care of manylinux. PyPy isn't useful as this is + # a binary extension. + CIBW_SKIP: pp* cp36-* cp37-* cp38-* *-manylinux_x86_64 + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-main-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl + + # It looks cibuildwheels did not clean build folder(CMake), and it results to Windows arm64 build failure(trying to reuse x86 build of .obj) + # So supply separated build job for Windows ARM64 build + # TODO: clean build folder using CIBW_BEFORE_ALL? + build_wheels_win_arm64: + name: Build ARM64 wheels on Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true # Optional, use if you use setuptools_scm + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_ARCHS_WINDOWS: "ARM64" + CIBW_SKIP: pp* + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-win-arm64 + path: ./wheelhouse/*.whl + + make_sdist: + name: Make SDist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Optional, use if you use setuptools_scm + fetch-tags: true # Optional, use if you use setuptools_scm + + - name: Build SDist + run: pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist + path: dist/*.tar.gz + + upload_all: + needs: [build_wheels_quick, build_wheels_main, build_wheels_win_arm64, make_sdist] + runs-on: ubuntu-latest + environment: release + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + # upload to PyPI on every tag starting with 'v' + # NOTE: Without github.event_name & githug.ref check, `upload_all` task is still triggered on 'main' branch push. + # (then get 'Branch "main" is not allowed to deploy to release due to environment protection rules.' error) + # So still do event_name and github.ref check. + # TODO: Make it work only using Github `environment` feature. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + # alternatively, to publish when a GitHub Release is created, use the following rule: + # if: github.event_name == 'push' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v4 + with: + pattern: cibw-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + # Use Trusted Publisher feature: + # https://docs.pypi.org/trusted-publishers/ + # so no use of PYPI_API_TOKEN + #password: ${{ secrets.PYPI_API_TOKEN }} + # + # Avoid race condition when using multiple CIs + skip-existing: true + verbose: true diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml deleted file mode 100644 index b37104a2..00000000 --- a/.github/workflows/wheels.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Build and upload to PyPI - -# Build on every branch push, tag push, and pull request change: -on: [push, pull_request] - -jobs: - - build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true # Optional, use if you use setuptools_scm - - - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 - # to supply options, put them in 'env', like: - # env: - # CIBW_SOME_OPTION: value - # Disable building PyPy wheels on all platforms - env: - CIBW_ARCHS_MACOS: "x86_64 universal2 arm64" - CIBW_ARCHS_WINDOWS: "AMD64 x86" - # disable aarm64 build since its too slow to build(docker + qemu) - CIBW_ARCHS_LINUX: "x86_64 i686" - # it looks cibuildwheel fails to add version string to wheel file for python 3.6, so skip it - CIBW_SKIP: pp* - - - uses: actions/upload-artifact@v4 - with: - name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} - path: ./wheelhouse/*.whl - - # It looks cibuildwheels did not clean build folder(CMake), and it results to Windows arm64 build failure(trying to reuse x86 build of .obj) - # So supply separated build job for Windows ARM64 build - # TODO: clean build folder using CIBW_BEFORE_ALL? - build_win_arm64_wheels: - name: Build ARM64 wheels on Windows. - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true # Optional, use if you use setuptools_scm - - - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 - # to supply options, put them in 'env', like: - # env: - # CIBW_SOME_OPTION: value - # Disable building PyPy wheels on all platforms - env: - CIBW_ARCHS_WINDOWS: "ARM64" - CIBW_SKIP: pp* - - - uses: actions/upload-artifact@v4 - with: - name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} - path: ./wheelhouse/*.whl - - make_sdist: - name: Make SDist - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Optional, use if you use setuptools_scm - fetch-tags: true # Optional, use if you use setuptools_scm - - - name: Build SDist - run: pipx run build --sdist - - - uses: actions/upload-artifact@v4 - with: - name: cibw-sdist - path: dist/*.tar.gz - - upload_all: - needs: [build_wheels, build_wheels, make_sdist] - runs-on: ubuntu-latest - environment: release - permissions: - # IMPORTANT: this permission is mandatory for trusted publishing - id-token: write - # upload to PyPI on every tag starting with 'v' - # NOTE: Without github.event_name & githug.ref check, `upload_all` task is still triggered on 'main' branch push. - # (then get 'Branch "main" is not allowed to deploy to release due to environment protection rules.' error) - # So still do event_name and github.ref check. - # TODO: Make it work only using Github `environment` feature. - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') - # alternatively, to publish when a GitHub Release is created, use the following rule: - # if: github.event_name == 'push' && github.event.action == 'published' - steps: - - uses: actions/download-artifact@v4 - with: - pattern: cibw-* - path: dist - merge-multiple: true - - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - # Use Trusted Publisher feature: - # https://docs.pypi.org/trusted-publishers/ - # so no use of PYPI_API_TOKEN - #password: ${{ secrets.PYPI_API_TOKEN }} - # - # Avoid race condition when using multiple CIs - skip-existing: true - verbose: true diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2580f172..dbc3e166 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,25 +1,3 @@ -# -# Python wheels build is now done in Github Actions + Cirrus CI(for arm build) -# so python build is disabled in Azure pipelines. -# - -variables: - # https://cibuildwheel.readthedocs.io/en/stable/cpp_standards/ - # cibuildwheel now supports python 3.6+(as of 2022 Oct) - #CIBW_SKIP: "pp*" - CIBW_BEFORE_BUILD: "pip install pybind11" - CIBW_ARCHS_LINUXBEFORE_BUILD: "pip install pybind11" - # disable aarch64 build for a while since it(pulling docker aarch64 image) exceeds Azure's 60 min limit - # NOTE: aarch64 linux support in Azure pipeline is not yet officially supported(as of 2022 Oct) https://github.com/microsoft/azure-pipelines-agent/issues/3935 - #CIBW_ARCHS_LINUX: auto aarch64 - CIBW_ARCHS_MACOS: x86_64 universal2 arm64 - #CIBW_BEFORE_BUILD_MACOS: "pip install -U pip setuptools" - #CIBW_BEFORE_BUILD_LINUX: "pip install -U pip setuptools" - #CIBW_TEST_COMMAND: TODO "python -c \"import tinyobjloader; tinyobjloader.test()\"" - CIBW_BUILD_VERBOSITY: "2" - #CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 - #CIBW_MANYLINUX_I686_IMAGE: manylinux2014 - jobs: - job: unit_linux pool: { vmImage: "ubuntu-latest" } @@ -29,133 +7,6 @@ jobs: make && ./tester displayName: Run unit tests - - job: python_format - pool: { vmImage: "ubuntu-latest" } - steps: - - task: UsePythonVersion@0 - - script: | - # 19.10b0 triggers 'cannot import name '_unicodefun' from 'click'' error. - # https://stackoverflow.com/questions/71673404/importerror-cannot-import-name-unicodefun-from-click - #pip install black==19.10b0 - #pip install black==22.3.0 - pip install black==22.10.0 - - black --check python/ - displayName: Check Python code format - - # Disabled: python build - ## - ## Ubuntu16.04 seems now deprecated(as of 2021/12/01), - ## so use `ubuntu-latest` - #- job: linux - # pool: {vmImage: "ubuntu-latest"} - # steps: - # - task: UsePythonVersion@0 - # - bash: | - # python3 -m pip install --upgrade pip - # pip3 install cibuildwheel twine - - # # Use pipx to build source dist - # pip3 install pipx - - # # Source dist - # pipx run build --sdist - # ls -la dist/* - - # # build binary wheels - # cibuildwheel --platform linux --output-dir wheelhouse . - - # - task: CopyFiles@2 - # inputs: - # contents: 'wheelhouse/**' - # targetFolder: $(Build.ArtifactStagingDirectory) - - # - task: CopyFiles@2 - # inputs: - # contents: 'dist/**' - # targetFolder: $(Build.ArtifactStagingDirectory) - - # - task: PublishBuildArtifacts@1 - # inputs: - # path: $(Build.ArtifactStagingDirectory) - # artifactName: tinyobjDeployLinux - - #- job: macos - # pool: {vmImage: 'macOS-latest'} - # variables: - # # Support C++11: https://github.com/joerick/cibuildwheel/pull/156 - # MACOSX_DEPLOYMENT_TARGET: 10.9 - # steps: - # - task: UsePythonVersion@0 - # - bash: | - # python3 -m pip install --upgrade pip - # pip3 install cibuildwheel - # cibuildwheel --platform macos --output-dir wheelhouse . - # - task: CopyFiles@2 - # inputs: - # contents: 'wheelhouse/*.whl' - # targetFolder: $(Build.ArtifactStagingDirectory) - # - task: PublishBuildArtifacts@1 - # inputs: - # path: $(Build.ArtifactStagingDirectory) - # artifactName: tinyobjDeployMacOS - - #- job: windows - # pool: {vmImage: 'windows-latest'} - # steps: - # - task: UsePythonVersion@0 - # - bash: | - # python -m pip install --upgrade pip - # pip install cibuildwheel - # cibuildwheel --platform windows --output-dir wheelhouse . - # - task: CopyFiles@2 - # inputs: - # contents: 'wheelhouse/*.whl' - # targetFolder: $(Build.ArtifactStagingDirectory) - # - task: PublishBuildArtifacts@1 - # inputs: - # path: $(Build.ArtifactStagingDirectory) - # artifactName: tinyobjDeployWindows - - #- job: deployPyPI - # # Based on vispy: https://github.com/vispy/vispy/blob/master/azure-pipelines.yml - # pool: {vmImage: 'ubuntu-latest'} - # condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) - # dependsOn: - # - linux - # - macos - # - windows - # steps: - # - task: UsePythonVersion@0 - - # # TODO(syoyo): Use buildType: specific to download multiple artifacts at once? - # - task: DownloadBuildArtifacts@0 - # inputs: - # artifactName: 'tinyobjDeployLinux' - # downloadPath: $(Pipeline.Workspace) - - # - task: DownloadBuildArtifacts@0 - # inputs: - # artifactName: 'tinyobjDeployMacOS' - # downloadPath: $(Pipeline.Workspace) - - # - task: DownloadBuildArtifacts@0 - # inputs: - # artifactName: 'tinyobjDeployWindows' - # downloadPath: $(Pipeline.Workspace) - - # # Publish to PyPI through twine - # - bash: | - # cd $(Pipeline.Workspace) - # find . - # python -m pip install --upgrade pip - # pip install twine - # echo tinyobjDeployLinux/dist/* - # echo tinyobjDeployLinux/wheelhouse/* tinyobjDeployMacOS/wheelhouse/* tinyobjDeployWindows/wheelhouse/* - # twine upload -u "__token__" --skip-existing tinyobjDeployLinux/dist/* tinyobjDeployLinux/wheelhouse/* tinyobjDeployMacOS/wheelhouse/* tinyobjDeployWindows/wheelhouse/* - # env: - # TWINE_PASSWORD: $(pypiToken2) - trigger: branches: include: diff --git a/fuzzer/runner.py b/fuzzer/runner.py index 0c06d4ba..a647d3ce 100644 --- a/fuzzer/runner.py +++ b/fuzzer/runner.py @@ -2,10 +2,11 @@ import glob import subprocess + def main(): for g in glob.glob("../tests/afl/id*"): print(g) - + cmd = ["../a.out", g] proc = subprocess.Popen(cmd) diff --git a/pyproject.toml b/pyproject.toml index d3ba7cf3..4f5cc706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,17 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 140 +force-exclude = ''' +( + /deps/.*$ + | /kuroga.py$ + | /config-msvc.py$ + | /config-posix.py$ + | ^python/build/.*$ + | ^python/dist/.*$ + | ^python/tinyobjloader.egg-info/.*$ +) +''' [project] name = "tinyobjloader" diff --git a/setup.py b/setup.py index cb950871..7b4daedd 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ # Adapted from https://github.com/pybind/python_example/blob/master/setup.py import sys -#from pybind11 import get_cmake_dir +# from pybind11 import get_cmake_dir # Available at setup time due to pyproject.toml -from pybind11.setup_helpers import Pybind11Extension#, build_ext +from pybind11.setup_helpers import Pybind11Extension # , build_ext from setuptools import setup try: @@ -12,9 +12,6 @@ except: __version__ = "2.0.0rc10" -with open("README.md", "r", encoding="utf8") as fh: - long_description = fh.read() - # The main interface is through Pybind11Extension. # * You can add cxx_std=11/14/17, and then build_ext can be removed. # * You can set include_pybind11=false to add the include directory yourself, @@ -25,27 +22,27 @@ # reproducible builds (https://github.com/pybind/python_example/pull/53) ext_modules = [ - Pybind11Extension("tinyobjloader", + Pybind11Extension( + "tinyobjloader", sorted(["python/bindings.cc", "python/tiny_obj_loader.cc"]), # Example: passing in the version to the compiled code - define_macros = [('VERSION_INFO', __version__)], + define_macros=[("VERSION_INFO", __version__)], cxx_std=11, - ), + ), ] setup( name="tinyobjloader", - packages=['python'], - #version=__version__, + packages=["python"], + # version=__version__, author="Syoyo Fujita", author_email="syoyo@lighttransport.com", url="iframe.php?url=https%3A%2F%2Fgithub.com%2Ftinyobjloader%2Ftinyobjloader", - #project_urls={ + # project_urls={ # "Issue Tracker": "https://github.com/tinyobjloader/tinyobjloader/issues", - #}, + # }, description="Tiny but powerful Wavefront OBJ loader", - long_description=long_description, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -60,10 +57,10 @@ "Programming Language :: Python :: 3", ], ext_modules=ext_modules, - #extras_require={"test": "pytest"}, + # extras_require={"test": "pytest"}, # Currently, build_ext only provides an optional "highest supported C++ # level" feature, but in the future it may provide more features. # cmdclass={"build_ext": build_ext}, - #zip_safe=False, - #python_requires=">=3.6", + # zip_safe=False, + # python_requires=">=3.6", ) diff --git a/tests/python/README.md b/tests/python/README.md new file mode 100644 index 00000000..543a238d --- /dev/null +++ b/tests/python/README.md @@ -0,0 +1,12 @@ +# tinyobjloader Python tests + +This folder hosts a project for running the Python binding tests. + +## Development + +The tests require NumPy. To optimize CI install times, the uv.lock excludes +NumPy, as for some Python versions, pinning a version would result in builds +from source which are then discarded. To run the tests locally, after running +`uv sync`, install into the venv a version of NumPy from the build matrix in +`.github/workflows/python.yml`. + diff --git a/tests/python/pyproject.toml b/tests/python/pyproject.toml new file mode 100644 index 00000000..8157856b --- /dev/null +++ b/tests/python/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "tinyobjloader-tests" +version = "0.0.1" +description = "Tests for tinyobjloader Python bindings" +readme = "README.md" +requires-python = ">=3.9" + +dependencies = ["pytest>=8.0", "black==22.10.0"] + +[build-system] +requires = ["hatchling>=1.24"] +build-backend = "hatchling.build" diff --git a/tests/python/tinyobjloader_tests/__init__.py b/tests/python/tinyobjloader_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/python/tinyobjloader_tests/loader.py b/tests/python/tinyobjloader_tests/loader.py new file mode 100644 index 00000000..9d7b3dad --- /dev/null +++ b/tests/python/tinyobjloader_tests/loader.py @@ -0,0 +1,33 @@ +from tinyobjloader import ObjReader, ObjReaderConfig + + +class LoadException(Exception): + pass + + +class Loader: + """ + A light wrapper around ObjReader to provide a convenient interface for testing. + """ + + def __init__(self, triangulate): + self.reader = ObjReader() + config = ObjReaderConfig() + config.triangulate = triangulate + self.config = config + + def load(self, mesh_path): + if not self.reader.ParseFromFile(mesh_path, self.config): + raise LoadException(self.reader.Error() or self.reader.Warning()) + + def loads(self, mesh_string): + if not self.reader.ParseFromString(mesh_string, "", self.config): + raise LoadException(self.reader.Error() or self.reader.Warning()) + + @property + def shapes(self): + return self.reader.GetShapes() + + @property + def attrib(self): + return self.reader.GetAttrib() diff --git a/tests/python/tinyobjloader_tests/test_loader.py b/tests/python/tinyobjloader_tests/test_loader.py new file mode 100644 index 00000000..8d7b8f50 --- /dev/null +++ b/tests/python/tinyobjloader_tests/test_loader.py @@ -0,0 +1,176 @@ +import numpy as np + +from .loader import Loader, LoadException + +TWO_QUADS = """ +v 0 0 0 +v 0 0 0 +v 0 0 0 +v 0 0 0 +f 1 2 3 4 +v 46.367584 82.676086 8.867414 +v 46.524185 82.81955 8.825487 +v 46.59864 83.086678 8.88121 +v 46.461926 82.834091 8.953863 +f 5 6 7 8 +""" + +MIXED_ARITY = """ +v 0 1 1 +v 0 2 2 +v 0 3 3 +v 0 4 4 +v 0 5 5 +f 1 2 3 4 +f 1 4 5 +""" + + +def test_numpy_face_vertices_two_quads(): + """ + Test for https://github.com/tinyobjloader/tinyobjloader/issues/400 + """ + + # Set up. + loader = Loader(triangulate=False) + loader.loads(TWO_QUADS) + + shapes = loader.shapes + assert len(shapes) == 1 + + # Confidence check. + (shape,) = shapes + expected_num_face_vertices = [4, 4] + assert shape.mesh.num_face_vertices == expected_num_face_vertices + + # Test. + np.testing.assert_array_equal(shape.mesh.numpy_num_face_vertices(), expected_num_face_vertices) + + +def test_numpy_face_vertices_two_quads_with_triangulate(): + """ + Test for https://github.com/tinyobjloader/tinyobjloader/issues/400 + """ + + # Set up. + loader = Loader(triangulate=True) + loader.loads(TWO_QUADS) + + shapes = loader.shapes + assert len(shapes) == 1 + + # Confidence check. + (shape,) = shapes + expected_num_face_vertices = [3, 3, 3, 3] + assert shape.mesh.num_face_vertices == expected_num_face_vertices + + # Test. + np.testing.assert_array_equal(shape.mesh.numpy_num_face_vertices(), expected_num_face_vertices) + + +def test_numpy_face_vertices_mixed_arity(): + """ + Test for: + - https://github.com/tinyobjloader/tinyobjloader/issues/400 + - https://github.com/tinyobjloader/tinyobjloader/issues/402 + """ + + # Set up. + loader = Loader(triangulate=False) + loader.loads(MIXED_ARITY) + + shapes = loader.shapes + assert len(shapes) == 1 + + # Confidence check. + (shape,) = shapes + expected_num_face_vertices = [4, 3] + assert shape.mesh.num_face_vertices == expected_num_face_vertices + + # Test. + np.testing.assert_array_equal(shape.mesh.numpy_num_face_vertices(), expected_num_face_vertices) + + +def test_numpy_face_vertices_mixed_arity_with_triangulate(): + """ + Test for https://github.com/tinyobjloader/tinyobjloader/issues/400 + """ + + # Set up. + loader = Loader(triangulate=True) + loader.loads(MIXED_ARITY) + + shapes = loader.shapes + assert len(shapes) == 1 + + # Confidence check. + (shape,) = shapes + expected_num_face_vertices = [3, 3, 3] + assert shape.mesh.num_face_vertices == expected_num_face_vertices + + # Test. + np.testing.assert_array_equal(shape.mesh.numpy_num_face_vertices(), expected_num_face_vertices) + + +def test_numpy_index_array_two_quads(): + """ + Test for https://github.com/tinyobjloader/tinyobjloader/issues/401 + """ + + # Set up. + loader = Loader(triangulate=False) + loader.loads(TWO_QUADS) + + shapes = loader.shapes + assert len(shapes) == 1 + + # Confidence check. + (shape,) = shapes + expected_vertex_index = [0, 1, 2, 3, 4, 5, 6, 7] + assert [x.vertex_index for x in shape.mesh.indices] == expected_vertex_index + + # Test. + expected_numpy_indices = [0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1, -1, 5, -1, -1, 6, -1, -1, 7, -1, -1] + np.testing.assert_array_equal(shape.mesh.numpy_indices(), expected_numpy_indices) + + +def test_numpy_vertex_array_two_quads(): + """ + Test for https://github.com/tinyobjloader/tinyobjloader/issues/401 + """ + + # Set up. + loader = Loader(triangulate=False) + loader.loads(TWO_QUADS) + + # Confidence check. + expected_vertices = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 46.367584, + 82.676086, + 8.867414, + 46.524185, + 82.81955, + 8.825487, + 46.59864, + 83.086678, + 8.88121, + 46.461926, + 82.834091, + 8.953863, + ] + np.testing.assert_array_almost_equal(loader.attrib.vertices, expected_vertices, decimal=6) + + # Test. + np.testing.assert_array_almost_equal(loader.attrib.numpy_vertices(), expected_vertices, decimal=6) diff --git a/tests/python/uv.lock b/tests/python/uv.lock new file mode 100644 index 00000000..98022816 --- /dev/null +++ b/tests/python/uv.lock @@ -0,0 +1,303 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "black" +version = "22.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/89/629fca2eea0899c06befaa58dc0f49d56807d454202bb2e54bd0d98c77f3/black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1", size = 547735, upload-time = "2022-10-06T22:44:48.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/49/ea03c318a25be359b8e5178a359d47e2da8f7524e1522c74b8f74c66b6f8/black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa", size = 1413786, upload-time = "2022-10-07T18:06:56.738Z" }, + { url = "https://files.pythonhosted.org/packages/a6/84/5c3f3ffc4143fa7e208d745d2239d915e74d3709fdbc64c3e98d3fd27e56/black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef", size = 1395367, upload-time = "2022-10-07T18:07:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/23/f4278377cabf882298b4766e977fd04377f288d1ccef706953076a1e0598/black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4", size = 1412948, upload-time = "2022-10-07T18:06:45.929Z" }, + { url = "https://files.pythonhosted.org/packages/2c/11/f2737cd3b458d91401801e83a014e87c63e8904dc063200f77826c352f54/black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb", size = 1248864, upload-time = "2022-10-07T18:34:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5f/9cfc6dd95965f8df30194472543e6f0515a10d78ea5378426ef1546735c7/black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7", size = 1542985, upload-time = "2022-10-06T22:54:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ce/22281871536b3d79474fd44d48dad48f7cbc5c3982bddf6a7495e7079d00/black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66", size = 1198188, upload-time = "2022-10-06T22:58:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2f/a8406a9e337a213802aa90a3e9fbf90c86f3edce92f527255fd381309b77/black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae", size = 1233231, upload-time = "2022-10-07T18:35:05.895Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9e/fa912c5ae4b8eb6d36982fc8ac2d779cf944dbd7c3c1fe7a28acf462c1ed/black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b", size = 1527386, upload-time = "2022-10-06T22:54:25.636Z" }, + { url = "https://files.pythonhosted.org/packages/56/df/913d71817c7034edba25d596c54f782c2f809b6af30367d2f00309e8890a/black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d", size = 1201344, upload-time = "2022-10-06T22:58:58.134Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/903cdf41514088d5a716538cb189c471ab34e56ae9a1c2da6b8bfe8e4dbf/black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0", size = 1248291, upload-time = "2022-10-07T18:34:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/b9/51/403b0b0eb9fb412ca02b79dc38472469f2f88c9aacc6bb5262143e4ff0bc/black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383", size = 1542631, upload-time = "2022-10-06T22:54:31.965Z" }, + { url = "https://files.pythonhosted.org/packages/ab/15/61119d166a44699827c112d7c4726421f14323c2cb7aa9f4c26628f237f9/black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de", size = 1197402, upload-time = "2022-10-06T22:59:03.766Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6f/74492b8852ee4f2ad2178178f6b65bc8fc80ad539abe56c1c23eab6732e2/black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458", size = 165761, upload-time = "2022-10-06T22:44:46.108Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "tinyobjloader-tests" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "black" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", specifier = "==22.10.0" }, + { name = "pytest", specifier = ">=8.0" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From a285d14032202d07a210f98176fe4e9021b39c19 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Mon, 2 Mar 2026 12:17:14 -0500 Subject: [PATCH 13/22] Readme cleanup + remove obsolete config (#409) - Remove obsolete Cirrus + Travis CI configs - Remove obsolete Coveralls badge - Add lacecore to projects - Update some obsolete readme info about CI --- .cirrus.yml | 30 ---------------------------- .travis.yml | 56 ----------------------------------------------------- README.md | 10 ++++------ 3 files changed, 4 insertions(+), 92 deletions(-) delete mode 100644 .cirrus.yml delete mode 100644 .travis.yml diff --git a/.cirrus.yml b/.cirrus.yml deleted file mode 100644 index 0aff9456..00000000 --- a/.cirrus.yml +++ /dev/null @@ -1,30 +0,0 @@ -build_and_store_wheels: &BUILD_AND_STORE_WHEELS - install_cibuildwheel_script: - - python -m pip install cibuildwheel==2.16.2 - run_cibuildwheel_script: - - cibuildwheel - wheels_artifacts: - path: "wheelhouse/*" - - # Upload only for tagged commit - only_if: $CIRRUS_TAG != '' - publish_script: - - python -m pip install twine - - python -m twine upload --repository-url https://upload.pypi.org/legacy/ --username __token__ wheelhouse/*.whl - - -linux_aarch64_task: - name: Build Linux aarch64 wheels. - compute_engine_instance: - image_project: cirrus-images - image: family/docker-builder-arm64 - architecture: arm64 - platform: linux - cpu: 4 - memory: 4G - environment: - TWINE_PASSWORD: ENCRYPTED[ade2037764e68fea251152f7585f3f77cdd748af06dc0f06942c45a8a8770fff19032c985f8dc193229c8adb2c0fecb9] - - install_pre_requirements_script: - - apt install -y python3-venv python-is-python3 - <<: *BUILD_AND_STORE_WHEELS diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 12b67f28..00000000 --- a/.travis.yml +++ /dev/null @@ -1,56 +0,0 @@ -language: cpp -sudo: required -matrix: - include: - - addons: &1 - apt: - sources: - - george-edison55-precise-backports - - ubuntu-toolchain-r-test - - llvm-toolchain-precise-3.7 - packages: - - cmake - - cmake-data - - ninja-build - - g++-4.9 - - clang-3.7 - compiler: clang - env: DEPLOY_BUILD=1 COMPILER_VERSION=3.7 BUILD_TYPE=Debug - - addons: *1 - compiler: clang - env: COMPILER_VERSION=3.7 BUILD_TYPE=Release - - addons: &2 - apt: - sources: - - george-edison55-precise-backports - - ubuntu-toolchain-r-test - packages: - - cmake - - cmake-data - - ninja-build - - g++-4.9 - compiler: gcc - env: COMPILER_VERSION=4.9 BUILD_TYPE=Debug - - addons: *2 - compiler: gcc - env: COMPILER_VERSION=4.9 BUILD_TYPE=Release - - addons: *1 - compiler: clang - env: COMPILER_VERSION=3.7 BUILD_TYPE=Debug -before_install: -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade; fi -- if [ -n "$REPORT_COVERAGE" ]; then sudo apt-get update python; fi -- if [ -n "$REPORT_COVERAGE" ]; then sudo apt-get install python-dev libffi-dev libssl-dev; fi -- if [ -n "$REPORT_COVERAGE" ]; then sudo pip install --upgrade pip; fi -- if [ -n "$REPORT_COVERAGE" ]; then CXX=g++ pip install --user requests[security]; fi -- if [ -n "$REPORT_COVERAGE" ]; then CXX=g++ pip install --user cpp-coveralls; fi -script: -- cd tests -- make check -- if [ -n "$REPORT_COVERAGE" ]; then coveralls -b . -r .. -e examples -e tools -e - jni -e python -e images -E ".*CompilerId.*" -E ".*feature_tests.*" ; fi -- cd .. -- rm -rf dist -- mkdir dist -- cp tiny_obj_loader.h dist/ - diff --git a/README.md b/README.md index 8bfcdc2b..3cb8d9ac 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/m6wfkvket7gth8wn/branch/master?svg=true)](https://ci.appveyor.com/project/syoyo/tinyobjloader-6e4qf/branch/master) -[![Coverage Status](https://coveralls.io/repos/github/syoyo/tinyobjloader/badge.svg?branch=master)](https://coveralls.io/github/syoyo/tinyobjloader?branch=master) - [![AUR version](https://img.shields.io/aur/version/tinyobjloader?logo=arch-linux)](https://aur.archlinux.org/packages/tinyobjloader) Tiny but powerful single file wavefront obj loader written in C++03. No dependency except for C++ STL. It can parse over 10M polygons with moderate memory and time. @@ -19,7 +17,7 @@ If you are looking for C99 version, please see https://github.com/syoyo/tinyobjl Version notice -------------- -We recommend to use `master`(`main`) branch. Its v2.0 release candidate. Most features are now nearly robust and stable(Remaining task for release v2.0 is polishing C++ and Python API, and fix built-in triangulation code). +We recommend using the `release` (main) branch. It contains the v2.0 release candidate. Most features are now nearly robust and stable. (The remaining task for release v2.0 is polishing C++ and Python API, and fix built-in triangulation code). We have released new version v1.0.0 on 20 Aug, 2016. Old version is available as `v0.9.x` branch https://github.com/syoyo/tinyobjloader/tree/v0.9.x @@ -76,6 +74,7 @@ TinyObjLoader is successfully used in ... * AGE (Arc Game Engine) - An open-source engine for building 2D & 3D real-time rendering and interactive contents: https://github.com/MohitSethi99/ArcGameEngine * [Wicked Engine](https://github.com/turanszkij/WickedEngine) - 3D engine with modern graphics * [Lumina Game Engine](https://github.com/MrDrElliot/LuminaEngine) - A modern, high-performance game engine built with Vulkan +* lacecore: Python polygonal mesh library optimized for cloud computation https://github.com/lace/lacecore * Your project here! (Plese send PR) ### Old version(v0.9.x) @@ -426,17 +425,16 @@ See [python/sample.py](python/sample.py) for example use of Python binding of ti ### CI + PyPI upload -cibuildwheels + twine upload for each git tagging event is handled in Github Actions and Cirrus CI(arm builds). +cibuildwheels + twine upload for each git tagging event is handled in Github Actions. #### How to bump version(For developer) -* Apply `black` to python files(`python/sample.py`) * Bump version in CMakeLists.txt * Commit and push `release`. Confirm C.I. build is OK. * Create tag starting with `v`(e.g. `v2.1.0`) * `git push --tags` * version settings is automatically handled in python binding through setuptools_scm. - * cibuildwheels + pypi upload(through twine) will be automatically triggered in Github Actions + Cirrus CI. + * cibuildwheels + pypi upload (through twine) will be automatically triggered in Github Actions. ## Tests From fcc90055f8f8b1f79146c41707a2cb432ae04cda Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Tue, 3 Mar 2026 12:52:39 -0500 Subject: [PATCH 14/22] Fix `numpy_num_face_vertices()` (#408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test Python + NumPy in CI * Add comments * Add more tests * Add more Python tests * assert_array_almost_equal * Try decimal=5 * Try to fix vertex test * Update a comment * Upgrade cibuildwheel * Fix toml * Match the types and call buf.release() * Rm buf.release() * Try to clear license warning on build * Fix license specifier * Move python tests out of python so they don’t end up in the wheel * Add some excludes * Reformat and exclude some python files * Update .github/workflows/python.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add a note about local numpy install * Reset a few files --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- python/bindings.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/bindings.cc b/python/bindings.cc index e7e6c951..d710b65f 100644 --- a/python/bindings.cc +++ b/python/bindings.cc @@ -149,9 +149,9 @@ PYBIND11_MODULE(tinyobjloader, tobj_module) .def(py::init<>()) .def_readonly("num_face_vertices", &mesh_t::num_face_vertices) .def("numpy_num_face_vertices", [] (mesh_t &instance) { - auto ret = py::array_t(instance.num_face_vertices.size()); + auto ret = py::array_t(instance.num_face_vertices.size()); py::buffer_info buf = ret.request(); - memcpy(buf.ptr, instance.num_face_vertices.data(), instance.num_face_vertices.size() * sizeof(unsigned char)); + memcpy(buf.ptr, instance.num_face_vertices.data(), instance.num_face_vertices.size() * sizeof(unsigned int)); return ret; }) .def("vertex_indices", [](mesh_t &self) { From ff228a9619ff9b298a1a79416e60b0b93010176b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:40:59 +0900 Subject: [PATCH 15/22] Derive numpy_num_face_vertices element type from member type instead of hardcoding unsigned int (#412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test Python + NumPy in CI * Add comments * Add more tests * Add more Python tests * assert_array_almost_equal * Try decimal=5 * Try to fix vertex test * Update a comment * Upgrade cibuildwheel * Fix toml * Match the types and call buf.release() * Rm buf.release() * Try to clear license warning on build * Fix license specifier * Move python tests out of python so they don’t end up in the wheel * Add some excludes * Reformat and exclude some python files * Update .github/workflows/python.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add a note about local numpy install * Reset a few files * Initial plan * Derive element type from num_face_vertices instead of hardcoding unsigned int Co-authored-by: paulmelnikow <1487036+paulmelnikow@users.noreply.github.com> * Remove accidentally committed build artifacts and update .gitignore Co-authored-by: paulmelnikow <1487036+paulmelnikow@users.noreply.github.com> --------- Co-authored-by: Paul Melnikow Co-authored-by: Paul Melnikow Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: paulmelnikow <1487036+paulmelnikow@users.noreply.github.com> Co-authored-by: Syoyo Fujita --- .gitignore | 5 +++++ python/bindings.cc | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f9b4d691..394552cc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ build/ /python/tiny_obj_loader.h /tests/tester /tests/tester.dSYM +/_codeql_build_dir/ +/_codeql_detected_source_root +/python/_version.py +/tinyobjloader.egg-info/ +**/__pycache__/ diff --git a/python/bindings.cc b/python/bindings.cc index d710b65f..a303e01f 100644 --- a/python/bindings.cc +++ b/python/bindings.cc @@ -149,9 +149,10 @@ PYBIND11_MODULE(tinyobjloader, tobj_module) .def(py::init<>()) .def_readonly("num_face_vertices", &mesh_t::num_face_vertices) .def("numpy_num_face_vertices", [] (mesh_t &instance) { - auto ret = py::array_t(instance.num_face_vertices.size()); + using T = typename std::remove_reference::type::value_type; + auto ret = py::array_t(instance.num_face_vertices.size()); py::buffer_info buf = ret.request(); - memcpy(buf.ptr, instance.num_face_vertices.data(), instance.num_face_vertices.size() * sizeof(unsigned int)); + memcpy(buf.ptr, instance.num_face_vertices.data(), instance.num_face_vertices.size() * sizeof(T)); return ret; }) .def("vertex_indices", [](mesh_t &self) { From d51d25014e8c7c471db6e262539e8ffb06945bd1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 04:33:08 +0900 Subject: [PATCH 16/22] Add regression test for `numpy_num_face_vertices()` fix (issue #400) (#418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test Python + NumPy in CI * Add comments * Add more tests * Add more Python tests * assert_array_almost_equal * Try decimal=5 * Try to fix vertex test * Update a comment * Upgrade cibuildwheel * Fix toml * Match the types and call buf.release() * Rm buf.release() * Try to clear license warning on build * Fix license specifier * Move python tests out of python so they don’t end up in the wheel * Add some excludes * Reformat and exclude some python files * Update .github/workflows/python.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add a note about local numpy install * Reset a few files * Initial plan * Add regression test and .obj file for issue-400 numpy_num_face_vertices() Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Fix black formatting with line-length=140 from pyproject.toml Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> --------- Co-authored-by: Paul Melnikow Co-authored-by: Paul Melnikow Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> Co-authored-by: Syoyo Fujita --- models/issue-400-num-face-vertices.obj | 15 +++++ .../python/tinyobjloader_tests/test_loader.py | 61 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 models/issue-400-num-face-vertices.obj diff --git a/models/issue-400-num-face-vertices.obj b/models/issue-400-num-face-vertices.obj new file mode 100644 index 00000000..77e25b48 --- /dev/null +++ b/models/issue-400-num-face-vertices.obj @@ -0,0 +1,15 @@ +# Regression test model for issue #400 - numpy_num_face_vertices() +# Mixed quad and triangle faces to verify correct uint type handling. +# With the bug (unsigned char instead of unsigned int in the numpy binding), +# numpy_num_face_vertices() returned all zeros for quad (4-vertex) faces. +v 0.0 0.0 0.0 +v 1.0 0.0 0.0 +v 1.0 1.0 0.0 +v 0.0 1.0 0.0 +v 0.5 0.5 1.0 +# quad face (num_face_vertices = 4) +f 1 2 3 4 +# triangle face (num_face_vertices = 3) +f 1 2 5 +# triangle face (num_face_vertices = 3) +f 2 3 5 diff --git a/tests/python/tinyobjloader_tests/test_loader.py b/tests/python/tinyobjloader_tests/test_loader.py index 8d7b8f50..c9159ea1 100644 --- a/tests/python/tinyobjloader_tests/test_loader.py +++ b/tests/python/tinyobjloader_tests/test_loader.py @@ -1,7 +1,11 @@ +from pathlib import Path + import numpy as np from .loader import Loader, LoadException +MODELS_DIR = Path(__file__).parent.parent.parent.parent / "models" + TWO_QUADS = """ v 0 0 0 v 0 0 0 @@ -130,7 +134,32 @@ def test_numpy_index_array_two_quads(): assert [x.vertex_index for x in shape.mesh.indices] == expected_vertex_index # Test. - expected_numpy_indices = [0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1, -1, 5, -1, -1, 6, -1, -1, 7, -1, -1] + expected_numpy_indices = [ + 0, + -1, + -1, + 1, + -1, + -1, + 2, + -1, + -1, + 3, + -1, + -1, + 4, + -1, + -1, + 5, + -1, + -1, + 6, + -1, + -1, + 7, + -1, + -1, + ] np.testing.assert_array_equal(shape.mesh.numpy_indices(), expected_numpy_indices) @@ -174,3 +203,33 @@ def test_numpy_vertex_array_two_quads(): # Test. np.testing.assert_array_almost_equal(loader.attrib.numpy_vertices(), expected_vertices, decimal=6) + + +def test_numpy_num_face_vertices_from_file(): + """ + Regression test for https://github.com/tinyobjloader/tinyobjloader/issues/400 + + Loads a mixed quad/triangle mesh from a .obj file and checks that + numpy_num_face_vertices() returns correct unsigned int values. + With the bug (unsigned char element type), values were read as zeros. + """ + + # Set up. + obj_path = str(MODELS_DIR / "issue-400-num-face-vertices.obj") + loader = Loader(triangulate=False) + loader.load(obj_path) + + shapes = loader.shapes + assert len(shapes) == 1 + + (shape,) = shapes + # The file has one quad face (4 vertices) and two triangle faces (3 vertices each). + expected_num_face_vertices = [4, 3, 3] + + # Confidence check using the non-numpy accessor. + assert shape.mesh.num_face_vertices == expected_num_face_vertices + + # Test: numpy_num_face_vertices() must return the same values with the correct dtype. + result = shape.mesh.numpy_num_face_vertices() + np.testing.assert_array_equal(result, expected_num_face_vertices) + assert result.dtype == np.dtype("uint32") From c20c9734b9764af19f5a7c1782c05d9d3ecf308f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 08:45:54 +0900 Subject: [PATCH 17/22] Fix texcoord_ws corruption, alpha_texname double-write, missing BOM strip in callback, and wrong test fixture path (#414) * Initial plan * Fix bugs and enhancements: texcoord_ws, alpha_texname, invalid test file, BOM in callback, texcoord w test Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Add test_loadObjWithCallback_with_BOM unit test for BOM stripping in callback API Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Add mixed vt w-present/w-omitted test case and model Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> Co-authored-by: Syoyo Fujita --- models/texcoord-w-mixed.obj | 15 +++++ models/texcoord-w.obj | 14 +++++ tests/tester.cc | 119 +++++++++++++++++++++++++++++++++++- tiny_obj_loader.h | 16 ++++- 4 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 models/texcoord-w-mixed.obj create mode 100644 models/texcoord-w.obj diff --git a/models/texcoord-w-mixed.obj b/models/texcoord-w-mixed.obj new file mode 100644 index 00000000..16421ae5 --- /dev/null +++ b/models/texcoord-w-mixed.obj @@ -0,0 +1,15 @@ +# OBJ file with mixed 3-component and 2-component texture coordinates +# Tests that texcoord_ws correctly stores 0.0 for omitted w values +v 0 0 0 +v 1 0 0 +v 1 1 0 +v 0 1 0 + +# vt lines alternating: w present, w omitted, w present, w omitted +vt 0.0 0.0 0.5 +vt 1.0 0.0 +vt 1.0 1.0 0.75 +vt 0.0 1.0 + +f 1/1 2/2 3/3 +f 1/1 3/3 4/4 diff --git a/models/texcoord-w.obj b/models/texcoord-w.obj new file mode 100644 index 00000000..019ea7ae --- /dev/null +++ b/models/texcoord-w.obj @@ -0,0 +1,14 @@ +# OBJ file with 3-component texture coordinates to test texcoord_ws parsing +v 0 0 0 +v 1 0 0 +v 1 1 0 +v 0 1 0 + +# texture coords with optional w component +vt 0.0 0.0 0.5 +vt 1.0 0.0 0.25 +vt 1.0 1.0 0.75 +vt 0.0 1.0 0.0 + +f 1/1 2/2 3/3 +f 1/1 3/3 4/4 diff --git a/tests/tester.cc b/tests/tester.cc index b5c4d0db..eb210980 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -889,7 +889,7 @@ void test_invalid_texture_vertex_index() { std::string err; bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, - "../models/invalid-relative-texture-vertex-index.obj", gMtlBasePath); + "../models/invalid-relative-texture-index.obj", gMtlBasePath); if (!warn.empty()) { std::cout << "WARN: " << warn << std::endl; @@ -1520,6 +1520,120 @@ void test_loadObj_with_BOM() { } +void test_texcoord_w_component() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + + std::string warn; + std::string err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + "../models/texcoord-w.obj", gMtlBasePath, + /*triangulate*/ false); + + if (!warn.empty()) { + std::cout << "WARN: " << warn << std::endl; + } + + if (!err.empty()) { + std::cerr << "ERR: " << err << std::endl; + } + + TEST_CHECK(true == ret); + TEST_CHECK(4 == attrib.texcoords.size() / 2); // 4 uv pairs + TEST_CHECK(4 == attrib.texcoord_ws.size()); // 4 w values + TEST_CHECK(FloatEquals(0.50f, attrib.texcoord_ws[0])); + TEST_CHECK(FloatEquals(0.25f, attrib.texcoord_ws[1])); + TEST_CHECK(FloatEquals(0.75f, attrib.texcoord_ws[2])); + TEST_CHECK(FloatEquals(0.00f, attrib.texcoord_ws[3])); +} + + + +void test_texcoord_w_mixed_component() { + // Test a mix of vt lines with the optional w present and omitted. + // Lines without w should produce 0.0 in texcoord_ws. + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + + std::string warn; + std::string err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + "../models/texcoord-w-mixed.obj", gMtlBasePath, + /*triangulate*/ false); + + if (!warn.empty()) { + std::cout << "WARN: " << warn << std::endl; + } + + if (!err.empty()) { + std::cerr << "ERR: " << err << std::endl; + } + + TEST_CHECK(true == ret); + TEST_CHECK(4 == attrib.texcoords.size() / 2); // 4 uv pairs + TEST_CHECK(4 == attrib.texcoord_ws.size()); // 4 w values (present or defaulted) + TEST_CHECK(FloatEquals(0.50f, attrib.texcoord_ws[0])); // w present + TEST_CHECK(FloatEquals(0.00f, attrib.texcoord_ws[1])); // w omitted -> 0.0 + TEST_CHECK(FloatEquals(0.75f, attrib.texcoord_ws[2])); // w present + TEST_CHECK(FloatEquals(0.00f, attrib.texcoord_ws[3])); // w omitted -> 0.0 +} + +void test_loadObjWithCallback_with_BOM() { + // Verify that LoadObjWithCallback correctly strips a UTF-8 BOM from the + // first line, just as LoadObj and LoadMtl do. + // We reuse cube_w_BOM.obj which starts with 0xEF 0xBB 0xBF followed by + // "mtllib cube_w_BOM.mtl". Without BOM stripping the mtllib line would + // not be recognised and no materials would be loaded; with BOM stripping + // all 8 vertices and 6 groups are parsed. + + struct CallbackData { + int vertex_count; + int group_count; + int material_count; + CallbackData() : vertex_count(0), group_count(0), material_count(0) {} + }; + + CallbackData data; + + tinyobj::callback_t cb; + cb.vertex_cb = [](void *user_data, tinyobj::real_t x, tinyobj::real_t y, + tinyobj::real_t z, tinyobj::real_t w) { + reinterpret_cast(user_data)->vertex_count++; + }; + cb.group_cb = [](void *user_data, const char **names, int num_names) { + if (num_names > 0) + reinterpret_cast(user_data)->group_count++; + }; + cb.mtllib_cb = [](void *user_data, const tinyobj::material_t *materials, + int num_materials) { + reinterpret_cast(user_data)->material_count += + num_materials; + }; + + std::ifstream ifs("../models/cube_w_BOM.obj"); + TEST_CHECK(ifs.is_open()); + + tinyobj::MaterialFileReader matReader(gMtlBasePath); + std::string warn, err; + bool ret = tinyobj::LoadObjWithCallback(ifs, cb, &data, &matReader, &warn, &err); + + if (!warn.empty()) { + std::cout << "WARN: " << warn << std::endl; + } + if (!err.empty()) { + std::cerr << "ERR: " << err << std::endl; + } + + TEST_CHECK(true == ret); + TEST_CHECK(8 == data.vertex_count); // 8 vertices in cube_w_BOM.obj + TEST_CHECK(6 == data.group_count); // 6 groups: front/back/right/top/left/bottom + TEST_CHECK(data.material_count > 0); // materials loaded => mtllib line was parsed +} + + + // Fuzzer test. // Just check if it does not crash. // Disable by default since Windows filesystem can't create filename of afl @@ -1633,4 +1747,7 @@ TEST_LIST = { test_default_kd_for_multiple_materials_issue391}, {"test_removeUtf8Bom", test_removeUtf8Bom}, {"test_loadObj_with_BOM", test_loadObj_with_BOM}, + {"test_loadObjWithCallback_with_BOM", test_loadObjWithCallback_with_BOM}, + {"test_texcoord_w_component", test_texcoord_w_component}, + {"test_texcoord_w_mixed_component", test_texcoord_w_mixed_component}, {NULL, NULL}}; diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index a927864e..fb2cbb8d 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -2386,7 +2386,6 @@ void LoadMtl(std::map *material_map, // alpha texture if ((0 == strncmp(token, "map_d", 5)) && IS_SPACE(token[5])) { token += 6; - material.alpha_texname = token; ParseTextureNameAndOption(&(material.alpha_texname), &(material.alpha_texopt), token); continue; @@ -2607,6 +2606,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, std::vector vertex_weights; // optional [w] component in `v` std::vector vn; std::vector vt; + std::vector vt_w; // optional [w] component in `vt` std::vector vc; std::vector vw; // tinyobj extension: vertex skin weights std::vector tags; @@ -2707,6 +2707,12 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, parseReal2(&x, &y, &token); vt.push_back(x); vt.push_back(y); + + // Parse optional w component + real_t w = static_cast(0.0); + parseReal(&token, &w); + vt_w.push_back(w); + continue; } @@ -3165,7 +3171,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, attrib->vertex_weights.swap(vertex_weights); attrib->normals.swap(vn); attrib->texcoords.swap(vt); - attrib->texcoord_ws.swap(vt); + attrib->texcoord_ws.swap(vt_w); attrib->colors.swap(vc); attrib->skin_weights.swap(vw); @@ -3191,9 +3197,12 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, std::vector names_out; std::string linebuf; + size_t line_num = 0; while (inStream.peek() != -1) { safeGetline(inStream, linebuf); + line_num++; + // Trim newline '\r\n' or '\n' if (linebuf.size() > 0) { if (linebuf[linebuf.size() - 1] == '\n') @@ -3208,6 +3217,9 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, if (linebuf.empty()) { continue; } + if (line_num == 1) { + linebuf = removeUtf8Bom(linebuf); + } // Skip leading space. const char *token = linebuf.c_str(); From 71a40e4975939aed3f5237313b7ff55205631e84 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:20:07 +0900 Subject: [PATCH 18/22] Add Windows long path and Unicode file path support for .obj/.mtl loading (#415) * Initial plan * Add Windows long path and Unicode file path support for .obj/.mtl loading Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Add unit tests for UTF-8 path and long path loading, with minimal test model files Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Fix Windows link error: add pragma comment for Advapi32.lib Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Fix Windows API correctness: wide temp path, wide mkdir, long-path copy, slash normalization Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> Co-authored-by: Syoyo Fujita --- models/utf8-path-test.mtl | 4 + models/utf8-path-test.obj | 6 ++ tests/tester.cc | 208 ++++++++++++++++++++++++++++++++++++++ tiny_obj_loader.h | 82 +++++++++++++++ 4 files changed, 300 insertions(+) create mode 100644 models/utf8-path-test.mtl create mode 100644 models/utf8-path-test.obj diff --git a/models/utf8-path-test.mtl b/models/utf8-path-test.mtl new file mode 100644 index 00000000..b89434a9 --- /dev/null +++ b/models/utf8-path-test.mtl @@ -0,0 +1,4 @@ +newmtl Material +Ka 0 0 0 +Kd 0.8 0.8 0.8 +Ks 0 0 0 diff --git a/models/utf8-path-test.obj b/models/utf8-path-test.obj new file mode 100644 index 00000000..a70ea094 --- /dev/null +++ b/models/utf8-path-test.obj @@ -0,0 +1,6 @@ +mtllib utf8-path-test.mtl +v 0.0 0.0 0.0 +v 1.0 0.0 0.0 +v 0.0 1.0 0.0 +usemtl Material +f 1 2 3 diff --git a/tests/tester.cc b/tests/tester.cc index eb210980..2a459f9f 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -29,6 +29,28 @@ #include #include #include +#include + +#ifdef _WIN32 +#include // _mkdir +#include // GetTempPathW, CreateDirectoryW, RegOpenKeyExA +#include // registry constants +#pragma comment(lib, "Advapi32.lib") // RegOpenKeyExA, RegQueryValueExA, RegCloseKey + +// Converts a UTF-16 wide string to a UTF-8 std::string. +static std::string WcharToUTF8(const std::wstring &wstr) { + if (wstr.empty()) return std::string(); + int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, + NULL, 0, NULL, NULL); + if (len <= 0) return std::string(); + std::string str(static_cast(len - 1), '\0'); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &str[0], len, NULL, NULL); + return str; +} +#else +#include +#include // mkdir +#endif template static bool FloatEquals(const T& a, const T& b) { @@ -399,6 +421,190 @@ static bool TestStreamLoadObj() { const char* gMtlBasePath = "../models/"; +// --------------------------------------------------------------------------- +// Helpers for path-related tests +// --------------------------------------------------------------------------- + +// Creates a single directory level. Returns true on success or if it already exists. +static bool MakeDir(const std::string& path) { +#ifdef _WIN32 + // Use the wide-character API so that paths with non-ASCII characters work. + std::wstring wpath = UTF8ToWchar(path); + if (wpath.empty()) return false; + if (CreateDirectoryW(wpath.c_str(), NULL) != 0) return true; + return GetLastError() == ERROR_ALREADY_EXISTS; +#else + return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST; +#endif +} + +// Removes a directory and all its contents. +// NOTE: All callers pass paths that are fully constructed within this test +// file from hardcoded string literals, so there is no user-controlled input +// that could be used for command injection. +static void RemoveTestDir(const std::string& path) { +#ifdef _WIN32 + std::string cmd = "rd /s /q \"" + path + "\""; +#else + std::string cmd = "rm -rf '" + path + "'"; +#endif + if (system(cmd.c_str()) != 0) { /* cleanup failure is non-fatal */ } +} + +// Copies a file in binary mode. The destination path is taken as UTF-8. +// On Windows, LongPathW(UTF8ToWchar()) is used so that long paths (> MAX_PATH) +// are handled, exercising the same conversion that tinyobjloader itself uses. +static bool CopyTestFile(const std::string& src, const std::string& dst) { + std::ifstream in(src.c_str(), std::ios::binary); + if (!in) return false; +#ifdef _WIN32 + // Apply long-path prefix so that the copy works even for paths > MAX_PATH. + std::ofstream out(LongPathW(UTF8ToWchar(dst)).c_str(), std::ios::binary); +#else + std::ofstream out(dst.c_str(), std::ios::binary); +#endif + if (!out) return false; + out << in.rdbuf(); + return !out.fail(); +} + +#ifdef _WIN32 +// Returns true if Windows has the system-wide long path support enabled +// (HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1). +static bool IsWindowsLongPathEnabled() { + HKEY hKey; + DWORD value = 0; + DWORD size = sizeof(DWORD); + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, + "SYSTEM\\CurrentControlSet\\Control\\FileSystem", 0, + KEY_READ, &hKey) == ERROR_SUCCESS) { + RegQueryValueExA(hKey, "LongPathsEnabled", NULL, NULL, + reinterpret_cast(&value), &size); + RegCloseKey(hKey); + } + return value != 0; +} +#endif // _WIN32 + +// --------------------------------------------------------------------------- +// Path-related tests +// --------------------------------------------------------------------------- + +// Test: load .obj/.mtl from a directory path containing UTF-8 non-ASCII +// characters. On Windows our code converts the UTF-8 path to UTF-16 before +// calling the file API. On Linux, UTF-8 paths are handled natively. +void test_load_obj_from_utf8_path() { + // Build a temp directory name that contains the UTF-8 encoded character é + // (U+00E9, encoded as \xC3\xA9 in UTF-8). +#ifdef _WIN32 + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + std::string test_dir = + WcharToUTF8(wtmpbuf) + "tinyobj_utf8_\xc3\xa9_test\\"; +#else + std::string test_dir = "/tmp/tinyobj_utf8_\xc3\xa9_test/"; +#endif + + if (!MakeDir(test_dir)) { + std::cout << "SKIPPED: Cannot create Unicode temp directory: " << test_dir + << "\n"; + return; + } + + const std::string obj_dst = test_dir + "utf8-path-test.obj"; + const std::string mtl_dst = test_dir + "utf8-path-test.mtl"; + + if (!CopyTestFile("../models/utf8-path-test.obj", obj_dst) || + !CopyTestFile("../models/utf8-path-test.mtl", mtl_dst)) { + RemoveTestDir(test_dir); + TEST_CHECK_(false, "Failed to copy test files to Unicode temp directory"); + return; + } + + tinyobj::ObjReader reader; + bool ret = reader.ParseFromFile(obj_dst); + + RemoveTestDir(test_dir); + + if (!reader.Warning().empty()) + std::cout << "WARN: " << reader.Warning() << "\n"; + if (!reader.Error().empty()) + std::cerr << "ERR: " << reader.Error() << "\n"; + + TEST_CHECK(ret == true); + TEST_CHECK(reader.GetShapes().size() == 1); + TEST_CHECK(reader.GetMaterials().size() == 1); +} + +// Test: load .obj/.mtl from a path whose total length exceeds MAX_PATH (260). +// On Windows, tinyobjloader prepends the \\?\ extended-length path prefix so +// that the file can be opened even on systems that have the OS-wide long path +// support enabled. The test is skipped when that support is not active. +// On Linux, long paths work natively; this test verifies no regression. +void test_load_obj_from_long_path() { +#ifdef _WIN32 + if (!IsWindowsLongPathEnabled()) { + std::cout + << "SKIPPED: Windows long path support (LongPathsEnabled) is not " + "enabled\n"; + return; + } + wchar_t wtmpbuf[MAX_PATH]; + GetTempPathW(MAX_PATH, wtmpbuf); + std::string base = WcharToUTF8(wtmpbuf); // e.g. "C:\Users\...\Temp\" + const char path_sep = '\\'; +#else + std::string base = "/tmp/"; + const char path_sep = '/'; +#endif + + // Create a two-level directory where the deepest directory name is 250 + // characters long. Combined with the base path and the filename + // "utf8-path-test.obj" (18 chars) the total file path comfortably exceeds + // MAX_PATH (260) on all supported platforms. + std::string test_root = base + "tinyobj_lp_test" + path_sep; + std::string long_subdir = test_root + std::string(250, 'a') + path_sep; + std::string obj_path = long_subdir + "utf8-path-test.obj"; + + // obj_path must exceed MAX_PATH for the test to be meaningful. + // (On a typical Windows installation it is ~320 chars; on Linux ~287 chars.) + if (obj_path.size() <= 260) { + std::cout << "SKIPPED: generated path (" << obj_path.size() + << " chars) does not exceed MAX_PATH=260\n"; + return; + } + + if (!MakeDir(test_root) || !MakeDir(long_subdir)) { + RemoveTestDir(test_root); + std::cout << "SKIPPED: Cannot create long-path temp directory: " + << long_subdir << "\n"; + return; + } + + if (!CopyTestFile("../models/utf8-path-test.obj", + long_subdir + "utf8-path-test.obj") || + !CopyTestFile("../models/utf8-path-test.mtl", + long_subdir + "utf8-path-test.mtl")) { + RemoveTestDir(test_root); + TEST_CHECK_(false, "Failed to copy test files to long-path directory"); + return; + } + + tinyobj::ObjReader reader; + bool ret = reader.ParseFromFile(obj_path); + + RemoveTestDir(test_root); + + if (!reader.Warning().empty()) + std::cout << "WARN: " << reader.Warning() << "\n"; + if (!reader.Error().empty()) + std::cerr << "ERR: " << reader.Error() << "\n"; + + TEST_CHECK(ret == true); + TEST_CHECK(reader.GetShapes().size() == 1); + TEST_CHECK(reader.GetMaterials().size() == 1); +} + void test_cornell_box() { TEST_CHECK(true == TestLoadObj("../models/cornell_box.obj", gMtlBasePath)); } @@ -1747,6 +1953,8 @@ TEST_LIST = { test_default_kd_for_multiple_materials_issue391}, {"test_removeUtf8Bom", test_removeUtf8Bom}, {"test_loadObj_with_BOM", test_loadObj_with_BOM}, + {"test_load_obj_from_utf8_path", test_load_obj_from_utf8_path}, + {"test_load_obj_from_long_path", test_load_obj_from_long_path}, {"test_loadObjWithCallback_with_BOM", test_loadObjWithCallback_with_BOM}, {"test_texcoord_w_component", test_texcoord_w_component}, {"test_texcoord_w_mixed_component", test_texcoord_w_mixed_component}, diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index fb2cbb8d..4f3f21de 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -667,6 +667,16 @@ bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, #include #include +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + #ifdef TINYOBJLOADER_USE_MAPBOX_EARCUT #ifdef TINYOBJLOADER_DONOT_INCLUDE_MAPBOX_EARCUT @@ -690,6 +700,66 @@ bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, #endif // TINYOBJLOADER_USE_MAPBOX_EARCUT +#ifdef _WIN32 +// Converts a UTF-8 encoded string to a UTF-16 wide string for use with +// Windows file APIs that support Unicode paths (including paths longer than +// MAX_PATH when combined with the extended-length path prefix). +static std::wstring UTF8ToWchar(const std::string &str) { + if (str.empty()) return std::wstring(); + int size_needed = + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), + static_cast(str.size()), NULL, 0); + if (size_needed == 0) return std::wstring(); + std::wstring wstr(static_cast(size_needed), L'\0'); + int result = + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), + static_cast(str.size()), &wstr[0], size_needed); + if (result == 0) return std::wstring(); + return wstr; +} + +// Prepends the Windows extended-length path prefix ("\\?\") to an absolute +// path when the path length meets or exceeds MAX_PATH (260 characters). +// This allows Windows APIs to handle paths up to 32767 characters long. +// UNC paths (starting with "\\") are converted to "\\?\UNC\" form. +static std::wstring LongPathW(const std::wstring &wpath) { + const std::wstring kLongPathPrefix = L"\\\\?\\"; + const std::wstring kUNCPrefix = L"\\\\"; + const std::wstring kLongUNCPathPrefix = L"\\\\?\\UNC\\"; + + // Already has the extended-length prefix; return as-is. + if (wpath.size() >= kLongPathPrefix.size() && + wpath.substr(0, kLongPathPrefix.size()) == kLongPathPrefix) { + return wpath; + } + + // Only add the prefix when the path is long enough to require it. + if (wpath.size() < MAX_PATH) { + return wpath; + } + + // Normalize forward slashes to backslashes: the extended-length "\\?\" + // prefix requires backslash separators only. + std::wstring normalized = wpath; + for (std::wstring::size_type i = 0; i < normalized.size(); ++i) { + if (normalized[i] == L'/') normalized[i] = L'\\'; + } + + // UNC path: "\\server\share\..." -> "\\?\UNC\server\share\..." + if (normalized.size() >= kUNCPrefix.size() && + normalized.substr(0, kUNCPrefix.size()) == kUNCPrefix) { + return kLongUNCPathPrefix + normalized.substr(kUNCPrefix.size()); + } + + // Absolute path with drive letter: "C:\..." -> "\\?\C:\..." + if (normalized.size() >= 2 && normalized[1] == L':') { + return kLongPathPrefix + normalized; + } + + return normalized; +} +#endif // _WIN32 + namespace tinyobj { MaterialReader::~MaterialReader() {} @@ -2503,7 +2573,11 @@ bool MaterialFileReader::operator()(const std::string &matId, for (size_t i = 0; i < paths.size(); i++) { std::string filepath = JoinPath(paths[i], matId); +#ifdef _WIN32 + std::ifstream matIStream(LongPathW(UTF8ToWchar(filepath)).c_str()); +#else std::ifstream matIStream(filepath.c_str()); +#endif if (matIStream) { LoadMtl(matMap, materials, &matIStream, warn, err); @@ -2521,7 +2595,11 @@ bool MaterialFileReader::operator()(const std::string &matId, } else { std::string filepath = matId; +#ifdef _WIN32 + std::ifstream matIStream(LongPathW(UTF8ToWchar(filepath)).c_str()); +#else std::ifstream matIStream(filepath.c_str()); +#endif if (matIStream) { LoadMtl(matMap, materials, &matIStream, warn, err); @@ -2571,7 +2649,11 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, std::stringstream errss; +#ifdef _WIN32 + std::ifstream ifs(LongPathW(UTF8ToWchar(filename)).c_str()); +#else std::ifstream ifs(filename); +#endif if (!ifs) { errss << "Cannot open file [" << filename << "]\n"; if (err) { From 5a2342f3b8ed1035bdd1822e5d71e363f1f825ea Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Tue, 3 Mar 2026 22:29:01 -0500 Subject: [PATCH 19/22] Move unit tests to GitHub Actions (#411) Based on discussion at https://github.com/tinyobjloader/tinyobjloader/pull/405#discussion_r2830642994 --- .github/workflows/unit.yml | 22 ++++++++++++++++++++++ azure-pipelines.yml | 21 --------------------- 2 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/unit.yml delete mode 100644 azure-pipelines.yml diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml new file mode 100644 index 00000000..af0072d1 --- /dev/null +++ b/.github/workflows/unit.yml @@ -0,0 +1,22 @@ +name: Unit Tests + +on: + push: + branches: + - '**' + tags: + - 'v*' + pull_request: + branches: + - '**' + +jobs: + unit_linux: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run unit tests + run: | + cd tests + make && ./tester diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index dbc3e166..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,21 +0,0 @@ -jobs: - - job: unit_linux - pool: { vmImage: "ubuntu-latest" } - steps: - - script: | - cd tests - make && ./tester - displayName: Run unit tests - -trigger: - branches: - include: - - '*' - tags: - include: - - 'v*' - -pr: - branches: - include: - - "*" From c748b62f30ba47663f2b774ad674633a4d515f90 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:16:00 +0900 Subject: [PATCH 20/22] Remove Azure build badge and AUR badge from README (#422) * Initial plan * Remove Azure build badge and AUR badge from README Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 3cb8d9ac..5574fb38 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,8 @@ [![PyPI version](https://badge.fury.io/py/tinyobjloader.svg)](https://badge.fury.io/py/tinyobjloader) -[![AZ Build Status](https://dev.azure.com/tinyobjloader/tinyobjloader/_apis/build/status/tinyobjloader.tinyobjloader?branchName=master)](https://dev.azure.com/tinyobjloader/tinyobjloader/_build/latest?definitionId=1&branchName=master) - [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/m6wfkvket7gth8wn/branch/master?svg=true)](https://ci.appveyor.com/project/syoyo/tinyobjloader-6e4qf/branch/master) -[![AUR version](https://img.shields.io/aur/version/tinyobjloader?logo=arch-linux)](https://aur.archlinux.org/packages/tinyobjloader) - Tiny but powerful single file wavefront obj loader written in C++03. No dependency except for C++ STL. It can parse over 10M polygons with moderate memory and time. `tinyobjloader` is good for embedding .obj loader to your (global illumination) renderer ;-) From dda7950c7a23448f06a8a5ba7403773be519514a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:22:48 +0900 Subject: [PATCH 21/22] Remove AppVeyor CI configuration and README badge (#421) * Initial plan * Remove AppVeyor CI configuration file Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Remove AppVeyor badge from README Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> Co-authored-by: Syoyo Fujita --- README.md | 2 -- appveyor.yml | 22 ---------------------- 2 files changed, 24 deletions(-) delete mode 100644 appveyor.yml diff --git a/README.md b/README.md index 5574fb38..1e447846 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [![PyPI version](https://badge.fury.io/py/tinyobjloader.svg)](https://badge.fury.io/py/tinyobjloader) -[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/m6wfkvket7gth8wn/branch/master?svg=true)](https://ci.appveyor.com/project/syoyo/tinyobjloader-6e4qf/branch/master) - Tiny but powerful single file wavefront obj loader written in C++03. No dependency except for C++ STL. It can parse over 10M polygons with moderate memory and time. `tinyobjloader` is good for embedding .obj loader to your (global illumination) renderer ;-) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 93f691ce..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: 1.0.{build} - -platform: x64 - -install: - ####################################################################################### - # All external dependencies are installed in C:\projects\deps - ####################################################################################### - - mkdir C:\projects\deps - - ####################################################################################### - # Install Ninja - ####################################################################################### - - set NINJA_URL="iframe.php?url=https%3A%2F%2Fgithub.com%2Fninja-build%2Fninja%2Freleases%2Fdownload%2Fv1.10.0%2Fninja-win.zip" - - appveyor DownloadFile %NINJA_URL% -FileName ninja.zip - - 7z x ninja.zip -oC:\projects\deps\ninja > nul - - set PATH=C:\projects\deps\ninja;%PATH% - - ninja --version - -build_script: - - cd tests - - vcbuild.bat From f27721f3c9cd20af4d4eee789431b92d0e0c4477 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:51:40 +0900 Subject: [PATCH 22/22] Rewrite OBJ/MTL parser to byte-stream based with StreamReader, add mmap support, and integrate Windows long path/Unicode support (#416) * Initial plan * Add max_iter guards to parsing loops and mmap support for .obj/.mtl loading Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Rewrite parser from line-based to byte-stream-based with EOF guards Replace safeGetline + raw const char* token pointer scanning with a StreamReader class that wraps (buf, len, idx) with bounds-checked peek/advance/match operations. Every byte access is guarded by an EOF check. Key changes: - Add StreamReader class for bounds-checked text parsing - Add sr_* parse functions that use StreamReader instead of const char** - Add LoadMtlInternal, LoadObjInternal, LoadObjWithCallbackInternal using StreamReader; public APIs wrap these - mmap paths construct StreamReader directly from mapped memory - Remove safeGetline (no longer needed) - Keep old parse functions for backward compat with public ParseTextureNameAndOption API - Keep IS_SPACE, IS_NEW_LINE, IS_DIGIT macros for old parse functions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rewrite parser to byte-stream based with StreamReader for bounds-checked parsing - Add StreamReader class wrapping (buf, length) with EOF-guarded peek/advance/match - Add sr_* parse functions using StreamReader instead of const char** pointers - Add LoadMtlInternal, LoadObjInternal, LoadObjWithCallbackInternal taking StreamReader - mmap paths construct StreamReader directly from mapped memory - Remove safeGetline (no longer needed) - Keep old parse functions + macros for ParseTextureNameAndOption backward compat Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Update tiny_obj_loader.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Integrate changes from release: Windows long paths, texcoord w-component, BOM in callback, test fixes - Add UTF8ToWchar/LongPathW helpers for Windows long path and Unicode support - Add Windows long path support (#ifdef _WIN32) in non-mmap ifstream fallbacks - Add vt_w vector and optional w component parsing in LoadObjInternal - Fix texcoord_ws.swap(vt) -> texcoord_ws.swap(vt_w) corruption - Add BOM stripping to LoadObjWithCallbackInternal - Fix invalid-relative-texture-vertex-index.obj -> invalid-relative-texture-index.obj - Add test models: texcoord-w.obj, texcoord-w-mixed.obj, utf8-path-test.obj/.mtl - Add tests: texcoord_w, texcoord_w_mixed, utf8_path, long_path, callback_BOM Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Add clang-style parse error reporting with early return on failure - Add column tracking (col_num_) to StreamReader, updated in all position-advancing methods (get, advance, skip_space, etc.) - Add current_line_text(), format_error(filename, msg) for clang-style diagnostics (file:line:col with source line and caret) - Add error stack to StreamReader (push_error, has_errors, get_errors) - Add error-reporting sr_parse overloads that return bool for success/failure: sr_parseInt, sr_parseReal, sr_parseReal2/3, sr_parseVertexWithColor (returns -1 on error) - Wire error-reporting overloads into LoadMtlInternal (Ka, Kd, Ks, Ni, Ns, illum, d, Tr, PBR properties) with early return on parse failure; change LoadMtlInternal from void to bool - Wire error-reporting overloads into LoadObjInternal (v, vn, vt, vw) with early return on parse failure - Upgrade existing error messages (face, line, points, skin weight) to use sr.format_error() for consistent clang-style output - Pass filename through call chain: LoadObj -> LoadObjInternal, MaterialFileReader -> LoadMtlInternal - Add warning_context.filename for fixIndex warnings - Add 6 new tests: column tracking, format_error output, error stack, malformed vertex/mtl errors, backward compatibility Co-Authored-By: Claude Opus 4.6 * Polish: hoist warning_context out of loop, fix no-op test - Move warning_context initialization (warn, filename) before the main parse loop in LoadObjInternal; only update line_number per iteration. Avoids a std::string copy of filename on every face/line/points line. - Replace test_parse_error_backward_compat (was just TEST_CHECK(true)) with an actual round-trip test: parse valid OBJ, verify no errors and correct vertex count. Co-Authored-By: Claude Opus 4.6 * Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Update tiny_obj_loader.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/tester.cc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix sr_parseTagTriple slash-delimited count parsing and add regression assertions Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Fix high/medium review findings: complete attrib reset, usemtl delimiter check, remove duplicate windows.h and dead errss Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Address low severity review items: document read_token, unify line_num naming, extract sr_skipTagField helper, refactor mmap to RAII MappedFile Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Harden MappedFile with is_mapped flag to prevent unmapping string literals Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Run GPT-5.3-Codex comprehensive review Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Remove accidental CTest artifact and ignore Testing/Temporary Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Fix stream offset handling and add oversized stream guards/tests Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Final deep-review fixes for mtllib callback failure path Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Stage-2 deep review fixes for mtllib empty tokens and mmap size guard parity Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Stage-3 deep review: preserve mtllib backslashes and harden stream size check Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Stage-4 review: fix callback usemtl parsing and harden int parsing Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Stage-5 review: harden size parsing and Windows mmap path Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> * Final polish: fix overflow guards, zero-length normalize, and minor inconsistencies - Fix potential size_t wraparound in StreamReader::match/char_at/peek_at bounds checks - Tighten exponent overflow guard to account for subsequent digit addition - Guard Normalize() against division by zero on zero-length vectors - Fix callback API usemtl advance(7)->advance(6) to match regular API - Fix test registration typo: "whitespece" -> "whitespace" Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syoyo <18676+syoyo@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Syoyo Fujita Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- .gitignore | 2 + Testing/Temporary/CTestCostData.txt | 1 + tests/tester.cc | 433 +++++- tiny_obj_loader.h | 1994 +++++++++++++++++++-------- 4 files changed, 1888 insertions(+), 542 deletions(-) create mode 100644 Testing/Temporary/CTestCostData.txt diff --git a/.gitignore b/.gitignore index 394552cc..4bd2eb9d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ build/ /python/_version.py /tinyobjloader.egg-info/ **/__pycache__/ + +/Testing/Temporary/ diff --git a/Testing/Temporary/CTestCostData.txt b/Testing/Temporary/CTestCostData.txt new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/Testing/Temporary/CTestCostData.txt @@ -0,0 +1 @@ +--- diff --git a/tests/tester.cc b/tests/tester.cc index 2a459f9f..fb692e7b 100644 --- a/tests/tester.cc +++ b/tests/tester.cc @@ -1,4 +1,5 @@ #define TINYOBJLOADER_IMPLEMENTATION +#define TINYOBJLOADER_STREAM_READER_MAX_BYTES (size_t(8) * size_t(1024) * size_t(1024)) #include "../tiny_obj_loader.h" #if defined(__clang__) @@ -632,6 +633,13 @@ void test_catmark_torus_creases0() { TEST_CHECK(1 == shapes.size()); TEST_CHECK(8 == shapes[0].mesh.tags.size()); + TEST_CHECK(std::string("crease") == shapes[0].mesh.tags[0].name); + TEST_CHECK(2 == shapes[0].mesh.tags[0].intValues.size()); + TEST_CHECK(1 == shapes[0].mesh.tags[0].floatValues.size()); + TEST_CHECK(0 == shapes[0].mesh.tags[0].stringValues.size()); + TEST_CHECK(1 == shapes[0].mesh.tags[0].intValues[0]); + TEST_CHECK(5 == shapes[0].mesh.tags[0].intValues[1]); + TEST_CHECK(FloatEquals(4.7f, shapes[0].mesh.tags[0].floatValues[0])); } void test_pbr() { @@ -1838,7 +1846,408 @@ void test_loadObjWithCallback_with_BOM() { TEST_CHECK(data.material_count > 0); // materials loaded => mtllib line was parsed } +void test_loadObjWithCallback_mtllib_failure_does_not_crash() { + // mtllib load failure should not crash callback path, and should report an + // error/warning while continuing OBJ parsing. + std::string obj_text = "mtllib test.mtl\nv 1 2 3\n"; + std::istringstream obj_stream(obj_text); + std::string oversized_mtl(TINYOBJLOADER_STREAM_READER_MAX_BYTES + size_t(1), ' '); + std::istringstream mtl_stream(oversized_mtl); + tinyobj::MaterialStreamReader mtl_reader(mtl_stream); + + struct CallbackData { + int vertex_count; + int mtllib_count; + CallbackData() : vertex_count(0), mtllib_count(0) {} + } data; + + tinyobj::callback_t cb; + cb.vertex_cb = [](void *user_data, tinyobj::real_t, tinyobj::real_t, + tinyobj::real_t, tinyobj::real_t) { + reinterpret_cast(user_data)->vertex_count++; + }; + cb.mtllib_cb = [](void *user_data, const tinyobj::material_t *, int) { + reinterpret_cast(user_data)->mtllib_count++; + }; + + std::string warn, err; + bool ret = tinyobj::LoadObjWithCallback(obj_stream, cb, &data, &mtl_reader, + &warn, &err); + + TEST_CHECK(ret == true); + TEST_CHECK(data.vertex_count == 1); + TEST_CHECK(data.mtllib_count == 0); + TEST_CHECK(warn.find("Failed to load material file(s)") != std::string::npos); + TEST_CHECK(err.find("input stream too large") != std::string::npos); +} + +void test_mtllib_empty_filename_is_ignored_loadobj() { + std::string obj_text = "mtllib \nv 1 2 3\n"; + std::istringstream obj_stream(obj_text); + + std::string mtl_text = "newmtl should_not_load\nKd 1 1 1\n"; + std::istringstream mtl_stream(mtl_text); + tinyobj::MaterialStreamReader mtl_reader(mtl_stream); + + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + &obj_stream, &mtl_reader); + + TEST_CHECK(ret == true); + TEST_CHECK(materials.empty()); + TEST_CHECK(warn.find("Looks like empty filename for mtllib") != std::string::npos); + TEST_CHECK(err.empty()); +} + +void test_mtllib_empty_filename_is_ignored_callback() { + std::string obj_text = "mtllib \nv 1 2 3\n"; + std::istringstream obj_stream(obj_text); + + std::string mtl_text = "newmtl should_not_load\nKd 1 1 1\n"; + std::istringstream mtl_stream(mtl_text); + tinyobj::MaterialStreamReader mtl_reader(mtl_stream); + + struct CallbackData { + int vertex_count; + int mtllib_count; + CallbackData() : vertex_count(0), mtllib_count(0) {} + } data; + + tinyobj::callback_t cb; + cb.vertex_cb = [](void *user_data, tinyobj::real_t, tinyobj::real_t, + tinyobj::real_t, tinyobj::real_t) { + reinterpret_cast(user_data)->vertex_count++; + }; + cb.mtllib_cb = [](void *user_data, const tinyobj::material_t *, int) { + reinterpret_cast(user_data)->mtllib_count++; + }; + + std::string warn, err; + bool ret = tinyobj::LoadObjWithCallback(obj_stream, cb, &data, &mtl_reader, + &warn, &err); + + TEST_CHECK(ret == true); + TEST_CHECK(data.vertex_count == 1); + TEST_CHECK(data.mtllib_count == 0); + TEST_CHECK(warn.find("Looks like empty filename for mtllib") != std::string::npos); + TEST_CHECK(err.empty()); +} + +void test_usemtl_callback_trims_trailing_comment() { + std::string obj_text = + "mtllib test.mtl\n" + "usemtl mat # trailing comment\n" + "v 0 0 0\n"; + std::istringstream obj_stream(obj_text); + + std::string mtl_text = "newmtl mat\nKd 1 1 1\n"; + std::istringstream mtl_stream(mtl_text); + tinyobj::MaterialStreamReader mtl_reader(mtl_stream); + + struct CallbackData { + int usemtl_count; + int last_material_id; + std::string last_name; + CallbackData() : usemtl_count(0), last_material_id(-1), last_name() {} + } data; + + tinyobj::callback_t cb; + cb.usemtl_cb = [](void *user_data, const char *name, int material_id) { + CallbackData *d = reinterpret_cast(user_data); + d->usemtl_count++; + d->last_name = name ? name : ""; + d->last_material_id = material_id; + }; + + std::string warn, err; + bool ret = tinyobj::LoadObjWithCallback(obj_stream, cb, &data, &mtl_reader, + &warn, &err); + + TEST_CHECK(ret == true); + TEST_CHECK(data.usemtl_count == 1); + TEST_CHECK(data.last_name == "mat"); + TEST_CHECK(data.last_material_id == 0); + TEST_CHECK(err.empty()); +} + +void test_tag_triple_huge_count_is_safely_rejected() { + std::string obj_text = + "v 0 0 0\n" + "v 1 0 0\n" + "v 0 1 0\n" + "f 1 2 3\n" + "t crease 999999999999999999999999999999999999999999999999999999999999999999/0/0\n"; + std::istringstream obj_stream(obj_text); + std::string mtl_text; + std::istringstream mtl_stream(mtl_text); + tinyobj::MaterialStreamReader mtl_reader(mtl_stream); + + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + &obj_stream, &mtl_reader); + + TEST_CHECK(ret == true); + TEST_CHECK(shapes.size() == size_t(1)); + TEST_CHECK(shapes[0].mesh.tags.size() == size_t(1)); + TEST_CHECK(shapes[0].mesh.tags[0].intValues.size() == size_t(0)); + TEST_CHECK(shapes[0].mesh.tags[0].floatValues.size() == size_t(0)); + TEST_CHECK(shapes[0].mesh.tags[0].stringValues.size() == size_t(0)); +} + + + +// Verify that mmap-based loading (TINYOBJLOADER_USE_MMAP) produces the same +// vertex/shape/material data as the standard ifstream-based path. +void test_mmap_and_standard_load_agree() { + const char *obj_file = "../models/cornell_box.obj"; + + // Load using whatever path is compiled in (mmap or ifstream). + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + obj_file, gMtlBasePath); + if (!warn.empty()) std::cout << "WARN: " << warn << "\n"; + if (!err.empty()) std::cerr << "ERR: " << err << "\n"; + TEST_CHECK(ret == true); + + // Also load via the stream API (always uses ifstream-equivalent path). + tinyobj::attrib_t attrib2; + std::vector shapes2; + std::vector materials2; + std::string warn2, err2; + std::ifstream ifs(obj_file); + TEST_CHECK(ifs.good()); + tinyobj::MaterialFileReader matReader(gMtlBasePath); + bool ret2 = tinyobj::LoadObj(&attrib2, &shapes2, &materials2, &warn2, &err2, + &ifs, &matReader); + TEST_CHECK(ret2 == true); + + // Compare results. + TEST_CHECK(attrib.vertices.size() == attrib2.vertices.size()); + TEST_CHECK(attrib.normals.size() == attrib2.normals.size()); + TEST_CHECK(shapes.size() == shapes2.size()); + TEST_CHECK(materials.size() == materials2.size()); + for (size_t i = 0; i < shapes.size(); i++) { + TEST_CHECK(shapes[i].mesh.indices.size() == shapes2[i].mesh.indices.size()); + } +} + +// Verify robustness: loading from a memory buffer (imemstream) is consistent +// with standard file loading. +void test_load_from_memory_buffer() { + const char *obj_file = "../models/cube.obj"; + + // Read file into memory manually. + std::ifstream file(obj_file, std::ios::binary | std::ios::ate); + TEST_CHECK(file.good()); + std::streamsize sz = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buf(static_cast(sz)); + TEST_CHECK(file.read(buf.data(), sz).good()); + file.close(); + + // Parse from the memory buffer via the stream API. + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + // Copy the memory buffer into a std::string and parse via std::istringstream. + std::string obj_text(buf.begin(), buf.end()); + std::istringstream obj_ss(obj_text); + tinyobj::MaterialFileReader matReader(gMtlBasePath); + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + &obj_ss, &matReader); + if (!warn.empty()) std::cout << "WARN: " << warn << "\n"; + if (!err.empty()) std::cerr << "ERR: " << err << "\n"; + TEST_CHECK(ret == true); + + // Compare with direct file load to check consistency. + tinyobj::attrib_t attrib2; + std::vector shapes2; + std::vector materials2; + std::string warn2, err2; + bool ret2 = tinyobj::LoadObj(&attrib2, &shapes2, &materials2, &warn2, &err2, + obj_file, gMtlBasePath); + TEST_CHECK(ret2 == true); + TEST_CHECK(attrib.vertices.size() == attrib2.vertices.size()); + TEST_CHECK(shapes.size() == shapes2.size()); +} + + +// --- Error reporting tests --- + +void test_streamreader_column_tracking() { + const char *input = "hello world\nfoo\n"; + tinyobj::StreamReader sr(input, strlen(input)); + + TEST_CHECK(sr.col_num() == 1); + TEST_CHECK(sr.line_num() == 1); + + // Advance 5 chars: "hello" + sr.advance(5); + TEST_CHECK(sr.col_num() == 6); // col is 1-based, after 5 chars -> col 6 + TEST_CHECK(sr.line_num() == 1); + + // skip_space: " " + sr.skip_space(); + TEST_CHECK(sr.col_num() == 7); + + // read_token: "world" + std::string tok = sr.read_token(); + TEST_CHECK(tok == "world"); + TEST_CHECK(sr.col_num() == 12); + + // skip_line: "\n" + sr.skip_line(); + TEST_CHECK(sr.line_num() == 2); + TEST_CHECK(sr.col_num() == 1); + + // get each char of "foo" + sr.get(); // 'f' + TEST_CHECK(sr.col_num() == 2); + sr.get(); // 'o' + sr.get(); // 'o' + TEST_CHECK(sr.col_num() == 4); +} + +void test_stream_load_from_current_offset() { + std::string prefix = "v 0 0 0\n"; + std::string payload = "v 1 2 3\n"; + std::string text = prefix + payload; + std::istringstream obj_ss(text); + obj_ss.seekg(static_cast(prefix.size()), std::ios::beg); + + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + &obj_ss, NULL); + if (!warn.empty()) std::cout << "WARN: " << warn << "\n"; + if (!err.empty()) std::cerr << "ERR: " << err << "\n"; + TEST_CHECK(ret == true); + TEST_CHECK(attrib.vertices.size() == 3); + TEST_CHECK(attrib.vertices[0] == 1.0f); + TEST_CHECK(attrib.vertices[1] == 2.0f); + TEST_CHECK(attrib.vertices[2] == 3.0f); +} + +void test_stream_load_rejects_oversized_input() { + std::string oversized(TINYOBJLOADER_STREAM_READER_MAX_BYTES + size_t(1), ' '); + std::istringstream obj_ss(oversized); + + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + &obj_ss, NULL); + TEST_CHECK(ret == false); + TEST_CHECK(err.find("input stream too large") != std::string::npos); +} + +void test_error_format_clang_style() { + const char *input = "v 1.0 abc 3.0\n"; + tinyobj::StreamReader sr(input, strlen(input)); + + // Position to the 'a' in 'abc' (column 7) + sr.advance(6); // past "v 1.0 " + TEST_CHECK(sr.col_num() == 7); + + std::string err = sr.format_error("test.obj", "expected number"); + // Should contain file:line:col + TEST_CHECK(err.find("test.obj:1:7: error: expected number") != std::string::npos); + // Should contain the source line + TEST_CHECK(err.find("v 1.0 abc 3.0") != std::string::npos); + // Should contain a caret + TEST_CHECK(err.find("^") != std::string::npos); +} + +void test_error_stack() { + const char *input = "test\n"; + tinyobj::StreamReader sr(input, strlen(input)); + + TEST_CHECK(!sr.has_errors()); + TEST_CHECK(sr.error_stack().empty()); + + sr.push_error("error 1\n"); + sr.push_error("error 2\n"); + TEST_CHECK(sr.has_errors()); + TEST_CHECK(sr.error_stack().size() == 2); + + std::string all = sr.get_errors(); + TEST_CHECK(all.find("error 1") != std::string::npos); + TEST_CHECK(all.find("error 2") != std::string::npos); + + sr.clear_errors(); + TEST_CHECK(!sr.has_errors()); + TEST_CHECK(sr.error_stack().empty()); +} + +void test_malformed_vertex_error() { + const char *obj_text = "v 1.0 abc 3.0\n"; + std::istringstream iss(obj_text); + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + &iss, NULL); + // Early return: malformed vertex coordinate is unrecoverable + TEST_CHECK(ret == false); + TEST_CHECK(err.find("expected number") != std::string::npos); + TEST_CHECK(err.find("abc") != std::string::npos); +} + +void test_malformed_mtl_error() { + const char *mtl_text = "newmtl test\nNs abc\n"; + std::istringstream mtl_iss(mtl_text); + std::map matMap; + std::vector materials; + std::string warn, err; + tinyobj::LoadMtl(&matMap, &materials, &mtl_iss, &warn, &err); + // LoadMtl is void (public API), but error should still be reported + TEST_CHECK(err.find("expected number") != std::string::npos); + TEST_CHECK(err.find("abc") != std::string::npos); +} + +void test_parse_error_backward_compat() { + // Verify that valid OBJ input parses without errors (the old non-error + // sr_parseReal path is still exercised by the callback API). + const char *obj_text = "v 1.0 2.0 3.0\nv 4.0 5.0 6.0\nf 1 2 1\n"; + std::istringstream iss(obj_text); + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, + &iss, NULL); + TEST_CHECK(ret == true); + TEST_CHECK(err.empty()); + TEST_CHECK(attrib.vertices.size() == 6); +} + +void test_split_string_preserves_non_escape_backslash() { + std::vector tokens; + tinyobj::SplitString("subdir\\file.mtl", ' ', '\\', tokens); + + TEST_CHECK(tokens.size() == 1); + TEST_CHECK(tokens[0] == "subdir\\file.mtl"); + + tokens.clear(); + tinyobj::SplitString("a\\ b.mtl", ' ', '\\', tokens); + TEST_CHECK(tokens.size() == 1); + TEST_CHECK(tokens[0] == "a b.mtl"); +} // Fuzzer test. // Just check if it does not crash. @@ -1935,7 +2344,7 @@ TEST_LIST = { test_usemtl_then_o_issue235}, {"mtl_searchpaths_issue244", test_mtl_searchpaths_issue244}, - {"usemtl_whitespece_issue246", + {"usemtl_whitespace_issue246", test_usemtl_whitespace_issue246}, {"texres_texopt_issue248", test_texres_texopt_issue248}, @@ -1956,6 +2365,28 @@ TEST_LIST = { {"test_load_obj_from_utf8_path", test_load_obj_from_utf8_path}, {"test_load_obj_from_long_path", test_load_obj_from_long_path}, {"test_loadObjWithCallback_with_BOM", test_loadObjWithCallback_with_BOM}, + {"test_loadObjWithCallback_mtllib_failure_does_not_crash", + test_loadObjWithCallback_mtllib_failure_does_not_crash}, + {"test_mtllib_empty_filename_is_ignored_loadobj", + test_mtllib_empty_filename_is_ignored_loadobj}, + {"test_mtllib_empty_filename_is_ignored_callback", + test_mtllib_empty_filename_is_ignored_callback}, + {"test_usemtl_callback_trims_trailing_comment", + test_usemtl_callback_trims_trailing_comment}, + {"test_tag_triple_huge_count_is_safely_rejected", + test_tag_triple_huge_count_is_safely_rejected}, {"test_texcoord_w_component", test_texcoord_w_component}, {"test_texcoord_w_mixed_component", test_texcoord_w_mixed_component}, + {"test_mmap_and_standard_load_agree", test_mmap_and_standard_load_agree}, + {"test_load_from_memory_buffer", test_load_from_memory_buffer}, + {"test_streamreader_column_tracking", test_streamreader_column_tracking}, + {"test_stream_load_from_current_offset", test_stream_load_from_current_offset}, + {"test_stream_load_rejects_oversized_input", test_stream_load_rejects_oversized_input}, + {"test_error_format_clang_style", test_error_format_clang_style}, + {"test_error_stack", test_error_stack}, + {"test_malformed_vertex_error", test_malformed_vertex_error}, + {"test_malformed_mtl_error", test_malformed_mtl_error}, + {"test_parse_error_backward_compat", test_parse_error_backward_compat}, + {"test_split_string_preserves_non_escape_backslash", + test_split_string_preserves_non_escape_backslash}, {NULL, NULL}}; diff --git a/tiny_obj_loader.h b/tiny_obj_loader.h index 4f3f21de..f57343a1 100644 --- a/tiny_obj_loader.h +++ b/tiny_obj_loader.h @@ -659,13 +659,11 @@ bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, #include #include #include +#include #include #include #include #include -#include -#include -#include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -677,6 +675,19 @@ bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, #include #endif +#ifdef TINYOBJLOADER_USE_MMAP +#if !defined(_WIN32) +// POSIX headers for mmap +#include +#include +#include +#include +#endif +#endif // TINYOBJLOADER_USE_MMAP +#include +#include +#include + #ifdef TINYOBJLOADER_USE_MAPBOX_EARCUT #ifdef TINYOBJLOADER_DONOT_INCLUDE_MAPBOX_EARCUT @@ -764,6 +775,382 @@ namespace tinyobj { MaterialReader::~MaterialReader() {} +// Byte-stream reader for bounds-checked text parsing. +// Replaces raw `const char*` token pointers with `(buf, len, idx)` triple. +// Every byte access is guarded by an EOF check. +class StreamReader { + public: +// Maximum number of bytes StreamReader will buffer from std::istream. +// Define this macro to a larger value if your application needs to parse +// very large streamed OBJ/MTL content. +#ifndef TINYOBJLOADER_STREAM_READER_MAX_BYTES +#define TINYOBJLOADER_STREAM_READER_MAX_BYTES (size_t(256) * size_t(1024) * size_t(1024)) +#endif + + StreamReader(const char *buf, size_t length) + : buf_(buf), length_(length), idx_(0), line_num_(1), col_num_(1) {} + + // Build from std::istream by reading all content into an internal buffer. + explicit StreamReader(std::istream &is) : buf_(NULL), length_(0), idx_(0), line_num_(1), col_num_(1) { + const size_t max_stream_bytes = TINYOBJLOADER_STREAM_READER_MAX_BYTES; + std::streampos start_pos = is.tellg(); + bool can_seek = (start_pos != std::streampos(-1)); + if (can_seek) { + is.seekg(0, std::ios::end); + std::streampos end_pos = is.tellg(); + if (end_pos >= start_pos) { + std::streamoff remaining_off = static_cast(end_pos - start_pos); + if (remaining_off < 0) { + is.seekg(start_pos); + push_error("failed to determine stream size\n"); + buf_ = ""; + length_ = 0; + return; + } + is.seekg(start_pos); + unsigned long long remaining_ull = static_cast(remaining_off); + if (remaining_ull > static_cast((std::numeric_limits::max)())) { + std::stringstream ss; + ss << "input stream too large for this platform (" << remaining_ull + << " bytes exceeds size_t max " << (std::numeric_limits::max)() << ")\n"; + push_error(ss.str()); + buf_ = ""; + length_ = 0; + return; + } + size_t remaining_size = static_cast(remaining_ull); + if (remaining_size > max_stream_bytes) { + std::stringstream ss; + ss << "input stream too large (" << remaining_size + << " bytes exceeds limit " << max_stream_bytes << " bytes)\n"; + push_error(ss.str()); + buf_ = ""; + length_ = 0; + return; + } + owned_buf_.resize(remaining_size); + if (remaining_size > 0) { + is.read(&owned_buf_[0], static_cast(remaining_size)); + } + size_t actually_read = static_cast(is.gcount()); + owned_buf_.resize(actually_read); + } + } + if (!can_seek || owned_buf_.empty()) { + // Stream doesn't support seeking, or seek probing failed. + if (can_seek) is.seekg(start_pos); + is.clear(); + std::vector content; + char chunk[4096]; + size_t total_read = 0; + while (is.good()) { + is.read(chunk, static_cast(sizeof(chunk))); + std::streamsize nread = is.gcount(); + if (nread <= 0) break; + size_t n = static_cast(nread); + if (n > (max_stream_bytes - total_read)) { + std::stringstream ss; + ss << "input stream too large (exceeds limit " << max_stream_bytes + << " bytes)\n"; + push_error(ss.str()); + owned_buf_.clear(); + buf_ = ""; + length_ = 0; + return; + } + content.insert(content.end(), chunk, chunk + n); + total_read += n; + } + owned_buf_.swap(content); + } + buf_ = owned_buf_.empty() ? "" : &owned_buf_[0]; + length_ = owned_buf_.size(); + } + + bool eof() const { return idx_ >= length_; } + size_t tell() const { return idx_; } + size_t size() const { return length_; } + size_t line_num() const { return line_num_; } + size_t col_num() const { return col_num_; } + + char peek() const { + if (idx_ >= length_) return '\0'; + return buf_[idx_]; + } + + char get() { + if (idx_ >= length_) return '\0'; + char c = buf_[idx_++]; + if (c == '\n') { line_num_++; col_num_ = 1; } else { col_num_++; } + return c; + } + + void advance(size_t n) { + for (size_t i = 0; i < n && idx_ < length_; i++) { + if (buf_[idx_] == '\n') { line_num_++; col_num_ = 1; } else { col_num_++; } + idx_++; + } + } + + void skip_space() { + while (idx_ < length_ && (buf_[idx_] == ' ' || buf_[idx_] == '\t')) { + col_num_++; + idx_++; + } + } + + void skip_space_and_cr() { + while (idx_ < length_ && (buf_[idx_] == ' ' || buf_[idx_] == '\t' || buf_[idx_] == '\r')) { + col_num_++; + idx_++; + } + } + + void skip_line() { + while (idx_ < length_) { + char c = buf_[idx_]; + if (c == '\n') { + idx_++; + line_num_++; + col_num_ = 1; + return; + } + if (c == '\r') { + idx_++; + if (idx_ < length_ && buf_[idx_] == '\n') { + idx_++; + } + line_num_++; + col_num_ = 1; + return; + } + col_num_++; + idx_++; + } + } + + bool at_line_end() const { + if (idx_ >= length_) return true; + char c = buf_[idx_]; + return (c == '\n' || c == '\r' || c == '\0'); + } + + std::string read_line() { + std::string result; + while (idx_ < length_) { + char c = buf_[idx_]; + if (c == '\n' || c == '\r') break; + result += c; + col_num_++; + idx_++; + } + return result; + } + + // Reads a whitespace-delimited token. Used by tests and as a general utility. + std::string read_token() { + skip_space(); + std::string result; + while (idx_ < length_) { + char c = buf_[idx_]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\0') break; + result += c; + col_num_++; + idx_++; + } + return result; + } + + bool match(const char *prefix, size_t len) const { + if (len > length_ - idx_) return false; + return (memcmp(buf_ + idx_, prefix, len) == 0); + } + + bool char_at(size_t offset, char c) const { + if (offset >= length_ - idx_) return false; + return buf_[idx_ + offset] == c; + } + + char peek_at(size_t offset) const { + if (offset >= length_ - idx_) return '\0'; + return buf_[idx_ + offset]; + } + + const char *current_ptr() const { + if (idx_ >= length_) return ""; + return buf_ + idx_; + } + + size_t remaining() const { + return (idx_ < length_) ? (length_ - idx_) : 0; + } + + // Returns the full text of the current line (for diagnostic display). + std::string current_line_text() const { + // Scan backward to find line start + size_t line_start = idx_; + while (line_start > 0 && buf_[line_start - 1] != '\n' && buf_[line_start - 1] != '\r') { + line_start--; + } + // Scan forward to find line end + size_t line_end = idx_; + while (line_end < length_ && buf_[line_end] != '\n' && buf_[line_end] != '\r') { + line_end++; + } + return std::string(buf_ + line_start, line_end - line_start); + } + + // Clang-style formatted error with file:line:col and caret. + std::string format_error(const std::string &filename, const std::string &msg) const { + std::stringstream line_ss, col_ss; + line_ss << line_num_; + col_ss << col_num_; + std::string result; + result += filename + ":" + line_ss.str() + ":" + col_ss.str() + ": error: " + msg + "\n"; + std::string line_text = current_line_text(); + result += line_text + "\n"; + // Build caret line preserving tab alignment + std::string caret; + size_t caret_pos = (col_num_ > 0) ? (col_num_ - 1) : 0; + for (size_t i = 0; i < caret_pos && i < line_text.size(); i++) { + caret += (line_text[i] == '\t') ? '\t' : ' '; + } + caret += "^"; + result += caret + "\n"; + return result; + } + + std::string format_error(const std::string &msg) const { + return format_error("", msg); + } + + // Error stack + void push_error(const std::string &msg) { + errors_.push_back(msg); + } + + void push_formatted_error(const std::string &filename, const std::string &msg) { + errors_.push_back(format_error(filename, msg)); + } + + bool has_errors() const { return !errors_.empty(); } + + std::string get_errors() const { + std::string result; + for (size_t i = 0; i < errors_.size(); i++) { + result += errors_[i]; + } + return result; + } + + const std::vector &error_stack() const { return errors_; } + + void clear_errors() { errors_.clear(); } + + private: + const char *buf_; + size_t length_; + size_t idx_; + size_t line_num_; + size_t col_num_; + std::vector owned_buf_; + std::vector errors_; +}; + +#ifdef TINYOBJLOADER_USE_MMAP +// RAII wrapper for memory-mapped file I/O. +// Opens a file and maps it into memory; the mapping is released on destruction. +// For empty files, data is set to "" and is_mapped remains false so close() +// will not attempt to unmap a string literal. +struct MappedFile { + const char *data; + size_t size; + bool is_mapped; // true when data points to an actual mapped region +#if defined(_WIN32) + HANDLE hFile; + HANDLE hMapping; +#else + void *mapped_ptr; +#endif + + MappedFile() : data(NULL), size(0), is_mapped(false) +#if defined(_WIN32) + , hFile(INVALID_HANDLE_VALUE), hMapping(NULL) +#else + , mapped_ptr(NULL) +#endif + {} + + // Opens and maps the file. Returns true on success. + bool open(const char *filepath) { +#if defined(_WIN32) + std::wstring wfilepath = LongPathW(UTF8ToWchar(std::string(filepath))); + hFile = CreateFileW(wfilepath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) return false; + LARGE_INTEGER fileSize; + if (!GetFileSizeEx(hFile, &fileSize)) { close(); return false; } + if (fileSize.QuadPart < 0) { close(); return false; } + unsigned long long fsize = static_cast(fileSize.QuadPart); + if (fsize > static_cast((std::numeric_limits::max)())) { + close(); + return false; + } + size = static_cast(fsize); + if (size == 0) { data = ""; return true; } // valid but empty; is_mapped stays false + hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + if (hMapping == NULL) { close(); return false; } + data = static_cast(MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0)); + if (!data) { close(); return false; } + is_mapped = true; + return true; +#else + int fd = ::open(filepath, O_RDONLY); + if (fd == -1) return false; + struct stat sb; + if (fstat(fd, &sb) != 0) { ::close(fd); return false; } + if (sb.st_size < 0) { ::close(fd); return false; } + if (static_cast(sb.st_size) > + static_cast((std::numeric_limits::max)())) { + ::close(fd); + return false; + } + size = static_cast(sb.st_size); + if (size == 0) { ::close(fd); data = ""; return true; } // valid but empty + mapped_ptr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + ::close(fd); + if (mapped_ptr == MAP_FAILED) { mapped_ptr = NULL; return false; } + data = static_cast(mapped_ptr); + is_mapped = true; + return true; +#endif + } + + void close() { +#if defined(_WIN32) + if (is_mapped && data) { UnmapViewOfFile(data); } + data = NULL; + is_mapped = false; + if (hMapping != NULL) { CloseHandle(hMapping); hMapping = NULL; } + if (hFile != INVALID_HANDLE_VALUE) { CloseHandle(hFile); hFile = INVALID_HANDLE_VALUE; } +#else + if (is_mapped && mapped_ptr && mapped_ptr != MAP_FAILED) { munmap(mapped_ptr, size); } + mapped_ptr = NULL; + data = NULL; + is_mapped = false; +#endif + size = 0; + } + + ~MappedFile() { close(); } + + private: + MappedFile(const MappedFile &); // non-copyable + MappedFile &operator=(const MappedFile &); // non-copyable +}; +#endif // TINYOBJLOADER_USE_MMAP + + struct vertex_index_t { int v_idx, vt_idx, vn_idx; vertex_index_t() : v_idx(-1), vt_idx(-1), vn_idx(-1) {} @@ -834,40 +1221,6 @@ struct PrimGroup { // See // http://stackoverflow.com/questions/6089231/getting-std-ifstream-to-handle-lf-cr-and-crlf -static std::istream &safeGetline(std::istream &is, std::string &t) { - t.clear(); - - // The characters in the stream are read one-by-one using a std::streambuf. - // That is faster than reading them one-by-one using the std::istream. - // Code that uses streambuf this way must be guarded by a sentry object. - // The sentry object performs various tasks, - // such as thread synchronization and updating the stream state. - - std::istream::sentry se(is, true); - std::streambuf *sb = is.rdbuf(); - - if (se) { - for (;;) { - int c = sb->sbumpc(); - switch (c) { - case '\n': - return is; - case '\r': - if (sb->sgetc() == '\n') sb->sbumpc(); - return is; - case EOF: - // Also handle the case when the last line has no line ending - if (t.empty()) is.setstate(std::ios::eofbit); - return is; - default: - t += static_cast(c); - } - } - } - - return is; -} - #define IS_SPACE(x) (((x) == ' ') || ((x) == '\t')) #define IS_DIGIT(x) \ (static_cast((x) - '0') < static_cast(10)) @@ -891,9 +1244,17 @@ static inline std::string removeUtf8Bom(const std::string& input) { return input; } +// Trim trailing spaces and tabs from a string. +static inline std::string trimTrailingWhitespace(const std::string &s) { + size_t end = s.find_last_not_of(" \t"); + if (end == std::string::npos) return ""; + return s.substr(0, end + 1); +} + struct warning_context { std::string *warn; size_t line_number; + std::string filename; }; // Make index zero-base, and also support relative index. @@ -912,9 +1273,9 @@ static inline bool fixIndex(int idx, int n, int *ret, bool allow_zero, // zero is not allowed according to the spec. if (context.warn) { (*context.warn) += - "A zero value index found (will have a value of -1 for normal and " - "tex indices. Line " + - toString(context.line_number) + ").\n"; + context.filename + ":" + toString(context.line_number) + + ": warning: zero value index found (will have a value of -1 for " + "normal and tex indices)\n"; } (*ret) = idx - 1; @@ -1085,7 +1446,7 @@ static bool tryParseDouble(const char *s, const char *s_end, double *result) { // To avoid annoying MSVC's min/max macro definiton, // Use hardcoded int max value if (exponent > - (2147483647 / 10)) { // 2147483647 = std::numeric_limits::max() + ((2147483647 - 9) / 10)) { // (INT_MAX - 9) / 10, guards both multiply and add // Integer overflow goto fail; } @@ -1345,10 +1706,448 @@ static vertex_index_t parseRawTriple(const char **token) { return vi; } - // i/j/k - (*token)++; // skip '/' - vi.vn_idx = atoi((*token)); - (*token) += strcspn((*token), "/ \t\r"); + // i/j/k + (*token)++; // skip '/' + vi.vn_idx = atoi((*token)); + (*token) += strcspn((*token), "/ \t\r"); + return vi; +} + +// --- Stream-based parse functions --- + +static inline std::string sr_parseString(StreamReader &sr) { + sr.skip_space(); + std::string s; + while (!sr.eof()) { + char c = sr.peek(); + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\0') break; + s += c; + sr.advance(1); + } + return s; +} + +static inline int sr_parseInt(StreamReader &sr) { + sr.skip_space(); + const char *start = sr.current_ptr(); + size_t rem = sr.remaining(); + size_t len = 0; + while (len < rem) { + char c = start[len]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\0') break; + len++; + } + int i = 0; + if (len > 0) { + char tmp[64]; + size_t copy_len = len < 63 ? len : 63; + if (copy_len != len) { + sr.advance(len); + return 0; + } + memcpy(tmp, start, copy_len); + tmp[copy_len] = '\0'; + errno = 0; + char *endptr = NULL; + long val = strtol(tmp, &endptr, 10); + const bool has_error = + (errno == ERANGE || endptr == tmp || + val > (std::numeric_limits::max)() || + val < (std::numeric_limits::min)()); + if (!has_error) { + i = static_cast(val); + } + } + sr.advance(len); + return i; +} + +static inline real_t sr_parseReal(StreamReader &sr, double default_value = 0.0) { + sr.skip_space(); + const char *start = sr.current_ptr(); + size_t rem = sr.remaining(); + size_t len = 0; + while (len < rem) { + char c = start[len]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\0') break; + len++; + } + double val = default_value; + if (len > 0) { + tryParseDouble(start, start + len, &val); + } + sr.advance(len); + return static_cast(val); +} + +static inline bool sr_parseReal(StreamReader &sr, real_t *out) { + sr.skip_space(); + const char *start = sr.current_ptr(); + size_t rem = sr.remaining(); + size_t len = 0; + while (len < rem) { + char c = start[len]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\0') break; + len++; + } + if (len == 0) return false; + double val; + bool ret = tryParseDouble(start, start + len, &val); + if (ret) { + (*out) = static_cast(val); + } + sr.advance(len); + return ret; +} + +static inline void sr_parseReal2(real_t *x, real_t *y, StreamReader &sr, + const double default_x = 0.0, + const double default_y = 0.0) { + (*x) = sr_parseReal(sr, default_x); + (*y) = sr_parseReal(sr, default_y); +} + +static inline void sr_parseReal3(real_t *x, real_t *y, real_t *z, + StreamReader &sr, + const double default_x = 0.0, + const double default_y = 0.0, + const double default_z = 0.0) { + (*x) = sr_parseReal(sr, default_x); + (*y) = sr_parseReal(sr, default_y); + (*z) = sr_parseReal(sr, default_z); +} + +static inline int sr_parseVertexWithColor(real_t *x, real_t *y, real_t *z, + real_t *r, real_t *g, real_t *b, + StreamReader &sr, + const double default_x = 0.0, + const double default_y = 0.0, + const double default_z = 0.0) { + (*x) = sr_parseReal(sr, default_x); + (*y) = sr_parseReal(sr, default_y); + (*z) = sr_parseReal(sr, default_z); + + bool has_r = sr_parseReal(sr, r); + if (!has_r) { + (*r) = (*g) = (*b) = 1.0; + return 3; + } + + bool has_g = sr_parseReal(sr, g); + if (!has_g) { + (*g) = (*b) = 1.0; + return 4; + } + + bool has_b = sr_parseReal(sr, b); + if (!has_b) { + (*r) = (*g) = (*b) = 1.0; + return 3; + } + + return 6; +} + +// --- Error-reporting overloads --- +// These overloads push clang-style diagnostics into `err` when parsing fails +// and return false so callers can early-return on unrecoverable parse errors. +// The original signatures are preserved above for backward compatibility. + +static inline bool sr_parseInt(StreamReader &sr, int *out, std::string *err, + const std::string &filename) { + sr.skip_space(); + const char *start = sr.current_ptr(); + size_t rem = sr.remaining(); + size_t len = 0; + while (len < rem) { + char c = start[len]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\0') break; + len++; + } + if (len == 0) { + if (err) { + (*err) += sr.format_error(filename, "expected integer value"); + } + *out = 0; + return false; + } + char tmp[64]; + size_t copy_len = len < 63 ? len : 63; + memcpy(tmp, start, copy_len); + tmp[copy_len] = '\0'; + if (copy_len != len) { + if (err) { + (*err) += sr.format_error(filename, "integer value too long"); + } + *out = 0; + sr.advance(len); + return false; + } + errno = 0; + char *endptr = NULL; + long val = strtol(tmp, &endptr, 10); + if (errno == ERANGE || val > (std::numeric_limits::max)() || + val < (std::numeric_limits::min)()) { + if (err) { + (*err) += sr.format_error(filename, + "integer value out of range, got '" + std::string(tmp) + "'"); + } + *out = 0; + sr.advance(len); + return false; + } + if (endptr == tmp || (*endptr != '\0' && *endptr != ' ' && *endptr != '\t')) { + if (err) { + (*err) += sr.format_error(filename, + "expected integer, got '" + std::string(tmp) + "'"); + } + *out = 0; + sr.advance(len); + return false; + } + *out = static_cast(val); + sr.advance(len); + return true; +} + +static inline bool sr_parseReal(StreamReader &sr, real_t *out, + double default_value, + std::string *err, + const std::string &filename) { + sr.skip_space(); + const char *start = sr.current_ptr(); + size_t rem = sr.remaining(); + size_t len = 0; + while (len < rem) { + char c = start[len]; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\0') break; + len++; + } + if (len == 0) { + // No token to parse — not necessarily an error (e.g. optional component). + *out = static_cast(default_value); + return true; + } + double val; + if (!tryParseDouble(start, start + len, &val)) { + if (err) { + char tmp[64]; + size_t copy_len = len < 63 ? len : 63; + memcpy(tmp, start, copy_len); + tmp[copy_len] = '\0'; + (*err) += sr.format_error(filename, + "expected number, got '" + std::string(tmp) + "'"); + } + *out = static_cast(default_value); + sr.advance(len); + return false; + } + *out = static_cast(val); + sr.advance(len); + return true; +} + +static inline bool sr_parseReal2(real_t *x, real_t *y, StreamReader &sr, + std::string *err, + const std::string &filename, + const double default_x = 0.0, + const double default_y = 0.0) { + if (!sr_parseReal(sr, x, default_x, err, filename)) return false; + if (!sr_parseReal(sr, y, default_y, err, filename)) return false; + return true; +} + +static inline bool sr_parseReal3(real_t *x, real_t *y, real_t *z, + StreamReader &sr, + std::string *err, + const std::string &filename, + const double default_x = 0.0, + const double default_y = 0.0, + const double default_z = 0.0) { + if (!sr_parseReal(sr, x, default_x, err, filename)) return false; + if (!sr_parseReal(sr, y, default_y, err, filename)) return false; + if (!sr_parseReal(sr, z, default_z, err, filename)) return false; + return true; +} + +// Returns number of components parsed (3, 4, or 6) on success, -1 on error. +static inline int sr_parseVertexWithColor(real_t *x, real_t *y, real_t *z, + real_t *r, real_t *g, real_t *b, + StreamReader &sr, + std::string *err, + const std::string &filename, + const double default_x = 0.0, + const double default_y = 0.0, + const double default_z = 0.0) { + if (!sr_parseReal(sr, x, default_x, err, filename)) return -1; + if (!sr_parseReal(sr, y, default_y, err, filename)) return -1; + if (!sr_parseReal(sr, z, default_z, err, filename)) return -1; + + bool has_r = sr_parseReal(sr, r); + if (!has_r) { + (*r) = (*g) = (*b) = 1.0; + return 3; + } + + bool has_g = sr_parseReal(sr, g); + if (!has_g) { + (*g) = (*b) = 1.0; + return 4; + } + + bool has_b = sr_parseReal(sr, b); + if (!has_b) { + (*r) = (*g) = (*b) = 1.0; + return 3; + } + + return 6; +} + +static inline int sr_parseIntNoSkip(StreamReader &sr); + +// Advance past remaining characters in a tag triple field (stops at '/', whitespace, or line end). +static inline void sr_skipTagField(StreamReader &sr) { + while (!sr.eof() && !sr.at_line_end() && !IS_SPACE(sr.peek()) && + sr.peek() != '/') { + sr.advance(1); + } +} + +static tag_sizes sr_parseTagTriple(StreamReader &sr) { + tag_sizes ts; + + sr.skip_space(); + ts.num_ints = sr_parseIntNoSkip(sr); + sr_skipTagField(sr); + if (!sr.eof() && sr.peek() == '/') { + sr.advance(1); + sr.skip_space(); + ts.num_reals = sr_parseIntNoSkip(sr); + sr_skipTagField(sr); + if (!sr.eof() && sr.peek() == '/') { + sr.advance(1); + ts.num_strings = sr_parseInt(sr); + } + } + return ts; +} + +static inline int sr_parseIntNoSkip(StreamReader &sr) { + const char *start = sr.current_ptr(); + size_t rem = sr.remaining(); + size_t len = 0; + if (len < rem && (start[len] == '+' || start[len] == '-')) len++; + while (len < rem && start[len] >= '0' && start[len] <= '9') len++; + int i = 0; + if (len > 0) { + char tmp[64]; + size_t copy_len = len < 63 ? len : 63; + if (copy_len != len) { + sr.advance(len); + return 0; + } + memcpy(tmp, start, copy_len); + tmp[copy_len] = '\0'; + errno = 0; + char *endptr = NULL; + long val = strtol(tmp, &endptr, 10); + if (errno == 0 && endptr != tmp && *endptr == '\0' && + val <= (std::numeric_limits::max)() && + val >= (std::numeric_limits::min)()) { + i = static_cast(val); + } + } + sr.advance(len); + return i; +} + +static inline void sr_skipUntil(StreamReader &sr, const char *delims) { + while (!sr.eof()) { + char c = sr.peek(); + for (const char *d = delims; *d; d++) { + if (c == *d) return; + } + sr.advance(1); + } +} + +static bool sr_parseTriple(StreamReader &sr, int vsize, int vnsize, int vtsize, + vertex_index_t *ret, const warning_context &context) { + if (!ret) return false; + + vertex_index_t vi(-1); + + sr.skip_space(); + if (!fixIndex(sr_parseIntNoSkip(sr), vsize, &vi.v_idx, false, context)) { + return false; + } + + sr_skipUntil(sr, "/ \t\r\n"); + if (sr.eof() || sr.peek() != '/') { + (*ret) = vi; + return true; + } + sr.advance(1); + + // i//k + if (!sr.eof() && sr.peek() == '/') { + sr.advance(1); + if (!fixIndex(sr_parseIntNoSkip(sr), vnsize, &vi.vn_idx, true, context)) { + return false; + } + sr_skipUntil(sr, "/ \t\r\n"); + (*ret) = vi; + return true; + } + + // i/j/k or i/j + if (!fixIndex(sr_parseIntNoSkip(sr), vtsize, &vi.vt_idx, true, context)) { + return false; + } + + sr_skipUntil(sr, "/ \t\r\n"); + if (sr.eof() || sr.peek() != '/') { + (*ret) = vi; + return true; + } + + // i/j/k + sr.advance(1); + if (!fixIndex(sr_parseIntNoSkip(sr), vnsize, &vi.vn_idx, true, context)) { + return false; + } + sr_skipUntil(sr, "/ \t\r\n"); + + (*ret) = vi; + return true; +} + +static vertex_index_t sr_parseRawTriple(StreamReader &sr) { + vertex_index_t vi(static_cast(0)); + + sr.skip_space(); + vi.v_idx = sr_parseIntNoSkip(sr); + sr_skipUntil(sr, "/ \t\r\n"); + if (sr.eof() || sr.peek() != '/') return vi; + sr.advance(1); + + // i//k + if (!sr.eof() && sr.peek() == '/') { + sr.advance(1); + vi.vn_idx = sr_parseIntNoSkip(sr); + sr_skipUntil(sr, "/ \t\r\n"); + return vi; + } + + // i/j/k or i/j + vi.vt_idx = sr_parseIntNoSkip(sr); + sr_skipUntil(sr, "/ \t\r\n"); + if (sr.eof() || sr.peek() != '/') return vi; + + sr.advance(1); + vi.vn_idx = sr_parseIntNoSkip(sr); + sr_skipUntil(sr, "/ \t\r\n"); return vi; } @@ -1550,7 +2349,9 @@ inline real_t GetLength(TinyObjPoint &e) { } inline TinyObjPoint Normalize(TinyObjPoint e) { - real_t inv_length = real_t(1) / GetLength(e); + real_t len = GetLength(e); + if (len <= real_t(0)) return TinyObjPoint(real_t(0), real_t(0), real_t(0)); + real_t inv_length = real_t(1) / len; return TinyObjPoint(e.x * inv_length, e.y * inv_length, e.z * inv_length); } @@ -2117,8 +2918,13 @@ static void SplitString(const std::string &s, char delim, char escape, if (escaping) { escaping = false; } else if (ch == escape) { - escaping = true; - continue; + if ((i + 1) < s.size()) { + const char next = s[i + 1]; + if ((next == delim) || (next == escape)) { + escaping = true; + continue; + } + } } else if (ch == delim) { if (!token.empty()) { elems.push_back(token); @@ -2132,6 +2938,20 @@ static void SplitString(const std::string &s, char delim, char escape, elems.push_back(token); } +static void RemoveEmptyTokens(std::vector *tokens) { + if (!tokens) return; + + const std::vector &src = *tokens; + std::vector filtered; + filtered.reserve(src.size()); + for (size_t i = 0; i < src.size(); i++) { + if (!src[i].empty()) { + filtered.push_back(src[i]); + } + } + tokens->swap(filtered); +} + static std::string JoinPath(const std::string &dir, const std::string &filename) { if (dir.empty()) { @@ -2147,12 +2967,18 @@ static std::string JoinPath(const std::string &dir, } } -void LoadMtl(std::map *material_map, - std::vector *materials, std::istream *inStream, - std::string *warning, std::string *err) { - (void)err; +static bool LoadMtlInternal(std::map *material_map, + std::vector *materials, + StreamReader &sr, + std::string *warning, std::string *err, + const std::string &filename = "") { + if (sr.has_errors()) { + if (err) { + (*err) += sr.get_errors(); + } + return false; + } - // Create a default material anyway. material_t material; InitMaterial(&material); @@ -2166,46 +2992,23 @@ void LoadMtl(std::map *material_map, std::stringstream warn_ss; - size_t line_no = 0; - std::string linebuf; - while (inStream->peek() != -1) { - safeGetline(*inStream, linebuf); - line_no++; - - // Trim trailing whitespace. - if (linebuf.size() > 0) { - linebuf = linebuf.substr(0, linebuf.find_last_not_of(" \t") + 1); - } - - // Trim newline '\r\n' or '\n' - if (linebuf.size() > 0) { - if (linebuf[linebuf.size() - 1] == '\n') - linebuf.erase(linebuf.size() - 1); - } - if (linebuf.size() > 0) { - if (linebuf[linebuf.size() - 1] == '\r') - linebuf.erase(linebuf.size() - 1); - } - - // Skip if empty line. - if (linebuf.empty()) { - continue; - } - if (line_no == 1) { - linebuf = removeUtf8Bom(linebuf); - } - - // Skip leading space. - const char *token = linebuf.c_str(); - token += strspn(token, " \t"); + // Handle BOM + if (sr.remaining() >= 3 && + static_cast(sr.peek()) == 0xEF && + static_cast(sr.peek_at(1)) == 0xBB && + static_cast(sr.peek_at(2)) == 0xBF) { + sr.advance(3); + } - assert(token); - if (token[0] == '\0') continue; // empty line + while (!sr.eof()) { + sr.skip_space(); + if (sr.at_line_end()) { sr.skip_line(); continue; } + if (sr.peek() == '#') { sr.skip_line(); continue; } - if (token[0] == '#') continue; // comment line + size_t line_num = sr.line_num(); // new mtl - if ((0 == strncmp(token, "newmtl", 6)) && IS_SPACE((token[6]))) { + if (sr.match("newmtl", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { // flush previous material. if (!material.name.empty()) { material_map->insert(std::pair( @@ -2213,18 +3016,15 @@ void LoadMtl(std::map *material_map, materials->push_back(material); } - // initial temporary material InitMaterial(&material); has_d = false; has_tr = false; has_kd = false; - // set new mtl name - token += 7; + sr.advance(7); { - std::string namebuf = parseString(&token); - // TODO: empty name check? + std::string namebuf = sr_parseString(sr); if (namebuf.empty()) { if (warning) { (*warning) += "empty material name in `newmtl`\n"; @@ -2232,313 +3032,361 @@ void LoadMtl(std::map *material_map, } material.name = namebuf; } + sr.skip_line(); continue; } // ambient - if (token[0] == 'K' && token[1] == 'a' && IS_SPACE((token[2]))) { - token += 2; + if (sr.peek() == 'K' && sr.peek_at(1) == 'a' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); real_t r, g, b; - parseReal3(&r, &g, &b, &token); + if (!sr_parseReal3(&r, &g, &b, sr, err, filename)) return false; material.ambient[0] = r; material.ambient[1] = g; material.ambient[2] = b; + sr.skip_line(); continue; } // diffuse - if (token[0] == 'K' && token[1] == 'd' && IS_SPACE((token[2]))) { - token += 2; + if (sr.peek() == 'K' && sr.peek_at(1) == 'd' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); real_t r, g, b; - parseReal3(&r, &g, &b, &token); + if (!sr_parseReal3(&r, &g, &b, sr, err, filename)) return false; material.diffuse[0] = r; material.diffuse[1] = g; material.diffuse[2] = b; has_kd = true; + sr.skip_line(); continue; } // specular - if (token[0] == 'K' && token[1] == 's' && IS_SPACE((token[2]))) { - token += 2; + if (sr.peek() == 'K' && sr.peek_at(1) == 's' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); real_t r, g, b; - parseReal3(&r, &g, &b, &token); + if (!sr_parseReal3(&r, &g, &b, sr, err, filename)) return false; material.specular[0] = r; material.specular[1] = g; material.specular[2] = b; + sr.skip_line(); continue; } // transmittance - if ((token[0] == 'K' && token[1] == 't' && IS_SPACE((token[2]))) || - (token[0] == 'T' && token[1] == 'f' && IS_SPACE((token[2])))) { - token += 2; + if ((sr.peek() == 'K' && sr.peek_at(1) == 't' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) || + (sr.peek() == 'T' && sr.peek_at(1) == 'f' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t'))) { + sr.advance(2); real_t r, g, b; - parseReal3(&r, &g, &b, &token); + if (!sr_parseReal3(&r, &g, &b, sr, err, filename)) return false; material.transmittance[0] = r; material.transmittance[1] = g; material.transmittance[2] = b; + sr.skip_line(); continue; } // ior(index of refraction) - if (token[0] == 'N' && token[1] == 'i' && IS_SPACE((token[2]))) { - token += 2; - material.ior = parseReal(&token); + if (sr.peek() == 'N' && sr.peek_at(1) == 'i' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); + if (!sr_parseReal(sr, &material.ior, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // emission - if (token[0] == 'K' && token[1] == 'e' && IS_SPACE(token[2])) { - token += 2; + if (sr.peek() == 'K' && sr.peek_at(1) == 'e' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); real_t r, g, b; - parseReal3(&r, &g, &b, &token); + if (!sr_parseReal3(&r, &g, &b, sr, err, filename)) return false; material.emission[0] = r; material.emission[1] = g; material.emission[2] = b; + sr.skip_line(); continue; } // shininess - if (token[0] == 'N' && token[1] == 's' && IS_SPACE(token[2])) { - token += 2; - material.shininess = parseReal(&token); + if (sr.peek() == 'N' && sr.peek_at(1) == 's' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); + if (!sr_parseReal(sr, &material.shininess, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // illum model - if (0 == strncmp(token, "illum", 5) && IS_SPACE(token[5])) { - token += 6; - material.illum = parseInt(&token); + if (sr.match("illum", 5) && (sr.peek_at(5) == ' ' || sr.peek_at(5) == '\t')) { + sr.advance(6); + if (!sr_parseInt(sr, &material.illum, err, filename)) return false; + sr.skip_line(); continue; } // dissolve - if ((token[0] == 'd' && IS_SPACE(token[1]))) { - token += 1; - material.dissolve = parseReal(&token); + if (sr.peek() == 'd' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(1); + if (!sr_parseReal(sr, &material.dissolve, 0.0, err, filename)) return false; if (has_tr) { warn_ss << "Both `d` and `Tr` parameters defined for \"" << material.name - << "\". Use the value of `d` for dissolve (line " << line_no + << "\". Use the value of `d` for dissolve (line " << line_num << " in .mtl.)\n"; } has_d = true; + sr.skip_line(); continue; } - if (token[0] == 'T' && token[1] == 'r' && IS_SPACE(token[2])) { - token += 2; + if (sr.peek() == 'T' && sr.peek_at(1) == 'r' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); if (has_d) { - // `d` wins. Ignore `Tr` value. warn_ss << "Both `d` and `Tr` parameters defined for \"" << material.name - << "\". Use the value of `d` for dissolve (line " << line_no + << "\". Use the value of `d` for dissolve (line " << line_num << " in .mtl.)\n"; } else { - // We invert value of Tr(assume Tr is in range [0, 1]) - // NOTE: Interpretation of Tr is application(exporter) dependent. For - // some application(e.g. 3ds max obj exporter), Tr = d(Issue 43) - material.dissolve = static_cast(1.0) - parseReal(&token); + real_t tr_val; + if (!sr_parseReal(sr, &tr_val, 0.0, err, filename)) return false; + material.dissolve = static_cast(1.0) - tr_val; } has_tr = true; + sr.skip_line(); continue; } // PBR: roughness - if (token[0] == 'P' && token[1] == 'r' && IS_SPACE(token[2])) { - token += 2; - material.roughness = parseReal(&token); + if (sr.peek() == 'P' && sr.peek_at(1) == 'r' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); + if (!sr_parseReal(sr, &material.roughness, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // PBR: metallic - if (token[0] == 'P' && token[1] == 'm' && IS_SPACE(token[2])) { - token += 2; - material.metallic = parseReal(&token); + if (sr.peek() == 'P' && sr.peek_at(1) == 'm' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); + if (!sr_parseReal(sr, &material.metallic, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // PBR: sheen - if (token[0] == 'P' && token[1] == 's' && IS_SPACE(token[2])) { - token += 2; - material.sheen = parseReal(&token); + if (sr.peek() == 'P' && sr.peek_at(1) == 's' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); + if (!sr_parseReal(sr, &material.sheen, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // PBR: clearcoat thickness - if (token[0] == 'P' && token[1] == 'c' && IS_SPACE(token[2])) { - token += 2; - material.clearcoat_thickness = parseReal(&token); + if (sr.peek() == 'P' && sr.peek_at(1) == 'c' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(2); + if (!sr_parseReal(sr, &material.clearcoat_thickness, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // PBR: clearcoat roughness - if ((0 == strncmp(token, "Pcr", 3)) && IS_SPACE(token[3])) { - token += 4; - material.clearcoat_roughness = parseReal(&token); + if (sr.match("Pcr", 3) && (sr.peek_at(3) == ' ' || sr.peek_at(3) == '\t')) { + sr.advance(4); + if (!sr_parseReal(sr, &material.clearcoat_roughness, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // PBR: anisotropy - if ((0 == strncmp(token, "aniso", 5)) && IS_SPACE(token[5])) { - token += 6; - material.anisotropy = parseReal(&token); + if (sr.match("aniso", 5) && (sr.peek_at(5) == ' ' || sr.peek_at(5) == '\t')) { + sr.advance(6); + if (!sr_parseReal(sr, &material.anisotropy, 0.0, err, filename)) return false; + sr.skip_line(); continue; } // PBR: anisotropy rotation - if ((0 == strncmp(token, "anisor", 6)) && IS_SPACE(token[6])) { - token += 7; - material.anisotropy_rotation = parseReal(&token); + if (sr.match("anisor", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + if (!sr_parseReal(sr, &material.anisotropy_rotation, 0.0, err, filename)) return false; + sr.skip_line(); continue; } + // For texture directives, read rest of line and delegate to + // ParseTextureNameAndOption (which uses the old const char* parse functions). + // ambient or ambient occlusion texture - if ((0 == strncmp(token, "map_Ka", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Ka", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.ambient_texname), - &(material.ambient_texopt), token); + &(material.ambient_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // diffuse texture - if ((0 == strncmp(token, "map_Kd", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Kd", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.diffuse_texname), - &(material.diffuse_texopt), token); - - // Set a decent diffuse default value if a diffuse texture is specified - // without a matching Kd value. + &(material.diffuse_texopt), line_rest.c_str()); if (!has_kd) { material.diffuse[0] = static_cast(0.6); material.diffuse[1] = static_cast(0.6); material.diffuse[2] = static_cast(0.6); } - + sr.skip_line(); continue; } // specular texture - if ((0 == strncmp(token, "map_Ks", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Ks", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.specular_texname), - &(material.specular_texopt), token); + &(material.specular_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // specular highlight texture - if ((0 == strncmp(token, "map_Ns", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Ns", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.specular_highlight_texname), - &(material.specular_highlight_texopt), token); + &(material.specular_highlight_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // bump texture - if (((0 == strncmp(token, "map_bump", 8)) || - (0 == strncmp(token, "map_Bump", 8))) && - IS_SPACE(token[8])) { - token += 9; + if ((sr.match("map_bump", 8) || sr.match("map_Bump", 8)) && + (sr.peek_at(8) == ' ' || sr.peek_at(8) == '\t')) { + sr.advance(9); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.bump_texname), - &(material.bump_texopt), token); + &(material.bump_texopt), line_rest.c_str()); + sr.skip_line(); continue; } - // bump texture - if ((0 == strncmp(token, "bump", 4)) && IS_SPACE(token[4])) { - token += 5; + // bump texture (short form) + if (sr.match("bump", 4) && (sr.peek_at(4) == ' ' || sr.peek_at(4) == '\t')) { + sr.advance(5); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.bump_texname), - &(material.bump_texopt), token); + &(material.bump_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // alpha texture - if ((0 == strncmp(token, "map_d", 5)) && IS_SPACE(token[5])) { - token += 6; + if (sr.match("map_d", 5) && (sr.peek_at(5) == ' ' || sr.peek_at(5) == '\t')) { + sr.advance(6); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.alpha_texname), - &(material.alpha_texopt), token); + &(material.alpha_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // displacement texture - if (((0 == strncmp(token, "map_disp", 8)) || - (0 == strncmp(token, "map_Disp", 8))) && - IS_SPACE(token[8])) { - token += 9; + if ((sr.match("map_disp", 8) || sr.match("map_Disp", 8)) && + (sr.peek_at(8) == ' ' || sr.peek_at(8) == '\t')) { + sr.advance(9); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.displacement_texname), - &(material.displacement_texopt), token); + &(material.displacement_texopt), line_rest.c_str()); + sr.skip_line(); continue; } - // displacement texture - if ((0 == strncmp(token, "disp", 4)) && IS_SPACE(token[4])) { - token += 5; + // displacement texture (short form) + if (sr.match("disp", 4) && (sr.peek_at(4) == ' ' || sr.peek_at(4) == '\t')) { + sr.advance(5); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.displacement_texname), - &(material.displacement_texopt), token); + &(material.displacement_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // reflection map - if ((0 == strncmp(token, "refl", 4)) && IS_SPACE(token[4])) { - token += 5; + if (sr.match("refl", 4) && (sr.peek_at(4) == ' ' || sr.peek_at(4) == '\t')) { + sr.advance(5); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.reflection_texname), - &(material.reflection_texopt), token); + &(material.reflection_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // PBR: roughness texture - if ((0 == strncmp(token, "map_Pr", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Pr", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.roughness_texname), - &(material.roughness_texopt), token); + &(material.roughness_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // PBR: metallic texture - if ((0 == strncmp(token, "map_Pm", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Pm", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.metallic_texname), - &(material.metallic_texopt), token); + &(material.metallic_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // PBR: sheen texture - if ((0 == strncmp(token, "map_Ps", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Ps", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.sheen_texname), - &(material.sheen_texopt), token); + &(material.sheen_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // PBR: emissive texture - if ((0 == strncmp(token, "map_Ke", 6)) && IS_SPACE(token[6])) { - token += 7; + if (sr.match("map_Ke", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.emissive_texname), - &(material.emissive_texopt), token); + &(material.emissive_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // PBR: normal map texture - if ((0 == strncmp(token, "norm", 4)) && IS_SPACE(token[4])) { - token += 5; + if (sr.match("norm", 4) && (sr.peek_at(4) == ' ' || sr.peek_at(4) == '\t')) { + sr.advance(5); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); ParseTextureNameAndOption(&(material.normal_texname), - &(material.normal_texopt), token); + &(material.normal_texopt), line_rest.c_str()); + sr.skip_line(); continue; } // unknown parameter - const char *_space = strchr(token, ' '); - if (!_space) { - _space = strchr(token, '\t'); - } - if (_space) { - std::ptrdiff_t len = _space - token; - std::string key(token, static_cast(len)); - std::string value = _space + 1; - material.unknown_parameter.insert( - std::pair(key, value)); + { + std::string line_rest = trimTrailingWhitespace(sr.read_line()); + const char *_lp = line_rest.c_str(); + const char *_space = strchr(_lp, ' '); + if (!_space) { + _space = strchr(_lp, '\t'); + } + if (_space) { + std::ptrdiff_t len = _space - _lp; + std::string key(_lp, static_cast(len)); + std::string value = _space + 1; + material.unknown_parameter.insert( + std::pair(key, value)); + } } + sr.skip_line(); } // flush last material. material_map->insert(std::pair( @@ -2548,8 +3396,18 @@ void LoadMtl(std::map *material_map, if (warning) { (*warning) = warn_ss.str(); } + + return true; +} + +void LoadMtl(std::map *material_map, + std::vector *materials, std::istream *inStream, + std::string *warning, std::string *err) { + StreamReader sr(*inStream); + LoadMtlInternal(material_map, materials, sr, warning, err); } + bool MaterialFileReader::operator()(const std::string &matId, std::vector *materials, std::map *matMap, @@ -2573,16 +3431,34 @@ bool MaterialFileReader::operator()(const std::string &matId, for (size_t i = 0; i < paths.size(); i++) { std::string filepath = JoinPath(paths[i], matId); +#ifdef TINYOBJLOADER_USE_MMAP + { + MappedFile mf; + if (!mf.open(filepath.c_str())) continue; + if (mf.size > TINYOBJLOADER_STREAM_READER_MAX_BYTES) { + if (err) { + std::stringstream ss; + ss << "input stream too large (" << mf.size + << " bytes exceeds limit " + << TINYOBJLOADER_STREAM_READER_MAX_BYTES << " bytes)\n"; + (*err) += ss.str(); + } + return false; + } + StreamReader sr(mf.data, mf.size); + return LoadMtlInternal(matMap, materials, sr, warn, err, filepath); + } +#else // !TINYOBJLOADER_USE_MMAP #ifdef _WIN32 std::ifstream matIStream(LongPathW(UTF8ToWchar(filepath)).c_str()); #else std::ifstream matIStream(filepath.c_str()); #endif if (matIStream) { - LoadMtl(matMap, materials, &matIStream, warn, err); - - return true; + StreamReader mtl_sr(matIStream); + return LoadMtlInternal(matMap, materials, mtl_sr, warn, err, filepath); } +#endif // TINYOBJLOADER_USE_MMAP } std::stringstream ss; @@ -2595,16 +3471,36 @@ bool MaterialFileReader::operator()(const std::string &matId, } else { std::string filepath = matId; + +#ifdef TINYOBJLOADER_USE_MMAP + { + MappedFile mf; + if (mf.open(filepath.c_str())) { + if (mf.size > TINYOBJLOADER_STREAM_READER_MAX_BYTES) { + if (err) { + std::stringstream ss; + ss << "input stream too large (" << mf.size + << " bytes exceeds limit " + << TINYOBJLOADER_STREAM_READER_MAX_BYTES << " bytes)\n"; + (*err) += ss.str(); + } + return false; + } + StreamReader sr(mf.data, mf.size); + return LoadMtlInternal(matMap, materials, sr, warn, err, filepath); + } + } +#else // !TINYOBJLOADER_USE_MMAP #ifdef _WIN32 std::ifstream matIStream(LongPathW(UTF8ToWchar(filepath)).c_str()); #else std::ifstream matIStream(filepath.c_str()); #endif if (matIStream) { - LoadMtl(matMap, materials, &matIStream, warn, err); - - return true; + StreamReader mtl_sr(matIStream); + return LoadMtlInternal(matMap, materials, mtl_sr, warn, err, filepath); } +#endif // TINYOBJLOADER_USE_MMAP std::stringstream ss; ss << "Material file [ " << filepath @@ -2621,7 +3517,6 @@ bool MaterialStreamReader::operator()(const std::string &matId, std::vector *materials, std::map *matMap, std::string *warn, std::string *err) { - (void)err; (void)matId; if (!m_inStream) { std::stringstream ss; @@ -2632,65 +3527,31 @@ bool MaterialStreamReader::operator()(const std::string &matId, return false; } - LoadMtl(matMap, materials, &m_inStream, warn, err); - - return true; + StreamReader mtl_sr(m_inStream); + return LoadMtlInternal(matMap, materials, mtl_sr, warn, err, ""); } -bool LoadObj(attrib_t *attrib, std::vector *shapes, - std::vector *materials, std::string *warn, - std::string *err, const char *filename, const char *mtl_basedir, - bool triangulate, bool default_vcols_fallback) { - attrib->vertices.clear(); - attrib->normals.clear(); - attrib->texcoords.clear(); - attrib->colors.clear(); - shapes->clear(); - - std::stringstream errss; - -#ifdef _WIN32 - std::ifstream ifs(LongPathW(UTF8ToWchar(filename)).c_str()); -#else - std::ifstream ifs(filename); -#endif - if (!ifs) { - errss << "Cannot open file [" << filename << "]\n"; +static bool LoadObjInternal(attrib_t *attrib, std::vector *shapes, + std::vector *materials, + std::string *warn, std::string *err, + StreamReader &sr, + MaterialReader *readMatFn, bool triangulate, + bool default_vcols_fallback, + const std::string &filename = "") { + if (sr.has_errors()) { if (err) { - (*err) = errss.str(); + (*err) += sr.get_errors(); } return false; } - std::string baseDir = mtl_basedir ? mtl_basedir : ""; - if (!baseDir.empty()) { -#ifndef _WIN32 - const char dirsep = '/'; -#else - const char dirsep = '\\'; -#endif - if (baseDir[baseDir.length() - 1] != dirsep) baseDir += dirsep; - } - MaterialFileReader matFileReader(baseDir); - - return LoadObj(attrib, shapes, materials, warn, err, &ifs, &matFileReader, - triangulate, default_vcols_fallback); -} - -bool LoadObj(attrib_t *attrib, std::vector *shapes, - std::vector *materials, std::string *warn, - std::string *err, std::istream *inStream, - MaterialReader *readMatFn /*= NULL*/, bool triangulate, - bool default_vcols_fallback) { - std::stringstream errss; - std::vector v; - std::vector vertex_weights; // optional [w] component in `v` + std::vector vertex_weights; std::vector vn; std::vector vt; std::vector vt_w; // optional [w] component in `vt` std::vector vc; - std::vector vw; // tinyobj extension: vertex skin weights + std::vector vw; std::vector tags; PrimGroup prim_group; std::string name; @@ -2700,9 +3561,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, std::map material_map; int material = -1; - // smoothing group id - unsigned int current_smoothing_id = - 0; // Initial value. 0 means no smoothing. + unsigned int current_smoothing_id = 0; int greatest_v_idx = -1; int greatest_vn_idx = -1; @@ -2710,57 +3569,42 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, shape_t shape; - bool found_all_colors = true; // check if all 'v' line has color info - - size_t line_num = 0; - std::string linebuf; - while (inStream->peek() != -1) { - safeGetline(*inStream, linebuf); - - line_num++; - - // Trim newline '\r\n' or '\n' - if (linebuf.size() > 0) { - if (linebuf[linebuf.size() - 1] == '\n') - linebuf.erase(linebuf.size() - 1); - } - if (linebuf.size() > 0) { - if (linebuf[linebuf.size() - 1] == '\r') - linebuf.erase(linebuf.size() - 1); - } + bool found_all_colors = true; - // Skip if empty line. - if (linebuf.empty()) { - continue; - } - if (line_num == 1) { - linebuf = removeUtf8Bom(linebuf); - } + // Handle BOM + if (sr.remaining() >= 3 && + static_cast(sr.peek()) == 0xEF && + static_cast(sr.peek_at(1)) == 0xBB && + static_cast(sr.peek_at(2)) == 0xBF) { + sr.advance(3); + } - // Skip leading space. - const char *token = linebuf.c_str(); - token += strspn(token, " \t"); + warning_context context; + context.warn = warn; + context.filename = filename; - assert(token); - if (token[0] == '\0') continue; // empty line + while (!sr.eof()) { + sr.skip_space(); + if (sr.at_line_end()) { sr.skip_line(); continue; } + if (sr.peek() == '#') { sr.skip_line(); continue; } - if (token[0] == '#') continue; // comment line + size_t line_num = sr.line_num(); // vertex - if (token[0] == 'v' && IS_SPACE((token[1]))) { - token += 2; + if (sr.peek() == 'v' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(2); real_t x, y, z; real_t r, g, b; - int num_components = parseVertexWithColor(&x, &y, &z, &r, &g, &b, &token); + int num_components = sr_parseVertexWithColor(&x, &y, &z, &r, &g, &b, sr, err, filename); + if (num_components < 0) return false; found_all_colors &= (num_components == 6); v.push_back(x); v.push_back(y); v.push_back(z); - vertex_weights.push_back( - r); // r = w, and initialized to 1.0 when `w` component is not found. + vertex_weights.push_back(r); if ((num_components == 6) || default_vcols_fallback) { vc.push_back(r); @@ -2768,169 +3612,163 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, vc.push_back(b); } + sr.skip_line(); continue; } // normal - if (token[0] == 'v' && token[1] == 'n' && IS_SPACE((token[2]))) { - token += 3; + if (sr.peek() == 'v' && sr.peek_at(1) == 'n' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(3); real_t x, y, z; - parseReal3(&x, &y, &z, &token); + if (!sr_parseReal3(&x, &y, &z, sr, err, filename)) return false; vn.push_back(x); vn.push_back(y); vn.push_back(z); + sr.skip_line(); continue; } // texcoord - if (token[0] == 'v' && token[1] == 't' && IS_SPACE((token[2]))) { - token += 3; + if (sr.peek() == 'v' && sr.peek_at(1) == 't' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(3); real_t x, y; - parseReal2(&x, &y, &token); + if (!sr_parseReal2(&x, &y, sr, err, filename)) return false; vt.push_back(x); vt.push_back(y); // Parse optional w component real_t w = static_cast(0.0); - parseReal(&token, &w); + sr_parseReal(sr, &w); vt_w.push_back(w); + sr.skip_line(); continue; } // skin weight. tinyobj extension - if (token[0] == 'v' && token[1] == 'w' && IS_SPACE((token[2]))) { - token += 3; + if (sr.peek() == 'v' && sr.peek_at(1) == 'w' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(3); - // vw ... - // example: - // vw 0 0 0.25 1 0.25 2 0.5 - - // TODO(syoyo): Add syntax check - int vid = 0; - vid = parseInt(&token); + int vid; + if (!sr_parseInt(sr, &vid, err, filename)) return false; skin_weight_t sw; - sw.vertex_id = vid; - while (!IS_NEW_LINE(token[0]) && token[0] != '#') { + size_t vw_loop_max = sr.remaining() + 1; + size_t vw_loop_iter = 0; + while (!sr.at_line_end() && sr.peek() != '#' && + vw_loop_iter < vw_loop_max) { real_t j, w; - // joint_id should not be negative, weight may be negative - // TODO(syoyo): # of elements check - parseReal2(&j, &w, &token, -1.0); + sr_parseReal2(&j, &w, sr, -1.0); if (j < static_cast(0)) { if (err) { - std::stringstream ss; - ss << "Failed parse `vw' line. joint_id is negative. " - "line " - << line_num << ".)\n"; - (*err) += ss.str(); + (*err) += sr.format_error(filename, + "failed to parse `vw' line: joint_id is negative"); } return false; } joint_and_weight_t jw; - jw.joint_id = int(j); jw.weight = w; sw.weightValues.push_back(jw); - - size_t n = strspn(token, " \t\r"); - token += n; + sr.skip_space_and_cr(); + vw_loop_iter++; } vw.push_back(sw); + sr.skip_line(); + continue; } - warning_context context; - context.warn = warn; context.line_number = line_num; // line - if (token[0] == 'l' && IS_SPACE((token[1]))) { - token += 2; + if (sr.peek() == 'l' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(2); __line_t line; - while (!IS_NEW_LINE(token[0]) && token[0] != '#') { + size_t l_loop_max = sr.remaining() + 1; + size_t l_loop_iter = 0; + while (!sr.at_line_end() && sr.peek() != '#' && + l_loop_iter < l_loop_max) { vertex_index_t vi; - if (!parseTriple(&token, static_cast(v.size() / 3), + if (!sr_parseTriple(sr, static_cast(v.size() / 3), static_cast(vn.size() / 3), static_cast(vt.size() / 2), &vi, context)) { if (err) { - (*err) += - "Failed to parse `l' line (e.g. a zero value for vertex index. " - "Line " + - toString(line_num) + ").\n"; + (*err) += sr.format_error(filename, + "failed to parse `l' line (invalid vertex index)"); } return false; } line.vertex_indices.push_back(vi); - - size_t n = strspn(token, " \t\r"); - token += n; + sr.skip_space_and_cr(); + l_loop_iter++; } prim_group.lineGroup.push_back(line); - + sr.skip_line(); continue; } // points - if (token[0] == 'p' && IS_SPACE((token[1]))) { - token += 2; + if (sr.peek() == 'p' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(2); __points_t pts; - while (!IS_NEW_LINE(token[0]) && token[0] != '#') { + size_t p_loop_max = sr.remaining() + 1; + size_t p_loop_iter = 0; + while (!sr.at_line_end() && sr.peek() != '#' && + p_loop_iter < p_loop_max) { vertex_index_t vi; - if (!parseTriple(&token, static_cast(v.size() / 3), + if (!sr_parseTriple(sr, static_cast(v.size() / 3), static_cast(vn.size() / 3), static_cast(vt.size() / 2), &vi, context)) { if (err) { - (*err) += - "Failed to parse `p' line (e.g. a zero value for vertex index. " - "Line " + - toString(line_num) + ").\n"; + (*err) += sr.format_error(filename, + "failed to parse `p' line (invalid vertex index)"); } return false; } pts.vertex_indices.push_back(vi); - - size_t n = strspn(token, " \t\r"); - token += n; + sr.skip_space_and_cr(); + p_loop_iter++; } prim_group.pointsGroup.push_back(pts); - + sr.skip_line(); continue; } // face - if (token[0] == 'f' && IS_SPACE((token[1]))) { - token += 2; - token += strspn(token, " \t"); + if (sr.peek() == 'f' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(2); + sr.skip_space(); face_t face; face.smoothing_group_id = current_smoothing_id; face.vertex_indices.reserve(3); - while (!IS_NEW_LINE(token[0]) && token[0] != '#') { + size_t f_loop_max = sr.remaining() + 1; + size_t f_loop_iter = 0; + while (!sr.at_line_end() && sr.peek() != '#' && + f_loop_iter < f_loop_max) { vertex_index_t vi; - if (!parseTriple(&token, static_cast(v.size() / 3), + if (!sr_parseTriple(sr, static_cast(v.size() / 3), static_cast(vn.size() / 3), static_cast(vt.size() / 2), &vi, context)) { if (err) { - (*err) += - "Failed to parse `f' line (e.g. a zero value for vertex index " - "or invalid relative vertex index). Line " + - toString(line_num) + ").\n"; + (*err) += sr.format_error(filename, + "failed to parse `f' line (invalid vertex index)"); } return false; } @@ -2942,20 +3780,19 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, greatest_vt_idx > vi.vt_idx ? greatest_vt_idx : vi.vt_idx; face.vertex_indices.push_back(vi); - size_t n = strspn(token, " \t\r"); - token += n; + sr.skip_space_and_cr(); + f_loop_iter++; } - // replace with emplace_back + std::move on C++11 prim_group.faceGroup.push_back(face); - + sr.skip_line(); continue; } // use mtl - if ((0 == strncmp(token, "usemtl", 6))) { - token += 6; - std::string namebuf = parseString(&token); + if (sr.match("usemtl", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(6); + std::string namebuf = sr_parseString(sr); int newMaterialId = -1; std::map::const_iterator it = @@ -2963,32 +3800,31 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, if (it != material_map.end()) { newMaterialId = it->second; } else { - // { error!! material not found } if (warn) { (*warn) += "material [ '" + namebuf + "' ] not found in .mtl\n"; } } if (newMaterialId != material) { - // Create per-face material. Thus we don't add `shape` to `shapes` at - // this time. - // just clear `faceGroup` after `exportGroupsToShape()` call. exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); prim_group.faceGroup.clear(); material = newMaterialId; } + sr.skip_line(); continue; } // load mtl - if ((0 == strncmp(token, "mtllib", 6)) && IS_SPACE((token[6]))) { + if (sr.match("mtllib", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { if (readMatFn) { - token += 7; + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); std::vector filenames; - SplitString(std::string(token), ' ', '\\', filenames); + SplitString(line_rest, ' ', '\\', filenames); + RemoveEmptyTokens(&filenames); if (filenames.empty()) { if (warn) { @@ -3036,15 +3872,16 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, } } + sr.skip_line(); continue; } // group name - if (token[0] == 'g' && IS_SPACE((token[1]))) { + if (sr.peek() == 'g' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { // flush previous face group. bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); - (void)ret; // return value not used. + (void)ret; if (shape.mesh.indices.size() > 0) { shapes->push_back(shape); @@ -3057,10 +3894,14 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, std::vector names; - while (!IS_NEW_LINE(token[0]) && token[0] != '#') { - std::string str = parseString(&token); + size_t g_loop_max = sr.remaining() + 1; + size_t g_loop_iter = 0; + while (!sr.at_line_end() && sr.peek() != '#' && + g_loop_iter < g_loop_max) { + std::string str = sr_parseString(sr); names.push_back(str); - token += strspn(token, " \t\r"); // skip tag + sr.skip_space_and_cr(); + g_loop_iter++; } // names[0] must be 'g' @@ -3077,10 +3918,6 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, std::stringstream ss; ss << names[1]; - // tinyobjloader does not support multiple groups for a primitive. - // Currently we concatinate multiple group names with a space to get - // single group name. - for (size_t i = 2; i < names.size(); i++) { ss << " " << names[i]; } @@ -3088,15 +3925,16 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, name = ss.str(); } + sr.skip_line(); continue; } // object name - if (token[0] == 'o' && IS_SPACE((token[1]))) { + if (sr.peek() == 'o' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { // flush previous face group. bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); - (void)ret; // return value not used. + (void)ret; if (shape.mesh.indices.size() > 0 || shape.lines.indices.size() > 0 || shape.points.indices.size() > 0) { @@ -3107,24 +3945,23 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, prim_group.clear(); shape = shape_t(); - // @todo { multiple object name? } - token += 2; - std::stringstream ss; - ss << token; - name = ss.str(); + sr.advance(2); + std::string rest = sr.read_line(); + name = rest; + sr.skip_line(); continue; } - if (token[0] == 't' && IS_SPACE(token[1])) { - const int max_tag_nums = 8192; // FIXME(syoyo): Parameterize. + if (sr.peek() == 't' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + const int max_tag_nums = 8192; tag_t tag; - token += 2; + sr.advance(2); - tag.name = parseString(&token); + tag.name = sr_parseString(sr); - tag_sizes ts = parseTagTriple(&token); + tag_sizes ts = sr_parseTagTriple(sr); if (ts.num_ints < 0) { ts.num_ints = 0; @@ -3150,58 +3987,57 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, tag.intValues.resize(static_cast(ts.num_ints)); for (size_t i = 0; i < static_cast(ts.num_ints); ++i) { - tag.intValues[i] = parseInt(&token); + tag.intValues[i] = sr_parseInt(sr); } tag.floatValues.resize(static_cast(ts.num_reals)); for (size_t i = 0; i < static_cast(ts.num_reals); ++i) { - tag.floatValues[i] = parseReal(&token); + tag.floatValues[i] = sr_parseReal(sr); } tag.stringValues.resize(static_cast(ts.num_strings)); for (size_t i = 0; i < static_cast(ts.num_strings); ++i) { - tag.stringValues[i] = parseString(&token); + tag.stringValues[i] = sr_parseString(sr); } tags.push_back(tag); + sr.skip_line(); continue; } - if (token[0] == 's' && IS_SPACE(token[1])) { + if (sr.peek() == 's' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { // smoothing group id - token += 2; + sr.advance(2); + sr.skip_space(); - // skip space. - token += strspn(token, " \t"); // skip space - - if (token[0] == '\0') { + if (sr.at_line_end()) { + sr.skip_line(); continue; } - if (token[0] == '\r' || token[1] == '\n') { + if (sr.peek() == '\r') { + sr.skip_line(); continue; } - if (strlen(token) >= 3 && token[0] == 'o' && token[1] == 'f' && - token[2] == 'f') { + if (sr.remaining() >= 3 && sr.match("off", 3)) { current_smoothing_id = 0; } else { - // assume number - int smGroupId = parseInt(&token); + int smGroupId = sr_parseInt(sr); if (smGroupId < 0) { - // parse error. force set to 0. - // FIXME(syoyo): Report warning. current_smoothing_id = 0; } else { current_smoothing_id = static_cast(smGroupId); } } + sr.skip_line(); continue; - } // smoothing group id + } // Ignore unknown command. + sr.skip_line(); } // not all vertices have colors, no default colors desired? -> clear colors @@ -3212,14 +4048,14 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, if (greatest_v_idx >= static_cast(v.size() / 3)) { if (warn) { std::stringstream ss; - ss << "Vertex indices out of bounds (line " << line_num << ".)\n\n"; + ss << "Vertex indices out of bounds (line " << sr.line_num() << ".)\n\n"; (*warn) += ss.str(); } } if (greatest_vn_idx >= static_cast(vn.size() / 3)) { if (warn) { std::stringstream ss; - ss << "Vertex normal indices out of bounds (line " << line_num + ss << "Vertex normal indices out of bounds (line " << sr.line_num() << ".)\n\n"; (*warn) += ss.str(); } @@ -3227,7 +4063,7 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, if (greatest_vt_idx >= static_cast(vt.size() / 2)) { if (warn) { std::stringstream ss; - ss << "Vertex texcoord indices out of bounds (line " << line_num + ss << "Vertex texcoord indices out of bounds (line " << sr.line_num() << ".)\n\n"; (*warn) += ss.str(); } @@ -3235,19 +4071,10 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, triangulate, v, warn); - // exportGroupsToShape return false when `usemtl` is called in the last - // line. - // we also add `shape` to `shapes` when `shape.mesh` has already some - // faces(indices) - if (ret || shape.mesh.indices - .size()) { // FIXME(syoyo): Support other prims(e.g. lines) + if (ret || shape.mesh.indices.size()) { shapes->push_back(shape); } - prim_group.clear(); // for safety - - if (err) { - (*err) += errss.str(); - } + prim_group.clear(); attrib->vertices.swap(v); attrib->vertex_weights.swap(vertex_weights); @@ -3260,17 +4087,116 @@ bool LoadObj(attrib_t *attrib, std::vector *shapes, return true; } -bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, - void *user_data /*= NULL*/, - MaterialReader *readMatFn /*= NULL*/, - std::string *warn, /* = NULL*/ - std::string *err /*= NULL*/) { - std::stringstream errss; +bool LoadObj(attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, + std::string *err, const char *filename, const char *mtl_basedir, + bool triangulate, bool default_vcols_fallback) { + attrib->vertices.clear(); + attrib->vertex_weights.clear(); + attrib->normals.clear(); + attrib->texcoords.clear(); + attrib->texcoord_ws.clear(); + attrib->colors.clear(); + attrib->skin_weights.clear(); + shapes->clear(); + + std::string baseDir = mtl_basedir ? mtl_basedir : ""; + if (!baseDir.empty()) { +#ifndef _WIN32 + const char dirsep = '/'; +#else + const char dirsep = '\\'; +#endif + if (baseDir[baseDir.length() - 1] != dirsep) baseDir += dirsep; + } + MaterialFileReader matFileReader(baseDir); + +#ifdef TINYOBJLOADER_USE_MMAP + { + MappedFile mf; + if (!mf.open(filename)) { + if (err) { + std::stringstream ss; + ss << "Cannot open file [" << filename << "]\n"; + (*err) = ss.str(); + } + return false; + } + if (mf.size > TINYOBJLOADER_STREAM_READER_MAX_BYTES) { + if (err) { + std::stringstream ss; + ss << "input stream too large (" << mf.size + << " bytes exceeds limit " + << TINYOBJLOADER_STREAM_READER_MAX_BYTES << " bytes)\n"; + (*err) += ss.str(); + } + return false; + } + StreamReader sr(mf.data, mf.size); + return LoadObjInternal(attrib, shapes, materials, warn, err, sr, + &matFileReader, triangulate, default_vcols_fallback, + filename); + } +#else // !TINYOBJLOADER_USE_MMAP +#ifdef _WIN32 + std::ifstream ifs(LongPathW(UTF8ToWchar(filename)).c_str()); +#else + std::ifstream ifs(filename); +#endif + if (!ifs) { + if (err) { + std::stringstream ss; + ss << "Cannot open file [" << filename << "]\n"; + (*err) = ss.str(); + } + return false; + } + { + StreamReader sr(ifs); + return LoadObjInternal(attrib, shapes, materials, warn, err, sr, + &matFileReader, triangulate, default_vcols_fallback, + filename); + } +#endif // TINYOBJLOADER_USE_MMAP +} + +bool LoadObj(attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, + std::string *err, std::istream *inStream, + MaterialReader *readMatFn /*= NULL*/, bool triangulate, + bool default_vcols_fallback) { + attrib->vertices.clear(); + attrib->vertex_weights.clear(); + attrib->normals.clear(); + attrib->texcoords.clear(); + attrib->texcoord_ws.clear(); + attrib->colors.clear(); + attrib->skin_weights.clear(); + shapes->clear(); + + StreamReader sr(*inStream); + return LoadObjInternal(attrib, shapes, materials, warn, err, sr, + readMatFn, triangulate, default_vcols_fallback); +} + + +static bool LoadObjWithCallbackInternal(StreamReader &sr, + const callback_t &callback, + void *user_data, + MaterialReader *readMatFn, + std::string *warn, + std::string *err) { + if (sr.has_errors()) { + if (err) { + (*err) += sr.get_errors(); + } + return false; + } // material std::set material_filenames; std::map material_map; - int material_id = -1; // -1 = invalid + int material_id = -1; std::vector indices; std::vector materials; @@ -3278,87 +4204,72 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, names.reserve(2); std::vector names_out; - std::string linebuf; - size_t line_num = 0; - while (inStream.peek() != -1) { - safeGetline(inStream, linebuf); - - line_num++; - - // Trim newline '\r\n' or '\n' - if (linebuf.size() > 0) { - if (linebuf[linebuf.size() - 1] == '\n') - linebuf.erase(linebuf.size() - 1); - } - if (linebuf.size() > 0) { - if (linebuf[linebuf.size() - 1] == '\r') - linebuf.erase(linebuf.size() - 1); - } - - // Skip if empty line. - if (linebuf.empty()) { - continue; - } - if (line_num == 1) { - linebuf = removeUtf8Bom(linebuf); - } - - // Skip leading space. - const char *token = linebuf.c_str(); - token += strspn(token, " \t"); - - assert(token); - if (token[0] == '\0') continue; // empty line + // Handle BOM + if (sr.remaining() >= 3 && + static_cast(sr.peek()) == 0xEF && + static_cast(sr.peek_at(1)) == 0xBB && + static_cast(sr.peek_at(2)) == 0xBF) { + sr.advance(3); + } - if (token[0] == '#') continue; // comment line + while (!sr.eof()) { + sr.skip_space(); + if (sr.at_line_end()) { sr.skip_line(); continue; } + if (sr.peek() == '#') { sr.skip_line(); continue; } // vertex - if (token[0] == 'v' && IS_SPACE((token[1]))) { - token += 2; + if (sr.peek() == 'v' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(2); real_t x, y, z; real_t r, g, b; - int num_components = parseVertexWithColor(&x, &y, &z, &r, &g, &b, &token); + int num_components = sr_parseVertexWithColor(&x, &y, &z, &r, &g, &b, sr); if (callback.vertex_cb) { - callback.vertex_cb(user_data, x, y, z, r); // r=w is optional + callback.vertex_cb(user_data, x, y, z, r); } if (callback.vertex_color_cb) { bool found_color = (num_components == 6); callback.vertex_color_cb(user_data, x, y, z, r, g, b, found_color); } + sr.skip_line(); continue; } // normal - if (token[0] == 'v' && token[1] == 'n' && IS_SPACE((token[2]))) { - token += 3; + if (sr.peek() == 'v' && sr.peek_at(1) == 'n' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(3); real_t x, y, z; - parseReal3(&x, &y, &z, &token); + sr_parseReal3(&x, &y, &z, sr); if (callback.normal_cb) { callback.normal_cb(user_data, x, y, z); } + sr.skip_line(); continue; } // texcoord - if (token[0] == 'v' && token[1] == 't' && IS_SPACE((token[2]))) { - token += 3; - real_t x, y, z; // y and z are optional. default = 0.0 - parseReal3(&x, &y, &z, &token); + if (sr.peek() == 'v' && sr.peek_at(1) == 't' && (sr.peek_at(2) == ' ' || sr.peek_at(2) == '\t')) { + sr.advance(3); + real_t x, y, z; + sr_parseReal3(&x, &y, &z, sr); if (callback.texcoord_cb) { callback.texcoord_cb(user_data, x, y, z); } + sr.skip_line(); continue; } // face - if (token[0] == 'f' && IS_SPACE((token[1]))) { - token += 2; - token += strspn(token, " \t"); + if (sr.peek() == 'f' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(2); + sr.skip_space(); indices.clear(); - while (!IS_NEW_LINE(token[0]) && token[0] != '#') { - vertex_index_t vi = parseRawTriple(&token); + size_t cf_loop_max = sr.remaining() + 1; + size_t cf_loop_iter = 0; + while (!sr.at_line_end() && sr.peek() != '#' && + cf_loop_iter < cf_loop_max) { + vertex_index_t vi = sr_parseRawTriple(sr); index_t idx; idx.vertex_index = vi.v_idx; @@ -3366,8 +4277,8 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, idx.texcoord_index = vi.vt_idx; indices.push_back(idx); - size_t n = strspn(token, " \t\r"); - token += n; + sr.skip_space_and_cr(); + cf_loop_iter++; } if (callback.index_cb && indices.size() > 0) { @@ -3375,15 +4286,14 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, static_cast(indices.size())); } + sr.skip_line(); continue; } // use mtl - if ((0 == strncmp(token, "usemtl", 6)) && IS_SPACE((token[6]))) { - token += 7; - std::stringstream ss; - ss << token; - std::string namebuf = ss.str(); + if (sr.match("usemtl", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { + sr.advance(6); + std::string namebuf = sr_parseString(sr); int newMaterialId = -1; std::map::const_iterator it = @@ -3391,7 +4301,6 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, if (it != material_map.end()) { newMaterialId = it->second; } else { - // { warn!! material not found } if (warn && (!callback.usemtl_cb)) { (*warn) += "material [ " + namebuf + " ] not found in .mtl\n"; } @@ -3405,16 +4314,19 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, callback.usemtl_cb(user_data, namebuf.c_str(), material_id); } + sr.skip_line(); continue; } // load mtl - if ((0 == strncmp(token, "mtllib", 6)) && IS_SPACE((token[6]))) { + if (sr.match("mtllib", 6) && (sr.peek_at(6) == ' ' || sr.peek_at(6) == '\t')) { if (readMatFn) { - token += 7; + sr.advance(7); + std::string line_rest = trimTrailingWhitespace(sr.read_line()); std::vector filenames; - SplitString(std::string(token), ' ', '\\', filenames); + SplitString(line_rest, ' ', '\\', filenames); + RemoveEmptyTokens(&filenames); if (filenames.empty()) { if (warn) { @@ -3436,7 +4348,7 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, &material_map, &warn_mtl, &err_mtl); if (warn && (!warn_mtl.empty())) { - (*warn) += warn_mtl; // This should be warn message. + (*warn) += warn_mtl; } if (err && (!err_mtl.empty())) { @@ -3457,7 +4369,7 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, "material.\n"; } } else { - if (callback.mtllib_cb) { + if (callback.mtllib_cb && !materials.empty()) { callback.mtllib_cb(user_data, &materials.at(0), static_cast(materials.size())); } @@ -3465,24 +4377,28 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, } } + sr.skip_line(); continue; } // group name - if (token[0] == 'g' && IS_SPACE((token[1]))) { + if (sr.peek() == 'g' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { names.clear(); - while (!IS_NEW_LINE(token[0]) && token[0] != '#') { - std::string str = parseString(&token); + size_t cg_loop_max = sr.remaining() + 1; + size_t cg_loop_iter = 0; + while (!sr.at_line_end() && sr.peek() != '#' && + cg_loop_iter < cg_loop_max) { + std::string str = sr_parseString(sr); names.push_back(str); - token += strspn(token, " \t\r"); // skip tag + sr.skip_space_and_cr(); + cg_loop_iter++; } assert(names.size() > 0); if (callback.group_cb) { if (names.size() > 1) { - // create const char* array. names_out.resize(names.size() - 1); for (size_t j = 0; j < names_out.size(); j++) { names_out[j] = names[j + 1].c_str(); @@ -3495,57 +4411,46 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, } } + sr.skip_line(); continue; } // object name - if (token[0] == 'o' && IS_SPACE((token[1]))) { - // @todo { multiple object name? } - token += 2; - - std::stringstream ss; - ss << token; - std::string object_name = ss.str(); + if (sr.peek() == 'o' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { + sr.advance(2); + std::string object_name = sr.read_line(); if (callback.object_cb) { callback.object_cb(user_data, object_name.c_str()); } + sr.skip_line(); continue; } #if 0 // @todo - if (token[0] == 't' && IS_SPACE(token[1])) { + if (sr.peek() == 't' && (sr.peek_at(1) == ' ' || sr.peek_at(1) == '\t')) { tag_t tag; - token += 2; - std::stringstream ss; - ss << token; - tag.name = ss.str(); - - token += tag.name.size() + 1; + sr.advance(2); + tag.name = sr_parseString(sr); - tag_sizes ts = parseTagTriple(&token); + tag_sizes ts = sr_parseTagTriple(sr); tag.intValues.resize(static_cast(ts.num_ints)); for (size_t i = 0; i < static_cast(ts.num_ints); ++i) { - tag.intValues[i] = atoi(token); - token += strcspn(token, "/ \t\r") + 1; + tag.intValues[i] = sr_parseInt(sr); } tag.floatValues.resize(static_cast(ts.num_reals)); for (size_t i = 0; i < static_cast(ts.num_reals); ++i) { - tag.floatValues[i] = parseReal(&token); - token += strcspn(token, "/ \t\r") + 1; + tag.floatValues[i] = sr_parseReal(sr); } tag.stringValues.resize(static_cast(ts.num_strings)); for (size_t i = 0; i < static_cast(ts.num_strings); ++i) { - std::stringstream ss; - ss << token; - tag.stringValues[i] = ss.str(); - token += tag.stringValues[i].size() + 1; + tag.stringValues[i] = sr_parseString(sr); } tags.push_back(tag); @@ -3553,15 +4458,22 @@ bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, #endif // Ignore unknown command. - } - - if (err) { - (*err) += errss.str(); + sr.skip_line(); } return true; } +bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, + void *user_data /*= NULL*/, + MaterialReader *readMatFn /*= NULL*/, + std::string *warn, /* = NULL*/ + std::string *err /*= NULL*/) { + StreamReader sr(inStream); + return LoadObjWithCallbackInternal(sr, callback, user_data, readMatFn, + warn, err); +} + bool ObjReader::ParseFromFile(const std::string &filename, const ObjReaderConfig &config) { std::string mtl_search_path;