1.3.49
 
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 return {radius, asin_safe(Cartesian.z / radius), atan2_2pi(Cartesian.x, Cartesian.y)};
758}
759
761 return {Spherical.radius * cosf(Spherical.elevation) * sinf(Spherical.azimuth), Spherical.radius * cosf(Spherical.elevation) * cosf(Spherical.azimuth), Spherical.radius * sinf(Spherical.elevation)};
762}
763
764vec2 helios::string2vec2(const char *str) {
765 float o[2];
766 std::string tmp;
767
768 std::istringstream stream(str);
769 int c = 0;
770 while (stream >> tmp && c < 2) {
771 if (!parse_float(tmp, o[c])) {
772 helios_runtime_error("ERROR (string2vec2): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
773 }
774 c++;
775 }
776
777 if (c < 2) {
778 helios_runtime_error("ERROR (string2vec2): Insufficient values in input string '" + std::string(str) + "'. Expected 2 values, got " + std::to_string(c));
779 }
780
781 return make_vec2(o[0], o[1]);
782}
783
784vec3 helios::string2vec3(const char *str) {
785 float o[3];
786 std::string tmp;
787
788 std::istringstream stream(str);
789 int c = 0;
790 while (stream >> tmp && c < 3) {
791 if (!parse_float(tmp, o[c])) {
792 helios_runtime_error("ERROR (string2vec3): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
793 }
794 c++;
795 }
796
797 if (c < 3) {
798 helios_runtime_error("ERROR (string2vec3): Insufficient values in input string '" + std::string(str) + "'. Expected 3 values, got " + std::to_string(c));
799 }
800
801 return make_vec3(o[0], o[1], o[2]);
802}
803
804vec4 helios::string2vec4(const char *str) {
805 float o[4];
806 std::string tmp;
807
808 std::istringstream stream(str);
809 int c = 0;
810 while (stream >> tmp && c < 4) {
811 if (!parse_float(tmp, o[c])) {
812 helios_runtime_error("ERROR (string2vec4): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
813 }
814 c++;
815 }
816
817 if (c < 4) {
818 helios_runtime_error("ERROR (string2vec4): Insufficient values in input string '" + std::string(str) + "'. Expected 4 values, got " + std::to_string(c));
819 }
820
821 return make_vec4(o[0], o[1], o[2], o[3]);
822}
823
824int2 helios::string2int2(const char *str) {
825 int o[2];
826 std::string tmp;
827
828 std::istringstream stream(str);
829 int c = 0;
830 while (stream >> tmp && c < 2) {
831 if (!parse_int(tmp, o[c])) {
832 helios_runtime_error("ERROR (string2int2): Invalid int value '" + tmp + "' in input string '" + std::string(str) + "'");
833 }
834 c++;
835 }
836
837 if (c < 2) {
838 helios_runtime_error("ERROR (string2int2): Insufficient values in input string '" + std::string(str) + "'. Expected 2 values, got " + std::to_string(c));
839 }
840
841 return make_int2(o[0], o[1]);
842}
843
844int3 helios::string2int3(const char *str) {
845 int o[3];
846 std::string tmp;
847
848 std::istringstream stream(str);
849 int c = 0;
850 while (stream >> tmp && c < 3) {
851 if (!parse_int(tmp, o[c])) {
852 helios_runtime_error("ERROR (string2int3): Invalid int value '" + tmp + "' in input string '" + std::string(str) + "'");
853 }
854 c++;
855 }
856
857 if (c < 3) {
858 helios_runtime_error("ERROR (string2int3): Insufficient values in input string '" + std::string(str) + "'. Expected 3 values, got " + std::to_string(c));
859 }
860
861 return make_int3(o[0], o[1], o[2]);
862}
863
864int4 helios::string2int4(const char *str) {
865 int o[4];
866 std::string tmp;
867
868 std::istringstream stream(str);
869 int c = 0;
870 while (stream >> tmp && c < 4) {
871 if (!parse_int(tmp, o[c])) {
872 helios_runtime_error("ERROR (string2int4): Invalid int value '" + tmp + "' in input string '" + std::string(str) + "'");
873 }
874 c++;
875 }
876
877 if (c < 4) {
878 helios_runtime_error("ERROR (string2int4): Insufficient values in input string '" + std::string(str) + "'. Expected 4 values, got " + std::to_string(c));
879 }
880
881 return make_int4(o[0], o[1], o[2], o[3]);
882}
883
885 float o[4] = {0, 0, 0, 1}; // Keep default alpha of 1
886 std::string tmp;
887
888 std::istringstream stream(str);
889 int c = 0;
890 while (stream >> tmp) {
891 if (c >= 4) {
892 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 + "'");
893 }
894
895 if (!parse_float(tmp, o[c])) {
896 helios_runtime_error("ERROR (string2RGBcolor): Invalid float value '" + tmp + "' in input string '" + std::string(str) + "'");
897 }
898 c++;
899 }
900
901 if (c < 3) {
902 helios_runtime_error("ERROR (string2RGBcolor): Insufficient values in input string '" + std::string(str) + "'. Expected at least 3 values (RGB), got " + std::to_string(c));
903 }
904
905 return make_RGBAcolor(o[0], o[1], o[2], o[3]);
906}
907
908bool helios::parse_float(const std::string &input_string, float &converted_float) {
909 try {
910 size_t read = 0;
911 std::string str = trim_whitespace(input_string);
912 double converted_double = std::stod(str, &read);
913 converted_float = (float) converted_double;
914 if (str.size() != read)
915 return false;
916 } catch (std::invalid_argument &e) {
917 return false;
918 }
919 return true;
920}
921
922bool helios::parse_double(const std::string &input_string, double &converted_double) {
923 try {
924 size_t read = 0;
925 std::string str = trim_whitespace(input_string);
926 converted_double = std::stod(str, &read);
927 if (str.size() != read)
928 return false;
929 } catch (std::invalid_argument &e) {
930 return false;
931 }
932 return true;
933}
934
935bool helios::parse_int(const std::string &input_string, int &converted_int) {
936 try {
937 size_t read = 0;
938 std::string str = trim_whitespace(input_string);
939 converted_int = std::stoi(str, &read);
940 if (str.size() != read)
941 return false;
942 } catch (std::invalid_argument &e) {
943 return false;
944 }
945 return true;
946}
947
948bool helios::parse_int2(const std::string &input_string, int2 &converted_int2) {
949 std::istringstream vecstream(input_string);
950 std::vector<std::string> tmp_s(2);
951 vecstream >> tmp_s[0];
952 vecstream >> tmp_s[1];
953 int2 tmp;
954 if (!parse_int(tmp_s[0], tmp.x) || !parse_int(tmp_s[1], tmp.y)) {
955 return false;
956 } else {
957 converted_int2 = tmp;
958 }
959 return true;
960}
961
962bool helios::parse_int3(const std::string &input_string, int3 &converted_int3) {
963 std::istringstream vecstream(input_string);
964 std::vector<std::string> tmp_s(3);
965 vecstream >> tmp_s[0];
966 vecstream >> tmp_s[1];
967 vecstream >> tmp_s[2];
968 int3 tmp;
969 if (!parse_int(tmp_s[0], tmp.x) || !parse_int(tmp_s[1], tmp.y) || !parse_int(tmp_s[2], tmp.z)) {
970 return false;
971 } else {
972 converted_int3 = tmp;
973 }
974 return true;
975}
976
977bool helios::parse_uint(const std::string &input_string, uint &converted_uint) {
978 try {
979 size_t read = 0;
980 std::string str = trim_whitespace(input_string);
981 int converted_int = std::stoi(str, &read);
982 if (str.size() != read || converted_int < 0) {
983 return false;
984 } else {
985 converted_uint = (uint) converted_int;
986 }
987 } catch (std::invalid_argument &e) {
988 return false;
989 }
990 return true;
991}
992
993bool helios::parse_vec2(const std::string &input_string, vec2 &converted_vec2) {
994 std::istringstream vecstream(input_string);
995 std::vector<std::string> tmp_s(2);
996 vecstream >> tmp_s[0];
997 vecstream >> tmp_s[1];
998 vec2 tmp;
999 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y)) {
1000 return false;
1001 } else {
1002 converted_vec2 = tmp;
1003 }
1004 return true;
1005}
1006
1007bool helios::parse_vec3(const std::string &input_string, vec3 &converted_vec3) {
1008 std::istringstream vecstream(input_string);
1009 std::vector<std::string> tmp_s(3);
1010 vecstream >> tmp_s[0];
1011 vecstream >> tmp_s[1];
1012 vecstream >> tmp_s[2];
1013 vec3 tmp;
1014 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y) || !parse_float(tmp_s[2], tmp.z)) {
1015 return false;
1016 } else {
1017 converted_vec3 = tmp;
1018 }
1019 return true;
1020}
1021
1022bool helios::parse_RGBcolor(const std::string &input_string, RGBcolor &converted_rgb) {
1023 std::istringstream vecstream(input_string);
1024 std::vector<std::string> tmp_s(3);
1025 vecstream >> tmp_s[0];
1026 vecstream >> tmp_s[1];
1027 vecstream >> tmp_s[2];
1028 RGBcolor tmp;
1029 if (!parse_float(tmp_s[0], tmp.r) || !parse_float(tmp_s[1], tmp.g) || !parse_float(tmp_s[2], tmp.b)) {
1030 return false;
1031 } else {
1032 if (tmp.r < 0 || tmp.g < 0 || tmp.b < 0 || tmp.r > 1.f || tmp.g > 1.f || tmp.b > 1.f) {
1033 return false;
1034 }
1035 converted_rgb = tmp;
1036 }
1037 return true;
1038}
1039
1040bool helios::open_xml_file(const std::string &xml_file, pugi::xml_document &xmldoc, std::string &error_string) {
1041 const std::string &fn = xml_file;
1042 std::string ext = getFileExtension(xml_file);
1043 if (ext != ".xml" && ext != ".XML") {
1044 error_string = "XML file " + fn + " is not XML format.";
1045 return false;
1046 }
1047
1048 // Resolve file path using the build directory path resolution system
1049 std::filesystem::path resolvedPath;
1050 try {
1051 resolvedPath = resolveFilePath(xml_file);
1052 } catch (const std::runtime_error &e) {
1053 error_string = std::string(e.what());
1054 return false;
1055 }
1056
1057 // load file
1058 pugi::xml_parse_result load_result = xmldoc.load_file(resolvedPath.string().c_str());
1059
1060 // error checking
1061 if (!load_result) {
1062 error_string = "XML file " + xml_file + " parsed with errors: " + load_result.description();
1063 return false;
1064 }
1065
1066 pugi::xml_node helios = xmldoc.child("helios");
1067
1068 if (helios.empty()) {
1069 error_string = "XML file " + xml_file + " does not have tag '<helios> ... </helios>' bounding all other tags.";
1070 return false;
1071 }
1072
1073 return true;
1074}
1075
1076int helios::parse_xml_tag_int(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1077 std::string value_string = node.child_value();
1078 if (value_string.empty()) {
1079 return 0;
1080 }
1081 int value;
1082 if (!parse_int(value_string, value)) {
1083 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' integer value.");
1084 }
1085 return value;
1086}
1087
1088float helios::parse_xml_tag_float(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1089 std::string value_string = node.child_value();
1090 if (value_string.empty()) {
1091 return 0;
1092 }
1093 float value;
1094 if (!parse_float(value_string, value)) {
1095 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' float value.");
1096 }
1097 return value;
1098}
1099
1100vec2 helios::parse_xml_tag_vec2(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1101 std::string value_string = node.child_value();
1102 if (value_string.empty()) {
1103 return {0, 0};
1104 }
1105 vec2 value;
1106 if (!parse_vec2(value_string, value)) {
1107 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' vec2 value.");
1108 }
1109 return value;
1110}
1111
1112vec3 helios::parse_xml_tag_vec3(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1113 std::string value_string = node.child_value();
1114 if (value_string.empty()) {
1115 return {0, 0, 0};
1116 }
1117 vec3 value;
1118 if (!parse_vec3(value_string, value)) {
1119 helios_runtime_error("ERROR (" + calling_function + "): Could not parse tag '" + tag + "' vec3 value.");
1120 }
1121 return value;
1122}
1123
1124std::string helios::parse_xml_tag_string(const pugi::xml_node &node, const std::string &tag, const std::string &calling_function) {
1125 return deblank(node.child_value());
1126}
1127
1128std::string helios::deblank(const char *input) {
1129 std::string out;
1130 out.reserve(std::strlen(input));
1131 for (const char *p = input; *p; ++p) {
1132 if (*p != ' ') {
1133 out.push_back(*p);
1134 }
1135 }
1136 return out;
1137}
1138
1139std::string helios::deblank(const std::string &input) {
1140 return deblank(input.c_str());
1141}
1142
1143std::string helios::trim_whitespace(const std::string &input) {
1144 static const std::string WHITESPACE = " \n\r\t\f\v";
1145
1146 // Find first non-whitespace character
1147 size_t start = input.find_first_not_of(WHITESPACE);
1148 if (start == std::string::npos) {
1149 return ""; // String is all whitespace
1150 }
1151
1152 // Find last non-whitespace character
1153 size_t end = input.find_last_not_of(WHITESPACE);
1154
1155 // Return the trimmed substring
1156 return input.substr(start, end - start + 1);
1157}
1158
1159std::vector<std::string> helios::separate_string_by_delimiter(const std::string &inputstring, const std::string &delimiter) {
1160 std::vector<std::string> separated_string;
1161
1162 // Handle empty delimiter case
1163 if (delimiter.empty()) {
1164 helios_runtime_error("ERROR (helios::separate_string_by_delimiter): Delimiter cannot be an empty string.");
1165 }
1166
1167 size_t pos = 0;
1168 size_t found;
1169 size_t max_characters = inputstring.size();
1170 size_t iter = 0;
1171 while ((found = inputstring.find(delimiter, pos)) != std::string::npos && iter <= max_characters) {
1172 separated_string.push_back(trim_whitespace(inputstring.substr(pos, found - pos)));
1173 pos = found + delimiter.size();
1174 iter++;
1175 }
1176
1177 // add the remaining part (including case of no delimiter found)
1178 separated_string.push_back(trim_whitespace(inputstring.substr(pos)));
1179
1180 return separated_string;
1181}
1182
1183float helios::sum(const std::vector<float> &vect) {
1184 if (vect.empty()) {
1185 helios_runtime_error("ERROR (sum): Vector is empty.");
1186 }
1187
1188 float m = 0;
1189 for (float i: vect) {
1190 m += i;
1191 }
1192
1193 return m;
1194}
1195
1196float helios::mean(const std::vector<float> &vect) {
1197 if (vect.empty()) {
1198 helios_runtime_error("ERROR (mean): Vector is empty.");
1199 }
1200
1201 float m = 0;
1202 for (float i: vect) {
1203 m += i;
1204 }
1205 m /= float(vect.size());
1206
1207 return m;
1208}
1209
1210float helios::min(const std::vector<float> &vect) {
1211 if (vect.empty()) {
1212 helios_runtime_error("ERROR (min): Vector is empty.");
1213 }
1214
1215 return *std::min_element(vect.begin(), vect.end());
1216}
1217
1218int helios::min(const std::vector<int> &vect) {
1219 if (vect.empty()) {
1220 helios_runtime_error("ERROR (min): Vector is empty.");
1221 }
1222
1223 return *std::min_element(vect.begin(), vect.end());
1224}
1225
1226vec3 helios::min(const std::vector<vec3> &vect) {
1227 if (vect.empty()) {
1228 helios_runtime_error("ERROR (min): Vector is empty.");
1229 }
1230
1231 vec3 vmin = vect.at(0);
1232
1233 for (int i = 1; i < vect.size(); i++) {
1234 if (vect.at(i).x < vmin.x) {
1235 vmin.x = vect.at(i).x;
1236 }
1237 if (vect.at(i).y < vmin.y) {
1238 vmin.y = vect.at(i).y;
1239 }
1240 if (vect.at(i).z < vmin.z) {
1241 vmin.z = vect.at(i).z;
1242 }
1243 }
1244
1245 return vmin;
1246}
1247
1248float helios::max(const std::vector<float> &vect) {
1249 if (vect.empty()) {
1250 helios_runtime_error("ERROR (max): Vector is empty.");
1251 }
1252
1253 return *std::max_element(vect.begin(), vect.end());
1254}
1255
1256int helios::max(const std::vector<int> &vect) {
1257 if (vect.empty()) {
1258 helios_runtime_error("ERROR (max): Vector is empty.");
1259 }
1260
1261 return *std::max_element(vect.begin(), vect.end());
1262}
1263
1264vec3 helios::max(const std::vector<vec3> &vect) {
1265 if (vect.empty()) {
1266 helios_runtime_error("ERROR (max): Vector is empty.");
1267 }
1268
1269 vec3 vmax = vect.at(0);
1270
1271 for (int i = 1; i < vect.size(); i++) {
1272 if (vect.at(i).x > vmax.x) {
1273 vmax.x = vect.at(i).x;
1274 }
1275 if (vect.at(i).y > vmax.y) {
1276 vmax.y = vect.at(i).y;
1277 }
1278 if (vect.at(i).z > vmax.z) {
1279 vmax.z = vect.at(i).z;
1280 }
1281 }
1282
1283 return vmax;
1284}
1285
1286float helios::stdev(const std::vector<float> &vect) {
1287 if (vect.empty()) {
1288 helios_runtime_error("ERROR (stdev): Vector is empty.");
1289 }
1290
1291 size_t size = vect.size();
1292
1293 float m = 0;
1294 for (float i: vect) {
1295 m += i;
1296 }
1297 m /= float(size);
1298
1299 float stdev = 0;
1300 for (float i: vect) {
1301 stdev += powf(i - m, 2.0);
1302 }
1303
1304 return sqrtf(stdev / float(size));
1305}
1306
1307float helios::median(std::vector<float> vect) {
1308 if (vect.empty()) {
1309 helios_runtime_error("ERROR (median): Vector is empty.");
1310 }
1311
1312 size_t size = vect.size();
1313
1314 sort(vect.begin(), vect.end());
1315
1316 size_t middle_index = size / 2;
1317
1318 float median;
1319 if (size % 2 == 0) {
1320 median = (vect.at(middle_index) + vect.at(middle_index - 1)) / 2.f;
1321 } else {
1322 median = vect.at(middle_index);
1323 }
1324 return median;
1325}
1326
1327Date helios::CalendarDay(int Julian_day, int year) {
1328 // ----------------------------- input checks ----------------------------
1329 if (Julian_day < 1 || Julian_day > 366)
1330 helios_runtime_error("ERROR (CalendarDay): Julian day out of range [1–366].");
1331
1332 if (year < 1000)
1333 helios_runtime_error("ERROR (CalendarDay): Year must be given in YYYY format.");
1334
1335 const bool leap = (year % 4 == 0 && year % 100 != 0) || // divisible by 4 but not by 100
1336 (year % 400 == 0); // or divisible by 400
1337
1338 if (!leap && Julian_day == 366)
1339 helios_runtime_error("ERROR (CalendarDay): Day 366 occurs only in leap years.");
1340
1341 // ------------------- month lengths for the chosen year -----------------
1342 // Index 0 = January, …, 11 = December
1343 int month_lengths[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1344 if (leap) // adjust February
1345 month_lengths[1] = 29;
1346
1347 // --------------------------- computation ------------------------------
1348 int d_remaining = Julian_day; // days still to account for
1349 int month = 1; // 1‑based calendar month
1350
1351 // subtract complete months until the remainder lies in the current month
1352 for (int i = 0; i < 12; ++i) {
1353 if (d_remaining > month_lengths[i]) {
1354 d_remaining -= month_lengths[i];
1355 ++month;
1356 } else {
1357 break;
1358 }
1359 }
1360
1361 // d_remaining is now the calendar day of the computed month
1362 return make_Date(d_remaining, month, year);
1363}
1364
1365
1366int helios::JulianDay(int day, int month, int year) {
1367 return JulianDay(make_Date(day, month, year));
1368}
1369
1370int helios::JulianDay(const Date &date) {
1371 int day = date.day;
1372 int month = date.month;
1373 int year = date.year;
1374
1375 // Validate inputs
1376 if (month < 1 || month > 12) {
1377 helios_runtime_error("ERROR (JulianDay): Month of year is out of range (month of " + std::to_string(month) + " was given).");
1378 }
1379
1380 // Get the correct number of days for the month (accounting for leap year in February)
1381 int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
1382
1383 // Correct leap year calculation
1384 if (bool isLeapYear = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
1385 daysInMonth[2] = 29;
1386 }
1387
1388 if (day < 1 || day > daysInMonth[month]) {
1389 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) + ").");
1390 }
1391
1392 if (year < 1000) {
1393 helios_runtime_error("ERROR (JulianDay): Year should be specified in YYYY format.");
1394 }
1395
1396 // Calculate day of year
1397 int dayOfYear = day;
1398 for (int m = 1; m < month; m++) {
1399 dayOfYear += daysInMonth[m];
1400 }
1401
1402 return dayOfYear;
1403}
1404
1405bool helios::PNGHasAlpha(const char *filename) {
1406 if (!filename) {
1407 helios_runtime_error("ERROR (PNGHasAlpha): Null filename provided.");
1408 }
1409
1410 std::string fn(filename);
1411 auto dot_pos = fn.find_last_of('.');
1412 if (dot_pos == std::string::npos) {
1413 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " has no extension.");
1414 }
1415 std::string ext = fn.substr(dot_pos + 1);
1416 if (ext != "png" && ext != "PNG") {
1417 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " is not PNG format.");
1418 }
1419
1420 // 3) Open file with RAII
1421 auto fileCloser = [](FILE *f) {
1422 if (f)
1423 std::fclose(f);
1424 };
1425 std::unique_ptr<FILE, decltype(fileCloser)> fp(std::fopen(fn.c_str(), "rb"), fileCloser);
1426 if (!fp) {
1427 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " could not be opened for reading.");
1428 }
1429
1430 // 4) Read & validate PNG signature
1431 unsigned char header[8];
1432 if (std::fread(header, 1, 8, fp.get()) != 8 || png_sig_cmp(header, 0, 8)) {
1433 helios_runtime_error("ERROR (PNGHasAlpha): File " + fn + " is not a valid PNG file.");
1434 }
1435
1436 png_structp png_ptr = nullptr;
1437 png_infop info_ptr = nullptr;
1438
1439 try {
1440 // 5) Create libpng read & info structs
1441 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1442 if (!png_ptr) {
1443 throw std::runtime_error("png_create_read_struct failed.");
1444 }
1445 info_ptr = png_create_info_struct(png_ptr);
1446 if (!info_ptr) {
1447 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1448 throw std::runtime_error("png_create_info_struct failed.");
1449 }
1450
1451 // 6) Error handling via setjmp
1452 if (setjmp(png_jmpbuf(png_ptr))) {
1453 throw std::runtime_error("Error during PNG initialization.");
1454 }
1455
1456 // 7) Initialize IO & read info
1457 png_init_io(png_ptr, fp.get());
1458 png_set_sig_bytes(png_ptr, 8);
1459 png_read_info(png_ptr, info_ptr);
1460
1461 // 8) Inspect color type and tRNS chunk
1462 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1463 bool has_tRNS = png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) != 0;
1464
1465 // 9) Determine alpha presence
1466 bool has_alpha = ((color_type & PNG_COLOR_MASK_ALPHA) != 0) || has_tRNS;
1467
1468 // 10) Clean up libpng structs
1469 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1470
1471 return has_alpha;
1472 } catch (const std::exception &e) {
1473 // Ensure libpng structs are freed on error
1474 if (png_ptr) {
1475 if (info_ptr)
1476 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1477 else
1478 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1479 }
1480 helios_runtime_error(std::string("ERROR (PNGHasAlpha): ") + e.what());
1481 }
1482
1483 // Should never reach here
1484 return false;
1485}
1486
1487std::vector<std::vector<bool>> helios::readPNGAlpha(const std::string &filename) {
1488 const std::string &fn = filename;
1489 auto dot = fn.find_last_of('.');
1490 if (dot == std::string::npos) {
1491 helios_runtime_error("ERROR (readPNGAlpha): File " + fn + " has no extension.");
1492 }
1493 std::string ext = fn.substr(dot + 1);
1494 if (ext != "png" && ext != "PNG") {
1495 helios_runtime_error("ERROR (readPNGAlpha): File " + fn + " is not PNG format.");
1496 }
1497
1498 int y;
1499
1500 std::vector<std::vector<bool>> mask;
1501
1502 png_structp png_ptr;
1503 png_infop info_ptr;
1504
1505 char header[8]; // 8 is the maximum size that can be checked
1506
1507 /* open file and test for it being a png */
1508 FILE *fp = fopen(filename.c_str(), "rb");
1509 if (!fp) {
1510 helios_runtime_error("ERROR (readPNGAlpha): File " + std::string(filename) + " could not be opened for reading.");
1511 }
1512 size_t head = fread(header, 1, 8, fp);
1513 // if (png_sig_cmp(header, 0, 8)){
1514 // std::cerr << "ERROR (read_png_alpha): File " << filename << " is not recognized as a PNG file." << std::endl;
1515 // exit(EXIT_FAILURE);
1516 // }
1517
1518 /* initialize stuff */
1519 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1520
1521 if (!png_ptr) {
1522 helios_runtime_error("ERROR (readPNGAlpha): png_create_read_struct failed.");
1523 }
1524
1525 info_ptr = png_create_info_struct(png_ptr);
1526 if (!info_ptr) {
1527 helios_runtime_error("ERROR (readPNGAlpha): png_create_info_struct failed.");
1528 }
1529
1530 if (setjmp(png_jmpbuf(png_ptr))) {
1531 helios_runtime_error("ERROR (readPNGAlpha): init_io failed.");
1532 }
1533
1534 png_init_io(png_ptr, fp);
1535 png_set_sig_bytes(png_ptr, 8);
1536
1537 png_read_info(png_ptr, info_ptr);
1538
1539 uint width = png_get_image_width(png_ptr, info_ptr);
1540 uint height = png_get_image_height(png_ptr, info_ptr);
1541 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1542 bool has_alpha = (color_type & PNG_COLOR_MASK_ALPHA) != 0 || png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) != 0;
1543
1544 mask.resize(height);
1545 for (uint i = 0; i < height; i++) {
1546 mask.at(i).resize(width);
1547 }
1548
1549 if (!has_alpha) {
1550 for (uint j = 0; j < height; ++j) {
1551 std::fill(mask.at(j).begin(), mask.at(j).end(), true);
1552 }
1553 fclose(fp);
1554 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1555 return mask;
1556 }
1557
1558 // number_of_passes = png_set_interlace_handling(png_ptr);
1559 png_read_update_info(png_ptr, info_ptr);
1560
1561 /* read file */
1562 if (setjmp(png_jmpbuf(png_ptr))) {
1563 helios_runtime_error("ERROR (readPNGAlpha): read_image failed.");
1564 }
1565
1566 auto *row_pointers = (png_bytep *) malloc(sizeof(png_bytep) * height);
1567 for (y = 0; y < height; y++)
1568 row_pointers[y] = (png_byte *) malloc(png_get_rowbytes(png_ptr, info_ptr));
1569
1570 png_read_image(png_ptr, row_pointers);
1571
1572 fclose(fp);
1573
1574 for (uint j = 0; j < height; j++) {
1575 png_byte *row = row_pointers[j];
1576 for (int i = 0; i < width; i++) {
1577 png_byte *ba = &(row[i * 4]);
1578 float alpha = ba[3];
1579 if (alpha < 250) {
1580 mask.at(j).at(i) = false;
1581 } else {
1582 mask.at(j).at(i) = true;
1583 }
1584 }
1585 }
1586
1587 for (y = 0; y < height; y++)
1588 png_free(png_ptr, row_pointers[y]);
1589 png_free(png_ptr, row_pointers);
1590 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1591
1592
1593 return mask;
1594}
1595
1596void helios::readPNG(const std::string &filename, uint &width, uint &height, std::vector<helios::RGBAcolor> &texture) {
1597 // 1) Safe extension check
1598 auto ext_pos = filename.find_last_of('.');
1599 if (ext_pos == std::string::npos) {
1600 helios_runtime_error("ERROR (readPNG): File " + filename + " has no extension.");
1601 }
1602 std::string ext = filename.substr(ext_pos + 1);
1603 std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); });
1604 if (ext != "png") {
1605 helios_runtime_error("ERROR (readPNG): File " + filename + " is not PNG format.");
1606 }
1607
1608 png_structp png_ptr = nullptr;
1609 png_infop info_ptr = nullptr;
1610
1611 try {
1612 //
1613 // 2) RAII for FILE*
1614 //
1615 auto fileDeleter = [](FILE *f) {
1616 if (f)
1617 fclose(f);
1618 };
1619 std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen(filename.c_str(), "rb"), fileDeleter);
1620 if (!fp) {
1621 throw std::runtime_error("File " + filename + " could not be opened.");
1622 }
1623
1624 // 3) Read & validate PNG signature
1625 unsigned char header[8];
1626 if (fread(header, 1, 8, fp.get()) != 8) {
1627 throw std::runtime_error("Failed to read PNG header from " + filename);
1628 }
1629 if (png_sig_cmp(header, 0, 8)) {
1630 throw std::runtime_error("File " + filename + " is not a valid PNG.");
1631 }
1632
1633 // 4) Create libpng structs
1634 png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1635 if (!png_ptr) {
1636 throw std::runtime_error("Failed to create PNG read struct.");
1637 }
1638 info_ptr = png_create_info_struct(png_ptr);
1639 if (!info_ptr) {
1640 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1641 throw std::runtime_error("Failed to create PNG info struct.");
1642 }
1643
1644 // 5) libpng error handling
1645 if (setjmp(png_jmpbuf(png_ptr))) {
1646 throw std::runtime_error("Error during PNG initialization.");
1647 }
1648
1649 // 6) Set up IO & read basic info
1650 png_init_io(png_ptr, fp.get());
1651 png_set_sig_bytes(png_ptr, 8);
1652 png_read_info(png_ptr, info_ptr);
1653
1654 // 7) Transformations → strip 16-bit, expand palette/gray, add alpha
1655 png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
1656 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
1657
1658 if (bit_depth == 16) {
1659 png_set_strip_16(png_ptr);
1660 }
1661 if (color_type == PNG_COLOR_TYPE_PALETTE) {
1662 png_set_palette_to_rgb(png_ptr);
1663 }
1664 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
1665 png_set_expand_gray_1_2_4_to_8(png_ptr);
1666 }
1667 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) {
1668 png_set_tRNS_to_alpha(png_ptr);
1669 }
1670 // Ensure we have RGBA
1671 if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) {
1672 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
1673 }
1674 if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
1675 png_set_gray_to_rgb(png_ptr);
1676 }
1677
1678 // 8) Handle interlacing
1679 png_set_interlace_handling(png_ptr);
1680
1681 // 9) Apply transforms & re-fetch info
1682 png_read_update_info(png_ptr, info_ptr);
1683
1684 // 10) Get & validate dimensions
1685 size_t w = png_get_image_width(png_ptr, info_ptr);
1686 size_t h = png_get_image_height(png_ptr, info_ptr);
1687 // Prevent overflow when resizing vectors
1688 constexpr size_t max_pixels = (std::numeric_limits<size_t>::max)() / sizeof(helios::RGBAcolor);
1689 if (w == 0 || h == 0 || w > max_pixels / h) {
1690 throw std::runtime_error("Invalid image dimensions: " + std::to_string(w) + "×" + std::to_string(h));
1691 }
1692 width = scast<uint>(w);
1693 height = scast<uint>(h);
1694
1695 // 11) Prepare row pointers
1696 size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);
1697 if (rowbytes < width * 4) {
1698 throw std::runtime_error("Unexpected row size: " + std::to_string(rowbytes));
1699 }
1700 std::vector<std::vector<png_byte>> row_data(height, std::vector<png_byte>(rowbytes));
1701 std::vector<png_bytep> row_pointers(height);
1702 for (uint y = 0; y < height; ++y) {
1703 row_pointers[y] = row_data[y].data();
1704 }
1705
1706 // 12) Read the image
1707 if (setjmp(png_jmpbuf(png_ptr))) {
1708 throw std::runtime_error("Error during PNG read.");
1709 }
1710 png_read_image(png_ptr, row_pointers.data());
1711
1712 // 13) Convert into normalized RGBAcolor
1713 texture.resize(scast<size_t>(width) * height);
1714 for (uint y = 0; y < height; ++y) {
1715 png_bytep row = row_pointers[y];
1716 for (uint x = 0; x < width; ++x) {
1717 png_bytep px = row + x * 4;
1718 auto &c = texture[y * width + x];
1719 c.r = px[0] / 255.0f;
1720 c.g = px[1] / 255.0f;
1721 c.b = px[2] / 255.0f;
1722 c.a = px[3] / 255.0f;
1723 }
1724 }
1725 } catch (const std::exception &e) {
1726 // Clean up libpng structs on error
1727 if (png_ptr) {
1728 if (info_ptr)
1729 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1730 else
1731 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1732 }
1733 helios_runtime_error("ERROR (readPNG): " + std::string(e.what()));
1734 }
1735
1736 // Normal cleanup
1737 if (png_ptr) {
1738 if (info_ptr)
1739 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
1740 else
1741 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
1742 }
1743}
1744
1745
1746void helios::writePNG(const std::string &filename, uint width, uint height, const std::vector<helios::RGBAcolor> &pixel_data) {
1747 FILE *fp = fopen(filename.c_str(), "wb");
1748 if (!fp) {
1749 helios_runtime_error("ERROR (writePNG): failed to open image file.");
1750 }
1751
1752 png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
1753 if (!png) {
1754 helios_runtime_error("ERROR (writePNG): failed to create PNG write structure.");
1755 }
1756
1757 png_infop info = png_create_info_struct(png);
1758 if (!info) {
1759 helios_runtime_error("ERROR (writePNG): failed to create PNG info structure.");
1760 }
1761
1762 if (setjmp(png_jmpbuf(png))) {
1763 helios_runtime_error("ERROR (writePNG): init_io failed.");
1764 }
1765
1766 png_init_io(png, fp);
1767
1768 // Output is 8bit depth, RGBA format.
1769 png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
1770 png_write_info(png, info);
1771
1772 // To remove the alpha channel for PNG_COLOR_TYPE_RGB format,
1773 // Use png_set_filler().
1774 // png_set_filler(png, 0, PNG_FILLER_AFTER);
1775
1776 std::vector<unsigned char *> row_pointers;
1777 row_pointers.resize(height);
1778
1779 std::vector<std::vector<unsigned char>> data;
1780 data.resize(height);
1781
1782 for (uint row = 0; row < height; row++) {
1783 data.at(row).resize(4 * width);
1784 for (uint col = 0; col < width; col++) {
1785 data.at(row).at(4 * col) = (unsigned char) round(clamp(pixel_data.at(row * width + col).r, 0.f, 1.f) * 255.f);
1786 data.at(row).at(4 * col + 1) = (unsigned char) round(clamp(pixel_data.at(row * width + col).g, 0.f, 1.f) * 255.f);
1787 data.at(row).at(4 * col + 2) = (unsigned char) round(clamp(pixel_data.at(row * width + col).b, 0.f, 1.f) * 255.f);
1788 data.at(row).at(4 * col + 3) = (unsigned char) round(clamp(pixel_data.at(row * width + col).a, 0.f, 1.f) * 255.f);
1789 }
1790 row_pointers.at(row) = &data.at(row).at(0);
1791 }
1792
1793 png_write_image(png, &row_pointers.at(0));
1794 png_write_end(png, nullptr);
1795
1796 fclose(fp);
1797
1798 png_destroy_write_struct(&png, &info);
1799}
1800
1801void helios::writePNG(const std::string &filename, uint width, uint height, const std::vector<unsigned char> &pixel_data) {
1802
1803 // Convert pixel_data array into RGBcolor vector
1804
1805 size_t pixels = width * height;
1806
1807 std::vector<RGBAcolor> rgb_data;
1808 rgb_data.resize(pixels);
1809
1810 size_t channels = pixel_data.size() / pixels;
1811
1812 if (channels < 3) {
1813 helios_runtime_error("ERROR (writePNG): Pixel data must have at least 3 color channels");
1814 }
1815
1816 // Convert pixel data into RGBA values
1817 for (size_t i = 0; i < pixels; i++) {
1818 rgb_data[i].r = float(pixel_data[i]) / 255.0f;
1819 rgb_data[i].g = float(pixel_data[i + pixels]) / 255.0f;
1820 rgb_data[i].b = float(pixel_data[i + 2 * pixels]) / 255.0f;
1821 rgb_data[i].a = channels > 3 ? float(pixel_data[i + 3 * pixels]) / 255.0f : 1.0f;
1822 }
1823
1824 // Call RGB version of writePNG
1825 writePNG(filename, width, height, rgb_data);
1826}
1827
1828
1830METHODDEF(void) jpg_error_exit(j_common_ptr cinfo) {
1831 char buffer[JMSG_LENGTH_MAX];
1832 (*cinfo->err->format_message)(cinfo, buffer);
1833 throw std::runtime_error(buffer);
1834}
1835
1836void helios::readJPEG(const std::string &filename, uint &width, uint &height, std::vector<helios::RGBcolor> &pixel_data) {
1837 auto file_extension = getFileExtension(filename);
1838 if (file_extension != ".jpg" && file_extension != ".JPG" && file_extension != ".jpeg" && file_extension != ".JPEG") {
1839 helios_runtime_error("ERROR (Context::readJPEG): File " + filename + " is not JPEG format.");
1840 }
1841
1842 jpeg_decompress_struct cinfo{};
1843
1844 jpeg_error_mgr jerr{};
1845 JSAMPARRAY buffer;
1846 int row_stride;
1847
1848 std::unique_ptr<FILE, int (*)(FILE *)> infile(fopen(filename.c_str(), "rb"), fclose);
1849 if (!infile) {
1850 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.");
1851 }
1852
1853 cinfo.err = jpeg_std_error(&jerr);
1854 jerr.error_exit = jpg_error_exit;
1855
1856 try {
1857 jpeg_create_decompress(&cinfo);
1858 jpeg_stdio_src(&cinfo, infile.get());
1859 (void) jpeg_read_header(&cinfo, (boolean) 1);
1860
1861 (void) jpeg_start_decompress(&cinfo);
1862
1863 row_stride = cinfo.output_width * cinfo.output_components;
1864 buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
1865
1866 width = cinfo.output_width;
1867 height = cinfo.output_height;
1868
1869 if (cinfo.output_components != 3) {
1870 helios_runtime_error("ERROR (Context::readJPEG): Image file does not have RGB components.");
1871 } else if (width == 0 || height == 0) {
1872 helios_runtime_error("ERROR (Context::readJPEG): Image file is empty.");
1873 }
1874
1875 pixel_data.resize(width * height);
1876
1877 JSAMPLE *ba;
1878 int row = 0;
1879 while (cinfo.output_scanline < cinfo.output_height) {
1880 (void) jpeg_read_scanlines(&cinfo, buffer, 1);
1881
1882 ba = buffer[0];
1883
1884 for (int col = 0; col < row_stride; col += 3) {
1885 pixel_data.at(row * width + col / 3) = make_RGBcolor(ba[col] / 255.f, ba[col + 1] / 255.f, ba[col + 2] / 255.f);
1886 }
1887
1888 row++;
1889 }
1890
1891 (void) jpeg_finish_decompress(&cinfo);
1892
1893 jpeg_destroy_decompress(&cinfo);
1894 } catch (...) {
1895 jpeg_destroy_decompress(&cinfo);
1896 throw;
1897 }
1898}
1899
1900helios::int2 helios::getImageResolutionJPEG(const std::string &filename) {
1901 auto file_extension = getFileExtension(filename);
1902 if (file_extension != ".jpg" && file_extension != ".JPG" && file_extension != ".jpeg" && file_extension != ".JPEG") {
1903 helios_runtime_error("ERROR (Context::getImageResolutionJPEG): File " + filename + " is not JPEG format.");
1904 }
1905
1906 jpeg_decompress_struct cinfo{};
1907
1908 jpeg_error_mgr jerr{};
1909 std::unique_ptr<FILE, int (*)(FILE *)> infile(fopen(filename.c_str(), "rb"), fclose);
1910 if (!infile) {
1911 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.");
1912 }
1913
1914 cinfo.err = jpeg_std_error(&jerr);
1915 jerr.error_exit = jpg_error_exit;
1916
1917 try {
1918 jpeg_create_decompress(&cinfo);
1919 jpeg_stdio_src(&cinfo, infile.get());
1920 (void) jpeg_read_header(&cinfo, (boolean) 1);
1921 (void) jpeg_start_decompress(&cinfo);
1922
1923 jpeg_destroy_decompress(&cinfo);
1924 } catch (...) {
1925 jpeg_destroy_decompress(&cinfo);
1926 throw;
1927 }
1928
1929 return make_int2(cinfo.output_width, cinfo.output_height);
1930}
1931
1932void helios::writeJPEG(const std::string &a_filename, uint width, uint height, const std::vector<helios::RGBcolor> &pixel_data) {
1933
1934 std::string filename = a_filename;
1935 auto file_extension = getFileExtension(filename);
1936 if (file_extension != ".jpg" && file_extension != ".JPG" && file_extension != ".jpeg" && file_extension != ".JPEG") {
1937 filename.append(".jpeg");
1938 }
1939
1940 if (pixel_data.size() != width * height) {
1941 helios_runtime_error("ERROR (Context::writeJPEG): Pixel data does not have size of width*height.");
1942 }
1943
1944 const uint bsize = 3 * width * height;
1945 std::vector<unsigned char> screen_shot_trans(bsize);
1946
1947 size_t ii = 0;
1948 for (size_t i = 0; i < width * height; i++) {
1949 screen_shot_trans.at(ii) = (unsigned char) round(clamp(pixel_data.at(i).r, 0.f, 1.f) * 255);
1950 screen_shot_trans.at(ii + 1) = (unsigned char) round(clamp(pixel_data.at(i).g, 0.f, 1.f) * 255);
1951 screen_shot_trans.at(ii + 2) = (unsigned char) round(clamp(pixel_data.at(i).b, 0.f, 1.f) * 255);
1952 ii += 3;
1953 }
1954
1955 struct jpeg_compress_struct cinfo{};
1956
1957 struct jpeg_error_mgr jerr{};
1958
1959 cinfo.err = jpeg_std_error(&jerr);
1960 jerr.error_exit = jpg_error_exit;
1961
1962 JSAMPROW row_pointer;
1963 int row_stride;
1964
1965 std::unique_ptr<FILE, int (*)(FILE *)> outfile(fopen(filename.c_str(), "wb"), fclose);
1966 if (!outfile) {
1967 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.");
1968 }
1969
1970 jpeg_create_compress(&cinfo);
1971 jpeg_stdio_dest(&cinfo, outfile.get());
1972
1973 cinfo.image_width = width; /* image width and height, in pixels */
1974 cinfo.image_height = height;
1975 cinfo.input_components = 3; /* # of color components per pixel */
1976 cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
1977
1978 jpeg_set_defaults(&cinfo);
1979
1980 jpeg_set_quality(&cinfo, 100, (boolean) 1 /* limit to baseline-JPEG values */);
1981
1982 jpeg_start_compress(&cinfo, (boolean) 1);
1983
1984 try {
1985 row_stride = width * 3; /* JSAMPLEs per row in image_buffer */
1986
1987 while (cinfo.next_scanline < cinfo.image_height) {
1988 row_pointer = (JSAMPROW) &screen_shot_trans[(cinfo.image_height - cinfo.next_scanline - 1) * row_stride];
1989 (void) jpeg_write_scanlines(&cinfo, &row_pointer, 1);
1990 }
1991
1992 jpeg_finish_compress(&cinfo);
1993 jpeg_destroy_compress(&cinfo);
1994 } catch (...) {
1995 jpeg_destroy_compress(&cinfo);
1996 throw;
1997 }
1998}
1999
2000void helios::writeJPEG(const std::string &a_filename, uint width, uint height, const std::vector<unsigned char> &pixel_data) {
2001
2002 // Convert pixel_data array into RGBcolor vector
2003
2004 size_t pixels = width * height;
2005
2006 std::vector<RGBcolor> rgb_data;
2007 rgb_data.resize(pixels);
2008
2009 size_t channels = pixel_data.size() / pixels;
2010
2011 if (channels < 3) {
2012 helios_runtime_error("ERROR (writeJPEG): Pixel data must have at least 3 color channels");
2013 }
2014
2015 // Convert pixel data into RGB values
2016 for (size_t i = 0; i < pixels; i++) {
2017 rgb_data[i].r = scast<float>(pixel_data[i]) / 255.0f;
2018 rgb_data[i].g = scast<float>(pixel_data[i + pixels]) / 255.0f;
2019 rgb_data[i].b = scast<float>(pixel_data[i + 2 * pixels]) / 255.0f;
2020 }
2021
2022 // Call RGB version of writeJPEG
2023 writeJPEG(a_filename, width, height, rgb_data);
2024}
2025
2026helios::vec3 helios::spline_interp3(float u, const vec3 &x_start, const vec3 &tan_start, const vec3 &x_end, const vec3 &tan_end) {
2027 // Perform interpolation between two 3D points using Cubic Hermite Spline
2028
2029 if (u < 0 || u > 1.f) {
2030 std::cerr << "WARNING (spline_interp3): Clamping query point 'u' to the interval (0,1)" << std::endl;
2031 u = clamp(u, 0.f, 1.f);
2032 }
2033
2034 // Basis matrix
2035 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};
2036
2037 // Control matrix
2038 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};
2039
2040 // Parameter vector
2041 const float P[4] = {u * u * u, u * u, u, 1.f};
2042
2043 float R[12] = {0.f};
2044
2045 for (int i = 0; i < 4; i++) {
2046 for (int j = 0; j < 3; j++) {
2047 for (int k = 0; k < 4; k++) {
2048 R[3 * i + j] = R[3 * i + j] + B[4 * i + k] * C[3 * k + j];
2049 }
2050 }
2051 }
2052
2053 float xq[3] = {0.f};
2054
2055 for (int j = 0; j < 3; j++) {
2056 for (int k = 0; k < 4; k++) {
2057 xq[j] = xq[j] + P[k] * R[3 * k + j];
2058 }
2059 }
2060
2061 return make_vec3(xq[0], xq[1], xq[2]);
2062}
2063
2064float helios::XMLloadfloat(const pugi::xml_node node, const char *field) {
2065 const char *field_str = node.child_value(field);
2066
2067 float value;
2068 if (strlen(field_str) == 0) {
2069 value = 99999;
2070 } else {
2071 if (!parse_float(field_str, value)) {
2072 value = 99999;
2073 }
2074 }
2075
2076 return value;
2077}
2078
2079int helios::XMLloadint(const pugi::xml_node node, const char *field) {
2080 const char *field_str = node.child_value(field);
2081
2082 int value;
2083 if (strlen(field_str) == 0) {
2084 value = 99999;
2085 } else {
2086 if (!parse_int(field_str, value)) {
2087 value = 99999;
2088 }
2089 }
2090
2091 return value;
2092}
2093
2094std::string helios::XMLloadstring(const pugi::xml_node node, const char *field) {
2095 const std::string field_str = deblank(node.child_value(field));
2096
2097 std::string value;
2098 if (field_str.empty()) {
2099 value = "99999";
2100 } else {
2101 value = field_str; // note: pugi loads xml data as a character. need to separate it into int
2102 }
2103
2104 return value;
2105}
2106
2107helios::vec2 helios::XMLloadvec2(const pugi::xml_node node, const char *field) {
2108 const char *field_str = node.child_value(field);
2109
2110 helios::vec2 value;
2111 if (strlen(field_str) == 0) {
2112 value = make_vec2(99999, 99999);
2113 } else {
2114 value = string2vec2(field_str); // note: pugi loads xml data as a character. need to separate it into 2 floats
2115 }
2116
2117 return value;
2118}
2119
2120helios::vec3 helios::XMLloadvec3(const pugi::xml_node node, const char *field) {
2121 const char *field_str = node.child_value(field);
2122
2123 helios::vec3 value;
2124 if (strlen(field_str) == 0) {
2125 value = make_vec3(99999, 99999, 99999);
2126 } else {
2127 value = string2vec3(field_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
2128 }
2129
2130 return value;
2131}
2132
2133helios::vec4 helios::XMLloadvec4(const pugi::xml_node node, const char *field) {
2134 const char *field_str = node.child_value(field);
2135
2136 helios::vec4 value;
2137 if (strlen(field_str) == 0) {
2138 value = make_vec4(99999, 99999, 99999, 99999);
2139 } else {
2140 value = string2vec4(field_str); // note: pugi loads xml data as a character. need to separate it into 4 floats
2141 }
2142
2143 return value;
2144}
2145
2146helios::int2 helios::XMLloadint2(const pugi::xml_node node, const char *field) {
2147 const char *field_str = node.child_value(field);
2148
2149 helios::int2 value;
2150 if (strlen(field_str) == 0) {
2151 value = make_int2(99999, 99999);
2152 } else {
2153 value = string2int2(field_str); // note: pugi loads xml data as a character. need to separate it into 2 ints
2154 }
2155
2156 return value;
2157}
2158
2159helios::int3 helios::XMLloadint3(const pugi::xml_node node, const char *field) {
2160 const char *field_str = node.child_value(field);
2161
2162 helios::int3 value;
2163 if (strlen(field_str) == 0) {
2164 value = make_int3(99999, 99999, 99999);
2165 } else {
2166 value = string2int3(field_str); // note: pugi loads xml data as a character. need to separate it into 3 ints
2167 }
2168
2169 return value;
2170}
2171
2172helios::int4 helios::XMLloadint4(const pugi::xml_node node, const char *field) {
2173 const char *field_str = node.child_value(field);
2174
2175 helios::int4 value;
2176 if (strlen(field_str) == 0) {
2177 value = make_int4(99999, 99999, 99999, 99999);
2178 } else {
2179 value = string2int4(field_str); // note: pugi loads xml data as a character. need to separate it into 4 ints
2180 }
2181
2182 return value;
2183}
2184
2185helios::RGBcolor helios::XMLloadrgb(const pugi::xml_node node, const char *field) {
2186 const char *field_str = node.child_value(field);
2187
2188 helios::RGBAcolor value;
2189 if (strlen(field_str) == 0) {
2190 value = make_RGBAcolor(1, 1, 1, 0);
2191 } else {
2192 value = string2RGBcolor(field_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
2193 }
2194
2195 return make_RGBcolor(value.r, value.g, value.b);
2196}
2197
2198helios::RGBAcolor helios::XMLloadrgba(const pugi::xml_node node, const char *field) {
2199 const char *field_str = node.child_value(field);
2200
2201 helios::RGBAcolor value;
2202 if (strlen(field_str) == 0) {
2203 value = make_RGBAcolor(1, 1, 1, 1);
2204 } else {
2205 value = string2RGBcolor(field_str); // note: pugi loads xml data as a character. need to separate it into 3 floats
2206 }
2207
2208 return value;
2209}
2210
2211float 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) {
2212 constexpr float DELTA_SEED = 1e-3f; // Increased for better initial slope estimate
2213 constexpr float DENOM_EPS = 1e-10f; // Relaxed flat function detection
2214 constexpr float MAX_STEP_FACTOR = 0.5f; // Limit step size for stability
2215
2216 /* ---- initial pair ---------------------------------------------- */
2217 float x0 = init_guess;
2218 float x1 = (std::fabs(init_guess) > 1.0f) ? init_guess * (1.0f + DELTA_SEED) : init_guess + DELTA_SEED;
2219
2220 float f0 = f(x0, vars, params);
2221 float f1 = f(x1, vars, params);
2222
2223 // If initial points have opposite signs, use bisection for robustness
2224 bool use_bisection = (f0 * f1 < 0);
2225 float bracket_low = use_bisection ? std::min(x0, x1) : 0;
2226 float bracket_high = use_bisection ? std::max(x0, x1) : 0;
2227
2228 for (int iter = 0; iter < max_iter; ++iter) {
2229
2230 float denom = f1 - f0;
2231
2232 /* ------- flat or nearly flat function ----------------------- */
2233 if (std::fabs(denom) < DENOM_EPS) {
2234 if (std::fabs(f1) < err_tol) { // already "close enough"
2235 return x1;
2236 }
2237 // Try a different approach if function is flat
2238 if (use_bisection) {
2239 float x2 = 0.5f * (bracket_low + bracket_high);
2240 if (std::fabs(x2 - x1) < err_tol * std::fabs(x2)) {
2241 return x2;
2242 }
2243 float f2 = f(x2, vars, params);
2244 if (f1 * f2 < 0) {
2245 bracket_high = x1;
2246 } else {
2247 bracket_low = x1;
2248 }
2249 x0 = x1;
2250 f0 = f1;
2251 x1 = x2;
2252 f1 = f2;
2253 continue;
2254 }
2255 std::cerr << "WARNING: fzero stagnated (|f'|≈0).\n";
2256 return x1; // graceful exit, finite value
2257 }
2258
2259 /* ------- secant update with step limiting -------------------- */
2260 float x2 = x1 - f1 * (x1 - x0) / denom;
2261
2262 // Limit step size for stability
2263 float step = x2 - x1;
2264 float max_step = MAX_STEP_FACTOR * std::max(std::fabs(x1), 1.0f);
2265 if (std::fabs(step) > max_step) {
2266 step = (step > 0) ? max_step : -max_step;
2267 x2 = x1 + step;
2268 }
2269
2270 if (!std::isfinite(x2)) { // overflow / NaN safeguard
2271 std::cerr << "WARNING: fzero produced non-finite iterate.\n";
2272 return x1;
2273 }
2274
2275 float f2 = f(x2, vars, params);
2276
2277 // Update brackets if using bisection fallback
2278 if (use_bisection) {
2279 if (f1 * f2 < 0) {
2280 bracket_high = x1;
2281 } else {
2282 bracket_low = x1;
2283 }
2284 }
2285
2286 /* ------- convergence criteria -------------------------------- */
2287 float rel_step = std::fabs(x2 - x1) / (std::fabs(x2) + 1.0f);
2288 if (std::fabs(f2) < err_tol && rel_step < err_tol) {
2289 return x2;
2290 }
2291
2292 /* ------- next iteration -------------------------------------- */
2293 x0 = x1;
2294 f0 = f1;
2295 x1 = x2;
2296 f1 = f2;
2297 }
2298
2299 std::cerr << "WARNING: fzero did not converge after " << max_iter << " iterations.\n";
2300 return x1; // best finite estimate
2301}
2302
2303float 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) {
2304 constexpr float DELTA_SEED = 1e-3f; // Increased for better initial slope estimate
2305 constexpr float DENOM_EPS = 1e-10f; // Relaxed flat function detection
2306 constexpr float MAX_STEP_FACTOR = 0.5f; // Limit step size for stability
2307
2308 converged = false; // Initialize as not converged
2309
2310 /* ---- initial pair ---------------------------------------------- */
2311 float x0 = init_guess;
2312 float x1 = (std::fabs(init_guess) > 1.0f) ? init_guess * (1.0f + DELTA_SEED) : init_guess + DELTA_SEED;
2313
2314 float f0 = f(x0, vars, params);
2315 float f1 = f(x1, vars, params);
2316
2317 // If initial points have opposite signs, use bisection for robustness
2318 bool use_bisection = (f0 * f1 < 0);
2319 float bracket_low = use_bisection ? std::min(x0, x1) : 0;
2320 float bracket_high = use_bisection ? std::max(x0, x1) : 0;
2321
2322 for (int iter = 0; iter < max_iter; ++iter) {
2323
2324 float denom = f1 - f0;
2325
2326 /* ------- flat or nearly flat function ----------------------- */
2327 if (std::fabs(denom) < DENOM_EPS) {
2328 if (std::fabs(f1) < err_tol) { // already "close enough"
2329 converged = true;
2330 return x1;
2331 }
2332 // Try a different approach if function is flat
2333 if (use_bisection) {
2334 float x2 = 0.5f * (bracket_low + bracket_high);
2335 if (std::fabs(x2 - x1) < err_tol * std::fabs(x2)) {
2336 converged = true;
2337 return x2;
2338 }
2339 float f2 = f(x2, vars, params);
2340 if (f1 * f2 < 0) {
2341 bracket_high = x1;
2342 } else {
2343 bracket_low = x1;
2344 }
2345 x0 = x1;
2346 f0 = f1;
2347 x1 = x2;
2348 f1 = f2;
2349 continue;
2350 }
2351 // Function is stagnated, not converged
2352 return x1; // graceful exit, finite value
2353 }
2354
2355 /* ------- secant update with step limiting -------------------- */
2356 float x2 = x1 - f1 * (x1 - x0) / denom;
2357
2358 // Limit step size for stability
2359 float step = x2 - x1;
2360 float max_step = MAX_STEP_FACTOR * std::max(std::fabs(x1), 1.0f);
2361 if (std::fabs(step) > max_step) {
2362 step = (step > 0) ? max_step : -max_step;
2363 x2 = x1 + step;
2364 }
2365
2366 if (!std::isfinite(x2)) { // overflow / NaN safeguard
2367 return x1;
2368 }
2369
2370 float f2 = f(x2, vars, params);
2371
2372 // Update brackets if using bisection fallback
2373 if (use_bisection) {
2374 if (f1 * f2 < 0) {
2375 bracket_high = x1;
2376 } else {
2377 bracket_low = x1;
2378 }
2379 }
2380
2381 /* ------- convergence criteria -------------------------------- */
2382 float rel_step = std::fabs(x2 - x1) / (std::fabs(x2) + 1.0f);
2383 if (std::fabs(f2) < err_tol && rel_step < err_tol) {
2384 converged = true;
2385 return x2;
2386 }
2387
2388 /* ------- next iteration -------------------------------------- */
2389 x0 = x1;
2390 f0 = f1;
2391 x1 = x2;
2392 f1 = f2;
2393 }
2394
2395 // Did not converge after max_iter iterations
2396 return x1; // best finite estimate
2397}
2398
2399float helios::interp1(const std::vector<helios::vec2> &points, float x) {
2400 // Handle empty input
2401 if (points.empty()) {
2402 helios_runtime_error("ERROR (interp1): Cannot interpolate with empty points vector.");
2403 }
2404
2405 // Handle single point case
2406 if (points.size() == 1) {
2407 return points[0].y;
2408 }
2409
2410 // Fast path: check first if data is increasing (most common case)
2411 // This avoids full validation for performance-critical applications
2412 constexpr float EPSILON = 1.0E-5f;
2413 bool is_likely_increasing = points.size() < 2 || points[1].x > points[0].x;
2414
2415 if (is_likely_increasing) {
2416 // Quick verification for increasing sequence
2417 bool is_valid_increasing = true;
2418 for (size_t i = 1; i < points.size() && is_valid_increasing; ++i) {
2419 float deltaX = points[i].x - points[i - 1].x;
2420 if (deltaX <= EPSILON) {
2421 is_valid_increasing = false;
2422 }
2423 }
2424
2425 if (is_valid_increasing) {
2426 // Handle extrapolation cases
2427 if (x <= points.front().x) {
2428 return points.front().y;
2429 }
2430 if (x >= points.back().x) {
2431 return points.back().y;
2432 }
2433
2434 // Optimized binary search for increasing sequence
2435 auto it = std::lower_bound(points.begin(), points.end(), x, [](const vec2 &point, float value) { return point.x < value; });
2436
2437 size_t upper_idx = std::distance(points.begin(), it);
2438 size_t lower_idx = upper_idx - 1;
2439
2440 const vec2 &p1 = points[lower_idx];
2441 const vec2 &p2 = points[upper_idx];
2442
2443 // Linear interpolation
2444 float t = (x - p1.x) / (p2.x - p1.x);
2445 return p1.y + t * (p2.y - p1.y);
2446 }
2447 }
2448
2449 // Fallback: full validation for decreasing or invalid sequences
2450 bool is_increasing = true;
2451 bool is_decreasing = true;
2452
2453 for (size_t i = 1; i < points.size(); ++i) {
2454 float deltaX = points[i].x - points[i - 1].x;
2455
2456 if (std::abs(deltaX) < EPSILON) {
2457 helios_runtime_error("ERROR (interp1): Adjacent X points cannot be equal.");
2458 }
2459
2460 if (deltaX > 0) {
2461 is_decreasing = false;
2462 } else {
2463 is_increasing = false;
2464 }
2465 }
2466
2467 if (!is_increasing && !is_decreasing) {
2468 helios_runtime_error("ERROR (interp1): X points must be monotonic (either all increasing or all decreasing).");
2469 }
2470
2471 // Handle extrapolation cases
2472 if (is_decreasing) {
2473 if (x >= points.front().x) {
2474 return points.front().y;
2475 }
2476 if (x <= points.back().x) {
2477 return points.back().y;
2478 }
2479
2480 // Optimized binary search for decreasing sequence
2481 auto it = std::lower_bound(points.begin(), points.end(), x, [](const vec2 &point, float value) { return point.x > value; });
2482
2483 size_t upper_idx = std::distance(points.begin(), it);
2484 if (upper_idx == 0)
2485 upper_idx = 1;
2486 size_t lower_idx = upper_idx - 1;
2487
2488 const vec2 &p1 = points[lower_idx];
2489 const vec2 &p2 = points[upper_idx];
2490
2491 // Linear interpolation
2492 float t = (x - p1.x) / (p2.x - p1.x);
2493 return p1.y + t * (p2.y - p1.y);
2494 }
2495
2496 // This should never be reached due to earlier validation
2497 helios_runtime_error("ERROR (interp1): Unexpected interpolation state.");
2498 return 0.0f; // Suppress compiler warning (never reached)
2499}
2500
2501std::string helios::getFileExtension(const std::string &filepath) {
2502 std::filesystem::path output_path_fs = filepath;
2503 return output_path_fs.extension().string();
2504}
2505
2506std::string helios::getFileStem(const std::string &filepath) {
2507 std::filesystem::path output_path_fs = filepath;
2508 return output_path_fs.stem().string();
2509}
2510
2511std::string helios::getFileName(const std::string &filepath) {
2512 std::filesystem::path output_path_fs = filepath;
2513 return output_path_fs.filename().string();
2514}
2515
2516std::string helios::getFilePath(const std::string &filepath, bool trailingslash) {
2517 std::filesystem::path output_path_fs = filepath;
2518 std::filesystem::path output_path = output_path_fs.parent_path();
2519 std::string out_str = output_path.make_preferred().string();
2520 if (trailingslash && !out_str.empty()) {
2521 char last = out_str.back();
2522 if (last != '/' && last != '\\') {
2523 out_str += std::filesystem::path::preferred_separator;
2524 }
2525 }
2526 return out_str;
2527}
2528
2529bool helios::validateOutputPath(std::string &output_path, const std::vector<std::string> &allowable_file_extensions) {
2530 if (output_path.empty()) { // path was empty
2531 return false;
2532 }
2533
2534 std::filesystem::path output_path_fs = output_path;
2535
2536 std::string output_file = output_path_fs.filename().string();
2537 std::string output_file_ext = output_path_fs.extension().string();
2538 std::string output_dir = output_path_fs.parent_path().string();
2539
2540 if (output_file.empty()) { // path was a directory without a file
2541
2542 // Make sure directory has a trailing slash
2543 if (output_dir.find_last_of('/') != output_dir.length() - 1) {
2544 output_path += "/";
2545 }
2546 }
2547
2548 // Create the output directory if it does not exist
2549 if (!output_dir.empty() && !std::filesystem::exists(output_dir)) {
2550 if (!std::filesystem::create_directory(output_dir)) {
2551 return false;
2552 }
2553 }
2554
2555 if (!output_file.empty() && !allowable_file_extensions.empty()) {
2556 // validate file extension
2557 bool valid_extension = false;
2558 for (const auto &ext: allowable_file_extensions) {
2559 if (output_file_ext == ext) {
2560 valid_extension = true;
2561 break;
2562 }
2563 }
2564 if (!valid_extension) {
2565 return false;
2566 }
2567 }
2568
2569 return true;
2570}
2571
2572//--------------------- HELIOS_BUILD PATH RESOLUTION -----------------------------------//
2573
2575std::string getBuildDirectory() {
2576 // Try HELIOS_BUILD environment variable (required)
2577 if (const char *buildDir = std::getenv("HELIOS_BUILD")) {
2578 return std::string(buildDir);
2579 }
2580
2581 // Fallback: assume current working directory contains the build
2582 // This is a simple fallback that should work for most use cases
2583 std::filesystem::path currentPath = std::filesystem::current_path();
2584
2585 // If we're already in a build directory, use it
2586 if (std::filesystem::exists(currentPath / "plugins")) {
2587 return currentPath.string();
2588 }
2589
2590 // If we're in a subdirectory, try going up to find build directory
2591 std::filesystem::path parent = currentPath.parent_path();
2592 if (std::filesystem::exists(parent / "plugins")) {
2593 return parent.string();
2594 }
2595
2596 // Last resort: use current directory
2597 return currentPath.string();
2598}
2599
2600std::filesystem::path helios::resolveAssetPath(const std::string &relativePath) {
2601 // This function is deprecated but kept for compatibility
2602 // It now just calls resolveFilePath
2603 return resolveFilePath(relativePath);
2604}
2605
2606std::filesystem::path helios::resolvePluginAsset(const std::string &pluginName, const std::string &assetPath) {
2607 std::string pluginAssetPath = "plugins/" + pluginName + "/" + assetPath;
2608 return resolveFilePath(pluginAssetPath);
2609}
2610
2611
2612std::filesystem::path helios::resolveFilePath(const std::string &filename) {
2613 // 1. If absolute path, validate and return
2614 std::filesystem::path filepath(filename);
2615 if (filepath.is_absolute()) {
2616 if (std::filesystem::exists(filepath)) {
2617 return std::filesystem::canonical(filepath);
2618 } else {
2619 helios_runtime_error("ERROR (helios::resolveFilePath): Absolute file path " + filename + " does not exist.");
2620 }
2621 }
2622
2623 // 2. First try: Check relative to current working directory
2624 std::filesystem::path currentDirPath = std::filesystem::current_path() / filename;
2625 if (std::filesystem::exists(currentDirPath)) {
2626 return std::filesystem::canonical(currentDirPath);
2627 }
2628
2629 // 3. Second try: Resolve relative to build directory (fallback for HELIOS_BUILD)
2630 std::string buildDir = getBuildDirectory();
2631 std::filesystem::path buildDirPath = std::filesystem::path(buildDir) / filename;
2632
2633 if (std::filesystem::exists(buildDirPath)) {
2634 return std::filesystem::canonical(buildDirPath);
2635 }
2636
2637 // File not found in either location - provide clear error message
2638 helios_runtime_error("ERROR (helios::resolveFilePath): Could not locate asset file: " + filename + " (checked: " + currentDirPath.string() + " and " + buildDirPath.string() + "). " +
2639 "Ensure file exists relative to current directory or HELIOS_BUILD path.");
2640 return {}; // This line should never be reached due to helios_runtime_error throwing
2641}
2642
2643std::filesystem::path helios::resolveSpectraPath(const std::string &spectraFile) {
2644 // All spectral data files should be looked for in the radiation plugin's spectral_data directory
2645 std::string spectraPath = "plugins/radiation/spectral_data/" + spectraFile;
2646 return resolveFilePath(spectraPath);
2647}
2648
2649bool helios::validateAssetPath(const std::filesystem::path &assetPath) {
2650 return std::filesystem::exists(assetPath) && std::filesystem::is_regular_file(assetPath);
2651}
2652
2653std::filesystem::path helios::findProjectRoot(const std::filesystem::path &startPath) {
2654 std::filesystem::path currentPath = std::filesystem::absolute(startPath);
2655
2656 while (!currentPath.empty() && currentPath != currentPath.parent_path()) {
2657 std::filesystem::path cmakeFile = currentPath / "CMakeLists.txt";
2658 if (std::filesystem::exists(cmakeFile) && std::filesystem::is_regular_file(cmakeFile)) {
2659 return currentPath;
2660 }
2661 currentPath = currentPath.parent_path();
2662 }
2663
2664 return {}; // Return empty path if not found
2665}
2666
2667std::filesystem::path helios::resolveProjectFile(const std::string &relativePath) {
2668 // Handle empty path
2669 if (relativePath.empty()) {
2670 helios_runtime_error("ERROR (resolveProjectFile): Cannot resolve empty file path.");
2671 }
2672
2673 // If it's already absolute, just validate and return it
2674 std::filesystem::path inputPath(relativePath);
2675 if (inputPath.is_absolute()) {
2676 if (validateAssetPath(inputPath)) {
2677 return inputPath;
2678 } else {
2679 helios_runtime_error("ERROR (resolveProjectFile): Absolute path '" + relativePath + "' does not exist or is not a regular file.");
2680 }
2681 }
2682
2683 // Strategy 1: Check current working directory
2684 std::filesystem::path cwdPath = std::filesystem::current_path() / relativePath;
2685 if (validateAssetPath(cwdPath)) {
2686 return std::filesystem::absolute(cwdPath);
2687 }
2688
2689 // Strategy 2: Check project directory
2690 std::filesystem::path projectRoot = findProjectRoot();
2691 if (!projectRoot.empty()) {
2692 std::filesystem::path projectPath = projectRoot / relativePath;
2693 if (validateAssetPath(projectPath)) {
2694 return std::filesystem::absolute(projectPath);
2695 }
2696 }
2697
2698 // Strategy 3: Error - file not found in either location
2699 std::string errorMsg = "ERROR (resolveProjectFile): Could not locate file '" + relativePath + "'. Searched in:\n";
2700 errorMsg += " - Current working directory: " + std::filesystem::current_path().string() + "\n";
2701 if (!projectRoot.empty()) {
2702 errorMsg += " - Project directory: " + projectRoot.string() + "\n";
2703 } else {
2704 errorMsg += " - Project directory: (not found - no CMakeLists.txt found in parent directories)\n";
2705 }
2706 errorMsg += "Ensure the file exists in one of these locations.";
2707
2708 helios_runtime_error(errorMsg);
2709 return {}; // Never reached due to exception
2710}
2711
2712std::vector<float> helios::importVectorFromFile(const std::string &filepath) {
2713 std::ifstream stream(filepath.c_str());
2714
2715 if (!stream.is_open()) {
2716 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.");
2717 }
2718
2719 std::istream_iterator<float> start(stream), end;
2720 std::vector<float> vec(start, end);
2721 return vec;
2722}
2723
2724float helios::sample_Beta_distribution(float mu, float nu, std::minstd_rand0 *generator) {
2725 // 1) draw two independent Gamma variates:
2726 // X ~ Gamma(α=ν, 1), Y ~ Gamma(β=μ, 1)
2727 std::gamma_distribution<float> dist_nu(nu, 1.0);
2728 std::gamma_distribution<float> dist_mu(mu, 1.0);
2729
2730 float X = dist_nu(*generator);
2731 float Y = dist_mu(*generator);
2732
2733 // 2) form the Beta = X/(X+Y)
2734 float b = X / (X + Y);
2735
2736 // 3) rescale to θ_L = (π/2)*b
2737 return 0.5f * PI_F * b;
2738}
2739
2740// Complete elliptic integral of the first kind via the arithmetic–geometric mean (AGM)
2741float compute_elliptic_integral_first_kind(float e) {
2742 // K(e) = π / (2 * AGM(1, sqrt(1 - e^2)))
2743 float a = 1.0f;
2744 float b = std::sqrt(1.0f - e * e);
2745 for (int iter = 0; iter < 10; ++iter) {
2746 float an = 0.5f * (a + b);
2747 float bn = std::sqrt(a * b);
2748 a = an;
2749 b = bn;
2750 }
2751 return PI_F / (2.0f * a);
2752}
2753
2754// Ellipsoidal PDF for leaf azimuth distribution
2755// phi: sample angle [0,2π), e: eccentricity, phi0: rotation offset, K_e: precomputed ellip. integral
2756float evaluate_ellipsoidal_azimuth_PDF(float phi, float e, float phi0, float K_e) {
2757 float d = phi - phi0;
2758 float c2 = (1.f - e * e) * std::cos(d) * std::cos(d) + std::sin(d) * std::sin(d);
2759 return 1.f / (4.f * K_e * std::sqrt(c2));
2760}
2761
2762// Sample phi from ellipsoidal distribution via rejection sampling
2763float helios::sample_ellipsoidal_azimuth(float e, float phi0_degrees, std::minstd_rand0 *generator) {
2764 // sanity‐check
2765 if (e < 0.f || e > 1.f) {
2766 helios_runtime_error("ERROR (helios::sample_ellipsoidal_azimuth): Eccentricity must be in [0,1].");
2767 }
2768
2769 // convert rotation offset to radians
2770 float phi0 = deg2rad(phi0_degrees);
2771
2772 // ellipse semiaxes: a=1, b = sqrt(1 - e^2)
2773 float a = 1.f;
2774 float b = std::sqrt(1.f - e * e);
2775
2776 // sample the ellipse parameter t uniformly in [0,2π)
2777 std::uniform_real_distribution<float> distT(0.f, 2.f * PI_F);
2778 float t = distT(*generator);
2779
2780 // point on the ellipse boundary
2781 float x = a * std::cos(t);
2782 float y = b * std::sin(t);
2783
2784 // compute its polar angle
2785 float phi = std::atan2(y, x) + phi0;
2786
2787 // wrap into [0,2π)
2788 if (phi < 0.f)
2789 phi += 2.f * PI_F;
2790 else if (phi >= 2.f * PI_F)
2791 phi -= 2.f * PI_F;
2792
2793 return phi;
2794}
2795
2796std::vector<float> helios::linspace(float start, float end, int num) {
2797 if (num <= 0) {
2798 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
2799 }
2800
2801 if (num == 1) {
2802 return {start};
2803 }
2804
2805 std::vector<float> result(num);
2806 float step = (end - start) / (num - 1);
2807
2808 for (int i = 0; i < num; ++i) {
2809 result[i] = start + i * step;
2810 }
2811
2812 result[num - 1] = end;
2813
2814 return result;
2815}
2816
2817std::vector<vec2> helios::linspace(const vec2 &start, const vec2 &end, int num) {
2818 if (num <= 0) {
2819 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
2820 }
2821
2822 if (num == 1) {
2823 return {start};
2824 }
2825
2826 std::vector<vec2> result(num);
2827 vec2 step = (end - start) / float(num - 1);
2828
2829 for (int i = 0; i < num; ++i) {
2830 result[i] = start + step * float(i);
2831 }
2832
2833 result[num - 1] = end;
2834
2835 return result;
2836}
2837
2838std::vector<vec3> helios::linspace(const vec3 &start, const vec3 &end, int num) {
2839 if (num <= 0) {
2840 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
2841 }
2842
2843 if (num == 1) {
2844 return {start};
2845 }
2846
2847 std::vector<vec3> result(num);
2848 vec3 step = (end - start) / float(num - 1);
2849
2850 for (int i = 0; i < num; ++i) {
2851 result[i] = start + step * float(i);
2852 }
2853
2854 result[num - 1] = end;
2855
2856 return result;
2857}
2858
2859std::vector<vec4> helios::linspace(const vec4 &start, const vec4 &end, int num) {
2860 if (num <= 0) {
2861 helios_runtime_error("ERROR (linspace): Number of points must be greater than 0.");
2862 }
2863
2864 if (num == 1) {
2865 return {start};
2866 }
2867
2868 std::vector<vec4> result(num);
2869 vec4 step = (end - start) / float(num - 1);
2870
2871 for (int i = 0; i < num; ++i) {
2872 result[i] = start + step * float(i);
2873 }
2874
2875 result[num - 1] = end;
2876
2877 return result;
2878}
2879
2880// float helios::sample_ellipsoidal_azimuth(
2881// float e,
2882// float phi0_degrees,
2883// std::minstd_rand0 *generator
2884// ) {
2885// // 1) sanity‐check
2886// if (e < 0.f || e > 1.f) {
2887// helios_runtime_error(
2888// "ERROR (helios::sample_ellipsoidal_azimuth): "
2889// "eccentricity must be in [0,1]."
2890// );
2891// }
2892//
2893// // 2) trivial uniform case
2894// std::uniform_real_distribution<float> distPhi(0.f, 2.f * PI_F);
2895// if (e == 0.f) {
2896// return distPhi(*generator);
2897// }
2898//
2899// // 3) precompute rotation offset
2900// float phi0 = deg2rad(phi0_degrees);
2901//
2902// // 4) rejection sampling: envelope = uniform φ, accept with ratio = (1–e²)/denominator
2903// std::uniform_real_distribution<float> dist01(0.f, 1.f);
2904// while (true) {
2905// float phi = distPhi(*generator);
2906// float d = phi - phi0;
2907// // wrap to [–π,π) for numerical stability
2908// if (d < -PI_F) d += 2.f*PI_F;
2909// else if (d >= PI_F) d -= 2.f*PI_F;
2910//
2911// // denominator = (1–e²)·cos²d + sin²d
2912// float c = std::cos(d), s = std::sin(d);
2913// float denom = (1.f - e*e)*c*c + s*s;
2914//
2915// // acceptance ratio ∈ (0,1]
2916// float ratio = (1.f - e*e) / denom;
2917//
2918// if (dist01(*generator) <= ratio) {
2919// // wrap phi back into [0,2π)
2920// if (phi < 0.f) phi += 2.f*PI_F;
2921// else if (phi >= 2.f*PI_F) phi -= 2.f*PI_F;
2922// return phi;
2923// }
2924// // otherwise retry
2925// }
2926// }