PyHelios 0.1.11
Loading...
Searching...
No Matches
DataTypes.py
Go to the documentation of this file.
1import ctypes
2import math
3from typing import Any, List
4from enum import IntEnum
5
6
7class PrimitiveType(IntEnum):
8 """Helios primitive type enumeration."""
9 Patch = 0
10 Triangle = 1
11 Disk = 2
12 Tile = 3
13 Sphere = 4
14 Tube = 5
15 Box = 6
16 Cone = 7
17 Polymesh = 8
18
19class int2(ctypes.Structure):
20 _fields_ = [('x', ctypes.c_int32), ('y', ctypes.c_int32)]
21
22 def __repr__(self) -> str:
23 return f'int2({self.x}, {self.y})'
24
25 def __str__(self) -> str:
26 return f'int2({self.x}, {self.y})'
27
28 def __new__(cls, x=None, y=None):
29 """Create instance - only pass cls to prevent TypeError on Windows."""
30 return ctypes.Structure.__new__(cls)
31
32 def __init__(self, x:int=0, y:int=0):
33 # Validate and set fields - do not call super().__init__()
34 if not isinstance(x, int):
35 raise ValueError(f"int2.x must be an integer, got {type(x).__name__}: {x}")
36 if not isinstance(y, int):
37 raise ValueError(f"int2.y must be an integer, got {type(y).__name__}: {y}")
38
39 self.x = x
40 self.y = y
41
42 def from_list(self, input_list:List[int]):
43 self.x = input_list[0]
44 self.y = input_list[1]
46 def to_list(self) -> List[int]:
47 return [self.x, self.y]
48
50
51class int3(ctypes.Structure):
52 _fields_ = [('x', ctypes.c_int32), ('y', ctypes.c_int32), ('z', ctypes.c_int32)]
53
54 def __repr__(self) -> str:
55 return f'int3({self.x}, {self.y}, {self.z})'
56
57 def __str__(self) -> str:
58 return f'int3({self.x}, {self.y}, {self.z})'
59
60 def __new__(cls, x=None, y=None, z=None):
61 """Create instance - only pass cls to prevent TypeError on Windows."""
62 return ctypes.Structure.__new__(cls)
64 def __init__(self, x:int=0, y:int=0, z:int=0):
65 # Validate and set fields - do not call super().__init__()
66 if not isinstance(x, int):
67 raise ValueError(f"int3.x must be an integer, got {type(x).__name__}: {x}")
68 if not isinstance(y, int):
69 raise ValueError(f"int3.y must be an integer, got {type(y).__name__}: {y}")
70 if not isinstance(z, int):
71 raise ValueError(f"int3.z must be an integer, got {type(z).__name__}: {z}")
72
73 self.x = x
74 self.y = y
75 self.z = z
76
77 def from_list(self, input_list:List[int]):
78 self.x = input_list[0]
79 self.y = input_list[1]
80 self.z = input_list[2]
82 def to_list(self) -> List[int]:
83 return [self.x, self.y, self.z]
84
85
86
87class int4(ctypes.Structure):
88 _fields_ = [('x', ctypes.c_int32), ('y', ctypes.c_int32), ('z', ctypes.c_int32), ('w', ctypes.c_int32)]
89
90 def __repr__(self) -> str:
91 return f'int4({self.x}, {self.y}, {self.z}, {self.w})'
92
93 def __str__(self) -> str:
94 return f'int4({self.x}, {self.y}, {self.z}, {self.w})'
95
96 def __new__(cls, x=None, y=None, z=None, w=None):
97 """Create instance - only pass cls to prevent TypeError on Windows."""
98 return ctypes.Structure.__new__(cls)
100 def __init__(self, x:int=0, y:int=0, z:int=0, w:int=0):
101 # Validate and set fields - do not call super().__init__()
102 if not isinstance(x, int):
103 raise ValueError(f"int4.x must be an integer, got {type(x).__name__}: {x}")
104 if not isinstance(y, int):
105 raise ValueError(f"int4.y must be an integer, got {type(y).__name__}: {y}")
106 if not isinstance(z, int):
107 raise ValueError(f"int4.z must be an integer, got {type(z).__name__}: {z}")
108 if not isinstance(w, int):
109 raise ValueError(f"int4.w must be an integer, got {type(w).__name__}: {w}")
110
111 self.x = x
112 self.y = y
113 self.z = z
114 self.w = w
115
116 def from_list(self, input_list:List[int]):
117 self.x = input_list[0]
118 self.y = input_list[1]
119 self.z = input_list[2]
120 self.w = input_list[3]
122 def to_list(self) -> List[int]:
123 return [self.x, self.y, self.z, self.w]
124
126
127class vec2(ctypes.Structure):
128 _fields_ = [('x', ctypes.c_float), ('y', ctypes.c_float)]
129
130 def __repr__(self) -> str:
131 return f'vec2({self.x}, {self.y})'
132
133 def __str__(self) -> str:
134 return f'vec2({self.x}, {self.y})'
135
136 def __new__(cls, x=None, y=None):
137 """Create instance - only pass cls to prevent TypeError on Windows."""
138 return ctypes.Structure.__new__(cls)
139
140 def __init__(self, x:float=0, y:float=0):
141 # Validate and set fields - do not call super().__init__()
142 if not self._is_finite_numeric(x):
143 raise ValueError(f"vec2.x must be a finite number, got {type(x).__name__}: {x}. "
144 f"Vector components must be finite (not NaN or infinity).")
145 if not self._is_finite_numeric(y):
146 raise ValueError(f"vec2.y must be a finite number, got {type(y).__name__}: {y}. "
147 f"Vector components must be finite (not NaN or infinity).")
148
149 self.x = float(x)
150 self.y = float(y)
151
152 def from_list(self, input_list:List[float]):
153 self.x = input_list[0]
154 self.y = input_list[1]
155
156 def to_list(self) -> List[float]:
157 return [self.x, self.y]
158
159 def magnitude(self) -> float:
160 """Return the magnitude (length) of the vector."""
161 import math
162 return math.sqrt(self.x * self.x + self.y * self.y)
163
164 def normalize(self) -> 'vec2':
165 """Return a normalized copy of this vector (unit length)."""
166 mag = self.magnitude()
167 if mag == 0:
168 return vec2(0, 0)
169 return vec2(self.x / mag, self.y / mag)
170
171 @staticmethod
172 def _is_finite_numeric(value) -> bool:
173 """Check if value is a finite number (not NaN or inf)."""
174 try:
175 float_value = float(value)
176 return math.isfinite(float_value)
177 except (ValueError, TypeError, OverflowError):
178 return False
179
180class vec3(ctypes.Structure):
181 _fields_ = [('x', ctypes.c_float), ('y', ctypes.c_float), ('z', ctypes.c_float)]
182
183 def __repr__(self) -> str:
184 return f'vec3({self.x}, {self.y}, {self.z})'
185
186 def __str__(self) -> str:
187 return f'vec3({self.x}, {self.y}, {self.z})'
188
189 def __new__(cls, x=None, y=None, z=None):
190 """Create instance - only pass cls to prevent TypeError on Windows."""
191 return ctypes.Structure.__new__(cls)
192
193 def __init__(self, x:float=0, y:float=0, z:float=0):
194 # Validate and set fields - do not call super().__init__()
195 if not self._is_finite_numeric(x):
196 raise ValueError(f"vec3.x must be a finite number, got {type(x).__name__}: {x}. "
197 f"Vector components must be finite (not NaN or infinity).")
198 if not self._is_finite_numeric(y):
199 raise ValueError(f"vec3.y must be a finite number, got {type(y).__name__}: {y}. "
200 f"Vector components must be finite (not NaN or infinity).")
201 if not self._is_finite_numeric(z):
202 raise ValueError(f"vec3.z must be a finite number, got {type(z).__name__}: {z}. "
203 f"Vector components must be finite (not NaN or infinity).")
204
205 self.x = float(x)
206 self.y = float(y)
207 self.z = float(z)
208
209 def from_list(self, input_list:List[float]):
210 self.x = input_list[0]
211 self.y = input_list[1]
212 self.z = input_list[2]
213
214 def to_list(self) -> List[float]:
215 return [self.x, self.y, self.z]
216
217 def to_tuple(self) -> tuple:
218 return (self.x, self.y, self.z)
219
220 def magnitude(self) -> float:
221 """Return the magnitude (length) of the vector."""
222 import math
223 return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
225 def normalize(self) -> 'vec3':
226 """Return a normalized copy of this vector (unit length)."""
227 mag = self.magnitude()
228 if mag == 0:
229 return vec3(0, 0, 0)
230 return vec3(self.x / mag, self.y / mag, self.z / mag)
232 @staticmethod
233 def _is_finite_numeric(value) -> bool:
234 """Check if value is a finite number (not NaN or inf)."""
235 try:
236 float_value = float(value)
237 return math.isfinite(float_value)
238 except (ValueError, TypeError, OverflowError):
239 return False
240
241
242class vec4(ctypes.Structure):
243 _fields_ = [('x', ctypes.c_float), ('y', ctypes.c_float), ('z', ctypes.c_float), ('w', ctypes.c_float)]
244
245 def __repr__(self) -> str:
246 return f'vec4({self.x}, {self.y}, {self.z}, {self.w})'
247
248 def __str__(self) -> str:
249 return f'vec4({self.x}, {self.y}, {self.z}, {self.w})'
250
251 def __new__(cls, x=None, y=None, z=None, w=None):
252 """Create instance - only pass cls to prevent TypeError on Windows."""
253 return ctypes.Structure.__new__(cls)
254
255 def __init__(self, x:float=0, y:float=0, z:float=0, w:float=0):
256 # Validate and set fields - do not call super().__init__()
257 if not self._is_finite_numeric(x):
258 raise ValueError(f"vec4.x must be a finite number, got {type(x).__name__}: {x}. "
259 f"Vector components must be finite (not NaN or infinity).")
260 if not self._is_finite_numeric(y):
261 raise ValueError(f"vec4.y must be a finite number, got {type(y).__name__}: {y}. "
262 f"Vector components must be finite (not NaN or infinity).")
263 if not self._is_finite_numeric(z):
264 raise ValueError(f"vec4.z must be a finite number, got {type(z).__name__}: {z}. "
265 f"Vector components must be finite (not NaN or infinity).")
266 if not self._is_finite_numeric(w):
267 raise ValueError(f"vec4.w must be a finite number, got {type(w).__name__}: {w}. "
268 f"Vector components must be finite (not NaN or infinity).")
269
270 self.x = float(x)
271 self.y = float(y)
272 self.z = float(z)
273 self.w = float(w)
275 def from_list(self, input_list:List[float]):
276 self.x = input_list[0]
277 self.y = input_list[1]
278 self.z = input_list[2]
279 self.w = input_list[3]
280
281 def to_list(self) -> List[float]:
282 return [self.x, self.y, self.z, self.w]
283
284 @staticmethod
285 def _is_finite_numeric(value) -> bool:
286 """Check if value is a finite number (not NaN or inf)."""
287 try:
288 float_value = float(value)
289 return math.isfinite(float_value)
290 except (ValueError, TypeError, OverflowError):
291 return False
295class RGBcolor(ctypes.Structure):
296 _fields_ = [('r', ctypes.c_float), ('g', ctypes.c_float), ('b', ctypes.c_float)]
298 def __repr__(self) -> str:
299 return f'RGBcolor({self.r}, {self.g}, {self.b})'
300
301 def __str__(self) -> str:
302 return f'RGBcolor({self.r}, {self.g}, {self.b})'
304 def __new__(cls, r=None, g=None, b=None):
305 """Create instance - only pass cls to prevent TypeError on Windows."""
306 return ctypes.Structure.__new__(cls)
307
308 def __init__(self, r:float=0, g:float=0, b:float=0):
309 # Validate and set fields - do not call super().__init__()
310 self._validate_color_component(r, 'r')
311 self._validate_color_component(g, 'g')
312 self._validate_color_component(b, 'b')
313
314 self.r = float(r)
315 self.g = float(g)
316 self.b = float(b)
317
318 def from_list(self, input_list:List[float]):
319 self.r = input_list[0]
320 self.g = input_list[1]
321 self.b = input_list[2]
322
323 def to_list(self) -> List[float]:
324 return [self.r, self.g, self.b]
326 def scale(self, factor: float) -> 'RGBcolor':
327 """Return a scaled copy of this color, clamped to [0, 1]."""
328 return RGBcolor(
329 min(1.0, max(0.0, self.r * factor)),
330 min(1.0, max(0.0, self.g * factor)),
331 min(1.0, max(0.0, self.b * factor))
333
334 @staticmethod
335 def _is_finite_numeric(value) -> bool:
336 """Check if value is a finite number (not NaN or inf)."""
337 try:
338 float_value = float(value)
339 return math.isfinite(float_value)
340 except (ValueError, TypeError, OverflowError):
341 return False
343 @staticmethod
344 def _validate_color_component(value, component_name):
345 """Validate a color component is finite and in range [0,1]."""
346 if not RGBcolor._is_finite_numeric(value):
347 raise ValueError(f"RGBcolor.{component_name} must be a finite number, "
348 f"got {type(value).__name__}: {value}. "
349 f"Color components must be finite values between 0 and 1.")
351 if not (0.0 <= value <= 1.0):
352 raise ValueError(f"RGBcolor.{component_name}={value} is outside valid range [0,1]. "
353 f"Color components must be normalized values between 0 and 1.")
355
356
357
358class RGBAcolor(ctypes.Structure):
359 _fields_ = [('r', ctypes.c_float), ('g', ctypes.c_float), ('b', ctypes.c_float), ('a', ctypes.c_float)]
360
361 def __repr__(self) -> str:
362 return f'RGBAcolor({self.r}, {self.g}, {self.b}, {self.a})'
363
364 def __str__(self) -> str:
365 return f'RGBAcolor({self.r}, {self.g}, {self.b}, {self.a})'
366
367 def __new__(cls, r=None, g=None, b=None, a=None):
368 """Create instance - only pass cls to prevent TypeError on Windows."""
369 return ctypes.Structure.__new__(cls)
370
371 def __init__(self, r:float=0, g:float=0, b:float=0, a:float=0):
372 # Validate and set fields - do not call super().__init__()
373 self._validate_color_component(r, 'r')
374 self._validate_color_component(g, 'g')
375 self._validate_color_component(b, 'b')
377
378 self.r = float(r)
379 self.g = float(g)
380 self.b = float(b)
381 self.a = float(a)
382
383 def from_list(self, input_list:List[float]):
384 self.r = input_list[0]
385 self.g = input_list[1]
386 self.b = input_list[2]
387 self.a = input_list[3]
388
389 def to_list(self) -> List[float]:
390 return [self.r, self.g, self.b, self.a]
391
392 def scale(self, factor: float) -> 'RGBAcolor':
393 """Return a scaled copy of this color, clamped to [0, 1]. Alpha unchanged."""
394 return RGBAcolor(
395 min(1.0, max(0.0, self.r * factor)),
396 min(1.0, max(0.0, self.g * factor)),
397 min(1.0, max(0.0, self.b * factor)),
398 self.a # Alpha not scaled
399 )
400
401 @staticmethod
402 def _is_finite_numeric(value) -> bool:
403 """Check if value is a finite number (not NaN or inf)."""
404 try:
405 float_value = float(value)
406 return math.isfinite(float_value)
407 except (ValueError, TypeError, OverflowError):
408 return False
409
410 @staticmethod
411 def _validate_color_component(value, component_name):
412 """Validate a color component is finite and in range [0,1]."""
413 if not RGBAcolor._is_finite_numeric(value):
414 raise ValueError(f"RGBAcolor.{component_name} must be a finite number, "
415 f"got {type(value).__name__}: {value}. "
416 f"Color components must be finite values between 0 and 1.")
418 if not (0.0 <= value <= 1.0):
419 raise ValueError(f"RGBAcolor.{component_name}={value} is outside valid range [0,1]. "
420 f"Color components must be normalized values between 0 and 1.")
421
422
424class SphericalCoord(ctypes.Structure):
425 _fields_ = [
426 ('radius', ctypes.c_float),
427 ('elevation', ctypes.c_float),
428 ('zenith', ctypes.c_float),
429 ('azimuth', ctypes.c_float)
430 ]
431
432 def __repr__(self) -> str:
433 return f'SphericalCoord({self.radius}, {self.elevation}, {self.zenith}, {self.azimuth})'
434
435 def __str__(self) -> str:
436 return f'SphericalCoord({self.radius}, {self.elevation}, {self.zenith}, {self.azimuth})'
437
438 def __new__(cls, radius=None, elevation=None, azimuth=None):
439 """Create instance - only pass cls to prevent TypeError on Windows."""
440 return ctypes.Structure.__new__(cls)
441
442 def __init__(self, radius:float=1, elevation:float=0, azimuth:float=0):
443 """
444 Initialize SphericalCoord fields with validation.
445 Do not call super().__init__() for Windows compatibility.
446
447 Args:
448 radius: Radius (default: 1)
449 elevation: Elevation angle in radians (default: 0)
450 azimuth: Azimuthal angle in radians (default: 0)
451
452 Note: zenith is automatically computed as (Ï€/2 - elevation) to match C++ behavior
453 """
454 # Validate inputs
455 if not self._is_finite_numeric(radius) or radius <= 0:
456 raise ValueError(f"SphericalCoord.radius must be a positive finite number, "
457 f"got {type(radius).__name__}: {radius}. "
458 f"Radius must be greater than 0.")
459
460 if not self._is_finite_numeric(elevation):
461 raise ValueError(f"SphericalCoord.elevation must be a finite number, "
462 f"got {type(elevation).__name__}: {elevation}. "
463 f"Elevation angle must be finite (not NaN or infinity).")
464
465 if not self._is_finite_numeric(azimuth):
466 raise ValueError(f"SphericalCoord.azimuth must be a finite number, "
467 f"got {type(azimuth).__name__}: {azimuth}. "
468 f"Azimuth angle must be finite (not NaN or infinity).")
469
470 # Initialize fields
471 self.radius = float(radius)
472 self.elevation = float(elevation)
473 self.zenith = 0.5 * math.pi - elevation # zenith = π/2 - elevation (matches C++)
474 self.azimuth = float(azimuth)
475
476 def from_list(self, input_list:List[float]):
477 self.radius = input_list[0]
478 self.elevation = input_list[1]
479 self.zenith = input_list[2]
480 self.azimuth = input_list[3]
481
482 def to_list(self) -> List[float]:
483 return [self.radius, self.elevation, self.zenith, self.azimuth]
484
485 @staticmethod
486 def _is_finite_numeric(value) -> bool:
487 """Check if value is a finite number (not NaN or inf)."""
488 try:
489 float_value = float(value)
490 return math.isfinite(float_value)
491 except (ValueError, TypeError, OverflowError):
492 return False
493
495
496class AxisRotation(ctypes.Structure):
497 """
498 Axis rotation structure for specifying shoot orientation in PlantArchitecture.
499
500 Represents rotation using pitch, yaw, and roll angles in degrees.
501 Used to define the orientation of shoots, stems, and branches during plant construction.
502 """
503 _fields_ = [
504 ('pitch', ctypes.c_float),
505 ('yaw', ctypes.c_float),
506 ('roll', ctypes.c_float)
507 ]
508
509 def __repr__(self) -> str:
510 return f'AxisRotation({self.pitch}, {self.yaw}, {self.roll})'
511
512 def __str__(self) -> str:
513 return f'AxisRotation({self.pitch}, {self.yaw}, {self.roll})'
515 def __new__(cls, pitch=None, yaw=None, roll=None):
516 """
517 Create AxisRotation instance.
518 Only pass cls to parent __new__ to prevent TypeError on Windows.
519 """
520 return ctypes.Structure.__new__(cls)
521
522 def __init__(self, pitch:float=0, yaw:float=0, roll:float=0):
523 """
524 Initialize AxisRotation fields with validation.
525 Do not call super().__init__() for Windows compatibility.
526
527 Args:
528 pitch: Pitch angle in degrees (rotation about transverse axis)
529 yaw: Yaw angle in degrees (rotation about vertical axis)
530 roll: Roll angle in degrees (rotation about longitudinal axis)
531
532 Raises:
533 ValueError: If any angle value is not finite
534 """
535 # Validate finite numeric inputs
536 if not self._is_finite_numeric(pitch):
537 raise ValueError(f"AxisRotation.pitch must be a finite number, got {type(pitch).__name__}: {pitch}. "
538 f"Rotation angles must be finite (not NaN or infinity).")
539 if not self._is_finite_numeric(yaw):
540 raise ValueError(f"AxisRotation.yaw must be a finite number, got {type(yaw).__name__}: {yaw}. "
541 f"Rotation angles must be finite (not NaN or infinity).")
542 if not self._is_finite_numeric(roll):
543 raise ValueError(f"AxisRotation.roll must be a finite number, got {type(roll).__name__}: {roll}. "
544 f"Rotation angles must be finite (not NaN or infinity).")
546 self.pitch = float(pitch)
547 self.yaw = float(yaw)
548 self.roll = float(roll)
550 def from_list(self, input_list:List[float]):
551 """Initialize from list [pitch, yaw, roll]"""
552 if len(input_list) < 3:
553 raise ValueError("AxisRotation.from_list requires a list with at least 3 elements [pitch, yaw, roll]")
554 self.pitch = input_list[0]
555 self.yaw = input_list[1]
556 self.roll = input_list[2]
557
558 def to_list(self) -> List[float]:
559 """Convert to list [pitch, yaw, roll]"""
560 return [self.pitch, self.yaw, self.roll]
561
562 @staticmethod
563 def _is_finite_numeric(value) -> bool:
564 """Check if value is a finite number (not NaN or inf)."""
565 try:
566 float_value = float(value)
567 return math.isfinite(float_value)
568 except (ValueError, TypeError, OverflowError):
569 return False
570
571
572# Factory functions to match C++ API
573def make_int2(x: int, y: int) -> int2:
574 """Make an int2 from two integers"""
575 return int2(x, y)
576
577def make_SphericalCoord(elevation_radians: float, azimuth_radians: float) -> SphericalCoord:
578 """
579 Make a SphericalCoord by specifying elevation and azimuth (C++ API compatibility).
581 Args:
582 elevation_radians: Elevation angle in radians
583 azimuth_radians: Azimuthal angle in radians
584
585 Returns:
586 SphericalCoord with radius=1, and automatically computed zenith
587 """
588 return SphericalCoord(radius=1, elevation=elevation_radians, azimuth=azimuth_radians)
589
590def make_int3(x: int, y: int, z: int) -> int3:
591 """Make an int3 from three integers"""
592 return int3(x, y, z)
594def make_int4(x: int, y: int, z: int, w: int) -> int4:
595 """Make an int4 from four integers"""
596 return int4(x, y, z, w)
598def make_vec2(x: float, y: float) -> vec2:
599 """Make a vec2 from two floats"""
600 return vec2(x, y)
601
602def make_vec3(x: float, y: float, z: float) -> vec3:
603 """Make a vec3 from three floats"""
604 return vec3(x, y, z)
606def make_vec4(x: float, y: float, z: float, w: float) -> vec4:
607 """Make a vec4 from four floats"""
608 return vec4(x, y, z, w)
609
610def make_RGBcolor(r: float, g: float, b: float) -> RGBcolor:
611 """Make an RGBcolor from three floats"""
612 return RGBcolor(r, g, b)
613
614def make_RGBAcolor(r: float, g: float, b: float, a: float) -> RGBAcolor:
615 """Make an RGBAcolor from four floats"""
616 return RGBAcolor(r, g, b, a)
617
618def make_AxisRotation(pitch: float, yaw: float, roll: float) -> AxisRotation:
619 """Make an AxisRotation from three angles in degrees"""
620 return AxisRotation(pitch, yaw, roll)
621
623class Time(ctypes.Structure):
624 """Helios Time structure for representing time values."""
625 _fields_ = [('second', ctypes.c_int32), ('minute', ctypes.c_int32), ('hour', ctypes.c_int32)]
626
627 def __repr__(self) -> str:
628 return f'Time({self.hour:02d}:{self.minute:02d}:{self.second:02d})'
629
630 def __str__(self) -> str:
631 return f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'
632
633 def __new__(cls, hour=None, minute=None, second=None):
634 """Create instance - only pass cls to prevent TypeError on Windows."""
635 return ctypes.Structure.__new__(cls)
636
637 def __init__(self, hour: int = 0, minute: int = 0, second: int = 0):
638 """
639 Initialize Time fields with validation.
640 Do not call super().__init__() for Windows compatibility.
641
642 Args:
643 hour: Hour (0-23)
644 minute: Minute (0-59)
645 second: Second (0-59)
646 """
647 # Validate inputs
648 if not isinstance(hour, int):
649 raise ValueError(f"Time.hour must be an integer, got {type(hour).__name__}: {hour}")
650 if not isinstance(minute, int):
651 raise ValueError(f"Time.minute must be an integer, got {type(minute).__name__}: {minute}")
652 if not isinstance(second, int):
653 raise ValueError(f"Time.second must be an integer, got {type(second).__name__}: {second}")
654
655 if hour < 0 or hour > 23:
656 raise ValueError(f"Time.hour must be between 0 and 23, got: {hour}")
657 if minute < 0 or minute > 59:
658 raise ValueError(f"Time.minute must be between 0 and 59, got: {minute}")
659 if second < 0 or second > 59:
660 raise ValueError(f"Time.second must be between 0 and 59, got: {second}")
661
662 # Initialize fields
663 self.hour = hour
664 self.minute = minute
665 self.second = second
666
667 def from_list(self, input_list: List[int]):
668 """Initialize from a list [hour, minute, second]"""
669 if len(input_list) < 3:
670 raise ValueError("Time.from_list requires a list with at least 3 elements [hour, minute, second]")
671 self.hour = input_list[0]
672 self.minute = input_list[1]
673 self.second = input_list[2]
674
675 def to_list(self) -> List[int]:
676 """Convert to list [hour, minute, second]"""
677 return [self.hour, self.minute, self.second]
679 def __eq__(self, other) -> bool:
680 """Check equality with another Time object"""
681 if not isinstance(other, Time):
682 return False
683 return (self.hour == other.hour and
684 self.minute == other.minute and
685 self.second == other.second)
686
687 def __ne__(self, other) -> bool:
688 """Check inequality with another Time object"""
689 return not self.__eq__(other)
690
691
692class Date(ctypes.Structure):
693 """Helios Date structure for representing date values."""
694 _fields_ = [('day', ctypes.c_int32), ('month', ctypes.c_int32), ('year', ctypes.c_int32)]
695
696 def __repr__(self) -> str:
697 return f'Date({self.year}-{self.month:02d}-{self.day:02d})'
698
699 def __str__(self) -> str:
700 return f'{self.year}-{self.month:02d}-{self.day:02d}'
701
702 def __new__(cls, year=None, month=None, day=None):
703 """Create instance - only pass cls to prevent TypeError on Windows."""
704 return ctypes.Structure.__new__(cls)
705
706 def __init__(self, year: int = 2023, month: int = 1, day: int = 1):
707 """
708 Initialize Date fields with validation.
709 Do not call super().__init__() for Windows compatibility.
710
711 Args:
712 year: Year (1900-3000)
713 month: Month (1-12)
714 day: Day (1-31)
715 """
716 # Validate inputs
717 if not isinstance(year, int):
718 raise ValueError(f"Date.year must be an integer, got {type(year).__name__}: {year}")
719 if not isinstance(month, int):
720 raise ValueError(f"Date.month must be an integer, got {type(month).__name__}: {month}")
721 if not isinstance(day, int):
722 raise ValueError(f"Date.day must be an integer, got {type(day).__name__}: {day}")
723
724 if year < 1900 or year > 3000:
725 raise ValueError(f"Date.year must be between 1900 and 3000, got: {year}")
726 if month < 1 or month > 12:
727 raise ValueError(f"Date.month must be between 1 and 12, got: {month}")
728 if day < 1 or day > 31:
729 raise ValueError(f"Date.day must be between 1 and 31, got: {day}")
730
731 # Initialize fields
732 self.year = year
733 self.month = month
734 self.day = day
735
736 def from_list(self, input_list: List[int]):
737 """Initialize from a list [year, month, day]"""
738 if len(input_list) < 3:
739 raise ValueError("Date.from_list requires a list with at least 3 elements [year, month, day]")
740 self.year = input_list[0]
741 self.month = input_list[1]
742 self.day = input_list[2]
743
744 def to_list(self) -> List[int]:
745 """Convert to list [year, month, day]"""
746 return [self.year, self.month, self.day]
747
748 def JulianDay(self) -> int:
749 """Calculate Julian day number for this date."""
750 a = (14 - self.month) // 12
751 y = self.year + 4800 - a
752 m = self.month + 12 * a - 3
753 return self.day + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 - 32045
754
755 def incrementDay(self) -> 'Date':
756 """Return a new Date object incremented by one day."""
757 import calendar
758 days_in_month = calendar.monthrange(self.year, self.month)[1]
759
760 new_day = self.day + 1
761 new_month = self.month
762 new_year = self.year
763
764 if new_day > days_in_month:
765 new_day = 1
766 new_month += 1
767 if new_month > 12:
768 new_month = 1
769 new_year += 1
770
771 return Date(new_year, new_month, new_day)
772
773 def isLeapYear(self) -> bool:
774 """Check if this date's year is a leap year."""
775 return (self.year % 4 == 0 and self.year % 100 != 0) or (self.year % 400 == 0)
776
777 def __eq__(self, other) -> bool:
778 """Check equality with another Date object"""
779 if not isinstance(other, Date):
780 return False
781 return (self.year == other.year and
782 self.month == other.month and
783 self.day == other.day)
784
785 def __ne__(self, other) -> bool:
786 """Check inequality with another Date object"""
787 return not self.__eq__(other)
789
790def make_Time(hour: int, minute: int, second: int) -> Time:
791 """Make a Time from hour, minute, second"""
792 return Time(hour, minute, second)
793
794def make_Date(year: int, month: int, day: int) -> Date:
795 """Make a Date from year, month, day"""
796 return Date(year, month, day)
797
798# Removed duplicate make_SphericalCoord function - keeping only the 2-parameter version above
Axis rotation structure for specifying shoot orientation in PlantArchitecture.
Definition DataTypes.py:545
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
Definition DataTypes.py:612
List[float] to_list(self)
Convert to list [pitch, yaw, roll].
Definition DataTypes.py:605
from_list(self, List[float] input_list)
Initialize from list [pitch, yaw, roll].
Definition DataTypes.py:597
Helios Date structure for representing date values.
Definition DataTypes.py:744
bool isLeapYear(self)
Check if this date's year is a leap year.
Definition DataTypes.py:828
bool __ne__(self, other)
Check inequality with another Date object.
Definition DataTypes.py:840
'Date' incrementDay(self)
Return a new Date object incremented by one day.
Definition DataTypes.py:810
bool __eq__(self, other)
Check equality with another Date object.
Definition DataTypes.py:832
List[int] to_list(self)
Convert to list [year, month, day].
Definition DataTypes.py:799
int JulianDay(self)
Calculate Julian day number for this date.
Definition DataTypes.py:803
Helios primitive type enumeration.
Definition DataTypes.py:8
_validate_color_component(value, component_name)
Validate a color component is finite and in range [0,1].
Definition DataTypes.py:450
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
Definition DataTypes.py:439
'RGBAcolor' scale(self, float factor)
Return a scaled copy of this color, clamped to [0, 1].
Definition DataTypes.py:427
__init__(self, float r=0, float g=0, float b=0, float a=0)
Definition DataTypes.py:405
__new__(cls, r=None, g=None, b=None, a=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:402
'RGBcolor' scale(self, float factor)
Return a scaled copy of this color, clamped to [0, 1].
Definition DataTypes.py:354
__new__(cls, r=None, g=None, b=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:332
__init__(self, float r=0, float g=0, float b=0)
Definition DataTypes.py:335
from_list(self, List[float] input_list)
Definition DataTypes.py:345
_validate_color_component(value, component_name)
Validate a color component is finite and in range [0,1].
Definition DataTypes.py:376
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
Definition DataTypes.py:365
from_list(self, List[float] input_list)
Definition DataTypes.py:517
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
Definition DataTypes.py:530
__init__(self, float radius=1, float elevation=0, float azimuth=0)
Initialize SphericalCoord fields with validation.
Definition DataTypes.py:494
__new__(cls, radius=None, elevation=None, azimuth=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:480
Helios Time structure for representing time values.
Definition DataTypes.py:672
__new__(cls, hour=None, minute=None, second=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:685
List[int] to_list(self)
Convert to list [hour, minute, second].
Definition DataTypes.py:727
from_list(self, List[int] input_list)
Initialize from a list [hour, minute, second].
Definition DataTypes.py:719
__init__(self, int hour=0, int minute=0, int second=0)
Initialize Time fields with validation.
Definition DataTypes.py:697
bool __eq__(self, other)
Check equality with another Time object.
Definition DataTypes.py:731
bool __ne__(self, other)
Check inequality with another Time object.
Definition DataTypes.py:739
from_list(self, List[int] input_list)
Definition DataTypes.py:45
__init__(self, int x=0, int y=0)
Definition DataTypes.py:35
__new__(cls, x=None, y=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:32
__init__(self, int x=0, int y=0, int z=0)
Definition DataTypes.py:70
from_list(self, List[int] input_list)
Definition DataTypes.py:83
__new__(cls, x=None, y=None, z=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:67
from_list(self, List[int] input_list)
Definition DataTypes.py:125
__init__(self, int x=0, int y=0, int z=0, int w=0)
Definition DataTypes.py:109
__new__(cls, x=None, y=None, z=None, w=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:106
from_list(self, List[float] input_list)
Definition DataTypes.py:164
'vec2' normalize(self)
Return a normalized copy of this vector (unit length).
Definition DataTypes.py:177
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
Definition DataTypes.py:187
float magnitude(self)
Return the magnitude (length) of the vector.
Definition DataTypes.py:172
__new__(cls, x=None, y=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:149
__init__(self, float x=0, float y=0)
Definition DataTypes.py:152
__init__(self, float x=0, float y=0, float z=0)
Definition DataTypes.py:210
from_list(self, List[float] input_list)
Definition DataTypes.py:226
'vec3' normalize(self)
Return a normalized copy of this vector (unit length).
Definition DataTypes.py:243
__new__(cls, x=None, y=None, z=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:207
float magnitude(self)
Return the magnitude (length) of the vector.
Definition DataTypes.py:238
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
Definition DataTypes.py:253
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
Definition DataTypes.py:310
__init__(self, float x=0, float y=0, float z=0, float w=0)
Definition DataTypes.py:277
__new__(cls, x=None, y=None, z=None, w=None)
Create only pass cls to prevent TypeError on Windows.
Definition DataTypes.py:274
from_list(self, List[float] input_list)
Definition DataTypes.py:297
RGBAcolor make_RGBAcolor(float r, float g, float b, float a)
Make an RGBAcolor from four floats.
Definition DataTypes.py:663
AxisRotation make_AxisRotation(float pitch, float yaw, float roll)
Make an AxisRotation from three angles in degrees.
Definition DataTypes.py:667
int3 make_int3(int x, int y, int z)
Make an int3 from three integers.
Definition DataTypes.py:639
Time make_Time(int hour, int minute, int second)
Make a Time from hour, minute, second.
Definition DataTypes.py:845
RGBcolor make_RGBcolor(float r, float g, float b)
Make an RGBcolor from three floats.
Definition DataTypes.py:659
Date make_Date(int year, int month, int day)
Make a Date from year, month, day.
Definition DataTypes.py:849
int4 make_int4(int x, int y, int z, int w)
Make an int4 from four integers.
Definition DataTypes.py:643
SphericalCoord make_SphericalCoord(float elevation_radians, float azimuth_radians)
Make a SphericalCoord by specifying elevation and azimuth (C++ API compatibility).
Definition DataTypes.py:635
vec2 make_vec2(float x, float y)
Make a vec2 from two floats.
Definition DataTypes.py:647
vec4 make_vec4(float x, float y, float z, float w)
Make a vec4 from four floats.
Definition DataTypes.py:655
int2 make_int2(int x, int y)
Make an int2 from two integers.
Definition DataTypes.py:622
vec3 make_vec3(float x, float y, float z)
Make a vec3 from three floats.
Definition DataTypes.py:651