1.3.72
 
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, false, 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(), true, true));
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, 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, false, 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("OBJ materials registered in Context material system") {
402 Context ctx;
403
404 std::vector<uint> UUIDs = ctx.loadOBJ("lib/models/test_cube_medium.obj", true);
405 DOCTEST_CHECK(UUIDs.size() == 12);
406
407 // Verify MTL materials are registered in the Context material system
408 std::vector<std::string> mat_labels = ctx.listMaterials();
409 DOCTEST_CHECK(mat_labels.size() == 3);
410
411 DOCTEST_CHECK(ctx.doesMaterialExist("red_material"));
412 DOCTEST_CHECK(ctx.doesMaterialExist("blue_material"));
413 DOCTEST_CHECK(ctx.doesMaterialExist("green_material"));
414
415 // Verify material colors match MTL Kd values
416 RGBAcolor red_color = ctx.getMaterialColor("red_material");
417 DOCTEST_CHECK(red_color.r == doctest::Approx(1.0f));
418 DOCTEST_CHECK(red_color.g == doctest::Approx(0.0f));
419 DOCTEST_CHECK(red_color.b == doctest::Approx(0.0f));
420
421 RGBAcolor blue_color = ctx.getMaterialColor("blue_material");
422 DOCTEST_CHECK(blue_color.r == doctest::Approx(0.0f));
423 DOCTEST_CHECK(blue_color.g == doctest::Approx(0.0f));
424 DOCTEST_CHECK(blue_color.b == doctest::Approx(1.0f));
425
426 RGBAcolor green_color = ctx.getMaterialColor("green_material");
427 DOCTEST_CHECK(green_color.r == doctest::Approx(0.0f));
428 DOCTEST_CHECK(green_color.g == doctest::Approx(1.0f));
429 DOCTEST_CHECK(green_color.b == doctest::Approx(0.0f));
430
431 // Verify every primitive has a material label assigned
432 for (uint uuid : UUIDs) {
433 std::string label = ctx.getPrimitiveMaterialLabel(uuid);
434 DOCTEST_CHECK(!label.empty());
435 bool valid_material = (label == "red_material" || label == "blue_material" || label == "green_material");
436 DOCTEST_CHECK(valid_material);
437 }
438
439 // Verify red_material has 4 triangles (2 faces * 2 tri each), same for others
440 int red_count = 0, blue_count = 0, green_count = 0;
441 for (uint uuid : UUIDs) {
442 std::string label = ctx.getPrimitiveMaterialLabel(uuid);
443 if (label == "red_material") red_count++;
444 else if (label == "blue_material") blue_count++;
445 else if (label == "green_material") green_count++;
446 }
447 DOCTEST_CHECK(red_count == 4);
448 DOCTEST_CHECK(blue_count == 4);
449 DOCTEST_CHECK(green_count == 4);
450 }
451
452 SUBCASE("Texture coordinate consistency with transformations") {
453 Context ctx;
454 std::vector<uint> UUIDs;
455
456 // Load with transformations to test that UV coordinates are preserved
457 vec3 origin = make_vec3(5.0f, 0.0f, 0.0f);
458 float height = 2.0f;
459 SphericalCoord rotation = make_SphericalCoord(M_PI / 4, 0);
460 RGBcolor color = RGB::blue;
461
462 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/obj_object_test.obj", origin, height, rotation, color, "ZUP", true));
463 DOCTEST_CHECK(UUIDs.size() > 0);
464
465 for (uint uuid: UUIDs) {
466 if (ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE) {
467 std::string texture_file = ctx.getPrimitiveTextureFile(uuid);
468
469 if (!texture_file.empty()) {
470 // UV coordinates should still be valid after transformations
471 std::vector<vec2> uv_coords = ctx.getPrimitiveTextureUV(uuid);
472 DOCTEST_CHECK(uv_coords.size() == 3);
473
474 for (const auto &uv: uv_coords) {
475 DOCTEST_CHECK(std::isfinite(uv.x));
476 DOCTEST_CHECK(std::isfinite(uv.y));
477 }
478
479 // Vertices should be transformed but texture coordinates unchanged
480 std::vector<vec3> vertices = ctx.getPrimitiveVertices(uuid);
481 DOCTEST_CHECK(vertices.size() == 3);
482
483 // At least one vertex should be displaced from origin due to transformations
484 bool found_transformed = false;
485 for (const auto &vertex: vertices) {
486 if (vertex.magnitude() > 1.0f) { // Original vertices are within unit range
487 found_transformed = true;
488 break;
489 }
490 }
491 DOCTEST_CHECK(found_transformed);
492 }
493 }
494 }
495 }
496
497 SUBCASE("Missing texture file handling") {
498 Context ctx;
499 std::vector<uint> UUIDs;
500
501 // This should load successfully even though texture.jpg doesn't exist
502 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", true));
503 DOCTEST_CHECK(UUIDs.size() > 0);
504
505 // Primitives should still be created even with missing texture files
506 for (uint uuid: UUIDs) {
507 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
508
509 // Should have valid geometry regardless of missing textures
510 std::vector<vec3> vertices = ctx.getPrimitiveVertices(uuid);
511 DOCTEST_CHECK(vertices.size() >= 3);
512 DOCTEST_CHECK(ctx.getPrimitiveArea(uuid) > 0.0f);
513 }
514 }
515}
516
517TEST_CASE("OBJ File Loading - Error Handling and Edge Cases") {
518 SUBCASE("Invalid face vertex indices") {
519 Context ctx;
520
521 // Create a test file with invalid face indices
522 std::string test_content = "# Test OBJ with invalid face indices\n"
523 "v 0.0 0.0 0.0\n"
524 "v 1.0 0.0 0.0\n"
525 "v 0.5 1.0 0.0\n"
526 "f 1 2 5\n"; // Face references vertex 5 but only 3 vertices exist
527
528 std::string test_file = "lib/models/test_invalid_face.obj";
529 std::ofstream file(test_file);
530 file << test_content;
531 file.close();
532
533 // Should throw helios_runtime_error for invalid face index
534 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(test_file.c_str(), true), std::runtime_error);
535
536 // Clean up
537 std::remove(test_file.c_str());
538 }
539
540 SUBCASE("Invalid texture coordinate indices") {
541 Context ctx;
542
543 // Create a test file with invalid UV indices
544 std::string test_content = "# Test OBJ with invalid UV indices\n"
545 "v 0.0 0.0 0.0\n"
546 "v 1.0 0.0 0.0\n"
547 "v 0.5 1.0 0.0\n"
548 "vt 0.0 0.0\n"
549 "vt 1.0 0.0\n"
550 "f 1/3 2/1 3/2\n"; // UV index 3 doesn't exist (only 2 UVs defined)
551
552 std::string test_file = "lib/models/test_invalid_uv.obj";
553 std::ofstream file(test_file);
554 file << test_content;
555 file.close();
556
557 // Should throw helios_runtime_error for invalid UV index
558 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(test_file.c_str(), true), std::runtime_error);
559
560 // Clean up
561 std::remove(test_file.c_str());
562 }
563
564 SUBCASE("Missing texture file") {
565 Context ctx;
566
567 // Create test OBJ and MTL files where MTL references non-existent texture
568 std::string obj_content = "# Test OBJ with missing texture\n"
569 "mtllib test_missing_texture.mtl\n"
570 "v 0.0 0.0 0.0\n"
571 "v 1.0 0.0 0.0\n"
572 "v 0.5 1.0 0.0\n"
573 "usemtl test_material\n"
574 "f 1 2 3\n";
575
576 std::string mtl_content = "newmtl test_material\n"
577 "Ka 0.2 0.2 0.2\n"
578 "Kd 0.8 0.8 0.8\n"
579 "map_Kd nonexistent_texture.jpg\n"; // This texture file doesn't exist
580
581 std::string obj_file = "lib/models/test_missing_texture.obj";
582 std::string mtl_file = "lib/models/test_missing_texture.mtl";
583
584 std::ofstream obj_f(obj_file);
585 obj_f << obj_content;
586 obj_f.close();
587
588 std::ofstream mtl_f(mtl_file);
589 mtl_f << mtl_content;
590 mtl_f.close();
591
592 // Should throw helios_runtime_error for missing texture file
593 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(obj_file.c_str(), true), std::runtime_error);
594
595 // Clean up
596 std::remove(obj_file.c_str());
597 std::remove(mtl_file.c_str());
598 }
599
600 SUBCASE("Texture without UV coordinates") {
601 Context ctx;
602
603 // Create a dummy texture file
604 std::string texture_file = "lib/models/test_dummy.jpg";
605 std::ofstream tex_f(texture_file);
606 tex_f << "dummy";
607 tex_f.close();
608
609 // Create test files where material has texture but face has no UV coordinates
610 std::string obj_content = "# Test OBJ with texture but no UV coordinates\n"
611 "mtllib test_no_uv.mtl\n"
612 "v 0.0 0.0 0.0\n"
613 "v 1.0 0.0 0.0\n"
614 "v 0.5 1.0 0.0\n"
615 "usemtl textured_material\n"
616 "f 1 2 3\n"; // Face has no UV indices
617
618 std::string mtl_content = "newmtl textured_material\n"
619 "Ka 0.2 0.2 0.2\n"
620 "Kd 0.8 0.8 0.8\n"
621 "map_Kd test_dummy.jpg\n";
622
623 std::string obj_file = "lib/models/test_no_uv.obj";
624 std::string mtl_file = "lib/models/test_no_uv.mtl";
625
626 std::ofstream obj_f(obj_file);
627 obj_f << obj_content;
628 obj_f.close();
629
630 std::ofstream mtl_f(mtl_file);
631 mtl_f << mtl_content;
632 mtl_f.close();
633
634 // Should throw helios_runtime_error for texture without UV coordinates
635 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(obj_file.c_str(), true), std::runtime_error);
636
637 // Clean up
638 std::remove(obj_file.c_str());
639 std::remove(mtl_file.c_str());
640 std::remove(texture_file.c_str());
641 }
642
643 SUBCASE("Zero vertex file") {
644 Context ctx;
645
646 // Create a test file with faces but no vertices
647 std::string test_content = "# Test OBJ with no vertices\n"
648 "f 1 2 3\n"; // Face references vertices but none are defined
649
650 std::string test_file = "lib/models/test_zero_vertex.obj";
651 std::ofstream file(test_file);
652 file << test_content;
653 file.close();
654
655 // Should throw helios_runtime_error for face referencing non-existent vertices
656 DOCTEST_CHECK_THROWS_AS(ctx.loadOBJ(test_file.c_str(), true), std::runtime_error);
657
658 // Clean up
659 std::remove(test_file.c_str());
660 }
661}
662
663TEST_CASE("OBJ File I/O - Performance Benchmarking") {
664 SUBCASE("Loading performance baseline") {
665 Context ctx;
666 std::vector<uint> UUIDs;
667
668 // Benchmark loading the large complex file
669 auto start = std::chrono::high_resolution_clock::now();
670 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", true));
671 auto end = std::chrono::high_resolution_clock::now();
672
673 auto load_duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
674
675 DOCTEST_CHECK(UUIDs.size() > 0);
676
677 // Store baseline metrics (for future optimization comparison)
678 float triangles_per_second = UUIDs.size() * 1000000.0f / load_duration.count();
679 float load_time_ms = load_duration.count() / 1000.0f;
680
681 // Basic sanity check - should process at least 1000 triangles/second
682 // (This is a very conservative baseline)
683 DOCTEST_CHECK(triangles_per_second > 1000.0f);
684 }
685
686 SUBCASE("Loading performance with transformations") {
687 Context ctx;
688 std::vector<uint> UUIDs;
689
690 // Test performance with transformations (more CPU intensive)
691 vec3 origin = make_vec3(10.0f, 5.0f, 0.0f);
692 float height = 2.0f;
693 SphericalCoord rotation = make_SphericalCoord(M_PI / 6, M_PI / 4);
694 RGBcolor color = RGB::green;
695
696 auto start = std::chrono::high_resolution_clock::now();
697 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_complex_large.obj", origin, height, rotation, color, "ZUP", true));
698 auto end = std::chrono::high_resolution_clock::now();
699
700 auto load_duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
701
702 DOCTEST_CHECK(UUIDs.size() > 0);
703
704 float triangles_per_second = UUIDs.size() * 1000000.0f / load_duration.count();
705 float load_time_ms = load_duration.count() / 1000.0f;
706
707 // Transformed loading should still maintain reasonable performance
708 DOCTEST_CHECK(triangles_per_second > 800.0f);
709 }
710
711 SUBCASE("Writing performance baseline") {
712 Context ctx;
713
714 // Create a substantial amount of geometry
715 std::vector<uint> uuids;
716 for (int i = 0; i < 1000; i++) {
717 float x = static_cast<float>(i % 10);
718 float z = static_cast<float>(i / 10);
719 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
720 uuids.push_back(tri);
721 }
722
723 // Benchmark writing
724 std::string output_file = "lib/models/test_perf_output.obj";
725 auto start = std::chrono::high_resolution_clock::now();
726 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
727 auto end = std::chrono::high_resolution_clock::now();
728
729 auto write_duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
730
731 // Basic sanity check - should write at least 500 triangles/second
732 float triangles_per_second = uuids.size() * 1000000.0f / write_duration.count();
733 DOCTEST_CHECK(triangles_per_second > 500.0f);
734
735 // Clean up
736 std::remove(output_file.c_str());
737 std::remove("lib/models/test_perf_output.mtl");
738 }
739}
740
741TEST_CASE("OBJ File I/O - Correctness Validation") {
742 SUBCASE("Round-trip geometry preservation") {
743 Context ctx1, ctx2, ctx3;
744
745 // Load original file
746 std::vector<uint> original_uuids;
747 DOCTEST_CHECK_NOTHROW(original_uuids = ctx1.loadOBJ("lib/models/test_cube_medium.obj", true));
748
749 // Write to new file
750 std::string intermediate_file = "lib/models/test_roundtrip.obj";
751 DOCTEST_CHECK_NOTHROW(ctx1.writeOBJ(intermediate_file.c_str(), original_uuids, false, true));
752
753 // Load the written file
754 std::vector<uint> roundtrip_uuids;
755 DOCTEST_CHECK_NOTHROW(roundtrip_uuids = ctx2.loadOBJ(intermediate_file.c_str(), true));
756
757 // Write again
758 std::string final_file = "lib/models/test_roundtrip2.obj";
759 DOCTEST_CHECK_NOTHROW(ctx2.writeOBJ(final_file.c_str(), roundtrip_uuids, false, true));
760
761 // Load final file
762 std::vector<uint> final_uuids;
763 DOCTEST_CHECK_NOTHROW(final_uuids = ctx3.loadOBJ(final_file.c_str(), true));
764
765 // All should have same number of triangles
766 DOCTEST_CHECK(original_uuids.size() == roundtrip_uuids.size());
767 DOCTEST_CHECK(roundtrip_uuids.size() == final_uuids.size());
768
769 // Total area should be preserved (approximately)
770 float original_area = 0, roundtrip_area = 0, final_area = 0;
771
772 for (uint uuid: original_uuids) {
773 original_area += ctx1.getPrimitiveArea(uuid);
774 }
775 for (uint uuid: roundtrip_uuids) {
776 roundtrip_area += ctx2.getPrimitiveArea(uuid);
777 }
778 for (uint uuid: final_uuids) {
779 final_area += ctx3.getPrimitiveArea(uuid);
780 }
781
782 DOCTEST_CHECK(original_area == doctest::Approx(roundtrip_area).epsilon(1e-4));
783 DOCTEST_CHECK(roundtrip_area == doctest::Approx(final_area).epsilon(1e-4));
784
785 // Clean up
786 std::remove(intermediate_file.c_str());
787 std::remove("lib/models/test_roundtrip.mtl");
788 std::remove(final_file.c_str());
789 std::remove("lib/models/test_roundtrip2.mtl");
790 }
791
792 SUBCASE("Material preservation") {
793 Context ctx1, ctx2;
794
795 // Load file with materials
796 std::vector<uint> original_uuids;
797 DOCTEST_CHECK_NOTHROW(original_uuids = ctx1.loadOBJ("lib/models/test_cube_medium.obj", true));
798
799 // Write and reload
800 std::string output_file = "lib/models/test_material_preservation.obj";
801 DOCTEST_CHECK_NOTHROW(ctx1.writeOBJ(output_file.c_str(), original_uuids, false, true));
802
803 std::vector<uint> reloaded_uuids;
804 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx2.loadOBJ(output_file.c_str(), true));
805
806 DOCTEST_CHECK(original_uuids.size() == reloaded_uuids.size());
807
808 // Colors should be preserved (at least approximately)
809 for (size_t i = 0; i < std::min(original_uuids.size(), reloaded_uuids.size()); i++) {
810 RGBcolor orig_color = ctx1.getPrimitiveColor(original_uuids[i]);
811 RGBcolor new_color = ctx2.getPrimitiveColor(reloaded_uuids[i]);
812
813 // Colors should be reasonable (not black unless intentionally black)
814 float orig_brightness = orig_color.r + orig_color.g + orig_color.b;
815 float new_brightness = new_color.r + new_color.g + new_color.b;
816 DOCTEST_CHECK((orig_brightness > 0.01f || new_brightness > 0.01f));
817 }
818
819 // Clean up
820 std::remove(output_file.c_str());
821 std::remove("lib/models/test_material_preservation.mtl");
822 }
823}
824
825TEST_CASE("OBJ WriteOBJ - Comprehensive Test Suite for Optimization") {
826
827 // Helper function to create test datasets
828 auto createTestDataset = [](Context &ctx, const std::string &type, int count) -> std::vector<uint> {
829 std::vector<uint> uuids;
830
831 if (type == "simple_triangles") {
832 for (int i = 0; i < count; i++) {
833 float x = static_cast<float>(i % 10);
834 float z = static_cast<float>(i / 10);
835 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)));
836 uuids.push_back(tri);
837 }
838 } else if (type == "mixed_primitives") {
839 for (int i = 0; i < count; i++) {
840 float x = static_cast<float>(i % 10);
841 float z = static_cast<float>(i / 10);
842 if (i % 2 == 0) {
843 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
844 uuids.push_back(tri);
845 } else {
846 uint patch = ctx.addPatch(make_vec3(x, 0, z), make_vec2(1, 1));
847 uuids.push_back(patch);
848 }
849 }
850 } else if (type == "textured_primitives") {
851 // For textured primitives, use solid colors instead of actual texture files
852 // to avoid JPEG validation issues in the test environment
853 for (int i = 0; i < count; i++) {
854 float x = static_cast<float>(i % 10);
855 float z = static_cast<float>(i / 10);
856 // Create triangles with different colors to simulate material variation
857 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));
858 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), color);
859 uuids.push_back(tri);
860 }
861 } else if (type == "multi_material") {
862 // Create different materials with different properties
863 for (int i = 0; i < count; i++) {
864 float x = static_cast<float>(i % 10);
865 float z = static_cast<float>(i / 10);
866
867 // Cycle through different material types
868 int material_type = i % 4;
869 uint prim;
870
871 switch (material_type) {
872 case 0: // Red solid
873 prim = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), RGB::red);
874 break;
875 case 1: // Green solid
876 prim = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), RGB::green);
877 break;
878 case 2: // Blue solid
879 prim = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), RGB::blue);
880 break;
881 default: // Custom color
882 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));
883 break;
884 }
885 uuids.push_back(prim);
886 }
887 } else if (type == "object_groups") {
888 for (int i = 0; i < count; i++) {
889 float x = static_cast<float>(i % 10);
890 float z = static_cast<float>(i / 10);
891 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
892
893 // Add object group labels
894 std::string object_label = "group_" + std::to_string(i / 25); // 25 triangles per group
895 ctx.setPrimitiveData(tri, "object_label", object_label);
896 uuids.push_back(tri);
897 }
898 }
899
900 return uuids;
901 };
902
903 SUBCASE("Small dataset validation") {
904 Context ctx;
905 std::vector<uint> uuids = createTestDataset(ctx, "simple_triangles", 50);
906
907 std::string output_file = "test_writeobj_small.obj";
908 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
909
910 // Verify round-trip correctness
911 Context ctx_reload;
912 std::vector<uint> reloaded_uuids;
913 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx_reload.loadOBJ(output_file.c_str(), true));
914
915 DOCTEST_CHECK(uuids.size() == reloaded_uuids.size());
916
917 // Clean up
918 std::remove(output_file.c_str());
919 std::remove("test_writeobj_small.mtl");
920 }
921
922 SUBCASE("Mixed primitives validation") {
923 Context ctx;
924 std::vector<uint> uuids = createTestDataset(ctx, "mixed_primitives", 100);
925
926 std::string output_file = "test_writeobj_mixed.obj";
927 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
928
929 // Verify round-trip correctness
930 Context ctx_reload;
931 std::vector<uint> reloaded_uuids;
932 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx_reload.loadOBJ(output_file.c_str(), true));
933
934 // Should have more primitives due to patch->triangle conversion
935 DOCTEST_CHECK(reloaded_uuids.size() >= uuids.size());
936
937 // Clean up
938 std::remove(output_file.c_str());
939 std::remove("test_writeobj_mixed.mtl");
940 }
941
942 SUBCASE("Textured primitives validation") {
943 Context ctx;
944 std::vector<uint> uuids = createTestDataset(ctx, "textured_primitives", 75);
945
946 std::string output_file = "test_writeobj_textured.obj";
947 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
948
949 // Verify round-trip correctness
950 Context ctx_reload;
951 std::vector<uint> reloaded_uuids;
952 DOCTEST_CHECK_NOTHROW(reloaded_uuids = ctx_reload.loadOBJ(output_file.c_str(), true));
953
954 DOCTEST_CHECK(uuids.size() == reloaded_uuids.size());
955
956 // Clean up
957 std::remove(output_file.c_str());
958 std::remove("test_writeobj_textured.mtl");
959 }
960
961
962 SUBCASE("Multi-material validation") {
963 Context ctx;
964 std::vector<uint> uuids = createTestDataset(ctx, "multi_material", 200);
965
966 std::string output_file = "test_writeobj_materials.obj";
967 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
968
969 // Verify MTL file was created and contains multiple materials
970 std::string mtl_file = "test_writeobj_materials.mtl";
971 std::ifstream mtl_check(mtl_file);
972 DOCTEST_CHECK(mtl_check.good());
973
974 int material_count = 0;
975 std::string line;
976 while (std::getline(mtl_check, line)) {
977 if (line.substr(0, 6) == "newmtl") {
978 material_count++;
979 }
980 }
981 mtl_check.close();
982
983 DOCTEST_CHECK(material_count >= 4); // Should have at least 4 different materials
984
985 // Clean up
986 std::remove(output_file.c_str());
987 std::remove(mtl_file.c_str());
988 }
989
990 SUBCASE("Object groups validation") {
991 Context ctx;
992 std::vector<uint> uuids = createTestDataset(ctx, "object_groups", 100);
993
994 std::string output_file = "test_writeobj_groups.obj";
995 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
996
997 // Verify OBJ file contains object group directives
998 std::ifstream obj_check(output_file);
999 DOCTEST_CHECK(obj_check.good());
1000
1001 int object_count = 0;
1002 std::string line;
1003 while (std::getline(obj_check, line)) {
1004 if (line.substr(0, 2) == "o ") {
1005 object_count++;
1006 }
1007 }
1008 obj_check.close();
1009
1010 DOCTEST_CHECK(object_count >= 4); // Should have at least 4 object groups
1011
1012 // Clean up
1013 std::remove(output_file.c_str());
1014 std::remove("test_writeobj_groups.mtl");
1015 }
1016}
1017
1018TEST_CASE("OBJ WriteOBJ - Performance Benchmarking Suite") {
1019
1020 // Performance benchmark helper
1021 auto benchmarkWriteOBJ = [](Context &ctx, const std::vector<uint> &uuids, const std::string &test_name) {
1022 std::string output_file = "bench_" + test_name + ".obj";
1023
1024 auto start = std::chrono::high_resolution_clock::now();
1025 ctx.writeOBJ(output_file.c_str(), uuids, false, true);
1026 auto end = std::chrono::high_resolution_clock::now();
1027
1028 auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
1029 float write_time_ms = duration.count() / 1000.0f;
1030 float primitives_per_second = uuids.size() * 1000000.0f / duration.count();
1031
1032 // Clean up
1033 std::remove(output_file.c_str());
1034 std::string mtl_file = "bench_" + test_name + ".mtl";
1035 std::remove(mtl_file.c_str());
1036
1037 return std::make_pair(write_time_ms, primitives_per_second);
1038 };
1039
1040 SUBCASE("Baseline performance - 1000 triangles") {
1041 Context ctx;
1042 std::vector<uint> uuids;
1043
1044 for (int i = 0; i < 1000; i++) {
1045 float x = static_cast<float>(i % 10);
1046 float z = static_cast<float>(i / 10);
1047 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1048 uuids.push_back(tri);
1049 }
1050
1051 auto [time_ms, prims_per_sec] = benchmarkWriteOBJ(ctx, uuids, "baseline_1k");
1052
1053 // Should achieve reasonable performance (conservative baseline)
1054 DOCTEST_CHECK(prims_per_sec > 100.0f);
1055 }
1056
1057 SUBCASE("Multi-material performance - 2000 primitives") {
1058 Context ctx;
1059 std::vector<uint> uuids;
1060
1061 for (int i = 0; i < 2000; i++) {
1062 float x = static_cast<float>(i % 20);
1063 float z = static_cast<float>(i / 20);
1064
1065 // Create 10 different materials
1066 RGBcolor color = make_RGBcolor((i % 10) / 10.0f, 0.5f, 0.7f);
1067 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), color);
1068 uuids.push_back(tri);
1069 }
1070
1071 auto [time_ms, prims_per_sec] = benchmarkWriteOBJ(ctx, uuids, "multi_material_2k");
1072
1073 // Should handle multiple materials efficiently
1074 DOCTEST_CHECK(prims_per_sec > 50.0f);
1075 }
1076
1077 SUBCASE("Large dataset performance - 5000 primitives") {
1078 Context ctx;
1079 std::vector<uint> uuids;
1080
1081 for (int i = 0; i < 5000; i++) {
1082 float x = static_cast<float>(i % 50);
1083 float z = static_cast<float>(i / 50);
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 auto [time_ms, prims_per_sec] = benchmarkWriteOBJ(ctx, uuids, "large_5k");
1089
1090 // Performance should remain reasonable for larger datasets
1091 DOCTEST_CHECK(prims_per_sec > 25.0f);
1092 }
1093
1094 SUBCASE("Memory usage monitoring") {
1095 Context ctx;
1096 std::vector<uint> uuids;
1097
1098 // Create substantial dataset for memory testing
1099 for (int i = 0; i < 3000; i++) {
1100 float x = static_cast<float>(i % 30);
1101 float z = static_cast<float>(i / 30);
1102 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1103 uuids.push_back(tri);
1104 }
1105
1106 std::string output_file = "bench_memory_test.obj";
1107
1108 // This test primarily verifies no memory leaks or excessive allocation
1109 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
1110
1111 // Verify file was created with reasonable size
1112 std::ifstream file_check(output_file, std::ios::ate);
1113 auto file_size = file_check.tellg();
1114 file_check.close();
1115
1116 DOCTEST_CHECK(file_size > 0);
1117 DOCTEST_CHECK(file_size < 50 * 1024 * 1024); // Should be less than 50MB
1118
1119 // Clean up
1120 std::remove(output_file.c_str());
1121 std::remove("bench_memory_test.mtl");
1122 }
1123}
1124
1125TEST_CASE("OBJ WriteOBJ - Stress Testing and Edge Cases") {
1126
1127 SUBCASE("Very large primitive count") {
1128 Context ctx;
1129 std::vector<uint> uuids;
1130
1131 // Test with 10000 primitives to stress test the system
1132 for (int i = 0; i < 10000; i++) {
1133 float x = static_cast<float>(i % 100);
1134 float z = static_cast<float>(i / 100);
1135 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1136 uuids.push_back(tri);
1137 }
1138
1139 std::string output_file = "stress_large.obj";
1140
1141 auto start = std::chrono::high_resolution_clock::now();
1142 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
1143 auto end = std::chrono::high_resolution_clock::now();
1144
1145 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
1146
1147 // Should complete within reasonable time (10 seconds max)
1148 DOCTEST_CHECK(duration.count() < 10000);
1149
1150 // Clean up
1151 std::remove(output_file.c_str());
1152 std::remove("stress_large.mtl");
1153 }
1154
1155 SUBCASE("Many materials stress test") {
1156 Context ctx;
1157 std::vector<uint> uuids;
1158
1159 // Create 500 primitives with 100 different materials
1160 for (int i = 0; i < 500; i++) {
1161 float x = static_cast<float>(i % 25);
1162 float z = static_cast<float>(i / 25);
1163
1164 // Create unique color for every 5th primitive (100 materials total)
1165 float r = static_cast<float>((i / 5) % 10) / 10.0f;
1166 float g = static_cast<float>((i / 5) % 10) / 10.0f;
1167 float b = static_cast<float>((i / 5) / 10) / 10.0f;
1168 RGBcolor color = make_RGBcolor(r, g, b);
1169
1170 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z), color);
1171 uuids.push_back(tri);
1172 }
1173
1174 std::string output_file = "stress_materials.obj";
1175 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
1176
1177 // Verify many materials were created
1178 std::string mtl_file = "stress_materials.mtl";
1179 std::ifstream mtl_check(mtl_file);
1180 int material_count = 0;
1181 std::string line;
1182 while (std::getline(mtl_check, line)) {
1183 if (line.substr(0, 6) == "newmtl") {
1184 material_count++;
1185 }
1186 }
1187 mtl_check.close();
1188
1189 DOCTEST_CHECK(material_count >= 50); // Should have many materials
1190
1191 // Clean up
1192 std::remove(output_file.c_str());
1193 std::remove(mtl_file.c_str());
1194 }
1195
1196 SUBCASE("Degenerate geometry handling") {
1197 Context ctx;
1198 std::vector<uint> uuids;
1199
1200 // Create some regular triangles
1201 for (int i = 0; i < 100; i++) {
1202 float x = static_cast<float>(i % 10);
1203 float z = static_cast<float>(i / 10);
1204 uint tri = ctx.addTriangle(make_vec3(x, 0, z), make_vec3(x + 1, 0, z), make_vec3(x + 0.5f, 1, z));
1205 uuids.push_back(tri);
1206 }
1207
1208 // Add some very small triangles that might be degenerate
1209 for (int i = 0; i < 10; i++) {
1210 float offset = i * 1e-8f;
1211 uint tri = ctx.addTriangle(make_vec3(100, 0, 0), make_vec3(100 + offset, 0, 0), make_vec3(100, offset, 0));
1212 uuids.push_back(tri);
1213 }
1214
1215 std::string output_file = "stress_degenerate.obj";
1216 DOCTEST_CHECK_NOTHROW(ctx.writeOBJ(output_file.c_str(), uuids, false, true));
1217
1218 // Verify file was written correctly by checking it exists and has content
1219 std::ifstream file_check(output_file);
1220 DOCTEST_CHECK(file_check.good());
1221
1222 // Count lines to verify content was written
1223 std::string line;
1224 int line_count = 0;
1225 while (std::getline(file_check, line)) {
1226 line_count++;
1227 }
1228 file_check.close();
1229
1230 DOCTEST_CHECK(line_count > 10); // Should have vertices, faces, etc.
1231
1232 // Clean up
1233 std::remove(output_file.c_str());
1234 std::remove("stress_degenerate.mtl");
1235 }
1236}
1237
1238TEST_CASE("OBJ Material Color Override Test") {
1239 SUBCASE("Material with Kd color and map_d transparency should use correct color") {
1240 Context ctx;
1241
1242 // Create a simple OBJ file with a material that has both Kd (diffuse color) and map_d (transparency map)
1243 std::string obj_content = R"(# Test OBJ file for material color override bug
1244mtllib test_material_color.mtl
1245o TestObject
1246v 0.0 0.0 0.0
1247v 1.0 0.0 0.0
1248v 0.5 1.0 0.0
1249vt 0.0 0.0
1250vt 1.0 0.0
1251vt 0.5 1.0
1252usemtl TestMaterial
1253f 1/1 2/2 3/3
1254)";
1255
1256 // Create a test MTL file with blue diffuse color and transparency map
1257 std::string mtl_content = R"(# Test MTL file for material color override bug
1258newmtl TestMaterial
1259Ns 250.000000
1260Ka 1.000000 1.000000 1.000000
1261Kd 0.000000 0.000000 0.800000
1262Ks 0.500000 0.500000 0.500000
1263Ke 0.000000 0.000000 0.000000
1264Ni 1.500000
1265illum 2
1266map_d lib/images/solid.jpg
1267)";
1268
1269 // Write test files
1270 std::ofstream obj_file("lib/models/test_material_color.obj");
1271 obj_file << obj_content;
1272 obj_file.close();
1273
1274 std::ofstream mtl_file("lib/models/test_material_color.mtl");
1275 mtl_file << mtl_content;
1276 mtl_file.close();
1277
1278 // Load the OBJ file with a default color (green) to ensure materials override it properly
1279 std::vector<uint> UUIDs;
1280 RGBcolor default_color = RGB::green;
1281 DOCTEST_CHECK_NOTHROW(UUIDs = ctx.loadOBJ("lib/models/test_material_color.obj", make_vec3(0, 0, 0), make_vec3(1, 1, 1), nullrotation, default_color, "ZUP", true));
1282 DOCTEST_CHECK(UUIDs.size() == 1);
1283
1284 // Verify that the triangle was loaded
1285 DOCTEST_CHECK(ctx.doesPrimitiveExist(UUIDs[0]));
1286 DOCTEST_CHECK(ctx.getPrimitiveType(UUIDs[0]) == PRIMITIVE_TYPE_TRIANGLE);
1287
1288 // Check that the primitive has a texture (the map_d transparency texture)
1289 std::string texture_file = ctx.getPrimitiveTextureFile(UUIDs[0]);
1290 DOCTEST_CHECK(!texture_file.empty());
1291
1292 // Most importantly: check that the color is BLUE (from Kd), not RED (old bug) or GREEN (default)
1293 RGBcolor primitive_color = ctx.getPrimitiveColor(UUIDs[0]);
1294
1295 // The material specified Kd 0.000000 0.000000 0.800000 (blue)
1296 DOCTEST_CHECK(primitive_color.r == doctest::Approx(0.0f).epsilon(1e-5));
1297 DOCTEST_CHECK(primitive_color.g == doctest::Approx(0.0f).epsilon(1e-5));
1298 DOCTEST_CHECK(primitive_color.b == doctest::Approx(0.8f).epsilon(1e-5));
1299
1300 // Verify that texture color is overridden (because only map_d was specified, not map_Kd)
1301 DOCTEST_CHECK(ctx.isPrimitiveTextureColorOverridden(UUIDs[0]));
1302
1303 // Clean up test files
1304 std::remove("lib/models/test_material_color.obj");
1305 std::remove("lib/models/test_material_color.mtl");
1306 }
1307}