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