1.3.77
 
Loading...
Searching...
No Matches
selfTest.cpp
1#include "Visualizer.h"
2
3#define DOCTEST_CONFIG_IMPLEMENT
4#include <filesystem>
5#include "doctest.h"
6#include "doctest_utils.h"
7
8using namespace helios;
9
10TEST_CASE("Visualizer::disableMessages") {
11 Visualizer visualizer(1000, 800, 16, false, true);
12
13 DOCTEST_CHECK_NOTHROW(visualizer.disableMessages());
14
15 capture_cerr cerr_buffer;
16 visualizer.setColorbarRange(20, 10);
17
18 DOCTEST_CHECK(!cerr_buffer.has_output());
19}
20
21TEST_CASE("Visualizer::enableMessages") {
22 Visualizer visualizer(1000, 800, 16, true, true);
23
24 DOCTEST_CHECK_NOTHROW(visualizer.enableMessages());
25
26 capture_cerr cerr_buffer;
27 visualizer.setColorbarRange(20, 10);
28
29 DOCTEST_CHECK(cerr_buffer.has_output());
30}
31
32TEST_CASE("Visualizer::setCameraPosition") {
33 Visualizer visualizer(1000, 800, 16, true, true);
34 helios::vec3 initial_position = make_vec3(1, 1, 1);
35 helios::vec3 initial_lookat = make_vec3(0, 0, 0);
36 visualizer.setCameraPosition(initial_position, initial_lookat);
37
38 // Verify that the transformation matrix updates correctly
39 std::vector<helios::vec3> positions = visualizer.getCameraPosition();
40 DOCTEST_CHECK(positions.size() == 2);
41 DOCTEST_CHECK(positions.at(1) == initial_position);
42 DOCTEST_CHECK(positions.at(0) == initial_lookat);
43}
44
45TEST_CASE("Visualizer::setLightingModel") {
46 Visualizer visualizer(1000, 800, 16, true, true);
47
48 DOCTEST_CHECK_NOTHROW(visualizer.setLightingModel(Visualizer::LIGHTING_NONE));
49 DOCTEST_CHECK_NOTHROW(visualizer.setLightingModel(Visualizer::LIGHTING_PHONG));
50 DOCTEST_CHECK_NOTHROW(visualizer.setLightingModel(Visualizer::LIGHTING_PHONG_SHADOWED));
51}
52
53TEST_CASE("Visualizer::setBackgroundColor and Visualizer::getBackgroundColor") {
54 Visualizer visualizer(1000, 800, 16, true, true);
55 helios::RGBcolor bgcolor = RGB::white;
56 visualizer.setBackgroundColor(bgcolor);
57 DOCTEST_CHECK(visualizer.getBackgroundColor() == bgcolor);
58}
59
60TEST_CASE("Visualizer::setLightIntensityFactor") {
61 Visualizer visualizer(1000, 800, 16, true, true);
62 DOCTEST_CHECK_NOTHROW(visualizer.setLightIntensityFactor(0.75f));
63}
64
65TEST_CASE("Visualizer::enableColorbar and Visualizer::disableColorbar") {
66 Visualizer visualizer(1000, 800, 16, true, true);
67 DOCTEST_CHECK_NOTHROW(visualizer.enableColorbar());
68 DOCTEST_CHECK_NOTHROW(visualizer.disableColorbar());
69}
70
71TEST_CASE("Visualizer::setColorbarPosition") {
72 Visualizer visualizer(1000, 800, 16, true, true);
73 DOCTEST_CHECK_NOTHROW(visualizer.setColorbarPosition(make_vec3(0.5f, 0.5f, 0.f)));
74 capture_cerr cerr_buffer;
75 DOCTEST_CHECK_THROWS_AS(visualizer.setColorbarPosition(make_vec3(-0.1f, 0.f, 0.f)), std::runtime_error);
76}
77
78TEST_CASE("Visualizer::setColorbarSize") {
79 Visualizer visualizer(1000, 800, 16, true, true);
80 DOCTEST_CHECK_NOTHROW(visualizer.setColorbarSize(make_vec2(0.1f, 0.05f)));
81 capture_cerr cerr_buffer;
82 DOCTEST_CHECK_THROWS_AS(visualizer.setColorbarSize(make_vec2(1.5f, 0.f)), std::runtime_error);
83}
84
85TEST_CASE("Visualizer::setColorbarRange") {
86 Visualizer visualizer(1000, 800, 16, true, true);
87 visualizer.enableMessages();
88 visualizer.setColorbarRange(0.f, 1.f);
89 capture_cerr cerr_buffer;
90 DOCTEST_CHECK_NOTHROW(visualizer.setColorbarRange(20.f, 10.f));
91 DOCTEST_CHECK(cerr_buffer.has_output());
92}
93
94TEST_CASE("Visualizer::setColorbarTicks") {
95 Visualizer visualizer(1000, 800, 16, true, true);
96 visualizer.setColorbarRange(0.f, 1.f);
97 std::vector<float> ticks{0.f, 0.5f, 1.f};
98 DOCTEST_CHECK_NOTHROW(visualizer.setColorbarTicks(ticks));
99 capture_cerr cerr_buffer;
100 DOCTEST_CHECK_THROWS_AS(visualizer.setColorbarTicks({}), std::runtime_error);
101 DOCTEST_CHECK_THROWS_AS(visualizer.setColorbarTicks({0.f, 0.5f, 0.4f}), std::runtime_error);
102}
103
104TEST_CASE("Visualizer::generateNiceTicks - Float data") {
105 // Test various ranges for float data
106 std::vector<float> ticks;
107
108 // Test range 0 to 1
109 ticks = Visualizer::generateNiceTicks(0.0f, 1.0f, false, 5);
110 DOCTEST_CHECK(ticks.size() >= 2);
111 DOCTEST_CHECK(ticks.front() <= 0.0f);
112 DOCTEST_CHECK(ticks.back() >= 1.0f);
113 // Should generate nice values like 0.0, 0.25, 0.5, 0.75, 1.0
114 for (size_t i = 1; i < ticks.size(); ++i) {
115 DOCTEST_CHECK(ticks[i] > ticks[i - 1]);
116 }
117
118 // Test range 0 to 100
119 ticks = Visualizer::generateNiceTicks(0.0f, 100.0f, false, 5);
120 DOCTEST_CHECK(ticks.size() >= 2);
121 DOCTEST_CHECK(ticks.front() <= 0.0f);
122 DOCTEST_CHECK(ticks.back() >= 100.0f);
123
124 // Test range 0 to 48.3
125 ticks = Visualizer::generateNiceTicks(0.0f, 48.3f, false, 5);
126 DOCTEST_CHECK(ticks.size() >= 2);
127 DOCTEST_CHECK(ticks.front() <= 0.0f);
128 DOCTEST_CHECK(ticks.back() >= 48.3f);
129 // Should generate ticks like 0, 25, 50 or similar nice numbers
130
131 // Test very small range
132 ticks = Visualizer::generateNiceTicks(0.0f, 0.1f, false, 5);
133 DOCTEST_CHECK(ticks.size() >= 2);
134
135 // Test negative range
136 ticks = Visualizer::generateNiceTicks(-10.0f, 10.0f, false, 5);
137 DOCTEST_CHECK(ticks.size() >= 2);
138 DOCTEST_CHECK(ticks.front() <= -10.0f);
139 DOCTEST_CHECK(ticks.back() >= 10.0f);
140
141 // Test very large range
142 ticks = Visualizer::generateNiceTicks(0.0f, 1e6f, false, 5);
143 DOCTEST_CHECK(ticks.size() >= 2);
144}
145
146TEST_CASE("Visualizer::generateNiceTicks - Integer data") {
147 std::vector<float> ticks;
148
149 // Test range 0 to 20 (integer)
150 ticks = Visualizer::generateNiceTicks(0.0f, 20.0f, true, 5);
151 DOCTEST_CHECK(ticks.size() >= 2);
152 // All ticks should be integers
153 for (float tick: ticks) {
154 DOCTEST_CHECK(std::fabs(tick - std::round(tick)) < 1e-6);
155 }
156
157 // Test range 0 to 7 (integer)
158 ticks = Visualizer::generateNiceTicks(0.0f, 7.0f, true, 5);
159 DOCTEST_CHECK(ticks.size() >= 2);
160 for (float tick: ticks) {
161 DOCTEST_CHECK(std::fabs(tick - std::round(tick)) < 1e-6);
162 }
163
164 // Test range 0 to 100 (integer)
165 ticks = Visualizer::generateNiceTicks(0.0f, 100.0f, true, 5);
166 DOCTEST_CHECK(ticks.size() >= 2);
167 for (float tick: ticks) {
168 DOCTEST_CHECK(std::fabs(tick - std::round(tick)) < 1e-6);
169 }
170}
171
172TEST_CASE("Visualizer::formatTickLabel - Float data") {
173 std::string label;
174
175 // Test formatting with spacing = 0.2 (nice number spacing, should show 1 decimal place)
176 label = Visualizer::formatTickLabel(0.0, 0.2, false);
177 DOCTEST_CHECK(label == "0.0");
178
179 label = Visualizer::formatTickLabel(0.4, 0.2, false);
180 DOCTEST_CHECK(label == "0.4");
181
182 label = Visualizer::formatTickLabel(1.0, 0.2, false);
183 DOCTEST_CHECK(label == "1.0");
184
185 // Test formatting with spacing = 1.0 (should show 0 decimal places)
186 label = Visualizer::formatTickLabel(0.0, 1.0, false);
187 DOCTEST_CHECK(label == "0");
188
189 label = Visualizer::formatTickLabel(10.0, 1.0, false);
190 DOCTEST_CHECK(label == "10");
191
192 // Test formatting with spacing = 0.1
193 label = Visualizer::formatTickLabel(0.5, 0.1, false);
194 DOCTEST_CHECK(label == "0.5");
195
196 // Test very small value (should use scientific notation)
197 label = Visualizer::formatTickLabel(1e-6, 1e-6, false);
198 DOCTEST_CHECK(label.find("e") != std::string::npos); // Should contain 'e' for scientific notation
199
200 // Test large value (should use scientific notation at 10,000+)
201 label = Visualizer::formatTickLabel(15000.0, 1000.0, false);
202 DOCTEST_CHECK(label.find("e") != std::string::npos);
203
204 // Test value below scientific notation threshold
205 label = Visualizer::formatTickLabel(9000.0, 1000.0, false);
206 DOCTEST_CHECK(label.find("e") == std::string::npos); // Should NOT use scientific notation
207}
208
209TEST_CASE("Visualizer::formatTickLabel - Integer data") {
210 std::string label;
211
212 // Test integer formatting
213 label = Visualizer::formatTickLabel(0.0, 1.0, true);
214 DOCTEST_CHECK(label == "0");
215
216 label = Visualizer::formatTickLabel(5.0, 1.0, true);
217 DOCTEST_CHECK(label == "5");
218
219 label = Visualizer::formatTickLabel(100.0, 10.0, true);
220 DOCTEST_CHECK(label == "100");
221
222 // Test rounding for integer data
223 label = Visualizer::formatTickLabel(5.4, 1.0, true);
224 DOCTEST_CHECK(label == "5");
225
226 label = Visualizer::formatTickLabel(5.6, 1.0, true);
227 DOCTEST_CHECK(label == "6");
228
229 // Test large integer values (should use scientific notation at 10,000+)
230 label = Visualizer::formatTickLabel(15000.0, 1000.0, true);
231 DOCTEST_CHECK(label.find("e") != std::string::npos);
232
233 // Test integer value below scientific notation threshold
234 label = Visualizer::formatTickLabel(9000.0, 1000.0, true);
235 DOCTEST_CHECK(label == "9000");
236}
237
238TEST_CASE("Visualizer::niceNumber") {
239 // Test rounding up (round = false)
240 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(0.72, false) - 1.0) < 1e-6);
241 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(1.5, false) - 2.0) < 1e-6);
242 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(3.2, false) - 5.0) < 1e-6);
243 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(7.5, false) - 10.0) < 1e-6);
244
245 // Test rounding to nearest (round = true)
246 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(1.2, true) - 1.0) < 1e-6);
247 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(1.6, true) - 2.0) < 1e-6);
248 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(3.5, true) - 5.0) < 1e-6);
249 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(6.0, true) - 5.0) < 1e-6);
250
251 // Test with different magnitudes
252 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(12.0, true) - 10.0) < 1e-6);
253 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(120.0, true) - 100.0) < 1e-6);
254 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(0.12, true) - 0.1) < 1e-6);
255
256 // Test zero
257 DOCTEST_CHECK(Visualizer::niceNumber(0.0, true) == 0.0);
258 DOCTEST_CHECK(Visualizer::niceNumber(0.0, false) == 0.0);
259
260 // Test negative values (should preserve sign)
261 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(-1.5, true) - (-2.0)) < 1e-6);
262 DOCTEST_CHECK(std::fabs(Visualizer::niceNumber(-3.2, true) - (-5.0)) < 1e-6); // -3.2 rounds to -5.0, not -2.0
263}
264
265TEST_CASE("Visualizer colorbar text attributes") {
266 Visualizer visualizer(1000, 800, 16, true, true);
267 DOCTEST_CHECK_NOTHROW(visualizer.setColorbarTitle("MyBar"));
268 DOCTEST_CHECK_NOTHROW(visualizer.setColorbarFontColor(RGB::yellow));
269 DOCTEST_CHECK_NOTHROW(visualizer.setColorbarFontSize(14));
270 capture_cerr cerr_buffer;
271 DOCTEST_CHECK_THROWS_AS(visualizer.setColorbarFontSize(0), std::runtime_error);
272}
273
274TEST_CASE("Visualizer::setColormap") {
275 Visualizer visualizer(1000, 800, 16, true, true);
276 DOCTEST_CHECK_NOTHROW(visualizer.setColormap(Visualizer::COLORMAP_COOL));
277 capture_cerr cerr_buffer;
278 DOCTEST_CHECK_THROWS_AS(visualizer.setColormap(Visualizer::COLORMAP_CUSTOM), std::runtime_error);
279 DOCTEST_CHECK_THROWS_AS(visualizer.setColormap(std::vector<RGBcolor>{RGB::red}, std::vector<float>{0.f, 1.f}), std::runtime_error);
280}
281
282TEST_CASE("Visualizer::PNG texture integration via primitives") {
283 Visualizer visualizer(1000, 800, 16, true, true);
284 const char *png_filename = "plugins/visualizer/textures/AlmondLeaf.png";
285
286 // Verify file exists before testing
287 DOCTEST_CHECK(std::filesystem::exists(png_filename));
288
289 // Test PNG texture loading through textured rectangle - internally calls read_png_file -> helios::readPNG
290 std::vector<helios::vec3> verts = {make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(1, 1, 0), make_vec3(0, 1, 0)};
291 size_t UUID1;
292 DOCTEST_CHECK_NOTHROW(UUID1 = visualizer.addRectangleByVertices(verts, png_filename, Visualizer::COORDINATES_CARTESIAN));
293 DOCTEST_CHECK(UUID1 != 0);
294
295 // Test PNG texture loading through textured triangle
296 size_t UUID2;
297 DOCTEST_CHECK_NOTHROW(UUID2 = visualizer.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0), png_filename, make_vec2(0, 0), make_vec2(1, 0), make_vec2(0, 1), Visualizer::COORDINATES_CARTESIAN));
298 DOCTEST_CHECK(UUID2 != 0);
299 DOCTEST_CHECK(UUID2 != UUID1); // Should be different primitives
300}
301
302TEST_CASE("Visualizer::JPEG texture integration via primitives") {
303 Visualizer visualizer(1000, 800, 16, true, true);
304 const char *jpeg_filename = "plugins/visualizer/textures/SkyDome_clouds.jpg";
305
306 // Verify file exists before testing
307 DOCTEST_CHECK(std::filesystem::exists(jpeg_filename));
308
309 // Test JPEG texture on rectangle using addRectangleByVertices which accepts texture files
310 std::vector<helios::vec3> verts = {make_vec3(1, 1, 1), make_vec3(3, 1, 1), make_vec3(3, 3, 1), make_vec3(1, 3, 1)};
311 size_t rect_UUID;
312 DOCTEST_CHECK_NOTHROW(rect_UUID = visualizer.addRectangleByVertices(verts, jpeg_filename, Visualizer::COORDINATES_CARTESIAN));
313 DOCTEST_CHECK(rect_UUID != 0);
314}
315
316TEST_CASE("Visualizer::Visualizer") {
317 DOCTEST_CHECK_NOTHROW(Visualizer v1(800, 600, true));
318 DOCTEST_CHECK_NOTHROW(Visualizer v2(1024, 768, 4, false, true));
319 DOCTEST_CHECK_NOTHROW(Visualizer v3(1280, 720, 8, false, true));
320}
321
322TEST_CASE("Visualizer texture copy") {
323 DOCTEST_CHECK(std::filesystem::exists("plugins/visualizer/textures/AlmondLeaf.png"));
324 DOCTEST_CHECK(std::filesystem::exists("plugins/visualizer/textures/Helios_watermark.png"));
325 DOCTEST_CHECK(std::filesystem::exists("plugins/visualizer/textures/SkyDome_clouds.jpg"));
326}
327
328TEST_CASE("Visualizer::addRectangleByCenter") {
329 Visualizer visualizer(1000, 800, 16, true, true);
330 size_t UUID;
331 DOCTEST_CHECK_NOTHROW(UUID = visualizer.addRectangleByCenter(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), RGB::red, Visualizer::COORDINATES_CARTESIAN));
332 DOCTEST_CHECK(UUID != 0);
333}
334
335TEST_CASE("Visualizer::addRectangleByCenter extreme") {
336 Visualizer visualizer(1000, 800, 16, true, true);
337 size_t UUID;
338 DOCTEST_CHECK_NOTHROW(UUID = visualizer.addRectangleByCenter(make_vec3(1e6, 1e6, 1e6), make_vec2(1e6, 1e6), make_SphericalCoord(0, 0), RGB::red, Visualizer::COORDINATES_CARTESIAN));
339 DOCTEST_CHECK(UUID != 0);
340}
341
342TEST_CASE("Visualizer::addRectangleByVertices variations") {
343 Visualizer visualizer(1000, 800, 16, true, true);
344 std::vector<helios::vec3> verts = {make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(1, 1, 0), make_vec3(0, 1, 0)};
345 size_t UUID1;
346 DOCTEST_CHECK_NOTHROW(UUID1 = visualizer.addRectangleByVertices(verts, RGB::blue, Visualizer::COORDINATES_CARTESIAN));
347 DOCTEST_CHECK(UUID1 != 0);
348 size_t UUID2;
349 DOCTEST_CHECK_NOTHROW(UUID2 = visualizer.addRectangleByVertices(verts, "plugins/visualizer/textures/AlmondLeaf.png", Visualizer::COORDINATES_CARTESIAN));
350 DOCTEST_CHECK(UUID2 != 0);
351}
352
353TEST_CASE("Visualizer::addTriangle") {
354 Visualizer visualizer(1000, 800, 16, true, true);
355 size_t UUID;
356 DOCTEST_CHECK_NOTHROW(UUID = visualizer.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0), RGB::blue, Visualizer::COORDINATES_CARTESIAN));
357 DOCTEST_CHECK(UUID != 0);
358}
359
360TEST_CASE("Visualizer::addTriangle textured") {
361 Visualizer visualizer(1000, 800, 16, true, true);
362 size_t UUID;
363 DOCTEST_CHECK_NOTHROW(UUID = visualizer.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0), "plugins/visualizer/textures/AlmondLeaf.png", make_vec2(0, 0), make_vec2(1, 0), make_vec2(0, 1), Visualizer::COORDINATES_CARTESIAN));
364 DOCTEST_CHECK(UUID != 0);
365}
366
367TEST_CASE("Visualizer::addVoxelByCenter") {
368 Visualizer visualizer(1000, 800, 16, true, true);
369 std::vector<size_t> UUIDs;
370 DOCTEST_CHECK_NOTHROW(UUIDs = visualizer.addVoxelByCenter(make_vec3(0, 0, 0), make_vec3(1, 1, 1), make_SphericalCoord(0, 0), RGB::green, Visualizer::COORDINATES_CARTESIAN));
371 DOCTEST_CHECK(UUIDs.size() == 6);
372}
373
374TEST_CASE("Visualizer::addSphereByCenter") {
375 Visualizer visualizer(1000, 800, 16, true, true);
376 uint N = 3;
377 std::vector<size_t> UUIDs;
378 DOCTEST_CHECK_NOTHROW(UUIDs = visualizer.addSphereByCenter(1.0f, make_vec3(0, 0, 0), N, RGB::blue, Visualizer::COORDINATES_CARTESIAN));
379 DOCTEST_CHECK(UUIDs.size() == 2 * N * (N - 1));
380}
381
382TEST_CASE("Visualizer::addCoordinateAxes") {
383 Visualizer visualizer(1000, 800, 16, true, true);
384 DOCTEST_CHECK_NOTHROW(visualizer.addCoordinateAxes(make_vec3(0, 0, 0), make_vec3(1, 1, 1), "XYZ"));
385}
386
387TEST_CASE("Visualizer::addLine") {
388 Visualizer visualizer(1000, 800, 16, true, true);
389 DOCTEST_CHECK(visualizer.addLine(make_vec3(-1, 3, 0), make_vec3(0, 4, 0), RGB::red, Visualizer::COORDINATES_CARTESIAN) != 0);
390}
391
392TEST_CASE("Visualizer::addLine with line width") {
393 Visualizer visualizer(1000, 800, 16, true, true);
394
395 // Test RGB line with custom width
396 size_t UUID1;
397 DOCTEST_CHECK_NOTHROW(UUID1 = visualizer.addLine(make_vec3(0, 0, 0), make_vec3(1, 1, 1), RGB::blue, 2.0f, Visualizer::COORDINATES_CARTESIAN));
398 DOCTEST_CHECK(UUID1 != 0);
399
400 // Test RGBA line with custom width
401 size_t UUID2;
402 DOCTEST_CHECK_NOTHROW(UUID2 = visualizer.addLine(make_vec3(2, 0, 0), make_vec3(3, 1, 1), make_RGBAcolor(1.0f, 0.0f, 0.0f, 0.5f), 5.0f, Visualizer::COORDINATES_CARTESIAN));
403 DOCTEST_CHECK(UUID2 != 0);
404 DOCTEST_CHECK(UUID2 != UUID1);
405
406 // Test with small width (should work without throwing)
407 size_t UUID3;
408 DOCTEST_CHECK_NOTHROW(UUID3 = visualizer.addLine(make_vec3(-1, 0, 0), make_vec3(-2, 1, 1), RGB::green, 0.5f, Visualizer::COORDINATES_CARTESIAN));
409 DOCTEST_CHECK(UUID3 != 0);
410
411 // Test with large width (should work without throwing)
412 size_t UUID4;
413 DOCTEST_CHECK_NOTHROW(UUID4 = visualizer.addLine(make_vec3(4, 0, 0), make_vec3(5, 1, 1), RGB::yellow, 10.0f, Visualizer::COORDINATES_CARTESIAN));
414 DOCTEST_CHECK(UUID4 != 0);
415
416 // Test with maximum valid width (should work)
417 size_t UUID5;
418 DOCTEST_CHECK_NOTHROW(UUID5 = visualizer.addLine(make_vec3(6, 0, 0), make_vec3(7, 1, 1), RGB::white, 100.0f, Visualizer::COORDINATES_CARTESIAN));
419 DOCTEST_CHECK(UUID5 != 0);
420
421 // Test with zero width (should throw error)
422 DOCTEST_CHECK_THROWS_AS(visualizer.addLine(make_vec3(8, 0, 0), make_vec3(9, 1, 1), RGB::red, 0.0f, Visualizer::COORDINATES_CARTESIAN), std::runtime_error);
423
424 // Test with negative width (should throw error)
425 DOCTEST_CHECK_THROWS_AS(visualizer.addLine(make_vec3(10, 0, 0), make_vec3(11, 1, 1), RGB::green, -1.0f, Visualizer::COORDINATES_CARTESIAN), std::runtime_error);
426
427 // Test with width exceeding maximum (should throw error)
428 DOCTEST_CHECK_THROWS_AS(visualizer.addLine(make_vec3(12, 0, 0), make_vec3(13, 1, 1), RGB::blue, 101.0f, Visualizer::COORDINATES_CARTESIAN), std::runtime_error);
429
430 // Test with normalized window coordinates and custom width (user's specific case)
431 size_t UUID6;
432 DOCTEST_CHECK_NOTHROW(UUID6 = visualizer.addLine(make_vec3(0, 0.5, 0), make_vec3(1, 0.5, 0), RGB::red, 20.0f, Visualizer::COORDINATES_WINDOW_NORMALIZED));
433 DOCTEST_CHECK(UUID6 != 0);
434}
435
436TEST_CASE("Visualizer::validateTextureFile") {
437 DOCTEST_CHECK(validateTextureFile("plugins/visualizer/textures/AlmondLeaf.png"));
438 DOCTEST_CHECK(!validateTextureFile("missing.png"));
439 DOCTEST_CHECK(!validateTextureFile("plugins/visualizer/textures/SkyDome_clouds.jpg", true));
440}
441
442TEST_CASE("Visualizer::point culling configuration simple") {
443 Visualizer visualizer(800, 600, true); // Headless mode
444
445 // Test that the new configuration methods exist and don't crash
446 DOCTEST_CHECK_NOTHROW(visualizer.setPointCullingEnabled(true));
447 DOCTEST_CHECK_NOTHROW(visualizer.setPointCullingEnabled(false));
448}
449
450TEST_CASE("Visualizer::addPoint basic functionality") {
451 Visualizer visualizer(800, 600, true); // Headless mode
452
453 // Test adding a single point
454 size_t point_uuid = visualizer.addPoint(make_vec3(0, 0, 0), RGB::red, 1.0f, Visualizer::COORDINATES_CARTESIAN);
455 DOCTEST_CHECK(point_uuid != 0);
456}
457
458TEST_CASE("Visualizer::addPoint with different sizes") {
459 Visualizer visualizer(800, 600, true); // Headless mode
460
461 // Test adding points with different sizes
462 size_t point1 = visualizer.addPoint(make_vec3(0, 0, 0), RGB::red, 1.0f, Visualizer::COORDINATES_CARTESIAN);
463 size_t point2 = visualizer.addPoint(make_vec3(1, 0, 0), RGB::green, 2.5f, Visualizer::COORDINATES_CARTESIAN);
464 size_t point3 = visualizer.addPoint(make_vec3(2, 0, 0), RGB::blue, 5.0f, Visualizer::COORDINATES_CARTESIAN);
465
466 // Test point with size outside supported range (should trigger warning)
467 capture_cerr cerr_buffer;
468 size_t point4 = visualizer.addPoint(make_vec3(3, 0, 0), RGB::yellow, 0.5f, Visualizer::COORDINATES_CARTESIAN);
469 DOCTEST_CHECK(cerr_buffer.has_output()); // Should capture warning about point size clamping
470
471 // Verify unique UUIDs were returned
472 DOCTEST_CHECK(point1 != 0);
473 DOCTEST_CHECK(point2 != 0);
474 DOCTEST_CHECK(point3 != 0);
475 DOCTEST_CHECK(point4 != 0);
476 DOCTEST_CHECK(point1 != point2);
477 DOCTEST_CHECK(point2 != point3);
478 DOCTEST_CHECK(point3 != point4);
479}
480
481TEST_CASE("Visualizer::addPoint RGBA with sizes") {
482 Visualizer visualizer(800, 600, true); // Headless mode
483
484 // Test adding RGBA points with different sizes
485 size_t point1 = visualizer.addPoint(make_vec3(0, 0, 0), make_RGBAcolor(1.0f, 0.0f, 0.0f, 0.8f), 1.5f, Visualizer::COORDINATES_CARTESIAN);
486 size_t point2 = visualizer.addPoint(make_vec3(1, 1, 1), make_RGBAcolor(0.0f, 1.0f, 0.0f, 0.6f), 3.0f, Visualizer::COORDINATES_CARTESIAN);
487
488 DOCTEST_CHECK(point1 != 0);
489 DOCTEST_CHECK(point2 != 0);
490 DOCTEST_CHECK(point1 != point2);
491}
492
493TEST_CASE("Visualizer::point culling metrics functionality") {
494 Visualizer visualizer(800, 600, true); // Headless mode
495
496 // Test that metrics can be retrieved
497 size_t total, rendered;
498 float time;
499 DOCTEST_CHECK_NOTHROW(visualizer.getPointRenderingMetrics(total, rendered, time));
500
501 // Add some points
502 for (int i = 0; i < 5; ++i) {
503 size_t uuid = visualizer.addPoint(make_vec3(i, 0, 0), RGB::orange, 1.0f, Visualizer::COORDINATES_CARTESIAN);
504 DOCTEST_CHECK(uuid != 0);
505 }
506
507 // Test metrics after adding points
508 DOCTEST_CHECK_NOTHROW(visualizer.getPointRenderingMetrics(total, rendered, time));
509
510 // Note: plotUpdate disabled in headless mode for testing - would require full OpenGL context
511}
512
513TEST_CASE("Visualizer::point size edge cases") {
514 Visualizer visualizer(800, 600, true); // Headless mode
515
516 // Test with very small point size (should trigger warning and not crash in headless mode)
517 capture_cerr cerr_buffer1;
518 size_t point1 = visualizer.addPoint(make_vec3(0, 0, 0), RGB::white, 0.001f, Visualizer::COORDINATES_CARTESIAN);
519 DOCTEST_CHECK(point1 != 0);
520 DOCTEST_CHECK(cerr_buffer1.has_output()); // Should capture warning about point size clamping
521
522 // Test with very large point size (should trigger warning and not crash in headless mode)
523 capture_cerr cerr_buffer2;
524 size_t point2 = visualizer.addPoint(make_vec3(1, 0, 0), RGB::white, 1000.0f, Visualizer::COORDINATES_CARTESIAN);
525 DOCTEST_CHECK(point2 != 0);
526 DOCTEST_CHECK(cerr_buffer2.has_output()); // Should capture warning about point size clamping
527
528 // Test with valid point size (should not trigger warning)
529 capture_cerr cerr_buffer3;
530 size_t point3 = visualizer.addPoint(make_vec3(2, 0, 0), RGB::white, 2.0f, Visualizer::COORDINATES_CARTESIAN);
531 DOCTEST_CHECK(point3 != 0);
532 DOCTEST_CHECK(!cerr_buffer3.has_output()); // Should not capture any warning
533
534 // Verify UUIDs are unique
535 DOCTEST_CHECK(point1 != point2);
536 DOCTEST_CHECK(point2 != point3);
537 DOCTEST_CHECK(point1 != point3);
538}
539
540TEST_CASE("CI/Offscreen - Basic OpenGL Context") {
541 // Test that we can create a headless visualizer with offscreen rendering
542 DOCTEST_CHECK_NOTHROW({
543 Visualizer visualizer(400, 300, 4, true, true); // headless=true
544 // If we get here without throwing, the offscreen context was created successfully
545 });
546}
547
548TEST_CASE("CI/Offscreen - Framebuffer Operations") {
549 Visualizer visualizer(200, 150, 0, true, true); // Small size for CI efficiency
550
551 // Test that we can perform basic OpenGL operations
552 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(RGB::black));
553 DOCTEST_CHECK_NOTHROW(visualizer.setLightDirection(make_vec3(0, 0, -1)));
554 DOCTEST_CHECK_NOTHROW(visualizer.setLightIntensityFactor(1.0f));
555}
556
557TEST_CASE("CI/Offscreen - Geometry Rendering") {
558 Visualizer visualizer(100, 100, 0, true, true); // Minimal size for speed
559
560 // Add some basic geometry directly to visualizer
561 size_t triangle = visualizer.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5, 1, 0), RGB::red, Visualizer::COORDINATES_CARTESIAN);
562 DOCTEST_CHECK(triangle != 0);
563
564 // Test basic rendering without crashing
565 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(RGB::black));
566}
567
568TEST_CASE("CI/Offscreen - Environment Variable Detection") {
569 // Test that environment variables are properly detected
570 // Note: This test runs in normal environment, so we just test the code paths
571
572 // Test with explicit headless=false but environment might force it
573 DOCTEST_CHECK_NOTHROW({
574 Visualizer visualizer(100, 100, 0, true, false); // headless=false
575 // Should still work - environment detection might force headless mode in CI
576 });
577}
578
579TEST_CASE("CI/Offscreen - Render Target Switching") {
580 Visualizer visualizer(64, 64, 0, true, true);
581
582 // Test switching to offscreen buffer
583 DOCTEST_CHECK_NOTHROW(visualizer.renderToOffscreenBuffer());
584
585 // Test that we can add geometry after switching render targets
586 size_t triangle = visualizer.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0.5, 1, 0), make_RGBcolor(1, 1, 1), Visualizer::COORDINATES_CARTESIAN);
587 DOCTEST_CHECK(triangle != 0);
588
589 // Test basic rendering operations
590 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(RGB::black));
591}
592
593TEST_CASE("CI/Offscreen - Stress Test") {
594 // Test multiple visualizers to ensure proper cleanup
595 std::vector<std::unique_ptr<Visualizer>> visualizers;
596
597 for (int i = 0; i < 3; ++i) {
598 DOCTEST_CHECK_NOTHROW({ visualizers.emplace_back(std::make_unique<Visualizer>(32, 32, 0, true, true)); });
599 }
600
601 // All visualizers should be valid
602 for (const auto &vis: visualizers) {
603 DOCTEST_CHECK(vis != nullptr);
604 }
605
606 // Cleanup happens automatically when unique_ptrs go out of scope
607}
608
609TEST_CASE("Visualizer::printWindow after plotUpdate regression test") {
610 // Regression test for the black image issue when calling printWindow() after plotUpdate(true)
611 // This test ensures the fix for the Ubuntu/Linux buffer reading issue works correctly
612
613 // Test works in both windowed and headless mode thanks to offscreen rendering support
614
616 Visualizer visualizer(200, 200, 0, true, true); // Small size for speed, headless mode
617 visualizer.disableMessages();
618
619 // Add some geometry to render (a simple sphere)
620 std::vector<uint> sphere_uuids = context.addSphere(10, make_vec3(0, 0, 0), 1.0f);
621 // Use material system for test geometry
622 std::string test_material = "test_visualizer_red_sphere";
623 if (!context.doesMaterialExist(test_material)) {
624 context.addMaterial(test_material);
625 context.setMaterialColor(test_material, make_RGBAcolor(RGB::red, 1.0f));
626 }
627 context.assignMaterialToPrimitive(sphere_uuids, test_material);
628
629 // Build geometry in visualizer
630 visualizer.buildContextGeometry(&context);
631
632 // Set camera to view the sphere
633 visualizer.setCameraPosition(make_vec3(0, 0, 3), make_vec3(0, 0, 0));
634
635 // This is the critical workflow that was failing: plotUpdate(true) followed by printWindow()
636 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true)); // render with hidden window
637
638 // Test screenshot functionality - this should NOT produce a black image
639 std::string test_filename = "test_printWindow_regression.jpg";
640 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str()));
641
642 // Verify the file was created
643 DOCTEST_CHECK(std::filesystem::exists(test_filename));
644
645 // Validate that the image is not all black (the original issue)
646 // Read back the pixels directly from the visualizer to verify content
647 std::vector<uint> pixel_buffer(200 * 200 * 3);
648 DOCTEST_CHECK_NOTHROW(visualizer.getWindowPixelsRGB(pixel_buffer.data()));
649
650 // Check that we have non-black pixels (red sphere should be visible)
651 bool has_non_black_pixels = false;
652 for (size_t i = 0; i < pixel_buffer.size(); i++) {
653 if (pixel_buffer[i] > 10) { // Allow for some tolerance due to anti-aliasing
654 has_non_black_pixels = true;
655 break;
656 }
657 }
658
659 DOCTEST_CHECK_MESSAGE(has_non_black_pixels, "Image appears to be all black - this indicates the original buffer reading issue");
660
661 // The key test: ensure we're not getting all black pixels (the original issue)
662 // This test validates that the buffer reading fix is working correctly
663
664 // Note: Offscreen rendering is already tested by existing "CI/Offscreen" test cases
665 // Our regression test focuses on the specific plotUpdate()->printWindow() workflow
666
667 // Clean up test file
668 if (std::filesystem::exists(test_filename)) {
669 std::filesystem::remove(test_filename);
670 }
671}
672
673TEST_CASE("Visualizer::printWindow after plotUpdate non-headless regression test") {
674 // Regression test for the black image issue when calling printWindow() after plotUpdate(true)
675 // in non-headless mode. Only runs when a display is available.
676
677 // Check if we have a display available (skip test if running in headless environment)
678 const char *display = std::getenv("DISPLAY");
679 const char *wayland_display = std::getenv("WAYLAND_DISPLAY");
680
681#ifdef __APPLE__
682 // On macOS, we can always create a window context
683 bool has_display = true;
684#else
685 // On Linux, check for X11 or Wayland display
686 bool has_display = (display != nullptr && strlen(display) > 0) || (wayland_display != nullptr && strlen(wayland_display) > 0);
687#endif
688
689 if (!has_display) {
690 // Skip test silently when no display is available
691 return;
692 }
693
695 Visualizer visualizer(200, 200, 0, true, false); // NON-headless mode - requires display
696 visualizer.disableMessages();
697
698 // Add some geometry to render (a simple sphere)
699 std::vector<uint> sphere_uuids = context.addSphere(10, make_vec3(0, 0, 0), 1.0f);
700 // Use material system for test geometry
701 std::string test_material = "test_visualizer_red_sphere";
702 if (!context.doesMaterialExist(test_material)) {
703 context.addMaterial(test_material);
704 context.setMaterialColor(test_material, make_RGBAcolor(RGB::red, 1.0f));
705 }
706 context.assignMaterialToPrimitive(sphere_uuids, test_material);
707
708 // Build geometry in visualizer
709 visualizer.buildContextGeometry(&context);
710
711 // Set camera to view the sphere
712 visualizer.setCameraPosition(make_vec3(0, 0, 3), make_vec3(0, 0, 0));
713
714 // This is the critical workflow that was failing: plotUpdate(true) followed by printWindow()
715 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true)); // render with hidden window
716
717 // Test screenshot functionality - this should NOT produce a black image
718 std::string test_filename = "test_printWindow_nonheadless_regression.jpg";
719 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str()));
720
721 // Verify the file was created
722 DOCTEST_CHECK(std::filesystem::exists(test_filename));
723
724 // Validate that the image is not all black (the original issue)
725 // Read back the pixels directly from the visualizer to verify content
726 std::vector<uint> pixel_buffer(200 * 200 * 3);
727 DOCTEST_CHECK_NOTHROW(visualizer.getWindowPixelsRGB(pixel_buffer.data()));
728
729 // Check that we have non-black pixels (red sphere should be visible)
730 bool has_non_black_pixels = false;
731 for (size_t i = 0; i < pixel_buffer.size(); i++) {
732 if (pixel_buffer[i] > 10) { // Allow for some tolerance due to anti-aliasing
733 has_non_black_pixels = true;
734 break;
735 }
736 }
737
738 DOCTEST_CHECK_MESSAGE(has_non_black_pixels, "Image appears to be all black in non-headless mode - buffer reading issue");
739
740 // Clean up test file
741 if (std::filesystem::exists(test_filename)) {
742 std::filesystem::remove(test_filename);
743 }
744}
745
746TEST_CASE("Visualizer::PNG with transparent background") {
747 // Test that PNG output with transparent background correctly renders geometry with transparency
748 Visualizer visualizer(200, 200, 16, true, true); // headless mode
749 visualizer.disableMessages();
750
751 // Add a red rectangle in the center
752 std::vector<helios::vec3> vertices{make_vec3(-0.3f, -0.3f, 0.f), make_vec3(0.3f, -0.3f, 0.f), make_vec3(0.3f, 0.3f, 0.f), make_vec3(-0.3f, 0.3f, 0.f)};
753
754 size_t rect_UUID;
755 DOCTEST_CHECK_NOTHROW(rect_UUID = visualizer.addRectangleByVertices(vertices, make_RGBcolor(1.f, 0.f, 0.f), Visualizer::COORDINATES_CARTESIAN));
756
757 // Set transparent background
758 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
759
760 // Render the scene
761 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
762
763 // Save to PNG
764 std::string test_filename = "test_transparent_bg.png";
765 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
766 DOCTEST_CHECK(std::filesystem::exists(test_filename));
767
768 // Read the PNG back to verify transparency
769 std::vector<helios::RGBAcolor> pixel_data;
770 uint width, height;
771 DOCTEST_CHECK_NOTHROW(helios::readPNG(test_filename, width, height, pixel_data));
772 DOCTEST_CHECK(width == 200);
773 DOCTEST_CHECK(height == 200);
774 DOCTEST_CHECK(pixel_data.size() == width * height);
775
776 // Count transparent and opaque pixels
777 int transparent_pixels = 0;
778 int opaque_red_pixels = 0;
779
780 for (const auto &pixel: pixel_data) {
781 if (pixel.a < 0.1f) {
782 // Fully transparent background pixel
783 transparent_pixels++;
784 } else if (pixel.a > 0.9f && pixel.r > 0.5f && pixel.g < 0.3f && pixel.b < 0.3f) {
785 // Opaque red pixel (the rectangle)
786 opaque_red_pixels++;
787 }
788 }
789
790 // We should have both transparent background pixels and opaque red rectangle pixels
791 DOCTEST_CHECK_MESSAGE(transparent_pixels > 1000, "Expected significant transparent background area, got " << transparent_pixels << " transparent pixels");
792 DOCTEST_CHECK_MESSAGE(opaque_red_pixels > 100, "Expected visible red rectangle in center, got " << opaque_red_pixels << " red pixels");
793
794 // Verify that the sum of different pixel types accounts for most of the image
795 DOCTEST_CHECK_MESSAGE(transparent_pixels + opaque_red_pixels > 0.8 * (width * height), "Transparent + opaque pixels should account for most of image");
796
797 // Clean up test file
798 if (std::filesystem::exists(test_filename)) {
799 std::filesystem::remove(test_filename);
800 }
801}
802
803TEST_CASE("Visualizer::headless render after windowed instance destroyed") {
804 // Smoke test for cross-mode instance ordering: a headless Visualizer constructed
805 // and rendered after a windowed Visualizer was created and destroyed in the same
806 // process must not crash. The windowed instance creates a shadow-map depth
807 // texture/framebuffer; a headless instance never creates these, so its
808 // depthTexture/framebufferID members must be zero-initialized (see Visualizer.h).
809 // If they were left uninitialized, the headless render path would bind a stale
810 // GL texture name as the shadow map, producing GL_INVALID_OPERATION and, on macOS,
811 // a driver-side crash. The original failure was allocator-layout dependent and only
812 // manifested reliably within the full suite, so this case guards the ordering rather
813 // than guaranteeing reproduction of the exact memory state.
814
815 // First, create and destroy a WINDOWED instance to populate freed memory with
816 // plausible GL handle values (mirrors the test-suite ordering that exposed the bug).
817 {
818 Visualizer windowed(200, 200, 0, true, false); // headless = false
819 windowed.disableMessages();
820 } // windowed destroyed here
821
822 // Now a HEADLESS instance whose render path binds the (default lighting) shadow map.
823 Visualizer headless(200, 200, 16, true, true); // headless = true
824 headless.disableMessages();
825
826 std::vector<helios::vec3> vertices{make_vec3(-0.3f, -0.3f, 0.f), make_vec3(0.3f, -0.3f, 0.f), make_vec3(0.3f, 0.3f, 0.f), make_vec3(-0.3f, 0.3f, 0.f)};
827 size_t rect_UUID;
828 DOCTEST_CHECK_NOTHROW(rect_UUID = headless.addRectangleByVertices(vertices, make_RGBcolor(0.f, 1.f, 0.f), Visualizer::COORDINATES_CARTESIAN));
829
830 // The render must complete without binding an uninitialized shadow-map texture.
831 DOCTEST_CHECK_NOTHROW(headless.plotUpdate(true));
832}
833
834TEST_CASE("Visualizer::PNG with transparent background (windowed mode)") {
835 // Test PNG output with transparent background in WINDOWED mode (not headless)
837
838 // Add a red patch via the Context (matching user's workflow)
839 uint patch_UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.6, 0.6), nullrotation, "plugins/visualizer/textures/AlmondLeaf.png");
840 // Use material system for test geometry with texture override
841 std::string test_material = "test_visualizer_red_patch";
842 if (!context.doesMaterialExist(test_material)) {
843 context.addMaterial(test_material);
844 context.setMaterialColor(test_material, make_RGBAcolor(1.f, 0.f, 0.f, 1.f));
845 context.setMaterialTexture(test_material, "plugins/visualizer/textures/AlmondLeaf.png");
846 context.setMaterialTextureColorOverride(test_material, true);
847 }
848 context.assignMaterialToPrimitive(patch_UUID, test_material); // Required to use vertex color instead of texture color
849
850 Visualizer visualizer(200, 200, 16, false, true);
851 visualizer.disableMessages();
852
853 // Set transparent background BEFORE building context geometry
854 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
855
856 // Use shadowed lighting to match user's code
857 DOCTEST_CHECK_NOTHROW(visualizer.setLightingModel(Visualizer::LIGHTING_PHONG_SHADOWED));
858
859 // Build context geometry (this is what user does)
860 DOCTEST_CHECK_NOTHROW(visualizer.buildContextGeometry(&context));
861
862 // Render the scene (use plotUpdate() without argument to match user's code exactly)
863 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate());
864
865 // Save to PNG
866 std::string test_filename = "test_transparent_bg_windowed.png";
867 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
868 DOCTEST_CHECK(std::filesystem::exists(test_filename));
869
870 // Read the PNG back to verify transparency
871 std::vector<helios::RGBAcolor> pixel_data;
872 uint width, height;
873 DOCTEST_CHECK_NOTHROW(helios::readPNG(test_filename, width, height, pixel_data));
874 // Note: width/height may be larger than 200 due to HiDPI/Retina scaling
875 DOCTEST_CHECK(width > 0);
876 DOCTEST_CHECK(height > 0);
877 DOCTEST_CHECK(pixel_data.size() == width * height);
878
879 // Count transparent, checkerboard, and opaque pixels
880 int transparent_pixels = 0;
881 int opaque_red_pixels = 0;
882 int checkerboard_pixels = 0; // Gray pixels from checkerboard texture
883
884 for (const auto &pixel: pixel_data) {
885 if (pixel.a < 0.1f) {
886 // Fully transparent background pixel
887 transparent_pixels++;
888 } else if (pixel.a > 0.9f && pixel.r > 0.5f && pixel.g < 0.3f && pixel.b < 0.3f) {
889 // Opaque red pixel (the rectangle)
890 opaque_red_pixels++;
891 } else if (pixel.a > 0.9f && pixel.r > 0.6f && pixel.r < 0.85f && std::abs(pixel.r - pixel.g) < 0.1f && std::abs(pixel.r - pixel.b) < 0.1f) {
892 // Gray pixels - likely from checkerboard (should NOT be present)
893 checkerboard_pixels++;
894 }
895 }
896
897 // The checkerboard should NOT appear in the output
898 DOCTEST_CHECK_MESSAGE(checkerboard_pixels == 0, "Checkerboard texture should not appear in PNG output, got " << checkerboard_pixels << " checkerboard pixels");
899
900 // We should have transparent background pixels (at least 25% of image)
901 uint total_pixels = width * height;
902 DOCTEST_CHECK_MESSAGE(transparent_pixels > total_pixels * 0.25, "Expected significant transparent background area, got " << transparent_pixels << " transparent pixels out of " << total_pixels);
903
904 // We should have the red rectangle (at least 2.5% of image)
905 DOCTEST_CHECK_MESSAGE(opaque_red_pixels > total_pixels * 0.025, "Expected visible red rectangle in center, got " << opaque_red_pixels << " red pixels out of " << total_pixels);
906
907 // Clean up test file
908 if (std::filesystem::exists(test_filename)) {
909 std::filesystem::remove(test_filename);
910 }
911}
912
913TEST_CASE("Visualizer::Transparent background with non-square window") {
914 // Test that checkerboard squares remain square regardless of window aspect ratio
915 // This test verifies that UV coordinates are properly adjusted based on window dimensions
916
918
919 // Add a small patch to have some geometry
920 uint patch_UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.3, 0.3), nullrotation, "plugins/visualizer/textures/AlmondLeaf.png");
921 // Use material system for test geometry with texture override
922 std::string test_material = "test_visualizer_red_patch_small";
923 if (!context.doesMaterialExist(test_material)) {
924 context.addMaterial(test_material);
925 context.setMaterialColor(test_material, make_RGBAcolor(1.f, 0.f, 0.f, 1.f));
926 context.setMaterialTexture(test_material, "plugins/visualizer/textures/AlmondLeaf.png");
927 context.setMaterialTextureColorOverride(test_material, true);
928 }
929 context.assignMaterialToPrimitive(patch_UUID, test_material);
930
931 // Test with a non-square window (800x600, aspect ratio 4:3)
932 Visualizer visualizer(800, 600, 16, false, true);
933 visualizer.disableMessages();
934
935 // Set transparent background
936 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
937
938 // Build geometry
939 DOCTEST_CHECK_NOTHROW(visualizer.buildContextGeometry(&context));
940
941 // Render and save
942 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
943
944 std::string test_filename = "test_transparent_bg_nonsquare.png";
945 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
946 DOCTEST_CHECK(std::filesystem::exists(test_filename));
947
948 // Read back to verify
949 std::vector<helios::RGBAcolor> pixel_data;
950 uint width, height;
951 DOCTEST_CHECK_NOTHROW(helios::readPNG(test_filename, width, height, pixel_data));
952 DOCTEST_CHECK(width > 0);
953 DOCTEST_CHECK(height > 0);
954
955 // Count pixels by type
956 int transparent_pixels = 0;
957 int opaque_red_pixels = 0;
958
959 for (const auto &pixel: pixel_data) {
960 if (pixel.a < 0.1f) {
961 transparent_pixels++;
962 } else if (pixel.a > 0.9f && pixel.r > 0.5f && pixel.g < 0.3f && pixel.b < 0.3f) {
963 opaque_red_pixels++;
964 }
965 }
966
967 // Verify we have transparent background and geometry
968 uint total_pixels = width * height;
969 DOCTEST_CHECK_MESSAGE(transparent_pixels > total_pixels * 0.5, "Expected significant transparent background, got " << transparent_pixels << " transparent pixels out of " << total_pixels);
970 DOCTEST_CHECK_MESSAGE(opaque_red_pixels > 100, "Expected visible red rectangle, got " << opaque_red_pixels << " red pixels");
971
972 // Clean up
973 if (std::filesystem::exists(test_filename)) {
974 std::filesystem::remove(test_filename);
975 }
976}
977
978TEST_CASE("Visualizer::Background color/transparent switching") {
979 // Test that switching between transparent and solid color background properly manages watermark visibility
980
982 uint patch_UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.3, 0.3), nullrotation, "plugins/visualizer/textures/AlmondLeaf.png");
983 // Use material system for test geometry with texture override
984 std::string test_material = "test_visualizer_red_patch_small";
985 if (!context.doesMaterialExist(test_material)) {
986 context.addMaterial(test_material);
987 context.setMaterialColor(test_material, make_RGBAcolor(1.f, 0.f, 0.f, 1.f));
988 context.setMaterialTexture(test_material, "plugins/visualizer/textures/AlmondLeaf.png");
989 context.setMaterialTextureColorOverride(test_material, true);
990 }
991 context.assignMaterialToPrimitive(patch_UUID, test_material);
992
993 SUBCASE("Watermark visible → transparent → solid color (should restore watermark)") {
994 Visualizer visualizer(200, 200, 16, false, true);
995 visualizer.disableMessages();
996 visualizer.buildContextGeometry(&context);
997
998 // Watermark should be visible by default
999 // (We can't directly check isWatermarkVisible since it's private, but we test the behavior)
1000
1001 // Switch to transparent background - should hide watermark
1002 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1003
1004 // Switch back to solid color - should restore watermark
1005 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.5f, 0.5f, 0.5f)));
1006
1007 // Verify transparent background is disabled
1008 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1009
1010 // If we can render without error, the watermark restoration worked
1011 std::string test_filename = "test_bg_switch_restore.png";
1012 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
1013 DOCTEST_CHECK(std::filesystem::exists(test_filename));
1014
1015 if (std::filesystem::exists(test_filename)) {
1016 std::filesystem::remove(test_filename);
1017 }
1018 }
1019
1020 SUBCASE("Watermark hidden → transparent → solid color (should NOT restore watermark)") {
1021 Visualizer visualizer(200, 200, 16, false, true);
1022 visualizer.disableMessages();
1023 visualizer.buildContextGeometry(&context);
1024
1025 // Manually hide watermark before enabling transparent background
1026 DOCTEST_CHECK_NOTHROW(visualizer.hideWatermark());
1027
1028 // Switch to transparent background
1029 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1030
1031 // Switch back to solid color - should NOT restore watermark (it was manually hidden)
1032 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.5f, 0.5f, 0.5f)));
1033
1034 // Verify rendering works
1035 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1036
1037 std::string test_filename = "test_bg_switch_no_restore.png";
1038 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
1039 DOCTEST_CHECK(std::filesystem::exists(test_filename));
1040
1041 if (std::filesystem::exists(test_filename)) {
1042 std::filesystem::remove(test_filename);
1043 }
1044 }
1045
1046 SUBCASE("Multiple switches between transparent and solid") {
1047 Visualizer visualizer(200, 200, 16, false, true);
1048 visualizer.disableMessages();
1049 visualizer.buildContextGeometry(&context);
1050
1051 // Multiple switches should work correctly
1052 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1053 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(1.f, 0.f, 0.f)));
1054 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1055 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.f, 1.f, 0.f)));
1056 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1057 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.f, 0.f, 1.f)));
1058
1059 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1060 }
1061}
1062
1063DOCTEST_TEST_CASE("Visualizer::Navigation Gizmo") {
1064 // Test the navigation gizmo functionality
1065
1066 SUBCASE("Navigation gizmo is enabled by default") {
1067 Visualizer visualizer(200, 200, 16, false, true);
1068 visualizer.disableMessages();
1069 // Gizmo should be enabled by default
1070 // We can't directly access the private member, but we can test the behavior
1071 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1072 }
1073
1074 SUBCASE("Show and hide navigation gizmo") {
1075 Visualizer visualizer(200, 200, 16, false, true);
1076 visualizer.disableMessages();
1077
1078 // Hide the gizmo
1079 DOCTEST_CHECK_NOTHROW(visualizer.hideNavigationGizmo());
1080 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1081
1082 // Show the gizmo
1083 DOCTEST_CHECK_NOTHROW(visualizer.showNavigationGizmo());
1084 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1085
1086 // Hide and show multiple times
1087 DOCTEST_CHECK_NOTHROW(visualizer.hideNavigationGizmo());
1088 DOCTEST_CHECK_NOTHROW(visualizer.showNavigationGizmo());
1089 DOCTEST_CHECK_NOTHROW(visualizer.hideNavigationGizmo());
1090 DOCTEST_CHECK_NOTHROW(visualizer.showNavigationGizmo());
1091 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1092 }
1093
1094 SUBCASE("Navigation gizmo with camera movement") {
1095 Visualizer visualizer(200, 200, 16, false, true);
1096 visualizer.disableMessages();
1097
1098 // Add some geometry to visualize
1099 auto sphere_center = make_vec3(0, 0, 0);
1100 auto sphere_radius = 1.0f;
1101 auto sphere_color = make_RGBcolor(1.f, 0.f, 0.f);
1102 auto sphere_uuids = visualizer.addSphereByCenter(sphere_radius, sphere_center, 10, sphere_color, Visualizer::COORDINATES_CARTESIAN);
1103
1104 // Set initial camera position
1105 visualizer.setCameraPosition(make_vec3(3, 3, 3), make_vec3(0, 0, 0));
1106 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1107
1108 // Move camera to a different position
1109 visualizer.setCameraPosition(make_vec3(-3, 3, 3), make_vec3(0, 0, 0));
1110 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1111
1112 // Move camera again
1113 visualizer.setCameraPosition(make_vec3(0, 5, 5), make_vec3(0, 0, 0));
1114 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1115 }
1116
1117 SUBCASE("Navigation gizmo with printWindow") {
1118 Visualizer visualizer(200, 200, 16, false, true);
1119 visualizer.disableMessages();
1120
1121 // Add some geometry
1122 auto sphere_uuids = visualizer.addSphereByCenter(1.0f, make_vec3(0, 0, 0), 10, make_RGBcolor(1.f, 0.f, 0.f), Visualizer::COORDINATES_CARTESIAN);
1123 visualizer.setCameraPosition(make_vec3(3, 3, 3), make_vec3(0, 0, 0));
1124
1125 // Show gizmo and take screenshot
1126 visualizer.showNavigationGizmo();
1127 std::string test_filename = "test_nav_gizmo_screenshot.jpg";
1128 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str()));
1129
1130 // Verify the file was created
1131 DOCTEST_CHECK(std::filesystem::exists(test_filename));
1132
1133 // Clean up test file
1134 if (std::filesystem::exists(test_filename)) {
1135 std::filesystem::remove(test_filename);
1136 }
1137 }
1138
1139 SUBCASE("Navigation gizmo state persists after printWindow") {
1140 Visualizer visualizer(200, 200, 16, false, true);
1141 visualizer.disableMessages();
1142
1143 // Enable gizmo
1144 visualizer.showNavigationGizmo();
1145
1146 // Take screenshot (gizmo should be hidden during screenshot but restored after)
1147 std::string test_filename = "test_nav_gizmo_persist.jpg";
1148 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str()));
1149
1150 // Gizmo should still be enabled after screenshot
1151 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1152
1153 // Clean up
1154 if (std::filesystem::exists(test_filename)) {
1155 std::filesystem::remove(test_filename);
1156 }
1157 }
1158}
1159
1160DOCTEST_TEST_CASE("GeometryHandler::getVertices coordinate system transformation") {
1161 // Test that getVertices() returns vertices in the same coordinate space they were provided
1162 // This is a regression test for the bug where COORDINATES_WINDOW_NORMALIZED vertices
1163 // were transformed to OpenGL space [-1,1] but getVertices() didn't apply inverse transformation
1164
1165 Visualizer visualizer(200, 200, 16, false, true);
1166
1167 SUBCASE("COORDINATES_WINDOW_NORMALIZED - getVertices should return original [0,1] coordinates") {
1168 // Create a rectangle with known normalized window coordinates [0,1]
1169 helios::vec3 center = make_vec3(0.852f, 0.1f, 0.011f);
1170 helios::vec2 size = make_vec2(0.02f, 0.025f);
1171
1172 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::red, Visualizer::COORDINATES_WINDOW_NORMALIZED);
1173 DOCTEST_CHECK(rect_id != 0);
1174
1175 // Get vertices back - they should be in the SAME coordinate space [0,1]
1176 auto vertices = visualizer.getGeometryVertices(rect_id);
1177 DOCTEST_CHECK(vertices.size() == 4);
1178
1179 // Calculate expected vertices from center and size
1180 float half_width = size.x * 0.5f;
1181 float half_height = size.y * 0.5f;
1182 helios::vec3 expected_v0 = make_vec3(center.x - half_width, center.y - half_height, center.z);
1183 helios::vec3 expected_v1 = make_vec3(center.x + half_width, center.y - half_height, center.z);
1184 helios::vec3 expected_v2 = make_vec3(center.x + half_width, center.y + half_height, center.z);
1185 helios::vec3 expected_v3 = make_vec3(center.x - half_width, center.y + half_height, center.z);
1186
1187 // Check that returned vertices are in [0,1] range (normalized window coordinates)
1188 float tolerance = 1e-5f;
1189 DOCTEST_CHECK_MESSAGE(std::abs(vertices[0].x - expected_v0.x) < tolerance, "v0.x expected " << expected_v0.x << " but got " << vertices[0].x);
1190 DOCTEST_CHECK_MESSAGE(std::abs(vertices[0].y - expected_v0.y) < tolerance, "v0.y expected " << expected_v0.y << " but got " << vertices[0].y);
1191 DOCTEST_CHECK_MESSAGE(std::abs(vertices[1].x - expected_v1.x) < tolerance, "v1.x expected " << expected_v1.x << " but got " << vertices[1].x);
1192 DOCTEST_CHECK_MESSAGE(std::abs(vertices[1].y - expected_v1.y) < tolerance, "v1.y expected " << expected_v1.y << " but got " << vertices[1].y);
1193
1194 // Verify vertices are actually in [0,1] range, not [-1,1] range
1195 for (const auto &v: vertices) {
1196 bool x_in_range = (v.x >= 0.0f) && (v.x <= 1.0f);
1197 DOCTEST_CHECK_MESSAGE(x_in_range, "Vertex x=" << v.x << " is outside [0,1] range - bug not fixed!");
1198 bool y_in_range = (v.y >= 0.0f) && (v.y <= 1.0f);
1199 DOCTEST_CHECK_MESSAGE(y_in_range, "Vertex y=" << v.y << " is outside [0,1] range - bug not fixed!");
1200 }
1201 }
1202
1203 SUBCASE("COORDINATES_CARTESIAN - getVertices should return original Cartesian coordinates") {
1204 // Create a rectangle with Cartesian coordinates (no transformation should occur)
1205 helios::vec3 center = make_vec3(5.0f, 3.0f, 2.0f);
1206 helios::vec2 size = make_vec2(1.0f, 2.0f);
1207
1208 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::blue, Visualizer::COORDINATES_CARTESIAN);
1209 DOCTEST_CHECK(rect_id != 0);
1210
1211 // Get vertices back - they should be unchanged
1212 auto vertices = visualizer.getGeometryVertices(rect_id);
1213 DOCTEST_CHECK(vertices.size() == 4);
1214
1215 // Calculate expected vertices
1216 float half_width = size.x * 0.5f;
1217 float half_height = size.y * 0.5f;
1218 helios::vec3 expected_v0 = make_vec3(center.x - half_width, center.y - half_height, center.z);
1219
1220 float tolerance = 1e-5f;
1221 DOCTEST_CHECK(std::abs(vertices[0].x - expected_v0.x) < tolerance);
1222 DOCTEST_CHECK(std::abs(vertices[0].y - expected_v0.y) < tolerance);
1223 DOCTEST_CHECK(std::abs(vertices[0].z - expected_v0.z) < tolerance);
1224 }
1225}
1226
1227DOCTEST_TEST_CASE("GeometryHandler::setVertices coordinate system transformation") {
1228 // Test that setVertices() applies the same transformation as addGeometry()
1229
1230 Visualizer visualizer(200, 200, 16, false, true);
1231
1232 SUBCASE("COORDINATES_WINDOW_NORMALIZED - setVertices should transform [0,1] to [-1,1]") {
1233 // Create a rectangle
1234 helios::vec3 center = make_vec3(0.5f, 0.5f, 0.0f);
1235 helios::vec2 size = make_vec2(0.2f, 0.2f);
1236 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::green, Visualizer::COORDINATES_WINDOW_NORMALIZED);
1237
1238 // Get original vertices
1239 auto original_vertices = visualizer.getGeometryVertices(rect_id);
1240
1241 // Modify vertices slightly (still in [0,1] space)
1242 std::vector<helios::vec3> new_vertices = original_vertices;
1243 for (auto &v: new_vertices) {
1244 v.x += 0.1f;
1245 v.y += 0.1f;
1246 }
1247
1248 // Set the modified vertices
1249 DOCTEST_CHECK_NOTHROW(visualizer.setGeometryVertices(rect_id, new_vertices));
1250
1251 // Get vertices back
1252 auto retrieved_vertices = visualizer.getGeometryVertices(rect_id);
1253
1254 // Verify we get back what we set (in the same coordinate space)
1255 float tolerance = 1e-5f;
1256 DOCTEST_CHECK(std::abs(retrieved_vertices[0].x - new_vertices[0].x) < tolerance);
1257 DOCTEST_CHECK(std::abs(retrieved_vertices[0].y - new_vertices[0].y) < tolerance);
1258 }
1259
1260 SUBCASE("COORDINATES_CARTESIAN - setVertices should not transform") {
1261 // Create a Cartesian rectangle
1262 helios::vec3 center = make_vec3(0.0f, 0.0f, 0.0f);
1263 helios::vec2 size = make_vec2(2.0f, 2.0f);
1264 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::yellow, Visualizer::COORDINATES_CARTESIAN);
1265
1266 // Get original vertices
1267 auto original_vertices = visualizer.getGeometryVertices(rect_id);
1268
1269 // Modify vertices
1270 std::vector<helios::vec3> new_vertices = original_vertices;
1271 for (auto &v: new_vertices) {
1272 v.x += 1.0f;
1273 v.y += 1.0f;
1274 }
1275
1276 // Set the modified vertices
1277 DOCTEST_CHECK_NOTHROW(visualizer.setGeometryVertices(rect_id, new_vertices));
1278
1279 // Get vertices back
1280 auto retrieved_vertices = visualizer.getGeometryVertices(rect_id);
1281
1282 // Verify we get back what we set
1283 float tolerance = 1e-5f;
1284 DOCTEST_CHECK(std::abs(retrieved_vertices[0].x - new_vertices[0].x) < tolerance);
1285 DOCTEST_CHECK(std::abs(retrieved_vertices[0].y - new_vertices[0].y) < tolerance);
1286 DOCTEST_CHECK(std::abs(retrieved_vertices[0].z - new_vertices[0].z) < tolerance);
1287 }
1288}
1289
1290DOCTEST_TEST_CASE("Visualizer::colorContextPrimitivesByObjectData orphan primitives use colormap-at-0") {
1291 // Regression test: when calling colorContextPrimitivesByObjectData(), primitives
1292 // that are not part of any compound object should be colored as if their object-data
1293 // value were 0 (so they get colormap-at-0), rather than rendering with their base RGBA.
1295
1296 // An orphan patch with a deliberately distinctive base RGBA color (bright red).
1297 // If the fix regresses, this red is what will appear in the rendered image.
1298 const RGBcolor base_color = make_RGBcolor(1.f, 0.f, 0.f);
1299 uint patch_UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1.f, 1.f), nullrotation, base_color);
1300 DOCTEST_CHECK(context.getPrimitiveParentObjectID(patch_UUID) == 0); // confirm it's an orphan
1301
1302 Visualizer visualizer(200, 200, 16, false, true); // headless
1303 visualizer.disableMessages();
1304 visualizer.setLightingModel(Visualizer::LIGHTING_NONE); // avoid shading skewing the rendered color
1305
1306 // Set an explicit colorbar range that does NOT include 0. With the default "hot"
1307 // colormap, query(0) under range [1,2] is clamped to colormap-min = (0,0,0) (black).
1308 visualizer.setColorbarRange(1.f, 2.f);
1309
1310 DOCTEST_CHECK_NOTHROW(visualizer.buildContextGeometry(&context));
1311 DOCTEST_CHECK_NOTHROW(visualizer.colorContextPrimitivesByObjectData("nonexistent_object_data"));
1312 // Disable the auto-enabled colorbar so it doesn't contribute red/yellow pixels from
1313 // the high end of the "hot" colormap, which would otherwise contaminate the pixel counts.
1314 DOCTEST_CHECK_NOTHROW(visualizer.disableColorbar());
1315 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1316
1317 std::string test_filename = "test_orphan_color_by_object_data.png";
1318 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
1319 DOCTEST_CHECK(std::filesystem::exists(test_filename));
1320
1321 std::vector<RGBAcolor> pixel_data;
1322 uint width, height;
1323 DOCTEST_CHECK_NOTHROW(helios::readPNG(test_filename, width, height, pixel_data));
1324
1325 // Walk all pixels: count red (base color leaking through — would mean fix regressed)
1326 // vs near-black (colormap_hot at value 0 — the expected post-fix behavior).
1327 int red_pixels = 0;
1328 int black_patch_pixels = 0;
1329 for (const auto &pixel: pixel_data) {
1330 if (pixel.r > 0.5f && pixel.g < 0.2f && pixel.b < 0.2f) {
1331 red_pixels++;
1332 } else if (pixel.r < 0.1f && pixel.g < 0.1f && pixel.b < 0.1f) {
1333 black_patch_pixels++;
1334 }
1335 }
1336
1337 DOCTEST_CHECK_MESSAGE(red_pixels == 0, "Orphan primitive rendered with its base RGBA (red) instead of colormap-at-0; got " << red_pixels << " red pixels");
1338 DOCTEST_CHECK_MESSAGE(black_patch_pixels > 100, "Expected the orphan patch to render as near-black (colormap_hot at 0); only got " << black_patch_pixels << " black pixels");
1339
1340 if (std::filesystem::exists(test_filename)) {
1341 std::filesystem::remove(test_filename);
1342 }
1343}
1344
1345int Visualizer::selfTest(int argc, char **argv) {
1346 return helios::runDoctestWithValidation(argc, argv);
1347}