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