1.3.64
 
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
615 Context context;
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
694 Context context;
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::PNG with transparent background (windowed mode)") {
804 // Test PNG output with transparent background in WINDOWED mode (not headless)
805 Context context;
806
807 // Add a red patch via the Context (matching user's workflow)
808 uint patch_UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.6, 0.6), nullrotation, "plugins/visualizer/textures/AlmondLeaf.png");
809 // Use material system for test geometry with texture override
810 std::string test_material = "test_visualizer_red_patch";
811 if (!context.doesMaterialExist(test_material)) {
812 context.addMaterial(test_material);
813 context.setMaterialColor(test_material, make_RGBAcolor(1.f, 0.f, 0.f, 1.f));
814 context.setMaterialTexture(test_material, "plugins/visualizer/textures/AlmondLeaf.png");
815 context.setMaterialTextureColorOverride(test_material, true);
816 }
817 context.assignMaterialToPrimitive(patch_UUID, test_material); // Required to use vertex color instead of texture color
818
819 Visualizer visualizer(200, 200, 16, false, true);
820 visualizer.disableMessages();
821
822 // Set transparent background BEFORE building context geometry
823 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
824
825 // Use shadowed lighting to match user's code
826 DOCTEST_CHECK_NOTHROW(visualizer.setLightingModel(Visualizer::LIGHTING_PHONG_SHADOWED));
827
828 // Build context geometry (this is what user does)
829 DOCTEST_CHECK_NOTHROW(visualizer.buildContextGeometry(&context));
830
831 // Render the scene (use plotUpdate() without argument to match user's code exactly)
832 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate());
833
834 // Save to PNG
835 std::string test_filename = "test_transparent_bg_windowed.png";
836 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
837 DOCTEST_CHECK(std::filesystem::exists(test_filename));
838
839 // Read the PNG back to verify transparency
840 std::vector<helios::RGBAcolor> pixel_data;
841 uint width, height;
842 DOCTEST_CHECK_NOTHROW(helios::readPNG(test_filename, width, height, pixel_data));
843 // Note: width/height may be larger than 200 due to HiDPI/Retina scaling
844 DOCTEST_CHECK(width > 0);
845 DOCTEST_CHECK(height > 0);
846 DOCTEST_CHECK(pixel_data.size() == width * height);
847
848 // Count transparent, checkerboard, and opaque pixels
849 int transparent_pixels = 0;
850 int opaque_red_pixels = 0;
851 int checkerboard_pixels = 0; // Gray pixels from checkerboard texture
852
853 for (const auto &pixel: pixel_data) {
854 if (pixel.a < 0.1f) {
855 // Fully transparent background pixel
856 transparent_pixels++;
857 } else if (pixel.a > 0.9f && pixel.r > 0.5f && pixel.g < 0.3f && pixel.b < 0.3f) {
858 // Opaque red pixel (the rectangle)
859 opaque_red_pixels++;
860 } 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) {
861 // Gray pixels - likely from checkerboard (should NOT be present)
862 checkerboard_pixels++;
863 }
864 }
865
866 // The checkerboard should NOT appear in the output
867 DOCTEST_CHECK_MESSAGE(checkerboard_pixels == 0, "Checkerboard texture should not appear in PNG output, got " << checkerboard_pixels << " checkerboard pixels");
868
869 // We should have transparent background pixels (at least 25% of image)
870 uint total_pixels = width * height;
871 DOCTEST_CHECK_MESSAGE(transparent_pixels > total_pixels * 0.25, "Expected significant transparent background area, got " << transparent_pixels << " transparent pixels out of " << total_pixels);
872
873 // We should have the red rectangle (at least 2.5% of image)
874 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);
875
876 // Clean up test file
877 if (std::filesystem::exists(test_filename)) {
878 std::filesystem::remove(test_filename);
879 }
880}
881
882TEST_CASE("Visualizer::Transparent background with non-square window") {
883 // Test that checkerboard squares remain square regardless of window aspect ratio
884 // This test verifies that UV coordinates are properly adjusted based on window dimensions
885
886 Context context;
887
888 // Add a small patch to have some geometry
889 uint patch_UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.3, 0.3), nullrotation, "plugins/visualizer/textures/AlmondLeaf.png");
890 // Use material system for test geometry with texture override
891 std::string test_material = "test_visualizer_red_patch_small";
892 if (!context.doesMaterialExist(test_material)) {
893 context.addMaterial(test_material);
894 context.setMaterialColor(test_material, make_RGBAcolor(1.f, 0.f, 0.f, 1.f));
895 context.setMaterialTexture(test_material, "plugins/visualizer/textures/AlmondLeaf.png");
896 context.setMaterialTextureColorOverride(test_material, true);
897 }
898 context.assignMaterialToPrimitive(patch_UUID, test_material);
899
900 // Test with a non-square window (800x600, aspect ratio 4:3)
901 Visualizer visualizer(800, 600, 16, false, true);
902 visualizer.disableMessages();
903
904 // Set transparent background
905 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
906
907 // Build geometry
908 DOCTEST_CHECK_NOTHROW(visualizer.buildContextGeometry(&context));
909
910 // Render and save
911 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
912
913 std::string test_filename = "test_transparent_bg_nonsquare.png";
914 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
915 DOCTEST_CHECK(std::filesystem::exists(test_filename));
916
917 // Read back to verify
918 std::vector<helios::RGBAcolor> pixel_data;
919 uint width, height;
920 DOCTEST_CHECK_NOTHROW(helios::readPNG(test_filename, width, height, pixel_data));
921 DOCTEST_CHECK(width > 0);
922 DOCTEST_CHECK(height > 0);
923
924 // Count pixels by type
925 int transparent_pixels = 0;
926 int opaque_red_pixels = 0;
927
928 for (const auto &pixel: pixel_data) {
929 if (pixel.a < 0.1f) {
930 transparent_pixels++;
931 } else if (pixel.a > 0.9f && pixel.r > 0.5f && pixel.g < 0.3f && pixel.b < 0.3f) {
932 opaque_red_pixels++;
933 }
934 }
935
936 // Verify we have transparent background and geometry
937 uint total_pixels = width * height;
938 DOCTEST_CHECK_MESSAGE(transparent_pixels > total_pixels * 0.5, "Expected significant transparent background, got " << transparent_pixels << " transparent pixels out of " << total_pixels);
939 DOCTEST_CHECK_MESSAGE(opaque_red_pixels > 100, "Expected visible red rectangle, got " << opaque_red_pixels << " red pixels");
940
941 // Clean up
942 if (std::filesystem::exists(test_filename)) {
943 std::filesystem::remove(test_filename);
944 }
945}
946
947TEST_CASE("Visualizer::Background color/transparent switching") {
948 // Test that switching between transparent and solid color background properly manages watermark visibility
949
950 Context context;
951 uint patch_UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(0.3, 0.3), nullrotation, "plugins/visualizer/textures/AlmondLeaf.png");
952 // Use material system for test geometry with texture override
953 std::string test_material = "test_visualizer_red_patch_small";
954 if (!context.doesMaterialExist(test_material)) {
955 context.addMaterial(test_material);
956 context.setMaterialColor(test_material, make_RGBAcolor(1.f, 0.f, 0.f, 1.f));
957 context.setMaterialTexture(test_material, "plugins/visualizer/textures/AlmondLeaf.png");
958 context.setMaterialTextureColorOverride(test_material, true);
959 }
960 context.assignMaterialToPrimitive(patch_UUID, test_material);
961
962 SUBCASE("Watermark visible → transparent → solid color (should restore watermark)") {
963 Visualizer visualizer(200, 200, 16, false, true);
964 visualizer.disableMessages();
965 visualizer.buildContextGeometry(&context);
966
967 // Watermark should be visible by default
968 // (We can't directly check isWatermarkVisible since it's private, but we test the behavior)
969
970 // Switch to transparent background - should hide watermark
971 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
972
973 // Switch back to solid color - should restore watermark
974 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.5f, 0.5f, 0.5f)));
975
976 // Verify transparent background is disabled
977 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
978
979 // If we can render without error, the watermark restoration worked
980 std::string test_filename = "test_bg_switch_restore.png";
981 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
982 DOCTEST_CHECK(std::filesystem::exists(test_filename));
983
984 if (std::filesystem::exists(test_filename)) {
985 std::filesystem::remove(test_filename);
986 }
987 }
988
989 SUBCASE("Watermark hidden → transparent → solid color (should NOT restore watermark)") {
990 Visualizer visualizer(200, 200, 16, false, true);
991 visualizer.disableMessages();
992 visualizer.buildContextGeometry(&context);
993
994 // Manually hide watermark before enabling transparent background
995 DOCTEST_CHECK_NOTHROW(visualizer.hideWatermark());
996
997 // Switch to transparent background
998 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
999
1000 // Switch back to solid color - should NOT restore watermark (it was manually hidden)
1001 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.5f, 0.5f, 0.5f)));
1002
1003 // Verify rendering works
1004 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1005
1006 std::string test_filename = "test_bg_switch_no_restore.png";
1007 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str(), "png"));
1008 DOCTEST_CHECK(std::filesystem::exists(test_filename));
1009
1010 if (std::filesystem::exists(test_filename)) {
1011 std::filesystem::remove(test_filename);
1012 }
1013 }
1014
1015 SUBCASE("Multiple switches between transparent and solid") {
1016 Visualizer visualizer(200, 200, 16, false, true);
1017 visualizer.disableMessages();
1018 visualizer.buildContextGeometry(&context);
1019
1020 // Multiple switches should work correctly
1021 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1022 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(1.f, 0.f, 0.f)));
1023 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1024 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.f, 1.f, 0.f)));
1025 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundTransparent());
1026 DOCTEST_CHECK_NOTHROW(visualizer.setBackgroundColor(make_RGBcolor(0.f, 0.f, 1.f)));
1027
1028 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1029 }
1030}
1031
1032DOCTEST_TEST_CASE("Visualizer::Navigation Gizmo") {
1033 // Test the navigation gizmo functionality
1034
1035 SUBCASE("Navigation gizmo is enabled by default") {
1036 Visualizer visualizer(200, 200, 16, false, true);
1037 visualizer.disableMessages();
1038 // Gizmo should be enabled by default
1039 // We can't directly access the private member, but we can test the behavior
1040 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1041 }
1042
1043 SUBCASE("Show and hide navigation gizmo") {
1044 Visualizer visualizer(200, 200, 16, false, true);
1045 visualizer.disableMessages();
1046
1047 // Hide the gizmo
1048 DOCTEST_CHECK_NOTHROW(visualizer.hideNavigationGizmo());
1049 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1050
1051 // Show the gizmo
1052 DOCTEST_CHECK_NOTHROW(visualizer.showNavigationGizmo());
1053 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1054
1055 // Hide and show multiple times
1056 DOCTEST_CHECK_NOTHROW(visualizer.hideNavigationGizmo());
1057 DOCTEST_CHECK_NOTHROW(visualizer.showNavigationGizmo());
1058 DOCTEST_CHECK_NOTHROW(visualizer.hideNavigationGizmo());
1059 DOCTEST_CHECK_NOTHROW(visualizer.showNavigationGizmo());
1060 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1061 }
1062
1063 SUBCASE("Navigation gizmo with camera movement") {
1064 Visualizer visualizer(200, 200, 16, false, true);
1065 visualizer.disableMessages();
1066
1067 // Add some geometry to visualize
1068 auto sphere_center = make_vec3(0, 0, 0);
1069 auto sphere_radius = 1.0f;
1070 auto sphere_color = make_RGBcolor(1.f, 0.f, 0.f);
1071 auto sphere_uuids = visualizer.addSphereByCenter(sphere_radius, sphere_center, 10, sphere_color, Visualizer::COORDINATES_CARTESIAN);
1072
1073 // Set initial camera position
1074 visualizer.setCameraPosition(make_vec3(3, 3, 3), make_vec3(0, 0, 0));
1075 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1076
1077 // Move camera to a different position
1078 visualizer.setCameraPosition(make_vec3(-3, 3, 3), make_vec3(0, 0, 0));
1079 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1080
1081 // Move camera again
1082 visualizer.setCameraPosition(make_vec3(0, 5, 5), make_vec3(0, 0, 0));
1083 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1084 }
1085
1086 SUBCASE("Navigation gizmo with printWindow") {
1087 Visualizer visualizer(200, 200, 16, false, true);
1088 visualizer.disableMessages();
1089
1090 // Add some geometry
1091 auto sphere_uuids = visualizer.addSphereByCenter(1.0f, make_vec3(0, 0, 0), 10, make_RGBcolor(1.f, 0.f, 0.f), Visualizer::COORDINATES_CARTESIAN);
1092 visualizer.setCameraPosition(make_vec3(3, 3, 3), make_vec3(0, 0, 0));
1093
1094 // Show gizmo and take screenshot
1095 visualizer.showNavigationGizmo();
1096 std::string test_filename = "test_nav_gizmo_screenshot.jpg";
1097 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str()));
1098
1099 // Verify the file was created
1100 DOCTEST_CHECK(std::filesystem::exists(test_filename));
1101
1102 // Clean up test file
1103 if (std::filesystem::exists(test_filename)) {
1104 std::filesystem::remove(test_filename);
1105 }
1106 }
1107
1108 SUBCASE("Navigation gizmo state persists after printWindow") {
1109 Visualizer visualizer(200, 200, 16, false, true);
1110 visualizer.disableMessages();
1111
1112 // Enable gizmo
1113 visualizer.showNavigationGizmo();
1114
1115 // Take screenshot (gizmo should be hidden during screenshot but restored after)
1116 std::string test_filename = "test_nav_gizmo_persist.jpg";
1117 DOCTEST_CHECK_NOTHROW(visualizer.printWindow(test_filename.c_str()));
1118
1119 // Gizmo should still be enabled after screenshot
1120 DOCTEST_CHECK_NOTHROW(visualizer.plotUpdate(true));
1121
1122 // Clean up
1123 if (std::filesystem::exists(test_filename)) {
1124 std::filesystem::remove(test_filename);
1125 }
1126 }
1127}
1128
1129DOCTEST_TEST_CASE("GeometryHandler::getVertices coordinate system transformation") {
1130 // Test that getVertices() returns vertices in the same coordinate space they were provided
1131 // This is a regression test for the bug where COORDINATES_WINDOW_NORMALIZED vertices
1132 // were transformed to OpenGL space [-1,1] but getVertices() didn't apply inverse transformation
1133
1134 Visualizer visualizer(200, 200, 16, false, true);
1135
1136 SUBCASE("COORDINATES_WINDOW_NORMALIZED - getVertices should return original [0,1] coordinates") {
1137 // Create a rectangle with known normalized window coordinates [0,1]
1138 helios::vec3 center = make_vec3(0.852f, 0.1f, 0.011f);
1139 helios::vec2 size = make_vec2(0.02f, 0.025f);
1140
1141 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::red, Visualizer::COORDINATES_WINDOW_NORMALIZED);
1142 DOCTEST_CHECK(rect_id != 0);
1143
1144 // Get vertices back - they should be in the SAME coordinate space [0,1]
1145 auto vertices = visualizer.getGeometryVertices(rect_id);
1146 DOCTEST_CHECK(vertices.size() == 4);
1147
1148 // Calculate expected vertices from center and size
1149 float half_width = size.x * 0.5f;
1150 float half_height = size.y * 0.5f;
1151 helios::vec3 expected_v0 = make_vec3(center.x - half_width, center.y - half_height, center.z);
1152 helios::vec3 expected_v1 = make_vec3(center.x + half_width, center.y - half_height, center.z);
1153 helios::vec3 expected_v2 = make_vec3(center.x + half_width, center.y + half_height, center.z);
1154 helios::vec3 expected_v3 = make_vec3(center.x - half_width, center.y + half_height, center.z);
1155
1156 // Check that returned vertices are in [0,1] range (normalized window coordinates)
1157 float tolerance = 1e-5f;
1158 DOCTEST_CHECK_MESSAGE(std::abs(vertices[0].x - expected_v0.x) < tolerance, "v0.x expected " << expected_v0.x << " but got " << vertices[0].x);
1159 DOCTEST_CHECK_MESSAGE(std::abs(vertices[0].y - expected_v0.y) < tolerance, "v0.y expected " << expected_v0.y << " but got " << vertices[0].y);
1160 DOCTEST_CHECK_MESSAGE(std::abs(vertices[1].x - expected_v1.x) < tolerance, "v1.x expected " << expected_v1.x << " but got " << vertices[1].x);
1161 DOCTEST_CHECK_MESSAGE(std::abs(vertices[1].y - expected_v1.y) < tolerance, "v1.y expected " << expected_v1.y << " but got " << vertices[1].y);
1162
1163 // Verify vertices are actually in [0,1] range, not [-1,1] range
1164 for (const auto &v: vertices) {
1165 bool x_in_range = (v.x >= 0.0f) && (v.x <= 1.0f);
1166 DOCTEST_CHECK_MESSAGE(x_in_range, "Vertex x=" << v.x << " is outside [0,1] range - bug not fixed!");
1167 bool y_in_range = (v.y >= 0.0f) && (v.y <= 1.0f);
1168 DOCTEST_CHECK_MESSAGE(y_in_range, "Vertex y=" << v.y << " is outside [0,1] range - bug not fixed!");
1169 }
1170 }
1171
1172 SUBCASE("COORDINATES_CARTESIAN - getVertices should return original Cartesian coordinates") {
1173 // Create a rectangle with Cartesian coordinates (no transformation should occur)
1174 helios::vec3 center = make_vec3(5.0f, 3.0f, 2.0f);
1175 helios::vec2 size = make_vec2(1.0f, 2.0f);
1176
1177 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::blue, Visualizer::COORDINATES_CARTESIAN);
1178 DOCTEST_CHECK(rect_id != 0);
1179
1180 // Get vertices back - they should be unchanged
1181 auto vertices = visualizer.getGeometryVertices(rect_id);
1182 DOCTEST_CHECK(vertices.size() == 4);
1183
1184 // Calculate expected vertices
1185 float half_width = size.x * 0.5f;
1186 float half_height = size.y * 0.5f;
1187 helios::vec3 expected_v0 = make_vec3(center.x - half_width, center.y - half_height, center.z);
1188
1189 float tolerance = 1e-5f;
1190 DOCTEST_CHECK(std::abs(vertices[0].x - expected_v0.x) < tolerance);
1191 DOCTEST_CHECK(std::abs(vertices[0].y - expected_v0.y) < tolerance);
1192 DOCTEST_CHECK(std::abs(vertices[0].z - expected_v0.z) < tolerance);
1193 }
1194}
1195
1196DOCTEST_TEST_CASE("GeometryHandler::setVertices coordinate system transformation") {
1197 // Test that setVertices() applies the same transformation as addGeometry()
1198
1199 Visualizer visualizer(200, 200, 16, false, true);
1200
1201 SUBCASE("COORDINATES_WINDOW_NORMALIZED - setVertices should transform [0,1] to [-1,1]") {
1202 // Create a rectangle
1203 helios::vec3 center = make_vec3(0.5f, 0.5f, 0.0f);
1204 helios::vec2 size = make_vec2(0.2f, 0.2f);
1205 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::green, Visualizer::COORDINATES_WINDOW_NORMALIZED);
1206
1207 // Get original vertices
1208 auto original_vertices = visualizer.getGeometryVertices(rect_id);
1209
1210 // Modify vertices slightly (still in [0,1] space)
1211 std::vector<helios::vec3> new_vertices = original_vertices;
1212 for (auto &v: new_vertices) {
1213 v.x += 0.1f;
1214 v.y += 0.1f;
1215 }
1216
1217 // Set the modified vertices
1218 DOCTEST_CHECK_NOTHROW(visualizer.setGeometryVertices(rect_id, new_vertices));
1219
1220 // Get vertices back
1221 auto retrieved_vertices = visualizer.getGeometryVertices(rect_id);
1222
1223 // Verify we get back what we set (in the same coordinate space)
1224 float tolerance = 1e-5f;
1225 DOCTEST_CHECK(std::abs(retrieved_vertices[0].x - new_vertices[0].x) < tolerance);
1226 DOCTEST_CHECK(std::abs(retrieved_vertices[0].y - new_vertices[0].y) < tolerance);
1227 }
1228
1229 SUBCASE("COORDINATES_CARTESIAN - setVertices should not transform") {
1230 // Create a Cartesian rectangle
1231 helios::vec3 center = make_vec3(0.0f, 0.0f, 0.0f);
1232 helios::vec2 size = make_vec2(2.0f, 2.0f);
1233 size_t rect_id = visualizer.addRectangleByCenter(center, size, make_SphericalCoord(0, 0), RGB::yellow, Visualizer::COORDINATES_CARTESIAN);
1234
1235 // Get original vertices
1236 auto original_vertices = visualizer.getGeometryVertices(rect_id);
1237
1238 // Modify vertices
1239 std::vector<helios::vec3> new_vertices = original_vertices;
1240 for (auto &v: new_vertices) {
1241 v.x += 1.0f;
1242 v.y += 1.0f;
1243 }
1244
1245 // Set the modified vertices
1246 DOCTEST_CHECK_NOTHROW(visualizer.setGeometryVertices(rect_id, new_vertices));
1247
1248 // Get vertices back
1249 auto retrieved_vertices = visualizer.getGeometryVertices(rect_id);
1250
1251 // Verify we get back what we set
1252 float tolerance = 1e-5f;
1253 DOCTEST_CHECK(std::abs(retrieved_vertices[0].x - new_vertices[0].x) < tolerance);
1254 DOCTEST_CHECK(std::abs(retrieved_vertices[0].y - new_vertices[0].y) < tolerance);
1255 DOCTEST_CHECK(std::abs(retrieved_vertices[0].z - new_vertices[0].z) < tolerance);
1256 }
1257}
1258
1259int Visualizer::selfTest(int argc, char **argv) {
1260 return helios::runDoctestWithValidation(argc, argv);
1261}