1.3.49
 
Loading...
Searching...
No Matches
Test_OBJ.h
1#pragma once
2// =================================================================================
3// Suite: OBJ File I/O Tests
4//
5// Tests for OBJ file loading and writing functionality in Context class.
6// Includes basic functionality, performance benchmarking, error handling, and
7// correctness validation tests.
8// =================================================================================
9
10#include <chrono>
11#include <fstream>
12
13TEST_CASE("OBJ File Loading - Basic Functionality") {
14 SUBCASE("Load simple triangle") {
15 Context ctx;
16 std::vector<uint> UUIDs;
17
18 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_triangle_simple.obj", true));
19 DOCTEST_CHECK(UUIDs.size() == 1);
20 DOCTEST_CHECK(ctx.getPrimitiveCount() == 1);
21
22 // Verify triangle properties
23 DOCTEST_CHECK(ctx.doesPrimitiveExist(UUIDs[0]));
24 DOCTEST_CHECK(ctx.getPrimitiveType(UUIDs[0]) == PRIMITIVE_TYPE_TRIANGLE);
25 DOCTEST_CHECK(ctx.getPrimitiveArea(UUIDs[0]) > 0.0f);
26
27 // Check vertices are approximately correct
28 std::vector<vec3> vertices = ctx.getPrimitiveVertices(UUIDs[0]);
29 DOCTEST_CHECK(vertices.size() == 3);
30 DOCTEST_CHECK(vertices[0].x == doctest::Approx(0.0f));
31 DOCTEST_CHECK(vertices[0].y == doctest::Approx(0.0f));
32 DOCTEST_CHECK(vertices[0].z == doctest::Approx(0.0f));
33 DOCTEST_CHECK(vertices[1].x == doctest::Approx(1.0f));
34 DOCTEST_CHECK(vertices[1].y == doctest::Approx(0.0f));
35 DOCTEST_CHECK(vertices[1].z == doctest::Approx(0.0f));
36 DOCTEST_CHECK(vertices[2].x == doctest::Approx(0.5f));
37 DOCTEST_CHECK(vertices[2].y == doctest::Approx(1.0f));
38 DOCTEST_CHECK(vertices[2].z == doctest::Approx(0.0f));
39 }
40
41 SUBCASE("Load cube with materials") {
42 Context ctx;
43 std::vector<uint> UUIDs;
44
45 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_cube_medium.obj", true));
46 DOCTEST_CHECK(UUIDs.size() == 12); // Cube has 6 faces, each split into 2 triangles
47 DOCTEST_CHECK(ctx.getPrimitiveCount() == 12);
48
49 // All should be triangles
50 for (uint uuid: UUIDs) {
51 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
52 DOCTEST_CHECK(ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE);
53 DOCTEST_CHECK(ctx.getPrimitiveArea(uuid) > 0.0f);
54 }
55
56 // Total surface area should be approximately 24 (6 faces * 2*2 area per face)
57 float total_area = 0.0f;
58 for (uint uuid: UUIDs) {
59 total_area += ctx.getPrimitiveArea(uuid);
60 }
61 DOCTEST_CHECK(total_area == doctest::Approx(24.0f).epsilon(1e-3));
62 }
63
64 SUBCASE("Load complex large model") {
65 Context ctx;
66 std::vector<uint> UUIDs;
67
68 // Record loading time for performance awareness
69 auto start = std::chrono::high_resolution_clock::now();
70 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", true));
71 auto end = std::chrono::high_resolution_clock::now();
72
73 // Should load successfully
74 DOCTEST_CHECK(UUIDs.size() > 5000); // Should have many triangles from grid + icosphere
75 DOCTEST_CHECK(ctx.getPrimitiveCount() == UUIDs.size());
76
77 // Verify all primitives are valid
78 for (uint uuid: UUIDs) {
79 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
80 DOCTEST_CHECK(ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE);
81 }
82 }
83
84 SUBCASE("Load with transformations") {
85 Context ctx;
86 std::vector<uint> UUIDs;
87
88 vec3 origin = make_vec3(5.0f, 10.0f, 2.0f);
89 float height = 3.0f;
90 SphericalCoord rotation = make_SphericalCoord(M_PI / 4, M_PI / 6);
91 RGBcolor color = RGB::red;
92
93 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_triangle_simple.obj", origin, height, rotation, color, "ZUP", true));
94 DOCTEST_CHECK(UUIDs.size() == 1);
95
96 // Check that triangle has been transformed
97 std::vector<vec3> vertices = ctx.getPrimitiveVertices(UUIDs[0]);
98 DOCTEST_CHECK(vertices.size() == 3);
99
100 // Vertices should not be at original positions due to transformation
101 DOCTEST_CHECK(!(vertices[0].x == 0.0f && vertices[0].y == 0.0f && vertices[0].z == 0.0f));
102
103 // Check color was applied
104 RGBcolor prim_color = ctx.getPrimitiveColor(UUIDs[0]);
105 DOCTEST_CHECK(prim_color.r == doctest::Approx(color.r));
106 DOCTEST_CHECK(prim_color.g == doctest::Approx(color.g));
107 DOCTEST_CHECK(prim_color.b == doctest::Approx(color.b));
108 }
109
110 SUBCASE("Load existing test model") {
111 Context ctx;
112 std::vector<uint> UUIDs;
113
114 // Test loading the existing test OBJ file in lib/models/
115 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/obj_object_test.obj", true));
116 DOCTEST_CHECK(UUIDs.size() > 0);
117 DOCTEST_CHECK(ctx.getPrimitiveCount() > 0);
118
119 // Verify all loaded primitives are valid
120 for (uint uuid: UUIDs) {
121 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
122 DOCTEST_CHECK(ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE);
123 DOCTEST_CHECK(ctx.getPrimitiveArea(uuid) > 0.0f);
124 }
125 }
126}
127
128TEST_CASE("OBJ File Writing - Basic Functionality") {
129 SUBCASE("Write simple geometry and reload") {
130 Context ctx;
131
132 // Create some test geometry
133 uint tri1 = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5, 1, 0), RGB::red);
134 uint tri2 = ctx.addTriangle(make_vec3(2, 0, 0), make_vec3(3, 0, 0), make_vec3(2.5, 1, 0), RGB::blue);
135
136 std::vector<uint> original_uuids = {tri1, tri2};
137
138 // Write to OBJ file
139 std::string output_file = "lib/models/test_output_simple.obj";
140 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), original_uuids, true));
141
142 // Verify file was created
143 std::ifstream file_check(output_file);
144 DOCTEST_CHECK(file_check.good());
145 file_check.close();
146
147 // Load back and compare
148 Context ctx2;
149 std::vector<uint> loaded_uuids;
150 DOCTEST_CHECK_NOTHROW(loaded_uuids = ctx2.loadOBJ(output_file.c_str(), true));
151
152 DOCTEST_CHECK(loaded_uuids.size() == 2);
153 DOCTEST_CHECK(ctx2.getPrimitiveCount() == 2);
154
155 // Check geometry is preserved (approximately)
156 for (size_t i = 0; i < loaded_uuids.size(); i++) {
157 std::vector<vec3> orig_verts = ctx.getPrimitiveVertices(original_uuids[i]);
158 std::vector<vec3> loaded_verts = ctx2.getPrimitiveVertices(loaded_uuids[i]);
159
160 DOCTEST_CHECK(orig_verts.size() == loaded_verts.size());
161 for (size_t j = 0; j < orig_verts.size(); j++) {
162 DOCTEST_CHECK(orig_verts[j].x == doctest::Approx(loaded_verts[j].x));
163 DOCTEST_CHECK(orig_verts[j].y == doctest::Approx(loaded_verts[j].y));
164 DOCTEST_CHECK(orig_verts[j].z == doctest::Approx(loaded_verts[j].z));
165 }
166 }
167
168 // Clean up
169 std::remove(output_file.c_str());
170 std::remove("lib/models/test_output_simple.mtl");
171 }
172
173 SUBCASE("Write all primitives") {
174 Context ctx;
175
176 // Create various primitive types
177 uint tri = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5, 1, 0));
178 uint patch = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1));
179
180 // Write all primitives
181 std::string output_file = "lib/models/test_output_all.obj";
182 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str()));
183
184 // Verify file was created and has content
185 std::ifstream file_check(output_file);
186 DOCTEST_CHECK(file_check.good());
187
188 std::string line;
189 bool has_vertices = false;
190 bool has_faces = false;
191 while (std::getline(file_check, line)) {
192 if (line.substr(0, 2) == "v ")
193 has_vertices = true;
194 if (line.substr(0, 2) == "f ")
195 has_faces = true;
196 }
197 file_check.close();
198
199 DOCTEST_CHECK(has_vertices);
200 DOCTEST_CHECK(has_faces);
201
202 // Clean up
203 std::remove(output_file.c_str());
204 std::remove("lib/models/test_output_all.mtl");
205 }
206}
207
208TEST_CASE("OBJ File I/O - Error Handling and Edge Cases") {
209 SUBCASE("Handle empty OBJ file") {
210 Context ctx;
211 std::vector<uint> UUIDs;
212
213 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_empty.obj", true));
214 DOCTEST_CHECK(UUIDs.empty());
215 DOCTEST_CHECK(ctx.getPrimitiveCount() == 0);
216 }
217
218 SUBCASE("Handle non-existent file") {
219 Context ctx;
220 std::vector<uint> UUIDs;
221
222 DOCTEST_CHECK_THROWS(UUIDs = ctx.loadOBJ("lib/models/does_not_exist.obj", true));
223 }
224
225 SUBCASE("Handle malformed OBJ file") {
226 Context ctx;
227
228 // This should throw an exception for malformed content
229 DOCTEST_CHECK_THROWS(ctx.loadOBJ("lib/models/test_malformed.obj", true));
230 }
231
232 SUBCASE("Handle invalid file extension") {
233 Context ctx;
234 std::vector<uint> UUIDs;
235
236 DOCTEST_CHECK_THROWS(UUIDs = ctx.loadOBJ("lib/models/test_triangle_simple.txt", true));
237 }
238
239 SUBCASE("Write to invalid path") {
240 Context ctx;
241 uint tri = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5, 1, 0));
242
243 // Try to write to invalid path
244 std::vector<uint> test_uuids = {tri};
245 DOCTEST_CHECK_THROWS(ctx.writeOBJ("/invalid/path/test.obj", test_uuids, true));
246 }
247
248 SUBCASE("Write with invalid UUIDs") {
249 Context ctx;
250 std::vector<uint> invalid_uuids = {999, 1000}; // Non-existent UUIDs
251
252 // Should throw exception for invalid UUIDs (fail-fast philosophy)
253 DOCTEST_CHECK_THROWS(ctx.writeOBJ("lib/models/test_invalid_uuid.obj", invalid_uuids, true));
254
255 // Clean up if file was created
256 std::remove("lib/models/test_invalid_uuid.obj");
257 std::remove("lib/models/test_invalid_uuid.mtl");
258 }
259}
260
261TEST_CASE("OBJ File Loading - Texture and UV Coordinate Testing") {
262 SUBCASE("Load textured model with UV coordinates") {
263 Context ctx;
264 std::vector<uint> UUIDs;
265
266 // Load the test model that has texture references in MTL file (not silent for debugging)
267 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", true));
268 DOCTEST_CHECK(UUIDs.size() > 0);
269
270 // Verify that at least some primitives were loaded
271 DOCTEST_CHECK(ctx.getPrimitiveCount() > 0);
272
273 bool found_textured_primitive = false;
274 int primitive_count = 0;
275 int textured_count = 0;
276
277 for (uint uuid: UUIDs) {
278 primitive_count++;
279 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
280
281 // Test primitive type
282 PrimitiveType type = ctx.getPrimitiveType(uuid);
283 DOCTEST_CHECK((type == PRIMITIVE_TYPE_TRIANGLE || type == PRIMITIVE_TYPE_PATCH));
284
285 // Test that vertices are valid
286 std::vector<vec3> vertices = ctx.getPrimitiveVertices(uuid);
287 DOCTEST_CHECK(vertices.size() >= 3);
288
289 // Check if this primitive has texture
290 if (type == PRIMITIVE_TYPE_TRIANGLE) {
291 // Test texture file access
292 std::string texture_file;
293 DOCTEST_CHECK_NOTHROW(texture_file = ctx.getPrimitiveTextureFile(uuid));
294
295 // Count textured primitives
296 if (!texture_file.empty()) {
297 textured_count++;
298 }
299
300 // For textured primitives, verify texture properties
301 if (!texture_file.empty()) {
302 found_textured_primitive = true;
303
304 // Test UV coordinates using public API
305 std::vector<vec2> uv_coords;
306 DOCTEST_CHECK_NOTHROW(uv_coords = ctx.getPrimitiveTextureUV(uuid));
307 DOCTEST_CHECK(uv_coords.size() == 3); // Triangle should have 3 UV coordinates
308
309 // Verify UV coordinates are finite
310 for (const auto &uv: uv_coords) {
311 DOCTEST_CHECK(std::isfinite(uv.x));
312 DOCTEST_CHECK(std::isfinite(uv.y));
313 // UV coordinates are often outside [0,1] for tiling, so just check they're finite
314 }
315
316 // Test texture transparency and color override properties
317 bool has_transparency, color_overridden;
318 DOCTEST_CHECK_NOTHROW(has_transparency = ctx.primitiveTextureHasTransparencyChannel(uuid));
319 DOCTEST_CHECK_NOTHROW(color_overridden = ctx.isPrimitiveTextureColorOverridden(uuid));
320
321 // These should be boolean values (not checking specific values, just that calls work)
322 DOCTEST_CHECK((has_transparency == true || has_transparency == false));
323 DOCTEST_CHECK((color_overridden == true || color_overridden == false));
324 }
325 } else if (type == PRIMITIVE_TYPE_PATCH) {
326 // Test patch texture properties
327 std::string texture_file;
328 DOCTEST_CHECK_NOTHROW(texture_file = ctx.getPrimitiveTextureFile(uuid));
329
330 if (!texture_file.empty()) {
331 textured_count++;
332 }
333
334 if (!texture_file.empty()) {
335 found_textured_primitive = true;
336
337 // Test UV coordinates for patches using public API
338 std::vector<vec2> uv_coords;
339 DOCTEST_CHECK_NOTHROW(uv_coords = ctx.getPrimitiveTextureUV(uuid));
340 DOCTEST_CHECK(uv_coords.size() == 4); // Patch should have 4 UV coordinates
341
342 // Verify UV coordinates are finite
343 for (const auto &uv: uv_coords) {
344 DOCTEST_CHECK(std::isfinite(uv.x));
345 DOCTEST_CHECK(std::isfinite(uv.y));
346 }
347 }
348 }
349
350 // Test color properties (should work for both textured and non-textured primitives)
351 RGBcolor color;
352 DOCTEST_CHECK_NOTHROW(color = ctx.getPrimitiveColor(uuid));
353 DOCTEST_CHECK(std::isfinite(color.r));
354 DOCTEST_CHECK(std::isfinite(color.g));
355 DOCTEST_CHECK(std::isfinite(color.b));
356 }
357
358 // For now, verify that texture methods work even if no textures found
359 // This ensures our OpenMP implementation doesn't break texture API calls
360 }
361
362 SUBCASE("Load model with material properties") {
363 Context ctx;
364 std::vector<uint> UUIDs;
365
366 // Load a model with different materials
367 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_cube_medium.obj", true));
368 DOCTEST_CHECK(UUIDs.size() > 0);
369
370 // Verify materials are applied correctly
371 std::vector<RGBcolor> colors_found;
372
373 for (uint uuid: UUIDs) {
374 RGBcolor color = ctx.getPrimitiveColor(uuid);
375 colors_found.push_back(color);
376
377 // Colors should be valid (split compound expressions for doctest)
378 DOCTEST_CHECK(color.r >= 0.0f);
379 DOCTEST_CHECK(color.r <= 1.0f);
380 DOCTEST_CHECK(color.g >= 0.0f);
381 DOCTEST_CHECK(color.g <= 1.0f);
382 DOCTEST_CHECK(color.b >= 0.0f);
383 DOCTEST_CHECK(color.b <= 1.0f);
384 }
385
386 // Check that we have different materials/colors by comparing first few colors
387 DOCTEST_CHECK(colors_found.size() >= 2);
388 if (colors_found.size() >= 2) {
389 // At least some colors should be different (indicating multiple materials)
390 bool found_different = false;
391 for (size_t i = 1; i < colors_found.size(); i++) {
392 if (colors_found[0].r != colors_found[i].r || colors_found[0].g != colors_found[i].g || colors_found[0].b != colors_found[i].b) {
393 found_different = true;
394 break;
395 }
396 }
397 DOCTEST_CHECK(found_different);
398 }
399 }
400
401 SUBCASE("Texture coordinate consistency with transformations") {
402 Context ctx;
403 std::vector<uint> UUIDs;
404
405 // Load with transformations to test that UV coordinates are preserved
406 vec3 origin = make_vec3(5.0f, 0.0f, 0.0f);
407 float height = 2.0f;
408 SphericalCoord rotation = make_SphericalCoord(M_PI / 4, 0);
409 RGBcolor color = RGB::blue;
410
411 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/obj_object_test.obj", origin, height, rotation, color, "ZUP", true));
412 DOCTEST_CHECK(UUIDs.size() > 0);
413
414 for (uint uuid: UUIDs) {
415 if (ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE) {
416 std::string texture_file = ctx.getPrimitiveTextureFile(uuid);
417
418 if (!texture_file.empty()) {
419 // UV coordinates should still be valid after transformations
420 std::vector<vec2> uv_coords = ctx.getPrimitiveTextureUV(uuid);
421 DOCTEST_CHECK(uv_coords.size() == 3);
422
423 for (const auto &uv: uv_coords) {
424 DOCTEST_CHECK(std::isfinite(uv.x));
425 DOCTEST_CHECK(std::isfinite(uv.y));
426 }
427
428 // Vertices should be transformed but texture coordinates unchanged
429 std::vector<vec3> vertices = ctx.getPrimitiveVertices(uuid);
430 DOCTEST_CHECK(vertices.size() == 3);
431
432 // At least one vertex should be displaced from origin due to transformations
433 bool found_transformed = false;
434 for (const auto &vertex: vertices) {
435 if (vertex.magnitude() > 1.0f) { // Original vertices are within unit range
436 found_transformed = true;
437 break;
438 }
439 }
440 DOCTEST_CHECK(found_transformed);
441 }
442 }
443 }
444 }
445
446 SUBCASE("Missing texture file handling") {
447 Context ctx;
448 std::vector<uint> UUIDs;
449
450 // This should load successfully even though texture.jpg doesn't exist
451 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", true));
452 DOCTEST_CHECK(UUIDs.size() > 0);
453
454 // Primitives should still be created even with missing texture files
455 for (uint uuid: UUIDs) {
456 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
457
458 // Should have valid geometry regardless of missing textures
459 std::vector<vec3> vertices = ctx.getPrimitiveVertices(uuid);
460 DOCTEST_CHECK(vertices.size() >= 3);
461 DOCTEST_CHECK(ctx.getPrimitiveArea(uuid) > 0.0f);
462 }
463 }
464}
465
466TEST_CASE("OBJ File Loading - Error Handling and Edge Cases") {
467 SUBCASE("Invalid face vertex indices") {
468 Context ctx;
469
470 // Create a test file with invalid face indices
471 std::string test_content = "# Test OBJ with invalid face indices\n"
472 "v 0.0 0.0 0.0\n"
473 "v 1.0 0.0 0.0\n"
474 "v 0.5 1.0 0.0\n"
475 "f 1 2 5\n"; // Face references vertex 5 but only 3 vertices exist
476
477 std::string test_file = "lib/models/test_invalid_face.obj";
478 std::ofstream file(test_file);
479 file << test_content;
480 file.close();
481
482 // Should throw helios_runtime_error for invalid face index
483 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(test_file.c_str(), true), std::runtime_error);
484
485 // Clean up
486 std::remove(test_file.c_str());
487 }
488
489 SUBCASE("Invalid texture coordinate indices") {
490 Context ctx;
491
492 // Create a test file with invalid UV indices
493 std::string test_content = "# Test OBJ with invalid UV indices\n"
494 "v 0.0 0.0 0.0\n"
495 "v 1.0 0.0 0.0\n"
496 "v 0.5 1.0 0.0\n"
497 "vt 0.0 0.0\n"
498 "vt 1.0 0.0\n"
499 "f 1/3 2/1 3/2\n"; // UV index 3 doesn't exist (only 2 UVs defined)
500
501 std::string test_file = "lib/models/test_invalid_uv.obj";
502 std::ofstream file(test_file);
503 file << test_content;
504 file.close();
505
506 // Should throw helios_runtime_error for invalid UV index
507 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(test_file.c_str(), true), std::runtime_error);
508
509 // Clean up
510 std::remove(test_file.c_str());
511 }
512
513 SUBCASE("Missing texture file") {
514 Context ctx;
515
516 // Create test OBJ and MTL files where MTL references non-existent texture
517 std::string obj_content = "# Test OBJ with missing texture\n"
518 "mtllib test_missing_texture.mtl\n"
519 "v 0.0 0.0 0.0\n"
520 "v 1.0 0.0 0.0\n"
521 "v 0.5 1.0 0.0\n"
522 "usemtl test_material\n"
523 "f 1 2 3\n";
524
525 std::string mtl_content = "newmtl test_material\n"
526 "Ka 0.2 0.2 0.2\n"
527 "Kd 0.8 0.8 0.8\n"
528 "map_Kd nonexistent_texture.jpg\n"; // This texture file doesn't exist
529
530 std::string obj_file = "lib/models/test_missing_texture.obj";
531 std::string mtl_file = "lib/models/test_missing_texture.mtl";
532
533 std::ofstream obj_f(obj_file);
534 obj_f << obj_content;
535 obj_f.close();
536
537 std::ofstream mtl_f(mtl_file);
538 mtl_f << mtl_content;
539 mtl_f.close();
540
541 // Should throw helios_runtime_error for missing texture file
542 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(obj_file.c_str(), true), std::runtime_error);
543
544 // Clean up
545 std::remove(obj_file.c_str());
546 std::remove(mtl_file.c_str());
547 }
548
549 SUBCASE("Texture without UV coordinates") {
550 Context ctx;
551
552 // Create a dummy texture file
553 std::string texture_file = "lib/models/test_dummy.jpg";
554 std::ofstream tex_f(texture_file);
555 tex_f << "dummy";
556 tex_f.close();
557
558 // Create test files where material has texture but face has no UV coordinates
559 std::string obj_content = "# Test OBJ with texture but no UV coordinates\n"
560 "mtllib test_no_uv.mtl\n"
561 "v 0.0 0.0 0.0\n"
562 "v 1.0 0.0 0.0\n"
563 "v 0.5 1.0 0.0\n"
564 "usemtl textured_material\n"
565 "f 1 2 3\n"; // Face has no UV indices
566
567 std::string mtl_content = "newmtl textured_material\n"
568 "Ka 0.2 0.2 0.2\n"
569 "Kd 0.8 0.8 0.8\n"
570 "map_Kd test_dummy.jpg\n";
571
572 std::string obj_file = "lib/models/test_no_uv.obj";
573 std::string mtl_file = "lib/models/test_no_uv.mtl";
574
575 std::ofstream obj_f(obj_file);
576 obj_f << obj_content;
577 obj_f.close();
578
579 std::ofstream mtl_f(mtl_file);
580 mtl_f << mtl_content;
581 mtl_f.close();
582
583 // Should throw helios_runtime_error for texture without UV coordinates
584 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(obj_file.c_str(), true), std::runtime_error);
585
586 // Clean up
587 std::remove(obj_file.c_str());
588 std::remove(mtl_file.c_str());
589 std::remove(texture_file.c_str());
590 }
591
592 SUBCASE("Zero vertex file") {
593 Context ctx;
594
595 // Create a test file with faces but no vertices
596 std::string test_content = "# Test OBJ with no vertices\n"
597 "f 1 2 3\n"; // Face references vertices but none are defined
598
599 std::string test_file = "lib/models/test_zero_vertex.obj";
600 std::ofstream file(test_file);
601 file << test_content;
602 file.close();
603
604 // Should throw helios_runtime_error for face referencing non-existent vertices
605 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(test_file.c_str(), true), std::runtime_error);
606
607 // Clean up
608 std::remove(test_file.c_str());
609 }
610}
611
612TEST_CASE("OBJ File I/O - Performance Benchmarking") {
613 SUBCASE("Loading performance baseline") {
614 Context ctx;
615 std::vector<uint> UUIDs;
616
617 // Benchmark loading the large complex file
618 auto start = std::chrono::high_resolution_clock::now();
619 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", true));
620 auto end = std::chrono::high_resolution_clock::now();
621
622 auto load_duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
623
624 DOCTEST_CHECK(UUIDs.size() > 0);
625
626 // Store baseline metrics (for future optimization comparison)
627 float triangles_per_second = UUIDs.size() * 1000000.0f / load_duration.count();
628 float load_time_ms = load_duration.count() / 1000.0f;
629
630 // Basic sanity check - should process at least 1000 triangles/second
631 // (This is a very conservative baseline)
632 DOCTEST_CHECK(triangles_per_second > 1000.0f);
633 }
634
635 SUBCASE("Loading performance with transformations") {
636 Context ctx;
637 std::vector<uint> UUIDs;
638
639 // Test performance with transformations (more CPU intensive)
640 vec3 origin = make_vec3(10.0f, 5.0f, 0.0f);
641 float height = 2.0f;
642 SphericalCoord rotation = make_SphericalCoord(M_PI / 6, M_PI / 4);
643 RGBcolor color = RGB::green;
644
645 auto start = std::chrono::high_resolution_clock::now();
646 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", origin, height, rotation, color, "ZUP", true));
647 auto end = std::chrono::high_resolution_clock::now();
648
649 auto load_duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
650
651 DOCTEST_CHECK(UUIDs.size() > 0);
652
653 float triangles_per_second = UUIDs.size() * 1000000.0f / load_duration.count();
654 float load_time_ms = load_duration.count() / 1000.0f;
655
656 // Transformed loading should still maintain reasonable performance
657 DOCTEST_CHECK(triangles_per_second > 800.0f);
658 }
659
660 SUBCASE("Writing performance baseline") {
661 Context ctx;
662
663 // Create a substantial amount of geometry
664 std::vector<uint> uuids;
665 for (int i = 0; i < 1000; i++) {
666 float x = static_cast<float>(i % 10);
667 float z = static_cast<float>(i / 10);
668 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
669 uuids.push_back(tri);
670 }
671
672 // Benchmark writing
673 std::string output_file = "lib/models/test_perf_output.obj";
674 auto start = std::chrono::high_resolution_clock::now();
675 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
676 auto end = std::chrono::high_resolution_clock::now();
677
678 auto write_duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
679
680 // Basic sanity check - should write at least 500 triangles/second
681 float triangles_per_second = uuids.size() * 1000000.0f / write_duration.count();
682 DOCTEST_CHECK(triangles_per_second > 500.0f);
683
684 // Clean up
685 std::remove(output_file.c_str());
686 std::remove("lib/models/test_perf_output.mtl");
687 }
688}
689
690TEST_CASE("OBJ File I/O - Correctness Validation") {
691 SUBCASE("Round-trip geometry preservation") {
692 Context ctx1, ctx2, ctx3;
693
694 // Load original file
695 std::vector<uint> original_uuids;
696 DOCTEST_CHECK_NOTHROW(original_uuids = ctx1.loadOBJ("lib/models/test_cube_medium.obj", true));
697
698 // Write to new file
699 std::string intermediate_file = "lib/models/test_roundtrip.obj";
700 DOCTEST_CHECK_NOTHROW(ctx1.writeOBJ(intermediate_file.c_str(), original_uuids, true));
701
702 // Load the written file
703 std::vector<uint> roundtrip_uuids;
704 DOCTEST_CHECK_NOTHROW(roundtrip_uuids = ctx2.loadOBJ(intermediate_file.c_str(), true));
705
706 // Write again
707 std::string final_file = "lib/models/test_roundtrip2.obj";
708 DOCTEST_CHECK_NOTHROW(ctx2.writeOBJ(final_file.c_str(), roundtrip_uuids, true));
709
710 // Load final file
711 std::vector<uint> final_uuids;
712 DOCTEST_CHECK_NOTHROW(final_uuids = ctx3.loadOBJ(final_file.c_str(), true));
713
714 // All should have same number of triangles
715 DOCTEST_CHECK(original_uuids.size() == roundtrip_uuids.size());
716 DOCTEST_CHECK(roundtrip_uuids.size() == final_uuids.size());
717
718 // Total area should be preserved (approximately)
719 float original_area = 0, roundtrip_area = 0, final_area = 0;
720
721 for (uint uuid: original_uuids) {
722 original_area += ctx1.getPrimitiveArea(uuid);
723 }
724 for (uint uuid: roundtrip_uuids) {
725 roundtrip_area += ctx2.getPrimitiveArea(uuid);
726 }
727 for (uint uuid: final_uuids) {
728 final_area += ctx3.getPrimitiveArea(uuid);
729 }
730
731 DOCTEST_CHECK(original_area == doctest::Approx(roundtrip_area).epsilon(1e-4));
732 DOCTEST_CHECK(roundtrip_area == doctest::Approx(final_area).epsilon(1e-4));
733
734 // Clean up
735 std::remove(intermediate_file.c_str());
736 std::remove("lib/models/test_roundtrip.mtl");
737 std::remove(final_file.c_str());
738 std::remove("lib/models/test_roundtrip2.mtl");
739 }
740
741 SUBCASE("Material preservation") {
742 Context ctx1, ctx2;
743
744 // Load file with materials
745 std::vector<uint> original_uuids;
746 DOCTEST_CHECK_NOTHROW(original_uuids = ctx1.loadOBJ("lib/models/test_cube_medium.obj", true));
747
748 // Write and reload
749 std::string output_file = "lib/models/test_material_preservation.obj";
750 DOCTEST_CHECK_NOTHROW(ctx1.writeOBJ(output_file.c_str(), original_uuids, true));
751
752 std::vector<uint> reloaded_uuids;
753 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx2.loadOBJ(output_file.c_str(), true));
754
755 DOCTEST_CHECK(original_uuids.size() == reloaded_uuids.size());
756
757 // Colors should be preserved (at least approximately)
758 for (size_t i = 0; i < std::min(original_uuids.size(), reloaded_uuids.size()); i++) {
759 RGBcolor orig_color = ctx1.getPrimitiveColor(original_uuids[i]);
760 RGBcolor new_color = ctx2.getPrimitiveColor(reloaded_uuids[i]);
761
762 // Colors should be reasonable (not black unless intentionally black)
763 float orig_brightness = orig_color.r + orig_color.g + orig_color.b;
764 float new_brightness = new_color.r + new_color.g + new_color.b;
765 DOCTEST_CHECK((orig_brightness > 0.01f || new_brightness > 0.01f));
766 }
767
768 // Clean up
769 std::remove(output_file.c_str());
770 std::remove("lib/models/test_material_preservation.mtl");
771 }
772}
773
774TEST_CASE("OBJ WriteOBJ - Comprehensive Test Suite for Optimization") {
775
776 // Helper function to create test datasets
777 auto createTestDataset = [](Context &ctx, const std::string &type, int count) -> std::vector<uint> {
778 std::vector<uint> uuids;
779
780 if (type == "simple_triangles") {
781 for (int i = 0; i < count; i++) {
782 float x = static_cast<float>(i % 10);
783 float z = static_cast<float>(i / 10);
784 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), make_RGBcolor(0.5f + 0.5f * (i % 3 == 0), 0.5f + 0.5f * (i % 3 == 1), 0.5f + 0.5f * (i % 3 == 2)));
785 uuids.push_back(tri);
786 }
787 } else if (type == "mixed_primitives") {
788 for (int i = 0; i < count; i++) {
789 float x = static_cast<float>(i % 10);
790 float z = static_cast<float>(i / 10);
791 if (i % 2 == 0) {
792 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
793 uuids.push_back(tri);
794 } else {
795 uint patch = ctx.addPatch(make_vec3(x, 0, z), make_vec2(1, 1));
796 uuids.push_back(patch);
797 }
798 }
799 } else if (type == "textured_primitives") {
800 // For textured primitives, use solid colors instead of actual texture files
801 // to avoid JPEG validation issues in the test environment
802 for (int i = 0; i < count; i++) {
803 float x = static_cast<float>(i % 10);
804 float z = static_cast<float>(i / 10);
805 // Create triangles with different colors to simulate material variation
806 RGBcolor color = make_RGBcolor(0.3f + 0.4f * (i % 3 == 0), 0.3f + 0.4f * (i % 3 == 1), 0.3f + 0.4f * (i % 3 == 2));
807 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), color);
808 uuids.push_back(tri);
809 }
810 } else if (type == "multi_material") {
811 // Create different materials with different properties
812 for (int i = 0; i < count; i++) {
813 float x = static_cast<float>(i % 10);
814 float z = static_cast<float>(i / 10);
815
816 // Cycle through different material types
817 int material_type = i % 4;
818 uint prim;
819
820 switch (material_type) {
821 case 0: // Red solid
822 prim = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), RGB::red);
823 break;
824 case 1: // Green solid
825 prim = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), RGB::green);
826 break;
827 case 2: // Blue solid
828 prim = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), RGB::blue);
829 break;
830 default: // Custom color
831 prim = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), make_RGBcolor(0.8f, 0.4f, 0.6f));
832 break;
833 }
834 uuids.push_back(prim);
835 }
836 } else if (type == "object_groups") {
837 for (int i = 0; i < count; i++) {
838 float x = static_cast<float>(i % 10);
839 float z = static_cast<float>(i / 10);
840 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
841
842 // Add object group labels
843 std::string object_label = "group_" + std::to_string(i / 25); // 25 triangles per group
844 ctx.setPrimitiveData(tri, "object_label", object_label);
845 uuids.push_back(tri);
846 }
847 }
848
849 return uuids;
850 };
851
852 SUBCASE("Small dataset validation") {
853 Context ctx;
854 std::vector<uint> uuids = createTestDataset(ctx, "simple_triangles", 50);
855
856 std::string output_file = "test_writeobj_small.obj";
857 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
858
859 // Verify round-trip correctness
860 Context ctx_reload;
861 std::vector<uint> reloaded_uuids;
862 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx_reload.loadOBJ(output_file.c_str(), true));
863
864 DOCTEST_CHECK(uuids.size() == reloaded_uuids.size());
865
866 // Clean up
867 std::remove(output_file.c_str());
868 std::remove("test_writeobj_small.mtl");
869 }
870
871 SUBCASE("Mixed primitives validation") {
872 Context ctx;
873 std::vector<uint> uuids = createTestDataset(ctx, "mixed_primitives", 100);
874
875 std::string output_file = "test_writeobj_mixed.obj";
876 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
877
878 // Verify round-trip correctness
879 Context ctx_reload;
880 std::vector<uint> reloaded_uuids;
881 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx_reload.loadOBJ(output_file.c_str(), true));
882
883 // Should have more primitives due to patch->triangle conversion
884 DOCTEST_CHECK(reloaded_uuids.size() >= uuids.size());
885
886 // Clean up
887 std::remove(output_file.c_str());
888 std::remove("test_writeobj_mixed.mtl");
889 }
890
891 SUBCASE("Textured primitives validation") {
892 Context ctx;
893 std::vector<uint> uuids = createTestDataset(ctx, "textured_primitives", 75);
894
895 std::string output_file = "test_writeobj_textured.obj";
896 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
897
898 // Verify round-trip correctness
899 Context ctx_reload;
900 std::vector<uint> reloaded_uuids;
901 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx_reload.loadOBJ(output_file.c_str(), true));
902
903 DOCTEST_CHECK(uuids.size() == reloaded_uuids.size());
904
905 // Clean up
906 std::remove(output_file.c_str());
907 std::remove("test_writeobj_textured.mtl");
908 }
909
910
911 SUBCASE("Multi-material validation") {
912 Context ctx;
913 std::vector<uint> uuids = createTestDataset(ctx, "multi_material", 200);
914
915 std::string output_file = "test_writeobj_materials.obj";
916 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, true));
917
918 // Verify MTL file was created and contains multiple materials
919 std::string mtl_file = "test_writeobj_materials.mtl";
920 std::ifstream mtl_check(mtl_file);
921 DOCTEST_CHECK(mtl_check.good());
922
923 int material_count = 0;
924 std::string line;
925 while (std::getline(mtl_check, line)) {
926 if (line.substr(0, 6) == "newmtl") {
927 material_count++;
928 }
929 }
930 mtl_check.close();
931
932 DOCTEST_CHECK(material_count >= 4); // Should have at least 4 different materials
933
934 // Clean up
935 std::remove(output_file.c_str());
936 std::remove(mtl_file.c_str());
937 }
938
939 SUBCASE("Object groups validation") {
940 Context ctx;
941 std::vector<uint> uuids = createTestDataset(ctx, "object_groups", 100);
942
943 std::string output_file = "test_writeobj_groups.obj";
944 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, true));
945
946 // Verify OBJ file contains object group directives
947 std::ifstream obj_check(output_file);
948 DOCTEST_CHECK(obj_check.good());
949
950 int object_count = 0;
951 std::string line;
952 while (std::getline(obj_check, line)) {
953 if (line.substr(0, 2) == "o ") {
954 object_count++;
955 }
956 }
957 obj_check.close();
958
959 DOCTEST_CHECK(object_count >= 4); // Should have at least 4 object groups
960
961 // Clean up
962 std::remove(output_file.c_str());
963 std::remove("test_writeobj_groups.mtl");
964 }
965}
966
967TEST_CASE("OBJ WriteOBJ - Performance Benchmarking Suite") {
968
969 // Performance benchmark helper
970 auto benchmarkWriteOBJ = [](Context &ctx, const std::vector<uint> &uuids, const std::string &test_name) {
971 std::string output_file = "bench_" + test_name + ".obj";
972
973 auto start = std::chrono::high_resolution_clock::now();
974 ctx.writeOBJ(output_file.c_str(), uuids);
975 auto end = std::chrono::high_resolution_clock::now();
976
977 auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
978 float write_time_ms = duration.count() / 1000.0f;
979 float primitives_per_second = uuids.size() * 1000000.0f / duration.count();
980
981 // Clean up
982 std::remove(output_file.c_str());
983 std::string mtl_file = "bench_" + test_name + ".mtl";
984 std::remove(mtl_file.c_str());
985
986 return std::make_pair(write_time_ms, primitives_per_second);
987 };
988
989 SUBCASE("Baseline performance - 1000 triangles") {
990 Context ctx;
991 std::vector<uint> uuids;
992
993 for (int i = 0; i < 1000; i++) {
994 float x = static_cast<float>(i % 10);
995 float z = static_cast<float>(i / 10);
996 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
997 uuids.push_back(tri);
998 }
999
1000 auto [time_ms, prims_per_sec] = benchmarkWriteOBJ(ctx, uuids, "baseline_1k");
1001
1002 // Should achieve reasonable performance (conservative baseline)
1003 DOCTEST_CHECK(prims_per_sec > 100.0f);
1004 }
1005
1006 SUBCASE("Multi-material performance - 2000 primitives") {
1007 Context ctx;
1008 std::vector<uint> uuids;
1009
1010 for (int i = 0; i < 2000; i++) {
1011 float x = static_cast<float>(i % 20);
1012 float z = static_cast<float>(i / 20);
1013
1014 // Create 10 different materials
1015 RGBcolor color = make_RGBcolor((i % 10) / 10.0f, 0.5f, 0.7f);
1016 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), color);
1017 uuids.push_back(tri);
1018 }
1019
1020 auto [time_ms, prims_per_sec] = benchmarkWriteOBJ(ctx, uuids, "multi_material_2k");
1021
1022 // Should handle multiple materials efficiently
1023 DOCTEST_CHECK(prims_per_sec > 50.0f);
1024 }
1025
1026 SUBCASE("Large dataset performance - 5000 primitives") {
1027 Context ctx;
1028 std::vector<uint> uuids;
1029
1030 for (int i = 0; i < 5000; i++) {
1031 float x = static_cast<float>(i % 50);
1032 float z = static_cast<float>(i / 50);
1033 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1034 uuids.push_back(tri);
1035 }
1036
1037 auto [time_ms, prims_per_sec] = benchmarkWriteOBJ(ctx, uuids, "large_5k");
1038
1039 // Performance should remain reasonable for larger datasets
1040 DOCTEST_CHECK(prims_per_sec > 25.0f);
1041 }
1042
1043 SUBCASE("Memory usage monitoring") {
1044 Context ctx;
1045 std::vector<uint> uuids;
1046
1047 // Create substantial dataset for memory testing
1048 for (int i = 0; i < 3000; i++) {
1049 float x = static_cast<float>(i % 30);
1050 float z = static_cast<float>(i / 30);
1051 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1052 uuids.push_back(tri);
1053 }
1054
1055 std::string output_file = "bench_memory_test.obj";
1056
1057 // This test primarily verifies no memory leaks or excessive allocation
1058 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
1059
1060 // Verify file was created with reasonable size
1061 std::ifstream file_check(output_file, std::ios::ate);
1062 auto file_size = file_check.tellg();
1063 file_check.close();
1064
1065 DOCTEST_CHECK(file_size > 0);
1066 DOCTEST_CHECK(file_size < 50 * 1024 * 1024); // Should be less than 50MB
1067
1068 // Clean up
1069 std::remove(output_file.c_str());
1070 std::remove("bench_memory_test.mtl");
1071 }
1072}
1073
1074TEST_CASE("OBJ WriteOBJ - Stress Testing and Edge Cases") {
1075
1076 SUBCASE("Very large primitive count") {
1077 Context ctx;
1078 std::vector<uint> uuids;
1079
1080 // Test with 10000 primitives to stress test the system
1081 for (int i = 0; i < 10000; i++) {
1082 float x = static_cast<float>(i % 100);
1083 float z = static_cast<float>(i / 100);
1084 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1085 uuids.push_back(tri);
1086 }
1087
1088 std::string output_file = "stress_large.obj";
1089
1090 auto start = std::chrono::high_resolution_clock::now();
1091 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
1092 auto end = std::chrono::high_resolution_clock::now();
1093
1094 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
1095
1096 // Should complete within reasonable time (10 seconds max)
1097 DOCTEST_CHECK(duration.count() < 10000);
1098
1099 // Clean up
1100 std::remove(output_file.c_str());
1101 std::remove("stress_large.mtl");
1102 }
1103
1104 SUBCASE("Many materials stress test") {
1105 Context ctx;
1106 std::vector<uint> uuids;
1107
1108 // Create 500 primitives with 100 different materials
1109 for (int i = 0; i < 500; i++) {
1110 float x = static_cast<float>(i % 25);
1111 float z = static_cast<float>(i / 25);
1112
1113 // Create unique color for every 5th primitive (100 materials total)
1114 float r = static_cast<float>((i / 5) % 10) / 10.0f;
1115 float g = static_cast<float>((i / 5) % 10) / 10.0f;
1116 float b = static_cast<float>((i / 5) / 10) / 10.0f;
1117 RGBcolor color = make_RGBcolor(r, g, b);
1118
1119 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), color);
1120 uuids.push_back(tri);
1121 }
1122
1123 std::string output_file = "stress_materials.obj";
1124 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
1125
1126 // Verify many materials were created
1127 std::string mtl_file = "stress_materials.mtl";
1128 std::ifstream mtl_check(mtl_file);
1129 int material_count = 0;
1130 std::string line;
1131 while (std::getline(mtl_check, line)) {
1132 if (line.substr(0, 6) == "newmtl") {
1133 material_count++;
1134 }
1135 }
1136 mtl_check.close();
1137
1138 DOCTEST_CHECK(material_count >= 50); // Should have many materials
1139
1140 // Clean up
1141 std::remove(output_file.c_str());
1142 std::remove(mtl_file.c_str());
1143 }
1144
1145 SUBCASE("Degenerate geometry handling") {
1146 Context ctx;
1147 std::vector<uint> uuids;
1148
1149 // Create some regular triangles
1150 for (int i = 0; i < 100; i++) {
1151 float x = static_cast<float>(i % 10);
1152 float z = static_cast<float>(i / 10);
1153 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1154 uuids.push_back(tri);
1155 }
1156
1157 // Add some very small triangles that might be degenerate
1158 for (int i = 0; i < 10; i++) {
1159 float offset = i * 1e-8f;
1160 uint tri = ctx.addTriangle(make_vec3(100, 0, 0), make_vec3(100 + offset, 0, 0), make_vec3(100, offset, 0));
1161 uuids.push_back(tri);
1162 }
1163
1164 std::string output_file = "stress_degenerate.obj";
1165 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids));
1166
1167 // Verify file was written correctly by checking it exists and has content
1168 std::ifstream file_check(output_file);
1169 DOCTEST_CHECK(file_check.good());
1170
1171 // Count lines to verify content was written
1172 std::string line;
1173 int line_count = 0;
1174 while (std::getline(file_check, line)) {
1175 line_count++;
1176 }
1177 file_check.close();
1178
1179 DOCTEST_CHECK(line_count > 10); // Should have vertices, faces, etc.
1180
1181 // Clean up
1182 std::remove(output_file.c_str());
1183 std::remove("stress_degenerate.mtl");
1184 }
1185}