1.3.64
 
Loading...
Searching...
No Matches
Test_context.h
1#pragma once
2// =================================================================================
3// Suite 4: Context Class
4//
5// Tests for the main Context class, which manages the scene, primitives,
6// objects, data, and simulation state.
7// =================================================================================
8TEST_CASE("Core Context State and Configuration") {
9 SUBCASE("Constructor and basic setup") {
10 Context ctx;
11 DOCTEST_CHECK(ctx.getPrimitiveCount() == 0);
12 DOCTEST_CHECK(ctx.getObjectCount() == 0);
13 DOCTEST_CHECK(!ctx.isGeometryDirty());
14
15 Date d = ctx.getDate();
16 DOCTEST_CHECK(d.day == 1);
17 DOCTEST_CHECK(d.month == 6);
18 DOCTEST_CHECK(d.year == 2000);
19
20 Time t = ctx.getTime();
21 DOCTEST_CHECK(t.hour == 12);
22 DOCTEST_CHECK(t.minute == 0);
23 DOCTEST_CHECK(t.second == 0);
24
25 Location l = ctx.getLocation();
26 DOCTEST_CHECK(l.latitude_deg == doctest::Approx(38.55));
27 DOCTEST_CHECK(l.longitude_deg == doctest::Approx(121.76));
28 DOCTEST_CHECK(l.UTC_offset == doctest::Approx(8));
29 }
30
31 SUBCASE("Random number generator") {
32 Context ctx;
33 ctx.seedRandomGenerator(12345);
34 std::minstd_rand0 *gen1 = ctx.getRandomGenerator();
35 float rand1 = (*gen1)();
36
37 ctx.seedRandomGenerator(12345);
38 std::minstd_rand0 *gen2 = ctx.getRandomGenerator();
39 float rand2 = (*gen2)();
40
41 DOCTEST_CHECK(rand1 == rand2);
42
43 float r_uniform = ctx.randu();
44 DOCTEST_CHECK(r_uniform >= 0.f);
45 DOCTEST_CHECK(r_uniform <= 1.f);
46
47 float r_norm = ctx.randn();
48 // Hard to test for normality, but let's check it's a number
49 DOCTEST_CHECK(!std::isnan(r_norm));
50 }
51
52 SUBCASE("Random number ranges") {
53 Context ctx;
54 ctx.seedRandomGenerator(6789);
55 float r = ctx.randu(-1.f, 1.f);
56 DOCTEST_CHECK(r >= -1.f);
57 DOCTEST_CHECK(r <= 1.f);
58 int ri = ctx.randu(0, 5);
59 DOCTEST_CHECK(ri >= 0);
60 DOCTEST_CHECK(ri <= 5);
61 float rn = ctx.randn(2.f, 0.5f);
62 DOCTEST_CHECK(!std::isnan(rn));
63 }
64
65 SUBCASE("Texture utility methods") {
66 Context ctx;
67 {
68 capture_cerr cerr_buffer;
69 DOCTEST_CHECK_NOTHROW(ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, "lib/images/solid.jpg"));
70 DOCTEST_CHECK_THROWS(ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, "lib/images/missing.png"));
71 DOCTEST_CHECK_THROWS(ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, "lib/images/invalid.txt"));
72 }
73
74 Texture tex("lib/images/solid.jpg");
75 DOCTEST_CHECK(tex.getTextureFile() == "lib/images/solid.jpg");
76 int2 res = tex.getImageResolution();
77 DOCTEST_CHECK(res.x == 5);
78 DOCTEST_CHECK(res.y == 5);
79 DOCTEST_CHECK(!tex.hasTransparencyChannel());
80 const auto *alpha = tex.getTransparencyData();
81 DOCTEST_CHECK(alpha->empty());
82 std::vector<vec2> uv{{0.f, 0.f}, {1.f, 0.f}, {1.f, 1.f}};
83 float sf = tex.getSolidFraction(uv);
84 DOCTEST_CHECK(sf == doctest::Approx(1.f));
85 }
86
87 SUBCASE("Geometry dirty flags") {
88 Context ctx;
89 uint p = ctx.addPatch();
90 DOCTEST_CHECK(ctx.isGeometryDirty());
91 DOCTEST_CHECK(ctx.isPrimitiveDirty(p));
92
93 ctx.markGeometryClean();
94 DOCTEST_CHECK(!ctx.isGeometryDirty());
95 DOCTEST_CHECK(!ctx.isPrimitiveDirty(p));
96
97 ctx.markPrimitiveDirty(p);
98 DOCTEST_CHECK(ctx.isGeometryDirty());
99 DOCTEST_CHECK(ctx.isPrimitiveDirty(p));
100
101 ctx.markPrimitiveClean(p);
102 DOCTEST_CHECK(!ctx.isGeometryDirty());
103 DOCTEST_CHECK(!ctx.isPrimitiveDirty(p));
104
105 ctx.markGeometryDirty();
106 DOCTEST_CHECK(ctx.isGeometryDirty());
107 }
108
109 SUBCASE("Geometry dirty flags vector") {
110 Context ctx;
111 std::vector<uint> ids{ctx.addPatch(), ctx.addPatch()};
112 ctx.markGeometryClean();
113 ctx.markPrimitiveDirty(ids);
114 for (uint id: ids) {
115 DOCTEST_CHECK(ctx.isPrimitiveDirty(id));
116 }
117 ctx.markPrimitiveClean(ids);
118 for (uint id: ids) {
119 DOCTEST_CHECK(!ctx.isPrimitiveDirty(id));
120 }
121
122 vec3 shift = make_vec3(1.f, 0.f, 0.f);
123 ctx.translatePrimitive(ids, shift);
124 for (uint id: ids) {
125 vec3 c = ctx.getPatchCenter(id);
126 DOCTEST_CHECK(c.x == doctest::Approx(shift.x).epsilon(errtol));
127 }
128 }
129
130 SUBCASE("Date and Time Manipulation") {
131 Context ctx;
132 ctx.setDate(15, 7, 2025);
133 Date d = ctx.getDate();
134 DOCTEST_CHECK(d.day == 15);
135 DOCTEST_CHECK(d.month == 7);
136 DOCTEST_CHECK(d.year == 2025);
137 DOCTEST_CHECK(strcmp(ctx.getMonthString(), "JUL") == 0);
138 DOCTEST_CHECK(ctx.getJulianDate() == 196);
139
140 ctx.setTime(45, 30, 10);
141 Time t = ctx.getTime();
142 DOCTEST_CHECK(t.hour == 10);
143 DOCTEST_CHECK(t.minute == 30);
144 DOCTEST_CHECK(t.second == 45);
145
146 capture_cerr cerr_buffer;
147 DOCTEST_CHECK_THROWS(ctx.setDate(32, 1, 2025));
148 DOCTEST_CHECK_THROWS(ctx.setTime(60, 0, 0));
149 }
150
151 SUBCASE("Location Manipulation") {
152 Context ctx;
153 Location loc = {40.7128, -74.0060, 10.0};
154 ctx.setLocation(loc);
155 Location l = ctx.getLocation();
156 DOCTEST_CHECK(l.latitude_deg == doctest::Approx(40.7128));
157 DOCTEST_CHECK(l.longitude_deg == doctest::Approx(-74.0060));
158 DOCTEST_CHECK(l.UTC_offset == doctest::Approx(10.0));
159 }
160
161 SUBCASE("primitive orientation and transforms") {
162 Context ctx;
163 uint id = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
164 ctx.markGeometryClean();
165
166 vec3 n = ctx.getPrimitiveNormal(id);
167 DOCTEST_CHECK(n == vec3(0.f, 0.f, 1.f));
168
169 ctx.setPrimitiveElevation(id, make_vec3(0, 0, 0), 0.f);
170 n = ctx.getPrimitiveNormal(id);
171 DOCTEST_CHECK(n.x == doctest::Approx(0.f).epsilon(errtol));
172 DOCTEST_CHECK(n.y == doctest::Approx(1.f).epsilon(errtol));
173 DOCTEST_CHECK(n.z == doctest::Approx(0.f).epsilon(errtol));
174
175 ctx.setPrimitiveAzimuth(id, make_vec3(0, 0, 0), 0.5f * PI_F);
176 n = ctx.getPrimitiveNormal(id);
177 DOCTEST_CHECK(n.x == doctest::Approx(1.f).epsilon(errtol));
178 DOCTEST_CHECK(n.y == doctest::Approx(0.f).epsilon(errtol));
179
180 ctx.setPrimitiveNormal(id, make_vec3(0, 0, 0), make_vec3(0, 0, 1));
181 n = ctx.getPrimitiveNormal(id);
182 DOCTEST_CHECK(n.z == doctest::Approx(1.f).epsilon(errtol));
183
184 float M[16];
185 makeTranslationMatrix(make_vec3(1.f, 2.f, 3.f), M);
186 ctx.setPrimitiveTransformationMatrix(id, M);
187 float out[16];
188 ctx.getPrimitiveTransformationMatrix(id, out);
189 for (int i = 0; i < 16; ++i) {
190 DOCTEST_CHECK(out[i] == doctest::Approx(M[i]));
191 }
192 DOCTEST_CHECK(ctx.isPrimitiveDirty(id));
193 }
194}
195
196TEST_CASE("Primitive Management: Creation, Properties, and Operations") {
197 SUBCASE("addPatch") {
198 vec3 center, center_r;
199 vec2 size, size_r;
200 std::vector<vec3> vertices, vertices_r;
201 SphericalCoord rotation, rotation_r;
202 vec3 normal, normal_r;
203 RGBcolor color, color_r;
204 uint UUID;
205 std::vector<uint> UUIDs;
206 PrimitiveType type;
207 float area_r;
208 uint objID;
209
210 Context context_test;
211
212 // uint addPatch( const vec3& center, const vec2& size );
213 center = make_vec3(1, 2, 3);
214 size = make_vec2(1, 2);
215 vertices.resize(4);
216 vertices.at(0) = center + make_vec3(-0.5f * size.x, -0.5f * size.y, 0.f);
217 vertices.at(1) = center + make_vec3(0.5f * size.x, -0.5f * size.y, 0.f);
218 vertices.at(2) = center + make_vec3(0.5f * size.x, 0.5f * size.y, 0.f);
219 vertices.at(3) = center + make_vec3(-0.5f * size.x, 0.5f * size.y, 0.f);
220
221 DOCTEST_CHECK_NOTHROW(UUID = context_test.addPatch(center, size));
222 DOCTEST_CHECK_NOTHROW(type = context_test.getPrimitiveType(UUID));
223 DOCTEST_CHECK_NOTHROW(center_r = context_test.getPatchCenter(UUID));
224 DOCTEST_CHECK_NOTHROW(size_r = context_test.getPatchSize(UUID));
225 DOCTEST_CHECK_NOTHROW(normal_r = context_test.getPrimitiveNormal(UUID));
226 DOCTEST_CHECK_NOTHROW(vertices_r = context_test.getPrimitiveVertices(UUID));
227 DOCTEST_CHECK_NOTHROW(area_r = context_test.getPrimitiveArea(UUID));
228 DOCTEST_CHECK_NOTHROW(color_r = context_test.getPrimitiveColor(UUID));
229
230 DOCTEST_CHECK(type == PRIMITIVE_TYPE_PATCH);
231 DOCTEST_CHECK(center_r.x == center.x);
232 DOCTEST_CHECK(center_r.y == center.y);
233 DOCTEST_CHECK(center_r.z == center.z);
234 DOCTEST_CHECK(size_r.x == size.x);
235 DOCTEST_CHECK(size_r.y == size.y);
236 DOCTEST_CHECK(normal_r.x == 0.f);
237 DOCTEST_CHECK(normal_r.y == 0.f);
238 DOCTEST_CHECK(normal_r.z == 1.f);
239 DOCTEST_CHECK(vertices_r.size() == 4);
240 DOCTEST_CHECK(vertices_r.at(0).x == vertices.at(0).x);
241 DOCTEST_CHECK(vertices_r.at(0).y == vertices.at(0).y);
242 DOCTEST_CHECK(vertices_r.at(0).z == vertices.at(0).z);
243 DOCTEST_CHECK(vertices_r.at(1).x == vertices.at(1).x);
244 DOCTEST_CHECK(vertices_r.at(1).y == vertices.at(1).y);
245 DOCTEST_CHECK(vertices_r.at(1).z == vertices.at(1).z);
246 DOCTEST_CHECK(vertices_r.at(2).x == vertices.at(2).x);
247 DOCTEST_CHECK(vertices_r.at(2).y == vertices.at(2).y);
248 DOCTEST_CHECK(vertices_r.at(2).z == vertices.at(2).z);
249 DOCTEST_CHECK(vertices_r.at(3).x == vertices.at(3).x);
250 DOCTEST_CHECK(vertices_r.at(3).y == vertices.at(3).y);
251 DOCTEST_CHECK(vertices_r.at(3).z == vertices.at(3).z);
252 CHECK(area_r == doctest::Approx(size.x * size.y).epsilon(errtol));
253 DOCTEST_CHECK(color_r.r == 0.f);
254 DOCTEST_CHECK(color_r.g == 0.f);
255 DOCTEST_CHECK(color_r.b == 0.f);
256 DOCTEST_CHECK(context_test.getPrimitiveTextureFile(UUID).empty());
257 }
258 SUBCASE("rotated patch") {
259 Context context_test;
260
261 vec3 center = make_vec3(1, 2, 3);
262 vec2 size = make_vec2(1, 2);
263 SphericalCoord rotation = make_SphericalCoord(1.f, 0.15f * PI_F, 0.5f * PI_F);
264 rotation.azimuth = 0.5f * PI_F;
265
266 uint UUID;
267 DOCTEST_CHECK_NOTHROW(UUID = context_test.addPatch(center, size, rotation));
268
269 vec3 normal_r;
270 DOCTEST_CHECK_NOTHROW(normal_r = context_test.getPrimitiveNormal(UUID));
271
272 SphericalCoord rotation_r;
273 DOCTEST_CHECK_NOTHROW(rotation_r = make_SphericalCoord(0.5f * PI_F - asinf(normal_r.z), atan2f(normal_r.x, normal_r.y)));
274
275 DOCTEST_CHECK_NOTHROW(context_test.deletePrimitive(UUID));
276
277 DOCTEST_CHECK(rotation_r.elevation == doctest::Approx(rotation.elevation).epsilon(errtol));
278 DOCTEST_CHECK(rotation_r.azimuth == doctest::Approx(rotation.azimuth).epsilon(errtol));
279 }
280 SUBCASE("addTriangle") {
281 Context context_test;
282
283 vec3 v0, v0_r;
284 vec3 v1, v1_r;
285 vec3 v2, v2_r;
286 uint UUID;
287
288 // uint addTriangle( const vec3& v0, const vec3& v1, const vec3& v2, const RGBcolor &color );
289 v0 = make_vec3(1, 2, 3);
290 v1 = make_vec3(2, 4, 6);
291 v2 = make_vec3(3, 6, 5);
292 std::vector<vec3> vertices{v0, v1, v2};
293 RGBcolor color = RGB::red;
294
295 DOCTEST_CHECK_NOTHROW(UUID = context_test.addTriangle(v0, v1, v2, color));
296 DOCTEST_CHECK(context_test.getPrimitiveType(UUID) == PRIMITIVE_TYPE_TRIANGLE);
297
298 vec3 normal = normalize(cross(v1 - v0, v2 - v1));
299 vec3 normal_r = context_test.getPrimitiveNormal(UUID);
300 DOCTEST_CHECK(normal_r.x == doctest::Approx(normal.x).epsilon(errtol));
301 DOCTEST_CHECK(normal_r.y == doctest::Approx(normal.y).epsilon(errtol));
302 DOCTEST_CHECK(normal_r.z == doctest::Approx(normal.z).epsilon(errtol));
303
304 std::vector<vec3> vertices_r;
305 DOCTEST_CHECK_NOTHROW(vertices_r = context_test.getPrimitiveVertices(UUID));
306 DOCTEST_CHECK(vertices_r.size() == 3);
307 DOCTEST_CHECK(vertices_r.at(0).x == v0.x);
308 DOCTEST_CHECK(vertices_r.at(0).y == v0.y);
309 DOCTEST_CHECK(vertices_r.at(0).z == v0.z);
310
311 RGBcolor color_r;
312 DOCTEST_CHECK_NOTHROW(color_r = context_test.getPrimitiveColor(UUID));
313 DOCTEST_CHECK(color_r.r == color.r);
314 DOCTEST_CHECK(color_r.g == color.g);
315 DOCTEST_CHECK(color_r.b == color.b);
316 DOCTEST_CHECK(context_test.getPrimitiveTextureFile(UUID).empty());
317
318 float a = (v1 - v0).magnitude();
319 float b = (v2 - v0).magnitude();
320 float c = (v2 - v1).magnitude();
321 float s = 0.5f * (a + b + c);
322 float area = sqrtf(s * (s - a) * (s - b) * (s - c));
323 float area_r;
324 DOCTEST_CHECK_NOTHROW(area_r = context_test.getPrimitiveArea(UUID));
325 DOCTEST_CHECK(area_r == doctest::Approx(area).epsilon(errtol));
326 }
327 SUBCASE("copyPrimitive (patch)") {
328 Context context_test;
329 uint UUID, UUID_cpy;
330
331 std::vector<float> cpdata{5.2f, 2.5f, 3.1f};
332
333 vec3 center = make_vec3(1, 2, 3);
334 vec2 size = make_vec2(1, 2);
335
336 DOCTEST_CHECK_NOTHROW(UUID = context_test.addPatch(center, size));
337
338 DOCTEST_CHECK_NOTHROW(context_test.setPrimitiveData(UUID, "somedata", cpdata));
339
340 DOCTEST_CHECK_NOTHROW(UUID_cpy = context_test.copyPrimitive(UUID));
341
342 vec3 center_cpy;
343 DOCTEST_CHECK_NOTHROW(center_cpy = context_test.getPatchCenter(UUID_cpy));
344 vec2 size_cpy;
345 DOCTEST_CHECK_NOTHROW(size_cpy = context_test.getPatchSize(UUID_cpy));
346
347 DOCTEST_CHECK(UUID_cpy == 1);
348 DOCTEST_CHECK(center_cpy.x == center.x);
349 DOCTEST_CHECK(center_cpy.y == center.y);
350 DOCTEST_CHECK(center_cpy.z == center.z);
351 DOCTEST_CHECK(size_cpy.x == size.x);
352 DOCTEST_CHECK(size_cpy.y == size.y);
353
354 std::vector<float> cpdata_copy;
355 context_test.getPrimitiveData(UUID_cpy, "somedata", cpdata_copy);
356
357 DOCTEST_CHECK(cpdata.size() == cpdata_copy.size());
358 for (uint i = 0; i < cpdata.size(); i++) {
359 DOCTEST_CHECK(cpdata.at(i) == cpdata_copy.at(i));
360 }
361
362 // translate the copied patch
363 vec3 shift = make_vec3(5.f, 4.f, 3.f);
364 DOCTEST_CHECK_NOTHROW(context_test.translatePrimitive(UUID_cpy, shift));
365 DOCTEST_CHECK_NOTHROW(center_cpy = context_test.getPatchCenter(UUID_cpy));
366 vec3 center_r;
367 DOCTEST_CHECK_NOTHROW(center_r = context_test.getPatchCenter(UUID));
368
369 DOCTEST_CHECK(center_cpy.x == doctest::Approx(center.x + shift.x).epsilon(errtol));
370 DOCTEST_CHECK(center_cpy.y == doctest::Approx(center.y + shift.y).epsilon(errtol));
371 DOCTEST_CHECK(center_cpy.z == doctest::Approx(center.z + shift.z).epsilon(errtol));
372 DOCTEST_CHECK(center_r.x == center.x);
373 DOCTEST_CHECK(center_r.y == center.y);
374 DOCTEST_CHECK(center_r.z == center.z);
375 }
376 SUBCASE("copyPrimitive (triangle)") {
377 Context context_test;
378
379 vec3 v0 = make_vec3(0, 0, 0);
380 vec3 v1 = make_vec3(1, 0, 0);
381 vec3 v2 = make_vec3(0, 1, 0);
382 uint UUID, UUID_cpy;
383
384 DOCTEST_CHECK_NOTHROW(UUID = context_test.addTriangle(v0, v1, v2, RGB::blue));
385 DOCTEST_CHECK_NOTHROW(UUID_cpy = context_test.copyPrimitive(UUID));
386
387 std::vector<vec3> verts_org, verts_cpy;
388 DOCTEST_CHECK_NOTHROW(verts_org = context_test.getPrimitiveVertices(UUID));
389 DOCTEST_CHECK_NOTHROW(verts_cpy = context_test.getPrimitiveVertices(UUID_cpy));
390 DOCTEST_CHECK(verts_org == verts_cpy);
391
392 vec3 shift = make_vec3(5.f, 4.f, 3.f);
393 DOCTEST_CHECK_NOTHROW(context_test.translatePrimitive(UUID_cpy, shift));
394 DOCTEST_CHECK_NOTHROW(verts_cpy = context_test.getPrimitiveVertices(UUID_cpy));
395 DOCTEST_CHECK(verts_cpy.at(0) == verts_org.at(0) + shift);
396 DOCTEST_CHECK(verts_cpy.at(1) == verts_org.at(1) + shift);
397 DOCTEST_CHECK(verts_cpy.at(2) == verts_org.at(2) + shift);
398
399 DOCTEST_CHECK_NOTHROW(context_test.deletePrimitive(UUID));
400 DOCTEST_CHECK(!context_test.doesPrimitiveExist(UUID));
401 }
402 SUBCASE("deletePrimitive") {
403 Context context_test;
404 uint UUID;
405 vec3 center = make_vec3(1, 2, 3);
406 vec2 size = make_vec2(1, 2);
407
408 DOCTEST_CHECK_NOTHROW(UUID = context_test.addPatch(center, size));
409
410 DOCTEST_CHECK_NOTHROW(context_test.deletePrimitive(UUID));
411
412 uint primitive_count;
413 DOCTEST_CHECK_NOTHROW(primitive_count = context_test.getPrimitiveCount(UUID));
414 DOCTEST_CHECK(primitive_count == 0);
415 DOCTEST_CHECK(!context_test.doesPrimitiveExist(UUID));
416 }
417 SUBCASE("primitive bounding box") {
418 Context context_test;
419 std::vector<uint> UUIDs;
420 UUIDs.push_back(context_test.addPatch(make_vec3(-1, 0, 0), make_vec2(0.5, 0.5)));
421 UUIDs.push_back(context_test.addPatch(make_vec3(1, 0, 0), make_vec2(0.5, 0.5)));
422
423 vec3 bmin, bmax;
424 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveBoundingBox(UUIDs, bmin, bmax));
425 DOCTEST_CHECK(bmin == make_vec3(-1.25f, -0.25f, 0.f));
426 DOCTEST_CHECK(bmax == make_vec3(1.25f, 0.25f, 0.f));
427 }
428 SUBCASE("primitive scale and data") {
429 Context context_test;
430 vec2 sz_0 = make_vec2(0.5f, 3.f);
431 float area0 = sz_0.x * sz_0.y;
432 float scale = 2.6f;
433 uint UUID = context_test.addPatch(make_vec3(0, 0, 0), sz_0);
434 context_test.scalePrimitive(UUID, make_vec3(scale, scale, scale));
435 float area1 = context_test.getPrimitiveArea(UUID);
436 DOCTEST_CHECK(area1 == doctest::Approx(scale * scale * area0).epsilon(1e-5));
437
438 float data = 5.f;
439 context_test.setPrimitiveData(UUID, "some_data", data);
440 DOCTEST_CHECK(context_test.doesPrimitiveDataExist(UUID, "some_data"));
441 float data_r;
442 context_test.getPrimitiveData(UUID, "some_data", data_r);
443 DOCTEST_CHECK(data_r == data);
444
445 std::vector<float> vec = {0, 1, 2, 3, 4};
446 context_test.setPrimitiveData(UUID, "vec_data", vec);
447 std::vector<float> vec_r;
448 context_test.getPrimitiveData(UUID, "vec_data", vec_r);
449 DOCTEST_CHECK(vec_r == vec);
450
451 std::vector<uint> UUIDs_filter;
452 std::vector<uint> UUIDs_multi;
453 for (uint i = 0; i < 4; i++) {
454 UUIDs_multi.push_back(context_test.addPatch());
455 }
456 context_test.setPrimitiveData(UUIDs_multi[0], "val", 4.f);
457 context_test.setPrimitiveData(UUIDs_multi[0], "str", "cat");
458 context_test.setPrimitiveData(UUIDs_multi[1], "val", 3.f);
459 context_test.setPrimitiveData(UUIDs_multi[1], "str", "cat");
460 context_test.setPrimitiveData(UUIDs_multi[2], "val", 2.f);
461 context_test.setPrimitiveData(UUIDs_multi[2], "str", "dog");
462 context_test.setPrimitiveData(UUIDs_multi[3], "val", 1.f);
463 context_test.setPrimitiveData(UUIDs_multi[3], "str", "dog");
464
465 UUIDs_filter = context_test.filterPrimitivesByData(UUIDs_multi, "val", 2.f, "<=");
466 DOCTEST_CHECK(UUIDs_filter.size() == 2);
467 DOCTEST_CHECK(std::find(UUIDs_filter.begin(), UUIDs_filter.end(), UUIDs_multi[2]) != UUIDs_filter.end());
468 DOCTEST_CHECK(std::find(UUIDs_filter.begin(), UUIDs_filter.end(), UUIDs_multi[3]) != UUIDs_filter.end());
469
470 UUIDs_filter = context_test.filterPrimitivesByData(UUIDs_multi, "str", "cat");
471 DOCTEST_CHECK(UUIDs_filter.size() == 2);
472 DOCTEST_CHECK(std::find(UUIDs_filter.begin(), UUIDs_filter.end(), UUIDs_multi[0]) != UUIDs_filter.end());
473 DOCTEST_CHECK(std::find(UUIDs_filter.begin(), UUIDs_filter.end(), UUIDs_multi[1]) != UUIDs_filter.end());
474 }
475 SUBCASE("texture uv and solid fraction") {
476 Context context_test;
477
478 vec2 sizep = make_vec2(2, 3);
479 const char *texture = "lib/images/disk_texture.png";
480 vec2 uv0 = make_vec2(0, 0);
481 vec2 uv1 = make_vec2(1, 0);
482 vec2 uv2 = make_vec2(1, 1);
483 vec2 uv3 = make_vec2(0, 1);
484 uint UUIDp = context_test.addPatch(make_vec3(2, 3, 4), sizep, nullrotation, texture, 0.5f * (uv0 + uv2), uv2 - uv0);
485 DOCTEST_CHECK(!context_test.getPrimitiveTextureFile(UUIDp).empty());
486 float Ap = context_test.getPrimitiveArea(UUIDp);
487 DOCTEST_CHECK(Ap == doctest::Approx(0.25f * PI_F * sizep.x * sizep.y).epsilon(0.01));
488 std::vector<vec2> uv = context_test.getPrimitiveTextureUV(UUIDp);
489 DOCTEST_CHECK(uv.size() == 4);
490 DOCTEST_CHECK(uv.at(0) == uv0);
491 DOCTEST_CHECK(uv.at(1) == uv1);
492 DOCTEST_CHECK(uv.at(2) == uv2);
493 DOCTEST_CHECK(uv.at(3) == uv3);
494
495 uint UUIDt = context_test.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(1, 1, 0), "lib/images/diamond_texture.png", make_vec2(0, 0), make_vec2(1, 0), make_vec2(1, 1));
496 float solid_fraction = context_test.getPrimitiveSolidFraction(UUIDt);
497 DOCTEST_CHECK(solid_fraction == doctest::Approx(0.5f).epsilon(errtol));
498 }
499
500 SUBCASE("advanced primitive transforms") {
501 Context ctx;
502 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
503 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1));
504 std::vector<uint> ids{p1, p2};
505 ctx.markGeometryClean();
506
507 ctx.rotatePrimitive(p1, 0.5f * PI_F, "x");
508 vec3 n = ctx.getPrimitiveNormal(p1);
509 DOCTEST_CHECK(n.y == doctest::Approx(-1.f).epsilon(errtol));
510 DOCTEST_CHECK(n.z == doctest::Approx(0.f).epsilon(errtol));
511
512 ctx.rotatePrimitive(ids, PI_F, make_vec3(0, 1, 0));
513 vec3 c = ctx.getPatchCenter(p2);
514 DOCTEST_CHECK(c.x == doctest::Approx(-1.f).epsilon(errtol));
515
516 ctx.rotatePrimitive(p1, PI_F, make_vec3(0, 0, 0), make_vec3(0, 0, 1));
517
518 ctx.scalePrimitiveAboutPoint(p2, make_vec3(2.f, 2.f, 2.f), make_vec3(0, 0, 0));
519 vec2 sz = ctx.getPatchSize(p2);
520 DOCTEST_CHECK(sz.x == doctest::Approx(2.f).epsilon(errtol));
521
522 ctx.scalePrimitiveAboutPoint(ids, make_vec3(0.5f, 0.5f, 0.5f), make_vec3(0, 0, 0));
523 sz = ctx.getPatchSize(p2);
524 DOCTEST_CHECK(sz.x == doctest::Approx(1.f).epsilon(errtol));
525 }
526}
527
528TEST_CASE("Triangle Scaling") {
529 Context ctx;
530 const float errtol = 0.0001f;
531
532 SUBCASE("scalePrimitive basic test") {
533 // Create a simple right triangle at the origin
534 vec3 v0 = make_vec3(0, 0, 0);
535 vec3 v1 = make_vec3(1, 0, 0);
536 vec3 v2 = make_vec3(0, 1, 0);
537 uint tri = ctx.addTriangle(v0, v1, v2);
538
539 // Get initial vertices and area
540 std::vector<vec3> verts_before = ctx.getPrimitiveVertices(tri);
541 float area_before = ctx.getPrimitiveArea(tri);
542
543 // Apply uniform 2x scaling
544 ctx.scalePrimitive(tri, make_vec3(2, 2, 2));
545
546 // Get vertices and area after scaling
547 std::vector<vec3> verts_after = ctx.getPrimitiveVertices(tri);
548 float area_after = ctx.getPrimitiveArea(tri);
549
550 // Expected: vertices should be doubled (scaling about origin)
551 // v0: (0,0,0) -> (0,0,0) [origin stays at origin]
552 // v1: (1,0,0) -> (2,0,0)
553 // v2: (0,1,0) -> (0,2,0)
554 // Area should be 4x larger (scale^2 for 2D)
555
556 DOCTEST_CHECK(verts_after[0].x == doctest::Approx(0.0f).epsilon(errtol));
557 DOCTEST_CHECK(verts_after[0].y == doctest::Approx(0.0f).epsilon(errtol));
558 DOCTEST_CHECK(verts_after[1].x == doctest::Approx(2.0f).epsilon(errtol));
559 DOCTEST_CHECK(verts_after[1].y == doctest::Approx(0.0f).epsilon(errtol));
560 DOCTEST_CHECK(verts_after[2].x == doctest::Approx(0.0f).epsilon(errtol));
561 DOCTEST_CHECK(verts_after[2].y == doctest::Approx(2.0f).epsilon(errtol));
562
563 DOCTEST_CHECK(area_after == doctest::Approx(4.0f * area_before).epsilon(errtol));
564 }
565
566 SUBCASE("scalePrimitiveAboutPoint test") {
567 // Create a simple right triangle at the origin
568 vec3 v0 = make_vec3(0, 0, 0);
569 vec3 v1 = make_vec3(1, 0, 0);
570 vec3 v2 = make_vec3(0, 1, 0);
571 uint tri = ctx.addTriangle(v0, v1, v2);
572
573 // Get initial area
574 float area_before = ctx.getPrimitiveArea(tri);
575
576 // Apply 2x scaling about origin
577 ctx.scalePrimitiveAboutPoint(tri, make_vec3(2, 2, 2), make_vec3(0, 0, 0));
578
579 // Get area after scaling
580 float area_after = ctx.getPrimitiveArea(tri);
581
582 // Expected: should behave same as scalePrimitive when scaling about origin
583 DOCTEST_CHECK(area_after == doctest::Approx(4.0f * area_before).epsilon(errtol));
584 }
585
586 SUBCASE("scalePrimitiveAboutPoint - scale about centroid") {
587 // Create a triangle NOT at the origin
588 vec3 v0 = make_vec3(1, 1, 0);
589 vec3 v1 = make_vec3(2, 1, 0);
590 vec3 v2 = make_vec3(1, 2, 0);
591 uint tri = ctx.addTriangle(v0, v1, v2);
592
593 // Calculate centroid
594 std::vector<vec3> verts_before = ctx.getPrimitiveVertices(tri);
595 vec3 center = make_vec3(0, 0, 0);
596 for (const auto &v: verts_before) {
597 center = center + v;
598 }
599 center = center / float(verts_before.size());
600
601 float area_before = ctx.getPrimitiveArea(tri);
602
603 // Scale by 0.5 about the centroid (like user's code)
604 ctx.scalePrimitiveAboutPoint(tri, make_vec3(0.5f, 0.5f, 0.5f), center);
605
606 // Get area after scaling
607 float area_after = ctx.getPrimitiveArea(tri);
608
609 // Expected: area should be 0.25x (scale^2)
610 DOCTEST_CHECK(area_after == doctest::Approx(0.25f * area_before).epsilon(errtol));
611 }
612
613 SUBCASE("triangle in compound object") {
614 // Create a compound object with a triangle
615 std::vector<uint> UUIDs;
616 UUIDs.push_back(ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0)));
617 uint objID = ctx.addPolymeshObject(UUIDs);
618
619 uint tri = UUIDs[0];
620 float area_before = ctx.getPrimitiveArea(tri);
621
622 // Try to scale the triangle (should be blocked)
623 bool has_warning;
624 {
625 capture_cerr cerr_buffer;
626 ctx.scalePrimitiveAboutPoint(tri, make_vec3(2, 2, 2), make_vec3(0, 0, 0));
627 has_warning = cerr_buffer.has_output();
628 } // cerr_buffer destroyed here
629 DOCTEST_CHECK(has_warning); // Should print warning
630
631 float area_after = ctx.getPrimitiveArea(tri);
632
633 // Area should NOT change (scaling blocked for compound objects)
634 DOCTEST_CHECK(area_after == doctest::Approx(area_before).epsilon(errtol));
635 }
636}
637
638TEST_CASE("Object Management") {
639 SUBCASE("addBoxObject") {
640 Context context_test;
641
642 vec3 center = make_vec3(1, 2, 3);
643 vec3 size = make_vec3(3, 2, 1);
644 int3 subdiv(1, 1, 1);
645
646 uint objID;
647 DOCTEST_CHECK_NOTHROW(objID = context_test.addBoxObject(center, size, subdiv));
648 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
649
650 DOCTEST_CHECK(UUIDs.size() == 6);
651 vec3 normal_r = context_test.getPrimitiveNormal(UUIDs.at(0));
652 DOCTEST_CHECK(doctest::Approx(normal_r.magnitude()).epsilon(errtol) == 1.f);
653 normal_r = context_test.getPrimitiveNormal(UUIDs.at(2));
654 DOCTEST_CHECK(doctest::Approx(normal_r.magnitude()).epsilon(errtol) == 1.f);
655
656 vec2 size_r = context_test.getPatchSize(UUIDs.at(0));
657 DOCTEST_CHECK(size_r.x == doctest::Approx(size.x).epsilon(errtol));
658 DOCTEST_CHECK(size_r.y == doctest::Approx(size.z).epsilon(errtol));
659
660 size_r = context_test.getPatchSize(UUIDs.at(2));
661 DOCTEST_CHECK(size_r.x == doctest::Approx(size.y).epsilon(errtol));
662 DOCTEST_CHECK(size_r.y == doctest::Approx(size.z).epsilon(errtol));
663
664 float volume = context_test.getBoxObjectVolume(objID);
665 DOCTEST_CHECK(volume == doctest::Approx(size.x * size.y * size.z).epsilon(errtol));
666 }
667 SUBCASE("addTileObject rotated") {
668 Context context_test;
669
670 vec3 center = make_vec3(1, 2, 3);
671 vec2 size = make_vec2(3, 2);
672 int2 subdiv(3, 3);
673 SphericalCoord rotation = make_SphericalCoord(0.25f * PI_F, 1.4f * PI_F);
674 uint objID = context_test.addTileObject(center, size, rotation, subdiv);
675
676 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
677 for (uint UUIDp: UUIDs) {
678 vec3 n = context_test.getPrimitiveNormal(UUIDp);
679 SphericalCoord rot = cart2sphere(n);
680 DOCTEST_CHECK(rot.zenith == doctest::Approx(rotation.zenith).epsilon(errtol));
681 DOCTEST_CHECK(rot.azimuth == doctest::Approx(rotation.azimuth).epsilon(errtol));
682 }
683 }
684 SUBCASE("textured tile area") {
685 Context context_test;
686
687 vec3 center = make_vec3(1, 2, 3);
688 vec2 size = make_vec2(3, 2);
689 int2 subdiv = make_int2(5, 5);
690 SphericalCoord rotation = make_SphericalCoord(0.1f * PI_F, 2.4f * PI_F);
691
692 uint objID = context_test.addTileObject(center, size, rotation, subdiv, "lib/images/disk_texture.png");
693 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
694 float area_sum = 0.f;
695 for (uint UUID: UUIDs) {
696 area_sum += context_test.getPrimitiveArea(UUID);
697 }
698 float area_exact = 0.25f * PI_F * size.x * size.y;
699 DOCTEST_CHECK(area_sum == doctest::Approx(area_exact).epsilon(5e-3));
700 }
701 SUBCASE("cone object transforms") {
702 Context context_test;
703 float r0 = 0.5f, r1 = 1.f, len = 2.f;
704 vec3 node0 = make_vec3(0, 0, 0);
705 vec3 node1 = make_vec3(0, 0, len);
706 uint cone = context_test.addConeObject(50, node0, node1, r0, r1);
707 context_test.translateObject(cone, make_vec3(1, 1, 1));
708 std::vector<vec3> nodes = context_test.getConeObjectNodes(cone);
709 DOCTEST_CHECK(nodes.at(0) == make_vec3(1, 1, 1));
710 DOCTEST_CHECK(nodes.at(1) == make_vec3(1, 1, 1 + len));
711 vec3 axis = cross(make_vec3(0, 0, 1), make_vec3(1, 0, 0));
712 float ang = acos_safe(make_vec3(1, 0, 0) * make_vec3(0, 0, 1));
713 context_test.translateObject(cone, -nodes.at(0));
714 context_test.rotateObject(cone, ang, axis);
715 context_test.translateObject(cone, nodes.at(0));
716 nodes = context_test.getConeObjectNodes(cone);
717 DOCTEST_CHECK(nodes.at(1).x == doctest::Approx(nodes.at(0).x + len).epsilon(errtol));
718 context_test.scaleConeObjectLength(cone, 2.0);
719 nodes = context_test.getConeObjectNodes(cone);
720 DOCTEST_CHECK(nodes.at(1).x == doctest::Approx(nodes.at(0).x + 2 * len).epsilon(errtol));
721 context_test.scaleConeObjectGirth(cone, 2.0);
722 std::vector<float> radii = context_test.getConeObjectNodeRadii(cone);
723 DOCTEST_CHECK(radii.at(0) == doctest::Approx(2 * r0).epsilon(errtol));
724 DOCTEST_CHECK(radii.at(1) == doctest::Approx(2 * r1).epsilon(errtol));
725 }
726
727 SUBCASE("rotate and scale objects") {
728 Context ctx;
729 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
730 ctx.rotateObject(obj, 0.5f * PI_F, "z");
731 vec3 bmin, bmax;
732 ctx.getObjectBoundingBox(obj, bmin, bmax);
733 DOCTEST_CHECK(bmax.x == doctest::Approx(0.5f).epsilon(errtol));
734
735 ctx.scaleObjectAboutPoint(obj, make_vec3(2.f, 2.f, 2.f), make_vec3(0, 0, 0));
736 ctx.getObjectBoundingBox(obj, bmin, bmax);
737 DOCTEST_CHECK(bmax.x > 0.5f);
738 }
739
740 SUBCASE("domain bounding sphere") {
741 Context ctx;
742 std::vector<uint> ids;
743 ids.push_back(ctx.addPatch(make_vec3(-1, 0, 0), make_vec2(1, 1)));
744 ids.push_back(ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1)));
745 vec3 c;
746 float r;
747 ctx.getDomainBoundingSphere(ids, c, r);
748 DOCTEST_CHECK(c.x == doctest::Approx(0.f).epsilon(errtol));
749 DOCTEST_CHECK(r > 1.f);
750 }
751
752 SUBCASE("copy and delete objects") {
753 Context ctx;
754 uint obj1 = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
755 uint obj2 = ctx.copyObject(obj1);
756 DOCTEST_CHECK(ctx.doesObjectExist(obj1));
757 DOCTEST_CHECK(ctx.doesObjectExist(obj2));
758 ctx.deleteObject(obj2);
759 DOCTEST_CHECK(!ctx.doesObjectExist(obj2));
760 }
761
762 SUBCASE("copy object with texture override preserves color") {
763 capture_cerr cerr_buffer; // Capture deprecation warnings from setPrimitiveColor/overridePrimitiveTextureColor
764 Context ctx;
765
766 // Create a tile with texture
767 std::vector<uint> UUIDs = ctx.addTile(nullorigin, make_vec2(1, 1), nullrotation, make_int2(2, 2), "lib/images/disk_texture.png");
768
769 // Set color and override texture - these trigger deprecation warnings (once per execution)
770 RGBcolor green_color = make_RGBcolor(0, 1, 0);
771 ctx.setPrimitiveColor(UUIDs, green_color);
772 ctx.overridePrimitiveTextureColor(UUIDs);
773
774 // Create object from primitives
775 uint objID = ctx.addPolymeshObject(UUIDs);
776
777 // Verify original object has correct color and texture override
778 DOCTEST_CHECK(ctx.getPrimitiveColor(UUIDs[0]) == green_color);
779 DOCTEST_CHECK(ctx.isPrimitiveTextureColorOverridden(UUIDs[0]));
780
781 // Copy the object
782 uint objID_copy = ctx.copyObject(objID);
783 std::vector<uint> UUIDs_copy = ctx.getObjectPrimitiveUUIDs(objID_copy);
784
785 // Verify copied object preserves both color and texture override
786 DOCTEST_CHECK(ctx.getPrimitiveColor(UUIDs_copy[0]) == green_color);
787 DOCTEST_CHECK(ctx.isPrimitiveTextureColorOverridden(UUIDs_copy[0]));
788
789 // Test with Triangle as well
790 uint triangle = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0), "lib/images/disk_texture.png", make_vec2(0, 0), make_vec2(1, 0), make_vec2(0, 1));
791 RGBcolor blue_color = make_RGBcolor(0, 0, 1);
792 ctx.setPrimitiveColor(triangle, blue_color);
793 ctx.overridePrimitiveTextureColor(triangle);
794
795 std::vector<uint> triangle_UUIDs = {triangle};
796 uint triangle_obj = ctx.addPolymeshObject(triangle_UUIDs);
797 uint triangle_obj_copy = ctx.copyObject(triangle_obj);
798 std::vector<uint> triangle_UUIDs_copy = ctx.getObjectPrimitiveUUIDs(triangle_obj_copy);
799
800 DOCTEST_CHECK(ctx.getPrimitiveColor(triangle_UUIDs_copy[0]) == blue_color);
801 DOCTEST_CHECK(ctx.isPrimitiveTextureColorOverridden(triangle_UUIDs_copy[0]));
802 }
803
804 SUBCASE("domain cropping") {
805 Context ctx;
806 uint p1 = ctx.addPatch(make_vec3(-2.f, 0.f, 0.f), make_vec2(1, 1));
807 uint p2 = ctx.addPatch(make_vec3(2.f, 0.f, 0.f), make_vec2(1, 1));
808 uint p3 = ctx.addPatch(make_vec3(0.f, 3.f, 0.f), make_vec2(1, 1));
809 uint p4 = ctx.addPatch(make_vec3(0.f, 0.f, 3.f), make_vec2(1, 1));
810
811 bool has_output1, has_output2;
812 {
813 capture_cerr cerr_buffer;
814 ctx.cropDomainX(make_vec2(-1.f, 1.f));
815 DOCTEST_CHECK(!ctx.doesPrimitiveExist(p1));
816 ctx.cropDomainY(make_vec2(-1.f, 1.f));
817 DOCTEST_CHECK(!ctx.doesPrimitiveExist(p3));
818 ctx.cropDomainZ(make_vec2(-1.f, 1.f));
819 DOCTEST_CHECK(!ctx.doesPrimitiveExist(p4));
820 has_output1 = cerr_buffer.has_output();
821 }
822 DOCTEST_CHECK(has_output1);
823
824 {
825 capture_cerr cerr_buffer;
826 std::vector<uint> ids_rem = ctx.getAllUUIDs();
827 ctx.cropDomain(ids_rem, make_vec2(-0.5f, 1.f), make_vec2(-0.5f, 1.f), make_vec2(-0.5f, 1.f));
828 DOCTEST_CHECK(!ctx.doesPrimitiveExist(p2));
829 has_output2 = cerr_buffer.has_output();
830 }
831 DOCTEST_CHECK(has_output2);
832 }
833}
834
835TEST_CASE("Data Management") {
836 SUBCASE("global and object data") {
837 Context context_test;
838 float gdata = 5.f;
839 context_test.setGlobalData("some_data", gdata);
840 float gdata_r;
841 DOCTEST_CHECK(context_test.doesGlobalDataExist("some_data"));
842 context_test.getGlobalData("some_data", gdata_r);
843 DOCTEST_CHECK(gdata_r == gdata);
844
845 std::vector<float> gvec{0, 1, 2, 3, 4};
846 context_test.setGlobalData("vec", gvec);
847 std::vector<float> gvec_r;
848 context_test.getGlobalData("vec", gvec_r);
849 DOCTEST_CHECK(gvec_r == gvec);
850
851 uint objID = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(3, 1), nullrotation, make_int2(3, 3));
852 float objdata = 7.f;
853 context_test.setObjectData(objID, "obj", objdata);
854 float objdata_r;
855 context_test.getObjectData(objID, "obj", objdata_r);
856 DOCTEST_CHECK(objdata_r == objdata);
857 }
858 SUBCASE("timeseries") {
859 Context ctx;
860 Date date = make_Date(12, 3, 2010);
861 ctx.setDate(date);
862 Time time0 = make_Time(13, 15, 39);
863 ctx.setTime(time0);
864 Time time1 = make_Time(time0.hour, 49, 14);
865 ctx.addTimeseriesData("ts", 302.3f, date, time0);
866 ctx.addTimeseriesData("ts", 305.3f, date, time1);
867 ctx.setCurrentTimeseriesPoint("ts", 0);
868 DOCTEST_CHECK(ctx.getTimeseriesLength("ts") == 2);
869 DOCTEST_CHECK(ctx.queryTimeseriesData("ts", 0) == doctest::Approx(302.3f));
870 DOCTEST_CHECK(ctx.queryTimeseriesData("ts", 1) == doctest::Approx(305.3f));
871 float val = ctx.queryTimeseriesData("ts", date, time1);
872 DOCTEST_CHECK(val == doctest::Approx(305.3f));
873 DOCTEST_CHECK(ctx.doesTimeseriesVariableExist("ts"));
874 std::vector<std::string> labels = ctx.listTimeseriesVariables();
875 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "ts") != labels.end());
876 DOCTEST_CHECK(ctx.queryTimeseriesData("ts", ctx.getTimeseriesLength("ts") - 1) == doctest::Approx(305.3f));
877 Time t1_r = ctx.queryTimeseriesTime("ts", 1);
878 Date d1_r = ctx.queryTimeseriesDate("ts", 1);
879 DOCTEST_CHECK(t1_r.minute == time1.minute);
880 DOCTEST_CHECK(d1_r.day == date.day);
881 ctx.setCurrentTimeseriesPoint("ts", 1);
882 DOCTEST_CHECK(ctx.queryTimeseriesData("ts") == doctest::Approx(305.3f));
883 }
884
885 SUBCASE("Primitive data") {
886 capture_cerr cerr_buffer;
887
888 Context ctx;
889 uint p = ctx.addPatch();
890 ctx.setPrimitiveData(p, "test_int", 5);
891 ctx.setPrimitiveData(p, "test_float", 3.14f);
892
893 // getPrimitiveDataType
894 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_int") == HELIOS_TYPE_INT);
895 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_float") == HELIOS_TYPE_FLOAT);
896
897 // getPrimitiveDataSize
898 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_int") == 1);
899
900 // clearPrimitiveData
901 ctx.clearPrimitiveData(p, "test_int");
902 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p, "test_int"));
903
904 // listPrimitiveData
905 std::vector<std::string> data_labels = ctx.listPrimitiveData(p);
906 DOCTEST_CHECK(std::find(data_labels.begin(), data_labels.end(), "test_float") != data_labels.end());
907
908 // getPrimitiveDataSize (doesn't exist)
909 DOCTEST_CHECK_THROWS(ctx.getPrimitiveDataSize(p, "test_int"));
910
911 // clearPrimitiveData
912 ctx.clearPrimitiveData(p, "test_int");
913 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p, "test_int"));
914
915 // listPrimitiveData
916 ctx.setPrimitiveData(p, "test_int", 5);
917 ctx.setPrimitiveData(p, "test_float", 3.14f);
918 std::vector<std::string> labels = ctx.listPrimitiveData(p);
919 DOCTEST_CHECK(labels.size() == 2);
920 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "test_int") != labels.end());
921 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "test_float") != labels.end());
922 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_float") == HELIOS_TYPE_FLOAT);
923 }
924}
925
926TEST_CASE("Data and Object Management") {
927
928 SUBCASE("Global data management") {
929 Context ctx;
930 ctx.setGlobalData("test_double", 1.23);
931 DOCTEST_CHECK(ctx.getGlobalDataSize("test_double") == 1);
932 DOCTEST_CHECK(ctx.getGlobalDataType("test_double") == HELIOS_TYPE_DOUBLE);
933 ctx.clearGlobalData("test_double");
934 DOCTEST_CHECK(!ctx.doesGlobalDataExist("test_double"));
935 ctx.setGlobalData("test_string", "hello");
936 std::vector<std::string> global_data_labels = ctx.listGlobalData();
937 DOCTEST_CHECK(std::find(global_data_labels.begin(), global_data_labels.end(), "test_string") != global_data_labels.end());
938 }
939
940 SUBCASE("Object data management") {
941 Context ctx;
942 uint obj = ctx.addBoxObject(nullorigin, make_vec3(1, 1, 1), make_int3(2, 3, 2));
943 ctx.setObjectData(obj, "test_vec", vec3(1, 2, 3));
944 DOCTEST_CHECK(ctx.getObjectDataSize(obj, "test_vec") == 1);
945 DOCTEST_CHECK(ctx.getObjectDataType("test_vec") == HELIOS_TYPE_VEC3);
946 ctx.clearObjectData(obj, "test_vec");
947 DOCTEST_CHECK(!ctx.doesObjectDataExist(obj, "test_vec"));
948 ctx.setObjectData(obj, "test_int", 42);
949 std::vector<std::string> object_data_labels = ctx.listObjectData(obj);
950 DOCTEST_CHECK(std::find(object_data_labels.begin(), object_data_labels.end(), "test_int") != object_data_labels.end());
951 }
952
953 SUBCASE("Object creation and manipulation") {
954 Context ctx;
955 uint disk = ctx.addDiskObject(10, make_vec3(0, 0, 0), make_vec2(1, 1));
956 DOCTEST_CHECK(ctx.getObjectType(disk) == OBJECT_TYPE_DISK);
957 DOCTEST_CHECK(ctx.getObjectArea(disk) > 0);
958 DOCTEST_CHECK(ctx.getDiskObjectCenter(disk) == make_vec3(0, 0, 0));
959 DOCTEST_CHECK(ctx.getDiskObjectSubdivisionCount(disk) == 10);
960 DOCTEST_CHECK(ctx.getDiskObjectSize(disk).x == doctest::Approx(1.f));
961
962 uint sphere = ctx.addSphereObject(10, make_vec3(1, 1, 1), 0.5f);
963 DOCTEST_CHECK(ctx.getObjectType(sphere) == OBJECT_TYPE_SPHERE);
964 DOCTEST_CHECK(ctx.getObjectArea(sphere) > 0);
965 DOCTEST_CHECK(ctx.getSphereObjectCenter(sphere) == make_vec3(1, 1, 1));
966 DOCTEST_CHECK(ctx.getSphereObjectSubdivisionCount(sphere) == 10);
967 DOCTEST_CHECK(ctx.getSphereObjectRadius(sphere).x == doctest::Approx(0.5f));
968
969 std::vector<uint> p_uuids;
970 p_uuids.push_back(ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0)));
971 uint polymesh = ctx.addPolymeshObject(p_uuids);
972 DOCTEST_CHECK(ctx.getObjectType(polymesh) == OBJECT_TYPE_POLYMESH);
973 DOCTEST_CHECK(ctx.getObjectArea(polymesh) > 0);
974 DOCTEST_CHECK(ctx.getObjectCenter(polymesh).z == doctest::Approx(0.f));
975
976 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1)};
977 std::vector<float> radii = {0.2f, 0.1f};
978 uint tube = ctx.addTubeObject(10, nodes, radii);
979 DOCTEST_CHECK(ctx.getObjectType(tube) == OBJECT_TYPE_TUBE);
980 DOCTEST_CHECK(ctx.getObjectArea(tube) > 0);
981 DOCTEST_CHECK(ctx.getObjectCenter(tube).z == doctest::Approx(0.5f));
982 DOCTEST_CHECK(ctx.getTubeObjectSubdivisionCount(tube) == 10);
983 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 2);
984 DOCTEST_CHECK(ctx.getTubeObjectNodeRadii(tube).size() == 2);
985 DOCTEST_CHECK(ctx.getTubeObjectNodeColors(tube).size() == 2);
986 DOCTEST_CHECK(ctx.getTubeObjectVolume(tube) > 0);
987 ctx.appendTubeSegment(tube, make_vec3(0, 0, 2), 0.05f, RGB::red);
988 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 3);
989 ctx.scaleTubeGirth(tube, 2.f);
990 DOCTEST_CHECK(ctx.getTubeObjectNodeRadii(tube)[0] == doctest::Approx(0.4f));
991 std::vector<float> new_radii = {0.3f, 0.2f, 0.1f};
992 ctx.setTubeRadii(tube, new_radii);
993 DOCTEST_CHECK(ctx.getTubeObjectNodeRadii(tube)[0] == doctest::Approx(0.3f));
994 ctx.scaleTubeLength(tube, 2.f);
995 std::vector<vec3> new_nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
996 ctx.setTubeNodes(tube, new_nodes);
997 ctx.pruneTubeNodes(tube, 1);
998 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 1);
999 }
1000
1001 SUBCASE("Object appearance and visibility") {
1002 Context ctx;
1003 uint box = ctx.addBoxObject(nullorigin, make_vec3(1, 1, 1), make_int3(2, 3, 2));
1004 ctx.overrideObjectTextureColor(box);
1005 // Cannot check state, only that it runs
1006 ctx.useObjectTextureColor(box);
1007 // Cannot check state, only that it runs
1008 ctx.hideObject(box);
1009 DOCTEST_CHECK(ctx.isObjectHidden(box));
1010 ctx.showObject(box);
1011 DOCTEST_CHECK(!ctx.isObjectHidden(box));
1012
1013 std::vector<uint> prims = ctx.getObjectPrimitiveUUIDs(box);
1014 ctx.hidePrimitive(prims);
1015 DOCTEST_CHECK(ctx.isPrimitiveHidden(prims[0]));
1016 ctx.showPrimitive(prims);
1017 DOCTEST_CHECK(!ctx.isPrimitiveHidden(prims[0]));
1018 }
1019
1020 SUBCASE("Primitive color and parent object") {
1021 capture_cerr cerr_buffer; // Capture deprecation warnings from setPrimitiveColor/usePrimitiveTextureColor
1022 Context ctx;
1023 uint p = ctx.addPatch();
1024 ctx.setPrimitiveColor(p, RGB::red);
1025 DOCTEST_CHECK(ctx.getPrimitiveColor(p) == RGB::red);
1026 ctx.overridePrimitiveTextureColor(p);
1027 DOCTEST_CHECK(ctx.isPrimitiveTextureColorOverridden(p));
1028 ctx.usePrimitiveTextureColor(p);
1029 DOCTEST_CHECK(!ctx.isPrimitiveTextureColorOverridden(p));
1030
1031 uint obj = ctx.addBoxObject(nullorigin, make_vec3(1, 1, 1), make_int3(2, 3, 2));
1032 ctx.setPrimitiveParentObjectID(p, obj);
1033 DOCTEST_CHECK(ctx.getPrimitiveParentObjectID(p) == obj);
1034 }
1035}
1036TEST_CASE("Object Management: Creation and Properties") {
1037
1038 SUBCASE("addSphereObject") {
1039 Context ctx;
1040 uint objID = ctx.addSphereObject(10, make_vec3(1, 2, 3), 5.f);
1041 DOCTEST_CHECK(ctx.doesObjectExist(objID));
1042 DOCTEST_CHECK(ctx.getSphereObjectCenter(objID) == make_vec3(1, 2, 3));
1043 DOCTEST_CHECK(ctx.getSphereObjectRadius(objID) == make_vec3(5.f, 5.f, 5.f));
1044 DOCTEST_CHECK(ctx.getSphereObjectSubdivisionCount(objID) == 10);
1045 }
1046
1047 SUBCASE("addDiskObject") {
1048 Context ctx;
1049 uint objID = ctx.addDiskObject(make_int2(8, 16), make_vec3(1, 2, 3), make_vec2(4, 5), nullrotation, RGB::red);
1050 DOCTEST_CHECK(ctx.doesObjectExist(objID));
1051 DOCTEST_CHECK(ctx.getDiskObjectCenter(objID) == make_vec3(1, 2, 3));
1052 DOCTEST_CHECK(ctx.getDiskObjectSize(objID) == make_vec2(4, 5));
1053 DOCTEST_CHECK(ctx.getDiskObjectSubdivisionCount(objID) == 8u);
1054 }
1055
1056 SUBCASE("addConeObject") {
1057 Context ctx;
1058 uint objID = ctx.addConeObject(10, make_vec3(0, 0, 0), make_vec3(0, 0, 5), 2.f, 1.f);
1059 DOCTEST_CHECK(ctx.doesObjectExist(objID));
1060 DOCTEST_CHECK(ctx.getConeObjectNode(objID, 0) == make_vec3(0, 0, 0));
1061 DOCTEST_CHECK(ctx.getConeObjectNode(objID, 1) == make_vec3(0, 0, 5));
1062 DOCTEST_CHECK(ctx.getConeObjectNodeRadius(objID, 0) == 2.f);
1063 DOCTEST_CHECK(ctx.getConeObjectNodeRadius(objID, 1) == 1.f);
1064 DOCTEST_CHECK(ctx.getConeObjectSubdivisionCount(objID) == 10);
1065 }
1066}
1067
1068TEST_CASE("Global Data Management") {
1069 SUBCASE("Integer Data") {
1070 Context ctx;
1071 ctx.setGlobalData("test_int", 123);
1072 DOCTEST_CHECK(ctx.doesGlobalDataExist("test_int"));
1073 int val;
1074 ctx.getGlobalData("test_int", val);
1075 DOCTEST_CHECK(val == 123);
1076 DOCTEST_CHECK(ctx.getGlobalDataSize("test_int") == 1);
1077 DOCTEST_CHECK(ctx.getGlobalDataType("test_int") == HELIOS_TYPE_INT);
1078 ctx.clearGlobalData("test_int");
1079 DOCTEST_CHECK(!ctx.doesGlobalDataExist("test_int"));
1080 }
1081
1082 SUBCASE("Vector Data") {
1083 Context ctx;
1084 std::vector<vec3> vec_data = {{1, 2, 3}, {4, 5, 6}};
1085 ctx.setGlobalData("test_vec", vec_data);
1086 DOCTEST_CHECK(ctx.doesGlobalDataExist("test_vec"));
1087 std::vector<vec3> read_vec;
1088 ctx.getGlobalData("test_vec", read_vec);
1089 DOCTEST_CHECK(read_vec.size() == 2);
1090 DOCTEST_CHECK(read_vec[1] == make_vec3(4, 5, 6));
1091 DOCTEST_CHECK(ctx.getGlobalDataSize("test_vec") == 2);
1092 DOCTEST_CHECK(ctx.getGlobalDataType("test_vec") == HELIOS_TYPE_VEC3);
1093 }
1094
1095 SUBCASE("List Data") {
1096 Context ctx;
1097 ctx.setGlobalData("d1", 1);
1098 ctx.setGlobalData("d2", 2.f);
1099 std::vector<std::string> labels = ctx.listGlobalData();
1100 DOCTEST_CHECK(labels.size() == 2);
1101 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "d1") != labels.end());
1102 }
1103}
1104
1105TEST_CASE("Context primitive data management") {
1106 Context ctx;
1107 uint p1 = ctx.addPatch();
1108 uint p2 = ctx.addPatch();
1109 ctx.setPrimitiveData(p1, "my_data", 10);
1110
1111 // copyPrimitiveData
1112 ctx.copyPrimitiveData(p1, p2);
1113 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p2, "my_data"));
1114 int val;
1115 ctx.getPrimitiveData(p2, "my_data", val);
1116 DOCTEST_CHECK(val == 10);
1117
1118 // renamePrimitiveData
1119 ctx.renamePrimitiveData(p1, "my_data", "new_data_name");
1120 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p1, "my_data"));
1121 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p1, "new_data_name"));
1122
1123 // duplicatePrimitiveData
1124 ctx.duplicatePrimitiveData(p2, "my_data", "my_data_copy");
1125 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p2, "my_data_copy"));
1126 ctx.getPrimitiveData(p2, "my_data_copy", val);
1127 DOCTEST_CHECK(val == 10);
1128
1129 // duplicatePrimitiveData (all primitives)
1130 ctx.setPrimitiveData(p1, "global_copy_test", 5.5f);
1131 ctx.duplicatePrimitiveData("global_copy_test", "global_copy_test_new");
1132 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p1, "global_copy_test_new"));
1133 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p2, "global_copy_test_new")); // p2 doesn't have original
1134
1135 ctx.clearPrimitiveData(p1, "new_data_name");
1136 ctx.setPrimitiveData(p2, "my_data_copy", 15);
1137 ctx.setPrimitiveData(p2, "my_data_copy", 20);
1138 ctx.clearPrimitiveData(p2, "my_data_copy");
1139 std::vector<std::string> all_labels = ctx.listAllPrimitiveDataLabels();
1140 DOCTEST_CHECK(std::find(all_labels.begin(), all_labels.end(), "my_data") != all_labels.end());
1141 DOCTEST_CHECK(std::find(all_labels.begin(), all_labels.end(), "my_data_copy") == all_labels.end());
1142 DOCTEST_CHECK(std::find(all_labels.begin(), all_labels.end(), "new_data_name") == all_labels.end());
1143}
1144
1145TEST_CASE("Context primitive data calculations") {
1146 Context ctx;
1147 std::vector<uint> uuids;
1148 for (int i = 0; i < 5; ++i) {
1149 uint p = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
1150 ctx.setPrimitiveData(p, "float_val", (float) i);
1151 ctx.setPrimitiveData(p, "double_val", (double) i);
1152 ctx.setPrimitiveData(p, "vec2_val", make_vec2((float) i, (float) i));
1153 uuids.push_back(p);
1154 }
1155
1156 // calculatePrimitiveDataMean
1157 float float_mean;
1158 ctx.calculatePrimitiveDataMean(uuids, "float_val", float_mean);
1159 DOCTEST_CHECK(float_mean == doctest::Approx(2.0f));
1160 double double_mean;
1161 ctx.calculatePrimitiveDataMean(uuids, "double_val", double_mean);
1162 DOCTEST_CHECK(double_mean == doctest::Approx(2.0));
1163 vec2 vec2_mean;
1164 ctx.calculatePrimitiveDataMean(uuids, "vec2_val", vec2_mean);
1165 DOCTEST_CHECK(vec2_mean.x == doctest::Approx(2.0f));
1166
1167 // calculatePrimitiveDataAreaWeightedMean
1168 float awt_mean_f;
1169 ctx.calculatePrimitiveDataAreaWeightedMean(uuids, "float_val", awt_mean_f);
1170 DOCTEST_CHECK(awt_mean_f == doctest::Approx(2.0f)); // Area is 1 for all
1171
1172 // calculatePrimitiveDataSum
1173 float float_sum;
1174 ctx.calculatePrimitiveDataSum(uuids, "float_val", float_sum);
1175 DOCTEST_CHECK(float_sum == doctest::Approx(10.0f));
1176
1177 // calculatePrimitiveDataAreaWeightedSum
1178 float awt_sum_f;
1179 ctx.calculatePrimitiveDataAreaWeightedSum(uuids, "float_val", awt_sum_f);
1180 DOCTEST_CHECK(awt_sum_f == doctest::Approx(10.0f));
1181
1182 // scalePrimitiveData
1183 ctx.scalePrimitiveData(uuids, "float_val", 2.0f);
1184 ctx.getPrimitiveData(uuids[2], "float_val", float_mean);
1185 DOCTEST_CHECK(float_mean == doctest::Approx(4.0f));
1186 ctx.scalePrimitiveData("double_val", 0.5f);
1187 ctx.getPrimitiveData(uuids[4], "double_val", double_mean);
1188 DOCTEST_CHECK(double_mean == doctest::Approx(2.0));
1189
1190 // incrementPrimitiveData
1191 ctx.setPrimitiveData(uuids, "int_val", 10);
1192 ctx.incrementPrimitiveData(uuids, "int_val", 5);
1193 int int_val;
1194 ctx.getPrimitiveData(uuids[0], "int_val", int_val);
1195 DOCTEST_CHECK(int_val == 15);
1196 bool has_warning;
1197 {
1198 capture_cerr cerr_buffer;
1199 ctx.incrementPrimitiveData(uuids, "float_val", 1); // Wrong type, should warn
1200 has_warning = cerr_buffer.has_output();
1201 }
1202 DOCTEST_CHECK(has_warning);
1203}
1204
1205TEST_CASE("Context primitive data aggregation and filtering") {
1206 Context ctx;
1207 std::vector<uint> uuids;
1208 for (int i = 0; i < 3; ++i) {
1209 uint p = ctx.addPatch();
1210 ctx.setPrimitiveData(p, "d1", (float) i);
1211 ctx.setPrimitiveData(p, "d2", (float) i * 2.0f);
1212 ctx.setPrimitiveData(p, "d3", (float) i * 3.0f);
1213 ctx.setPrimitiveData(p, "filter_me", i);
1214 uuids.push_back(p);
1215 }
1216
1217 // aggregatePrimitiveDataSum
1218 std::vector<std::string> labels = {"d1", "d2", "d3"};
1219 ctx.aggregatePrimitiveDataSum(uuids, labels, "sum_data");
1220 float sum_val;
1221 ctx.getPrimitiveData(uuids[1], "sum_data", sum_val);
1222 DOCTEST_CHECK(sum_val == doctest::Approx(1.f + 2.f + 3.f));
1223
1224 // aggregatePrimitiveDataProduct
1225 ctx.aggregatePrimitiveDataProduct(uuids, labels, "prod_data");
1226 float prod_val;
1227 ctx.getPrimitiveData(uuids[2], "prod_data", prod_val);
1228 DOCTEST_CHECK(prod_val == doctest::Approx(2.f * 4.f * 6.f));
1229
1230 // filterPrimitivesByData
1231 std::vector<uint> filtered = ctx.filterPrimitivesByData(uuids, "filter_me", 1, ">=");
1232 DOCTEST_CHECK(filtered.size() == 2);
1233 filtered = ctx.filterPrimitivesByData(uuids, "filter_me", 1, "==");
1234 DOCTEST_CHECK(filtered.size() == 1);
1235 DOCTEST_CHECK(filtered[0] == uuids[1]);
1236 capture_cerr cerr_buffer;
1237 DOCTEST_CHECK_THROWS_AS(filtered = ctx.filterPrimitivesByData(uuids, "filter_me", 1, "!!"), std::runtime_error);
1238}
1239
1240TEST_CASE("Object data") {
1241 Context ctx;
1242 uint o = ctx.addTileObject(nullorigin, make_vec2(1, 1), nullrotation, make_int2(2, 2));
1243 ctx.setObjectData(o, "test_int", 5);
1244 ctx.setObjectData(o, "test_float", 3.14f);
1245
1246 // getObjectDataType
1247 DOCTEST_CHECK(ctx.getObjectDataType("test_int") == HELIOS_TYPE_INT);
1248#ifdef HELIOS_DEBUG
1249 capture_cerr cerr_buffer;
1250 DOCTEST_CHECK_THROWS_AS(ctx.getObjectDataType("non_existent"), std::runtime_error);
1251#endif
1252
1253 // getObjectDataSize
1254 DOCTEST_CHECK(ctx.getObjectDataSize(o, "test_int") == 1);
1255
1256 // clearObjectData
1257 ctx.clearObjectData(o, "test_int");
1258 DOCTEST_CHECK(!ctx.doesObjectDataExist(o, "test_int"));
1259
1260 // listObjectData
1261 std::vector<std::string> data_labels = ctx.listObjectData(o);
1262 DOCTEST_CHECK(std::find(data_labels.begin(), data_labels.end(), "test_float") != data_labels.end());
1263}
1264
1265TEST_CASE("Context object data management") {
1266 Context ctx;
1267 uint o1 = ctx.addTileObject(nullorigin, make_vec2(1, 1), nullrotation, make_int2(2, 2));
1268 uint o2 = ctx.addTileObject(nullorigin, make_vec2(1, 1), nullrotation, make_int2(2, 2));
1269 ctx.setObjectData(o1, "my_data", 10);
1270
1271 // copyObjectData
1272 ctx.copyObjectData(o1, o2);
1273 DOCTEST_CHECK(ctx.doesObjectDataExist(o2, "my_data"));
1274
1275 // renameObjectData
1276 ctx.renameObjectData(o1, "my_data", "new_name");
1277 DOCTEST_CHECK(!ctx.doesObjectDataExist(o1, "my_data"));
1278 DOCTEST_CHECK(ctx.doesObjectDataExist(o1, "new_name"));
1279
1280 // duplicateObjectData
1281 ctx.duplicateObjectData(o2, "my_data", "my_data_copy");
1282 DOCTEST_CHECK(ctx.doesObjectDataExist(o2, "my_data_copy"));
1283
1284 std::vector<std::string> all_obj_labels = ctx.listAllObjectDataLabels();
1285 DOCTEST_CHECK(std::find(all_obj_labels.begin(), all_obj_labels.end(), "my_data") != all_obj_labels.end());
1286 DOCTEST_CHECK(std::find(all_obj_labels.begin(), all_obj_labels.end(), "my_data_copy") != all_obj_labels.end());
1287 DOCTEST_CHECK(std::find(all_obj_labels.begin(), all_obj_labels.end(), "new_name") != all_obj_labels.end());
1288}
1289
1290TEST_CASE("Global data") {
1291 Context ctx;
1292 ctx.setGlobalData("g_int", 5);
1293 ctx.setGlobalData("g_float", 3.14f);
1294
1295 // getGlobalDataType/Size/Exists
1296 DOCTEST_CHECK(ctx.doesGlobalDataExist("g_int"));
1297 DOCTEST_CHECK(ctx.getGlobalDataType("g_int") == HELIOS_TYPE_INT);
1298 DOCTEST_CHECK(ctx.getGlobalDataSize("g_int") == 1);
1299
1300 // rename/duplicate/clear
1301 ctx.duplicateGlobalData("g_int", "g_int_copy");
1302 DOCTEST_CHECK(ctx.doesGlobalDataExist("g_int_copy"));
1303 ctx.renameGlobalData("g_int", "g_int_new");
1304 DOCTEST_CHECK(!ctx.doesGlobalDataExist("g_int"));
1305 DOCTEST_CHECK(ctx.doesGlobalDataExist("g_int_new"));
1306 ctx.clearGlobalData("g_int_new");
1307 DOCTEST_CHECK(!ctx.doesGlobalDataExist("g_int_new"));
1308
1309 // listGlobalData
1310 std::vector<std::string> g_labels = ctx.listGlobalData();
1311 DOCTEST_CHECK(g_labels.size() > 0);
1312
1313 // incrementGlobalData
1314 ctx.setGlobalData("inc_me", 10);
1315 ctx.incrementGlobalData("inc_me", 5);
1316 int val;
1317 ctx.getGlobalData("inc_me", val);
1318 DOCTEST_CHECK(val == 15);
1319 bool has_warning;
1320 {
1321 capture_cerr cerr_buffer;
1322 ctx.incrementGlobalData("g_float", 1); // Wrong type
1323 has_warning = cerr_buffer.has_output();
1324 }
1325 DOCTEST_CHECK(has_warning);
1326}
1327
1328TEST_CASE("Voxel Management") {
1329 SUBCASE("addVoxel and voxel properties") {
1330 Context ctx;
1331
1332 vec3 center = make_vec3(1, 2, 3);
1333 vec3 size = make_vec3(2, 4, 6);
1334 float rotation = 0.5f * PI_F;
1335
1336 uint vox1 = ctx.addVoxel(center, size);
1337 DOCTEST_CHECK(ctx.getPrimitiveType(vox1) == PRIMITIVE_TYPE_VOXEL);
1338 DOCTEST_CHECK(ctx.getVoxelCenter(vox1) == center);
1339 DOCTEST_CHECK(ctx.getVoxelSize(vox1) == size);
1340
1341 uint vox2 = ctx.addVoxel(center, size, rotation);
1342 DOCTEST_CHECK(ctx.getVoxelCenter(vox2) == center);
1343 DOCTEST_CHECK(ctx.getVoxelSize(vox2) == size);
1344
1345 uint vox3 = ctx.addVoxel(center, size, rotation, RGB::red);
1346 DOCTEST_CHECK(ctx.getPrimitiveColor(vox3) == RGB::red);
1347
1348 uint vox4 = ctx.addVoxel(center, size, rotation, RGBA::red);
1349 RGBAcolor color_rgba = ctx.getPrimitiveColorRGBA(vox4);
1350 DOCTEST_CHECK(color_rgba.r == RGBA::red.r);
1351 DOCTEST_CHECK(color_rgba.a == RGBA::red.a);
1352
1353 DOCTEST_CHECK(ctx.getPrimitiveCount() >= 4);
1354
1355 float area = ctx.getPrimitiveArea(vox1);
1356 DOCTEST_CHECK(area == doctest::Approx(2.f * (size.x * size.y + size.y * size.z + size.x * size.z)));
1357 }
1358}
1359
1360TEST_CASE("Texture Management") {
1361 SUBCASE("texture validation and properties") {
1362 capture_cerr cerr_buffer; // Capture deprecation warnings from setPrimitiveTextureFile
1363 Context ctx;
1364
1365 uint patch = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, "lib/images/solid.jpg");
1366 ctx.setPrimitiveTextureFile(patch, "lib/images/solid.jpg");
1367 DOCTEST_CHECK(ctx.getPrimitiveTextureFile(patch) == "lib/images/solid.jpg");
1368 DOCTEST_CHECK(!ctx.primitiveTextureHasTransparencyChannel(patch));
1369
1370 Texture tex("lib/images/solid.jpg");
1371 std::vector<vec2> uv = {{0, 0}, {1, 0}, {1, 1}};
1372 float solid_frac = tex.getSolidFraction(uv);
1373 DOCTEST_CHECK(solid_frac == doctest::Approx(1.f));
1374 }
1375}
1376
1377TEST_CASE("Triangle Management") {
1378 SUBCASE("setTriangleVertices") {
1379 Context ctx;
1380 vec3 v0 = make_vec3(0, 0, 0);
1381 vec3 v1 = make_vec3(1, 0, 0);
1382 vec3 v2 = make_vec3(0, 1, 0);
1383 uint tri = ctx.addTriangle(v0, v1, v2);
1384
1385 vec3 new_v0 = make_vec3(1, 1, 1);
1386 vec3 new_v1 = make_vec3(2, 1, 1);
1387 vec3 new_v2 = make_vec3(1, 2, 1);
1388 ctx.setTriangleVertices(tri, new_v0, new_v1, new_v2);
1389
1390 std::vector<vec3> vertices = ctx.getPrimitiveVertices(tri);
1391 DOCTEST_CHECK(vertices[0] == new_v0);
1392 DOCTEST_CHECK(vertices[1] == new_v1);
1393 DOCTEST_CHECK(vertices[2] == new_v2);
1394 }
1395}
1396
1397TEST_CASE("UUID and Object Management") {
1398 SUBCASE("getAllUUIDs and cleanDeletedUUIDs") {
1399 Context ctx;
1400 uint p1 = ctx.addPatch();
1401 uint p2 = ctx.addPatch();
1402 uint p3 = ctx.addPatch();
1403
1404 std::vector<uint> all_uuids = ctx.getAllUUIDs();
1405 DOCTEST_CHECK(all_uuids.size() == 3);
1406 DOCTEST_CHECK(std::find(all_uuids.begin(), all_uuids.end(), p1) != all_uuids.end());
1407
1408 ctx.deletePrimitive(p2);
1409 std::vector<uint> uuids_with_deleted = {p1, p2, p3};
1410 ctx.cleanDeletedUUIDs(uuids_with_deleted);
1411 DOCTEST_CHECK(uuids_with_deleted.size() == 2);
1412 DOCTEST_CHECK(std::find(uuids_with_deleted.begin(), uuids_with_deleted.end(), p2) == uuids_with_deleted.end());
1413
1414 std::vector<std::vector<uint>> nested_uuids = {{p1, p2}, {p3, p2}};
1415 ctx.cleanDeletedUUIDs(nested_uuids);
1416 DOCTEST_CHECK(nested_uuids[0].size() == 1);
1417 DOCTEST_CHECK(nested_uuids[1].size() == 1);
1418
1419 std::vector<std::vector<std::vector<uint>>> triple_nested = {{{p1, p2, p3}}};
1420 ctx.cleanDeletedUUIDs(triple_nested);
1421 DOCTEST_CHECK(triple_nested[0][0].size() == 2);
1422 }
1423
1424 SUBCASE("object management utilities") {
1425 Context ctx;
1426 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1427
1428 DOCTEST_CHECK(ctx.areObjectPrimitivesComplete(obj));
1429
1430 std::vector<uint> obj_ids = {obj, 999};
1431 ctx.cleanDeletedObjectIDs(obj_ids);
1432 DOCTEST_CHECK(obj_ids.size() == 1);
1433 DOCTEST_CHECK(obj_ids[0] == obj);
1434
1435 std::vector<std::vector<uint>> nested_obj_ids = {{obj, 999}, {obj}};
1436 ctx.cleanDeletedObjectIDs(nested_obj_ids);
1437 DOCTEST_CHECK(nested_obj_ids[0].size() == 1);
1438 DOCTEST_CHECK(nested_obj_ids[1].size() == 1);
1439
1440 std::vector<std::vector<std::vector<uint>>> triple_nested_obj = {{{obj, 999}}};
1441 ctx.cleanDeletedObjectIDs(triple_nested_obj);
1442 DOCTEST_CHECK(triple_nested_obj[0][0].size() == 1);
1443
1444 DOCTEST_CHECK(ctx.doesObjectExist(obj));
1445
1446 vec3 new_origin = make_vec3(5, 5, 5);
1447 ctx.setObjectOrigin(obj, new_origin);
1448
1449 vec3 new_normal = make_vec3(0, 1, 0);
1450 ctx.setObjectAverageNormal(obj, make_vec3(0, 0, 0), new_normal);
1451 }
1452}
1453
1454TEST_CASE("Tile Object Advanced Features") {
1455 SUBCASE("tile object subdivision management") {
1456 Context ctx;
1457 uint tile = ctx.addTileObject(make_vec3(0, 0, 0), make_vec2(4, 4), nullrotation, make_int2(2, 2));
1458
1459 float area_ratio = ctx.getTileObjectAreaRatio(tile);
1460 DOCTEST_CHECK(area_ratio > 0.f);
1461
1462 ctx.setTileObjectSubdivisionCount({tile}, make_int2(4, 4));
1463
1464 ctx.setTileObjectSubdivisionCount({tile}, 0.5f);
1465 }
1466}
1467
1468TEST_CASE("Pseudocolor Visualization") {
1469 SUBCASE("colorPrimitiveByDataPseudocolor") {
1470 Context ctx;
1471 std::vector<uint> patches;
1472 for (int i = 0; i < 5; i++) {
1473 uint p = ctx.addPatch();
1474 ctx.setPrimitiveData(p, "value", float(i));
1475 patches.push_back(p);
1476 }
1477
1478 DOCTEST_CHECK_NOTHROW(ctx.colorPrimitiveByDataPseudocolor(patches, "value", "hot", 10));
1479 DOCTEST_CHECK_NOTHROW(ctx.colorPrimitiveByDataPseudocolor(patches, "value", "rainbow", 5, 0.f, 4.f));
1480 }
1481}
1482
1483TEST_CASE("Date and Time Extensions") {
1484 SUBCASE("getMonthString") {
1485 Context ctx;
1486 ctx.setDate(15, 1, 2025);
1487 DOCTEST_CHECK(strcmp(ctx.getMonthString(), "JAN") == 0);
1488 ctx.setDate(15, 2, 2025);
1489 DOCTEST_CHECK(strcmp(ctx.getMonthString(), "FEB") == 0);
1490 ctx.setDate(15, 12, 2025);
1491 DOCTEST_CHECK(strcmp(ctx.getMonthString(), "DEC") == 0);
1492 }
1493}
1494
1495TEST_CASE("Tube Object Management") {
1496 SUBCASE("appendTubeSegment with texture") {
1497 Context ctx;
1498 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1)};
1499 std::vector<float> radii = {0.2f, 0.1f};
1500 uint tube = ctx.addTubeObject(10, nodes, radii);
1501
1502 ctx.appendTubeSegment(tube, make_vec3(0, 0, 2), 0.05f, "lib/images/solid.jpg", make_vec2(0.5f, 1.0f));
1503 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 3);
1504 }
1505}
1506
1507TEST_CASE("Edge Cases and Additional Coverage") {
1508 SUBCASE("Julian date edge cases") {
1509 Context ctx;
1510 ctx.setDate(1, 1, 2025);
1511 DOCTEST_CHECK(ctx.getJulianDate() == 1);
1512
1513 ctx.setDate(31, 12, 2025);
1514 DOCTEST_CHECK(ctx.getJulianDate() == 365);
1515
1516 ctx.setDate(100, 2025);
1517 Date d = ctx.getDate();
1518 DOCTEST_CHECK(d.day == 10);
1519 DOCTEST_CHECK(d.month == 4);
1520 }
1521
1522 SUBCASE("time edge cases") {
1523 Context ctx;
1524 ctx.setTime(0, 0, 0);
1525 Time t = ctx.getTime();
1526 DOCTEST_CHECK(t.hour == 0);
1527 DOCTEST_CHECK(t.minute == 0);
1528 DOCTEST_CHECK(t.second == 0);
1529
1530 ctx.setTime(59, 59, 23);
1531 t = ctx.getTime();
1532 DOCTEST_CHECK(t.hour == 23);
1533 DOCTEST_CHECK(t.minute == 59);
1534 DOCTEST_CHECK(t.second == 59);
1535 }
1536
1537 SUBCASE("random number edge cases") {
1538 Context ctx;
1539 ctx.seedRandomGenerator(0);
1540
1541 float r1 = ctx.randu(5.f, 5.f);
1542 DOCTEST_CHECK(r1 == doctest::Approx(5.f));
1543
1544 int ri = ctx.randu(10, 10);
1545 DOCTEST_CHECK(ri == 10);
1546
1547 float rn = ctx.randn(0.f, 0.f);
1548 DOCTEST_CHECK(rn == doctest::Approx(0.f));
1549 }
1550
1551 SUBCASE("texture edge cases") {
1552 capture_cerr cerr_buffer; // Capture deprecation warnings from overridePrimitiveTextureColor/usePrimitiveTextureColor
1553 Context ctx;
1554 uint patch = ctx.addPatch();
1555
1556 ctx.overridePrimitiveTextureColor(patch);
1557 ctx.usePrimitiveTextureColor(patch);
1558
1559 std::vector<uint> patches = {patch};
1560 ctx.overridePrimitiveTextureColor(patches);
1561 ctx.usePrimitiveTextureColor(patches);
1562
1563 DOCTEST_CHECK(!ctx.isPrimitiveTextureColorOverridden(patch));
1564 }
1565
1566 SUBCASE("primitive existence checks") {
1567 Context ctx;
1568 uint p1 = ctx.addPatch();
1569 uint p2 = ctx.addPatch();
1570
1571 DOCTEST_CHECK(ctx.doesPrimitiveExist(p1));
1572 DOCTEST_CHECK(ctx.doesPrimitiveExist({p1, p2}));
1573
1574 ctx.deletePrimitive(p1);
1575 DOCTEST_CHECK(!ctx.doesPrimitiveExist(p1));
1576 DOCTEST_CHECK(!ctx.doesPrimitiveExist({p1, p2}));
1577 DOCTEST_CHECK(ctx.doesPrimitiveExist(std::vector<uint>{p2}));
1578 }
1579
1580 SUBCASE("object containment checks") {
1581 Context ctx;
1582 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1583 std::vector<uint> prims = ctx.getObjectPrimitiveUUIDs(obj);
1584
1585 DOCTEST_CHECK(ctx.doesObjectContainPrimitive(obj, prims[0]));
1586
1587 uint independent_patch = ctx.addPatch();
1588 DOCTEST_CHECK(!ctx.doesObjectContainPrimitive(obj, independent_patch));
1589 }
1590
1591 SUBCASE("transformation matrix operations") {
1592 Context ctx;
1593 uint p = ctx.addPatch();
1594
1595 float identity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
1596 ctx.setPrimitiveTransformationMatrix(p, identity);
1597
1598 std::vector<uint> patches = {p};
1599 ctx.setPrimitiveTransformationMatrix(patches, identity);
1600
1601 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1602 ctx.setObjectTransformationMatrix(obj, identity);
1603
1604 std::vector<uint> objs = {obj};
1605 ctx.setObjectTransformationMatrix(objs, identity);
1606
1607 float retrieved[16];
1608 ctx.getObjectTransformationMatrix(obj, retrieved);
1609 for (int i = 0; i < 16; i++) {
1610 DOCTEST_CHECK(retrieved[i] == doctest::Approx(identity[i]));
1611 }
1612 }
1613
1614 SUBCASE("object type and texture checks") {
1615 Context ctx;
1616 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1617
1618 DOCTEST_CHECK(!ctx.objectHasTexture(obj));
1619
1620 uint textured_obj = ctx.addTileObject(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, make_int2(2, 2), "lib/images/solid.jpg");
1621 DOCTEST_CHECK(ctx.objectHasTexture(textured_obj));
1622 }
1623
1624 SUBCASE("tube object segment operations") {
1625 Context ctx;
1626 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
1627 std::vector<float> radii = {0.2f, 0.15f, 0.1f};
1628 uint tube = ctx.addTubeObject(10, nodes, radii);
1629
1630 float seg_volume = ctx.getTubeObjectSegmentVolume(tube, 0);
1631 DOCTEST_CHECK(seg_volume > 0.f);
1632
1633 seg_volume = ctx.getTubeObjectSegmentVolume(tube, 1);
1634 DOCTEST_CHECK(seg_volume > 0.f);
1635 }
1636
1637 SUBCASE("cone object advanced properties") {
1638 Context ctx;
1639 uint cone = ctx.addConeObject(10, make_vec3(0, 0, 0), make_vec3(0, 0, 2), 1.f, 0.5f);
1640
1641 float radius0 = ctx.getConeObjectNodeRadius(cone, 0);
1642 DOCTEST_CHECK(radius0 == doctest::Approx(1.f));
1643
1644 float radius1 = ctx.getConeObjectNodeRadius(cone, 1);
1645 DOCTEST_CHECK(radius1 == doctest::Approx(0.5f));
1646
1647 float length = ctx.getConeObjectLength(cone);
1648 DOCTEST_CHECK(length == doctest::Approx(2.f));
1649
1650 DOCTEST_CHECK(ctx.getConeObjectSubdivisionCount(cone) == 10);
1651 }
1652
1653 SUBCASE("primitive color operations") {
1654 capture_cerr cerr_buffer; // Suppress deprecation warnings from setPrimitiveColor
1655 Context ctx;
1656 uint p = ctx.addPatch();
1657
1658 ctx.setPrimitiveColor(p, RGB::blue);
1659 DOCTEST_CHECK(ctx.getPrimitiveColor(p) == RGB::blue);
1660
1661 ctx.setPrimitiveColor(p, RGBA::green);
1662 RGBAcolor rgba = ctx.getPrimitiveColorRGBA(p);
1663 DOCTEST_CHECK(rgba.r == RGBA::green.r);
1664 DOCTEST_CHECK(rgba.a == RGBA::green.a);
1665
1666 std::vector<uint> patches = {p};
1667 ctx.setPrimitiveColor(patches, RGB::red);
1668 DOCTEST_CHECK(ctx.getPrimitiveColor(p) == RGB::red);
1669
1670 ctx.setPrimitiveColor(patches, RGBA::yellow);
1671 rgba = ctx.getPrimitiveColorRGBA(p);
1672 DOCTEST_CHECK(rgba.r == RGBA::yellow.r);
1673 }
1674
1675 SUBCASE("object color operations") {
1676 capture_cerr cerr_buffer; // Suppress deprecation warnings from setObjectColor (calls setPrimitiveColor internally)
1677 Context ctx;
1678 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1679
1680 ctx.setObjectColor(obj, RGB::cyan);
1681 ctx.setObjectColor(obj, RGBA::magenta);
1682
1683 std::vector<uint> objs = {obj};
1684 ctx.setObjectColor(objs, RGB::white);
1685 ctx.setObjectColor(objs, RGBA::black);
1686 }
1687}
1688
1689TEST_CASE("Print and Information Functions") {
1690 SUBCASE("printPrimitiveInfo and printObjectInfo") {
1691 Context ctx;
1692 uint patch = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
1693 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1694
1695 // Capture stdout output from these functions
1696 bool has_output;
1697 std::string output;
1698 {
1699 capture_cout cout_buffer;
1700 DOCTEST_CHECK_NOTHROW(ctx.printPrimitiveInfo(patch));
1701 DOCTEST_CHECK_NOTHROW(ctx.printObjectInfo(obj));
1702 has_output = cout_buffer.has_output();
1703 output = cout_buffer.get_captured_output();
1704 } // cout_buffer destroyed here
1705
1706 // Verify that output was captured (functions should produce output)
1707 DOCTEST_CHECK(has_output);
1708 DOCTEST_CHECK(output.find("Info for UUID") != std::string::npos);
1709 DOCTEST_CHECK(output.find("Info for ObjID") != std::string::npos);
1710 }
1711}
1712
1713TEST_CASE("Object Pointer Access") {
1714 SUBCASE("getObjectPointer functions") {
1715 Context ctx;
1716
1717 uint box = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1718 DOCTEST_CHECK(ctx.doesObjectExist(box));
1719
1720 uint disk = ctx.addDiskObject(10, make_vec3(0, 0, 0), make_vec2(1, 1));
1721 DOCTEST_CHECK(ctx.doesObjectExist(disk));
1722
1723 uint sphere = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
1724 DOCTEST_CHECK(ctx.doesObjectExist(sphere));
1725
1726 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1)};
1727 std::vector<float> radii = {0.2f, 0.1f};
1728 uint tube = ctx.addTubeObject(10, nodes, radii);
1729 DOCTEST_CHECK(ctx.doesObjectExist(tube));
1730
1731 uint cone = ctx.addConeObject(10, make_vec3(0, 0, 0), make_vec3(0, 0, 1), 0.5f, 0.3f);
1732 DOCTEST_CHECK(ctx.doesObjectExist(cone));
1733
1734 std::vector<uint> prim_uuids = {ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0))};
1735 uint polymesh = ctx.addPolymeshObject(prim_uuids);
1736 DOCTEST_CHECK(ctx.doesObjectExist(polymesh));
1737 }
1738}
1739
1740TEST_CASE("Advanced Primitive Operations") {
1741 SUBCASE("primitive visibility and print operations") {
1742 Context ctx;
1743 uint p1 = ctx.addPatch();
1744 uint p2 = ctx.addPatch();
1745
1746 // Test hiding/showing primitives
1747 ctx.hidePrimitive(p1);
1748 DOCTEST_CHECK(ctx.isPrimitiveHidden(p1));
1749 ctx.showPrimitive(p1);
1750 DOCTEST_CHECK(!ctx.isPrimitiveHidden(p1));
1751
1752 std::vector<uint> patches = {p1, p2};
1753 ctx.hidePrimitive(patches);
1754 DOCTEST_CHECK(ctx.isPrimitiveHidden(p1));
1755 DOCTEST_CHECK(ctx.isPrimitiveHidden(p2));
1756 ctx.showPrimitive(patches);
1757 DOCTEST_CHECK(!ctx.isPrimitiveHidden(p1));
1758 DOCTEST_CHECK(!ctx.isPrimitiveHidden(p2));
1759 }
1760
1761 SUBCASE("primitive counts by type") {
1762 Context ctx;
1763 uint initial_patch_count = ctx.getPatchCount();
1764 uint initial_triangle_count = ctx.getTriangleCount();
1765
1766 uint p1 = ctx.addPatch();
1767 uint p2 = ctx.addPatch();
1768 uint tri = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0));
1769
1770 DOCTEST_CHECK(ctx.getPatchCount() == initial_patch_count + 2);
1771 DOCTEST_CHECK(ctx.getTriangleCount() == initial_triangle_count + 1);
1772
1773 // Test with hidden primitives
1774 ctx.hidePrimitive(p1);
1775 DOCTEST_CHECK(ctx.getPatchCount(false) == initial_patch_count + 1); // exclude hidden
1776 DOCTEST_CHECK(ctx.getPatchCount(true) == initial_patch_count + 2); // include hidden
1777 }
1778}
1779
1780TEST_CASE("Data Type and Size Functions") {
1781 SUBCASE("primitive data type operations") {
1782 Context ctx;
1783 uint p = ctx.addPatch();
1784
1785 ctx.setPrimitiveData(p, "test_int", 42);
1786 ctx.setPrimitiveData(p, "test_float", 3.14f);
1787 ctx.setPrimitiveData(p, "test_vec3", make_vec3(1, 2, 3));
1788
1789 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_int") == HELIOS_TYPE_INT);
1790 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_float") == HELIOS_TYPE_FLOAT);
1791 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_vec3") == HELIOS_TYPE_VEC3);
1792
1793 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_int") == 1);
1794 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_vec3") == 1);
1795
1796 std::vector<float> vec_data = {1.0f, 2.0f, 3.0f};
1797 ctx.setPrimitiveData(p, "test_vector", vec_data);
1798 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_vector") == 3);
1799 }
1800}
1801
1802TEST_CASE("Additional Missing Coverage") {
1803 SUBCASE("getDirtyUUIDs function") {
1804 Context ctx;
1805 uint p1 = ctx.addPatch();
1806 uint p2 = ctx.addPatch();
1807
1808 ctx.markGeometryClean();
1809 std::vector<uint> dirty_uuids = ctx.getDirtyUUIDs();
1810 DOCTEST_CHECK(dirty_uuids.empty());
1811
1812 ctx.markPrimitiveDirty(p1);
1813 dirty_uuids = ctx.getDirtyUUIDs();
1814 DOCTEST_CHECK(dirty_uuids.size() == 1);
1815 DOCTEST_CHECK(std::find(dirty_uuids.begin(), dirty_uuids.end(), p1) != dirty_uuids.end());
1816 }
1817}
1818
1819TEST_CASE("Advanced Object Operations") {
1820 SUBCASE("object primitive count and area calculations") {
1821 Context ctx;
1822 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(2, 3, 4), make_int3(1, 1, 1));
1823
1824 DOCTEST_CHECK(ctx.getObjectPrimitiveCount(obj) == 6); // 6 faces of a box
1825
1826 float area = ctx.getObjectArea(obj);
1827 float expected_area = 2 * (2 * 3 + 3 * 4 + 2 * 4); // surface area of box
1828 DOCTEST_CHECK(area == doctest::Approx(expected_area).epsilon(0.01));
1829 }
1830
1831 SUBCASE("object bounding box operations") {
1832 Context ctx;
1833 uint obj = ctx.addBoxObject(make_vec3(1, 2, 3), make_vec3(2, 4, 6), make_int3(1, 1, 1));
1834
1835 vec3 min_corner, max_corner;
1836 ctx.getObjectBoundingBox(obj, min_corner, max_corner);
1837
1838 DOCTEST_CHECK(min_corner.x == doctest::Approx(0.f).epsilon(0.01));
1839 DOCTEST_CHECK(max_corner.x == doctest::Approx(2.f).epsilon(0.01));
1840 DOCTEST_CHECK(min_corner.y == doctest::Approx(0.f).epsilon(0.01));
1841 DOCTEST_CHECK(max_corner.y == doctest::Approx(4.f).epsilon(0.01));
1842
1843 std::vector<uint> objs = {obj};
1844 ctx.getObjectBoundingBox(objs, min_corner, max_corner);
1845 DOCTEST_CHECK(min_corner.x == doctest::Approx(0.f).epsilon(0.01));
1846 DOCTEST_CHECK(max_corner.x == doctest::Approx(2.f).epsilon(0.01));
1847 }
1848}
1849
1850TEST_CASE("Additional Object Features") {
1851 SUBCASE("getAllObjectIDs") {
1852 Context ctx;
1853 uint obj1 = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1854 uint obj2 = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
1855
1856 std::vector<uint> all_ids = ctx.getAllObjectIDs();
1857 DOCTEST_CHECK(all_ids.size() >= 2);
1858 DOCTEST_CHECK(std::find(all_ids.begin(), all_ids.end(), obj1) != all_ids.end());
1859 DOCTEST_CHECK(std::find(all_ids.begin(), all_ids.end(), obj2) != all_ids.end());
1860 }
1861
1862 SUBCASE("object type checks") {
1863 Context ctx;
1864 uint box = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1865 uint sphere = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
1866 uint disk = ctx.addDiskObject(10, make_vec3(0, 0, 0), make_vec2(1, 1));
1867
1868 DOCTEST_CHECK(ctx.getObjectType(box) == OBJECT_TYPE_BOX);
1869 DOCTEST_CHECK(ctx.getObjectType(sphere) == OBJECT_TYPE_SPHERE);
1870 DOCTEST_CHECK(ctx.getObjectType(disk) == OBJECT_TYPE_DISK);
1871 }
1872}
1873
1874TEST_CASE("Comprehensive Object Property Tests") {
1875 SUBCASE("rotation operations on objects") {
1876 Context ctx;
1877 std::vector<uint> objs;
1878 objs.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
1879 objs.push_back(ctx.addBoxObject(make_vec3(1, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
1880
1881 DOCTEST_CHECK_NOTHROW(ctx.rotateObject(objs, 0.5f * PI_F, "z"));
1882 DOCTEST_CHECK_NOTHROW(ctx.rotateObject(objs, 0.5f * PI_F, make_vec3(0, 0, 1)));
1883 DOCTEST_CHECK_NOTHROW(ctx.rotateObject(objs, 0.5f * PI_F, make_vec3(0, 0, 0), make_vec3(0, 0, 1)));
1884 DOCTEST_CHECK_NOTHROW(ctx.rotateObjectAboutOrigin(objs, 0.5f * PI_F, make_vec3(0, 0, 1)));
1885 }
1886
1887 SUBCASE("scaling operations on objects") {
1888 Context ctx;
1889 std::vector<uint> objs;
1890 objs.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
1891
1892 DOCTEST_CHECK_NOTHROW(ctx.scaleObject(objs, make_vec3(2, 2, 2)));
1893 DOCTEST_CHECK_NOTHROW(ctx.scaleObjectAboutCenter(objs, make_vec3(0.5f, 0.5f, 0.5f)));
1894 DOCTEST_CHECK_NOTHROW(ctx.scaleObjectAboutPoint(objs, make_vec3(2, 2, 2), make_vec3(0, 0, 0)));
1895 DOCTEST_CHECK_NOTHROW(ctx.scaleObjectAboutOrigin(objs, make_vec3(0.5f, 0.5f, 0.5f)));
1896 }
1897
1898 SUBCASE("translation operations on objects") {
1899 Context ctx;
1900 std::vector<uint> objs;
1901 objs.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
1902
1903 DOCTEST_CHECK_NOTHROW(ctx.translateObject(objs, make_vec3(1, 2, 3)));
1904 }
1905}
1906
1907TEST_CASE("Domain and Bounding Operations") {
1908 SUBCASE("domain bounding sphere") {
1909 Context ctx;
1910 ctx.addPatch(make_vec3(-2, 0, 0), make_vec2(1, 1));
1911 ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1));
1912
1913 vec3 center;
1914 float radius;
1915 ctx.getDomainBoundingSphere(center, radius);
1916 DOCTEST_CHECK(center.x == doctest::Approx(0.f).epsilon(0.1));
1917 DOCTEST_CHECK(radius > 2.f);
1918 }
1919}
1920
1921TEST_CASE("Missing Data and State Functions") {
1922 SUBCASE("listTimeseriesVariables") {
1923 Context ctx;
1924 Date date = make_Date(1, 1, 2025);
1925 Time time = make_Time(0, 0, 12);
1926
1927 ctx.addTimeseriesData("temp", 25.5f, date, time);
1928 ctx.addTimeseriesData("humidity", 60.0f, date, time);
1929
1930 std::vector<std::string> vars = ctx.listTimeseriesVariables();
1931 DOCTEST_CHECK(vars.size() >= 2);
1932 DOCTEST_CHECK(std::find(vars.begin(), vars.end(), "temp") != vars.end());
1933 DOCTEST_CHECK(std::find(vars.begin(), vars.end(), "humidity") != vars.end());
1934 }
1935
1936 SUBCASE("getUniquePrimitiveParentObjectIDs") {
1937 Context ctx;
1938 uint obj1 = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1939 uint obj2 = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
1940
1941 std::vector<uint> all_prims = ctx.getAllUUIDs();
1942 std::vector<uint> obj_ids = ctx.getUniquePrimitiveParentObjectIDs(all_prims);
1943 DOCTEST_CHECK(obj_ids.size() >= 2);
1944 DOCTEST_CHECK(std::find(obj_ids.begin(), obj_ids.end(), obj1) != obj_ids.end());
1945 DOCTEST_CHECK(std::find(obj_ids.begin(), obj_ids.end(), obj2) != obj_ids.end());
1946 }
1947}
1948
1949TEST_CASE("Comprehensive Coverage Tests") {
1950 SUBCASE("additional object operations with vectors") {
1951 Context ctx;
1952 std::vector<uint> obj_ids;
1953 obj_ids.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
1954 obj_ids.push_back(ctx.addBoxObject(make_vec3(2, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
1955
1956 std::vector<uint> all_uuids = ctx.getObjectPrimitiveUUIDs(obj_ids);
1957 DOCTEST_CHECK(all_uuids.size() == 12); // 6 faces per box * 2 boxes
1958
1959 std::vector<std::vector<uint>> nested_obj_ids = {{obj_ids[0]}, {obj_ids[1]}};
1960 std::vector<uint> nested_uuids = ctx.getObjectPrimitiveUUIDs(nested_obj_ids);
1961 DOCTEST_CHECK(nested_uuids.size() == 12);
1962
1963 ctx.hideObject(obj_ids);
1964 DOCTEST_CHECK(ctx.isObjectHidden(obj_ids[0]));
1965 DOCTEST_CHECK(ctx.isObjectHidden(obj_ids[1]));
1966
1967 ctx.showObject(obj_ids);
1968 DOCTEST_CHECK(!ctx.isObjectHidden(obj_ids[0]));
1969 DOCTEST_CHECK(!ctx.isObjectHidden(obj_ids[1]));
1970 }
1971
1972 SUBCASE("object texture color overrides") {
1973 Context ctx;
1974 std::vector<uint> obj_ids;
1975 obj_ids.push_back(ctx.addTileObject(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, make_int2(2, 2), "lib/images/solid.jpg"));
1976
1977 ctx.overrideObjectTextureColor(obj_ids);
1978 ctx.useObjectTextureColor(obj_ids);
1979 }
1980}
1981
1982TEST_CASE("getAllUUIDs Cache Performance") {
1983 SUBCASE("Cache invalidation on primitive add/delete") {
1984 Context ctx;
1985
1986 // Initial empty state
1987 std::vector<uint> empty_uuids = ctx.getAllUUIDs();
1988 DOCTEST_CHECK(empty_uuids.empty());
1989
1990 // Add primitives and test cache invalidation
1991 uint p1 = ctx.addPatch();
1992 std::vector<uint> one_uuid = ctx.getAllUUIDs();
1993 DOCTEST_CHECK(one_uuid.size() == 1);
1994 DOCTEST_CHECK(one_uuid[0] == p1);
1995
1996 // Test cache consistency - repeated calls should return same result
1997 std::vector<uint> same_uuid = ctx.getAllUUIDs();
1998 DOCTEST_CHECK(same_uuid.size() == 1);
1999 DOCTEST_CHECK(same_uuid[0] == p1);
2000
2001 // Add more primitives
2002 uint t1 = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0));
2003 uint v1 = ctx.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
2004
2005 std::vector<uint> three_uuids = ctx.getAllUUIDs();
2006 DOCTEST_CHECK(three_uuids.size() == 3);
2007 DOCTEST_CHECK(std::find(three_uuids.begin(), three_uuids.end(), p1) != three_uuids.end());
2008 DOCTEST_CHECK(std::find(three_uuids.begin(), three_uuids.end(), t1) != three_uuids.end());
2009 DOCTEST_CHECK(std::find(three_uuids.begin(), three_uuids.end(), v1) != three_uuids.end());
2010
2011 // Test delete invalidation
2012 ctx.deletePrimitive(t1);
2013 std::vector<uint> two_uuids = ctx.getAllUUIDs();
2014 DOCTEST_CHECK(two_uuids.size() == 2);
2015 DOCTEST_CHECK(std::find(two_uuids.begin(), two_uuids.end(), t1) == two_uuids.end());
2016 DOCTEST_CHECK(std::find(two_uuids.begin(), two_uuids.end(), p1) != two_uuids.end());
2017 DOCTEST_CHECK(std::find(two_uuids.begin(), two_uuids.end(), v1) != two_uuids.end());
2018 }
2019
2020 SUBCASE("Cache invalidation on hide/show primitives") {
2021 Context ctx;
2022 uint p1 = ctx.addPatch();
2023 uint p2 = ctx.addPatch();
2024 uint p3 = ctx.addPatch();
2025
2026 // All visible initially
2027 std::vector<uint> all_visible = ctx.getAllUUIDs();
2028 DOCTEST_CHECK(all_visible.size() == 3);
2029
2030 // Hide one primitive
2031 ctx.hidePrimitive(p2);
2032 std::vector<uint> two_visible = ctx.getAllUUIDs();
2033 DOCTEST_CHECK(two_visible.size() == 2);
2034 DOCTEST_CHECK(std::find(two_visible.begin(), two_visible.end(), p2) == two_visible.end());
2035 DOCTEST_CHECK(std::find(two_visible.begin(), two_visible.end(), p1) != two_visible.end());
2036 DOCTEST_CHECK(std::find(two_visible.begin(), two_visible.end(), p3) != two_visible.end());
2037
2038 // Hide multiple primitives
2039 std::vector<uint> to_hide = {p1, p3};
2040 ctx.hidePrimitive(to_hide);
2041 std::vector<uint> none_visible = ctx.getAllUUIDs();
2042 DOCTEST_CHECK(none_visible.empty());
2043
2044 // Show one primitive back
2045 ctx.showPrimitive(p1);
2046 std::vector<uint> one_visible = ctx.getAllUUIDs();
2047 DOCTEST_CHECK(one_visible.size() == 1);
2048 DOCTEST_CHECK(one_visible[0] == p1);
2049
2050 // Show all primitives back
2051 std::vector<uint> to_show = {p2, p3};
2052 ctx.showPrimitive(to_show);
2053 std::vector<uint> all_back = ctx.getAllUUIDs();
2054 DOCTEST_CHECK(all_back.size() == 3);
2055 }
2056
2057 SUBCASE("Cache invalidation on copy primitives") {
2058 Context ctx;
2059 uint original = ctx.addPatch();
2060
2061 std::vector<uint> before_copy = ctx.getAllUUIDs();
2062 DOCTEST_CHECK(before_copy.size() == 1);
2063
2064 uint copied = ctx.copyPrimitive(original);
2065 std::vector<uint> after_copy = ctx.getAllUUIDs();
2066 DOCTEST_CHECK(after_copy.size() == 2);
2067 DOCTEST_CHECK(std::find(after_copy.begin(), after_copy.end(), original) != after_copy.end());
2068 DOCTEST_CHECK(std::find(after_copy.begin(), after_copy.end(), copied) != after_copy.end());
2069
2070 // Test multiple copy
2071 std::vector<uint> originals = {original, copied};
2072 std::vector<uint> copies = ctx.copyPrimitive(originals);
2073 std::vector<uint> after_multi_copy = ctx.getAllUUIDs();
2074 DOCTEST_CHECK(after_multi_copy.size() == 4);
2075 for (uint copy_id: copies) {
2076 DOCTEST_CHECK(std::find(after_multi_copy.begin(), after_multi_copy.end(), copy_id) != after_multi_copy.end());
2077 }
2078 }
2079
2080 SUBCASE("Cache consistency across mixed operations") {
2081 Context ctx;
2082
2083 // Complex sequence of operations
2084 uint p1 = ctx.addPatch();
2085 uint p2 = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0));
2086
2087 std::vector<uint> step1 = ctx.getAllUUIDs();
2088 DOCTEST_CHECK(step1.size() == 2);
2089
2090 ctx.hidePrimitive(p1);
2091 std::vector<uint> step2 = ctx.getAllUUIDs();
2092 DOCTEST_CHECK(step2.size() == 1);
2093 DOCTEST_CHECK(step2[0] == p2);
2094
2095 uint p3 = ctx.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
2096 std::vector<uint> step3 = ctx.getAllUUIDs();
2097 DOCTEST_CHECK(step3.size() == 2);
2098
2099 ctx.showPrimitive(p1);
2100 std::vector<uint> step4 = ctx.getAllUUIDs();
2101 DOCTEST_CHECK(step4.size() == 3);
2102
2103 ctx.deletePrimitive(p2);
2104 std::vector<uint> step5 = ctx.getAllUUIDs();
2105 DOCTEST_CHECK(step5.size() == 2);
2106 DOCTEST_CHECK(std::find(step5.begin(), step5.end(), p2) == step5.end());
2107 DOCTEST_CHECK(std::find(step5.begin(), step5.end(), p1) != step5.end());
2108 DOCTEST_CHECK(std::find(step5.begin(), step5.end(), p3) != step5.end());
2109 }
2110}
2111
2112TEST_CASE("Error Handling") {
2113 SUBCASE("Context error handling") {
2114 Context context_test;
2115 uint tri = context_test.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0), RGB::green);
2116 capture_cerr cerr_buffer;
2117 vec3 center;
2118#ifdef HELIOS_DEBUG
2119 DOCTEST_CHECK_THROWS_AS(center = context_test.getPatchCenter(tri), std::runtime_error);
2120#endif
2121
2122 uint vox = context_test.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
2123 std::vector<uint> vlist{vox};
2124 DOCTEST_CHECK_THROWS_AS(context_test.rotatePrimitive(vlist, PI_F / 4.f, "a"), std::runtime_error);
2125 }
2126}
2127
2128TEST_CASE("Zero Area Triangle Detection") {
2129 SUBCASE("addTubeObject with nearly identical vertices should not create zero-area triangles") {
2130 Context ctx;
2131
2132 // Test case based on problematic vertices from plant architecture
2133 std::vector<vec3> nodes = {make_vec3(0.300000012f, -0.112000048f, 0.00999999978f), make_vec3(0.29995966f, -0.111979447f, 0.0109989736f), make_vec3(0.299919307f, -0.111958846f, 0.0119979475f)};
2134
2135 std::vector<float> radii = {0.000500000024f, 0.000500000024f, 0.000500000024f};
2136 std::vector<RGBcolor> colors = {RGB::green, RGB::green, RGB::green};
2137
2138 // Use exact same parameters as failing case: Ndiv_internode_radius = 7
2139 uint tube_obj = ctx.addTubeObject(7, nodes, radii, colors);
2140
2141 // Verify the tube object was created
2142 DOCTEST_CHECK(ctx.doesObjectExist(tube_obj));
2143
2144 // Get all primitives in the tube and check their areas
2145 std::vector<uint> tube_primitives = ctx.getObjectPrimitiveUUIDs(tube_obj);
2146 DOCTEST_CHECK(tube_primitives.size() > 0);
2147
2148 for (uint uuid: tube_primitives) {
2149 float area = ctx.getPrimitiveArea(uuid);
2150 DOCTEST_CHECK(area > 0.0f); // No zero-area triangles
2151 DOCTEST_CHECK(area > 1e-12f); // Area should be reasonably above precision limit
2152 }
2153 }
2154
2155 SUBCASE("addTubeObject with extremely small displacements") {
2156 Context ctx;
2157
2158 // Even more extreme case - displacements on the order of 1e-5
2159 std::vector<vec3> nodes = {make_vec3(0.0f, 0.0f, 0.0f), make_vec3(1e-5f, 1e-5f, 1e-3f), make_vec3(2e-5f, 2e-5f, 2e-3f)};
2160
2161 std::vector<float> radii = {1e-4f, 1e-4f, 1e-4f};
2162
2163 uint tube_obj = ctx.addTubeObject(6, nodes, radii);
2164 DOCTEST_CHECK(ctx.doesObjectExist(tube_obj));
2165
2166 std::vector<uint> tube_primitives = ctx.getObjectPrimitiveUUIDs(tube_obj);
2167 for (uint uuid: tube_primitives) {
2168 float area = ctx.getPrimitiveArea(uuid);
2169 DOCTEST_CHECK(area > 0.0f);
2170 }
2171 }
2172}
2173
2174TEST_CASE("Transparent Texture Zero Area Validation") {
2175 SUBCASE("addSphere with transparent texture should filter zero-area triangles") {
2176 Context ctx;
2177
2178 // Test with diamond texture (has transparency)
2179 std::vector<uint> sphere_uuids = ctx.addSphere(20, make_vec3(0, 0, 0), 1.0f, "lib/images/diamond_texture.png");
2180
2181 // All returned primitives should have positive area
2182 DOCTEST_CHECK(sphere_uuids.size() > 0);
2183 for (uint uuid: sphere_uuids) {
2184 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
2185 float area = ctx.getPrimitiveArea(uuid);
2186 DOCTEST_CHECK(area > 0.0f);
2187 }
2188
2189 // Test with disk texture (more transparency)
2190 std::vector<uint> sphere_disk_uuids = ctx.addSphere(30, make_vec3(2, 0, 0), 1.0f, "lib/images/disk_texture.png");
2191
2192 DOCTEST_CHECK(sphere_disk_uuids.size() > 0);
2193 for (uint uuid: sphere_disk_uuids) {
2194 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
2195 float area = ctx.getPrimitiveArea(uuid);
2196 DOCTEST_CHECK(area > 0.0f);
2197 }
2198
2199 // Verify ALL primitives in each sphere have positive area
2200 int zero_area_count_diamond = 0;
2201 for (uint uuid: sphere_uuids) {
2202 float area = ctx.getPrimitiveArea(uuid);
2203 if (area <= 0.0f) {
2204 zero_area_count_diamond++;
2205 }
2206 }
2207 DOCTEST_CHECK(zero_area_count_diamond == 0);
2208
2209 int zero_area_count_disk = 0;
2210 for (uint uuid: sphere_disk_uuids) {
2211 float area = ctx.getPrimitiveArea(uuid);
2212 if (area <= 0.0f) {
2213 zero_area_count_disk++;
2214 }
2215 }
2216 DOCTEST_CHECK(zero_area_count_disk == 0);
2217
2218 // Compare with solid sphere for reference
2219 std::vector<uint> solid_sphere_uuids = ctx.addSphere(20, make_vec3(4, 0, 0), 1.0f, RGB::green);
2220
2221 int zero_area_count_solid = 0;
2222 for (uint uuid: solid_sphere_uuids) {
2223 float area = ctx.getPrimitiveArea(uuid);
2224 if (area <= 0.0f) {
2225 zero_area_count_solid++;
2226 }
2227 }
2228 DOCTEST_CHECK(zero_area_count_solid == 0);
2229 }
2230
2231 SUBCASE("texture transparency validation preserves object integrity") {
2232 Context ctx;
2233
2234 // Create textured sphere and verify all returned UUIDs are valid
2235 std::vector<uint> sphere_uuids = ctx.addSphere(15, make_vec3(0, 0, 0), 1.0f, "lib/images/diamond_texture.png");
2236
2237 // Check that all returned primitives exist and have positive area
2238 for (uint uuid: sphere_uuids) {
2239 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
2240 DOCTEST_CHECK(ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE);
2241
2242 float area = ctx.getPrimitiveArea(uuid);
2243 DOCTEST_CHECK(area > 0.0f);
2244 DOCTEST_CHECK(area > 1e-10f); // Should be significantly above precision threshold
2245
2246 // Verify solid fraction is reasonable (not exactly 0 or 1)
2247 float solid_fraction = ctx.getPrimitiveSolidFraction(uuid);
2248 DOCTEST_CHECK(solid_fraction > 0.0f);
2249 DOCTEST_CHECK(solid_fraction <= 1.0f);
2250 }
2251
2252 // Comprehensive check: verify no zero-area primitives exist anywhere in context
2253 std::vector<uint> all_uuids = ctx.getAllUUIDs();
2254 int total_zero_area = 0;
2255 int total_negative_area = 0;
2256
2257 for (uint uuid: all_uuids) {
2258 float area = ctx.getPrimitiveArea(uuid);
2259 if (area == 0.0f) {
2260 total_zero_area++;
2261 }
2262 if (area < 0.0f) {
2263 total_negative_area++;
2264 }
2265 }
2266
2267 // No zero or negative area primitives should exist
2268 DOCTEST_CHECK(total_zero_area == 0);
2269 DOCTEST_CHECK(total_negative_area == 0);
2270
2271 // Additional validation: check that all primitives have reasonable solid fractions
2272 for (uint uuid: sphere_uuids) {
2273 float solid_fraction = ctx.getPrimitiveSolidFraction(uuid);
2274 DOCTEST_CHECK(solid_fraction >= 0.0f);
2275 DOCTEST_CHECK(solid_fraction <= 1.0f);
2276
2277 // For textured primitives, effective area should be geometric_area * solid_fraction
2278 if (ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE) {
2279 vec3 v0 = ctx.getTriangleVertex(uuid, 0);
2280 vec3 v1 = ctx.getTriangleVertex(uuid, 1);
2281 vec3 v2 = ctx.getTriangleVertex(uuid, 2);
2282 float geometric_area = calculateTriangleArea(v0, v1, v2);
2283 float effective_area = ctx.getPrimitiveArea(uuid);
2284
2285 // Effective area should be <= geometric area (due to solid fraction)
2286 DOCTEST_CHECK(effective_area <= geometric_area + 1e-6f); // Allow small numerical tolerance
2287 DOCTEST_CHECK(effective_area > 0.0f);
2288 }
2289 }
2290
2291 // Test zero-area validation for other primitive methods (addTube, addDisk, addCone)
2292 DOCTEST_SUBCASE("Test Other Primitive Methods Zero Area Validation") {
2293 Context ctx_other;
2294
2295 // Test addTube with transparent texture
2296 std::vector<vec3> tube_nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
2297 std::vector<float> tube_radii = {0.1f, 0.15f, 0.1f};
2298 std::vector<uint> tube_uuids = ctx_other.addTube(8, tube_nodes, tube_radii, "lib/images/diamond_texture.png");
2299
2300 // All returned UUIDs should have positive area
2301 int tube_positive_area = 0, tube_zero_area = 0;
2302 for (uint uuid: tube_uuids) {
2303 float area = ctx_other.getPrimitiveArea(uuid);
2304 DOCTEST_CHECK(area >= 0.0f);
2305 if (area > 0.0f) {
2306 tube_positive_area++;
2307 } else {
2308 tube_zero_area++;
2309 }
2310 }
2311
2312 DOCTEST_CHECK(tube_positive_area > 0); // Should have some positive area triangles
2313 DOCTEST_CHECK(tube_zero_area == 0); // Should have no zero area triangles
2314
2315 // Test addDisk with transparent texture
2316 std::vector<uint> disk_uuids = ctx_other.addDisk(make_int2(4, 3), make_vec3(0, 0, 0), make_vec2(1.0f, 1.0f), make_SphericalCoord(0, 0), "lib/images/disk_texture.png");
2317
2318 // All returned UUIDs should have positive area
2319 int disk_positive_area = 0, disk_zero_area = 0;
2320 for (uint uuid: disk_uuids) {
2321 float area = ctx_other.getPrimitiveArea(uuid);
2322 DOCTEST_CHECK(area >= 0.0f);
2323 if (area > 0.0f) {
2324 disk_positive_area++;
2325 } else {
2326 disk_zero_area++;
2327 }
2328 }
2329
2330 DOCTEST_CHECK(disk_positive_area > 0); // Should have some positive area triangles
2331 DOCTEST_CHECK(disk_zero_area == 0); // Should have no zero area triangles
2332
2333 // Test addCone with transparent texture
2334 std::vector<uint> cone_uuids = ctx_other.addCone(8, make_vec3(0, 0, 0), make_vec3(0, 0, 1), 0.1f, 0.2f, "lib/images/diamond_texture.png");
2335
2336 // All returned UUIDs should have positive area
2337 int cone_positive_area = 0, cone_zero_area = 0;
2338 for (uint uuid: cone_uuids) {
2339 float area = ctx_other.getPrimitiveArea(uuid);
2340 DOCTEST_CHECK(area >= 0.0f);
2341 if (area > 0.0f) {
2342 cone_positive_area++;
2343 } else {
2344 cone_zero_area++;
2345 }
2346 }
2347
2348 DOCTEST_CHECK(cone_positive_area > 0); // Should have some positive area triangles
2349 DOCTEST_CHECK(cone_zero_area == 0); // Should have no zero area triangles
2350
2351 // Test addTile with transparent texture (should already work, but verify)
2352 std::vector<uint> tile_uuids = ctx_other.addTile(make_vec3(0, 0, 0), make_vec2(1.0f, 1.0f), make_SphericalCoord(0, 0), make_int2(4, 4), "lib/images/diamond_texture.png");
2353
2354 // All returned UUIDs should have positive area
2355 int tile_positive_area = 0, tile_zero_area = 0;
2356 for (uint uuid: tile_uuids) {
2357 float area = ctx_other.getPrimitiveArea(uuid);
2358 DOCTEST_CHECK(area >= 0.0f);
2359 if (area > 0.0f) {
2360 tile_positive_area++;
2361 } else {
2362 tile_zero_area++;
2363 }
2364 }
2365
2366 DOCTEST_CHECK(tile_positive_area > 0); // Should have some positive area triangles
2367 DOCTEST_CHECK(tile_zero_area == 0); // Should have no zero area triangles
2368 }
2369
2370 // Test zero-area validation for compound object methods
2371 DOCTEST_SUBCASE("Test Compound Object Methods Zero Area Validation") {
2372 Context ctx_compound;
2373
2374 // Test addSphereObject with transparent texture
2375 uint sphere_obj = ctx_compound.addSphereObject(8, make_vec3(0, 0, 0), 0.5f, "lib/images/diamond_texture.png");
2376 std::vector<uint> sphere_primitives = ctx_compound.getObjectPrimitiveUUIDs(sphere_obj);
2377
2378 // All primitives should have positive area
2379 int sphere_positive_area = 0, sphere_zero_area = 0;
2380 for (uint uuid: sphere_primitives) {
2381 float area = ctx_compound.getPrimitiveArea(uuid);
2382 DOCTEST_CHECK(area >= 0.0f);
2383 if (area > 0.0f) {
2384 sphere_positive_area++;
2385 } else {
2386 sphere_zero_area++;
2387 }
2388 }
2389
2390 DOCTEST_CHECK(sphere_positive_area > 0); // Should have some positive area triangles
2391 DOCTEST_CHECK(sphere_zero_area == 0); // Should have no zero area triangles
2392
2393 // Test addTubeObject with transparent texture
2394 std::vector<vec3> tube_nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
2395 std::vector<float> tube_radii = {0.1f, 0.15f, 0.1f};
2396 uint tube_obj = ctx_compound.addTubeObject(8, tube_nodes, tube_radii, "lib/images/diamond_texture.png");
2397 std::vector<uint> tube_primitives = ctx_compound.getObjectPrimitiveUUIDs(tube_obj);
2398
2399 // All primitives should have positive area
2400 int tube_positive_area = 0, tube_zero_area = 0;
2401 for (uint uuid: tube_primitives) {
2402 float area = ctx_compound.getPrimitiveArea(uuid);
2403 DOCTEST_CHECK(area >= 0.0f);
2404 if (area > 0.0f) {
2405 tube_positive_area++;
2406 } else {
2407 tube_zero_area++;
2408 }
2409 }
2410
2411 DOCTEST_CHECK(tube_positive_area > 0); // Should have some positive area triangles
2412 DOCTEST_CHECK(tube_zero_area == 0); // Should have no zero area triangles
2413
2414 // Test addDiskObject with transparent texture
2415 uint disk_obj = ctx_compound.addDiskObject(make_int2(4, 3), make_vec3(0, 0, 0), make_vec2(1.0f, 1.0f), make_SphericalCoord(0, 0), "lib/images/disk_texture.png");
2416 std::vector<uint> disk_primitives = ctx_compound.getObjectPrimitiveUUIDs(disk_obj);
2417
2418 // All primitives should have positive area
2419 int disk_positive_area = 0, disk_zero_area = 0;
2420 for (uint uuid: disk_primitives) {
2421 float area = ctx_compound.getPrimitiveArea(uuid);
2422 DOCTEST_CHECK(area >= 0.0f);
2423 if (area > 0.0f) {
2424 disk_positive_area++;
2425 } else {
2426 disk_zero_area++;
2427 }
2428 }
2429
2430 DOCTEST_CHECK(disk_positive_area > 0); // Should have some positive area triangles
2431 DOCTEST_CHECK(disk_zero_area == 0); // Should have no zero area triangles
2432
2433 // Test addConeObject with transparent texture
2434 uint cone_obj = ctx_compound.addConeObject(8, make_vec3(0, 0, 0), make_vec3(0, 0, 1), 0.1f, 0.2f, "lib/images/diamond_texture.png");
2435 std::vector<uint> cone_primitives = ctx_compound.getObjectPrimitiveUUIDs(cone_obj);
2436
2437 // All primitives should have positive area
2438 int cone_positive_area = 0, cone_zero_area = 0;
2439 for (uint uuid: cone_primitives) {
2440 float area = ctx_compound.getPrimitiveArea(uuid);
2441 DOCTEST_CHECK(area >= 0.0f);
2442 if (area > 0.0f) {
2443 cone_positive_area++;
2444 } else {
2445 cone_zero_area++;
2446 }
2447 }
2448
2449 DOCTEST_CHECK(cone_positive_area > 0); // Should have some positive area triangles
2450 DOCTEST_CHECK(cone_zero_area == 0); // Should have no zero area triangles
2451 }
2452 }
2453}
2454
2455TEST_CASE("File path resolution priority") {
2456 SUBCASE("resolveFilePath current directory priority") {
2457 // Test that the new file resolution logic checks current directory first,
2458 // then falls back to HELIOS_BUILD directory
2459
2460 // Create a test texture file in the current directory
2461 std::string testFileName = "test_file_resolution.jpg";
2462 std::filesystem::path currentDirFile = std::filesystem::current_path() / testFileName;
2463
2464 // Copy the existing texture for our test
2465 std::filesystem::path sourceTexture = "core/lib/models/texture.jpg";
2466
2467 if (std::filesystem::exists(sourceTexture)) {
2468 // Copy to current directory
2469 std::filesystem::copy_file(sourceTexture, currentDirFile, std::filesystem::copy_options::overwrite_existing);
2470 DOCTEST_CHECK(std::filesystem::exists(currentDirFile));
2471
2472 // Test resolveFilePath function directly
2473 std::filesystem::path resolved = helios::resolveFilePath(testFileName);
2474 DOCTEST_CHECK(resolved == std::filesystem::canonical(currentDirFile));
2475
2476 // Clean up
2477 std::filesystem::remove(currentDirFile);
2478 }
2479 }
2480
2481 SUBCASE("addPatch with texture from current directory") {
2482 Context ctx;
2483
2484 // Create test directory structure in current working directory
2485 std::filesystem::create_directories("test_models");
2486 std::string testTexture = "test_models/test_texture.jpg";
2487 std::filesystem::path testTexturePath = std::filesystem::current_path() / testTexture;
2488
2489 // Copy source texture
2490 std::filesystem::path sourceTexture = "core/lib/models/texture.jpg";
2491
2492 if (std::filesystem::exists(sourceTexture)) {
2493 std::filesystem::copy_file(sourceTexture, testTexturePath, std::filesystem::copy_options::overwrite_existing);
2494
2495 // This should work with the fix - loads from current directory first
2496 // addPatch uses resolveFilePath internally for texture loading
2497 SphericalCoord rotation = make_SphericalCoord(0, 0);
2498 uint patch_id;
2499 DOCTEST_CHECK_NOTHROW({ patch_id = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), rotation, testTexture.c_str()); });
2500 DOCTEST_CHECK(patch_id > 0);
2501
2502 // Verify the texture loaded correctly
2503 bool has_transparency = ctx.primitiveTextureHasTransparencyChannel(patch_id);
2504 DOCTEST_CHECK((has_transparency || !has_transparency)); // Just verify it's a boolean (texture loaded)
2505
2506 // Clean up
2507 std::filesystem::remove(testTexturePath);
2508 std::filesystem::remove("test_models");
2509 }
2510 }
2511
2512 SUBCASE("Material System - Label-Based Creation") {
2513 Context ctx;
2514
2515 // Default material should exist (but not counted in getMaterialCount or listMaterials)
2516 DOCTEST_CHECK(ctx.doesMaterialExist("__default__"));
2517 DOCTEST_CHECK(ctx.getMaterialCount() == 0); // No user-created materials yet
2518
2519 // Create materials with labels
2520 ctx.addMaterial("leaf_material");
2521 DOCTEST_CHECK(ctx.doesMaterialExist("leaf_material"));
2522 DOCTEST_CHECK(ctx.getMaterialCount() == 1);
2523
2524 ctx.addMaterial("bark_material");
2525 DOCTEST_CHECK(ctx.doesMaterialExist("bark_material"));
2526 DOCTEST_CHECK(ctx.getMaterialCount() == 2);
2527
2528 // List materials (only user-created, not default or auto-generated)
2529 std::vector<std::string> labels = ctx.listMaterials();
2530 DOCTEST_CHECK(labels.size() == 2);
2531
2532 // Reserved labels should fail
2533 DOCTEST_CHECK_THROWS(ctx.addMaterial("__reserved"));
2534 }
2535
2536 SUBCASE("Material System - Properties") {
2537 Context ctx;
2538
2539 // Create and set material properties
2540 ctx.addMaterial("test_mat");
2541
2542 RGBAcolor purple = make_RGBAcolor(0.5f, 0, 0.5f, 1);
2543 ctx.setMaterialColor("test_mat", purple);
2544
2545 RGBAcolor color = ctx.getMaterialColor("test_mat");
2546 DOCTEST_CHECK(color.r == doctest::Approx(0.5f).epsilon(0.001));
2547 DOCTEST_CHECK(color.g == doctest::Approx(0.0f).epsilon(0.001));
2548 DOCTEST_CHECK(color.b == doctest::Approx(0.5f).epsilon(0.001));
2549
2550 // Set texture
2551 ctx.setMaterialTexture("test_mat", "lib/images/disk_texture.png");
2552 std::string tex = ctx.getMaterialTexture("test_mat");
2553 DOCTEST_CHECK(tex == "lib/images/disk_texture.png");
2554
2555 // Texture override
2556 ctx.setMaterialTextureColorOverride("test_mat", true);
2557 DOCTEST_CHECK(ctx.isMaterialTextureColorOverridden("test_mat"));
2558
2559 ctx.setMaterialTextureColorOverride("test_mat", false);
2560 DOCTEST_CHECK(!ctx.isMaterialTextureColorOverridden("test_mat"));
2561
2562 // Twosided flag - test default value
2563 DOCTEST_CHECK(ctx.getMaterialTwosidedFlag("test_mat") == 1); // Default is 1 (two-sided)
2564
2565 // Twosided flag - set to 0 (one-sided)
2566 ctx.setMaterialTwosidedFlag("test_mat", 0);
2567 DOCTEST_CHECK(ctx.getMaterialTwosidedFlag("test_mat") == 0);
2568
2569 // Twosided flag - set back to 1 (two-sided)
2570 ctx.setMaterialTwosidedFlag("test_mat", 1);
2571 DOCTEST_CHECK(ctx.getMaterialTwosidedFlag("test_mat") == 1);
2572 }
2573
2574 SUBCASE("Material System - Assignment to Primitives") {
2575 Context ctx;
2576
2577 // Create material
2578 ctx.addMaterial("red_mat");
2579 ctx.setMaterialColor("red_mat", make_RGBAcolor(1, 0, 0, 1));
2580
2581 // Create primitives with default color
2582 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2583 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2584
2585 // Assign material
2586 ctx.assignMaterialToPrimitive(p1, "red_mat");
2587 ctx.assignMaterialToPrimitive(p2, "red_mat");
2588
2589 // Check primitive material label
2590 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(p1) == "red_mat");
2591 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(p2) == "red_mat");
2592
2593 // Check primitive color reflects material
2594 RGBcolor c1 = ctx.getPrimitiveColor(p1);
2595 DOCTEST_CHECK(c1.r == doctest::Approx(1.0f).epsilon(0.001));
2596 DOCTEST_CHECK(c1.g == doctest::Approx(0.0f).epsilon(0.001));
2597
2598 // Modify material - should affect both primitives
2599 ctx.setMaterialColor("red_mat", make_RGBAcolor(0, 1, 0, 1)); // Green
2600
2601 c1 = ctx.getPrimitiveColor(p1);
2602 RGBcolor c2 = ctx.getPrimitiveColor(p2);
2603 DOCTEST_CHECK(c1.g == doctest::Approx(1.0f).epsilon(0.001));
2604 DOCTEST_CHECK(c2.g == doctest::Approx(1.0f).epsilon(0.001));
2605
2606 // Reverse lookup
2607 std::vector<uint> users = ctx.getPrimitivesUsingMaterial("red_mat");
2608 DOCTEST_CHECK(users.size() == 2);
2609 }
2610
2611 SUBCASE("Material System - Batch Assignment") {
2612 Context ctx;
2613
2614 ctx.addMaterial("batch_mat");
2615 ctx.setMaterialColor("batch_mat", make_RGBAcolor(0.5f, 0.5f, 0.5f, 1));
2616
2617 std::vector<uint> UUIDs;
2618 for (int i = 0; i < 10; i++) {
2619 UUIDs.push_back(ctx.addPatch(make_vec3(i, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0)));
2620 }
2621
2622 // Batch assign
2623 ctx.assignMaterialToPrimitive(UUIDs, "batch_mat");
2624
2625 // Verify all have the material
2626 for (uint uuid: UUIDs) {
2627 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(uuid) == "batch_mat");
2628 }
2629 }
2630
2631 SUBCASE("Material System - Deletion") {
2632 Context ctx;
2633
2634 ctx.addMaterial("temp_mat");
2635 ctx.setMaterialColor("temp_mat", make_RGBAcolor(1, 0, 0, 1));
2636
2637 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2638 ctx.assignMaterialToPrimitive(p1, "temp_mat");
2639
2640 // Delete material - primitive should revert to default
2641 capture_cerr c; // Capture warning about material in use
2642 ctx.deleteMaterial("temp_mat");
2643
2644 DOCTEST_CHECK(!ctx.doesMaterialExist("temp_mat"));
2645 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(p1) == "__default__");
2646 }
2647
2648 SUBCASE("Material System - XML Round-Trip") {
2649 Context ctx;
2650
2651 // Create materials
2652 ctx.addMaterial("red_mat");
2653 ctx.setMaterialColor("red_mat", make_RGBAcolor(1, 0, 0, 1));
2654
2655 ctx.addMaterial("textured_mat");
2656 ctx.setMaterialColor("textured_mat", make_RGBAcolor(0, 1, 0, 1));
2657 ctx.setMaterialTexture("textured_mat", "lib/images/disk_texture.png");
2658
2659 // Create a material with non-default twosided_flag
2660 ctx.addMaterial("onesided_mat");
2661 ctx.setMaterialColor("onesided_mat", make_RGBAcolor(0, 0, 1, 1));
2662 ctx.setMaterialTwosidedFlag("onesided_mat", 0); // One-sided
2663
2664 // Create and assign primitives
2665 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2666 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2667 uint p3 = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2668 uint p4 = ctx.addPatch(make_vec3(3, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2669
2670 ctx.assignMaterialToPrimitive(p1, "red_mat");
2671 ctx.assignMaterialToPrimitive(p2, "textured_mat");
2672 ctx.assignMaterialToPrimitive(p3, "red_mat");
2673 ctx.assignMaterialToPrimitive(p4, "onesided_mat");
2674
2675 // Write to XML
2676 ctx.writeXML("test_materials.xml", {p1, p2, p3, p4}, true);
2677
2678 // Load into new context
2679 Context ctx2;
2680 std::vector<uint> loaded_UUIDs = ctx2.loadXML("test_materials.xml", true);
2681
2682 DOCTEST_CHECK(loaded_UUIDs.size() == 4);
2683
2684 // Verify materials were preserved
2685 DOCTEST_CHECK(ctx2.doesMaterialExist("red_mat"));
2686 DOCTEST_CHECK(ctx2.doesMaterialExist("textured_mat"));
2687 DOCTEST_CHECK(ctx2.doesMaterialExist("onesided_mat"));
2688
2689 RGBcolor loaded_color1 = ctx2.getPrimitiveColor(loaded_UUIDs[0]);
2690 DOCTEST_CHECK(loaded_color1.r == doctest::Approx(1.0f).epsilon(0.001));
2691
2692 DOCTEST_CHECK(ctx2.getPrimitiveTextureFile(loaded_UUIDs[1]) == "lib/images/disk_texture.png");
2693
2694 // Verify twosided_flag was preserved
2695 DOCTEST_CHECK(ctx2.getMaterialTwosidedFlag("red_mat") == 1); // Default
2696 DOCTEST_CHECK(ctx2.getMaterialTwosidedFlag("textured_mat") == 1); // Default
2697 DOCTEST_CHECK(ctx2.getMaterialTwosidedFlag("onesided_mat") == 0); // Non-default
2698
2699 // Clean up
2700 std::filesystem::remove("test_materials.xml");
2701 }
2702
2703 SUBCASE("getPrimitiveTwosidedFlag helper function") {
2704 Context ctx;
2705
2706 // Create materials with different twosided_flag values
2707 ctx.addMaterial("onesided_mat");
2708 ctx.setMaterialTwosidedFlag("onesided_mat", 0);
2709
2710 ctx.addMaterial("twosided_mat");
2711 ctx.setMaterialTwosidedFlag("twosided_mat", 1);
2712
2713 ctx.addMaterial("transparent_mat");
2714 ctx.setMaterialTwosidedFlag("transparent_mat", 2);
2715
2716 // Create primitives
2717 uint UUID_mat_onesided = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
2718 uint UUID_mat_twosided = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1));
2719 uint UUID_mat_transparent = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1));
2720 uint UUID_prim_data = ctx.addPatch(make_vec3(3, 0, 0), make_vec2(1, 1));
2721 uint UUID_default = ctx.addPatch(make_vec3(4, 0, 0), make_vec2(1, 1));
2722
2723 // Assign materials
2724 ctx.assignMaterialToPrimitive(UUID_mat_onesided, "onesided_mat");
2725 ctx.assignMaterialToPrimitive(UUID_mat_twosided, "twosided_mat");
2726 ctx.assignMaterialToPrimitive(UUID_mat_transparent, "transparent_mat");
2727
2728 // Set primitive data on one primitive (no user material assigned)
2729 ctx.setPrimitiveData(UUID_prim_data, "twosided_flag", uint(0));
2730
2731 // Test: Material takes precedence - one-sided material
2732 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_onesided) == 0);
2733
2734 // Test: Material takes precedence - two-sided material
2735 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_twosided) == 1);
2736
2737 // Test: Material supports values > 1 (transparent)
2738 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_transparent) == 2);
2739
2740 // Test: Primitive data fallback (no user material)
2741 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_prim_data) == 0);
2742
2743 // Test: Default value when no material or primitive data
2744 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_default) == 1);
2745
2746 // Test: Custom default value
2747 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_default, 2) == 2);
2748
2749 // Test: Material takes precedence over primitive data
2750 // First, set primitive data on a primitive with a material
2751 ctx.setPrimitiveData(UUID_mat_onesided, "twosided_flag", uint(1)); // Try to override with primitive data
2752 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_onesided) == 0); // Should still return material value (0)
2753 }
2754
2755 SUBCASE("Material Data - Setting and Getting with Labels") {
2756 Context ctx;
2757
2758 // Create a material
2759 ctx.addMaterial("data_mat");
2760
2761 // Test uint data
2762 ctx.setMaterialData("data_mat", "twosided_flag", 1u);
2763 DOCTEST_CHECK(ctx.doesMaterialDataExist("data_mat", "twosided_flag"));
2764 DOCTEST_CHECK(ctx.getMaterialDataType("data_mat", "twosided_flag") == HELIOS_TYPE_UINT);
2765 uint flag_val;
2766 ctx.getMaterialData("data_mat", "twosided_flag", flag_val);
2767 DOCTEST_CHECK(flag_val == 1u);
2768
2769 // Test int data
2770 ctx.setMaterialData("data_mat", "test_int", -42);
2771 int int_val;
2772 ctx.getMaterialData("data_mat", "test_int", int_val);
2773 DOCTEST_CHECK(int_val == -42);
2774
2775 // Test float data
2776 ctx.setMaterialData("data_mat", "test_float", 3.14f);
2777 float float_val;
2778 ctx.getMaterialData("data_mat", "test_float", float_val);
2779 DOCTEST_CHECK(float_val == doctest::Approx(3.14f).epsilon(0.001));
2780
2781 // Test vec3 data
2782 vec3 test_vec = make_vec3(1, 2, 3);
2783 ctx.setMaterialData("data_mat", "test_vec3", test_vec);
2784 vec3 vec_val;
2785 ctx.getMaterialData("data_mat", "test_vec3", vec_val);
2786 DOCTEST_CHECK(vec_val.x == doctest::Approx(1.0f).epsilon(0.001));
2787 DOCTEST_CHECK(vec_val.y == doctest::Approx(2.0f).epsilon(0.001));
2788 DOCTEST_CHECK(vec_val.z == doctest::Approx(3.0f).epsilon(0.001));
2789
2790 // Test string data
2791 ctx.setMaterialData("data_mat", "test_string", std::string("hello"));
2792 std::string str_val;
2793 ctx.getMaterialData("data_mat", "test_string", str_val);
2794 DOCTEST_CHECK(str_val == "hello");
2795
2796 // Test clearing data
2797 ctx.clearMaterialData("data_mat", "test_int");
2798 DOCTEST_CHECK(!ctx.doesMaterialDataExist("data_mat", "test_int"));
2799 }
2800
2801 SUBCASE("Material Data - Fallback Helper Method") {
2802 Context ctx;
2803
2804 // Create material with data
2805 ctx.addMaterial("fallback_mat");
2806 ctx.setMaterialData("fallback_mat", "twosided_flag", 0u);
2807
2808 // Create primitive with this material
2809 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2810 ctx.assignMaterialToPrimitive(p1, "fallback_mat");
2811
2812 // Test getDataWithMaterialFallback - should get data from material
2813 uint flag_val;
2814 ctx.getDataWithMaterialFallback(p1, "twosided_flag", flag_val);
2815 DOCTEST_CHECK(flag_val == 0u);
2816
2817 // Create another primitive with material but add primitive-specific data
2818 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2819 ctx.assignMaterialToPrimitive(p2, "fallback_mat");
2820 ctx.setPrimitiveData(p2, "custom_data", 42);
2821
2822 // Test fallback - should get data from primitive since material doesn't have it
2823 int custom_val;
2824 ctx.getDataWithMaterialFallback(p2, "custom_data", custom_val);
2825 DOCTEST_CHECK(custom_val == 42);
2826
2827 // Create third primitive with no special data
2828 uint p3 = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2829 ctx.assignMaterialToPrimitive(p3, "fallback_mat");
2830
2831 // Test fallback - should throw error for non-existent data
2832 int nonexistent_val;
2833 DOCTEST_CHECK_THROWS(ctx.getDataWithMaterialFallback(p3, "nonexistent", nonexistent_val));
2834 }
2835
2836 SUBCASE("Material Data - XML Round-Trip with Labels") {
2837 Context ctx;
2838
2839 // Create material with data
2840 ctx.addMaterial("data_round_trip_mat");
2841 ctx.setMaterialColor("data_round_trip_mat", make_RGBAcolor(0.5f, 0.25f, 0.75f, 1));
2842 ctx.setMaterialData("data_round_trip_mat", "twosided_flag", 1u);
2843 ctx.setMaterialData("data_round_trip_mat", "reflectance", 0.8f);
2844 ctx.setMaterialData("data_round_trip_mat", "normal", make_vec3(0, 0, 1));
2845
2846 // Create primitives with this material
2847 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2848 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2849 ctx.assignMaterialToPrimitive(p1, "data_round_trip_mat");
2850 ctx.assignMaterialToPrimitive(p2, "data_round_trip_mat");
2851
2852 // Write to XML
2853 ctx.writeXML("test_material_data.xml", true);
2854
2855 // Load into new context
2856 Context ctx2;
2857 ctx2.loadXML("test_material_data.xml", true);
2858
2859 // Verify material and data were preserved
2860 DOCTEST_CHECK(ctx2.doesMaterialExist("data_round_trip_mat"));
2861
2862 DOCTEST_CHECK(ctx2.doesMaterialDataExist("data_round_trip_mat", "twosided_flag"));
2863 uint flag_val;
2864 ctx2.getMaterialData("data_round_trip_mat", "twosided_flag", flag_val);
2865 DOCTEST_CHECK(flag_val == 1u);
2866
2867 DOCTEST_CHECK(ctx2.doesMaterialDataExist("data_round_trip_mat", "reflectance"));
2868 float refl_val;
2869 ctx2.getMaterialData("data_round_trip_mat", "reflectance", refl_val);
2870 DOCTEST_CHECK(refl_val == doctest::Approx(0.8f).epsilon(0.001));
2871
2872 DOCTEST_CHECK(ctx2.doesMaterialDataExist("data_round_trip_mat", "normal"));
2873 vec3 norm_val;
2874 ctx2.getMaterialData("data_round_trip_mat", "normal", norm_val);
2875 DOCTEST_CHECK(norm_val.x == doctest::Approx(0.0f).epsilon(0.001));
2876 DOCTEST_CHECK(norm_val.y == doctest::Approx(0.0f).epsilon(0.001));
2877 DOCTEST_CHECK(norm_val.z == doctest::Approx(1.0f).epsilon(0.001));
2878
2879 // Clean up
2880 std::filesystem::remove("test_material_data.xml");
2881 }
2882
2883 SUBCASE("Material Methods - getPrimitiveMaterialID and getMaterial") {
2884 Context ctx;
2885
2886 // Create materials
2887 ctx.addMaterial("test_mat_1");
2888 ctx.setMaterialColor("test_mat_1", make_RGBAcolor(1, 0, 0, 1));
2889 uint mat1_id = ctx.getMaterialIDFromLabel("test_mat_1");
2890
2891 ctx.addMaterial("test_mat_2");
2892 ctx.setMaterialColor("test_mat_2", make_RGBAcolor(0, 1, 0, 1));
2893 uint mat2_id = ctx.getMaterialIDFromLabel("test_mat_2");
2894
2895 // Create primitives and assign materials
2896 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
2897 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1));
2898 uint p3 = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1));
2899
2900 ctx.assignMaterialToPrimitive(p1, "test_mat_1");
2901 ctx.assignMaterialToPrimitive(p2, "test_mat_2");
2902 ctx.assignMaterialToPrimitive(p3, "test_mat_1");
2903
2904 // Test getPrimitiveMaterialID
2905 DOCTEST_CHECK(ctx.getPrimitiveMaterialID(p1) == mat1_id);
2906 DOCTEST_CHECK(ctx.getPrimitiveMaterialID(p2) == mat2_id);
2907 DOCTEST_CHECK(ctx.getPrimitiveMaterialID(p3) == mat1_id);
2908
2909 // Test getMaterial
2910 const Material &mat1 = ctx.getMaterial(mat1_id);
2911 DOCTEST_CHECK(mat1.label == "test_mat_1");
2912 DOCTEST_CHECK(mat1.color.r == doctest::Approx(1.0f));
2913 DOCTEST_CHECK(mat1.color.g == doctest::Approx(0.0f));
2914 DOCTEST_CHECK(mat1.color.b == doctest::Approx(0.0f));
2915
2916 const Material &mat2 = ctx.getMaterial(mat2_id);
2917 DOCTEST_CHECK(mat2.label == "test_mat_2");
2918 DOCTEST_CHECK(mat2.color.r == doctest::Approx(0.0f));
2919 DOCTEST_CHECK(mat2.color.g == doctest::Approx(1.0f));
2920 DOCTEST_CHECK(mat2.color.b == doctest::Approx(0.0f));
2921
2922 // Test getMaterial with invalid ID throws error
2923 DOCTEST_CHECK_THROWS((void) ctx.getMaterial(99999));
2924 }
2925
2926 SUBCASE("Material Methods - getMaterialIDFromLabel") {
2927 Context ctx;
2928
2929 // Create several materials
2930 ctx.addMaterial("material_a");
2931 ctx.addMaterial("material_b");
2932 ctx.addMaterial("material_c");
2933
2934 // Test getting IDs from labels
2935 uint id_a = ctx.getMaterialIDFromLabel("material_a");
2936 uint id_b = ctx.getMaterialIDFromLabel("material_b");
2937 uint id_c = ctx.getMaterialIDFromLabel("material_c");
2938
2939 // IDs should be unique
2940 DOCTEST_CHECK(id_a != id_b);
2941 DOCTEST_CHECK(id_b != id_c);
2942 DOCTEST_CHECK(id_a != id_c);
2943
2944 // Getting same label should return same ID
2945 DOCTEST_CHECK(ctx.getMaterialIDFromLabel("material_a") == id_a);
2946 DOCTEST_CHECK(ctx.getMaterialIDFromLabel("material_b") == id_b);
2947
2948 // Non-existent label should throw error
2949 DOCTEST_CHECK_THROWS((void) ctx.getMaterialIDFromLabel("nonexistent_material"));
2950 }
2951
2952 SUBCASE("Material copy-on-write - basic color modification") {
2953 Context context;
2954
2955 // Create two primitives with same color (shared material via deduplication)
2956 uint uuid1 = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), RGB::red);
2957 uint uuid2 = context.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), RGB::red);
2958
2959 // Verify they share material initially
2960 std::string mat1_before = context.getPrimitiveMaterialLabel(uuid1);
2961 std::string mat2_before = context.getPrimitiveMaterialLabel(uuid2);
2962 DOCTEST_CHECK(mat1_before == mat2_before);
2963
2964 // Modify one primitive's color
2965 context.setPrimitiveColor(uuid1, RGB::blue);
2966
2967 // Verify materials are now different (copy-on-write occurred)
2968 std::string mat1_after = context.getPrimitiveMaterialLabel(uuid1);
2969 std::string mat2_after = context.getPrimitiveMaterialLabel(uuid2);
2970 DOCTEST_CHECK(mat1_after != mat2_after);
2971
2972 // Verify colors are independent
2973 RGBcolor color1 = context.getPrimitiveColor(uuid1);
2974 RGBcolor color2 = context.getPrimitiveColor(uuid2);
2975 DOCTEST_CHECK(color1 == RGB::blue);
2976 DOCTEST_CHECK(color2 == RGB::red);
2977 }
2978
2979 SUBCASE("Material copy-on-write - object-level modification") {
2980 Context context;
2981
2982 // Create two sphere objects with same color
2983 uint obj1 = context.addSphereObject(10, make_vec3(0, 0, 0), 1.f, RGB::green);
2984 uint obj2 = context.addSphereObject(10, make_vec3(3, 0, 0), 1.f, RGB::green);
2985
2986 // Modify one object's color
2987 context.setObjectColor(obj1, RGB::yellow);
2988
2989 // Verify objects have different colors
2990 auto prims1 = context.getObjectPrimitiveUUIDs(obj1);
2991 auto prims2 = context.getObjectPrimitiveUUIDs(obj2);
2992
2993 RGBcolor color1 = context.getPrimitiveColor(prims1[0]);
2994 RGBcolor color2 = context.getPrimitiveColor(prims2[0]);
2995
2996 DOCTEST_CHECK(color1 == RGB::yellow);
2997 DOCTEST_CHECK(color2 == RGB::green);
2998 }
2999
3000 SUBCASE("Material copy-on-write - non-shared optimization") {
3001 Context context;
3002
3003 // Create single primitive with explicit color
3004 uint uuid = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), RGB::cyan);
3005
3006 std::string mat1 = context.getPrimitiveMaterialLabel(uuid);
3007
3008 // Modify color - should NOT create new material since it's not shared
3009 context.setPrimitiveColor(uuid, RGB::magenta);
3010
3011 std::string mat2 = context.getPrimitiveMaterialLabel(uuid);
3012
3013 // Material should be same (no copy needed, just modified in place)
3014 DOCTEST_CHECK(mat1 == mat2);
3015 }
3016}