1.3.72
 
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 // updateTimeseriesData: replace value at an existing (date, time)
885 ctx.updateTimeseriesData("ts", date, time0, 999.9f);
886 DOCTEST_CHECK(ctx.queryTimeseriesData("ts", 0) == doctest::Approx(999.9f));
887 DOCTEST_CHECK(ctx.queryTimeseriesData("ts", date, time0) == doctest::Approx(999.9f));
888 // The other point should be untouched
889 DOCTEST_CHECK(ctx.queryTimeseriesData("ts", 1) == doctest::Approx(305.3f));
890 // Length is unchanged (update, not insert)
891 DOCTEST_CHECK(ctx.getTimeseriesLength("ts") == 2);
892
893 // Error: unknown label
894 {
895 capture_cerr cerr_buffer;
896 DOCTEST_CHECK_THROWS(ctx.updateTimeseriesData("nonexistent", date, time0, 0.f));
897 }
898
899 // Error: known label but (date, time) does not match any existing point
900 {
901 Time time_missing = make_Time(time0.hour, 0, 0);
902 capture_cerr cerr_buffer;
903 DOCTEST_CHECK_THROWS(ctx.updateTimeseriesData("ts", date, time_missing, 0.f));
904 }
905 }
906
907 SUBCASE("Primitive data") {
908 capture_cerr cerr_buffer;
909
910 Context ctx;
911 uint p = ctx.addPatch();
912 ctx.setPrimitiveData(p, "test_int", 5);
913 ctx.setPrimitiveData(p, "test_float", 3.14f);
914
915 // getPrimitiveDataType
916 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_int") == HELIOS_TYPE_INT);
917 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_float") == HELIOS_TYPE_FLOAT);
918
919 // getPrimitiveDataSize
920 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_int") == 1);
921
922 // clearPrimitiveData
923 ctx.clearPrimitiveData(p, "test_int");
924 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p, "test_int"));
925
926 // listPrimitiveData
927 std::vector<std::string> data_labels = ctx.listPrimitiveData(p);
928 DOCTEST_CHECK(std::find(data_labels.begin(), data_labels.end(), "test_float") != data_labels.end());
929
930 // getPrimitiveDataSize (doesn't exist)
931 DOCTEST_CHECK_THROWS(ctx.getPrimitiveDataSize(p, "test_int"));
932
933 // clearPrimitiveData
934 ctx.clearPrimitiveData(p, "test_int");
935 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p, "test_int"));
936
937 // listPrimitiveData
938 ctx.setPrimitiveData(p, "test_int", 5);
939 ctx.setPrimitiveData(p, "test_float", 3.14f);
940 std::vector<std::string> labels = ctx.listPrimitiveData(p);
941 DOCTEST_CHECK(labels.size() == 2);
942 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "test_int") != labels.end());
943 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "test_float") != labels.end());
944 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_float") == HELIOS_TYPE_FLOAT);
945 }
946}
947
948TEST_CASE("Data and Object Management") {
949
950 SUBCASE("Global data management") {
951 Context ctx;
952 ctx.setGlobalData("test_double", 1.23);
953 DOCTEST_CHECK(ctx.getGlobalDataSize("test_double") == 1);
954 DOCTEST_CHECK(ctx.getGlobalDataType("test_double") == HELIOS_TYPE_DOUBLE);
955 ctx.clearGlobalData("test_double");
956 DOCTEST_CHECK(!ctx.doesGlobalDataExist("test_double"));
957 ctx.setGlobalData("test_string", "hello");
958 std::vector<std::string> global_data_labels = ctx.listGlobalData();
959 DOCTEST_CHECK(std::find(global_data_labels.begin(), global_data_labels.end(), "test_string") != global_data_labels.end());
960 }
961
962 SUBCASE("Object data management") {
963 Context ctx;
964 uint obj = ctx.addBoxObject(nullorigin, make_vec3(1, 1, 1), make_int3(2, 3, 2));
965 ctx.setObjectData(obj, "test_vec", vec3(1, 2, 3));
966 DOCTEST_CHECK(ctx.getObjectDataSize(obj, "test_vec") == 1);
967 DOCTEST_CHECK(ctx.getObjectDataType("test_vec") == HELIOS_TYPE_VEC3);
968 ctx.clearObjectData(obj, "test_vec");
969 DOCTEST_CHECK(!ctx.doesObjectDataExist(obj, "test_vec"));
970 ctx.setObjectData(obj, "test_int", 42);
971 std::vector<std::string> object_data_labels = ctx.listObjectData(obj);
972 DOCTEST_CHECK(std::find(object_data_labels.begin(), object_data_labels.end(), "test_int") != object_data_labels.end());
973 }
974
975 SUBCASE("Object creation and manipulation") {
976 Context ctx;
977 uint disk = ctx.addDiskObject(10, make_vec3(0, 0, 0), make_vec2(1, 1));
978 DOCTEST_CHECK(ctx.getObjectType(disk) == OBJECT_TYPE_DISK);
979 DOCTEST_CHECK(ctx.getObjectArea(disk) > 0);
980 DOCTEST_CHECK(ctx.getDiskObjectCenter(disk) == make_vec3(0, 0, 0));
981 DOCTEST_CHECK(ctx.getDiskObjectSubdivisionCount(disk) == 10);
982 DOCTEST_CHECK(ctx.getDiskObjectSize(disk).x == doctest::Approx(1.f));
983
984 uint sphere = ctx.addSphereObject(10, make_vec3(1, 1, 1), 0.5f);
985 DOCTEST_CHECK(ctx.getObjectType(sphere) == OBJECT_TYPE_SPHERE);
986 DOCTEST_CHECK(ctx.getObjectArea(sphere) > 0);
987 DOCTEST_CHECK(ctx.getSphereObjectCenter(sphere) == make_vec3(1, 1, 1));
988 DOCTEST_CHECK(ctx.getSphereObjectSubdivisionCount(sphere) == 10);
989 DOCTEST_CHECK(ctx.getSphereObjectRadius(sphere).x == doctest::Approx(0.5f));
990
991 std::vector<uint> p_uuids;
992 p_uuids.push_back(ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0)));
993 uint polymesh = ctx.addPolymeshObject(p_uuids);
994 DOCTEST_CHECK(ctx.getObjectType(polymesh) == OBJECT_TYPE_POLYMESH);
995 DOCTEST_CHECK(ctx.getObjectArea(polymesh) > 0);
996 DOCTEST_CHECK(ctx.getObjectCenter(polymesh).z == doctest::Approx(0.f));
997
998 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1)};
999 std::vector<float> radii = {0.2f, 0.1f};
1000 uint tube = ctx.addTubeObject(10, nodes, radii);
1001 DOCTEST_CHECK(ctx.getObjectType(tube) == OBJECT_TYPE_TUBE);
1002 DOCTEST_CHECK(ctx.getObjectArea(tube) > 0);
1003 DOCTEST_CHECK(ctx.getObjectCenter(tube).z == doctest::Approx(0.5f));
1004 DOCTEST_CHECK(ctx.getTubeObjectSubdivisionCount(tube) == 10);
1005 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 2);
1006 DOCTEST_CHECK(ctx.getTubeObjectNodeRadii(tube).size() == 2);
1007 DOCTEST_CHECK(ctx.getTubeObjectNodeColors(tube).size() == 2);
1008 DOCTEST_CHECK(ctx.getTubeObjectVolume(tube) > 0);
1009 ctx.appendTubeSegment(tube, make_vec3(0, 0, 2), 0.05f, RGB::red);
1010 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 3);
1011 ctx.scaleTubeGirth(tube, 2.f);
1012 DOCTEST_CHECK(ctx.getTubeObjectNodeRadii(tube)[0] == doctest::Approx(0.4f));
1013 std::vector<float> new_radii = {0.3f, 0.2f, 0.1f};
1014 ctx.setTubeRadii(tube, new_radii);
1015 DOCTEST_CHECK(ctx.getTubeObjectNodeRadii(tube)[0] == doctest::Approx(0.3f));
1016 ctx.scaleTubeLength(tube, 2.f);
1017 std::vector<vec3> new_nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
1018 ctx.setTubeNodes(tube, new_nodes);
1019 ctx.pruneTubeNodes(tube, 1);
1020 DOCTEST_CHECK_FALSE(ctx.doesObjectExist(tube));
1021 }
1022
1023 SUBCASE("Object appearance and visibility") {
1024 Context ctx;
1025 uint box = ctx.addBoxObject(nullorigin, make_vec3(1, 1, 1), make_int3(2, 3, 2));
1026 ctx.overrideObjectTextureColor(box);
1027 // Cannot check state, only that it runs
1028 ctx.useObjectTextureColor(box);
1029 // Cannot check state, only that it runs
1030 ctx.hideObject(box);
1031 DOCTEST_CHECK(ctx.isObjectHidden(box));
1032 ctx.showObject(box);
1033 DOCTEST_CHECK(!ctx.isObjectHidden(box));
1034
1035 std::vector<uint> prims = ctx.getObjectPrimitiveUUIDs(box);
1036 ctx.hidePrimitive(prims);
1037 DOCTEST_CHECK(ctx.isPrimitiveHidden(prims[0]));
1038 ctx.showPrimitive(prims);
1039 DOCTEST_CHECK(!ctx.isPrimitiveHidden(prims[0]));
1040 }
1041
1042 SUBCASE("Primitive color and parent object") {
1043 capture_cerr cerr_buffer; // Capture deprecation warnings from setPrimitiveColor/usePrimitiveTextureColor
1044 Context ctx;
1045 uint p = ctx.addPatch();
1046 ctx.setPrimitiveColor(p, RGB::red);
1047 DOCTEST_CHECK(ctx.getPrimitiveColor(p) == RGB::red);
1048 ctx.overridePrimitiveTextureColor(p);
1049 DOCTEST_CHECK(ctx.isPrimitiveTextureColorOverridden(p));
1050 ctx.usePrimitiveTextureColor(p);
1051 DOCTEST_CHECK(!ctx.isPrimitiveTextureColorOverridden(p));
1052
1053 uint obj = ctx.addBoxObject(nullorigin, make_vec3(1, 1, 1), make_int3(2, 3, 2));
1054 ctx.setPrimitiveParentObjectID(p, obj);
1055 DOCTEST_CHECK(ctx.getPrimitiveParentObjectID(p) == obj);
1056 }
1057}
1058TEST_CASE("Object Management: Creation and Properties") {
1059
1060 SUBCASE("addSphereObject") {
1061 Context ctx;
1062 uint objID = ctx.addSphereObject(10, make_vec3(1, 2, 3), 5.f);
1063 DOCTEST_CHECK(ctx.doesObjectExist(objID));
1064 DOCTEST_CHECK(ctx.getSphereObjectCenter(objID) == make_vec3(1, 2, 3));
1065 DOCTEST_CHECK(ctx.getSphereObjectRadius(objID) == make_vec3(5.f, 5.f, 5.f));
1066 DOCTEST_CHECK(ctx.getSphereObjectSubdivisionCount(objID) == 10);
1067 }
1068
1069 SUBCASE("addDiskObject") {
1070 Context ctx;
1071 uint objID = ctx.addDiskObject(make_int2(8, 16), make_vec3(1, 2, 3), make_vec2(4, 5), nullrotation, RGB::red);
1072 DOCTEST_CHECK(ctx.doesObjectExist(objID));
1073 DOCTEST_CHECK(ctx.getDiskObjectCenter(objID) == make_vec3(1, 2, 3));
1074 DOCTEST_CHECK(ctx.getDiskObjectSize(objID) == make_vec2(4, 5));
1075 DOCTEST_CHECK(ctx.getDiskObjectSubdivisionCount(objID) == 8u);
1076 }
1077
1078 SUBCASE("addConeObject") {
1079 Context ctx;
1080 uint objID = ctx.addConeObject(10, make_vec3(0, 0, 0), make_vec3(0, 0, 5), 2.f, 1.f);
1081 DOCTEST_CHECK(ctx.doesObjectExist(objID));
1082 DOCTEST_CHECK(ctx.getConeObjectNode(objID, 0) == make_vec3(0, 0, 0));
1083 DOCTEST_CHECK(ctx.getConeObjectNode(objID, 1) == make_vec3(0, 0, 5));
1084 DOCTEST_CHECK(ctx.getConeObjectNodeRadius(objID, 0) == 2.f);
1085 DOCTEST_CHECK(ctx.getConeObjectNodeRadius(objID, 1) == 1.f);
1086 DOCTEST_CHECK(ctx.getConeObjectSubdivisionCount(objID) == 10);
1087 }
1088}
1089
1090TEST_CASE("Global Data Management") {
1091 SUBCASE("Integer Data") {
1092 Context ctx;
1093 ctx.setGlobalData("test_int", 123);
1094 DOCTEST_CHECK(ctx.doesGlobalDataExist("test_int"));
1095 int val;
1096 ctx.getGlobalData("test_int", val);
1097 DOCTEST_CHECK(val == 123);
1098 DOCTEST_CHECK(ctx.getGlobalDataSize("test_int") == 1);
1099 DOCTEST_CHECK(ctx.getGlobalDataType("test_int") == HELIOS_TYPE_INT);
1100 ctx.clearGlobalData("test_int");
1101 DOCTEST_CHECK(!ctx.doesGlobalDataExist("test_int"));
1102 }
1103
1104 SUBCASE("Vector Data") {
1105 Context ctx;
1106 std::vector<vec3> vec_data = {{1, 2, 3}, {4, 5, 6}};
1107 ctx.setGlobalData("test_vec", vec_data);
1108 DOCTEST_CHECK(ctx.doesGlobalDataExist("test_vec"));
1109 std::vector<vec3> read_vec;
1110 ctx.getGlobalData("test_vec", read_vec);
1111 DOCTEST_CHECK(read_vec.size() == 2);
1112 DOCTEST_CHECK(read_vec[1] == make_vec3(4, 5, 6));
1113 DOCTEST_CHECK(ctx.getGlobalDataSize("test_vec") == 2);
1114 DOCTEST_CHECK(ctx.getGlobalDataType("test_vec") == HELIOS_TYPE_VEC3);
1115 }
1116
1117 SUBCASE("List Data") {
1118 Context ctx;
1119 ctx.setGlobalData("d1", 1);
1120 ctx.setGlobalData("d2", 2.f);
1121 std::vector<std::string> labels = ctx.listGlobalData();
1122 DOCTEST_CHECK(labels.size() == 2);
1123 DOCTEST_CHECK(std::find(labels.begin(), labels.end(), "d1") != labels.end());
1124 }
1125}
1126
1127TEST_CASE("Context primitive data management") {
1128 Context ctx;
1129 uint p1 = ctx.addPatch();
1130 uint p2 = ctx.addPatch();
1131 ctx.setPrimitiveData(p1, "my_data", 10);
1132
1133 // copyPrimitiveData
1134 ctx.copyPrimitiveData(p1, p2);
1135 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p2, "my_data"));
1136 int val;
1137 ctx.getPrimitiveData(p2, "my_data", val);
1138 DOCTEST_CHECK(val == 10);
1139
1140 // renamePrimitiveData
1141 ctx.renamePrimitiveData(p1, "my_data", "new_data_name");
1142 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p1, "my_data"));
1143 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p1, "new_data_name"));
1144
1145 // duplicatePrimitiveData
1146 ctx.duplicatePrimitiveData(p2, "my_data", "my_data_copy");
1147 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p2, "my_data_copy"));
1148 ctx.getPrimitiveData(p2, "my_data_copy", val);
1149 DOCTEST_CHECK(val == 10);
1150
1151 // duplicatePrimitiveData (all primitives)
1152 ctx.setPrimitiveData(p1, "global_copy_test", 5.5f);
1153 ctx.duplicatePrimitiveData("global_copy_test", "global_copy_test_new");
1154 DOCTEST_CHECK(ctx.doesPrimitiveDataExist(p1, "global_copy_test_new"));
1155 DOCTEST_CHECK(!ctx.doesPrimitiveDataExist(p2, "global_copy_test_new")); // p2 doesn't have original
1156
1157 ctx.clearPrimitiveData(p1, "new_data_name");
1158 ctx.setPrimitiveData(p2, "my_data_copy", 15);
1159 ctx.setPrimitiveData(p2, "my_data_copy", 20);
1160 ctx.clearPrimitiveData(p2, "my_data_copy");
1161 std::vector<std::string> all_labels = ctx.listAllPrimitiveDataLabels();
1162 DOCTEST_CHECK(std::find(all_labels.begin(), all_labels.end(), "my_data") != all_labels.end());
1163 DOCTEST_CHECK(std::find(all_labels.begin(), all_labels.end(), "my_data_copy") == all_labels.end());
1164 DOCTEST_CHECK(std::find(all_labels.begin(), all_labels.end(), "new_data_name") == all_labels.end());
1165}
1166
1167TEST_CASE("Context primitive data calculations") {
1168 Context ctx;
1169 std::vector<uint> uuids;
1170 for (int i = 0; i < 5; ++i) {
1171 uint p = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
1172 ctx.setPrimitiveData(p, "float_val", (float) i);
1173 ctx.setPrimitiveData(p, "double_val", (double) i);
1174 ctx.setPrimitiveData(p, "vec2_val", make_vec2((float) i, (float) i));
1175 uuids.push_back(p);
1176 }
1177
1178 // calculatePrimitiveDataMean
1179 float float_mean;
1180 ctx.calculatePrimitiveDataMean(uuids, "float_val", float_mean);
1181 DOCTEST_CHECK(float_mean == doctest::Approx(2.0f));
1182 double double_mean;
1183 ctx.calculatePrimitiveDataMean(uuids, "double_val", double_mean);
1184 DOCTEST_CHECK(double_mean == doctest::Approx(2.0));
1185 vec2 vec2_mean;
1186 ctx.calculatePrimitiveDataMean(uuids, "vec2_val", vec2_mean);
1187 DOCTEST_CHECK(vec2_mean.x == doctest::Approx(2.0f));
1188
1189 // calculatePrimitiveDataAreaWeightedMean
1190 float awt_mean_f;
1191 ctx.calculatePrimitiveDataAreaWeightedMean(uuids, "float_val", awt_mean_f);
1192 DOCTEST_CHECK(awt_mean_f == doctest::Approx(2.0f)); // Area is 1 for all
1193
1194 // calculatePrimitiveDataSum
1195 float float_sum;
1196 ctx.calculatePrimitiveDataSum(uuids, "float_val", float_sum);
1197 DOCTEST_CHECK(float_sum == doctest::Approx(10.0f));
1198
1199 // calculatePrimitiveDataAreaWeightedSum
1200 float awt_sum_f;
1201 ctx.calculatePrimitiveDataAreaWeightedSum(uuids, "float_val", awt_sum_f);
1202 DOCTEST_CHECK(awt_sum_f == doctest::Approx(10.0f));
1203
1204 // scalePrimitiveData
1205 ctx.scalePrimitiveData(uuids, "float_val", 2.0f);
1206 ctx.getPrimitiveData(uuids[2], "float_val", float_mean);
1207 DOCTEST_CHECK(float_mean == doctest::Approx(4.0f));
1208 ctx.scalePrimitiveData("double_val", 0.5f);
1209 ctx.getPrimitiveData(uuids[4], "double_val", double_mean);
1210 DOCTEST_CHECK(double_mean == doctest::Approx(2.0));
1211
1212 // incrementPrimitiveData
1213 ctx.setPrimitiveData(uuids, "int_val", 10);
1214 ctx.incrementPrimitiveData(uuids, "int_val", 5);
1215 int int_val;
1216 ctx.getPrimitiveData(uuids[0], "int_val", int_val);
1217 DOCTEST_CHECK(int_val == 15);
1218 bool has_warning;
1219 {
1220 capture_cerr cerr_buffer;
1221 ctx.incrementPrimitiveData(uuids, "float_val", 1); // Wrong type, should warn
1222 has_warning = cerr_buffer.has_output();
1223 }
1224 DOCTEST_CHECK(has_warning);
1225}
1226
1227TEST_CASE("Context primitive data aggregation and filtering") {
1228 Context ctx;
1229 std::vector<uint> uuids;
1230 for (int i = 0; i < 3; ++i) {
1231 uint p = ctx.addPatch();
1232 ctx.setPrimitiveData(p, "d1", (float) i);
1233 ctx.setPrimitiveData(p, "d2", (float) i * 2.0f);
1234 ctx.setPrimitiveData(p, "d3", (float) i * 3.0f);
1235 ctx.setPrimitiveData(p, "filter_me", i);
1236 uuids.push_back(p);
1237 }
1238
1239 // aggregatePrimitiveDataSum
1240 std::vector<std::string> labels = {"d1", "d2", "d3"};
1241 ctx.aggregatePrimitiveDataSum(uuids, labels, "sum_data");
1242 float sum_val;
1243 ctx.getPrimitiveData(uuids[1], "sum_data", sum_val);
1244 DOCTEST_CHECK(sum_val == doctest::Approx(1.f + 2.f + 3.f));
1245
1246 // aggregatePrimitiveDataProduct
1247 ctx.aggregatePrimitiveDataProduct(uuids, labels, "prod_data");
1248 float prod_val;
1249 ctx.getPrimitiveData(uuids[2], "prod_data", prod_val);
1250 DOCTEST_CHECK(prod_val == doctest::Approx(2.f * 4.f * 6.f));
1251
1252 // filterPrimitivesByData
1253 std::vector<uint> filtered = ctx.filterPrimitivesByData(uuids, "filter_me", 1, ">=");
1254 DOCTEST_CHECK(filtered.size() == 2);
1255 filtered = ctx.filterPrimitivesByData(uuids, "filter_me", 1, "==");
1256 DOCTEST_CHECK(filtered.size() == 1);
1257 DOCTEST_CHECK(filtered[0] == uuids[1]);
1258 capture_cerr cerr_buffer;
1259 DOCTEST_CHECK_THROWS_AS(filtered = ctx.filterPrimitivesByData(uuids, "filter_me", 1, "!!"), std::runtime_error);
1260}
1261
1262TEST_CASE("Object data") {
1263 Context ctx;
1264 uint o = ctx.addTileObject(nullorigin, make_vec2(1, 1), nullrotation, make_int2(2, 2));
1265 ctx.setObjectData(o, "test_int", 5);
1266 ctx.setObjectData(o, "test_float", 3.14f);
1267
1268 // getObjectDataType
1269 DOCTEST_CHECK(ctx.getObjectDataType("test_int") == HELIOS_TYPE_INT);
1270#ifdef HELIOS_DEBUG
1271 capture_cerr cerr_buffer;
1272 DOCTEST_CHECK_THROWS_AS(ctx.getObjectDataType("non_existent"), std::runtime_error);
1273#endif
1274
1275 // getObjectDataSize
1276 DOCTEST_CHECK(ctx.getObjectDataSize(o, "test_int") == 1);
1277
1278 // clearObjectData
1279 ctx.clearObjectData(o, "test_int");
1280 DOCTEST_CHECK(!ctx.doesObjectDataExist(o, "test_int"));
1281
1282 // listObjectData
1283 std::vector<std::string> data_labels = ctx.listObjectData(o);
1284 DOCTEST_CHECK(std::find(data_labels.begin(), data_labels.end(), "test_float") != data_labels.end());
1285}
1286
1287TEST_CASE("Context object data management") {
1288 Context ctx;
1289 uint o1 = ctx.addTileObject(nullorigin, make_vec2(1, 1), nullrotation, make_int2(2, 2));
1290 uint o2 = ctx.addTileObject(nullorigin, make_vec2(1, 1), nullrotation, make_int2(2, 2));
1291 ctx.setObjectData(o1, "my_data", 10);
1292
1293 // copyObjectData
1294 ctx.copyObjectData(o1, o2);
1295 DOCTEST_CHECK(ctx.doesObjectDataExist(o2, "my_data"));
1296
1297 // renameObjectData
1298 ctx.renameObjectData(o1, "my_data", "new_name");
1299 DOCTEST_CHECK(!ctx.doesObjectDataExist(o1, "my_data"));
1300 DOCTEST_CHECK(ctx.doesObjectDataExist(o1, "new_name"));
1301
1302 // duplicateObjectData
1303 ctx.duplicateObjectData(o2, "my_data", "my_data_copy");
1304 DOCTEST_CHECK(ctx.doesObjectDataExist(o2, "my_data_copy"));
1305
1306 std::vector<std::string> all_obj_labels = ctx.listAllObjectDataLabels();
1307 DOCTEST_CHECK(std::find(all_obj_labels.begin(), all_obj_labels.end(), "my_data") != all_obj_labels.end());
1308 DOCTEST_CHECK(std::find(all_obj_labels.begin(), all_obj_labels.end(), "my_data_copy") != all_obj_labels.end());
1309 DOCTEST_CHECK(std::find(all_obj_labels.begin(), all_obj_labels.end(), "new_name") != all_obj_labels.end());
1310}
1311
1312TEST_CASE("Global data") {
1313 Context ctx;
1314 ctx.setGlobalData("g_int", 5);
1315 ctx.setGlobalData("g_float", 3.14f);
1316
1317 // getGlobalDataType/Size/Exists
1318 DOCTEST_CHECK(ctx.doesGlobalDataExist("g_int"));
1319 DOCTEST_CHECK(ctx.getGlobalDataType("g_int") == HELIOS_TYPE_INT);
1320 DOCTEST_CHECK(ctx.getGlobalDataSize("g_int") == 1);
1321
1322 // rename/duplicate/clear
1323 ctx.duplicateGlobalData("g_int", "g_int_copy");
1324 DOCTEST_CHECK(ctx.doesGlobalDataExist("g_int_copy"));
1325 ctx.renameGlobalData("g_int", "g_int_new");
1326 DOCTEST_CHECK(!ctx.doesGlobalDataExist("g_int"));
1327 DOCTEST_CHECK(ctx.doesGlobalDataExist("g_int_new"));
1328 ctx.clearGlobalData("g_int_new");
1329 DOCTEST_CHECK(!ctx.doesGlobalDataExist("g_int_new"));
1330
1331 // listGlobalData
1332 std::vector<std::string> g_labels = ctx.listGlobalData();
1333 DOCTEST_CHECK(g_labels.size() > 0);
1334
1335 // incrementGlobalData
1336 ctx.setGlobalData("inc_me", 10);
1337 ctx.incrementGlobalData("inc_me", 5);
1338 int val;
1339 ctx.getGlobalData("inc_me", val);
1340 DOCTEST_CHECK(val == 15);
1341 bool has_warning;
1342 {
1343 capture_cerr cerr_buffer;
1344 ctx.incrementGlobalData("g_float", 1); // Wrong type
1345 has_warning = cerr_buffer.has_output();
1346 }
1347 DOCTEST_CHECK(has_warning);
1348}
1349
1350TEST_CASE("Voxel Management") {
1351 SUBCASE("addVoxel and voxel properties") {
1352 Context ctx;
1353
1354 vec3 center = make_vec3(1, 2, 3);
1355 vec3 size = make_vec3(2, 4, 6);
1356 float rotation = 0.5f * PI_F;
1357
1358 uint vox1 = ctx.addVoxel(center, size);
1359 DOCTEST_CHECK(ctx.getPrimitiveType(vox1) == PRIMITIVE_TYPE_VOXEL);
1360 DOCTEST_CHECK(ctx.getVoxelCenter(vox1) == center);
1361 DOCTEST_CHECK(ctx.getVoxelSize(vox1) == size);
1362
1363 uint vox2 = ctx.addVoxel(center, size, rotation);
1364 DOCTEST_CHECK(ctx.getVoxelCenter(vox2) == center);
1365 DOCTEST_CHECK(ctx.getVoxelSize(vox2) == size);
1366
1367 uint vox3 = ctx.addVoxel(center, size, rotation, RGB::red);
1368 DOCTEST_CHECK(ctx.getPrimitiveColor(vox3) == RGB::red);
1369
1370 uint vox4 = ctx.addVoxel(center, size, rotation, RGBA::red);
1371 RGBAcolor color_rgba = ctx.getPrimitiveColorRGBA(vox4);
1372 DOCTEST_CHECK(color_rgba.r == RGBA::red.r);
1373 DOCTEST_CHECK(color_rgba.a == RGBA::red.a);
1374
1375 DOCTEST_CHECK(ctx.getPrimitiveCount() >= 4);
1376
1377 float area = ctx.getPrimitiveArea(vox1);
1378 DOCTEST_CHECK(area == doctest::Approx(2.f * (size.x * size.y + size.y * size.z + size.x * size.z)));
1379 }
1380}
1381
1382TEST_CASE("Texture Management") {
1383 SUBCASE("texture validation and properties") {
1384 capture_cerr cerr_buffer; // Capture deprecation warnings from setPrimitiveTextureFile
1385 Context ctx;
1386
1387 uint patch = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, "lib/images/solid.jpg");
1388 ctx.setPrimitiveTextureFile(patch, "lib/images/solid.jpg");
1389 DOCTEST_CHECK(ctx.getPrimitiveTextureFile(patch) == "lib/images/solid.jpg");
1390 DOCTEST_CHECK(!ctx.primitiveTextureHasTransparencyChannel(patch));
1391
1392 Texture tex("lib/images/solid.jpg");
1393 std::vector<vec2> uv = {{0, 0}, {1, 0}, {1, 1}};
1394 float solid_frac = tex.getSolidFraction(uv);
1395 DOCTEST_CHECK(solid_frac == doctest::Approx(1.f));
1396 }
1397}
1398
1399TEST_CASE("Triangle Management") {
1400 SUBCASE("setTriangleVertices") {
1401 Context ctx;
1402 vec3 v0 = make_vec3(0, 0, 0);
1403 vec3 v1 = make_vec3(1, 0, 0);
1404 vec3 v2 = make_vec3(0, 1, 0);
1405 uint tri = ctx.addTriangle(v0, v1, v2);
1406
1407 vec3 new_v0 = make_vec3(1, 1, 1);
1408 vec3 new_v1 = make_vec3(2, 1, 1);
1409 vec3 new_v2 = make_vec3(1, 2, 1);
1410 ctx.setTriangleVertices(tri, new_v0, new_v1, new_v2);
1411
1412 std::vector<vec3> vertices = ctx.getPrimitiveVertices(tri);
1413 DOCTEST_CHECK(vertices[0] == new_v0);
1414 DOCTEST_CHECK(vertices[1] == new_v1);
1415 DOCTEST_CHECK(vertices[2] == new_v2);
1416 }
1417}
1418
1419TEST_CASE("UUID and Object Management") {
1420 SUBCASE("getAllUUIDs and cleanDeletedUUIDs") {
1421 Context ctx;
1422 uint p1 = ctx.addPatch();
1423 uint p2 = ctx.addPatch();
1424 uint p3 = ctx.addPatch();
1425
1426 std::vector<uint> all_uuids = ctx.getAllUUIDs();
1427 DOCTEST_CHECK(all_uuids.size() == 3);
1428 DOCTEST_CHECK(std::find(all_uuids.begin(), all_uuids.end(), p1) != all_uuids.end());
1429
1430 ctx.deletePrimitive(p2);
1431 std::vector<uint> uuids_with_deleted = {p1, p2, p3};
1432 ctx.cleanDeletedUUIDs(uuids_with_deleted);
1433 DOCTEST_CHECK(uuids_with_deleted.size() == 2);
1434 DOCTEST_CHECK(std::find(uuids_with_deleted.begin(), uuids_with_deleted.end(), p2) == uuids_with_deleted.end());
1435
1436 std::vector<std::vector<uint>> nested_uuids = {{p1, p2}, {p3, p2}};
1437 ctx.cleanDeletedUUIDs(nested_uuids);
1438 DOCTEST_CHECK(nested_uuids[0].size() == 1);
1439 DOCTEST_CHECK(nested_uuids[1].size() == 1);
1440
1441 std::vector<std::vector<std::vector<uint>>> triple_nested = {{{p1, p2, p3}}};
1442 ctx.cleanDeletedUUIDs(triple_nested);
1443 DOCTEST_CHECK(triple_nested[0][0].size() == 2);
1444 }
1445
1446 SUBCASE("object management utilities") {
1447 Context ctx;
1448 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1449
1450 DOCTEST_CHECK(ctx.areObjectPrimitivesComplete(obj));
1451
1452 std::vector<uint> obj_ids = {obj, 999};
1453 ctx.cleanDeletedObjectIDs(obj_ids);
1454 DOCTEST_CHECK(obj_ids.size() == 1);
1455 DOCTEST_CHECK(obj_ids[0] == obj);
1456
1457 std::vector<std::vector<uint>> nested_obj_ids = {{obj, 999}, {obj}};
1458 ctx.cleanDeletedObjectIDs(nested_obj_ids);
1459 DOCTEST_CHECK(nested_obj_ids[0].size() == 1);
1460 DOCTEST_CHECK(nested_obj_ids[1].size() == 1);
1461
1462 std::vector<std::vector<std::vector<uint>>> triple_nested_obj = {{{obj, 999}}};
1463 ctx.cleanDeletedObjectIDs(triple_nested_obj);
1464 DOCTEST_CHECK(triple_nested_obj[0][0].size() == 1);
1465
1466 DOCTEST_CHECK(ctx.doesObjectExist(obj));
1467
1468 vec3 new_origin = make_vec3(5, 5, 5);
1469 ctx.setObjectOrigin(obj, new_origin);
1470
1471 vec3 new_normal = make_vec3(0, 1, 0);
1472 ctx.setObjectAverageNormal(obj, make_vec3(0, 0, 0), new_normal);
1473 }
1474}
1475
1476TEST_CASE("Tile Object Advanced Features") {
1477 SUBCASE("tile object subdivision management") {
1478 Context ctx;
1479 uint tile = ctx.addTileObject(make_vec3(0, 0, 0), make_vec2(4, 4), nullrotation, make_int2(2, 2));
1480
1481 float area_ratio = ctx.getTileObjectAreaRatio(tile);
1482 DOCTEST_CHECK(area_ratio > 0.f);
1483
1484 ctx.setTileObjectSubdivisionCount({tile}, make_int2(4, 4));
1485
1486 ctx.setTileObjectSubdivisionCount({tile}, 0.5f);
1487 }
1488}
1489
1490TEST_CASE("Pseudocolor Visualization") {
1491 SUBCASE("colorPrimitiveByDataPseudocolor") {
1492 Context ctx;
1493 std::vector<uint> patches;
1494 for (int i = 0; i < 5; i++) {
1495 uint p = ctx.addPatch();
1496 ctx.setPrimitiveData(p, "value", float(i));
1497 patches.push_back(p);
1498 }
1499
1500 DOCTEST_CHECK_NOTHROW(ctx.colorPrimitiveByDataPseudocolor(patches, "value", "hot", 10));
1501 DOCTEST_CHECK_NOTHROW(ctx.colorPrimitiveByDataPseudocolor(patches, "value", "rainbow", 5, 0.f, 4.f));
1502 }
1503}
1504
1505TEST_CASE("Date and Time Extensions") {
1506 SUBCASE("getMonthString") {
1507 Context ctx;
1508 ctx.setDate(15, 1, 2025);
1509 DOCTEST_CHECK(strcmp(ctx.getMonthString(), "JAN") == 0);
1510 ctx.setDate(15, 2, 2025);
1511 DOCTEST_CHECK(strcmp(ctx.getMonthString(), "FEB") == 0);
1512 ctx.setDate(15, 12, 2025);
1513 DOCTEST_CHECK(strcmp(ctx.getMonthString(), "DEC") == 0);
1514 }
1515}
1516
1517TEST_CASE("Tube Object Management") {
1518 SUBCASE("appendTubeSegment with texture") {
1519 Context ctx;
1520 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1)};
1521 std::vector<float> radii = {0.2f, 0.1f};
1522 uint tube = ctx.addTubeObject(10, nodes, radii);
1523
1524 ctx.appendTubeSegment(tube, make_vec3(0, 0, 2), 0.05f, "lib/images/solid.jpg", make_vec2(0.5f, 1.0f));
1525 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 3);
1526 }
1527
1528 SUBCASE("setTubeNodes preserves color-to-segment mapping") {
1529 // Regression test: updateTriangleVertices() must use the same loop order
1530 // as addTubeObject() to maintain correct UUID-to-vertex mapping.
1531 Context ctx;
1532 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2), make_vec3(0, 0, 3)};
1533 std::vector<float> radii = {0.5f, 0.5f, 0.5f, 0.5f};
1534 std::vector<RGBcolor> colors = {RGB::red, RGB::yellow, RGB::green, RGB::blue};
1535
1536 int subdiv = 8;
1537 uint tube = ctx.addTubeObject(subdiv, nodes, radii, colors);
1538
1539 // Translate nodes slightly — this calls updateTriangleVertices()
1540 std::vector<vec3> new_nodes = nodes;
1541 for (auto &n : new_nodes) n.x += 0.1f;
1542 ctx.setTubeNodes(tube, new_nodes);
1543
1544 // Verify: each triangle's vertices should be near the segment matching its color.
1545 // Segment i spans from new_nodes[i].z to new_nodes[i+1].z.
1546 // A triangle with color.at(i) should have all vertices with z in [nodes[i].z, nodes[i+1].z].
1547 std::vector<uint> uuids = ctx.getObjectPrimitiveUUIDs(tube);
1548 int mismatches = 0;
1549 for (uint uuid : uuids) {
1550 RGBcolor c = ctx.getPrimitiveColorRGB(uuid);
1551
1552 // Determine which segment this triangle belongs to based on its color
1553 int expected_segment = -1;
1554 if (c.r == RGB::red.r && c.g == RGB::red.g && c.b == RGB::red.b) expected_segment = 0;
1555 else if (c.r == RGB::yellow.r && c.g == RGB::yellow.g && c.b == RGB::yellow.b) expected_segment = 1;
1556 else if (c.r == RGB::green.r && c.g == RGB::green.g && c.b == RGB::green.b) expected_segment = 2;
1557
1558 DOCTEST_REQUIRE(expected_segment >= 0);
1559
1560 float z_min = new_nodes[expected_segment].z;
1561 float z_max = new_nodes[expected_segment + 1].z;
1562
1563 // Check all 3 vertices are within the segment's z-range (with tolerance for radial offset)
1564 for (uint v = 0; v < 3; v++) {
1565 vec3 vert = ctx.getTriangleVertex(uuid, v);
1566 if (vert.z < z_min - 0.01f || vert.z > z_max + 0.01f) {
1567 mismatches++;
1568 break;
1569 }
1570 }
1571 }
1572 DOCTEST_CHECK_MESSAGE(mismatches == 0, "Found " << mismatches << " triangles with vertices outside their color's segment range after setTubeNodes()");
1573 }
1574
1575 SUBCASE("pruneTubeNodes deletes correct primitives") {
1576 Context ctx;
1577 // 4 nodes → 3 segments. Prune at index 3 → keep 2 segments (nodes 0,1,2).
1578 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2), make_vec3(0, 0, 3)};
1579 std::vector<float> radii = {0.5f, 0.5f, 0.5f, 0.5f};
1580 std::vector<RGBcolor> colors = {RGB::red, RGB::yellow, RGB::green, RGB::blue};
1581
1582 int subdiv = 8;
1583 uint tube = ctx.addTubeObject(subdiv, nodes, radii, colors);
1584
1585 uint uuids_before = ctx.getObjectPrimitiveUUIDs(tube).size();
1586 // 3 segments * 8 subdivisions * 2 triangles/subdivision = 48
1587 DOCTEST_CHECK(uuids_before == 48);
1588
1589 ctx.pruneTubeNodes(tube, 3);
1590
1591 // Object should still exist with 3 nodes (2 segments)
1592 DOCTEST_CHECK(ctx.doesObjectExist(tube));
1593 DOCTEST_CHECK(ctx.getTubeObjectNodeCount(tube) == 3);
1594
1595 // 2 segments * 8 subdivisions * 2 triangles = 32
1596 std::vector<uint> remaining = ctx.getObjectPrimitiveUUIDs(tube);
1597 DOCTEST_CHECK(remaining.size() == 32);
1598
1599 // All remaining triangles should have colors from segments 0 or 1 (red or yellow)
1600 int bad_colors = 0;
1601 for (uint uuid : remaining) {
1602 RGBcolor c = ctx.getPrimitiveColorRGB(uuid);
1603 bool is_red = (c.r == RGB::red.r && c.g == RGB::red.g && c.b == RGB::red.b);
1604 bool is_yellow = (c.r == RGB::yellow.r && c.g == RGB::yellow.g && c.b == RGB::yellow.b);
1605 if (!is_red && !is_yellow) bad_colors++;
1606 }
1607 DOCTEST_CHECK(bad_colors == 0);
1608
1609 // Pruning to 1 node should delete the object entirely
1610 ctx.pruneTubeNodes(tube, 1);
1611 DOCTEST_CHECK_FALSE(ctx.doesObjectExist(tube));
1612 }
1613
1614 SUBCASE("setTubeNodes maintains circular cross-sections after bending") {
1615 Context ctx;
1616 // Create a straight vertical tube
1617 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
1618 std::vector<float> radii = {0.5f, 0.5f, 0.5f};
1619 int subdiv = 16;
1620 uint tube = ctx.addTubeObject(subdiv, nodes, radii);
1621
1622 // Bend the tube 90 degrees at the tip
1623 std::vector<vec3> bent_nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(1, 0, 1)};
1624 ctx.setTubeNodes(tube, bent_nodes);
1625
1626 // Verify cross-sections are circular and perpendicular to the local axis:
1627 // For each triangle vertex, find the nearest node, check that the radial vector
1628 // (vertex - node) is perpendicular to the local axis and has the correct magnitude.
1629 std::vector<uint> uuids = ctx.getObjectPrimitiveUUIDs(tube);
1630
1631 // Compute axial directions at each node
1632 std::vector<vec3> axial(3);
1633 axial[0] = (bent_nodes[1] - bent_nodes[0]);
1634 axial[0].normalize();
1635 axial[1] = 0.5f * ((bent_nodes[1] - bent_nodes[0]) + (bent_nodes[2] - bent_nodes[1]));
1636 axial[1].normalize();
1637 axial[2] = (bent_nodes[2] - bent_nodes[1]);
1638 axial[2].normalize();
1639
1640 int perp_failures = 0;
1641 int radius_failures = 0;
1642
1643 for (uint uuid : uuids) {
1644 for (uint v = 0; v < 3; v++) {
1645 vec3 vert = ctx.getTriangleVertex(uuid, v);
1646
1647 // Find nearest node
1648 int nearest = 0;
1649 float min_dist = (vert - bent_nodes[0]).magnitude();
1650 for (int n = 1; n < 3; n++) {
1651 float d = (vert - bent_nodes[n]).magnitude();
1652 if (d < min_dist) {
1653 min_dist = d;
1654 nearest = n;
1655 }
1656 }
1657
1658 vec3 radial = vert - bent_nodes[nearest];
1659 float dot = fabs(radial * axial[nearest]);
1660 if (dot > 0.02f) {
1661 perp_failures++;
1662 }
1663 if (fabs(radial.magnitude() - 0.5f) > 0.02f) {
1664 radius_failures++;
1665 }
1666 }
1667 }
1668
1669 DOCTEST_CHECK_MESSAGE(perp_failures == 0, "Found " << perp_failures << " vertices not perpendicular to local tube axis after bending");
1670 DOCTEST_CHECK_MESSAGE(radius_failures == 0, "Found " << radius_failures << " vertices with incorrect radius after bending");
1671 }
1672}
1673
1674TEST_CASE("Edge Cases and Additional Coverage") {
1675 SUBCASE("Julian date edge cases") {
1676 Context ctx;
1677 ctx.setDate(1, 1, 2025);
1678 DOCTEST_CHECK(ctx.getJulianDate() == 1);
1679
1680 ctx.setDate(31, 12, 2025);
1681 DOCTEST_CHECK(ctx.getJulianDate() == 365);
1682
1683 ctx.setDate(100, 2025);
1684 Date d = ctx.getDate();
1685 DOCTEST_CHECK(d.day == 10);
1686 DOCTEST_CHECK(d.month == 4);
1687 }
1688
1689 SUBCASE("time edge cases") {
1690 Context ctx;
1691 ctx.setTime(0, 0, 0);
1692 Time t = ctx.getTime();
1693 DOCTEST_CHECK(t.hour == 0);
1694 DOCTEST_CHECK(t.minute == 0);
1695 DOCTEST_CHECK(t.second == 0);
1696
1697 ctx.setTime(59, 59, 23);
1698 t = ctx.getTime();
1699 DOCTEST_CHECK(t.hour == 23);
1700 DOCTEST_CHECK(t.minute == 59);
1701 DOCTEST_CHECK(t.second == 59);
1702 }
1703
1704 SUBCASE("random number edge cases") {
1705 Context ctx;
1706 ctx.seedRandomGenerator(0);
1707
1708 float r1 = ctx.randu(5.f, 5.f);
1709 DOCTEST_CHECK(r1 == doctest::Approx(5.f));
1710
1711 int ri = ctx.randu(10, 10);
1712 DOCTEST_CHECK(ri == 10);
1713
1714 float rn = ctx.randn(0.f, 0.f);
1715 DOCTEST_CHECK(rn == doctest::Approx(0.f));
1716 }
1717
1718 SUBCASE("texture edge cases") {
1719 capture_cerr cerr_buffer; // Capture deprecation warnings from overridePrimitiveTextureColor/usePrimitiveTextureColor
1720 Context ctx;
1721 uint patch = ctx.addPatch();
1722
1723 ctx.overridePrimitiveTextureColor(patch);
1724 ctx.usePrimitiveTextureColor(patch);
1725
1726 std::vector<uint> patches = {patch};
1727 ctx.overridePrimitiveTextureColor(patches);
1728 ctx.usePrimitiveTextureColor(patches);
1729
1730 DOCTEST_CHECK(!ctx.isPrimitiveTextureColorOverridden(patch));
1731 }
1732
1733 SUBCASE("primitive existence checks") {
1734 Context ctx;
1735 uint p1 = ctx.addPatch();
1736 uint p2 = ctx.addPatch();
1737
1738 DOCTEST_CHECK(ctx.doesPrimitiveExist(p1));
1739 DOCTEST_CHECK(ctx.doesPrimitiveExist({p1, p2}));
1740
1741 ctx.deletePrimitive(p1);
1742 DOCTEST_CHECK(!ctx.doesPrimitiveExist(p1));
1743 DOCTEST_CHECK(!ctx.doesPrimitiveExist({p1, p2}));
1744 DOCTEST_CHECK(ctx.doesPrimitiveExist(std::vector<uint>{p2}));
1745 }
1746
1747 SUBCASE("object containment checks") {
1748 Context ctx;
1749 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1750 std::vector<uint> prims = ctx.getObjectPrimitiveUUIDs(obj);
1751
1752 DOCTEST_CHECK(ctx.doesObjectContainPrimitive(obj, prims[0]));
1753
1754 uint independent_patch = ctx.addPatch();
1755 DOCTEST_CHECK(!ctx.doesObjectContainPrimitive(obj, independent_patch));
1756 }
1757
1758 SUBCASE("transformation matrix operations") {
1759 Context ctx;
1760 uint p = ctx.addPatch();
1761
1762 float identity[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
1763 ctx.setPrimitiveTransformationMatrix(p, identity);
1764
1765 std::vector<uint> patches = {p};
1766 ctx.setPrimitiveTransformationMatrix(patches, identity);
1767
1768 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1769 ctx.setObjectTransformationMatrix(obj, identity);
1770
1771 std::vector<uint> objs = {obj};
1772 ctx.setObjectTransformationMatrix(objs, identity);
1773
1774 float retrieved[16];
1775 ctx.getObjectTransformationMatrix(obj, retrieved);
1776 for (int i = 0; i < 16; i++) {
1777 DOCTEST_CHECK(retrieved[i] == doctest::Approx(identity[i]));
1778 }
1779 }
1780
1781 SUBCASE("object type and texture checks") {
1782 Context ctx;
1783 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1784
1785 DOCTEST_CHECK(!ctx.objectHasTexture(obj));
1786
1787 uint textured_obj = ctx.addTileObject(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, make_int2(2, 2), "lib/images/solid.jpg");
1788 DOCTEST_CHECK(ctx.objectHasTexture(textured_obj));
1789 }
1790
1791 SUBCASE("tube object segment operations") {
1792 Context ctx;
1793 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
1794 std::vector<float> radii = {0.2f, 0.15f, 0.1f};
1795 uint tube = ctx.addTubeObject(10, nodes, radii);
1796
1797 float seg_volume = ctx.getTubeObjectSegmentVolume(tube, 0);
1798 DOCTEST_CHECK(seg_volume > 0.f);
1799
1800 seg_volume = ctx.getTubeObjectSegmentVolume(tube, 1);
1801 DOCTEST_CHECK(seg_volume > 0.f);
1802 }
1803
1804 SUBCASE("cone object advanced properties") {
1805 Context ctx;
1806 uint cone = ctx.addConeObject(10, make_vec3(0, 0, 0), make_vec3(0, 0, 2), 1.f, 0.5f);
1807
1808 float radius0 = ctx.getConeObjectNodeRadius(cone, 0);
1809 DOCTEST_CHECK(radius0 == doctest::Approx(1.f));
1810
1811 float radius1 = ctx.getConeObjectNodeRadius(cone, 1);
1812 DOCTEST_CHECK(radius1 == doctest::Approx(0.5f));
1813
1814 float length = ctx.getConeObjectLength(cone);
1815 DOCTEST_CHECK(length == doctest::Approx(2.f));
1816
1817 DOCTEST_CHECK(ctx.getConeObjectSubdivisionCount(cone) == 10);
1818 }
1819
1820 SUBCASE("primitive color operations") {
1821 capture_cerr cerr_buffer; // Suppress deprecation warnings from setPrimitiveColor
1822 Context ctx;
1823 uint p = ctx.addPatch();
1824
1825 ctx.setPrimitiveColor(p, RGB::blue);
1826 DOCTEST_CHECK(ctx.getPrimitiveColor(p) == RGB::blue);
1827
1828 ctx.setPrimitiveColor(p, RGBA::green);
1829 RGBAcolor rgba = ctx.getPrimitiveColorRGBA(p);
1830 DOCTEST_CHECK(rgba.r == RGBA::green.r);
1831 DOCTEST_CHECK(rgba.a == RGBA::green.a);
1832
1833 std::vector<uint> patches = {p};
1834 ctx.setPrimitiveColor(patches, RGB::red);
1835 DOCTEST_CHECK(ctx.getPrimitiveColor(p) == RGB::red);
1836
1837 ctx.setPrimitiveColor(patches, RGBA::yellow);
1838 rgba = ctx.getPrimitiveColorRGBA(p);
1839 DOCTEST_CHECK(rgba.r == RGBA::yellow.r);
1840 }
1841
1842 SUBCASE("object color operations") {
1843 capture_cerr cerr_buffer; // Suppress deprecation warnings from setObjectColor (calls setPrimitiveColor internally)
1844 Context ctx;
1845 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1846
1847 ctx.setObjectColor(obj, RGB::cyan);
1848 ctx.setObjectColor(obj, RGBA::magenta);
1849
1850 std::vector<uint> objs = {obj};
1851 ctx.setObjectColor(objs, RGB::white);
1852 ctx.setObjectColor(objs, RGBA::black);
1853 }
1854}
1855
1856TEST_CASE("Print and Information Functions") {
1857 SUBCASE("printPrimitiveInfo and printObjectInfo") {
1858 Context ctx;
1859 uint patch = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
1860 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1861
1862 // Capture stdout output from these functions
1863 bool has_output;
1864 std::string output;
1865 {
1866 capture_cout cout_buffer;
1867 DOCTEST_CHECK_NOTHROW(ctx.printPrimitiveInfo(patch));
1868 DOCTEST_CHECK_NOTHROW(ctx.printObjectInfo(obj));
1869 has_output = cout_buffer.has_output();
1870 output = cout_buffer.get_captured_output();
1871 } // cout_buffer destroyed here
1872
1873 // Verify that output was captured (functions should produce output)
1874 DOCTEST_CHECK(has_output);
1875 DOCTEST_CHECK(output.find("Info for UUID") != std::string::npos);
1876 DOCTEST_CHECK(output.find("Info for ObjID") != std::string::npos);
1877 }
1878}
1879
1880TEST_CASE("Object Pointer Access") {
1881 SUBCASE("getObjectPointer functions") {
1882 Context ctx;
1883
1884 uint box = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
1885 DOCTEST_CHECK(ctx.doesObjectExist(box));
1886
1887 uint disk = ctx.addDiskObject(10, make_vec3(0, 0, 0), make_vec2(1, 1));
1888 DOCTEST_CHECK(ctx.doesObjectExist(disk));
1889
1890 uint sphere = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
1891 DOCTEST_CHECK(ctx.doesObjectExist(sphere));
1892
1893 std::vector<vec3> nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1)};
1894 std::vector<float> radii = {0.2f, 0.1f};
1895 uint tube = ctx.addTubeObject(10, nodes, radii);
1896 DOCTEST_CHECK(ctx.doesObjectExist(tube));
1897
1898 uint cone = ctx.addConeObject(10, make_vec3(0, 0, 0), make_vec3(0, 0, 1), 0.5f, 0.3f);
1899 DOCTEST_CHECK(ctx.doesObjectExist(cone));
1900
1901 std::vector<uint> prim_uuids = {ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0))};
1902 uint polymesh = ctx.addPolymeshObject(prim_uuids);
1903 DOCTEST_CHECK(ctx.doesObjectExist(polymesh));
1904 }
1905}
1906
1907TEST_CASE("Advanced Primitive Operations") {
1908 SUBCASE("primitive visibility and print operations") {
1909 Context ctx;
1910 uint p1 = ctx.addPatch();
1911 uint p2 = ctx.addPatch();
1912
1913 // Test hiding/showing primitives
1914 ctx.hidePrimitive(p1);
1915 DOCTEST_CHECK(ctx.isPrimitiveHidden(p1));
1916 ctx.showPrimitive(p1);
1917 DOCTEST_CHECK(!ctx.isPrimitiveHidden(p1));
1918
1919 std::vector<uint> patches = {p1, p2};
1920 ctx.hidePrimitive(patches);
1921 DOCTEST_CHECK(ctx.isPrimitiveHidden(p1));
1922 DOCTEST_CHECK(ctx.isPrimitiveHidden(p2));
1923 ctx.showPrimitive(patches);
1924 DOCTEST_CHECK(!ctx.isPrimitiveHidden(p1));
1925 DOCTEST_CHECK(!ctx.isPrimitiveHidden(p2));
1926 }
1927
1928 SUBCASE("primitive counts by type") {
1929 Context ctx;
1930 uint initial_patch_count = ctx.getPatchCount();
1931 uint initial_triangle_count = ctx.getTriangleCount();
1932
1933 uint p1 = ctx.addPatch();
1934 uint p2 = ctx.addPatch();
1935 uint tri = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0));
1936
1937 DOCTEST_CHECK(ctx.getPatchCount() == initial_patch_count + 2);
1938 DOCTEST_CHECK(ctx.getTriangleCount() == initial_triangle_count + 1);
1939
1940 // Test with hidden primitives
1941 ctx.hidePrimitive(p1);
1942 DOCTEST_CHECK(ctx.getPatchCount(false) == initial_patch_count + 1); // exclude hidden
1943 DOCTEST_CHECK(ctx.getPatchCount(true) == initial_patch_count + 2); // include hidden
1944 }
1945}
1946
1947TEST_CASE("Data Type and Size Functions") {
1948 SUBCASE("primitive data type operations") {
1949 Context ctx;
1950 uint p = ctx.addPatch();
1951
1952 ctx.setPrimitiveData(p, "test_int", 42);
1953 ctx.setPrimitiveData(p, "test_float", 3.14f);
1954 ctx.setPrimitiveData(p, "test_vec3", make_vec3(1, 2, 3));
1955
1956 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_int") == HELIOS_TYPE_INT);
1957 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_float") == HELIOS_TYPE_FLOAT);
1958 DOCTEST_CHECK(ctx.getPrimitiveDataType("test_vec3") == HELIOS_TYPE_VEC3);
1959
1960 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_int") == 1);
1961 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_vec3") == 1);
1962
1963 std::vector<float> vec_data = {1.0f, 2.0f, 3.0f};
1964 ctx.setPrimitiveData(p, "test_vector", vec_data);
1965 DOCTEST_CHECK(ctx.getPrimitiveDataSize(p, "test_vector") == 3);
1966 }
1967}
1968
1969TEST_CASE("Additional Missing Coverage") {
1970 SUBCASE("getDirtyUUIDs function") {
1971 Context ctx;
1972 uint p1 = ctx.addPatch();
1973 uint p2 = ctx.addPatch();
1974
1975 ctx.markGeometryClean();
1976 std::vector<uint> dirty_uuids = ctx.getDirtyUUIDs();
1977 DOCTEST_CHECK(dirty_uuids.empty());
1978
1979 ctx.markPrimitiveDirty(p1);
1980 dirty_uuids = ctx.getDirtyUUIDs();
1981 DOCTEST_CHECK(dirty_uuids.size() == 1);
1982 DOCTEST_CHECK(std::find(dirty_uuids.begin(), dirty_uuids.end(), p1) != dirty_uuids.end());
1983 }
1984}
1985
1986TEST_CASE("Advanced Object Operations") {
1987 SUBCASE("object primitive count and area calculations") {
1988 Context ctx;
1989 uint obj = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(2, 3, 4), make_int3(1, 1, 1));
1990
1991 DOCTEST_CHECK(ctx.getObjectPrimitiveCount(obj) == 6); // 6 faces of a box
1992
1993 float area = ctx.getObjectArea(obj);
1994 float expected_area = 2 * (2 * 3 + 3 * 4 + 2 * 4); // surface area of box
1995 DOCTEST_CHECK(area == doctest::Approx(expected_area).epsilon(0.01));
1996 }
1997
1998 SUBCASE("object bounding box operations") {
1999 Context ctx;
2000 uint obj = ctx.addBoxObject(make_vec3(1, 2, 3), make_vec3(2, 4, 6), make_int3(1, 1, 1));
2001
2002 vec3 min_corner, max_corner;
2003 ctx.getObjectBoundingBox(obj, min_corner, max_corner);
2004
2005 DOCTEST_CHECK(min_corner.x == doctest::Approx(0.f).epsilon(0.01));
2006 DOCTEST_CHECK(max_corner.x == doctest::Approx(2.f).epsilon(0.01));
2007 DOCTEST_CHECK(min_corner.y == doctest::Approx(0.f).epsilon(0.01));
2008 DOCTEST_CHECK(max_corner.y == doctest::Approx(4.f).epsilon(0.01));
2009
2010 std::vector<uint> objs = {obj};
2011 ctx.getObjectBoundingBox(objs, min_corner, max_corner);
2012 DOCTEST_CHECK(min_corner.x == doctest::Approx(0.f).epsilon(0.01));
2013 DOCTEST_CHECK(max_corner.x == doctest::Approx(2.f).epsilon(0.01));
2014 }
2015}
2016
2017TEST_CASE("Additional Object Features") {
2018 SUBCASE("getAllObjectIDs") {
2019 Context ctx;
2020 uint obj1 = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
2021 uint obj2 = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
2022
2023 std::vector<uint> all_ids = ctx.getAllObjectIDs();
2024 DOCTEST_CHECK(all_ids.size() >= 2);
2025 DOCTEST_CHECK(std::find(all_ids.begin(), all_ids.end(), obj1) != all_ids.end());
2026 DOCTEST_CHECK(std::find(all_ids.begin(), all_ids.end(), obj2) != all_ids.end());
2027 }
2028
2029 SUBCASE("object type checks") {
2030 Context ctx;
2031 uint box = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
2032 uint sphere = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
2033 uint disk = ctx.addDiskObject(10, make_vec3(0, 0, 0), make_vec2(1, 1));
2034
2035 DOCTEST_CHECK(ctx.getObjectType(box) == OBJECT_TYPE_BOX);
2036 DOCTEST_CHECK(ctx.getObjectType(sphere) == OBJECT_TYPE_SPHERE);
2037 DOCTEST_CHECK(ctx.getObjectType(disk) == OBJECT_TYPE_DISK);
2038 }
2039}
2040
2041TEST_CASE("Comprehensive Object Property Tests") {
2042 SUBCASE("rotation operations on objects") {
2043 Context ctx;
2044 std::vector<uint> objs;
2045 objs.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
2046 objs.push_back(ctx.addBoxObject(make_vec3(1, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
2047
2048 DOCTEST_CHECK_NOTHROW(ctx.rotateObject(objs, 0.5f * PI_F, "z"));
2049 DOCTEST_CHECK_NOTHROW(ctx.rotateObject(objs, 0.5f * PI_F, make_vec3(0, 0, 1)));
2050 DOCTEST_CHECK_NOTHROW(ctx.rotateObject(objs, 0.5f * PI_F, make_vec3(0, 0, 0), make_vec3(0, 0, 1)));
2051 DOCTEST_CHECK_NOTHROW(ctx.rotateObjectAboutOrigin(objs, 0.5f * PI_F, make_vec3(0, 0, 1)));
2052 }
2053
2054 SUBCASE("scaling operations on objects") {
2055 Context ctx;
2056 std::vector<uint> objs;
2057 objs.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
2058
2059 DOCTEST_CHECK_NOTHROW(ctx.scaleObject(objs, make_vec3(2, 2, 2)));
2060 DOCTEST_CHECK_NOTHROW(ctx.scaleObjectAboutCenter(objs, make_vec3(0.5f, 0.5f, 0.5f)));
2061 DOCTEST_CHECK_NOTHROW(ctx.scaleObjectAboutPoint(objs, make_vec3(2, 2, 2), make_vec3(0, 0, 0)));
2062 DOCTEST_CHECK_NOTHROW(ctx.scaleObjectAboutOrigin(objs, make_vec3(0.5f, 0.5f, 0.5f)));
2063 }
2064
2065 SUBCASE("translation operations on objects") {
2066 Context ctx;
2067 std::vector<uint> objs;
2068 objs.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
2069
2070 DOCTEST_CHECK_NOTHROW(ctx.translateObject(objs, make_vec3(1, 2, 3)));
2071 }
2072}
2073
2074TEST_CASE("Domain and Bounding Operations") {
2075 SUBCASE("domain bounding sphere") {
2076 Context ctx;
2077 ctx.addPatch(make_vec3(-2, 0, 0), make_vec2(1, 1));
2078 ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1));
2079
2080 vec3 center;
2081 float radius;
2082 ctx.getDomainBoundingSphere(center, radius);
2083 DOCTEST_CHECK(center.x == doctest::Approx(0.f).epsilon(0.1));
2084 DOCTEST_CHECK(radius > 2.f);
2085 }
2086}
2087
2088TEST_CASE("Missing Data and State Functions") {
2089 SUBCASE("listTimeseriesVariables") {
2090 Context ctx;
2091 Date date = make_Date(1, 1, 2025);
2092 Time time = make_Time(0, 0, 12);
2093
2094 ctx.addTimeseriesData("temp", 25.5f, date, time);
2095 ctx.addTimeseriesData("humidity", 60.0f, date, time);
2096
2097 std::vector<std::string> vars = ctx.listTimeseriesVariables();
2098 DOCTEST_CHECK(vars.size() >= 2);
2099 DOCTEST_CHECK(std::find(vars.begin(), vars.end(), "temp") != vars.end());
2100 DOCTEST_CHECK(std::find(vars.begin(), vars.end(), "humidity") != vars.end());
2101 }
2102
2103 SUBCASE("clearTimeseriesData") {
2104 Context ctx;
2105 Date date = make_Date(1, 1, 2025);
2106 Time time = make_Time(0, 0, 12);
2107
2108 // Add some timeseries data
2109 ctx.addTimeseriesData("temp", 25.5f, date, time);
2110 ctx.addTimeseriesData("humidity", 60.0f, date, time);
2111 DOCTEST_CHECK(ctx.listTimeseriesVariables().size() == 2);
2112 DOCTEST_CHECK(ctx.doesTimeseriesVariableExist("temp"));
2113
2114 // Clear and verify
2115 ctx.clearTimeseriesData();
2116 DOCTEST_CHECK(ctx.listTimeseriesVariables().empty());
2117 DOCTEST_CHECK_FALSE(ctx.doesTimeseriesVariableExist("temp"));
2118 DOCTEST_CHECK_FALSE(ctx.doesTimeseriesVariableExist("humidity"));
2119
2120 // Calling on already-empty context should be a no-op
2121 ctx.clearTimeseriesData();
2122 DOCTEST_CHECK(ctx.listTimeseriesVariables().empty());
2123 }
2124
2125 SUBCASE("deleteTimeseriesVariable") {
2126 Context ctx;
2127 Date date = make_Date(1, 1, 2025);
2128 Time time0 = make_Time(0, 0, 12);
2129 Time time1 = make_Time(1, 0, 12);
2130
2131 ctx.addTimeseriesData("temp", 25.5f, date, time0);
2132 ctx.addTimeseriesData("temp", 26.5f, date, time1);
2133 ctx.addTimeseriesData("humidity", 60.0f, date, time0);
2134 DOCTEST_CHECK(ctx.listTimeseriesVariables().size() == 2);
2135
2136 // Delete one variable; the other must remain intact and queryable.
2137 ctx.deleteTimeseriesVariable("temp");
2138 DOCTEST_CHECK_FALSE(ctx.doesTimeseriesVariableExist("temp"));
2139 DOCTEST_CHECK(ctx.doesTimeseriesVariableExist("humidity"));
2140 DOCTEST_CHECK(ctx.getTimeseriesLength("humidity") == 1);
2141 DOCTEST_CHECK(ctx.queryTimeseriesData("humidity", 0) == doctest::Approx(60.0f));
2142
2143 // Re-adding the same label after deletion creates a fresh series.
2144 ctx.addTimeseriesData("temp", 99.9f, date, time0);
2145 DOCTEST_CHECK(ctx.doesTimeseriesVariableExist("temp"));
2146 DOCTEST_CHECK(ctx.getTimeseriesLength("temp") == 1);
2147 DOCTEST_CHECK(ctx.queryTimeseriesData("temp", 0) == doctest::Approx(99.9f));
2148
2149 // Deleting a non-existent variable warns but does not throw.
2150 std::string captured;
2151 {
2152 capture_cerr cerr_buffer;
2153 DOCTEST_CHECK_NOTHROW(ctx.deleteTimeseriesVariable("nonexistent"));
2154 captured = cerr_buffer.get_captured_output();
2155 }
2156 DOCTEST_CHECK(captured.find("WARNING") != std::string::npos);
2157 DOCTEST_CHECK(captured.find("nonexistent") != std::string::npos);
2158 }
2159
2160 SUBCASE("getUniquePrimitiveParentObjectIDs") {
2161 Context ctx;
2162 uint obj1 = ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1));
2163 uint obj2 = ctx.addSphereObject(10, make_vec3(0, 0, 0), 1.f);
2164
2165 std::vector<uint> all_prims = ctx.getAllUUIDs();
2166 std::vector<uint> obj_ids = ctx.getUniquePrimitiveParentObjectIDs(all_prims);
2167 DOCTEST_CHECK(obj_ids.size() >= 2);
2168 DOCTEST_CHECK(std::find(obj_ids.begin(), obj_ids.end(), obj1) != obj_ids.end());
2169 DOCTEST_CHECK(std::find(obj_ids.begin(), obj_ids.end(), obj2) != obj_ids.end());
2170 }
2171}
2172
2173TEST_CASE("Comprehensive Coverage Tests") {
2174 SUBCASE("additional object operations with vectors") {
2175 Context ctx;
2176 std::vector<uint> obj_ids;
2177 obj_ids.push_back(ctx.addBoxObject(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
2178 obj_ids.push_back(ctx.addBoxObject(make_vec3(2, 0, 0), make_vec3(1, 1, 1), make_int3(1, 1, 1)));
2179
2180 std::vector<uint> all_uuids = ctx.getObjectPrimitiveUUIDs(obj_ids);
2181 DOCTEST_CHECK(all_uuids.size() == 12); // 6 faces per box * 2 boxes
2182
2183 std::vector<std::vector<uint>> nested_obj_ids = {{obj_ids[0]}, {obj_ids[1]}};
2184 std::vector<uint> nested_uuids = ctx.getObjectPrimitiveUUIDs(nested_obj_ids);
2185 DOCTEST_CHECK(nested_uuids.size() == 12);
2186
2187 ctx.hideObject(obj_ids);
2188 DOCTEST_CHECK(ctx.isObjectHidden(obj_ids[0]));
2189 DOCTEST_CHECK(ctx.isObjectHidden(obj_ids[1]));
2190
2191 ctx.showObject(obj_ids);
2192 DOCTEST_CHECK(!ctx.isObjectHidden(obj_ids[0]));
2193 DOCTEST_CHECK(!ctx.isObjectHidden(obj_ids[1]));
2194 }
2195
2196 SUBCASE("object texture color overrides") {
2197 Context ctx;
2198 std::vector<uint> obj_ids;
2199 obj_ids.push_back(ctx.addTileObject(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, make_int2(2, 2), "lib/images/solid.jpg"));
2200
2201 ctx.overrideObjectTextureColor(obj_ids);
2202 ctx.useObjectTextureColor(obj_ids);
2203 }
2204}
2205
2206TEST_CASE("getAllUUIDs Cache Performance") {
2207 SUBCASE("Cache invalidation on primitive add/delete") {
2208 Context ctx;
2209
2210 // Initial empty state
2211 std::vector<uint> empty_uuids = ctx.getAllUUIDs();
2212 DOCTEST_CHECK(empty_uuids.empty());
2213
2214 // Add primitives and test cache invalidation
2215 uint p1 = ctx.addPatch();
2216 std::vector<uint> one_uuid = ctx.getAllUUIDs();
2217 DOCTEST_CHECK(one_uuid.size() == 1);
2218 DOCTEST_CHECK(one_uuid[0] == p1);
2219
2220 // Test cache consistency - repeated calls should return same result
2221 std::vector<uint> same_uuid = ctx.getAllUUIDs();
2222 DOCTEST_CHECK(same_uuid.size() == 1);
2223 DOCTEST_CHECK(same_uuid[0] == p1);
2224
2225 // Add more primitives
2226 uint t1 = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0));
2227 uint v1 = ctx.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
2228
2229 std::vector<uint> three_uuids = ctx.getAllUUIDs();
2230 DOCTEST_CHECK(three_uuids.size() == 3);
2231 DOCTEST_CHECK(std::find(three_uuids.begin(), three_uuids.end(), p1) != three_uuids.end());
2232 DOCTEST_CHECK(std::find(three_uuids.begin(), three_uuids.end(), t1) != three_uuids.end());
2233 DOCTEST_CHECK(std::find(three_uuids.begin(), three_uuids.end(), v1) != three_uuids.end());
2234
2235 // Test delete invalidation
2236 ctx.deletePrimitive(t1);
2237 std::vector<uint> two_uuids = ctx.getAllUUIDs();
2238 DOCTEST_CHECK(two_uuids.size() == 2);
2239 DOCTEST_CHECK(std::find(two_uuids.begin(), two_uuids.end(), t1) == two_uuids.end());
2240 DOCTEST_CHECK(std::find(two_uuids.begin(), two_uuids.end(), p1) != two_uuids.end());
2241 DOCTEST_CHECK(std::find(two_uuids.begin(), two_uuids.end(), v1) != two_uuids.end());
2242 }
2243
2244 SUBCASE("Cache invalidation on hide/show primitives") {
2245 Context ctx;
2246 uint p1 = ctx.addPatch();
2247 uint p2 = ctx.addPatch();
2248 uint p3 = ctx.addPatch();
2249
2250 // All visible initially
2251 std::vector<uint> all_visible = ctx.getAllUUIDs();
2252 DOCTEST_CHECK(all_visible.size() == 3);
2253
2254 // Hide one primitive
2255 ctx.hidePrimitive(p2);
2256 std::vector<uint> two_visible = ctx.getAllUUIDs();
2257 DOCTEST_CHECK(two_visible.size() == 2);
2258 DOCTEST_CHECK(std::find(two_visible.begin(), two_visible.end(), p2) == two_visible.end());
2259 DOCTEST_CHECK(std::find(two_visible.begin(), two_visible.end(), p1) != two_visible.end());
2260 DOCTEST_CHECK(std::find(two_visible.begin(), two_visible.end(), p3) != two_visible.end());
2261
2262 // Hide multiple primitives
2263 std::vector<uint> to_hide = {p1, p3};
2264 ctx.hidePrimitive(to_hide);
2265 std::vector<uint> none_visible = ctx.getAllUUIDs();
2266 DOCTEST_CHECK(none_visible.empty());
2267
2268 // Show one primitive back
2269 ctx.showPrimitive(p1);
2270 std::vector<uint> one_visible = ctx.getAllUUIDs();
2271 DOCTEST_CHECK(one_visible.size() == 1);
2272 DOCTEST_CHECK(one_visible[0] == p1);
2273
2274 // Show all primitives back
2275 std::vector<uint> to_show = {p2, p3};
2276 ctx.showPrimitive(to_show);
2277 std::vector<uint> all_back = ctx.getAllUUIDs();
2278 DOCTEST_CHECK(all_back.size() == 3);
2279 }
2280
2281 SUBCASE("Cache invalidation on copy primitives") {
2282 Context ctx;
2283 uint original = ctx.addPatch();
2284
2285 std::vector<uint> before_copy = ctx.getAllUUIDs();
2286 DOCTEST_CHECK(before_copy.size() == 1);
2287
2288 uint copied = ctx.copyPrimitive(original);
2289 std::vector<uint> after_copy = ctx.getAllUUIDs();
2290 DOCTEST_CHECK(after_copy.size() == 2);
2291 DOCTEST_CHECK(std::find(after_copy.begin(), after_copy.end(), original) != after_copy.end());
2292 DOCTEST_CHECK(std::find(after_copy.begin(), after_copy.end(), copied) != after_copy.end());
2293
2294 // Test multiple copy
2295 std::vector<uint> originals = {original, copied};
2296 std::vector<uint> copies = ctx.copyPrimitive(originals);
2297 std::vector<uint> after_multi_copy = ctx.getAllUUIDs();
2298 DOCTEST_CHECK(after_multi_copy.size() == 4);
2299 for (uint copy_id: copies) {
2300 DOCTEST_CHECK(std::find(after_multi_copy.begin(), after_multi_copy.end(), copy_id) != after_multi_copy.end());
2301 }
2302 }
2303
2304 SUBCASE("Cache consistency across mixed operations") {
2305 Context ctx;
2306
2307 // Complex sequence of operations
2308 uint p1 = ctx.addPatch();
2309 uint p2 = ctx.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0));
2310
2311 std::vector<uint> step1 = ctx.getAllUUIDs();
2312 DOCTEST_CHECK(step1.size() == 2);
2313
2314 ctx.hidePrimitive(p1);
2315 std::vector<uint> step2 = ctx.getAllUUIDs();
2316 DOCTEST_CHECK(step2.size() == 1);
2317 DOCTEST_CHECK(step2[0] == p2);
2318
2319 uint p3 = ctx.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
2320 std::vector<uint> step3 = ctx.getAllUUIDs();
2321 DOCTEST_CHECK(step3.size() == 2);
2322
2323 ctx.showPrimitive(p1);
2324 std::vector<uint> step4 = ctx.getAllUUIDs();
2325 DOCTEST_CHECK(step4.size() == 3);
2326
2327 ctx.deletePrimitive(p2);
2328 std::vector<uint> step5 = ctx.getAllUUIDs();
2329 DOCTEST_CHECK(step5.size() == 2);
2330 DOCTEST_CHECK(std::find(step5.begin(), step5.end(), p2) == step5.end());
2331 DOCTEST_CHECK(std::find(step5.begin(), step5.end(), p1) != step5.end());
2332 DOCTEST_CHECK(std::find(step5.begin(), step5.end(), p3) != step5.end());
2333 }
2334}
2335
2336TEST_CASE("Error Handling") {
2337 SUBCASE("Context error handling") {
2338 Context context_test;
2339 uint tri = context_test.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0), RGB::green);
2340 capture_cerr cerr_buffer;
2341 vec3 center;
2342#ifdef HELIOS_DEBUG
2343 DOCTEST_CHECK_THROWS_AS(center = context_test.getPatchCenter(tri), std::runtime_error);
2344#endif
2345
2346 uint vox = context_test.addVoxel(make_vec3(0, 0, 0), make_vec3(1, 1, 1));
2347 std::vector<uint> vlist{vox};
2348 DOCTEST_CHECK_THROWS_AS(context_test.rotatePrimitive(vlist, PI_F / 4.f, "a"), std::runtime_error);
2349 }
2350}
2351
2352TEST_CASE("Zero Area Triangle Detection") {
2353 SUBCASE("addTubeObject with nearly identical vertices should not create zero-area triangles") {
2354 Context ctx;
2355
2356 // Test case based on problematic vertices from plant architecture
2357 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)};
2358
2359 std::vector<float> radii = {0.000500000024f, 0.000500000024f, 0.000500000024f};
2360 std::vector<RGBcolor> colors = {RGB::green, RGB::green, RGB::green};
2361
2362 // Use exact same parameters as failing case: Ndiv_internode_radius = 7
2363 uint tube_obj = ctx.addTubeObject(7, nodes, radii, colors);
2364
2365 // Verify the tube object was created
2366 DOCTEST_CHECK(ctx.doesObjectExist(tube_obj));
2367
2368 // Get all primitives in the tube and check their areas
2369 std::vector<uint> tube_primitives = ctx.getObjectPrimitiveUUIDs(tube_obj);
2370 DOCTEST_CHECK(tube_primitives.size() > 0);
2371
2372 for (uint uuid: tube_primitives) {
2373 float area = ctx.getPrimitiveArea(uuid);
2374 DOCTEST_CHECK(area > 0.0f); // No zero-area triangles
2375 DOCTEST_CHECK(area > 1e-12f); // Area should be reasonably above precision limit
2376 }
2377 }
2378
2379 SUBCASE("addTubeObject with extremely small displacements") {
2380 Context ctx;
2381
2382 // Even more extreme case - displacements on the order of 1e-5
2383 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)};
2384
2385 std::vector<float> radii = {1e-4f, 1e-4f, 1e-4f};
2386
2387 uint tube_obj = ctx.addTubeObject(6, nodes, radii);
2388 DOCTEST_CHECK(ctx.doesObjectExist(tube_obj));
2389
2390 std::vector<uint> tube_primitives = ctx.getObjectPrimitiveUUIDs(tube_obj);
2391 for (uint uuid: tube_primitives) {
2392 float area = ctx.getPrimitiveArea(uuid);
2393 DOCTEST_CHECK(area > 0.0f);
2394 }
2395 }
2396}
2397
2398TEST_CASE("Transparent Texture Zero Area Validation") {
2399 SUBCASE("addSphere with transparent texture should filter zero-area triangles") {
2400 Context ctx;
2401
2402 // Test with diamond texture (has transparency)
2403 std::vector<uint> sphere_uuids = ctx.addSphere(20, make_vec3(0, 0, 0), 1.0f, "lib/images/diamond_texture.png");
2404
2405 // All returned primitives should have positive area
2406 DOCTEST_CHECK(sphere_uuids.size() > 0);
2407 for (uint uuid: sphere_uuids) {
2408 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
2409 float area = ctx.getPrimitiveArea(uuid);
2410 DOCTEST_CHECK(area > 0.0f);
2411 }
2412
2413 // Test with disk texture (more transparency)
2414 std::vector<uint> sphere_disk_uuids = ctx.addSphere(30, make_vec3(2, 0, 0), 1.0f, "lib/images/disk_texture.png");
2415
2416 DOCTEST_CHECK(sphere_disk_uuids.size() > 0);
2417 for (uint uuid: sphere_disk_uuids) {
2418 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
2419 float area = ctx.getPrimitiveArea(uuid);
2420 DOCTEST_CHECK(area > 0.0f);
2421 }
2422
2423 // Verify ALL primitives in each sphere have positive area
2424 int zero_area_count_diamond = 0;
2425 for (uint uuid: sphere_uuids) {
2426 float area = ctx.getPrimitiveArea(uuid);
2427 if (area <= 0.0f) {
2428 zero_area_count_diamond++;
2429 }
2430 }
2431 DOCTEST_CHECK(zero_area_count_diamond == 0);
2432
2433 int zero_area_count_disk = 0;
2434 for (uint uuid: sphere_disk_uuids) {
2435 float area = ctx.getPrimitiveArea(uuid);
2436 if (area <= 0.0f) {
2437 zero_area_count_disk++;
2438 }
2439 }
2440 DOCTEST_CHECK(zero_area_count_disk == 0);
2441
2442 // Compare with solid sphere for reference
2443 std::vector<uint> solid_sphere_uuids = ctx.addSphere(20, make_vec3(4, 0, 0), 1.0f, RGB::green);
2444
2445 int zero_area_count_solid = 0;
2446 for (uint uuid: solid_sphere_uuids) {
2447 float area = ctx.getPrimitiveArea(uuid);
2448 if (area <= 0.0f) {
2449 zero_area_count_solid++;
2450 }
2451 }
2452 DOCTEST_CHECK(zero_area_count_solid == 0);
2453 }
2454
2455 SUBCASE("texture transparency validation preserves object integrity") {
2456 Context ctx;
2457
2458 // Create textured sphere and verify all returned UUIDs are valid
2459 std::vector<uint> sphere_uuids = ctx.addSphere(15, make_vec3(0, 0, 0), 1.0f, "lib/images/diamond_texture.png");
2460
2461 // Check that all returned primitives exist and have positive area
2462 for (uint uuid: sphere_uuids) {
2463 DOCTEST_CHECK(ctx.doesPrimitiveExist(uuid));
2464 DOCTEST_CHECK(ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE);
2465
2466 float area = ctx.getPrimitiveArea(uuid);
2467 DOCTEST_CHECK(area > 0.0f);
2468 DOCTEST_CHECK(area > 1e-10f); // Should be significantly above precision threshold
2469
2470 // Verify solid fraction is reasonable (not exactly 0 or 1)
2471 float solid_fraction = ctx.getPrimitiveSolidFraction(uuid);
2472 DOCTEST_CHECK(solid_fraction > 0.0f);
2473 DOCTEST_CHECK(solid_fraction <= 1.0f);
2474 }
2475
2476 // Comprehensive check: verify no zero-area primitives exist anywhere in context
2477 std::vector<uint> all_uuids = ctx.getAllUUIDs();
2478 int total_zero_area = 0;
2479 int total_negative_area = 0;
2480
2481 for (uint uuid: all_uuids) {
2482 float area = ctx.getPrimitiveArea(uuid);
2483 if (area == 0.0f) {
2484 total_zero_area++;
2485 }
2486 if (area < 0.0f) {
2487 total_negative_area++;
2488 }
2489 }
2490
2491 // No zero or negative area primitives should exist
2492 DOCTEST_CHECK(total_zero_area == 0);
2493 DOCTEST_CHECK(total_negative_area == 0);
2494
2495 // Additional validation: check that all primitives have reasonable solid fractions
2496 for (uint uuid: sphere_uuids) {
2497 float solid_fraction = ctx.getPrimitiveSolidFraction(uuid);
2498 DOCTEST_CHECK(solid_fraction >= 0.0f);
2499 DOCTEST_CHECK(solid_fraction <= 1.0f);
2500
2501 // For textured primitives, effective area should be geometric_area * solid_fraction
2502 if (ctx.getPrimitiveType(uuid) == PRIMITIVE_TYPE_TRIANGLE) {
2503 vec3 v0 = ctx.getTriangleVertex(uuid, 0);
2504 vec3 v1 = ctx.getTriangleVertex(uuid, 1);
2505 vec3 v2 = ctx.getTriangleVertex(uuid, 2);
2506 float geometric_area = calculateTriangleArea(v0, v1, v2);
2507 float effective_area = ctx.getPrimitiveArea(uuid);
2508
2509 // Effective area should be <= geometric area (due to solid fraction)
2510 DOCTEST_CHECK(effective_area <= geometric_area + 1e-6f); // Allow small numerical tolerance
2511 DOCTEST_CHECK(effective_area > 0.0f);
2512 }
2513 }
2514
2515 // Test zero-area validation for other primitive methods (addTube, addDisk, addCone)
2516 DOCTEST_SUBCASE("Test Other Primitive Methods Zero Area Validation") {
2517 Context ctx_other;
2518
2519 // Test addTube with transparent texture
2520 std::vector<vec3> tube_nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
2521 std::vector<float> tube_radii = {0.1f, 0.15f, 0.1f};
2522 std::vector<uint> tube_uuids = ctx_other.addTube(8, tube_nodes, tube_radii, "lib/images/diamond_texture.png");
2523
2524 // All returned UUIDs should have positive area
2525 int tube_positive_area = 0, tube_zero_area = 0;
2526 for (uint uuid: tube_uuids) {
2527 float area = ctx_other.getPrimitiveArea(uuid);
2528 DOCTEST_CHECK(area >= 0.0f);
2529 if (area > 0.0f) {
2530 tube_positive_area++;
2531 } else {
2532 tube_zero_area++;
2533 }
2534 }
2535
2536 DOCTEST_CHECK(tube_positive_area > 0); // Should have some positive area triangles
2537 DOCTEST_CHECK(tube_zero_area == 0); // Should have no zero area triangles
2538
2539 // Test addDisk with transparent texture
2540 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");
2541
2542 // All returned UUIDs should have positive area
2543 int disk_positive_area = 0, disk_zero_area = 0;
2544 for (uint uuid: disk_uuids) {
2545 float area = ctx_other.getPrimitiveArea(uuid);
2546 DOCTEST_CHECK(area >= 0.0f);
2547 if (area > 0.0f) {
2548 disk_positive_area++;
2549 } else {
2550 disk_zero_area++;
2551 }
2552 }
2553
2554 DOCTEST_CHECK(disk_positive_area > 0); // Should have some positive area triangles
2555 DOCTEST_CHECK(disk_zero_area == 0); // Should have no zero area triangles
2556
2557 // Test addCone with transparent texture
2558 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");
2559
2560 // All returned UUIDs should have positive area
2561 int cone_positive_area = 0, cone_zero_area = 0;
2562 for (uint uuid: cone_uuids) {
2563 float area = ctx_other.getPrimitiveArea(uuid);
2564 DOCTEST_CHECK(area >= 0.0f);
2565 if (area > 0.0f) {
2566 cone_positive_area++;
2567 } else {
2568 cone_zero_area++;
2569 }
2570 }
2571
2572 DOCTEST_CHECK(cone_positive_area > 0); // Should have some positive area triangles
2573 DOCTEST_CHECK(cone_zero_area == 0); // Should have no zero area triangles
2574
2575 // Test addTile with transparent texture (should already work, but verify)
2576 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");
2577
2578 // All returned UUIDs should have positive area
2579 int tile_positive_area = 0, tile_zero_area = 0;
2580 for (uint uuid: tile_uuids) {
2581 float area = ctx_other.getPrimitiveArea(uuid);
2582 DOCTEST_CHECK(area >= 0.0f);
2583 if (area > 0.0f) {
2584 tile_positive_area++;
2585 } else {
2586 tile_zero_area++;
2587 }
2588 }
2589
2590 DOCTEST_CHECK(tile_positive_area > 0); // Should have some positive area triangles
2591 DOCTEST_CHECK(tile_zero_area == 0); // Should have no zero area triangles
2592 }
2593
2594 // Test zero-area validation for compound object methods
2595 DOCTEST_SUBCASE("Test Compound Object Methods Zero Area Validation") {
2596 Context ctx_compound;
2597
2598 // Test addSphereObject with transparent texture
2599 uint sphere_obj = ctx_compound.addSphereObject(8, make_vec3(0, 0, 0), 0.5f, "lib/images/diamond_texture.png");
2600 std::vector<uint> sphere_primitives = ctx_compound.getObjectPrimitiveUUIDs(sphere_obj);
2601
2602 // All primitives should have positive area
2603 int sphere_positive_area = 0, sphere_zero_area = 0;
2604 for (uint uuid: sphere_primitives) {
2605 float area = ctx_compound.getPrimitiveArea(uuid);
2606 DOCTEST_CHECK(area >= 0.0f);
2607 if (area > 0.0f) {
2608 sphere_positive_area++;
2609 } else {
2610 sphere_zero_area++;
2611 }
2612 }
2613
2614 DOCTEST_CHECK(sphere_positive_area > 0); // Should have some positive area triangles
2615 DOCTEST_CHECK(sphere_zero_area == 0); // Should have no zero area triangles
2616
2617 // Test addTubeObject with transparent texture
2618 std::vector<vec3> tube_nodes = {make_vec3(0, 0, 0), make_vec3(0, 0, 1), make_vec3(0, 0, 2)};
2619 std::vector<float> tube_radii = {0.1f, 0.15f, 0.1f};
2620 uint tube_obj = ctx_compound.addTubeObject(8, tube_nodes, tube_radii, "lib/images/diamond_texture.png");
2621 std::vector<uint> tube_primitives = ctx_compound.getObjectPrimitiveUUIDs(tube_obj);
2622
2623 // All primitives should have positive area
2624 int tube_positive_area = 0, tube_zero_area = 0;
2625 for (uint uuid: tube_primitives) {
2626 float area = ctx_compound.getPrimitiveArea(uuid);
2627 DOCTEST_CHECK(area >= 0.0f);
2628 if (area > 0.0f) {
2629 tube_positive_area++;
2630 } else {
2631 tube_zero_area++;
2632 }
2633 }
2634
2635 DOCTEST_CHECK(tube_positive_area > 0); // Should have some positive area triangles
2636 DOCTEST_CHECK(tube_zero_area == 0); // Should have no zero area triangles
2637
2638 // Test addDiskObject with transparent texture
2639 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");
2640 std::vector<uint> disk_primitives = ctx_compound.getObjectPrimitiveUUIDs(disk_obj);
2641
2642 // All primitives should have positive area
2643 int disk_positive_area = 0, disk_zero_area = 0;
2644 for (uint uuid: disk_primitives) {
2645 float area = ctx_compound.getPrimitiveArea(uuid);
2646 DOCTEST_CHECK(area >= 0.0f);
2647 if (area > 0.0f) {
2648 disk_positive_area++;
2649 } else {
2650 disk_zero_area++;
2651 }
2652 }
2653
2654 DOCTEST_CHECK(disk_positive_area > 0); // Should have some positive area triangles
2655 DOCTEST_CHECK(disk_zero_area == 0); // Should have no zero area triangles
2656
2657 // Test addConeObject with transparent texture
2658 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");
2659 std::vector<uint> cone_primitives = ctx_compound.getObjectPrimitiveUUIDs(cone_obj);
2660
2661 // All primitives should have positive area
2662 int cone_positive_area = 0, cone_zero_area = 0;
2663 for (uint uuid: cone_primitives) {
2664 float area = ctx_compound.getPrimitiveArea(uuid);
2665 DOCTEST_CHECK(area >= 0.0f);
2666 if (area > 0.0f) {
2667 cone_positive_area++;
2668 } else {
2669 cone_zero_area++;
2670 }
2671 }
2672
2673 DOCTEST_CHECK(cone_positive_area > 0); // Should have some positive area triangles
2674 DOCTEST_CHECK(cone_zero_area == 0); // Should have no zero area triangles
2675 }
2676 }
2677}
2678
2679TEST_CASE("File path resolution priority") {
2680 SUBCASE("resolveFilePath current directory priority") {
2681 // Test that the new file resolution logic checks current directory first,
2682 // then falls back to HELIOS_BUILD directory
2683
2684 // Create a test texture file in the current directory
2685 std::string testFileName = "test_file_resolution.jpg";
2686 std::filesystem::path currentDirFile = std::filesystem::current_path() / testFileName;
2687
2688 // Copy the existing texture for our test
2689 std::filesystem::path sourceTexture = "core/lib/models/texture.jpg";
2690
2691 if (std::filesystem::exists(sourceTexture)) {
2692 // Copy to current directory
2693 std::filesystem::copy_file(sourceTexture, currentDirFile, std::filesystem::copy_options::overwrite_existing);
2694 DOCTEST_CHECK(std::filesystem::exists(currentDirFile));
2695
2696 // Test resolveFilePath function directly
2697 std::filesystem::path resolved = helios::resolveFilePath(testFileName);
2698 DOCTEST_CHECK(resolved == std::filesystem::canonical(currentDirFile));
2699
2700 // Clean up
2701 std::filesystem::remove(currentDirFile);
2702 }
2703 }
2704
2705 SUBCASE("addPatch with texture from current directory") {
2706 Context ctx;
2707
2708 // Create test directory structure in current working directory
2709 std::filesystem::create_directories("test_models");
2710 std::string testTexture = "test_models/test_texture.jpg";
2711 std::filesystem::path testTexturePath = std::filesystem::current_path() / testTexture;
2712
2713 // Copy source texture
2714 std::filesystem::path sourceTexture = "core/lib/models/texture.jpg";
2715
2716 if (std::filesystem::exists(sourceTexture)) {
2717 std::filesystem::copy_file(sourceTexture, testTexturePath, std::filesystem::copy_options::overwrite_existing);
2718
2719 // This should work with the fix - loads from current directory first
2720 // addPatch uses resolveFilePath internally for texture loading
2721 SphericalCoord rotation = make_SphericalCoord(0, 0);
2722 uint patch_id;
2723 DOCTEST_CHECK_NOTHROW({ patch_id = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), rotation, testTexture.c_str()); });
2724 DOCTEST_CHECK(patch_id > 0);
2725
2726 // Verify the texture loaded correctly
2727 bool has_transparency = ctx.primitiveTextureHasTransparencyChannel(patch_id);
2728 DOCTEST_CHECK((has_transparency || !has_transparency)); // Just verify it's a boolean (texture loaded)
2729
2730 // Clean up
2731 std::filesystem::remove(testTexturePath);
2732 std::filesystem::remove("test_models");
2733 }
2734 }
2735
2736 SUBCASE("Material System - Label-Based Creation") {
2737 Context ctx;
2738
2739 // Default material should exist (but not counted in getMaterialCount or listMaterials)
2740 DOCTEST_CHECK(ctx.doesMaterialExist("__default__"));
2741 DOCTEST_CHECK(ctx.getMaterialCount() == 0); // No user-created materials yet
2742
2743 // Create materials with labels
2744 ctx.addMaterial("leaf_material");
2745 DOCTEST_CHECK(ctx.doesMaterialExist("leaf_material"));
2746 DOCTEST_CHECK(ctx.getMaterialCount() == 1);
2747
2748 ctx.addMaterial("bark_material");
2749 DOCTEST_CHECK(ctx.doesMaterialExist("bark_material"));
2750 DOCTEST_CHECK(ctx.getMaterialCount() == 2);
2751
2752 // List materials (only user-created, not default or auto-generated)
2753 std::vector<std::string> labels = ctx.listMaterials();
2754 DOCTEST_CHECK(labels.size() == 2);
2755
2756 // Reserved labels should fail
2757 DOCTEST_CHECK_THROWS(ctx.addMaterial("__reserved"));
2758 }
2759
2760 SUBCASE("Material System - Rename") {
2761 Context ctx;
2762
2763 // Create a triangle with a texture to generate an auto-material
2764 uint UUID = 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));
2765 std::string auto_label = ctx.getPrimitiveMaterialLabel(UUID);
2766 DOCTEST_CHECK(auto_label.substr(0, 7) == "__auto_");
2767 DOCTEST_CHECK(ctx.doesMaterialExist(auto_label));
2768
2769 // Rename the auto-generated material
2770 ctx.renameMaterial(auto_label, "bean_trifoliate_leaf");
2771 DOCTEST_CHECK(ctx.doesMaterialExist("bean_trifoliate_leaf"));
2772 // Auto-label is retained as a lookup alias for deduplication
2773 DOCTEST_CHECK(ctx.doesMaterialExist(auto_label));
2774
2775 // Primitive should now report the new display label
2776 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(UUID) == "bean_trifoliate_leaf");
2777
2778 // Properties should be preserved
2779 std::string tex = ctx.getMaterialTexture("bean_trifoliate_leaf");
2780 DOCTEST_CHECK(tex == "lib/images/disk_texture.png");
2781
2782 // Deduplication: a new triangle with the same texture should reuse the renamed material
2783 uint UUID2 = ctx.addTriangle(make_vec3(2, 0, 0), make_vec3(3, 0, 0), make_vec3(2, 1, 0), "lib/images/disk_texture.png", make_vec2(0, 0), make_vec2(1, 0), make_vec2(0, 1));
2784 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(UUID2) == "bean_trifoliate_leaf");
2785
2786 // Rename a user-created (non-auto) material — old label should NOT be retained
2787 ctx.addMaterial("old_name");
2788 ctx.renameMaterial("old_name", "new_name");
2789 DOCTEST_CHECK(ctx.doesMaterialExist("new_name"));
2790 DOCTEST_CHECK(!ctx.doesMaterialExist("old_name"));
2791
2792 // Error cases
2793 DOCTEST_CHECK_THROWS(ctx.renameMaterial("nonexistent", "new_name2")); // old label doesn't exist
2794 DOCTEST_CHECK_THROWS(ctx.renameMaterial("bean_trifoliate_leaf", "__reserved")); // new label reserved
2795 DOCTEST_CHECK_THROWS(ctx.renameMaterial("bean_trifoliate_leaf", "new_name")); // new label already exists
2796 DOCTEST_CHECK_THROWS(ctx.renameMaterial("bean_trifoliate_leaf", "")); // empty label
2797 }
2798
2799 SUBCASE("Material System - Properties") {
2800 Context ctx;
2801
2802 // Create and set material properties
2803 ctx.addMaterial("test_mat");
2804
2805 RGBAcolor purple = make_RGBAcolor(0.5f, 0, 0.5f, 1);
2806 ctx.setMaterialColor("test_mat", purple);
2807
2808 RGBAcolor color = ctx.getMaterialColor("test_mat");
2809 DOCTEST_CHECK(color.r == doctest::Approx(0.5f).epsilon(0.001));
2810 DOCTEST_CHECK(color.g == doctest::Approx(0.0f).epsilon(0.001));
2811 DOCTEST_CHECK(color.b == doctest::Approx(0.5f).epsilon(0.001));
2812
2813 // Set texture
2814 ctx.setMaterialTexture("test_mat", "lib/images/disk_texture.png");
2815 std::string tex = ctx.getMaterialTexture("test_mat");
2816 DOCTEST_CHECK(tex == "lib/images/disk_texture.png");
2817
2818 // Texture override
2819 ctx.setMaterialTextureColorOverride("test_mat", true);
2820 DOCTEST_CHECK(ctx.isMaterialTextureColorOverridden("test_mat"));
2821
2822 ctx.setMaterialTextureColorOverride("test_mat", false);
2823 DOCTEST_CHECK(!ctx.isMaterialTextureColorOverridden("test_mat"));
2824
2825 // Twosided flag - test default value
2826 DOCTEST_CHECK(ctx.getMaterialTwosidedFlag("test_mat") == 1); // Default is 1 (two-sided)
2827
2828 // Twosided flag - set to 0 (one-sided)
2829 ctx.setMaterialTwosidedFlag("test_mat", 0);
2830 DOCTEST_CHECK(ctx.getMaterialTwosidedFlag("test_mat") == 0);
2831
2832 // Twosided flag - set back to 1 (two-sided)
2833 ctx.setMaterialTwosidedFlag("test_mat", 1);
2834 DOCTEST_CHECK(ctx.getMaterialTwosidedFlag("test_mat") == 1);
2835 }
2836
2837 SUBCASE("Material System - Assignment to Primitives") {
2838 Context ctx;
2839
2840 // Create material
2841 ctx.addMaterial("red_mat");
2842 ctx.setMaterialColor("red_mat", make_RGBAcolor(1, 0, 0, 1));
2843
2844 // Create primitives with default color
2845 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2846 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2847
2848 // Assign material
2849 ctx.assignMaterialToPrimitive(p1, "red_mat");
2850 ctx.assignMaterialToPrimitive(p2, "red_mat");
2851
2852 // Check primitive material label
2853 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(p1) == "red_mat");
2854 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(p2) == "red_mat");
2855
2856 // Check primitive color reflects material
2857 RGBcolor c1 = ctx.getPrimitiveColor(p1);
2858 DOCTEST_CHECK(c1.r == doctest::Approx(1.0f).epsilon(0.001));
2859 DOCTEST_CHECK(c1.g == doctest::Approx(0.0f).epsilon(0.001));
2860
2861 // Modify material - should affect both primitives
2862 ctx.setMaterialColor("red_mat", make_RGBAcolor(0, 1, 0, 1)); // Green
2863
2864 c1 = ctx.getPrimitiveColor(p1);
2865 RGBcolor c2 = ctx.getPrimitiveColor(p2);
2866 DOCTEST_CHECK(c1.g == doctest::Approx(1.0f).epsilon(0.001));
2867 DOCTEST_CHECK(c2.g == doctest::Approx(1.0f).epsilon(0.001));
2868
2869 // Reverse lookup
2870 std::vector<uint> users = ctx.getPrimitivesUsingMaterial("red_mat");
2871 DOCTEST_CHECK(users.size() == 2);
2872 }
2873
2874 SUBCASE("Material System - Batch Assignment") {
2875 Context ctx;
2876
2877 ctx.addMaterial("batch_mat");
2878 ctx.setMaterialColor("batch_mat", make_RGBAcolor(0.5f, 0.5f, 0.5f, 1));
2879
2880 std::vector<uint> UUIDs;
2881 for (int i = 0; i < 10; i++) {
2882 UUIDs.push_back(ctx.addPatch(make_vec3(i, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0)));
2883 }
2884
2885 // Batch assign
2886 ctx.assignMaterialToPrimitive(UUIDs, "batch_mat");
2887
2888 // Verify all have the material
2889 for (uint uuid: UUIDs) {
2890 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(uuid) == "batch_mat");
2891 }
2892 }
2893
2894 SUBCASE("Material System - Deletion") {
2895 Context ctx;
2896
2897 ctx.addMaterial("temp_mat");
2898 ctx.setMaterialColor("temp_mat", make_RGBAcolor(1, 0, 0, 1));
2899
2900 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2901 ctx.assignMaterialToPrimitive(p1, "temp_mat");
2902
2903 // Delete material - primitive should revert to default
2904 capture_cerr c; // Capture warning about material in use
2905 ctx.deleteMaterial("temp_mat");
2906
2907 DOCTEST_CHECK(!ctx.doesMaterialExist("temp_mat"));
2908 DOCTEST_CHECK(ctx.getPrimitiveMaterialLabel(p1) == "__default__");
2909 }
2910
2911 SUBCASE("Material System - XML Round-Trip") {
2912 Context ctx;
2913
2914 // Create materials
2915 ctx.addMaterial("red_mat");
2916 ctx.setMaterialColor("red_mat", make_RGBAcolor(1, 0, 0, 1));
2917
2918 ctx.addMaterial("textured_mat");
2919 ctx.setMaterialColor("textured_mat", make_RGBAcolor(0, 1, 0, 1));
2920 ctx.setMaterialTexture("textured_mat", "lib/images/disk_texture.png");
2921
2922 // Create a material with non-default twosided_flag
2923 ctx.addMaterial("onesided_mat");
2924 ctx.setMaterialColor("onesided_mat", make_RGBAcolor(0, 0, 1, 1));
2925 ctx.setMaterialTwosidedFlag("onesided_mat", 0); // One-sided
2926
2927 // Create and assign primitives
2928 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2929 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2930 uint p3 = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2931 uint p4 = ctx.addPatch(make_vec3(3, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
2932
2933 ctx.assignMaterialToPrimitive(p1, "red_mat");
2934 ctx.assignMaterialToPrimitive(p2, "textured_mat");
2935 ctx.assignMaterialToPrimitive(p3, "red_mat");
2936 ctx.assignMaterialToPrimitive(p4, "onesided_mat");
2937
2938 // Write to XML
2939 ctx.writeXML("test_materials.xml", {p1, p2, p3, p4}, true);
2940
2941 // Load into new context
2942 Context ctx2;
2943 std::vector<uint> loaded_UUIDs = ctx2.loadXML("test_materials.xml", true);
2944
2945 DOCTEST_CHECK(loaded_UUIDs.size() == 4);
2946
2947 // Verify materials were preserved
2948 DOCTEST_CHECK(ctx2.doesMaterialExist("red_mat"));
2949 DOCTEST_CHECK(ctx2.doesMaterialExist("textured_mat"));
2950 DOCTEST_CHECK(ctx2.doesMaterialExist("onesided_mat"));
2951
2952 RGBcolor loaded_color1 = ctx2.getPrimitiveColor(loaded_UUIDs[0]);
2953 DOCTEST_CHECK(loaded_color1.r == doctest::Approx(1.0f).epsilon(0.001));
2954
2955 DOCTEST_CHECK(ctx2.getPrimitiveTextureFile(loaded_UUIDs[1]) == "lib/images/disk_texture.png");
2956
2957 // Verify twosided_flag was preserved
2958 DOCTEST_CHECK(ctx2.getMaterialTwosidedFlag("red_mat") == 1); // Default
2959 DOCTEST_CHECK(ctx2.getMaterialTwosidedFlag("textured_mat") == 1); // Default
2960 DOCTEST_CHECK(ctx2.getMaterialTwosidedFlag("onesided_mat") == 0); // Non-default
2961
2962 // Clean up
2963 std::filesystem::remove("test_materials.xml");
2964 }
2965
2966 SUBCASE("getPrimitiveTwosidedFlag helper function") {
2967 Context ctx;
2968
2969 // Create materials with different twosided_flag values
2970 ctx.addMaterial("onesided_mat");
2971 ctx.setMaterialTwosidedFlag("onesided_mat", 0);
2972
2973 ctx.addMaterial("twosided_mat");
2974 ctx.setMaterialTwosidedFlag("twosided_mat", 1);
2975
2976 ctx.addMaterial("transparent_mat");
2977 ctx.setMaterialTwosidedFlag("transparent_mat", 2);
2978
2979 // Create primitives
2980 uint UUID_mat_onesided = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
2981 uint UUID_mat_twosided = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1));
2982 uint UUID_mat_transparent = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1));
2983 uint UUID_prim_data = ctx.addPatch(make_vec3(3, 0, 0), make_vec2(1, 1));
2984 uint UUID_default = ctx.addPatch(make_vec3(4, 0, 0), make_vec2(1, 1));
2985
2986 // Assign materials
2987 ctx.assignMaterialToPrimitive(UUID_mat_onesided, "onesided_mat");
2988 ctx.assignMaterialToPrimitive(UUID_mat_twosided, "twosided_mat");
2989 ctx.assignMaterialToPrimitive(UUID_mat_transparent, "transparent_mat");
2990
2991 // Set primitive data on one primitive (no user material assigned)
2992 ctx.setPrimitiveData(UUID_prim_data, "twosided_flag", uint(0));
2993
2994 // Test: Material takes precedence - one-sided material
2995 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_onesided) == 0);
2996
2997 // Test: Material takes precedence - two-sided material
2998 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_twosided) == 1);
2999
3000 // Test: Material supports values > 1 (transparent)
3001 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_transparent) == 2);
3002
3003 // Test: Primitive data fallback (no user material)
3004 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_prim_data) == 0);
3005
3006 // Test: Default value when no material or primitive data
3007 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_default) == 1);
3008
3009 // Test: Custom default value
3010 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_default, 2) == 2);
3011
3012 // Test: Material takes precedence over primitive data
3013 // First, set primitive data on a primitive with a material
3014 ctx.setPrimitiveData(UUID_mat_onesided, "twosided_flag", uint(1)); // Try to override with primitive data
3015 DOCTEST_CHECK(ctx.getPrimitiveTwosidedFlag(UUID_mat_onesided) == 0); // Should still return material value (0)
3016 }
3017
3018 SUBCASE("Material Data - Setting and Getting with Labels") {
3019 Context ctx;
3020
3021 // Create a material
3022 ctx.addMaterial("data_mat");
3023
3024 // Test uint data
3025 ctx.setMaterialData("data_mat", "twosided_flag", 1u);
3026 DOCTEST_CHECK(ctx.doesMaterialDataExist("data_mat", "twosided_flag"));
3027 DOCTEST_CHECK(ctx.getMaterialDataType("data_mat", "twosided_flag") == HELIOS_TYPE_UINT);
3028 uint flag_val;
3029 ctx.getMaterialData("data_mat", "twosided_flag", flag_val);
3030 DOCTEST_CHECK(flag_val == 1u);
3031
3032 // Test int data
3033 ctx.setMaterialData("data_mat", "test_int", -42);
3034 int int_val;
3035 ctx.getMaterialData("data_mat", "test_int", int_val);
3036 DOCTEST_CHECK(int_val == -42);
3037
3038 // Test float data
3039 ctx.setMaterialData("data_mat", "test_float", 3.14f);
3040 float float_val;
3041 ctx.getMaterialData("data_mat", "test_float", float_val);
3042 DOCTEST_CHECK(float_val == doctest::Approx(3.14f).epsilon(0.001));
3043
3044 // Test vec3 data
3045 vec3 test_vec = make_vec3(1, 2, 3);
3046 ctx.setMaterialData("data_mat", "test_vec3", test_vec);
3047 vec3 vec_val;
3048 ctx.getMaterialData("data_mat", "test_vec3", vec_val);
3049 DOCTEST_CHECK(vec_val.x == doctest::Approx(1.0f).epsilon(0.001));
3050 DOCTEST_CHECK(vec_val.y == doctest::Approx(2.0f).epsilon(0.001));
3051 DOCTEST_CHECK(vec_val.z == doctest::Approx(3.0f).epsilon(0.001));
3052
3053 // Test string data
3054 ctx.setMaterialData("data_mat", "test_string", std::string("hello"));
3055 std::string str_val;
3056 ctx.getMaterialData("data_mat", "test_string", str_val);
3057 DOCTEST_CHECK(str_val == "hello");
3058
3059 // Test clearing data
3060 ctx.clearMaterialData("data_mat", "test_int");
3061 DOCTEST_CHECK(!ctx.doesMaterialDataExist("data_mat", "test_int"));
3062 }
3063
3064 SUBCASE("Material Data - Fallback Helper Method") {
3065 Context ctx;
3066
3067 // Create material with data
3068 ctx.addMaterial("fallback_mat");
3069 ctx.setMaterialData("fallback_mat", "twosided_flag", 0u);
3070
3071 // Create primitive with this material
3072 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
3073 ctx.assignMaterialToPrimitive(p1, "fallback_mat");
3074
3075 // Test getDataWithMaterialFallback - should get data from material
3076 uint flag_val;
3077 ctx.getDataWithMaterialFallback(p1, "twosided_flag", flag_val);
3078 DOCTEST_CHECK(flag_val == 0u);
3079
3080 // Create another primitive with material but add primitive-specific data
3081 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
3082 ctx.assignMaterialToPrimitive(p2, "fallback_mat");
3083 ctx.setPrimitiveData(p2, "custom_data", 42);
3084
3085 // Test fallback - should get data from primitive since material doesn't have it
3086 int custom_val;
3087 ctx.getDataWithMaterialFallback(p2, "custom_data", custom_val);
3088 DOCTEST_CHECK(custom_val == 42);
3089
3090 // Create third primitive with no special data
3091 uint p3 = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
3092 ctx.assignMaterialToPrimitive(p3, "fallback_mat");
3093
3094 // Test fallback - should throw error for non-existent data
3095 int nonexistent_val;
3096 DOCTEST_CHECK_THROWS(ctx.getDataWithMaterialFallback(p3, "nonexistent", nonexistent_val));
3097 }
3098
3099 SUBCASE("Material Data - XML Round-Trip with Labels") {
3100 Context ctx;
3101
3102 // Create material with data
3103 ctx.addMaterial("data_round_trip_mat");
3104 ctx.setMaterialColor("data_round_trip_mat", make_RGBAcolor(0.5f, 0.25f, 0.75f, 1));
3105 ctx.setMaterialData("data_round_trip_mat", "twosided_flag", 1u);
3106 ctx.setMaterialData("data_round_trip_mat", "reflectance", 0.8f);
3107 ctx.setMaterialData("data_round_trip_mat", "normal", make_vec3(0, 0, 1));
3108
3109 // Create primitives with this material
3110 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
3111 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBcolor(0, 0, 0));
3112 ctx.assignMaterialToPrimitive(p1, "data_round_trip_mat");
3113 ctx.assignMaterialToPrimitive(p2, "data_round_trip_mat");
3114
3115 // Write to XML
3116 ctx.writeXML("test_material_data.xml", true);
3117
3118 // Load into new context
3119 Context ctx2;
3120 ctx2.loadXML("test_material_data.xml", true);
3121
3122 // Verify material and data were preserved
3123 DOCTEST_CHECK(ctx2.doesMaterialExist("data_round_trip_mat"));
3124
3125 DOCTEST_CHECK(ctx2.doesMaterialDataExist("data_round_trip_mat", "twosided_flag"));
3126 uint flag_val;
3127 ctx2.getMaterialData("data_round_trip_mat", "twosided_flag", flag_val);
3128 DOCTEST_CHECK(flag_val == 1u);
3129
3130 DOCTEST_CHECK(ctx2.doesMaterialDataExist("data_round_trip_mat", "reflectance"));
3131 float refl_val;
3132 ctx2.getMaterialData("data_round_trip_mat", "reflectance", refl_val);
3133 DOCTEST_CHECK(refl_val == doctest::Approx(0.8f).epsilon(0.001));
3134
3135 DOCTEST_CHECK(ctx2.doesMaterialDataExist("data_round_trip_mat", "normal"));
3136 vec3 norm_val;
3137 ctx2.getMaterialData("data_round_trip_mat", "normal", norm_val);
3138 DOCTEST_CHECK(norm_val.x == doctest::Approx(0.0f).epsilon(0.001));
3139 DOCTEST_CHECK(norm_val.y == doctest::Approx(0.0f).epsilon(0.001));
3140 DOCTEST_CHECK(norm_val.z == doctest::Approx(1.0f).epsilon(0.001));
3141
3142 // Clean up
3143 std::filesystem::remove("test_material_data.xml");
3144 }
3145
3146 SUBCASE("Material Methods - getPrimitiveMaterialID and getMaterial") {
3147 Context ctx;
3148
3149 // Create materials
3150 ctx.addMaterial("test_mat_1");
3151 ctx.setMaterialColor("test_mat_1", make_RGBAcolor(1, 0, 0, 1));
3152 uint mat1_id = ctx.getMaterialIDFromLabel("test_mat_1");
3153
3154 ctx.addMaterial("test_mat_2");
3155 ctx.setMaterialColor("test_mat_2", make_RGBAcolor(0, 1, 0, 1));
3156 uint mat2_id = ctx.getMaterialIDFromLabel("test_mat_2");
3157
3158 // Create primitives and assign materials
3159 uint p1 = ctx.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
3160 uint p2 = ctx.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1));
3161 uint p3 = ctx.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1));
3162
3163 ctx.assignMaterialToPrimitive(p1, "test_mat_1");
3164 ctx.assignMaterialToPrimitive(p2, "test_mat_2");
3165 ctx.assignMaterialToPrimitive(p3, "test_mat_1");
3166
3167 // Test getPrimitiveMaterialID
3168 DOCTEST_CHECK(ctx.getPrimitiveMaterialID(p1) == mat1_id);
3169 DOCTEST_CHECK(ctx.getPrimitiveMaterialID(p2) == mat2_id);
3170 DOCTEST_CHECK(ctx.getPrimitiveMaterialID(p3) == mat1_id);
3171
3172 // Test getMaterial
3173 const Material &mat1 = ctx.getMaterial(mat1_id);
3174 DOCTEST_CHECK(mat1.label == "test_mat_1");
3175 DOCTEST_CHECK(mat1.color.r == doctest::Approx(1.0f));
3176 DOCTEST_CHECK(mat1.color.g == doctest::Approx(0.0f));
3177 DOCTEST_CHECK(mat1.color.b == doctest::Approx(0.0f));
3178
3179 const Material &mat2 = ctx.getMaterial(mat2_id);
3180 DOCTEST_CHECK(mat2.label == "test_mat_2");
3181 DOCTEST_CHECK(mat2.color.r == doctest::Approx(0.0f));
3182 DOCTEST_CHECK(mat2.color.g == doctest::Approx(1.0f));
3183 DOCTEST_CHECK(mat2.color.b == doctest::Approx(0.0f));
3184
3185 // Test getMaterial with invalid ID throws error
3186 DOCTEST_CHECK_THROWS((void) ctx.getMaterial(99999));
3187 }
3188
3189 SUBCASE("Material Methods - getMaterialIDFromLabel") {
3190 Context ctx;
3191
3192 // Create several materials
3193 ctx.addMaterial("material_a");
3194 ctx.addMaterial("material_b");
3195 ctx.addMaterial("material_c");
3196
3197 // Test getting IDs from labels
3198 uint id_a = ctx.getMaterialIDFromLabel("material_a");
3199 uint id_b = ctx.getMaterialIDFromLabel("material_b");
3200 uint id_c = ctx.getMaterialIDFromLabel("material_c");
3201
3202 // IDs should be unique
3203 DOCTEST_CHECK(id_a != id_b);
3204 DOCTEST_CHECK(id_b != id_c);
3205 DOCTEST_CHECK(id_a != id_c);
3206
3207 // Getting same label should return same ID
3208 DOCTEST_CHECK(ctx.getMaterialIDFromLabel("material_a") == id_a);
3209 DOCTEST_CHECK(ctx.getMaterialIDFromLabel("material_b") == id_b);
3210
3211 // Non-existent label should throw error
3212 DOCTEST_CHECK_THROWS((void) ctx.getMaterialIDFromLabel("nonexistent_material"));
3213 }
3214
3215 SUBCASE("Material copy-on-write - basic color modification") {
3216 Context context;
3217
3218 // Create two primitives with same color (shared material via deduplication)
3219 uint uuid1 = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), RGB::red);
3220 uint uuid2 = context.addPatch(make_vec3(2, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), RGB::red);
3221
3222 // Verify they share material initially
3223 std::string mat1_before = context.getPrimitiveMaterialLabel(uuid1);
3224 std::string mat2_before = context.getPrimitiveMaterialLabel(uuid2);
3225 DOCTEST_CHECK(mat1_before == mat2_before);
3226
3227 // Modify one primitive's color
3228 context.setPrimitiveColor(uuid1, RGB::blue);
3229
3230 // Verify materials are now different (copy-on-write occurred)
3231 std::string mat1_after = context.getPrimitiveMaterialLabel(uuid1);
3232 std::string mat2_after = context.getPrimitiveMaterialLabel(uuid2);
3233 DOCTEST_CHECK(mat1_after != mat2_after);
3234
3235 // Verify colors are independent
3236 RGBcolor color1 = context.getPrimitiveColor(uuid1);
3237 RGBcolor color2 = context.getPrimitiveColor(uuid2);
3238 DOCTEST_CHECK(color1 == RGB::blue);
3239 DOCTEST_CHECK(color2 == RGB::red);
3240 }
3241
3242 SUBCASE("Material copy-on-write - object-level modification") {
3243 Context context;
3244
3245 // Create two sphere objects with same color
3246 uint obj1 = context.addSphereObject(10, make_vec3(0, 0, 0), 1.f, RGB::green);
3247 uint obj2 = context.addSphereObject(10, make_vec3(3, 0, 0), 1.f, RGB::green);
3248
3249 // Modify one object's color
3250 context.setObjectColor(obj1, RGB::yellow);
3251
3252 // Verify objects have different colors
3253 auto prims1 = context.getObjectPrimitiveUUIDs(obj1);
3254 auto prims2 = context.getObjectPrimitiveUUIDs(obj2);
3255
3256 RGBcolor color1 = context.getPrimitiveColor(prims1[0]);
3257 RGBcolor color2 = context.getPrimitiveColor(prims2[0]);
3258
3259 DOCTEST_CHECK(color1 == RGB::yellow);
3260 DOCTEST_CHECK(color2 == RGB::green);
3261 }
3262
3263 SUBCASE("Material copy-on-write - non-shared optimization") {
3264 Context context;
3265
3266 // Create single primitive with explicit color
3267 uint uuid = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), RGB::cyan);
3268
3269 std::string mat1 = context.getPrimitiveMaterialLabel(uuid);
3270
3271 // Modify color - should NOT create new material since it's not shared
3272 context.setPrimitiveColor(uuid, RGB::magenta);
3273
3274 std::string mat2 = context.getPrimitiveMaterialLabel(uuid);
3275
3276 // Material should be same (no copy needed, just modified in place)
3277 DOCTEST_CHECK(mat1 == mat2);
3278 }
3279}
3280
3281TEST_CASE("Context Timeseries File Loading") {
3282
3283 SUBCASE("ISO-8601 UTC datetime") {
3284 Context ctx;
3285 std::string warning_msg;
3286 {
3287 capture_cerr capture;
3288 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_iso8601_utc.csv", {}, ",", "ISO8601", 0);
3289 warning_msg = capture.get_captured_output();
3290 }
3291 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3292
3293 Date date = make_Date(3, 2, 2026);
3294 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(10, 0, 0));
3295 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3296
3297 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(12, 0, 0));
3298 DOCTEST_CHECK(temp2 == doctest::Approx(17.8f));
3299
3300 float humid = ctx.queryTimeseriesData("humidity", date, make_Time(10, 0, 0));
3301 DOCTEST_CHECK(humid == doctest::Approx(0.65f));
3302
3303 // UTC offset should be set to 0 (ISO offset Z → Helios 0)
3304 Location loc = ctx.getLocation();
3305 DOCTEST_CHECK(loc.UTC_offset == doctest::Approx(0.0f));
3306 }
3307
3308 SUBCASE("ISO-8601 with timezone offset") {
3309 Context ctx;
3310 std::string warning_msg;
3311 {
3312 capture_cerr capture;
3313 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_iso8601_offset.csv", {}, ",", "ISO8601", 0);
3314 warning_msg = capture.get_captured_output();
3315 }
3316 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3317
3318 Date date = make_Date(3, 2, 2026);
3319 // The local time is used (02:00, 03:00, 04:00)
3320 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(2, 0, 0));
3321 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3322
3323 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(4, 0, 0));
3324 DOCTEST_CHECK(temp2 == doctest::Approx(17.8f));
3325
3326 // ISO -08:00 → Helios UTC_offset = +8 (West-positive convention)
3327 Location loc = ctx.getLocation();
3328 DOCTEST_CHECK(loc.UTC_offset == doctest::Approx(8.0f));
3329 }
3330
3331 SUBCASE("Compact date (YYYYMMDD no delimiters)") {
3332 Context ctx;
3333 std::string warning_msg;
3334 {
3335 capture_cerr capture;
3336 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_compact_date.csv", {}, ",", "YYYYMMDD", 0);
3337 warning_msg = capture.get_captured_output();
3338 }
3339 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3340
3341 Date date = make_Date(3, 2, 2026);
3342 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(10, 0, 0));
3343 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3344
3345 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(12, 0, 0));
3346 DOCTEST_CHECK(temp2 == doctest::Approx(17.8f));
3347 }
3348
3349 SUBCASE("Compact datetime (YYYYMMDDHH)") {
3350 Context ctx;
3351 std::string warning_msg;
3352 {
3353 capture_cerr capture;
3354 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_compact_datetime.csv", {}, ",", "YYYYMMDDHH", 0);
3355 warning_msg = capture.get_captured_output();
3356 }
3357 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3358
3359 Date date = make_Date(3, 2, 2026);
3360 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(10, 0, 0));
3361 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3362
3363 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(12, 0, 0));
3364 DOCTEST_CHECK(temp2 == doctest::Approx(17.8f));
3365 }
3366
3367 SUBCASE("Time column (HH:MM and HH:MM:SS)") {
3368 Context ctx;
3369 std::string warning_msg;
3370 {
3371 capture_cerr capture;
3372 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_time_column.csv", {}, ",", "YYYYMMDD", 0);
3373 warning_msg = capture.get_captured_output();
3374 }
3375 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3376
3377 Date date = make_Date(3, 2, 2026);
3378 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(10, 30, 0));
3379 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3380
3381 // HH:MM:SS format: 11:15:30
3382 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(11, 15, 30));
3383 DOCTEST_CHECK(temp2 == doctest::Approx(16.2f));
3384
3385 float temp3 = ctx.queryTimeseriesData("temperature", date, make_Time(12, 0, 0));
3386 DOCTEST_CHECK(temp3 == doctest::Approx(17.8f));
3387 }
3388
3389 SUBCASE("Datetime with space separator") {
3390 Context ctx;
3391 std::string warning_msg;
3392 {
3393 capture_cerr capture;
3394 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_datetime_space.csv", {}, " ", "YYYY-MM-DD HH:MM", 0);
3395 warning_msg = capture.get_captured_output();
3396 }
3397 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3398
3399 Date date = make_Date(3, 2, 2026);
3400 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(10, 0, 0));
3401 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3402
3403 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(12, 0, 0));
3404 DOCTEST_CHECK(temp2 == doctest::Approx(17.8f));
3405 }
3406
3407 SUBCASE("European DD/MM/YYYY HH:MM datetime") {
3408 Context ctx;
3409 std::string warning_msg;
3410 {
3411 capture_cerr capture;
3412 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_ddmmyyyy_hhmm.csv", {}, ",", "DD/MM/YYYY HH:MM", 0);
3413 warning_msg = capture.get_captured_output();
3414 }
3415 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3416
3417 Date date = make_Date(3, 2, 2026); // 03/02/2026 = Feb 3rd in DD/MM/YYYY
3418 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(10, 0, 0));
3419 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3420
3421 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(12, 0, 0));
3422 DOCTEST_CHECK(temp2 == doctest::Approx(17.8f));
3423 }
3424
3425 SUBCASE("US MM/DD/YYYY HH:MM datetime") {
3426 Context ctx;
3427 std::string warning_msg;
3428 {
3429 capture_cerr capture;
3430 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_mmddyyyy_hhmm.csv", {}, ",", "MM/DD/YYYY HH:MM", 0);
3431 warning_msg = capture.get_captured_output();
3432 }
3433 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3434
3435 Date date = make_Date(3, 2, 2026); // 02/03/2026 = Feb 3rd in MM/DD/YYYY
3436 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(10, 0, 0));
3437 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3438
3439 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(12, 0, 0));
3440 DOCTEST_CHECK(temp2 == doctest::Approx(17.8f));
3441 }
3442
3443 SUBCASE("Backward compatibility - existing weather_data.csv") {
3444 Context ctx;
3445 // weather_data.csv has: date "1-2-2020" with DDMMYYYY = day 1, month 2 = Feb 1
3446 std::string warning_msg;
3447 {
3448 capture_cerr capture;
3449 ctx.loadTabularTimeseriesData("lib/testdata/weather_data.csv", {}, ",", "DDMMYYYY", 0);
3450 warning_msg = capture.get_captured_output();
3451 }
3452 DOCTEST_CHECK(warning_msg.find("headerlines argument was specified as zero") != std::string::npos);
3453
3454 Date date = make_Date(1, 2, 2020);
3455 float temp = ctx.queryTimeseriesData("temperature", date, make_Time(13, 0, 0));
3456 DOCTEST_CHECK(temp == doctest::Approx(35.32343f));
3457
3458 float temp2 = ctx.queryTimeseriesData("temperature", date, make_Time(14, 0, 0));
3459 DOCTEST_CHECK(temp2 == doctest::Approx(36.23432f));
3460 }
3461
3462 SUBCASE("User-specified column labels") {
3463 Context ctx;
3464 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_iso8601_utc.csv",
3465 {"datetime", "temp", "rh"}, ",", "ISO8601", 1);
3466
3467 Date date = make_Date(3, 2, 2026);
3468 float temp = ctx.queryTimeseriesData("temp", date, make_Time(10, 0, 0));
3469 DOCTEST_CHECK(temp == doctest::Approx(15.5f));
3470
3471 float rh = ctx.queryTimeseriesData("rh", date, make_Time(10, 0, 0));
3472 DOCTEST_CHECK(rh == doctest::Approx(0.65f));
3473 }
3474
3475 SUBCASE("Real data: Open-Meteo Davis CA (ISO-8601 no seconds)") {
3476 // Real Open-Meteo data for Davis, CA. File has 3 metadata header lines + 1 blank line.
3477 // Datetime format is ISO-8601 without seconds: "2024-01-01T00:00"
3478 // Column header says "time" but it's a full datetime — user must remap with labels.
3479 Context ctx;
3480 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_openmeteo_davis.csv",
3481 {"datetime", "temperature", "humidity", "precipitation"},
3482 ",", "ISO8601", 4);
3483
3484 Date jan1 = make_Date(1, 1, 2024);
3485 Date jan2 = make_Date(2, 1, 2024);
3486
3487 // First row: 2024-01-01T00:00, 9.1, 96, 0.00
3488 float temp_midnight = ctx.queryTimeseriesData("temperature", jan1, make_Time(0, 0, 0));
3489 DOCTEST_CHECK(temp_midnight == doctest::Approx(9.1f));
3490
3491 // Row: 2024-01-01T12:00, 14.1, 66, 0.00
3492 float temp_noon = ctx.queryTimeseriesData("temperature", jan1, make_Time(12, 0, 0));
3493 DOCTEST_CHECK(temp_noon == doctest::Approx(14.1f));
3494
3495 float humid_noon = ctx.queryTimeseriesData("humidity", jan1, make_Time(12, 0, 0));
3496 DOCTEST_CHECK(humid_noon == doctest::Approx(66.0f));
3497
3498 // Row: 2024-01-02T18:00, 11.1, 91, 1.20
3499 float precip = ctx.queryTimeseriesData("precipitation", jan2, make_Time(18, 0, 0));
3500 DOCTEST_CHECK(precip == doctest::Approx(1.20f));
3501 }
3502
3503 SUBCASE("Real data: Open-Meteo NYC (ISO-8601 negative temperatures)") {
3504 // Real Open-Meteo data for New York City. Tests negative values and ISO-8601 no-seconds.
3505 Context ctx;
3506 ctx.loadTabularTimeseriesData("lib/testdata/timeseries_openmeteo_nyc.csv",
3507 {"datetime", "temperature", "precipitation"},
3508 ",", "ISO8601", 4);
3509
3510 Date jan1 = make_Date(1, 1, 2024);
3511 Date jan2 = make_Date(2, 1, 2024);
3512 Date jan3 = make_Date(3, 1, 2024);
3513
3514 // Row: 2024-01-01T00:00, 1.8, 0.00
3515 float temp = ctx.queryTimeseriesData("temperature", jan1, make_Time(0, 0, 0));
3516 DOCTEST_CHECK(temp == doctest::Approx(1.8f));
3517
3518 // Row: 2024-01-02T07:00, -4.6, 0.00 (negative temperature)
3519 float temp_cold = ctx.queryTimeseriesData("temperature", jan2, make_Time(7, 0, 0));
3520 DOCTEST_CHECK(temp_cold == doctest::Approx(-4.6f));
3521
3522 // Row: 2024-01-02T00:00, -1.1, 0.00
3523 float temp_neg = ctx.queryTimeseriesData("temperature", jan2, make_Time(0, 0, 0));
3524 DOCTEST_CHECK(temp_neg == doctest::Approx(-1.1f));
3525
3526 // Row: 2024-01-03T13:00, 7.7, 0.00
3527 float temp_warm = ctx.queryTimeseriesData("temperature", jan3, make_Time(13, 0, 0));
3528 DOCTEST_CHECK(temp_warm == doctest::Approx(7.7f));
3529 }
3530}