1.3.72
 
Loading...
Searching...
No Matches
global.cpp
Go to the documentation of this file.
1
16#include "global.h"
17
18// EXR Libraries (reading and writing OpenEXR images)
19// NOTE: tinyexr must be included before jpeglib because on Windows, tinyexr
20// includes <windows.h> which defines INT32 as int, and libjpeg's jmorecfg.h
21// has guards (#ifndef _BASETSD_H_) to skip its own conflicting typedef.
22#define TINYEXR_USE_MINIZ 0
23#include "zlib.h"
24#define TINYEXR_IMPLEMENTATION
25#include "tinyexr.h"
26
27// PNG Libraries (reading and writing PNG images)
29#define PNG_DEBUG 3
31#define PNG_SKIP_SETJMP_CHECK 1
32#include "png.h"
33
34// JPEG Libraries (reading and writing JPEG images)
35extern "C" {
36#include "jpeglib.h"
37}
38
39using namespace helios;
40
41void helios::helios_runtime_error(const std::string &error_message) {
42#ifdef HELIOS_DEBUG
43 std::cerr << error_message << std::endl;
44#endif
45 throw(std::runtime_error(error_message));
46}
47
48RGBcolor RGB::red = make_RGBcolor(1.f, 0.f, 0.f);
49RGBcolor RGB::blue = make_RGBcolor(0.f, 0.f, 1.f);
50RGBcolor RGB::green = make_RGBcolor(0.f, 0.6f, 0.f);
51RGBcolor RGB::cyan = make_RGBcolor(0.f, 1.f, 1.f);
52RGBcolor RGB::magenta = make_RGBcolor(1.f, 0.f, 1.f);
53RGBcolor RGB::yellow = make_RGBcolor(1.f, 1.f, 0.f);
54RGBcolor RGB::orange = make_RGBcolor(1.f, 0.5f, 0.f);
55RGBcolor RGB::violet = make_RGBcolor(0.5f, 0.f, 0.5f);
56RGBcolor RGB::black = make_RGBcolor(0.f, 0.f, 0.f);
57RGBcolor RGB::white = make_RGBcolor(1.f, 1.f, 1.f);
58RGBcolor RGB::lime = make_RGBcolor(0.f, 1.f, 0.f);
59RGBcolor RGB::silver = make_RGBcolor(0.75f, 0.75f, 0.75f);
60RGBcolor RGB::gray = make_RGBcolor(0.5f, 0.5f, 0.5f);
61RGBcolor RGB::navy = make_RGBcolor(0.f, 0.f, 0.5f);
62RGBcolor RGB::brown = make_RGBcolor(0.55f, 0.27f, 0.075);
63RGBcolor RGB::khaki = make_RGBcolor(0.94f, 0.92f, 0.55f);
64RGBcolor RGB::greenyellow = make_RGBcolor(0.678f, 1.f, 0.184f);
65RGBcolor RGB::forestgreen = make_RGBcolor(0.133f, 0.545f, 0.133f);
66RGBcolor RGB::yellowgreen = make_RGBcolor(0.6, 0.8, 0.2);
67RGBcolor RGB::goldenrod = make_RGBcolor(0.855, 0.647, 0.126);
68
69RGBAcolor RGBA::red = make_RGBAcolor(RGB::red, 1.f);
70RGBAcolor RGBA::blue = make_RGBAcolor(RGB::blue, 1.f);
71RGBAcolor RGBA::green = make_RGBAcolor(RGB::green, 1.f);
72RGBAcolor RGBA::cyan = make_RGBAcolor(RGB::cyan, 1.f);
73RGBAcolor RGBA::magenta = make_RGBAcolor(RGB::magenta, 1.f);
74RGBAcolor RGBA::yellow = make_RGBAcolor(RGB::yellow, 1.f);
75RGBAcolor RGBA::orange = make_RGBAcolor(RGB::orange, 1.f);
76RGBAcolor RGBA::violet = make_RGBAcolor(RGB::violet, 1.f);
77RGBAcolor RGBA::black = make_RGBAcolor(RGB::black, 1.f);
78RGBAcolor RGBA::white = make_RGBAcolor(RGB::white, 1.f);
79RGBAcolor RGBA::lime = make_RGBAcolor(RGB::lime, 1.f);
80RGBAcolor RGBA::silver = make_RGBAcolor(RGB::silver, 1.f);
81RGBAcolor RGBA::gray = make_RGBAcolor(RGB::gray, 1.f);
82RGBAcolor RGBA::navy = make_RGBAcolor(RGB::navy, 1.f);
83RGBAcolor RGBA::brown = make_RGBAcolor(RGB::brown, 1.f);
84RGBAcolor RGBA::khaki = make_RGBAcolor(RGB::khaki, 1.f);
85RGBAcolor RGBA::greenyellow = make_RGBAcolor(RGB::greenyellow, 1.f);
86RGBAcolor RGBA::forestgreen = make_RGBAcolor(RGB::forestgreen, 1.f);
87RGBAcolor RGBA::yellowgreen = make_RGBAcolor(RGB::yellowgreen, 1.f);
88RGBAcolor RGBA::goldenrod = make_RGBAcolor(RGB::goldenrod, 1.f);
89
92
93RGBcolor helios::blend(const RGBcolor &color0, const RGBcolor &color1, float weight) {
94 RGBcolor color_out;
95 weight = clamp(weight, 0.f, 1.f);
96 color_out.r = weight * color1.r + (1.f - weight) * color0.r;
97 color_out.g = weight * color1.g + (1.f - weight) * color0.g;
98 color_out.b = weight * color1.b + (1.f - weight) * color0.b;
99 return color_out;
100}
101
102RGBAcolor helios::blend(const RGBAcolor &color0, const RGBAcolor &color1, float weight) {
103 RGBAcolor color_out;
104 weight = clamp(weight, 0.f, 1.f);
105 color_out.r = weight * color1.r + (1.f - weight) * color0.r;
106 color_out.g = weight * color1.g + (1.f - weight) * color0.g;
107 color_out.b = weight * color1.b + (1.f - weight) * color0.b;
108 color_out.a = weight * color1.a + (1.f - weight) * color0.a;
109 return color_out;
110}
111
112vec3 helios::rotatePoint(const vec3 &position, const SphericalCoord &rotation) {
113 return rotatePoint(position, rotation.elevation, rotation.azimuth);
114}
115
116vec3 helios::rotatePoint(const vec3 &position, float theta, float phi) {
117 if (theta == 0.f && phi == 0.f) {
118 return position;
119 }
120
121 float Ry[3][3], Rz[3][3];
122
123 const float st = sin(theta);
124 const float ct = cos(theta);
125
126 const float sp = sin(phi);
127 const float cp = cos(phi);
128
129 // Setup the rotation matrix, this matrix is based off of the rotation matrix used in glRotatef.
130 Ry[0][0] = ct;
131 Ry[0][1] = 0.f;
132 Ry[0][2] = st;
133 Ry[1][0] = 0.f;
134 Ry[1][1] = 1.f;
135 Ry[1][2] = 0.f;
136 Ry[2][0] = -st;
137 Ry[2][1] = 0.f;
138 Ry[2][2] = ct;
139
140 Rz[0][0] = cp;
141 Rz[0][1] = -sp;
142 Rz[0][2] = 0.f;
143 Rz[1][0] = sp;
144 Rz[1][1] = cp;
145 Rz[1][2] = 0.f;
146 Rz[2][0] = 0.f;
147 Rz[2][1] = 0.f;
148 Rz[2][2] = 1.f;
149
150 // Multiply Ry*Rz
151
152 float rotMat[3][3] = {0.f};
153
154 for (int i = 0; i < 3; i++) {
155 for (int j = 0; j < 3; j++) {
156 for (int k = 0; k < 3; k++) {
157 rotMat[i][j] = rotMat[i][j] + Rz[i][k] * Ry[k][j];
158 }
159 }
160 }
161
162 // Multiply the rotation matrix with the position vector.
163 vec3 tmp;
164 tmp.x = rotMat[0][0] * position.x + rotMat[0][1] * position.y + rotMat[0][2] * position.z;
165 tmp.y = rotMat[1][0] * position.x + rotMat[1][1] * position.y + rotMat[1][2] * position.z;
166 tmp.z = rotMat[2][0] * position.x + rotMat[2][1] * position.y + rotMat[2][2] * position.z;
167
168 return tmp;
169}
170
171vec3 helios::rotatePointAboutLine(const vec3 &point, const vec3 &line_base, const vec3 &line_direction, float theta) {
172 if (theta == 0.f) {
173 return point;
174 }
175
176 // for reference this was taken from http://inside.mines.edu/fs_home/gmurray/ArbitraryAxisRotation/
177
178 vec3 position;
179
180 vec3 tmp = line_direction;
181 float mag = tmp.magnitude();
182 if (mag < 1e-6f) {
183 return point;
184 }
185 tmp = tmp / mag;
186 const float u = tmp.x;
187 const float v = tmp.y;
188 const float w = tmp.z;
189
190 const float a = line_base.x;
191 const float b = line_base.y;
192 const float c = line_base.z;
193
194 const float x = point.x;
195 const float y = point.y;
196 const float z = point.z;
197
198 const float st = sin(theta);
199 const float ct = cos(theta);
200
201 position.x = (a * (v * v + w * w) - u * (b * v + c * w - u * x - v * y - w * z)) * (1 - ct) + x * ct + (-c * v + b * w - w * y + v * z) * st;
202 position.y = (b * (u * u + w * w) - v * (a * u + c * w - u * x - v * y - w * z)) * (1 - ct) + y * ct + (c * u - a * w + w * x - u * z) * st;
203 position.z = (c * (u * u + v * v) - w * (a * u + b * v - u * x - v * y - w * z)) * (1 - ct) + z * ct + (-b * u + a * v - v * x + u * y) * st;
204
205 return position;
206}
207
208float helios::calculateTriangleArea(const vec3 &v0, const vec3 &v1, const vec3 &v2) {
209 vec3 edge1 = v1 - v0;
210 vec3 edge2 = v2 - v0;
211 return 0.5f * cross(edge1, edge2).magnitude();
212}
213
215 int skips_leap[] = {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335};
216 int skips_nonleap[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
217 int *skips;
218
219 if (isLeapYear()) { // leap year
220 skips = skips_leap;
221 } else { // non-leap year
222 skips = skips_nonleap;
223 }
224
225 return skips[month - 1] + day;
226}
227
229 // Compute “Julian day of year” for *this
230 const int jd = Calendar2Julian(*this);
231
232 // 2) Sanity-check jd
233 bool leap = isLeapYear();
234 const int maxJD = leap ? 366 : 365;
235 if (jd < 1 || jd > maxJD) {
236 helios_runtime_error("ERROR (incrementDay): current date out of range (JD=" + std::to_string(jd) + ")");
237 }
238
239 // Advance
240 if (jd < maxJD) {
241 // still inside this year
242 const Date next = Julian2Calendar(jd + 1, year);
243 day = next.day;
244 month = next.month;
245 // year unchanged
246 } else {
247 // rollover to Jan 1 of next year
248 year += 1;
249 month = 1;
250 day = 1;
251 }
252}
253
255 if (year % 400 == 0) {
256 return true; // Divisible by 400: leap year
257 } else if (year % 100 == 0) {
258 return false; // Divisible by 100 but not 400: not a leap year
259 } else if (year % 4 == 0) {
260 return true; // Divisible by 4 but not 100: leap year
261 } else {
262 return false; // Not divisible by 4: not a leap year
263 }
264}
265
267 return float(rand()) / float(RAND_MAX + 1.);
268}
269
270int helios::randu(int imin, int imax) {
271 float ru = randu();
272
273 if (imin == imax || imin > imax) {
274 return imin;
275 } else {
276 return imin + (int) lround(float(imax - imin) * ru);
277 }
278}
279
280float helios::acos_safe(float x) {
281 if (x < -1.0)
282 x = -1.0;
283 else if (x > 1.0)
284 x = 1.0;
285 return acosf(x);
286}
287
288float helios::asin_safe(float x) {
289 if (x < -1.0)
290 x = -1.0;
291 else if (x > 1.0)
292 x = 1.0;
293 return asinf(x);
294}
295
296bool helios::lineIntersection(const vec2 &p1, const vec2 &q1, const vec2 &p2, const vec2 &q2) {
297 constexpr float EPSILON = 1e-9f;
298
299 float ax = q1.x - p1.x; // direction of line a
300 float ay = q1.y - p1.y;
301
302 float bx = p2.x - q2.x; // direction of line b, reversed
303 float by = p2.y - q2.y;
304
305 float dx = p2.x - p1.x; // right-hand side
306 float dy = p2.y - p1.y;
307
308 float det = ax * by - ay * bx;
309
310 if (std::abs(det) < EPSILON) {
311 // Lines are parallel or collinear
312 // Check if they are collinear by testing if p2 lies on line through p1 and q1
313 float cross = dx * ay - dy * ax;
314 if (std::abs(cross) > EPSILON) {
315 return false; // Parallel but not collinear
316 }
317
318 // Lines are collinear - check if segments overlap
319 // Project all points onto the dominant axis to avoid division by zero
320 float dot_aa = ax * ax + ay * ay;
321 if (dot_aa < EPSILON) {
322 // First segment is a point
323 return pointOnSegment(p1, p2, q2);
324 }
325
326 // Project onto the line through p1,q1
327 float t0 = 0.0f; // p1 projection
328 float t1 = 1.0f; // q1 projection
329 float t2 = ((p2.x - p1.x) * ax + (p2.y - p1.y) * ay) / dot_aa; // p2 projection
330 float t3 = ((q2.x - p1.x) * ax + (q2.y - p1.y) * ay) / dot_aa; // q2 projection
331
332 // Ensure t2 <= t3
333 if (t2 > t3) {
334 std::swap(t2, t3);
335 }
336
337 // Check if intervals [t0,t1] and [t2,t3] overlap
338 return !(t1 < t2 || t3 < t0);
339 }
340
341 // Lines are not parallel - check if intersection point lies on both segments
342 float r = (dx * by - dy * bx) / det;
343 float s = (ax * dy - ay * dx) / det;
344
345 return (r >= 0 && r <= 1 && s >= 0 && s <= 1);
346}
347
348bool helios::pointOnSegment(const vec2 &point, const vec2 &seg_start, const vec2 &seg_end) {
349 constexpr float EPSILON = 1e-9f;
350
351 // Check if point is collinear with segment
352 float cross = (point.y - seg_start.y) * (seg_end.x - seg_start.x) - (point.x - seg_start.x) * (seg_end.y - seg_start.y);
353 if (std::abs(cross) > EPSILON) {
354 return false;
355 }
356
357 // Check if point is within segment bounds
358 float min_x = std::min(seg_start.x, seg_end.x);
359 float max_x = std::max(seg_start.x, seg_end.x);
360 float min_y = std::min(seg_start.y, seg_end.y);
361 float max_y = std::max(seg_start.y, seg_end.y);
362
363 return (point.x >= min_x - EPSILON && point.x <= max_x + EPSILON && point.y >= min_y - EPSILON && point.y <= max_y + EPSILON);
364}
365
366bool helios::pointInPolygon(const vec2 &p, const std::vector<vec2> &poly) {
367 constexpr float EPS = 1e-6f;
368 const std::size_t n = poly.size();
369 if (n < 3) {
370 return false;
371 }
372
373 /* vertex coincidence */
374 for (const vec2 &v: poly) {
375 if (std::abs(p.x - v.x) < EPS && std::abs(p.y - v.y) < EPS) {
376 return true; // vertex counts as inside
377 }
378 }
379
380 /* ray–edge crossings */
381 int crossings = 0;
382 for (std::size_t i = 0; i < n; ++i) {
383 const vec2 &a = poly[i];
384 const vec2 &b = poly[(i + 1) % n];
385
386 if ((a.y > p.y) != (b.y > p.y)) {
387
388 float x_hit = a.x + (p.y - a.y) * (b.x - a.x) / (b.y - a.y);
389
390 if (x_hit >= p.x - EPS) {
391 ++crossings;
392 }
393 }
394 }
395
396 return (crossings & 1) == 1;
397}
398
399helios::ProgressBar::ProgressBar(size_t total, int width, bool enable, const std::string &progress_message) : total_steps(total), current_step(0), bar_width(width), enabled(enable), message(progress_message) {
400}
401
403 current_step++;
404 double progress = (double) current_step / total_steps;
405
406 if (enabled) {
407 int filled = (int) (progress * bar_width);
408
409 std::cout << "\r" << message << ": [";
410 for (int i = 0; i < bar_width; ++i) {
411 if (i < filled)
412 std::cout << "=";
413 else if (i == filled)
414 std::cout << ">";
415 else
416 std::cout << " ";
417 }
418 std::cout << "] " << (int) (progress * 100) << "% (" << current_step << "/" << total_steps << ")";
419 std::cout.flush();
420
421 if (current_step >= total_steps) {
422 std::cout << std::endl;
423 }
424 }
425
426 if (callback) {
427 callback(static_cast<float>(progress), message);
428 }
429}
430
431void helios::ProgressBar::update(size_t step_number) {
432 current_step = step_number;
433 if (current_step > total_steps) {
434 current_step = total_steps;
435 }
436
437 double progress = (double) current_step / total_steps;
438
439 if (enabled) {
440 int filled = (int) (progress * bar_width);
441
442 std::cout << "\r" << message << ": [";
443 for (int i = 0; i < bar_width; ++i) {
444 if (i < filled)
445 std::cout << "=";
446 else if (i == filled)
447 std::cout << ">";
448 else
449 std::cout << " ";
450 }
451 std::cout << "] " << (int) (progress * 100) << "% (" << current_step << "/" << total_steps << ")";
452 std::cout.flush();
453
454 if (current_step >= total_steps) {
455 std::cout << std::endl;
456 }
457 }
458
459 if (callback) {
460 callback(static_cast<float>(progress), message);
461 }
462}
463
465 if (enabled && current_step < total_steps) {
466 current_step = total_steps;
467 update(current_step);
468 }
469}
470
472 enabled = enable;
473}
474
476 return enabled;
477}
478
479void helios::ProgressBar::setCallback(std::function<void(float, const std::string&)> cb) {
480 callback = std::move(cb);
481}
482
483void helios::wait(float seconds) {
484 int msec = (int) lround(seconds * 1000.f);
485 std::this_thread::sleep_for(std::chrono::milliseconds(msec));
486}
487
488void helios::makeRotationMatrix(const float rotation, const char *axis, float (&T)[16]) {
489 float sx = sin(rotation);
490 float cx = cos(rotation);
491
492 if (strcmp(axis, "x") == 0) {
493 T[0] = 1.f; //(0,0)
494 T[1] = 0.f; //(0,1)
495 T[2] = 0.f; //(0,2)
496 T[3] = 0.f; //(0,3)
497 T[4] = 0.f; //(1,0)
498 T[5] = cx; //(1,1)
499 T[6] = -sx; //(1,2)
500 T[7] = 0.f; //(1,3)
501 T[8] = 0.f; //(2,0)
502 T[9] = sx; //(2,1)
503 T[10] = cx; //(2,2)
504 T[11] = 0.f; //(2,3)
505 } else if (strcmp(axis, "y") == 0) {
506 T[0] = cx; //(0,0)
507 T[1] = 0.f; //(0,1)
508 T[2] = sx; //(0,2)
509 T[3] = 0.f; //(0,3)
510 T[4] = 0.f; //(1,0)
511 T[5] = 1.f; //(1,1)
512 T[6] = 0.f; //(1,2)
513 T[7] = 0.f; //(1,3)
514 T[8] = -sx; //(2,0)
515 T[9] = 0.f; //(2,1)
516 T[10] = cx; //(2,2)
517 T[11] = 0.f; //(2,3)
518 } else if (strcmp(axis, "z") == 0) {
519 T[0] = cx; //(0,0)
520 T[1] = -sx; //(0,1)
521 T[2] = 0.f; //(0,2)
522 T[3] = 0.f; //(0,3)
523 T[4] = sx; //(1,0)
524 T[5] = cx; //(1,1)
525 T[6] = 0.f; //(1,2)
526 T[7] = 0.f; //(1,3)
527 T[8] = 0.f; //(2,0)
528 T[9] = 0.f; //(2,1)
529 T[10] = 1.f; //(2,2)
530 T[11] = 0.f; //(2,3)
531 } else {
532 helios_runtime_error("ERROR (makeRotationMatrix): Rotation axis should be one of x, y, or z.");
533 }
534 T[12] = T[13] = T[14] = 0.f;
535 T[15] = 1.f;
536}
537
538void helios::makeRotationMatrix(float rotation, const helios::vec3 &axis, float (&T)[16]) {
539 vec3 u = axis;
540 u.normalize();
541
542 float sx = sin(rotation);
543 float cx = cos(rotation);
544
545 T[0] = cx + u.x * u.x * (1.f - cx); //(0,0)
546 T[1] = u.x * u.y * (1.f - cx) - u.z * sx; //(0,1)
547 T[2] = u.x * u.z * (1.f - cx) + u.y * sx; //(0,2)
548 T[3] = 0.f; //(0,3)
549 T[4] = u.y * u.x * (1.f - cx) + u.z * sx; //(1,0)
550 T[5] = cx + u.y * u.y * (1.f - cx); //(1,1)
551 T[6] = u.y * u.z * (1.f - cx) - u.x * sx; //(1,2)
552 T[7] = 0.f; //(1,3)
553 T[8] = u.z * u.x * (1.f - cx) - u.y * sx; //(2,0)
554 T[9] = u.z * u.y * (1.f - cx) + u.x * sx; //(2,1)
555 T[10] = cx + u.z * u.z * (1.f - cx); //(2,2)
556 T[11] = 0.f; //(2,3)
557
558 T[12] = T[13] = T[14] = 0.f;
559 T[15] = 1.f;
560}
561
562void helios::makeRotationMatrix(float rotation, const helios::vec3 &origin, const helios::vec3 &axis, float (&T)[16]) {
563 // Construct inverse translation matrix to translate back to the origin
564 float Ttrans[16];
565 makeIdentityMatrix(Ttrans);
566
567 Ttrans[3] = -origin.x; //(0,3)
568 Ttrans[7] = -origin.y; //(1,3)
569 Ttrans[11] = -origin.z; //(2,3)
570
571 // Construct rotation matrix
572 vec3 u = axis;
573 u.normalize();
574
575 float sx = sin(rotation);
576 float cx = cos(rotation);
577
578 float Trot[16];
579 makeIdentityMatrix(Trot);
580
581 Trot[0] = cx + u.x * u.x * (1.f - cx); //(0,0)
582 Trot[1] = u.x * u.y * (1.f - cx) - u.z * sx; //(0,1)
583 Trot[2] = u.x * u.z * (1.f - cx) + u.y * sx; //(0,2)
584 Trot[3] = 0.f; //(0,3)
585 Trot[4] = u.y * u.x * (1.f - cx) + u.z * sx; //(1,0)
586 Trot[5] = cx + u.y * u.y * (1.f - cx); //(1,1)
587 Trot[6] = u.y * u.z * (1.f - cx) - u.x * sx; //(1,2)
588 Trot[7] = 0.f; //(1,3)
589 Trot[8] = u.z * u.x * (1.f - cx) - u.y * sx; //(2,0)
590 Trot[9] = u.z * u.y * (1.f - cx) + u.x * sx; //(2,1)
591 Trot[10] = cx + u.z * u.z * (1.f - cx); //(2,2)
592 Trot[11] = 0.f; //(2,3)
593
594 // Multiply first two matrices and store in 'T'
595 matmult(Trot, Ttrans, T);
596
597 // Construct transformation matrix to translate back to 'origin'
598 Ttrans[3] = origin.x; //(0,3)
599 Ttrans[7] = origin.y; //(1,3)
600 Ttrans[11] = origin.z; //(2,3)
601
602 matmult(Ttrans, T, T);
603}
604
605void helios::makeTranslationMatrix(const helios::vec3 &translation, float (&T)[16]) {
606 T[0] = 1.f; //(0,0)
607 T[1] = 0.f; //(0,1)
608 T[2] = 0.f; //(0,2)
609 T[3] = translation.x; //(0,3)
610 T[4] = 0.f; //(1,0)
611 T[5] = 1.f; //(1,1)
612 T[6] = 0.f; //(1,2)
613 T[7] = translation.y; //(1,3)
614 T[8] = 0.f; //(2,0)
615 T[9] = 0.f; //(2,1)
616 T[10] = 1.f; //(2,2)
617 T[11] = translation.z; //(2,3)
618 T[12] = 0.f; //(3,0)
619 T[13] = 0.f; //(3,1)
620 T[14] = 0.f; //(3,2)
621 T[15] = 1.f; //(3,3)
622}
623
624void helios::makeScaleMatrix(const helios::vec3 &scale, float (&transform)[16]) {
625 transform[0] = scale.x; //(0,0)
626 transform[1] = 0.f; //(0,1)
627 transform[2] = 0.f; //(0,2)
628 transform[3] = 0.f; //(0,3)
629 transform[4] = 0.f; //(1,0)
630 transform[5] = scale.y; //(1,1)
631 transform[6] = 0.f; //(1,2)
632 transform[7] = 0.f; //(1,3)
633 transform[8] = 0.f; //(2,0)
634 transform[9] = 0.f; //(2,1)
635 transform[10] = scale.z; //(2,2)
636 transform[11] = 0.f; //(2,3)
637 transform[12] = 0.f; //(3,0)
638 transform[13] = 0.f; //(3,1)
639 transform[14] = 0.f; //(3,2)
640 transform[15] = 1.f; //(3,3)
641}
642
643void helios::makeScaleMatrix(const helios::vec3 &scale, const helios::vec3 &point, float (&transform)[16]) {
644 transform[0] = scale.x; //(0,0)
645 transform[1] = 0.f; //(0,1)
646 transform[2] = 0.f; //(0,2)
647 transform[3] = point.x * (1 - scale.x); //(0,3)
648 transform[4] = 0.f; //(1,0)
649 transform[5] = scale.y; //(1,1)
650 transform[6] = 0.f; //(1,2)
651 transform[7] = point.y * (1 - scale.y); //(1,3)
652 transform[8] = 0.f; //(2,0)
653 transform[9] = 0.f; //(2,1)
654 transform[10] = scale.z; //(2,2)
655 transform[11] = point.z * (1 - scale.z); //(2,3)
656 transform[12] = 0.f; //(3,0)
657 transform[13] = 0.f; //(3,1)
658 transform[14] = 0.f; //(3,2)
659 transform[15] = 1.f; //(3,3)
660}
661
662void helios::matmult(const float ML[16], const float MR[16], float (&T)[16]) {
663 float M[16] = {0.f};
664
665 for (int i = 0; i < 4; i++) {
666 for (int j = 0; j < 4; j++) {
667 for (int k = 0; k < 4; k++) {
668 M[4 * i + j] = M[4 * i + j] + ML[4 * i + k] * MR[4 * k + j];
669 }
670 }
671 }
672
673 for (int i = 0; i < 16; i++) {
674 T[i] = M[i];
675 }
676}
677
678void helios::vecmult(const float M[16], const helios::vec3 &v3, helios::vec3 &result) {
679 float v[4] = {v3.x, v3.y, v3.z, 1.f};
680
681 float V[4] = {0.f};
682
683 for (int i = 0; i < 4; ++i) {
684 for (int k = 0; k < 4; ++k) {
685 V[i] += M[4 * i + k] * v[k];
686 }
687 }
688
689 result.x = V[0];
690 result.y = V[1];
691 result.z = V[2];
692}
693
694void helios::vecmult(const float M[16], const float v[3], float (&result)[3]) {
695 float V[4] = {0.f};
696 float v4[4] = {v[0], v[1], v[2], 1.f};
697
698 for (int j = 0; j < 4; j++) {
699 for (int k = 0; k < 4; k++) {
700 V[j] = V[j] + v4[k] * M[k + 4 * j];
701 }
702 }
703
704 for (int i = 0; i < 3; i++) {
705 result[i] = V[i];
706 }
707}
708
709void helios::makeIdentityMatrix(float (&T)[16]) {
710 /* [0,0] */
711 T[0] = 1.f;
712 /* [0,1] */
713 T[1] = 0.f;
714 /* [0,2] */
715 T[2] = 0.f;
716 /* [0,3] */
717 T[3] = 0.f;
718 /* [1,0] */
719 T[4] = 0.f;
720 /* [1,1] */
721 T[5] = 1.f;
722 /* [1,2] */
723 T[6] = 0.f;
724 /* [1,3] */
725 T[7] = 0.f;
726 /* [2,0] */
727 T[8] = 0.f;
728 /* [2,1] */
729 T[9] = 0.f;
730 /* [2,2] */
731 T[10] = 1.f;
732 /* [2,3] */
733 T[11] = 0.f;
734 /* [3,0] */
735 T[12] = 0.f;
736 /* [3,1] */
737 T[13] = 0.f;
738 /* [3,2] */
739 T[14] = 0.f;
740 /* [3,3] */
741 T[15] = 1.f;
742}
743
744float helios::deg2rad(float deg) {
745 return deg * float(M_PI) / 180.f;
746}
747
748float helios::rad2deg(float rad) {
749 return rad * 180.f / float(M_PI);
750}
751
752float helios::atan2_2pi(float y, float x) {
753 float v = 0;
754
755 if (x > 0.f) {
756 v = atanf(y / x);
757 }
758 if (y >= 0.f && x < 0.f) {
759 v = float(M_PI) + atanf(y / x);
760 }
761 if (y < 0.f && x < 0.f) {
762 v = -float(M_PI) + atanf(y / x);
763 }
764 if (y > 0.f && x == 0.f) {
765 v = 0.5f * float(M_PI);
766 }
767 if (y < 0.f && x == 0.f) {
768 v = -0.5f * float(M_PI);
769 }
770 if (v < 0.f) {
771 v = v + 2.f * float(M_PI);
772 }
773 return v;
774}
775
777 float radius = sqrtf(Cartesian.x * Cartesian.x + Cartesian.y * Cartesian.y + Cartesian.z * Cartesian.z);
778
779 // Add small epsilon to prevent singularity when vector is exactly vertical (x=0, y=0)
780 // This prevents gimbal lock for cameras pointing straight up/down
781 // Use positive y offset so atan2(0, eps) gives azimuth = 0 (pointing in +y direction)
782 float x_safe = Cartesian.x;
783 float y_safe = Cartesian.y;
784 if (fabsf(x_safe) < 1e-7f && fabsf(y_safe) < 1e-7f) {
785 y_safe = 1e-7f; // Positive offset gives azimuth = 0 (+y direction)
786 }
787
788 return {radius, asin_safe(Cartesian.z / radius), atan2_2pi(x_safe, y_safe)};
789}
790
792 return {Spherical.radius * cosf(Spherical.elevation) * sinf(Spherical.azimuth), Spherical.radius * cosf(Spherical.elevation) * cosf(Spherical.azimuth), Spherical.radius * sinf(Spherical.elevation)};
793}
794
795vec2 helios::string2vec2(const char *str) {
796 float o[2];
797 std::string tmp;
798
799 std::istringstream stream(str);
800 int c = 0;
801 while (stream >> tmp && c < 2) {
802 if (!parse_float(tmp, o[c])) {
803 helios_runtime_error("ERROR (string2vec2): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
804 }
805 c++;
806 }
807
808 if (c < 2) {
809 helios_runtime_error("ERROR (string2vec2): Insufficient values in input string '" + std::string(str) + "'. Expected 2 values, got " + std::to_string(c));
810 }
811
812 return make_vec2(o[0], o[1]);
813}
814
815vec3 helios::string2vec3(const char *str) {
816 float o[3];
817 std::string tmp;
818
819 std::istringstream stream(str);
820 int c = 0;
821 while (stream >> tmp && c < 3) {
822 if (!parse_float(tmp, o[c])) {
823 helios_runtime_error("ERROR (string2vec3): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
824 }
825 c++;
826 }
827
828 if (c < 3) {
829 helios_runtime_error("ERROR (string2vec3): Insufficient values in input string '" + std::string(str) + "'. Expected 3 values, got " + std::to_string(c));
830 }
831
832 return make_vec3(o[0], o[1], o[2]);
833}
834
835vec4 helios::string2vec4(const char *str) {
836 float o[4];
837 std::string tmp;
838
839 std::istringstream stream(str);
840 int c = 0;
841 while (stream >> tmp && c < 4) {
842 if (!parse_float(tmp, o[c])) {
843 helios_runtime_error("ERROR (string2vec4): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
844 }
845 c++;
846 }
847
848 if (c < 4) {
849 helios_runtime_error("ERROR (string2vec4): Insufficient values in input string '" + std::string(str) + "'. Expected 4 values, got " + std::to_string(c));
850 }
851
852 return make_vec4(o[0], o[1], o[2], o[3]);
853}
854
855int2 helios::string2int2(const char *str) {
856 int o[2];
857 std::string tmp;
858
859 std::istringstream stream(str);
860 int c = 0;
861 while (stream >> tmp && c < 2) {
862 if (!parse_int(tmp, o[c])) {
863 helios_runtime_error("ERROR (string2int2): Invalid int value '" + tmp + "' in input string '" + std::string(str) + "'");
864 }
865 c++;
866 }
867
868 if (c < 2) {
869 helios_runtime_error("ERROR (string2int2): Insufficient values in input string '" + std::string(str) + "'. Expected 2 values, got " + std::to_string(c));
870 }
871
872 return make_int2(o[0], o[1]);
873}
874
875int3 helios::string2int3(const char *str) {
876 int o[3];
877 std::string tmp;
878
879 std::istringstream stream(str);
880 int c = 0;
881 while (stream >> tmp && c < 3) {
882 if (!parse_int(tmp, o[c])) {
883 helios_runtime_error("ERROR (string2int3): Invalid int value '" + tmp + "' in input string '" + std::string(str) + "'");
884 }
885 c++;
886 }
887
888 if (c < 3) {
889 helios_runtime_error("ERROR (string2int3): Insufficient values in input string '" + std::string(str) + "'. Expected 3 values, got " + std::to_string(c));
890 }
891
892 return make_int3(o[0], o[1], o[2]);
893}
894
895int4 helios::string2int4(const char *str) {
896 int o[4];
897 std::string tmp;
898
899 std::istringstream stream(str);
900 int c = 0;
901 while (stream >> tmp && c < 4) {
902 if (!parse_int(tmp, o[c])) {
903 helios_runtime_error("ERROR (string2int4): Invalid int value '" + tmp + "' in input string '" + std::string(str) + "'");
904 }
905 c++;
906 }
907
908 if (c < 4) {
909 helios_runtime_error("ERROR (string2int4): Insufficient values in input string '" + std::string(str) + "'. Expected 4 values, got " + std::to_string(c));
910 }
911
912 return make_int4(o[0], o[1], o[2], o[3]);
913}
914
916 float o[4] = {0, 0, 0, 1}; // Keep default alpha of 1
917 std::string tmp;
918
919 std::istringstream stream(str);
920 int c = 0;
921 while (stream >> tmp) {
922 if (c >= 4) {
923 helios_runtime_error("ERROR (string2RGBcolor): Too many values in input string '" + std::string(str) + "'. Expected at most 4 values (RGBA), but found additional value '" + tmp + "'");
924 }
925
926 if (!parse_float(tmp, o[c])) {
927 helios_runtime_error("ERROR (string2RGBcolor): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
928 }
929 c++;
930 }
931
932 if (c < 3) {
933 helios_runtime_error("ERROR (string2RGBcolor): Insufficient values in input string '" + std::string(str) + "'. Expected at least 3 values (RGB), got " + std::to_string(c));
934 }
935
936 return make_RGBAcolor(o[0], o[1], o[2], o[3]);
937}
938
939bool helios::parse_float(const std::string &input_string, float &converted_float) {
940 try {
941 size_t read = 0;
942 std::string str = trim_whitespace(input_string);
943 double converted_double = std::stod(str, &read);
944 converted_float = (float) converted_double;
945 if (str.size() != read)
946 return false;
947 } catch (std::invalid_argument &e) {
948 return false;
949 }
950 return true;
951}
952
953bool helios::parse_double(const std::string &input_string, double &converted_double) {
954 try {
955 size_t read = 0;
956 std::string str = trim_whitespace(input_string);
957 converted_double = std::stod(str, &read);
958 if (str.size() != read)
959 return false;
960 } catch (std::invalid_argument &e) {
961 return false;
962 }
963 return true;
964}
965
966bool helios::parse_int(const std::string &input_string, int &converted_int) {
967 try {
968 size_t read = 0;
969 std::string str = trim_whitespace(input_string);
970 converted_int = std::stoi(str, &read);
971 if (str.size() != read)
972 return false;
973 } catch (std::invalid_argument &e) {
974 return false;
975 }
976 return true;
977}
978
979bool helios::parse_int2(const std::string &input_string, int2 &converted_int2) {
980 std::istringstream vecstream(input_string);
981 std::vector<std::string> tmp_s(2);
982 vecstream >> tmp_s[0];
983 vecstream >> tmp_s[1];
984 int2 tmp;
985 if (!parse_int(tmp_s[0], tmp.x) || !parse_int(tmp_s[1], tmp.y)) {
986 return false;
987 } else {
988 converted_int2 = tmp;
989 }
990 return true;
991}
992
993bool helios::parse_int3(const std::string &input_string, int3 &converted_int3) {
994 std::istringstream vecstream(input_string);
995 std::vector<std::string> tmp_s(3);
996 vecstream >> tmp_s[0];
997 vecstream >> tmp_s[1];
998 vecstream >> tmp_s[2];
999 int3 tmp;
1000 if (!parse_int(tmp_s[0], tmp.x) || !parse_int(tmp_s[1], tmp.y) || !parse_int(tmp_s[2], tmp.z)) {
1001 return false;
1002 } else {
1003 converted_int3 = tmp;
1004 }
1005 return true;
1006}
1007
1008bool helios::parse_uint(const std::string &input_string, uint &converted_uint) {
1009 try {
1010 size_t read = 0;
1011 std::string str = trim_whitespace(input_string);
1012 int converted_int = std::stoi(str, &read);
1013 if (str.size() != read || converted_int < 0) {
1014 return false;
1015 } else {
1016 converted_uint = (uint) converted_int;
1017 }
1018 } catch (std::invalid_argument &e) {
1019 return false;
1020 }
1021 return true;
1022}
1023
1024bool helios::parse_vec2(const std::string &input_string, vec2 &converted_vec2) {
1025 std::istringstream vecstream(input_string);
1026 std::vector<std::string> tmp_s(2);
1027 vecstream >> tmp_s[0];
1028 vecstream >> tmp_s[1];
1029 vec2 tmp;
1030 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y)) {
1031 return false;
1032 } else {
1033 converted_vec2 = tmp;
1034 }
1035 return true;
1036}
1037
1038bool helios::parse_vec3(const std::string &input_string, vec3 &converted_vec3) {
1039 std::istringstream vecstream(input_string);
1040 std::vector<std::string> tmp_s(3);
1041 vecstream >> tmp_s[0];
1042 vecstream >> tmp_s[1];
1043 vecstream >> tmp_s[2];
1044 vec3 tmp;
1045 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y) || !parse_float(tmp_s[2], tmp.z)) {
1046 return false;
1047 } else {
1048 converted_vec3 = tmp;
1049 }
1050 return true;
1051}
1052
1053bool helios::parse_RGBcolor(const std::string &input_string, RGBcolor &converted_rgb) {
1054 std::istringstream vecstream(input_string);
1055 std::vector<std::string> tmp_s(3);
1056 vecstream >> tmp_s[0];
1057 vecstream >> tmp_s[1];
1058 vecstream >> tmp_s[2];
1059 RGBcolor tmp;
1060 if (!parse_float(tmp_s[0], tmp.r) || !parse_float(tmp_s[1], tmp.g) || !parse_float(tmp_s[2], tmp.b)) {
1061 return false;
1062 } else {
1063 if (tmp.r < 0 || tmp.g < 0 || tmp.b < 0 || tmp.r > 1.f || tmp.g > 1.f || tmp.b > 1.f) {
1064 return false;
1065 }
1066 converted_rgb = tmp;
1067 }
1068 return true;
1069}
1070
1071bool helios::open_xml_file(const std::string &xml_file, pugi::xml_document &xmldoc, std::string &error_string) {
1072 const std::string &fn = xml_file;
1073 std::string ext = getFileExtension(xml_file);
1074 if (ext != ".xml" && ext != ".XML") {
1075 error_string = "XML file " + fn + " is not XML format.";
1076 return false;
1077 }
1078
1079 // Resolve file path using the build directory path resolution system
1080 std::filesystem::path resolvedPath;
1081 try {
1082 resolvedPath = resolveFilePath(xml_file);
1083 } catch (const std::runtime_error &e) {
1084 error_string = std::string(e.what());
1085 return false;
1086 }
1087
1088 // load file
1089 pugi::xml_parse_result load_result = xmldoc.load_file(resolvedPath.string().c_str());
1090
1091 // error checking
1092 if (!load_result) {
1093 error_string = "XML file " + xml_file + " parsed with errors: " + load_result.description();
1094 return false;
1095 }
1096
1097 pugi::xml_node helios = xmldoc.child("helios");
1098
1099 if (helios.empty()) {
1100 error_string = "XML file " + xml_file + " does not have tag '<helios> ... </helios>' bounding all other tags.";
1101 return false;
1102 }
1103
1104 return true;
1105}
1106
1107int helios::parse_xml_tag_int(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1108 std::string value_string = node.child_value();
1109 if (value_string.empty()) {
1110 return 0;
1111 }
1112 int value;
1113 if (!parse_int(value_string, value)) {
1114 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' integer value.");
1115 }
1116 return value;
1117}
1118
1119float helios::parse_xml_tag_float(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1120 std::string value_string = node.child_value();
1121 if (value_string.empty()) {
1122 return 0;
1123 }
1124 float value;
1125 if (!parse_float(value_string, value)) {
1126 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' float value.");
1127 }
1128 return value;
1129}
1130
1131vec2 helios::parse_xml_tag_vec2(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1132 std::string value_string = node.child_value();
1133 if (value_string.empty()) {
1134 return {0, 0};
1135 }
1136 vec2 value;
1137 if (!parse_vec2(value_string, value)) {
1138 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' vec2 value.");
1139 }
1140 return value;
1141}
1142
1143vec3 helios::parse_xml_tag_vec3(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1144 std::string value_string = node.child_value();
1145 if (value_string.empty()) {
1146 return {0, 0, 0};
1147 }
1148 vec3 value;
1149 if (!parse_vec3(value_string, value)) {
1150 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' vec3 value.");
1151 }
1152 return value;
1153}
1154
1155std::string helios::parse_xml_tag_string(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1156 return deblank(node.child_value());
1157}
1158
1159std::string helios::deblank(const char *input) {
1160 std::string out;
1161 out.reserve(std::strlen(input));
1162 for (const char *p = input; *p; ++p) {
1163 if (*p != ' ') {
1164 out.push_back(*p);
1165 }
1166 }
1167 return out;
1168}
1169
1170std::string helios::deblank(const std::string &input) {
1171 return deblank(input.c_str());
1172}
1173
1174std::string helios::trim_whitespace(const std::string &input) {
1175 static const std::string WHITESPACE = " \n\r\t\f\v";
1176
1177 // Find first non-whitespace character
1178 size_t start = input.find_first_not_of(WHITESPACE);
1179 if (start == std::string::npos) {
1180 return ""; // String is all whitespace
1181 }
1182
1183 // Find last non-whitespace character
1184 size_t end = input.find_last_not_of(WHITESPACE);
1185
1186 // Return the trimmed substring
1187 return input.substr(start, end - start + 1);
1188}
1189
1190std::vector<std::string> helios::separate_string_by_delimiter(const std::string &inputstring, const std::string &delimiter) {
1191 std::vector<std::string> separated_string;
1192
1193 // Handle empty delimiter case
1194 if (delimiter.empty()) {
1195 helios_runtime_error("ERROR (helios::separate_string_by_delimiter): Delimiter cannot be an empty string.");
1196 }
1197
1198 size_t pos = 0;
1199 size_t found;
1200 size_t max_characters = inputstring.size();
1201 size_t iter = 0;
1202 while ((found = inputstring.find(delimiter, pos)) != std::string::npos && iter <= max_characters) {
1203 separated_string.push_back(trim_whitespace(inputstring.substr(pos, found - pos)));
1204 pos = found + delimiter.size();
1205 iter++;
1206 }
1207
1208 // add the remaining part (including case of no delimiter found)
1209 separated_string.push_back(trim_whitespace(inputstring.substr(pos)));
1210
1211 return separated_string;
1212}
1213
1214float helios::sum(const std::vector<float> &vect) {
1215 if (vect.empty()) {
1216 helios_runtime_error("ERROR (sum): Vector is empty.");
1217 }
1218
1219 float m = 0;
1220 for (float i: vect) {
1221 m += i;
1222 }
1223
1224 return m;
1225}
1226
1227float helios::mean(const std::vector<float> &vect) {
1228 if (vect.empty()) {
1229 helios_runtime_error("ERROR (mean): Vector is empty.");
1230 }
1231
1232 float m = 0;
1233 for (float i: vect) {
1234 m += i;
1235 }
1236 m /= float(vect.size());
1237
1238 return m;
1239}
1240
1241float helios::min(const std::vector<float> &vect) {
1242 if (vect.empty()) {
1243 helios_runtime_error("ERROR (min): Vector is empty.");
1244 }
1245
1246 return *std::min_element(vect.begin(), vect.end());
1247}
1248
1249int helios::min(const std::vector<int> &vect) {
1250 if (vect.empty()) {
1251 helios_runtime_error("ERROR (min): Vector is empty.");
1252 }
1253
1254 return *std::min_element(vect.begin(), vect.end());
1255}
1256
1257vec3 helios::min(const std::vector<vec3> &vect) {
1258 if (vect.empty()) {
1259 helios_runtime_error("ERROR (min): Vector is empty.");
1260 }
1261
1262 vec3 vmin = vect.at(0);
1263
1264 for (int i = 1; i < vect.size(); i++) {
1265 if (vect.at(i).x < vmin.x) {
1266 vmin.x = vect.at(i).x;
1267 }
1268 if (vect.at(i).y < vmin.y) {
1269 vmin.y = vect.at(i).y;
1270 }
1271 if (vect.at(i).z < vmin.z) {
1272 vmin.z = vect.at(i).z;
1273 }
1274 }
1275
1276 return vmin;
1277}
1278
1279float helios::max(const std::vector<float> &vect) {
1280 if (vect.empty()) {
1281 helios_runtime_error("ERROR (max): Vector is empty.");
1282 }
1283
1284 return *std::max_element(vect.begin(), vect.end());
1285}
1286
1287int helios::max(const std::vector<int> &vect) {
1288 if (vect.empty()) {
1289 helios_runtime_error("ERROR (max): Vector is empty.");
1290 }
1291
1292 return *std::max_element(vect.begin(), vect.end());
1293}
1294
1295vec3 helios::max(const std::vector<vec3> &vect) {
1296 if (vect.empty()) {
1297 helios_runtime_error("ERROR (max): Vector is empty.");
1298 }
1299
1300 vec3 vmax = vect.at(0);
1301
1302 for (int i = 1; i < vect.size(); i++) {
1303 if (vect.at(i).x > vmax.x) {
1304 vmax.x = vect.at(i).x;
1305 }
1306 if (vect.at(i).y > vmax.y) {
1307 vmax.y = vect.at(i).y;
1308 }
1309 if (vect.at(i).z > vmax.z) {
1310 vmax.z = vect.at(i).z;
1311 }
1312 }
1313
1314 return vmax;
1315}
1316
1317float helios::stdev(const std::vector<float> &vect) {
1318 if (vect.empty()) {
1319 helios_runtime_error("ERROR (stdev): Vector is empty.");
1320 }
1321
1322 size_t size = vect.size();
1323
1324 float m = 0;
1325 for (float i: vect) {
1326 m += i;
1327 }
1328 m /= float(size);
1329
1330 float stdev = 0;
1331 for (float i: vect) {
1332 stdev += powf(i - m, 2.0);
1333 }
1334
1335 return sqrtf(stdev / float(size));
1336}
1337
1338float helios::median(std::vector<float> vect) {
1339 if (vect.empty()) {
1340 helios_runtime_error("ERROR (median): Vector is empty.");
1341 }
1342
1343 size_t size = vect.size();
1344
1345 sort(vect.begin(), vect.end());
1346
1347 size_t middle_index = size / 2;
1348
1349 float median;
1350 if (size % 2 == 0) {
1351 median = (vect.at(middle_index) + vect.at(middle_index - 1)) / 2.f;
1352 } else {
1353 median = vect.at(middle_index);
1354 }
1355 return median;
1356}
1357
1358Date helios::CalendarDay(int Julian_day, int year) {
1359 // ----------------------------- input checks ----------------------------
1360 if (Julian_day < 1 || Julian_day > 366)
1361 helios_runtime_error("ERROR (CalendarDay): Julian day out of range [1–366].");
1362
1363 if (year < 1000)
1364 helios_runtime_error("ERROR (CalendarDay): Year must be given in YYYY format.");
1365
1366 const bool leap = (year % 4 == 0 && year % 100 != 0) || // divisible by 4 but not by 100
1367 (year % 400 == 0); // or divisible by 400
1368
1369 if (!leap && Julian_day == 366)
1370 helios_runtime_error("ERROR (CalendarDay): Day 366 occurs only in leap years.");
1371
1372 // ------------------- month lengths for the chosen year -----------------
1373 // Index 0 = January, …, 11 = December
1374 int month_lengths[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1375 if (leap) // adjust February
1376 month_lengths[1] = 29;
1377
1378 // --------------------------- computation ------------------------------
1379 int d_remaining = Julian_day; // days still to account for
1380 int month = 1; // 1‑based calendar month
1381
1382 // subtract complete months until the remainder lies in the current month
1383 for (int i = 0; i < 12; ++i) {
1384 if (d_remaining > month_lengths[i]) {
1385 d_remaining -= month_lengths[i];
1386 ++month;
1387 } else {
1388 break;
1389 }
1390 }
1391
1392 // d_remaining is now the calendar day of the computed month
1393 return make_Date(d_remaining, month, year);
1394}
1395
1396
1397int helios::JulianDay(int day, int month, int year) {
1398 return JulianDay(make_Date(day, month, year));
1399}
1400
1401int helios::JulianDay(const Date &date) {
1402 int day = date.day;
1403 int month = date.month;
1404 int year = date.year;
1405
1406 // Validate inputs
1407 if (month < 1 || month > 12) {
1408 helios_runtime_error("ERROR (JulianDay): Month of year is out of range (month of " + std::to_string(month) + " was given).");
1409 }
1410
1411 // Get the correct number of days for the month (accounting for leap year in February)
1412 int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1413
1414 // Correct leap year calculation
1415 if (bool isLeapYear = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
1416 daysInMonth[2] = 29;
1417 }
1418
1419 if (day < 1 || day > daysInMonth[month]) {
1420 helios_runtime_error("ERROR (JulianDay): Day of month is out of range (day of " + std::to_string(day) + " was given for month " + std::to_string(month) + ").");
1421 }
1422
1423 if (year < 1000) {
1424 helios_runtime_error("ERROR (JulianDay): Year should be specified in YYYY format.");
1425 }
1426
1427 // Calculate day of year
1428 int dayOfYear = day;
1429 for (int m = 1; m < month; m++) {
1430 dayOfYear += daysInMonth[m];
1431 }
1432
1433 return dayOfYear;
1434}
1435
1436bool helios::PNGHasAlpha(const char *filename) {
1437 if (!filename) {
1438 helios_runtime_error("ERROR (PNGHasAlpha): Null filename provided.");
1439 }
1440
1441 std::string fn(filename);
1442 auto dot_pos = fn.find_last_of('.');
1443 if (dot_pos == std::string::npos) {
1444 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " has no extension.");
1445 }
1446 std::string ext = fn.substr(dot_pos + 1);
1447 if (ext != "png" && ext != "PNG") {
1448 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " is not PNG format.");
1449 }
1450
1451 // 3) Open file with RAII
1452 auto fileCloser = [](FILE *f) {
1453 if (f)
1454 std::fclose(f);
1455 };
1456 std::unique_ptr<FILE, decltype(fileCloser)> fp(std::fopen(fn.c_str(), "rb"), fileCloser);
1457 if (!fp) {
1458 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " could not be opened for reading.");
1459 }
1460
1461 // 4) Read & validate PNG signature
1462 unsigned char header[8];
1463 if (std::fread(header, 1, 8, fp.get()) != 8 || png_sig_cmp(header, 0, 8)) {
1464 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " is not a valid PNG file.");
1465 }
1466
1467 png_structp png_ptr = nullptr;
1468 png_infop info_ptr = nullptr;
1469
1470 try {
1471 // 5) Create libpng read & info structs
1472 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1473 if (!png_ptr) {
1474 throw std::runtime_error("png_create_read_struct failed.");
1475 }
1476 info_ptr = png_create_info_struct(png_ptr);
1477 if (!info_ptr) {
1478 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1479 throw std::runtime_error("png_create_info_struct failed.");
1480 }
1481
1482 // 6) Error handling via setjmp
1483 if (setjmp(png_jmpbuf(png_ptr))) {
1484 throw std::runtime_error("Error during PNG initialization.");
1485 }
1486
1487 // 7) Initialize IO & read info
1488 png_init_io(png_ptr, fp.get());
1489 png_set_sig_bytes(png_ptr, 8);
1490 png_read_info(png_ptr, info_ptr);
1491
1492 // 8) Inspect color type and tRNS chunk
1493 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1494 bool has_tRNS = png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) != 0;
1495
1496 // 9) Determine alpha presence
1497 bool has_alpha = ((color_type & PNG_COLOR_MASK_ALPHA) != 0) || has_tRNS;
1498
1499 // 10) Clean up libpng structs
1500 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1501
1502 return has_alpha;
1503 } catch (const std::exception &e) {
1504 // Ensure libpng structs are freed on error
1505 if (png_ptr) {
1506 if (info_ptr)
1507 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1508 else
1509 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1510 }
1511 helios_runtime_error(std::string("ERROR (PNGHasAlpha): ") + e.what());
1512 }
1513
1514 // Should never reach here
1515 return false;
1516}
1517
1518std::vector<std::vector<bool>> helios::readPNGAlpha(const std::string &filename) {
1519 const std::string &fn = filename;
1520 auto dot = fn.find_last_of('.');
1521 if (dot == std::string::npos) {
1522 helios_runtime_error("ERROR (readPNGAlpha): File " + fn + " has no extension.");
1523 }
1524 std::string ext = fn.substr(dot + 1);
1525 std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); });
1526 if (ext != "png") {
1527 helios_runtime_error("ERROR (readPNGAlpha): File " + fn + " is not PNG format.");
1528 }
1529
1530 std::vector<std::vector<bool>> mask;
1531 png_structp png_ptr = nullptr;
1532 png_infop info_ptr = nullptr;
1533
1534 try {
1535 // RAII for FILE*
1536 auto fileDeleter = [](FILE *f) {
1537 if (f)
1538 fclose(f);
1539 };
1540 std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen(filename.c_str(), "rb"), fileDeleter);
1541 if (!fp) {
1542 throw std::runtime_error("File " + filename + " could not be opened for reading.");
1543 }
1544
1545 // Read & validate PNG signature
1546 unsigned char header[8];
1547 if (fread(header, 1, 8, fp.get()) != 8) {
1548 throw std::runtime_error("Failed to read PNG header from " + filename);
1549 }
1550 if (png_sig_cmp(header, 0, 8)) {
1551 throw std::runtime_error("File " + filename + " is not a valid PNG.");
1552 }
1553
1554 // Create libpng structs
1555 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1556 if (!png_ptr) {
1557 throw std::runtime_error("png_create_read_struct failed.");
1558 }
1559
1560 info_ptr = png_create_info_struct(png_ptr);
1561 if (!info_ptr) {
1562 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1563 throw std::runtime_error("png_create_info_struct failed.");
1564 }
1565
1566 // libpng error handling
1567 if (setjmp(png_jmpbuf(png_ptr))) {
1568 throw std::runtime_error("Error during PNG initialization.");
1569 }
1570
1571 png_init_io(png_ptr, fp.get());
1572 png_set_sig_bytes(png_ptr, 8);
1573 png_read_info(png_ptr, info_ptr);
1574
1575 uint width = png_get_image_width(png_ptr, info_ptr);
1576 uint height = png_get_image_height(png_ptr, info_ptr);
1577 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1578 png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
1579 bool has_alpha = (color_type & PNG_COLOR_MASK_ALPHA) != 0 || png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) != 0;
1580
1581 mask.resize(height);
1582 for (uint i = 0; i < height; i++) {
1583 mask.at(i).resize(width);
1584 }
1585
1586 if (!has_alpha) {
1587 for (uint j = 0; j < height; ++j) {
1588 std::fill(mask.at(j).begin(), mask.at(j).end(), true);
1589 }
1590 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1591 return mask;
1592 }
1593
1594 // Apply transformations to ensure we get RGBA format (4 bytes per pixel)
1595 if (bit_depth == 16) {
1596 png_set_strip_16(png_ptr);
1597 }
1598 if (color_type == PNG_COLOR_TYPE_PALETTE) {
1599 png_set_palette_to_rgb(png_ptr);
1600 }
1601 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
1602 png_set_expand_gray_1_2_4_to_8(png_ptr);
1603 }
1604 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
1605 png_set_tRNS_to_alpha(png_ptr);
1606 }
1607 // Only add filler if we don't already have alpha from tRNS
1608 if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) && (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)) {
1609 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
1610 }
1611 if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
1612 png_set_gray_to_rgb(png_ptr);
1613 }
1614
1615 png_set_interlace_handling(png_ptr);
1616 png_read_update_info(png_ptr, info_ptr);
1617
1618 // Prepare row pointers using RAII containers
1619 size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);
1620 std::vector<std::vector<png_byte>> row_data(height, std::vector<png_byte>(rowbytes));
1621 std::vector<png_bytep> row_pointers(height);
1622 for (uint y = 0; y < height; ++y) {
1623 row_pointers[y] = row_data[y].data();
1624 }
1625
1626 // Read the image
1627 if (setjmp(png_jmpbuf(png_ptr))) {
1628 throw std::runtime_error("Error during PNG read.");
1629 }
1630 png_read_image(png_ptr, row_pointers.data());
1631
1632 // Extract alpha mask
1633 for (uint j = 0; j < height; j++) {
1634 png_byte *row = row_pointers[j];
1635 for (uint i = 0; i < width; i++) {
1636 png_byte *ba = &(row[i * 4]);
1637 float alpha = ba[3];
1638 mask.at(j).at(i) = (alpha >= 250);
1639 }
1640 }
1641
1642 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1643
1644 } catch (const std::exception &e) {
1645 if (png_ptr) {
1646 png_destroy_read_struct(&png_ptr, info_ptr ? &info_ptr : nullptr, nullptr);
1647 }
1648 helios_runtime_error(std::string("ERROR (readPNGAlpha): ") + e.what());
1649 }
1650
1651 return mask;
1652}
1653
1654void helios::readPNG(const std::string &filename, uint &width, uint &height, std::vector<helios::RGBAcolor> &texture) {
1655 // 1) Safe extension check
1656 auto ext_pos = filename.find_last_of('.');
1657 if (ext_pos == std::string::npos) {
1658 helios_runtime_error("ERROR (readPNG): File " + filename + " has no extension.");
1659 }
1660 std::string ext = filename.substr(ext_pos + 1);
1661 std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); });
1662 if (ext != "png") {
1663 helios_runtime_error("ERROR (readPNG): File " + filename + " is not PNG format.");
1664 }
1665
1666 png_structp png_ptr = nullptr;
1667 png_infop info_ptr = nullptr;
1668
1669 try {
1670 //
1671 // 2) RAII for FILE*
1672 //
1673 auto fileDeleter = [](FILE *f) {
1674 if (f)
1675 fclose(f);
1676 };
1677 std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen(filename.c_str(), "rb"), fileDeleter);
1678 if (!fp) {
1679 throw std::runtime_error("File " + filename + " could not be opened.");
1680 }
1681
1682 // 3) Read & validate PNG signature
1683 unsigned char header[8];
1684 if (fread(header, 1, 8, fp.get()) != 8) {
1685 throw std::runtime_error("Failed to read PNG header from " + filename);
1686 }
1687 if (png_sig_cmp(header, 0, 8)) {
1688 throw std::runtime_error("File " + filename + " is not a valid PNG.");
1689 }
1690
1691 // 4) Create libpng structs
1692 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1693 if (!png_ptr) {
1694 throw std::runtime_error("Failed to create PNG read struct.");
1695 }
1696 info_ptr = png_create_info_struct(png_ptr);
1697 if (!info_ptr) {
1698 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1699 throw std::runtime_error("Failed to create PNG info struct.");
1700 }
1701
1702 // 5) libpng error handling
1703 if (setjmp(png_jmpbuf(png_ptr))) {
1704 throw std::runtime_error("Error during PNG initialization.");
1705 }
1706
1707 // 6) Set up IO & read basic info
1708 png_init_io(png_ptr, fp.get());
1709 png_set_sig_bytes(png_ptr, 8);
1710 png_read_info(png_ptr, info_ptr);
1711
1712 // 7) Transformations → strip 16-bit, expand palette/gray, add alpha
1713 png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
1714 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1715
1716 if (bit_depth == 16) {
1717 png_set_strip_16(png_ptr);
1718 }
1719 if (color_type == PNG_COLOR_TYPE_PALETTE) {
1720 png_set_palette_to_rgb(png_ptr);
1721 }
1722 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
1723 png_set_expand_gray_1_2_4_to_8(png_ptr);
1724 }
1725 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
1726 png_set_tRNS_to_alpha(png_ptr);
1727 }
1728 // Ensure we have RGBA - but only add filler if we don't already have alpha from tRNS
1729 if (!png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) && (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)) {
1730 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
1731 }
1732 if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
1733 png_set_gray_to_rgb(png_ptr);
1734 }
1735
1736 // 8) Handle interlacing
1737 png_set_interlace_handling(png_ptr);
1738
1739 // 9) Apply transforms & re-fetch info
1740 png_read_update_info(png_ptr, info_ptr);
1741
1742 // 10) Get & validate dimensions
1743 size_t w = png_get_image_width(png_ptr, info_ptr);
1744 size_t h = png_get_image_height(png_ptr, info_ptr);
1745 // Prevent overflow when resizing vectors
1746 constexpr size_t max_pixels = (std::numeric_limits<size_t>::max)() / sizeof(helios::RGBAcolor);
1747 if (w == 0 || h == 0 || w > max_pixels / h) {
1748 throw std::runtime_error("Invalid image dimensions: " + std::to_string(w) + "×" + std::to_string(h));
1749 }
1750 width = scast<uint>(w);
1751 height = scast<uint>(h);
1752
1753 // 11) Prepare row pointers
1754 size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);
1755 if (rowbytes < width * 4) {
1756 throw std::runtime_error("Unexpected row size: " + std::to_string(rowbytes));
1757 }
1758 std::vector<std::vector<png_byte>> row_data(height, std::vector<png_byte>(rowbytes));
1759 std::vector<png_bytep> row_pointers(height);
1760 for (uint y = 0; y < height; ++y) {
1761 row_pointers[y] = row_data[y].data();
1762 }
1763
1764 // 12) Read the image
1765 if (setjmp(png_jmpbuf(png_ptr))) {
1766 throw std::runtime_error("Error during PNG read.");
1767 }
1768 png_read_image(png_ptr, row_pointers.data());
1769
1770 // 13) Convert into normalized RGBAcolor
1771 texture.resize(scast<size_t>(width) * height);
1772 for (uint y = 0; y < height; ++y) {
1773 png_bytep row = row_pointers[y];
1774 for (uint x = 0; x < width; ++x) {
1775 png_bytep px = row + x * 4;
1776 auto &c = texture[y * width + x];
1777 c.r = px[0] / 255.0f;
1778 c.g = px[1] / 255.0f;
1779 c.b = px[2] / 255.0f;
1780 c.a = px[3] / 255.0f;
1781 }
1782 }
1783 } catch (const std::exception &e) {
1784 // Clean up libpng structs on error
1785 if (png_ptr) {
1786 if (info_ptr)
1787 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1788 else
1789 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1790 }
1791 helios_runtime_error("ERROR (readPNG): " + std::string(e.what()));
1792 }
1793
1794 // Normal cleanup
1795 if (png_ptr) {
1796 if (info_ptr)
1797 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1798 else
1799 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1800 }
1801}
1802
1803
1804void helios::writePNG(const std::string &filename, uint width, uint height, const std::vector<helios::RGBAcolor> &pixel_data) {
1805 FILE *fp = fopen(filename.c_str(), "wb");
1806 if (!fp) {
1807 helios_runtime_error("ERROR (writePNG): failed to open image file.");
1808 }
1809
1810 png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1811 if (!png) {
1812 helios_runtime_error("ERROR (writePNG): failed to create PNG write structure.");
1813 }
1814
1815 png_infop info = png_create_info_struct(png);
1816 if (!info) {
1817 helios_runtime_error("ERROR (writePNG): failed to create PNG info structure.");
1818 }
1819
1820 if (setjmp(png_jmpbuf(png))) {
1821 helios_runtime_error("ERROR (writePNG): init_io failed.");
1822 }
1823
1824 png_init_io(png, fp);
1825
1826 // Output is 8bit depth, RGBA format.
1827 png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
1828 png_write_info(png, info);
1829
1830 // To remove the alpha channel for PNG_COLOR_TYPE_RGB format,
1831 // Use png_set_filler().
1832 // png_set_filler(png, 0, PNG_FILLER_AFTER);
1833
1834 std::vector<unsigned char *> row_pointers;
1835 row_pointers.resize(height);
1836
1837 std::vector<std::vector<unsigned char>> data;
1838 data.resize(height);
1839
1840 for (uint row = 0; row < height; row++) {
1841 data.at(row).resize(4 * width);
1842 for (uint col = 0; col < width; col++) {
1843 data.at(row).at(4 * col) = (unsigned char) round(clamp(pixel_data.at(row * width + col).r, 0.f, 1.f) * 255.f);
1844 data.at(row).at(4 * col + 1) = (unsigned char) round(clamp(pixel_data.at(row * width + col).g, 0.f, 1.f) * 255.f);
1845 data.at(row).at(4 * col + 2) = (unsigned char) round(clamp(pixel_data.at(row * width + col).b, 0.f, 1.f) * 255.f);
1846 data.at(row).at(4 * col + 3) = (unsigned char) round(clamp(pixel_data.at(row * width + col).a, 0.f, 1.f) * 255.f);
1847 }
1848 row_pointers.at(row) = &data.at(row).at(0);
1849 }
1850
1851 png_write_image(png, &row_pointers.at(0));
1852 png_write_end(png, nullptr);
1853
1854 fclose(fp);
1855
1856 png_destroy_write_struct(&png, &info);
1857}
1858
1859void helios::writePNG(const std::string &filename, uint width, uint height, const std::vector<unsigned char> &pixel_data) {
1860
1861 // Convert pixel_data array into RGBcolor vector
1862
1863 size_t pixels = width * height;
1864
1865 std::vector<RGBAcolor> rgb_data;
1866 rgb_data.resize(pixels);
1867
1868 size_t channels = pixel_data.size() / pixels;
1869
1870 if (channels < 3) {
1871 helios_runtime_error("ERROR (writePNG): Pixel data must have at least 3 color channels");
1872 }
1873
1874 // Convert pixel data into RGBA values
1875 for (size_t i = 0; i < pixels; i++) {
1876 rgb_data[i].r = float(pixel_data[i]) / 255.0f;
1877 rgb_data[i].g = float(pixel_data[i + pixels]) / 255.0f;
1878 rgb_data[i].b = float(pixel_data[i + 2 * pixels]) / 255.0f;
1879 rgb_data[i].a = channels > 3 ? float(pixel_data[i + 3 * pixels]) / 255.0f : 1.0f;
1880 }
1881
1882 // Call RGB version of writePNG
1883 writePNG(filename, width, height, rgb_data);
1884}
1885
1886
1888METHODDEF(void) jpg_error_exit(j_common_ptr cinfo) {
1889 char buffer[JMSG_LENGTH_MAX];
1890 (*cinfo->err->format_message)(cinfo, buffer);
1891 throw std::runtime_error(buffer);
1892}
1893
1894void helios::readJPEG(const std::string &filename, uint &width, uint &height, std::vector<helios::RGBcolor> &pixel_data) {
1895 auto file_extension = getFileExtension(filename);
1896 if (file_extension != ".jpg" && file_extension != ".JPG" && file_extension != ".jpeg" && file_extension != ".JPEG") {
1897 helios_runtime_error("ERROR (Context::readJPEG): File " + filename + " is not JPEG format.");
1898 }
1899
1900 jpeg_decompress_struct cinfo{};
1901
1902 jpeg_error_mgr jerr{};
1903 JSAMPARRAY buffer;
1904 int row_stride;
1905
1906 std::unique_ptr<FILE, int (*)(FILE *)> infile(fopen(filename.c_str(), "rb"), fclose);
1907 if (!infile) {
1908 helios_runtime_error("ERROR (Context::readJPEG): File " + filename + " could not be opened. Check that the file exists and that you have permission to read it.");
1909 }
1910
1911 cinfo.err = jpeg_std_error(&jerr);
1912 jerr.error_exit = jpg_error_exit;
1913
1914 try {
1915 jpeg_create_decompress(&cinfo);
1916 jpeg_stdio_src(&cinfo, infile.get());
1917 (void) jpeg_read_header(&cinfo, (boolean) 1);
1918
1919 (void) jpeg_start_decompress(&cinfo);
1920
1921 row_stride = cinfo.output_width * cinfo.output_components;
1922 buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
1923
1924 width = cinfo.output_width;
1925 height = cinfo.output_height;
1926
1927 if (cinfo.output_components != 3) {
1928 helios_runtime_error("ERROR (Context::readJPEG): Image file does not have RGB components.");
1929 } else if (width == 0 || height == 0) {
1930 helios_runtime_error("ERROR (Context::readJPEG): Image file is empty.");
1931 }
1932
1933 pixel_data.resize(width * height);
1934
1935 JSAMPLE *ba;
1936 int row = 0;
1937 while (cinfo.output_scanline < cinfo.output_height) {
1938 (void) jpeg_read_scanlines(&cinfo, buffer, 1);
1939
1940 ba = buffer[0];
1941
1942 for (int col = 0; col < row_stride; col += 3) {
1943 pixel_data.at(row * width + col / 3) = make_RGBcolor(ba[col] / 255.f, ba[col + 1] / 255.f, ba[col + 2] / 255.f);
1944 }
1945
1946 row++;
1947 }
1948
1949 (void) jpeg_finish_decompress(&cinfo);
1950
1951 jpeg_destroy_decompress(&cinfo);
1952 } catch (...) {
1953 jpeg_destroy_decompress(&cinfo);
1954 throw;
1955 }
1956}
1957
1958helios::int2 helios::getImageResolutionJPEG(const std::string &filename) {
1959 auto file_extension = getFileExtension(filename);
1960 if (file_extension != ".jpg" && file_extension != ".JPG" && file_extension != ".jpeg" && file_extension != ".JPEG") {
1961 helios_runtime_error("ERROR (Context::getImageResolutionJPEG): File " + filename + " is not JPEG format.");
1962 }
1963
1964 jpeg_decompress_struct cinfo{};
1965
1966 jpeg_error_mgr jerr{};
1967 std::unique_ptr<FILE, int (*)(FILE *)> infile(fopen(filename.c_str(), "rb"), fclose);
1968 if (!infile) {
1969 helios_runtime_error("ERROR (Context::getImageResolutionJPEG): File " + filename + " could not be opened. Check that the file exists and that you have permission to read it.");
1970 }
1971
1972 cinfo.err = jpeg_std_error(&jerr);
1973 jerr.error_exit = jpg_error_exit;
1974
1975 try {
1976 jpeg_create_decompress(&cinfo);
1977 jpeg_stdio_src(&cinfo, infile.get());
1978 (void) jpeg_read_header(&cinfo, (boolean) 1);
1979 (void) jpeg_start_decompress(&cinfo);
1980
1981 jpeg_destroy_decompress(&cinfo);
1982 } catch (...) {
1983 jpeg_destroy_decompress(&cinfo);
1984 throw;
1985 }
1986
1987 return make_int2(cinfo.output_width, cinfo.output_height);
1988}
1989
1990void helios::writeJPEG(const std::string &a_filename, uint width, uint height, const std::vector<helios::RGBcolor> &pixel_data) {
1991
1992 std::string filename = a_filename;
1993 auto file_extension = getFileExtension(filename);
1994 if (file_extension != ".jpg" && file_extension != ".JPG" && file_extension != ".jpeg" && file_extension != ".JPEG") {
1995 filename.append(".jpeg");
1996 }
1997
1998 if (pixel_data.size() != width * height) {
1999 helios_runtime_error("ERROR (Context::writeJPEG): Pixel data does not have size of width*height.");
2000 }
2001
2002 const uint bsize = 3 * width * height;
2003 std::vector<unsigned char> screen_shot_trans(bsize);
2004
2005 size_t ii = 0;
2006 for (size_t i = 0; i < width * height; i++) {
2007 screen_shot_trans.at(ii) = (unsigned char) round(clamp(pixel_data.at(i).r, 0.f, 1.f) * 255);
2008 screen_shot_trans.at(ii + 1) = (unsigned char) round(clamp(pixel_data.at(i).g, 0.f, 1.f) * 255);
2009 screen_shot_trans.at(ii + 2) = (unsigned char) round(clamp(pixel_data.at(i).b, 0.f, 1.f) * 255);
2010 ii += 3;
2011 }
2012
2013 struct jpeg_compress_struct cinfo{};
2014
2015 struct jpeg_error_mgr jerr{};
2016
2017 cinfo.err = jpeg_std_error(&jerr);
2018 jerr.error_exit = jpg_error_exit;
2019
2020 JSAMPROW row_pointer;
2021 int row_stride;
2022
2023 std::unique_ptr<FILE, int (*)(FILE *)> outfile(fopen(filename.c_str(), "wb"), fclose);
2024 if (!outfile) {
2025 helios_runtime_error("ERROR (Context::writeJPEG): File " + filename + " could not be opened. Check that the file path is correct you have permission to write to it.");
2026 }
2027
2028 jpeg_create_compress(&cinfo);
2029 jpeg_stdio_dest(&cinfo, outfile.get());
2030
2031 cinfo.image_width = width; /* image width and height, in pixels */
2032 cinfo.image_height = height;
2033 cinfo.input_components = 3; /* # of color components per pixel */
2034 cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
2035
2036 jpeg_set_defaults(&cinfo);
2037
2038 jpeg_set_quality(&cinfo, 100, (boolean) 1 /* limit to baseline-JPEG values */);
2039
2040 jpeg_start_compress(&cinfo, (boolean) 1);
2041
2042 try {
2043 row_stride = width * 3; /* JSAMPLEs per row in image_buffer */
2044
2045 while (cinfo.next_scanline < cinfo.image_height) {
2046 row_pointer = (JSAMPROW) &screen_shot_trans[(cinfo.image_height - cinfo.next_scanline - 1) * row_stride];
2047 (void) jpeg_write_scanlines(&cinfo, &row_pointer, 1);
2048 }
2049
2050 jpeg_finish_compress(&cinfo);
2051 jpeg_destroy_compress(&cinfo);
2052 } catch (...) {
2053 jpeg_destroy_compress(&cinfo);
2054 throw;
2055 }
2056}
2057
2058void helios::writeJPEG(const std::string &a_filename, uint width, uint height, const std::vector<unsigned char> &pixel_data) {
2059
2060 // Convert pixel_data array into RGBcolor vector
2061
2062 size_t pixels = width * height;
2063
2064 std::vector<RGBcolor> rgb_data;
2065 rgb_data.resize(pixels);
2066
2067 size_t channels = pixel_data.size() / pixels;
2068
2069 if (channels < 3) {
2070 helios_runtime_error("ERROR (writeJPEG): Pixel data must have at least 3 color channels");
2071 }
2072
2073 // Convert pixel data into RGB values
2074 for (size_t i = 0; i < pixels; i++) {
2075 rgb_data[i].r = scast<float>(pixel_data[i]) / 255.0f;
2076 rgb_data[i].g = scast<float>(pixel_data[i + pixels]) / 255.0f;
2077 rgb_data[i].b = scast<float>(pixel_data[i + 2 * pixels]) / 255.0f;
2078 }
2079
2080 // Call RGB version of writeJPEG
2081 writeJPEG(a_filename, width, height, rgb_data);
2082}
2083
2084void helios::writeEXR(const std::string &filename, uint width, uint height, const std::vector<float> &pixel_data, const std::string &channel_name) {
2085
2086 if (pixel_data.size() != width * height) {
2087 helios_runtime_error("ERROR (writeEXR): pixel_data size (" + std::to_string(pixel_data.size()) + ") does not match width*height (" + std::to_string(width * height) + ").");
2088 }
2089
2090 EXRHeader header;
2091 InitEXRHeader(&header);
2092
2093 EXRImage image;
2094 InitEXRImage(&image);
2095
2096 image.num_channels = 1;
2097 image.width = scast<int>(width);
2098 image.height = scast<int>(height);
2099
2100 float *image_ptr[1];
2101 image_ptr[0] = const_cast<float *>(pixel_data.data());
2102
2103 image.images = reinterpret_cast<unsigned char **>(image_ptr);
2104
2105 header.num_channels = 1;
2106 header.channels = scast<EXRChannelInfo *>(malloc(sizeof(EXRChannelInfo)));
2107 strncpy(header.channels[0].name, channel_name.c_str(), 255);
2108 header.channels[0].name[255] = '\0';
2109
2110 header.pixel_types = scast<int *>(malloc(sizeof(int)));
2111 header.requested_pixel_types = scast<int *>(malloc(sizeof(int)));
2112 header.pixel_types[0] = TINYEXR_PIXELTYPE_FLOAT;
2113 header.requested_pixel_types[0] = TINYEXR_PIXELTYPE_FLOAT;
2114
2115 header.compression_type = TINYEXR_COMPRESSIONTYPE_ZIP;
2116
2117 const char *err = nullptr;
2118 int ret = SaveEXRImageToFile(&image, &header, filename.c_str(), &err);
2119
2120 free(header.channels);
2121 free(header.pixel_types);
2122 free(header.requested_pixel_types);
2123
2124 if (ret != TINYEXR_SUCCESS) {
2125 std::string error_msg = "ERROR (writeEXR): Failed to write EXR file '" + filename + "'";
2126 if (err) {
2127 error_msg += ": " + std::string(err);
2128 FreeEXRErrorMessage(err);
2129 }
2130 helios_runtime_error(error_msg);
2131 }
2132}
2133
2134void helios::writeEXR(const std::string &filename, uint width, uint height, const std::vector<std::vector<float>> &channel_data, const std::vector<std::string> &channel_names) {
2135
2136 if (channel_data.size() != channel_names.size()) {
2137 helios_runtime_error("ERROR (writeEXR): channel_data size (" + std::to_string(channel_data.size()) + ") does not match channel_names size (" + std::to_string(channel_names.size()) + ").");
2138 }
2139 if (channel_data.empty()) {
2140 helios_runtime_error("ERROR (writeEXR): channel_data is empty.");
2141 }
2142 for (size_t c = 0; c < channel_data.size(); c++) {
2143 if (channel_data[c].size() != width * height) {
2144 helios_runtime_error("ERROR (writeEXR): channel_data[" + std::to_string(c) + "] size (" + std::to_string(channel_data[c].size()) + ") does not match width*height (" + std::to_string(width * height) + ").");
2145 }
2146 }
2147
2148 int num_channels = scast<int>(channel_data.size());
2149
2150 // Map band names to standard EXR channel names (R, G, B, A) for broad compatibility.
2151 // Names that don't map to a standard channel are kept as-is.
2152 auto mapChannelName = [](const std::string &name) -> std::string {
2153 std::string lower = name;
2154 std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
2155 if (lower == "r" || lower == "red") return "R";
2156 if (lower == "g" || lower == "green") return "G";
2157 if (lower == "b" || lower == "blue") return "B";
2158 if (lower == "a" || lower == "alpha") return "A";
2159 return name;
2160 };
2161
2162 std::vector<std::string> exr_channel_names(num_channels);
2163 for (int c = 0; c < num_channels; c++) {
2164 exr_channel_names[c] = mapChannelName(channel_names[c]);
2165 }
2166
2167 // Sort channels alphabetically (EXR convention)
2168 std::vector<size_t> sort_indices(num_channels);
2169 for (size_t i = 0; i < sort_indices.size(); i++) {
2170 sort_indices[i] = i;
2171 }
2172 std::sort(sort_indices.begin(), sort_indices.end(), [&](size_t a, size_t b) {
2173 return exr_channel_names[a] < exr_channel_names[b];
2174 });
2175
2176 EXRHeader header;
2177 InitEXRHeader(&header);
2178
2179 EXRImage image;
2180 InitEXRImage(&image);
2181
2182 image.num_channels = num_channels;
2183 image.width = scast<int>(width);
2184 image.height = scast<int>(height);
2185
2186 std::vector<float *> image_ptrs(num_channels);
2187 for (int c = 0; c < num_channels; c++) {
2188 image_ptrs[c] = const_cast<float *>(channel_data[sort_indices[c]].data());
2189 }
2190 image.images = reinterpret_cast<unsigned char **>(image_ptrs.data());
2191
2192 header.num_channels = num_channels;
2193 header.channels = scast<EXRChannelInfo *>(malloc(sizeof(EXRChannelInfo) * num_channels));
2194 header.pixel_types = scast<int *>(malloc(sizeof(int) * num_channels));
2195 header.requested_pixel_types = scast<int *>(malloc(sizeof(int) * num_channels));
2196
2197 for (int c = 0; c < num_channels; c++) {
2198 strncpy(header.channels[c].name, exr_channel_names[sort_indices[c]].c_str(), 255);
2199 header.channels[c].name[255] = '\0';
2200 header.pixel_types[c] = TINYEXR_PIXELTYPE_FLOAT;
2201 header.requested_pixel_types[c] = TINYEXR_PIXELTYPE_FLOAT;
2202 }
2203
2204 header.compression_type = TINYEXR_COMPRESSIONTYPE_ZIP;
2205
2206 const char *err = nullptr;
2207 int ret = SaveEXRImageToFile(&image, &header, filename.c_str(), &err);
2208
2209 free(header.channels);
2210 free(header.pixel_types);
2211 free(header.requested_pixel_types);
2212
2213 if (ret != TINYEXR_SUCCESS) {
2214 std::string error_msg = "ERROR (writeEXR): Failed to write EXR file '" + filename + "'";
2215 if (err) {
2216 error_msg += ": " + std::string(err);
2217 FreeEXRErrorMessage(err);
2218 }
2219 helios_runtime_error(error_msg);
2220 }
2221}
2222
2223helios::vec3 helios::spline_interp3(float u, const vec3 &x_start, const vec3 &tan_start, const vec3 &x_end, const vec3 &tan_end) {
2224 // Perform interpolation between two 3D points using Cubic Hermite Spline
2225
2226 if (u < 0 || u > 1.f) {
2227 std::cerr << "WARNING (spline_interp3): Clamping query point 'u' to the interval (0,1)" << std::endl;
2228 u = clamp(u, 0.f, 1.f);
2229 }
2230
2231 // Basis matrix
2232 float B[16] = {2.f, -2.f, 1.f, 1.f, -3.f, 3.f, -2.f, -1.f, 0, 0, 1.f, 0, 1.f, 0, 0, 0};
2233
2234 // Control matrix
2235 const float C[12] = {x_start.x, x_start.y, x_start.z, x_end.x, x_end.y, x_end.z, tan_start.x, tan_start.y, tan_start.z, tan_end.x, tan_end.y, tan_end.z};
2236
2237 // Parameter vector
2238 const float P[4] = {u * u * u, u * u, u, 1.f};
2239
2240 float R[12] = {0.f};
2241
2242 for (int i = 0; i < 4; i++) {
2243 for (int j = 0; j < 3; j++) {
2244 for (int k = 0; k < 4; k++) {
2245 R[3 * i + j] = R[3 * i + j] + B[4 * i + k] * C[3 * k + j];
2246 }
2247 }
2248 }
2249
2250 float xq[3] = {0.f};
2251
2252 for (int j = 0; j < 3; j++) {
2253 for (int k = 0; k < 4; k++) {
2254 xq[j] = xq[j] + P[k] * R[3 * k + j];
2255 }
2256 }
2257
2258 return make_vec3(xq[0], xq[1], xq[2]);
2259}
2260
2261float helios::XMLloadfloat(const pugi::xml_node node, const char *field) {
2262 const char *field_str = node.child_value(field);
2263
2264 float value;
2265 if (strlen(field_str) == 0) {
2266 value = 99999;
2267 } else {
2268 if (!parse_float(field_str, value)) {
2269 value = 99999;
2270 }
2271 }
2272
2273 return value;
2274}
2275
2276int helios::XMLloadint(const pugi::xml_node node, const char *field) {
2277 const char *field_str = node.child_value(field);
2278
2279 int value;
2280 if (strlen(field_str) == 0) {
2281 value = 99999;
2282 } else {
2283 if (!parse_int(field_str, value)) {
2284 value = 99999;
2285 }
2286 }
2287
2288 return value;
2289}
2290
2291std::string helios::XMLloadstring(const pugi::xml_node node, const char *field) {
2292 const std::string field_str = deblank(node.child_value(field));
2293
2294 std::string value;
2295 if (field_str.empty()) {
2296 value = "99999";
2297 } else {
2298 value = field_str; // note: pugi loads xml data as a character. need to separate it into int
2299 }
2300
2301 return value;
2302}
2303
2304helios::vec2 helios::XMLloadvec2(const pugi::xml_node node, const char *field) {
2305 const char *field_str = node.child_value(field);
2306
2307 helios::vec2 value;
2308 if (strlen(field_str) == 0) {
2309 value = make_vec2(99999, 99999);
2310 } else {
2311 value = string2vec2(field_str); // note: pugi loads xml data as a character. need to separate it into 2 floats
2312 }
2313
2314 return value;
2315}
2316
2317helios::vec3 helios::XMLloadvec3(const pugi::xml_node node, const char *field) {
2318 const char *field_str = node.child_value(field);
2319
2320 helios::vec3 value;
2321 if (strlen(field_str) == 0) {
2322 value = make_vec3(99999, 99999, 99999);
2323 } else {
2324 value = string2vec3(field_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
2325 }
2326
2327 return value;
2328}
2329
2330helios::vec4 helios::XMLloadvec4(const pugi::xml_node node, const char *field) {
2331 const char *field_str = node.child_value(field);
2332
2333 helios::vec4 value;
2334 if (strlen(field_str) == 0) {
2335 value = make_vec4(99999, 99999, 99999, 99999);
2336 } else {
2337 value = string2vec4(field_str); // note: pugi loads xml data as a character. need to separate it into 4 floats
2338 }
2339
2340 return value;
2341}
2342
2343helios::int2 helios::XMLloadint2(const pugi::xml_node node, const char *field) {
2344 const char *field_str = node.child_value(field);
2345
2346 helios::int2 value;
2347 if (strlen(field_str) == 0) {
2348 value = make_int2(99999, 99999);
2349 } else {
2350 value = string2int2(field_str); // note: pugi loads xml data as a character. need to separate it into 2 ints
2351 }
2352
2353 return value;
2354}
2355
2356helios::int3 helios::XMLloadint3(const pugi::xml_node node, const char *field) {
2357 const char *field_str = node.child_value(field);
2358
2359 helios::int3 value;
2360 if (strlen(field_str) == 0) {
2361 value = make_int3(99999, 99999, 99999);
2362 } else {
2363 value = string2int3(field_str); // note: pugi loads xml data as a character. need to separate it into 3 ints
2364 }
2365
2366 return value;
2367}
2368
2369helios::int4 helios::XMLloadint4(const pugi::xml_node node, const char *field) {
2370 const char *field_str = node.child_value(field);
2371
2372 helios::int4 value;
2373 if (strlen(field_str) == 0) {
2374 value = make_int4(99999, 99999, 99999, 99999);
2375 } else {
2376 value = string2int4(field_str); // note: pugi loads xml data as a character. need to separate it into 4 ints
2377 }
2378
2379 return value;
2380}
2381
2382helios::RGBcolor helios::XMLloadrgb(const pugi::xml_node node, const char *field) {
2383 const char *field_str = node.child_value(field);
2384
2385 helios::RGBAcolor value;
2386 if (strlen(field_str) == 0) {
2387 value = make_RGBAcolor(1, 1, 1, 0);
2388 } else {
2389 value = string2RGBcolor(field_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
2390 }
2391
2392 return make_RGBcolor(value.r, value.g, value.b);
2393}
2394
2395helios::RGBAcolor helios::XMLloadrgba(const pugi::xml_node node, const char *field) {
2396 const char *field_str = node.child_value(field);
2397
2398 helios::RGBAcolor value;
2399 if (strlen(field_str) == 0) {
2400 value = make_RGBAcolor(1, 1, 1, 1);
2401 } else {
2402 value = string2RGBcolor(field_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
2403 }
2404
2405 return value;
2406}
2407
2408float helios::fzero(float (*f)(float, std::vector<float> &, const void *), std::vector<float> &vars, const void *params, float init_guess, float err_tol, int max_iter, WarningAggregator *warnings) {
2409 constexpr float DELTA_SEED = 1e-3f; // Increased for better initial slope estimate
2410 constexpr float DENOM_EPS = 1e-10f; // Relaxed flat function detection
2411 constexpr float MAX_STEP_FACTOR = 0.5f; // Limit step size for stability
2412
2413 /* ---- initial pair ---------------------------------------------- */
2414 float x0 = init_guess;
2415 float x1 = (std::fabs(init_guess) > 1.0f) ? init_guess * (1.0f + DELTA_SEED) : init_guess + DELTA_SEED;
2416
2417 float f0 = f(x0, vars, params);
2418 float f1 = f(x1, vars, params);
2419
2420 // If initial points have opposite signs, use bisection for robustness
2421 bool use_bisection = (f0 * f1 < 0);
2422 float bracket_low = use_bisection ? std::min(x0, x1) : 0;
2423 float bracket_high = use_bisection ? std::max(x0, x1) : 0;
2424
2425 for (int iter = 0; iter < max_iter; ++iter) {
2426
2427 float denom = f1 - f0;
2428
2429 /* ------- flat or nearly flat function ----------------------- */
2430 if (std::fabs(denom) < DENOM_EPS) {
2431 if (std::fabs(f1) < err_tol) { // already "close enough"
2432 return x1;
2433 }
2434 // Try a different approach if function is flat
2435 if (use_bisection) {
2436 float x2 = 0.5f * (bracket_low + bracket_high);
2437 if (std::fabs(x2 - x1) < err_tol * std::fabs(x2)) {
2438 return x2;
2439 }
2440 float f2 = f(x2, vars, params);
2441 if (f1 * f2 < 0) {
2442 bracket_high = x1;
2443 } else {
2444 bracket_low = x1;
2445 }
2446 x0 = x1;
2447 f0 = f1;
2448 x1 = x2;
2449 f1 = f2;
2450 continue;
2451 }
2452 if (warnings) {
2453 warnings->addWarning("fzero_stagnation", "fzero stagnated (|f'|≈0).");
2454 }
2455 return x1; // graceful exit, finite value
2456 }
2457
2458 /* ------- secant update with step limiting -------------------- */
2459 float x2 = x1 - f1 * (x1 - x0) / denom;
2460
2461 // Limit step size for stability
2462 float step = x2 - x1;
2463 float max_step = MAX_STEP_FACTOR * std::max(std::fabs(x1), 1.0f);
2464 if (std::fabs(step) > max_step) {
2465 step = (step > 0) ? max_step : -max_step;
2466 x2 = x1 + step;
2467 }
2468
2469 if (!std::isfinite(x2)) { // overflow / NaN safeguard
2470 if (warnings) {
2471 warnings->addWarning("fzero_nonfinite", "fzero produced non-finite iterate.");
2472 }
2473 return x1;
2474 }
2475
2476 float f2 = f(x2, vars, params);
2477
2478 // Update brackets if using bisection fallback
2479 if (use_bisection) {
2480 if (f1 * f2 < 0) {
2481 bracket_high = x1;
2482 } else {
2483 bracket_low = x1;
2484 }
2485 }
2486
2487 /* ------- convergence criteria -------------------------------- */
2488 float rel_step = std::fabs(x2 - x1) / (std::fabs(x2) + 1.0f);
2489 if (std::fabs(f2) < err_tol && rel_step < err_tol) {
2490 return x2;
2491 }
2492
2493 /* ------- next iteration -------------------------------------- */
2494 x0 = x1;
2495 f0 = f1;
2496 x1 = x2;
2497 f1 = f2;
2498 }
2499
2500 if (warnings) {
2501 warnings->addWarning("fzero_convergence_failure", "fzero did not converge after " + std::to_string(max_iter) + " iterations.");
2502 }
2503 return x1; // best finite estimate
2504}
2505
2506float helios::fzero(float (*f)(float, std::vector<float> &, const void *), std::vector<float> &vars, const void *params, float init_guess, bool &converged, float err_tol, int max_iter) {
2507 constexpr float DELTA_SEED = 1e-3f; // Increased for better initial slope estimate
2508 constexpr float DENOM_EPS = 1e-10f; // Relaxed flat function detection
2509 constexpr float MAX_STEP_FACTOR = 0.5f; // Limit step size for stability
2510
2511 converged = false; // Initialize as not converged
2512
2513 /* ---- initial pair ---------------------------------------------- */
2514 float x0 = init_guess;
2515 float x1 = (std::fabs(init_guess) > 1.0f) ? init_guess * (1.0f + DELTA_SEED) : init_guess + DELTA_SEED;
2516
2517 float f0 = f(x0, vars, params);
2518 float f1 = f(x1, vars, params);
2519
2520 // If initial points have opposite signs, use bisection for robustness
2521 bool use_bisection = (f0 * f1 < 0);
2522 float bracket_low = use_bisection ? std::min(x0, x1) : 0;
2523 float bracket_high = use_bisection ? std::max(x0, x1) : 0;
2524
2525 for (int iter = 0; iter < max_iter; ++iter) {
2526
2527 float denom = f1 - f0;
2528
2529 /* ------- flat or nearly flat function ----------------------- */
2530 if (std::fabs(denom) < DENOM_EPS) {
2531 if (std::fabs(f1) < err_tol) { // already "close enough"
2532 converged = true;
2533 return x1;
2534 }
2535 // Try a different approach if function is flat
2536 if (use_bisection) {
2537 float x2 = 0.5f * (bracket_low + bracket_high);
2538 if (std::fabs(x2 - x1) < err_tol * std::fabs(x2)) {
2539 converged = true;
2540 return x2;
2541 }
2542 float f2 = f(x2, vars, params);
2543 if (f1 * f2 < 0) {
2544 bracket_high = x1;
2545 } else {
2546 bracket_low = x1;
2547 }
2548 x0 = x1;
2549 f0 = f1;
2550 x1 = x2;
2551 f1 = f2;
2552 continue;
2553 }
2554 // Function is stagnated, not converged
2555 return x1; // graceful exit, finite value
2556 }
2557
2558 /* ------- secant update with step limiting -------------------- */
2559 float x2 = x1 - f1 * (x1 - x0) / denom;
2560
2561 // Limit step size for stability
2562 float step = x2 - x1;
2563 float max_step = MAX_STEP_FACTOR * std::max(std::fabs(x1), 1.0f);
2564 if (std::fabs(step) > max_step) {
2565 step = (step > 0) ? max_step : -max_step;
2566 x2 = x1 + step;
2567 }
2568
2569 if (!std::isfinite(x2)) { // overflow / NaN safeguard
2570 return x1;
2571 }
2572
2573 float f2 = f(x2, vars, params);
2574
2575 // Update brackets if using bisection fallback
2576 if (use_bisection) {
2577 if (f1 * f2 < 0) {
2578 bracket_high = x1;
2579 } else {
2580 bracket_low = x1;
2581 }
2582 }
2583
2584 /* ------- convergence criteria -------------------------------- */
2585 float rel_step = std::fabs(x2 - x1) / (std::fabs(x2) + 1.0f);
2586 if (std::fabs(f2) < err_tol && rel_step < err_tol) {
2587 converged = true;
2588 return x2;
2589 }
2590
2591 /* ------- next iteration -------------------------------------- */
2592 x0 = x1;
2593 f0 = f1;
2594 x1 = x2;
2595 f1 = f2;
2596 }
2597
2598 // Did not converge after max_iter iterations
2599 return x1; // best finite estimate
2600}
2601
2602float helios::interp1(const std::vector<helios::vec2> &points, float x) {
2603 // Handle empty input
2604 if (points.empty()) {
2605 helios_runtime_error("ERROR (interp1): Cannot interpolate with empty points vector.");
2606 }
2607
2608 // Handle single point case
2609 if (points.size() == 1) {
2610 return points[0].y;
2611 }
2612
2613 // Fast path: check first if data is increasing (most common case)
2614 // This avoids full validation for performance-critical applications
2615 constexpr float EPSILON = 1.0E-5f;
2616 bool is_likely_increasing = points.size() < 2 || points[1].x > points[0].x;
2617
2618 if (is_likely_increasing) {
2619 // Quick verification for increasing sequence
2620 bool is_valid_increasing = true;
2621 for (size_t i = 1; i < points.size() && is_valid_increasing; ++i) {
2622 float deltaX = points[i].x - points[i - 1].x;
2623 if (deltaX <= EPSILON) {
2624 is_valid_increasing = false;
2625 }
2626 }
2627
2628 if (is_valid_increasing) {
2629 // Handle extrapolation cases
2630 if (x <= points.front().x) {
2631 return points.front().y;
2632 }
2633 if (x >= points.back().x) {
2634 return points.back().y;
2635 }
2636
2637 // Optimized binary search for increasing sequence
2638 auto it = std::lower_bound(points.begin(), points.end(), x, [](const vec2 &point, float value) { return point.x < value; });
2639
2640 size_t upper_idx = std::distance(points.begin(), it);
2641 size_t lower_idx = upper_idx - 1;
2642
2643 const vec2 &p1 = points[lower_idx];
2644 const vec2 &p2 = points[upper_idx];
2645
2646 // Linear interpolation
2647 float t = (x - p1.x) / (p2.x - p1.x);
2648 return p1.y + t * (p2.y - p1.y);
2649 }
2650 }
2651
2652 // Fallback: full validation for decreasing or invalid sequences
2653 bool is_increasing = true;
2654 bool is_decreasing = true;
2655
2656 for (size_t i = 1; i < points.size(); ++i) {
2657 float deltaX = points[i].x - points[i - 1].x;
2658
2659 if (std::abs(deltaX) < EPSILON) {
2660 helios_runtime_error("ERROR (interp1): Adjacent X points cannot be equal.");
2661 }
2662
2663 if (deltaX > 0) {
2664 is_decreasing = false;
2665 } else {
2666 is_increasing = false;
2667 }
2668 }
2669
2670 if (!is_increasing && !is_decreasing) {
2671 helios_runtime_error("ERROR (interp1): X points must be monotonic (either all increasing or all decreasing).");
2672 }
2673
2674 // Handle extrapolation cases
2675 if (is_decreasing) {
2676 if (x >= points.front().x) {
2677 return points.front().y;
2678 }
2679 if (x <= points.back().x) {
2680 return points.back().y;
2681 }
2682
2683 // Optimized binary search for decreasing sequence
2684 auto it = std::lower_bound(points.begin(), points.end(), x, [](const vec2 &point, float value) { return point.x > value; });
2685
2686 size_t upper_idx = std::distance(points.begin(), it);
2687 if (upper_idx == 0)
2688 upper_idx = 1;
2689 size_t lower_idx = upper_idx - 1;
2690
2691 const vec2 &p1 = points[lower_idx];
2692 const vec2 &p2 = points[upper_idx];
2693
2694 // Linear interpolation
2695 float t = (x - p1.x) / (p2.x - p1.x);
2696 return p1.y + t * (p2.y - p1.y);
2697 }
2698
2699 // This should never be reached due to earlier validation
2700 helios_runtime_error("ERROR (interp1): Unexpected interpolation state.");
2701 return 0.0f; // Suppress compiler warning (never reached)
2702}
2703
2704std::string helios::getFileExtension(const std::string &filepath) {
2705 std::filesystem::path output_path_fs = filepath;
2706 return output_path_fs.extension().string();
2707}
2708
2709std::string helios::getFileStem(const std::string &filepath) {
2710 std::filesystem::path output_path_fs = filepath;
2711 return output_path_fs.stem().string();
2712}
2713
2714std::string helios::getFileName(const std::string &filepath) {
2715 std::filesystem::path output_path_fs = filepath;
2716 return output_path_fs.filename().string();
2717}
2718
2719std::string helios::getFilePath(const std::string &filepath, bool trailingslash) {
2720 std::filesystem::path output_path_fs = filepath;
2721 std::filesystem::path output_path = output_path_fs.parent_path();
2722 std::string out_str = output_path.make_preferred().string();
2723 if (trailingslash && !out_str.empty()) {
2724 char last = out_str.back();
2725 if (last != '/' && last != '\\') {
2726 out_str += std::filesystem::path::preferred_separator;
2727 }
2728 }
2729 return out_str;
2730}
2731
2732bool helios::validateOutputPath(std::string &output_path, const std::vector<std::string> &allowable_file_extensions) {
2733 if (output_path.empty()) { // path was empty
2734 return false;
2735 }
2736
2737 std::filesystem::path output_path_fs = output_path;
2738
2739 std::string output_file = output_path_fs.filename().string();
2740 std::string output_file_ext = output_path_fs.extension().string();
2741 std::string output_dir = output_path_fs.parent_path().string();
2742
2743 if (output_file.empty()) { // path was a directory without a file
2744
2745 // Make sure directory has a trailing slash
2746 if (output_dir.find_last_of('/') != output_dir.length() - 1) {
2747 output_path += "/";
2748 }
2749 } else if (isDirectoryPath(output_path)) {
2750 // Path is a directory (either exists as one or is clearly intended to be one)
2751 // Ensure it has a trailing slash so file concatenation works correctly
2752 if (output_path.back() != '/' && output_path.back() != '\\') {
2753 output_path += "/";
2754 }
2755 }
2756
2757 // Create the output directory if it does not exist
2758 if (!output_dir.empty() && !std::filesystem::exists(output_dir)) {
2759 if (!std::filesystem::create_directory(output_dir)) {
2760 return false;
2761 }
2762 }
2763
2764 if (!output_file.empty() && !allowable_file_extensions.empty()) {
2765 // validate file extension
2766 bool valid_extension = false;
2767 for (const auto &ext: allowable_file_extensions) {
2768 if (output_file_ext == ext) {
2769 valid_extension = true;
2770 break;
2771 }
2772 }
2773 if (!valid_extension) {
2774 return false;
2775 }
2776 }
2777
2778 return true;
2779}
2780
2781bool helios::isDirectoryPath(const std::string &path) {
2782 if (path.empty()) {
2783 return false;
2784 }
2785
2786 // Check if path exists and is a directory
2787 if (std::filesystem::exists(path) && std::filesystem::is_directory(path)) {
2788 return true;
2789 }
2790
2791 // If path doesn't exist, use heuristics to determine if it's intended to be a directory
2792
2793 // 1. Check for trailing slash (most reliable indicator)
2794 if (path.back() == '/' || path.back() == '\\') {
2795 return true;
2796 }
2797
2798 // 2. Check if the last component has a file extension
2799 std::filesystem::path path_obj(path);
2800 std::string extension = path_obj.extension().string();
2801
2802 // If there's no extension, it's likely a directory
2803 // (This handles cases like "./annotations" vs "./file.txt")
2804 if (extension.empty()) {
2805 std::string filename = path_obj.filename().string();
2806
2807 // Handle special cases: dotfiles are usually files, not directories
2808 if (filename.front() == '.' && filename != "." && filename != "..") {
2809 return false; // .bashrc, .gitignore, .hidden files, etc.
2810 }
2811
2812 // For other cases without extension, assume it's a directory
2813 // This fixes the bug where "./annotations" was treated as a file
2814 return true;
2815 }
2816
2817 // 3. If it has an extension, it's likely a file
2818 return false;
2819}
2820
2821//--------------------- HELIOS_BUILD PATH RESOLUTION -----------------------------------//
2822
2824std::string getBuildDirectory() {
2825 // Try HELIOS_BUILD environment variable (required)
2826 if (const char *buildDir = std::getenv("HELIOS_BUILD")) {
2827 return std::string(buildDir);
2828 }
2829
2830 // Fallback: assume current working directory contains the build
2831 // This is a simple fallback that should work for most use cases
2832 std::filesystem::path currentPath = std::filesystem::current_path();
2833
2834 // If we're already in a build directory, use it
2835 if (std::filesystem::exists(currentPath / "plugins")) {
2836 return currentPath.string();
2837 }
2838
2839 // If we're in a subdirectory, try going up to find build directory
2840 std::filesystem::path parent = currentPath.parent_path();
2841 if (std::filesystem::exists(parent / "plugins")) {
2842 return parent.string();
2843 }
2844
2845 // Last resort: use current directory
2846 return currentPath.string();
2847}
2848
2849std::filesystem::path helios::resolveAssetPath(const std::string &relativePath) {
2850 // This function is deprecated but kept for compatibility
2851 // It now just calls resolveFilePath
2852 return resolveFilePath(relativePath);
2853}
2854
2855std::filesystem::path helios::tryResolvePluginAsset(const std::string &pluginName, const std::string &assetPath) {
2856 std::string pluginAssetPath = "plugins/" + pluginName + "/" + assetPath;
2857 return tryResolveFilePath(pluginAssetPath);
2858}
2859
2860std::filesystem::path helios::resolvePluginAsset(const std::string &pluginName, const std::string &assetPath) {
2861 std::string pluginAssetPath = "plugins/" + pluginName + "/" + assetPath;
2862 return resolveFilePath(pluginAssetPath);
2863}
2864
2865
2866std::filesystem::path helios::tryResolveFilePath(const std::string &filename) {
2867 // Non-throwing version for probing file existence
2868 if (filename.empty()) {
2869 return {};
2870 }
2871
2872 // 1. If absolute path, validate and return
2873 std::filesystem::path filepath(filename);
2874 if (filepath.is_absolute()) {
2875 if (std::filesystem::exists(filepath)) {
2876 return std::filesystem::canonical(filepath);
2877 } else {
2878 return {};
2879 }
2880 }
2881
2882 // 2. First try: Check relative to current working directory
2883 std::filesystem::path currentDirPath = std::filesystem::current_path() / filename;
2884 if (std::filesystem::exists(currentDirPath)) {
2885 return std::filesystem::canonical(currentDirPath);
2886 }
2887
2888 // 3. Second try: Resolve relative to build directory (fallback for HELIOS_BUILD)
2889 std::string buildDir = getBuildDirectory();
2890 std::filesystem::path buildDirPath = std::filesystem::path(buildDir) / filename;
2891
2892 if (std::filesystem::exists(buildDirPath)) {
2893 return std::filesystem::canonical(buildDirPath);
2894 }
2895
2896 // File not found in any location
2897 return {};
2898}
2899
2900std::filesystem::path helios::resolveFilePath(const std::string &filename) {
2901 // Handle empty string case - return current working directory
2902 if (filename.empty()) {
2903 return std::filesystem::current_path();
2904 }
2905
2906 // Try to resolve using the non-throwing version first
2907 std::filesystem::path result = tryResolveFilePath(filename);
2908
2909 if (!result.empty()) {
2910 return result;
2911 }
2912
2913 // File not found - provide clear error message
2914 std::filesystem::path currentDirPath = std::filesystem::current_path() / filename;
2915 std::string buildDir = getBuildDirectory();
2916 std::filesystem::path buildDirPath = std::filesystem::path(buildDir) / filename;
2917
2918 helios_runtime_error("ERROR (helios::resolveFilePath): Could not locate asset file: " + filename + " (checked: " + currentDirPath.string() + " and " + buildDirPath.string() + "). " +
2919 "Ensure file exists relative to current directory or HELIOS_BUILD path.");
2920 return {}; // This line should never be reached due to helios_runtime_error throwing
2921}
2922
2923std::filesystem::path helios::resolveSpectraPath(const std::string &spectraFile) {
2924 // All spectral data files should be looked for in the radiation plugin's spectral_data directory
2925 std::string spectraPath = "plugins/radiation/spectral_data/" + spectraFile;
2926 return resolveFilePath(spectraPath);
2927}
2928
2929bool helios::validateAssetPath(const std::filesystem::path &assetPath) {
2930 return std::filesystem::exists(assetPath) && std::filesystem::is_regular_file(assetPath);
2931}
2932
2933std::filesystem::path helios::findProjectRoot(const std::filesystem::path &startPath) {
2934 std::filesystem::path currentPath = std::filesystem::absolute(startPath);
2935
2936 while (!currentPath.empty() && currentPath != currentPath.parent_path()) {
2937 std::filesystem::path cmakeFile = currentPath / "CMakeLists.txt";
2938 if (std::filesystem::exists(cmakeFile) && std::filesystem::is_regular_file(cmakeFile)) {
2939 return currentPath;
2940 }
2941 currentPath = currentPath.parent_path();
2942 }
2943
2944 return {}; // Return empty path if not found
2945}
2946
2947std::filesystem::path helios::resolveProjectFile(const std::string &relativePath) {
2948 // Handle empty path
2949 if (relativePath.empty()) {
2950 helios_runtime_error("ERROR (resolveProjectFile): Cannot resolve empty file path.");
2951 }
2952
2953 // If it's already absolute, just validate and return it
2954 std::filesystem::path inputPath(relativePath);
2955 if (inputPath.is_absolute()) {
2956 if (validateAssetPath(inputPath)) {
2957 return inputPath;
2958 } else {
2959 helios_runtime_error("ERROR (resolveProjectFile): Absolute path '" + relativePath + "' does not exist or is not a regular file.");
2960 }
2961 }
2962
2963 // Strategy 1: Check current working directory
2964 std::filesystem::path cwdPath = std::filesystem::current_path() / relativePath;
2965 if (validateAssetPath(cwdPath)) {
2966 return std::filesystem::absolute(cwdPath);
2967 }
2968
2969 // Strategy 2: Check project directory
2970 std::filesystem::path projectRoot = findProjectRoot();
2971 if (!projectRoot.empty()) {
2972 std::filesystem::path projectPath = projectRoot / relativePath;
2973 if (validateAssetPath(projectPath)) {
2974 return std::filesystem::absolute(projectPath);
2975 }
2976 }
2977
2978 // Strategy 3: Error - file not found in either location
2979 std::string errorMsg = "ERROR (resolveProjectFile): Could not locate file '" + relativePath + "'. Searched in:\n";
2980 errorMsg += " - Current working directory: " + std::filesystem::current_path().string() + "\n";
2981 if (!projectRoot.empty()) {
2982 errorMsg += " - Project directory: " + projectRoot.string() + "\n";
2983 } else {
2984 errorMsg += " - Project directory: (not found - no CMakeLists.txt found in parent directories)\n";
2985 }
2986 errorMsg += "Ensure the file exists in one of these locations.";
2987
2988 helios_runtime_error(errorMsg);
2989 return {}; // Never reached due to exception
2990}
2991
2992std::vector<float> helios::importVectorFromFile(const std::string &filepath) {
2993 std::ifstream stream(filepath.c_str());
2994
2995 if (!stream.is_open()) {
2996 helios_runtime_error("ERROR (helios::importVectorFromFile): File " + filepath + " could not be opened for reading. Check that it exists and that you have permission to read it.");
2997 }
2998
2999 std::istream_iterator<float> start(stream), end;
3000 std::vector<float> vec(start, end);
3001 return vec;
3002}
3003
3004float helios::sample_Beta_distribution(float mu, float nu, std::minstd_rand0 *generator) {
3005 // 1) draw two independent Gamma variates:
3006 // X ~ Gamma(α=ν, 1), Y ~ Gamma(β=μ, 1)
3007 std::gamma_distribution<float> dist_nu(nu, 1.0);
3008 std::gamma_distribution<float> dist_mu(mu, 1.0);
3009
3010 float X = dist_nu(*generator);
3011 float Y = dist_mu(*generator);
3012
3013 // 2) form the Beta = X/(X+Y)
3014 float b = X / (X + Y);
3015
3016 // 3) rescale to θ_L = (π/2)*b
3017 return 0.5f * PI_F * b;
3018}
3019
3020// Complete elliptic integral of the first kind via the arithmetic–geometric mean (AGM)
3021float compute_elliptic_integral_first_kind(float e) {
3022 // K(e) = π / (2 * AGM(1, sqrt(1 - e^2)))
3023 float a = 1.0f;
3024 float b = std::sqrt(1.0f - e * e);
3025 for (int iter = 0; iter < 10; ++iter) {
3026 float an = 0.5f * (a + b);
3027 float bn = std::sqrt(a * b);
3028 a = an;
3029 b = bn;
3030 }
3031 return PI_F / (2.0f * a);
3032}
3033
3034// Ellipsoidal PDF for leaf azimuth distribution
3035// phi: sample angle [0,2π), e: eccentricity, phi0: rotation offset, K_e: precomputed ellip. integral
3036float evaluate_ellipsoidal_azimuth_PDF(float phi, float e, float phi0, float K_e) {
3037 float d = phi - phi0;
3038 float c2 = (1.f - e * e) * std::cos(d) * std::cos(d) + std::sin(d) * std::sin(d);
3039 return 1.f / (4.f * K_e * std::sqrt(c2));
3040}
3041
3042// Sample phi from ellipsoidal distribution via rejection sampling
3043float helios::sample_ellipsoidal_azimuth(float e, float phi0_degrees, std::minstd_rand0 *generator) {
3044 // sanity‐check
3045 if (e < 0.f || e > 1.f) {
3046 helios_runtime_error("ERROR (helios::sample_ellipsoidal_azimuth): Eccentricity must be in [0,1].");
3047 }
3048
3049 // convert rotation offset to radians
3050 float phi0 = deg2rad(phi0_degrees);
3051
3052 // ellipse semiaxes: a=1, b = sqrt(1 - e^2)
3053 float a = 1.f;
3054 float b = std::sqrt(1.f - e * e);
3055
3056 // sample the ellipse parameter t uniformly in [0,2π)
3057 std::uniform_real_distribution<float> distT(0.f, 2.f * PI_F);
3058 float t = distT(*generator);
3059
3060 // point on the ellipse boundary
3061 float x = a * std::cos(t);
3062 float y = b * std::sin(t);
3063
3064 // compute its polar angle
3065 float phi = std::atan2(y, x) + phi0;
3066
3067 // wrap into [0,2π)
3068 if (phi < 0.f)
3069 phi += 2.f * PI_F;
3070 else if (phi >= 2.f * PI_F)
3071 phi -= 2.f * PI_F;
3072
3073 return phi;
3074}
3075
3076std::vector<float> helios::linspace(float start, float end, int num) {
3077 if (num <= 0) {
3078 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
3079 }
3080
3081 if (num == 1) {
3082 return {start};
3083 }
3084
3085 std::vector<float> result(num);
3086 float step = (end - start) / (num - 1);
3087
3088 for (int i = 0; i < num; ++i) {
3089 result[i] = start + i * step;
3090 }
3091
3092 result[num - 1] = end;
3093
3094 return result;
3095}
3096
3097std::vector<vec2> helios::linspace(const vec2 &start, const vec2 &end, int num) {
3098 if (num <= 0) {
3099 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
3100 }
3101
3102 if (num == 1) {
3103 return {start};
3104 }
3105
3106 std::vector<vec2> result(num);
3107 vec2 step = (end - start) / float(num - 1);
3108
3109 for (int i = 0; i < num; ++i) {
3110 result[i] = start + step * float(i);
3111 }
3112
3113 result[num - 1] = end;
3114
3115 return result;
3116}
3117
3118std::vector<vec3> helios::linspace(const vec3 &start, const vec3 &end, int num) {
3119 if (num <= 0) {
3120 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
3121 }
3122
3123 if (num == 1) {
3124 return {start};
3125 }
3126
3127 std::vector<vec3> result(num);
3128 vec3 step = (end - start) / float(num - 1);
3129
3130 for (int i = 0; i < num; ++i) {
3131 result[i] = start + step * float(i);
3132 }
3133
3134 result[num - 1] = end;
3135
3136 return result;
3137}
3138
3139std::vector<vec4> helios::linspace(const vec4 &start, const vec4 &end, int num) {
3140 if (num <= 0) {
3141 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
3142 }
3143
3144 if (num == 1) {
3145 return {start};
3146 }
3147
3148 std::vector<vec4> result(num);
3149 vec4 step = (end - start) / float(num - 1);
3150
3151 for (int i = 0; i < num; ++i) {
3152 result[i] = start + step * float(i);
3153 }
3154
3155 result[num - 1] = end;
3156
3157 return result;
3158}
3159
3160// float helios::sample_ellipsoidal_azimuth(
3161// float e,
3162// float phi0_degrees,
3163// std::minstd_rand0 *generator
3164// ) {
3165// // 1) sanity‐check
3166// if (e < 0.f || e > 1.f) {
3167// helios_runtime_error(
3168// "ERROR (helios::sample_ellipsoidal_azimuth): "
3169// "eccentricity must be in [0,1]."
3170// );
3171// }
3172//
3173// // 2) trivial uniform case
3174// std::uniform_real_distribution<float> distPhi(0.f, 2.f * PI_F);
3175// if (e == 0.f) {
3176// return distPhi(*generator);
3177// }
3178//
3179// // 3) precompute rotation offset
3180// float phi0 = deg2rad(phi0_degrees);
3181//
3182// // 4) rejection sampling: envelope = uniform φ, accept with ratio = (1–e²)/denominator
3183// std::uniform_real_distribution<float> dist01(0.f, 1.f);
3184//--------- WarningAggregator Implementation ---------//
3185
3186void helios::WarningAggregator::addWarning(const std::string &category, const std::string &message) {
3187 if (!enabled_) {
3188 return;
3189 }
3190
3191 std::lock_guard<std::mutex> lock(mutex_);
3192
3193 // Increment total count (always)
3194 counts_[category]++;
3195
3196 // Only store up to MAX_EXAMPLES messages to prevent memory issues
3197 auto &messages = warnings_[category];
3198 if (messages.size() < MAX_EXAMPLES) {
3199 messages.push_back(message);
3200 }
3201}
3202
3203void helios::WarningAggregator::report(std::ostream &stream, bool compact) {
3204 std::lock_guard<std::mutex> lock(mutex_);
3205
3206 if (counts_.empty()) {
3207 return; // Nothing to report
3208 }
3209
3210 // Report each category
3211 for (const auto &entry: counts_) {
3212 const std::string &category = entry.first;
3213 size_t count = entry.second;
3214
3215 stream << "WARNING: " << count << " instance" << (count > 1 ? "s" : "") << " of '" << category << "'";
3216
3217 if (!compact) {
3218 // Original behavior: show examples
3219 const auto &messages = warnings_[category];
3220
3221 // Show first few examples
3222 size_t examples_to_show = std::min(size_t(3), messages.size());
3223 stream << " (showing first " << examples_to_show << "):" << std::endl;
3224
3225 for (size_t i = 0; i < examples_to_show; ++i) {
3226 stream << " - " << messages[i] << std::endl;
3227 }
3228
3229 if (count > MAX_EXAMPLES) {
3230 stream << " (Note: More than " << MAX_EXAMPLES << " warnings of this type were encountered)" << std::endl;
3231 }
3232 stream << std::endl;
3233 } else {
3234 // Compact mode: just the count
3235 stream << std::endl;
3236 }
3237 }
3238
3239 // Clear after reporting
3240 warnings_.clear();
3241 counts_.clear();
3242}
3243
3244size_t helios::WarningAggregator::getCount(const std::string &category) const {
3245 std::lock_guard<std::mutex> lock(mutex_);
3246
3247 auto it = counts_.find(category);
3248 if (it != counts_.end()) {
3249 return it->second;
3250 }
3251 return 0;
3252}
3253
3255 std::lock_guard<std::mutex> lock(mutex_);
3256 warnings_.clear();
3257 counts_.clear();
3258}
3259
3261 enabled_ = enabled;
3262}
3263
3265 return enabled_;
3266}
3267
3268// while (true) {
3269// float phi = distPhi(*generator);
3270// float d = phi - phi0;
3271// // wrap to [–π,π) for numerical stability
3272// if (d < -PI_F) d += 2.f*PI_F;
3273// else if (d >= PI_F) d -= 2.f*PI_F;
3274//
3275// // denominator = (1–e²)·cos²d + sin²d
3276// float c = std::cos(d), s = std::sin(d);
3277// float denom = (1.f - e*e)*c*c + s*s;
3278//
3279// // acceptance ratio ∈ (0,1]
3280// float ratio = (1.f - e*e) / denom;
3281//
3282// if (dist01(*generator) <= ratio) {
3283// // wrap phi back into [0,2π)
3284// if (phi < 0.f) phi += 2.f*PI_F;
3285// else if (phi >= 2.f*PI_F) phi -= 2.f*PI_F;
3286// return phi;
3287// }
3288// // otherwise retry
3289// }
3290// }