2from dataclasses
import dataclass
3from typing
import List, Optional, Union
8from .wrappers
import UContextWrapper
as context_wrapper
9from .wrappers.DataTypes
import vec2, vec3, vec4, int2, int3, int4, SphericalCoord, RGBcolor, PrimitiveType
10from .plugins.loader
import LibraryLoadError, validate_library, get_library_info
11from .plugins.registry
import get_plugin_registry
12from .validation.geometry
import (
13 validate_patch_params, validate_triangle_params, validate_sphere_params,
14 validate_tube_params, validate_box_params
21 Physical properties and geometry information for a primitive.
22 This is separate from primitive data (user-defined key-value pairs).
25 primitive_type: PrimitiveType
30 centroid: Optional[vec3] =
None
33 """Calculate centroid from vertices if not provided."""
36 total_x = sum(v.x
for v
in self.
vertices)
37 total_y = sum(v.y
for v
in self.
vertices)
38 total_z = sum(v.z
for v
in self.
vertices)
40 self.
centroid =
vec3(total_x / count, total_y / count, total_z / count)
45 Central simulation environment for PyHelios that manages 3D primitives and their data.
47 The Context class provides methods for:
48 - Creating geometric primitives (patches, triangles)
49 - Creating compound geometry (tiles, spheres, tubes, boxes)
50 - Loading 3D models from files (PLY, OBJ, XML)
51 - Managing primitive data (flexible key-value storage)
52 - Querying primitive properties and collections
53 - Batch operations on multiple primitives
56 - UUID-based primitive tracking
57 - Comprehensive primitive data system with auto-type detection
58 - Efficient array-based data retrieval via getPrimitiveDataArray()
59 - Cross-platform compatibility with mock mode support
60 - Context manager protocol for resource cleanup
63 >>> with Context() as context:
64 ... # Create primitives
65 ... patch_uuid = context.addPatch(center=vec3(0, 0, 0))
66 ... triangle_uuid = context.addTriangle(vec3(0,0,0), vec3(1,0,0), vec3(0.5,1,0))
68 ... # Set primitive data
69 ... context.setPrimitiveDataFloat(patch_uuid, "temperature", 25.5)
70 ... context.setPrimitiveDataFloat(triangle_uuid, "temperature", 30.2)
72 ... # Get data efficiently as NumPy array
73 ... temps = context.getPrimitiveDataArray([patch_uuid, triangle_uuid], "temperature")
74 ... print(temps) # [25.5 30.2]
85 library_info = get_library_info()
86 if library_info.get(
'is_mock',
False):
88 print(
"Warning: PyHelios running in development mock mode - functionality is limited")
89 print(
"Available plugins: None (mock mode)")
96 if not validate_library():
97 raise LibraryLoadError(
98 "Native Helios library validation failed. Some required functions are missing. "
99 "Try rebuilding the native library: build_scripts/build_helios"
101 except LibraryLoadError:
103 except Exception
as e:
104 raise LibraryLoadError(
105 f
"Failed to validate native Helios library: {e}. "
106 f
"To enable development mode without native libraries, set PYHELIOS_DEV_MODE=1"
111 self.
context = context_wrapper.createContext()
114 raise LibraryLoadError(
115 "Failed to create Helios context. Native library may not be functioning correctly."
120 except Exception
as e:
122 raise LibraryLoadError(
123 f
"Failed to create Helios context: {e}. "
124 f
"Ensure native libraries are built and accessible."
128 """Helper method to check if context is available with detailed error messages."""
133 "Context is in mock mode - native functionality not available.\n"
134 "Build native libraries with 'python build_scripts/build_helios.py' or set PYHELIOS_DEV_MODE=1 for development."
138 "Context has been cleaned up and is no longer usable.\n"
139 "This usually means you're trying to use a Context outside its 'with' statement scope.\n"
141 "Fix: Ensure all Context usage is inside the 'with Context() as context:' block:\n"
142 " with Context() as context:\n"
143 " # All context operations must be here\n"
144 " with SomePlugin(context) as plugin:\n"
145 " plugin.do_something()\n"
146 " with Visualizer() as vis:\n"
147 " vis.buildContextGeometry(context) # Still inside Context scope\n"
148 " # Context is cleaned up here - cannot use context after this point"
152 "Context creation failed - native functionality not available.\n"
153 "Build native libraries with 'python build_scripts/build_helios.py'"
158 f
"Context is not available (state: {self._lifecycle_state}).\n"
159 "Build native libraries with 'python build_scripts/build_helios.py' or set PYHELIOS_DEV_MODE=1 for development."
163 """Validate that a UUID exists in this context.
166 uuid: The UUID to validate
169 RuntimeError: If UUID is invalid or doesn't exist in context
172 if not isinstance(uuid, int)
or uuid < 0:
173 raise RuntimeError(f
"Invalid UUID: {uuid}. UUIDs must be non-negative integers.")
178 if uuid
not in valid_uuids:
179 raise RuntimeError(f
"UUID {uuid} does not exist in context. Valid UUIDs: {valid_uuids[:10]}{'...' if len(valid_uuids) > 10 else ''}")
189 def _validate_file_path(self, filename: str, expected_extensions: List[str] =
None) -> str:
190 """Validate and normalize file path for security.
193 filename: File path to validate
194 expected_extensions: List of allowed file extensions (e.g., ['.ply', '.obj'])
197 Normalized absolute path
201 ValueError: If path is invalid or potentially dangerous
202 FileNotFoundError: If file does not exist
207 abs_path = os.path.abspath(filename)
211 normalized_path = os.path.normpath(abs_path)
212 if abs_path != normalized_path:
213 raise ValueError(f
"Invalid file path (potential path traversal): {filename}")
216 if expected_extensions:
217 file_ext = os.path.splitext(abs_path)[1].lower()
218 if file_ext
not in [ext.lower()
for ext
in expected_extensions]:
219 raise ValueError(f
"Invalid file extension '{file_ext}'. Expected one of: {expected_extensions}")
222 if not os.path.exists(abs_path):
223 raise FileNotFoundError(f
"File not found: {abs_path}")
226 if not os.path.isfile(abs_path):
227 raise ValueError(f
"Path is not a file: {abs_path}")
232 """Validate and normalize output file path for security.
235 filename: Output file path to validate
236 expected_extensions: List of allowed file extensions (e.g., ['.ply', '.obj'])
239 Normalized absolute path
242 ValueError: If path is invalid or potentially dangerous
243 PermissionError: If output directory is not writable
248 if not filename
or not filename.strip():
249 raise ValueError(
"Filename cannot be empty")
252 abs_path = os.path.abspath(filename)
255 normalized_path = os.path.normpath(abs_path)
256 if abs_path != normalized_path:
257 raise ValueError(f
"Invalid file path (potential path traversal): {filename}")
260 if expected_extensions:
261 file_ext = os.path.splitext(abs_path)[1].lower()
262 if file_ext
not in [ext.lower()
for ext
in expected_extensions]:
263 raise ValueError(f
"Invalid file extension '{file_ext}'. Expected one of: {expected_extensions}")
266 output_dir = os.path.dirname(abs_path)
267 if not os.path.exists(output_dir):
268 raise ValueError(f
"Output directory does not exist: {output_dir}")
269 if not os.access(output_dir, os.W_OK):
270 raise PermissionError(f
"Output directory is not writable: {output_dir}")
277 def __exit__(self, exc_type, exc_value, traceback):
279 context_wrapper.destroyContext(self.
context)
289 context_wrapper.markGeometryClean(self.
context)
293 context_wrapper.markGeometryDirty(self.
context)
298 return context_wrapper.isGeometryDirty(self.
context)
300 @validate_patch_params
301 def addPatch(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1), rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None) -> int:
306 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
307 return context_wrapper.addPatchWithCenterSizeRotationAndColor(self.
context, center.to_list(), size.to_list(), rotation_list, color.to_list())
309 @validate_triangle_params
310 def addTriangle(self, vertex0: vec3, vertex1: vec3, vertex2: vec3, color: Optional[RGBcolor] =
None) -> int:
311 """Add a triangle primitive to the context
314 vertex0: First vertex of the triangle
315 vertex1: Second vertex of the triangle
316 vertex2: Third vertex of the triangle
317 color: Optional triangle color (defaults to white)
320 UUID of the created triangle primitive
324 return context_wrapper.addTriangle(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list())
326 return context_wrapper.addTriangleWithColor(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list(), color.to_list())
329 texture_file: str, uv0: vec2, uv1: vec2, uv2: vec2) -> int:
330 """Add a textured triangle primitive to the context
332 Creates a triangle with texture mapping. The texture image is mapped to the triangle
333 surface using UV coordinates, where (0,0) represents the top-left corner of the image
334 and (1,1) represents the bottom-right corner.
337 vertex0: First vertex of the triangle
338 vertex1: Second vertex of the triangle
339 vertex2: Third vertex of the triangle
340 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
341 uv0: UV texture coordinates for first vertex
342 uv1: UV texture coordinates for second vertex
343 uv2: UV texture coordinates for third vertex
346 UUID of the created textured triangle primitive
349 ValueError: If texture file path is invalid
350 FileNotFoundError: If texture file doesn't exist
351 RuntimeError: If context is in mock mode
354 >>> context = Context()
355 >>> # Create a textured triangle
356 >>> vertex0 = vec3(0, 0, 0)
357 >>> vertex1 = vec3(1, 0, 0)
358 >>> vertex2 = vec3(0.5, 1, 0)
359 >>> uv0 = vec2(0, 0) # Bottom-left of texture
360 >>> uv1 = vec2(1, 0) # Bottom-right of texture
361 >>> uv2 = vec2(0.5, 1) # Top-center of texture
362 >>> uuid = context.addTriangleTextured(vertex0, vertex1, vertex2,
363 ... "texture.png", uv0, uv1, uv2)
369 [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp'])
372 return context_wrapper.addTriangleWithTexture(
374 vertex0.to_list(), vertex1.to_list(), vertex2.to_list(),
375 validated_texture_file,
376 uv0.to_list(), uv1.to_list(), uv2.to_list()
381 primitive_type = context_wrapper.getPrimitiveType(self.
context, uuid)
386 return context_wrapper.getPrimitiveArea(self.
context, uuid)
390 normal_ptr = context_wrapper.getPrimitiveNormal(self.
context, uuid)
391 v =
vec3(normal_ptr[0], normal_ptr[1], normal_ptr[2])
396 size = ctypes.c_uint()
397 vertices_ptr = context_wrapper.getPrimitiveVertices(self.
context, uuid, ctypes.byref(size))
399 vertices_list = ctypes.cast(vertices_ptr, ctypes.POINTER(ctypes.c_float * size.value)).contents
400 vertices = [
vec3(vertices_list[i], vertices_list[i+1], vertices_list[i+2])
for i
in range(0, size.value, 3)]
405 color_ptr = context_wrapper.getPrimitiveColor(self.
context, uuid)
406 return RGBcolor(color_ptr[0], color_ptr[1], color_ptr[2])
410 return context_wrapper.getPrimitiveCount(self.
context)
414 size = ctypes.c_uint()
415 uuids_ptr = context_wrapper.getAllUUIDs(self.
context, ctypes.byref(size))
416 return list(uuids_ptr[:size.value])
420 return context_wrapper.getObjectCount(self.
context)
424 size = ctypes.c_uint()
425 objectids_ptr = context_wrapper.getAllObjectIDs(self.
context, ctypes.byref(size))
426 return list(objectids_ptr[:size.value])
430 Get physical properties and geometry information for a single primitive.
433 uuid: UUID of the primitive
436 PrimitiveInfo object containing physical properties and geometry
448 primitive_type=primitive_type,
457 Get physical properties and geometry information for all primitives in the context.
460 List of PrimitiveInfo objects for all primitives
467 Get physical properties and geometry information for all primitives belonging to a specific object.
470 object_id: ID of the object
473 List of PrimitiveInfo objects for primitives in the object
475 object_uuids = context_wrapper.getObjectPrimitiveUUIDs(self.
context, object_id)
479 def addTile(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
480 rotation: Optional[SphericalCoord] =
None, subdiv: int2 =
int2(1, 1),
481 color: Optional[RGBcolor] =
None) -> List[int]:
483 Add a subdivided patch (tile) to the context.
485 A tile is a patch subdivided into a regular grid of smaller patches,
486 useful for creating detailed surfaces or terrain.
489 center: 3D coordinates of tile center (default: origin)
490 size: Width and height of the tile (default: 1x1)
491 rotation: Orientation of the tile (default: no rotation)
492 subdiv: Number of subdivisions in x and y directions (default: 1x1)
493 color: Color of the tile (default: white)
496 List of UUIDs for all patches created in the tile
499 >>> context = Context()
500 >>> # Create a 2x2 meter tile subdivided into 4x4 patches
501 >>> tile_uuids = context.addTile(
502 ... center=vec3(0, 0, 1),
504 ... subdiv=int2(4, 4),
505 ... color=RGBcolor(0.5, 0.8, 0.2)
507 >>> print(f"Created {len(tile_uuids)} patches")
512 if not isinstance(center, vec3):
513 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
514 if not isinstance(size, vec2):
515 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
516 if rotation
is not None and not isinstance(rotation, SphericalCoord):
517 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
518 if not isinstance(subdiv, int2):
519 raise ValueError(f
"Subdiv must be an int2, got {type(subdiv).__name__}")
520 if color
is not None and not isinstance(color, RGBcolor):
521 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
524 if any(s <= 0
for s
in size.to_list()):
525 raise ValueError(
"All size dimensions must be positive")
526 if any(s <= 0
for s
in subdiv.to_list()):
527 raise ValueError(
"All subdivision counts must be positive")
533 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
535 if color
and not (color.r == 1.0
and color.g == 1.0
and color.b == 1.0):
536 return context_wrapper.addTileWithColor(
537 self.
context, center.to_list(), size.to_list(),
538 rotation_list, subdiv.to_list(), color.to_list()
541 return context_wrapper.addTile(
542 self.
context, center.to_list(), size.to_list(),
543 rotation_list, subdiv.to_list()
546 @validate_sphere_params
547 def addSphere(self, center: vec3 =
vec3(0, 0, 0), radius: float = 1.0,
548 ndivs: int = 10, color: Optional[RGBcolor] =
None) -> List[int]:
550 Add a sphere to the context.
552 The sphere is tessellated into triangular faces based on the specified
556 center: 3D coordinates of sphere center (default: origin)
557 radius: Radius of the sphere (default: 1.0)
558 ndivs: Number of divisions for tessellation (default: 10)
559 Higher values create smoother spheres but more triangles
560 color: Color of the sphere (default: white)
563 List of UUIDs for all triangles created in the sphere
566 >>> context = Context()
567 >>> # Create a red sphere at (1, 2, 3) with radius 0.5
568 >>> sphere_uuids = context.addSphere(
569 ... center=vec3(1, 2, 3),
572 ... color=RGBcolor(1, 0, 0)
574 >>> print(f"Created sphere with {len(sphere_uuids)} triangles")
579 if not isinstance(center, vec3):
580 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
581 if not isinstance(radius, (int, float)):
582 raise ValueError(f
"Radius must be a number, got {type(radius).__name__}")
583 if not isinstance(ndivs, int):
584 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
585 if color
is not None and not isinstance(color, RGBcolor):
586 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
590 raise ValueError(
"Sphere radius must be positive")
592 raise ValueError(
"Number of divisions must be at least 3")
595 return context_wrapper.addSphereWithColor(
596 self.
context, ndivs, center.to_list(), radius, color.to_list()
599 return context_wrapper.addSphere(
600 self.
context, ndivs, center.to_list(), radius
603 @validate_tube_params
604 def addTube(self, nodes: List[vec3], radii: Union[float, List[float]],
605 ndivs: int = 6, colors: Optional[Union[RGBcolor, List[RGBcolor]]] =
None) -> List[int]:
607 Add a tube (pipe/cylinder) to the context.
609 The tube is defined by a series of nodes (path) with radius at each node.
610 It's tessellated into triangular faces based on the number of radial divisions.
613 nodes: List of 3D points defining the tube path (at least 2 nodes)
614 radii: Radius at each node. Can be:
615 - Single float: constant radius for all nodes
616 - List of floats: radius for each node (must match nodes length)
617 ndivs: Number of radial divisions (default: 6)
618 Higher values create smoother tubes but more triangles
619 colors: Colors at each node. Can be:
621 - Single RGBcolor: constant color for all nodes
622 - List of RGBcolor: color for each node (must match nodes length)
625 List of UUIDs for all triangles created in the tube
628 >>> context = Context()
629 >>> # Create a curved tube with varying radius
630 >>> nodes = [vec3(0, 0, 0), vec3(1, 0, 0), vec3(2, 1, 0)]
631 >>> radii = [0.1, 0.2, 0.1]
632 >>> colors = [RGBcolor(1, 0, 0), RGBcolor(0, 1, 0), RGBcolor(0, 0, 1)]
633 >>> tube_uuids = context.addTube(nodes, radii, ndivs=8, colors=colors)
634 >>> print(f"Created tube with {len(tube_uuids)} triangles")
639 if not isinstance(nodes, (list, tuple)):
640 raise ValueError(f
"Nodes must be a list or tuple, got {type(nodes).__name__}")
641 if not isinstance(ndivs, int):
642 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
643 if colors
is not None and not isinstance(colors, (RGBcolor, list, tuple)):
644 raise ValueError(f
"Colors must be RGBcolor, list, tuple, or None, got {type(colors).__name__}")
648 raise ValueError(
"Tube requires at least 2 nodes")
650 raise ValueError(
"Number of radial divisions must be at least 3")
653 if isinstance(radii, (int, float)):
654 radii_list = [float(radii)] * len(nodes)
656 radii_list = [float(r)
for r
in radii]
657 if len(radii_list) != len(nodes):
658 raise ValueError(f
"Number of radii ({len(radii_list)}) must match number of nodes ({len(nodes)})")
661 if any(r <= 0
for r
in radii_list):
662 raise ValueError(
"All radii must be positive")
667 nodes_flat.extend(node.to_list())
671 return context_wrapper.addTube(self.
context, ndivs, nodes_flat, radii_list)
672 elif isinstance(colors, RGBcolor):
674 colors_flat = colors.to_list() * len(nodes)
677 if len(colors) != len(nodes):
678 raise ValueError(f
"Number of colors ({len(colors)}) must match number of nodes ({len(nodes)})")
681 colors_flat.extend(color.to_list())
683 return context_wrapper.addTubeWithColor(self.
context, ndivs, nodes_flat, radii_list, colors_flat)
686 def addBox(self, center: vec3 =
vec3(0, 0, 0), size: vec3 =
vec3(1, 1, 1),
687 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None) -> List[int]:
689 Add a rectangular box to the context.
691 The box is subdivided into patches on each face based on the specified
695 center: 3D coordinates of box center (default: origin)
696 size: Width, height, and depth of the box (default: 1x1x1)
697 subdiv: Number of subdivisions in x, y, and z directions (default: 1x1x1)
698 Higher values create more detailed surfaces
699 color: Color of the box (default: white)
702 List of UUIDs for all patches created on the box faces
705 >>> context = Context()
706 >>> # Create a blue box subdivided for detail
707 >>> box_uuids = context.addBox(
708 ... center=vec3(0, 0, 2),
709 ... size=vec3(2, 1, 0.5),
710 ... subdiv=int3(4, 2, 1),
711 ... color=RGBcolor(0, 0, 1)
713 >>> print(f"Created box with {len(box_uuids)} patches")
718 if not isinstance(center, vec3):
719 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
720 if not isinstance(size, vec3):
721 raise ValueError(f
"Size must be a vec3, got {type(size).__name__}")
722 if not isinstance(subdiv, int3):
723 raise ValueError(f
"Subdiv must be an int3, got {type(subdiv).__name__}")
724 if color
is not None and not isinstance(color, RGBcolor):
725 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
728 if any(s <= 0
for s
in size.to_list()):
729 raise ValueError(
"All box dimensions must be positive")
730 if any(s < 1
for s
in subdiv.to_list()):
731 raise ValueError(
"All subdivision counts must be at least 1")
734 return context_wrapper.addBoxWithColor(
735 self.
context, center.to_list(), size.to_list(),
736 subdiv.to_list(), color.to_list()
739 return context_wrapper.addBox(
740 self.
context, center.to_list(), size.to_list(), subdiv.to_list()
743 def loadPLY(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
744 rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None,
745 upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
747 Load geometry from a PLY (Stanford Polygon) file.
750 filename: Path to the PLY file to load
751 origin: Origin point for positioning the geometry (optional)
752 height: Height scaling factor (optional)
753 rotation: Rotation to apply to the geometry (optional)
754 color: Default color for geometry without color data (optional)
755 upaxis: Up axis orientation ("YUP" or "ZUP")
756 silent: If True, suppress loading output messages
759 List of UUIDs for the loaded primitives
765 if origin
is None and height
is None and rotation
is None and color
is None:
767 return context_wrapper.loadPLY(self.
context, validated_filename, silent)
769 elif origin
is not None and height
is not None and rotation
is None and color
is None:
771 return context_wrapper.loadPLYWithOriginHeight(self.
context, validated_filename, origin.to_list(), height, upaxis, silent)
773 elif origin
is not None and height
is not None and rotation
is not None and color
is None:
775 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
776 return context_wrapper.loadPLYWithOriginHeightRotation(self.
context, validated_filename, origin.to_list(), height, rotation_list, upaxis, silent)
778 elif origin
is not None and height
is not None and rotation
is None and color
is not None:
780 return context_wrapper.loadPLYWithOriginHeightColor(self.
context, validated_filename, origin.to_list(), height, color.to_list(), upaxis, silent)
782 elif origin
is not None and height
is not None and rotation
is not None and color
is not None:
784 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
785 return context_wrapper.loadPLYWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation_list, color.to_list(), upaxis, silent)
788 raise ValueError(
"Invalid parameter combination. When using transformations, both origin and height are required.")
790 def loadOBJ(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
791 scale: Optional[vec3] =
None, rotation: Optional[SphericalCoord] =
None,
792 color: Optional[RGBcolor] =
None, upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
794 Load geometry from an OBJ (Wavefront) file.
797 filename: Path to the OBJ file to load
798 origin: Origin point for positioning the geometry (optional)
799 height: Height scaling factor (optional, alternative to scale)
800 scale: Scale factor for all dimensions (optional, alternative to height)
801 rotation: Rotation to apply to the geometry (optional)
802 color: Default color for geometry without color data (optional)
803 upaxis: Up axis orientation ("YUP" or "ZUP")
804 silent: If True, suppress loading output messages
807 List of UUIDs for the loaded primitives
813 if origin
is None and height
is None and scale
is None and rotation
is None and color
is None:
815 return context_wrapper.loadOBJ(self.
context, validated_filename, silent)
817 elif origin
is not None and height
is not None and scale
is None and rotation
is not None and color
is not None:
819 return context_wrapper.loadOBJWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), silent)
821 elif origin
is not None and height
is not None and scale
is None and rotation
is not None and color
is not None and upaxis !=
"YUP":
823 return context_wrapper.loadOBJWithOriginHeightRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), upaxis, silent)
825 elif origin
is not None and scale
is not None and rotation
is not None and color
is not None:
827 return context_wrapper.loadOBJWithOriginScaleRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), scale.to_list(), rotation.to_list(), color.to_list(), upaxis, silent)
830 raise ValueError(
"Invalid parameter combination. For OBJ loading, you must provide either: " +
831 "1) No parameters (simple load), " +
832 "2) origin + height + rotation + color, " +
833 "3) origin + height + rotation + color + upaxis, or " +
834 "4) origin + scale + rotation + color + upaxis")
836 def loadXML(self, filename: str, quiet: bool =
False) -> List[int]:
838 Load geometry from a Helios XML file.
841 filename: Path to the XML file to load
842 quiet: If True, suppress loading output messages
845 List of UUIDs for the loaded primitives
851 return context_wrapper.loadXML(self.
context, validated_filename, quiet)
853 def writePLY(self, filename: str, UUIDs: Optional[List[int]] =
None) ->
None:
855 Write geometry to a PLY (Stanford Polygon) file.
858 filename: Path to the output PLY file
859 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
862 ValueError: If filename is invalid or UUIDs are invalid
863 PermissionError: If output directory is not writable
864 FileNotFoundError: If UUIDs do not exist in context
865 RuntimeError: If Context is in mock mode
868 >>> context.writePLY("output.ply") # Export all primitives
869 >>> context.writePLY("subset.ply", [uuid1, uuid2]) # Export specific primitives
878 context_wrapper.writePLY(self.
context, validated_filename)
882 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
889 context_wrapper.writePLYWithUUIDs(self.
context, validated_filename, UUIDs)
891 def writeOBJ(self, filename: str, UUIDs: Optional[List[int]] =
None,
892 primitive_data_fields: Optional[List[str]] =
None,
893 write_normals: bool =
False, silent: bool =
False) ->
None:
895 Write geometry to an OBJ (Wavefront) file.
898 filename: Path to the output OBJ file
899 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
900 primitive_data_fields: Optional list of primitive data field names to export
901 write_normals: Whether to include vertex normals in the output
902 silent: Whether to suppress output messages during export
905 ValueError: If filename is invalid, UUIDs are invalid, or data fields don't exist
906 PermissionError: If output directory is not writable
907 FileNotFoundError: If UUIDs do not exist in context
908 RuntimeError: If Context is in mock mode
911 >>> context.writeOBJ("output.obj") # Export all primitives
912 >>> context.writeOBJ("subset.obj", [uuid1, uuid2]) # Export specific primitives
913 >>> context.writeOBJ("with_data.obj", [uuid1], ["temperature", "area"]) # Export with data
922 context_wrapper.writeOBJ(self.
context, validated_filename, write_normals, silent)
923 elif primitive_data_fields
is None:
926 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
932 context_wrapper.writeOBJWithUUIDs(self.
context, validated_filename, UUIDs, write_normals, silent)
936 raise ValueError(
"UUIDs list cannot be empty when exporting primitive data")
937 if not primitive_data_fields:
938 raise ValueError(
"primitive_data_fields list cannot be empty")
947 context_wrapper.writeOBJWithPrimitiveData(self.
context, validated_filename, UUIDs, primitive_data_fields, write_normals, silent)
950 colors: Optional[np.ndarray] =
None) -> List[int]:
952 Add triangles from NumPy arrays (compatible with trimesh, Open3D format).
955 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
956 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
957 colors: Optional NumPy array of shape (N, 3) or (M, 3) containing RGB colors as float32/float64
958 If shape (N, 3): per-vertex colors
959 If shape (M, 3): per-triangle colors
962 List of UUIDs for the added triangles
965 ValueError: If array dimensions are invalid
968 if vertices.ndim != 2
or vertices.shape[1] != 3:
969 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
970 if faces.ndim != 2
or faces.shape[1] != 3:
971 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
974 max_vertex_index = np.max(faces)
975 if max_vertex_index >= vertices.shape[0]:
976 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
979 per_vertex_colors =
False
980 per_triangle_colors =
False
981 if colors
is not None:
982 if colors.ndim != 2
or colors.shape[1] != 3:
983 raise ValueError(f
"Colors array must have shape (N, 3) or (M, 3), got {colors.shape}")
984 if colors.shape[0] == vertices.shape[0]:
985 per_vertex_colors =
True
986 elif colors.shape[0] == faces.shape[0]:
987 per_triangle_colors =
True
989 raise ValueError(f
"Colors array shape {colors.shape} doesn't match vertices ({vertices.shape[0]},) or faces ({faces.shape[0]},)")
992 vertices_float = vertices.astype(np.float32)
993 faces_int = faces.astype(np.int32)
994 if colors
is not None:
995 colors_float = colors.astype(np.float32)
999 for i
in range(faces.shape[0]):
1001 v0_idx, v1_idx, v2_idx = faces_int[i]
1004 vertex0 = vertices_float[v0_idx].tolist()
1005 vertex1 = vertices_float[v1_idx].tolist()
1006 vertex2 = vertices_float[v2_idx].tolist()
1011 uuid = context_wrapper.addTriangle(self.
context, vertex0, vertex1, vertex2)
1012 elif per_triangle_colors:
1014 color = colors_float[i].tolist()
1015 uuid = context_wrapper.addTriangleWithColor(self.
context, vertex0, vertex1, vertex2, color)
1016 elif per_vertex_colors:
1018 color = np.mean([colors_float[v0_idx], colors_float[v1_idx], colors_float[v2_idx]], axis=0).tolist()
1019 uuid = context_wrapper.addTriangleWithColor(self.
context, vertex0, vertex1, vertex2, color)
1021 triangle_uuids.append(uuid)
1023 return triangle_uuids
1026 uv_coords: np.ndarray, texture_files: Union[str, List[str]],
1027 material_ids: Optional[np.ndarray] =
None) -> List[int]:
1029 Add textured triangles from NumPy arrays with support for multiple textures.
1031 This method supports both single-texture and multi-texture workflows:
1032 - Single texture: Pass a single texture file string, all faces use the same texture
1033 - Multiple textures: Pass a list of texture files and material_ids array specifying which texture each face uses
1036 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
1037 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
1038 uv_coords: NumPy array of shape (N, 2) containing UV texture coordinates as float32/float64
1039 texture_files: Single texture file path (str) or list of texture file paths (List[str])
1040 material_ids: Optional NumPy array of shape (M,) containing material ID for each face.
1041 If None and texture_files is a list, all faces use texture 0.
1042 If None and texture_files is a string, this parameter is ignored.
1045 List of UUIDs for the added textured triangles
1048 ValueError: If array dimensions are invalid or material IDs are out of range
1051 # Single texture usage (backward compatible)
1052 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, "texture.png")
1054 # Multi-texture usage (Open3D style)
1055 >>> texture_files = ["wood.png", "metal.png", "glass.png"]
1056 >>> material_ids = np.array([0, 0, 1, 1, 2, 2]) # 6 faces using different textures
1057 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, texture_files, material_ids)
1062 if vertices.ndim != 2
or vertices.shape[1] != 3:
1063 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
1064 if faces.ndim != 2
or faces.shape[1] != 3:
1065 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
1066 if uv_coords.ndim != 2
or uv_coords.shape[1] != 2:
1067 raise ValueError(f
"UV coordinates array must have shape (N, 2), got {uv_coords.shape}")
1070 if uv_coords.shape[0] != vertices.shape[0]:
1071 raise ValueError(f
"UV coordinates count ({uv_coords.shape[0]}) must match vertices count ({vertices.shape[0]})")
1074 max_vertex_index = np.max(faces)
1075 if max_vertex_index >= vertices.shape[0]:
1076 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
1079 if isinstance(texture_files, str):
1081 texture_file_list = [texture_files]
1082 if material_ids
is None:
1083 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
1086 if not np.all(material_ids == 0):
1087 raise ValueError(
"When using single texture file, all material IDs must be 0")
1090 texture_file_list = list(texture_files)
1091 if len(texture_file_list) == 0:
1092 raise ValueError(
"Texture files list cannot be empty")
1094 if material_ids
is None:
1096 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
1099 if material_ids.ndim != 1
or material_ids.shape[0] != faces.shape[0]:
1100 raise ValueError(f
"Material IDs must have shape ({faces.shape[0]},), got {material_ids.shape}")
1103 max_material_id = np.max(material_ids)
1104 if max_material_id >= len(texture_file_list):
1105 raise ValueError(f
"Material ID {max_material_id} exceeds texture count {len(texture_file_list)}")
1108 for i, texture_file
in enumerate(texture_file_list):
1111 except (FileNotFoundError, ValueError)
as e:
1112 raise ValueError(f
"Texture file {i} ({texture_file}): {e}")
1115 if 'addTrianglesFromArraysMultiTextured' in context_wrapper._AVAILABLE_TRIANGLE_FUNCTIONS:
1116 return context_wrapper.addTrianglesFromArraysMultiTextured(
1117 self.
context, vertices, faces, uv_coords, texture_file_list, material_ids
1121 from .wrappers.DataTypes
import vec3, vec2
1123 vertices_float = vertices.astype(np.float32)
1124 faces_int = faces.astype(np.int32)
1125 uv_coords_float = uv_coords.astype(np.float32)
1128 for i
in range(faces.shape[0]):
1130 v0_idx, v1_idx, v2_idx = faces_int[i]
1133 vertex0 =
vec3(vertices_float[v0_idx][0], vertices_float[v0_idx][1], vertices_float[v0_idx][2])
1134 vertex1 =
vec3(vertices_float[v1_idx][0], vertices_float[v1_idx][1], vertices_float[v1_idx][2])
1135 vertex2 =
vec3(vertices_float[v2_idx][0], vertices_float[v2_idx][1], vertices_float[v2_idx][2])
1138 uv0 =
vec2(uv_coords_float[v0_idx][0], uv_coords_float[v0_idx][1])
1139 uv1 =
vec2(uv_coords_float[v1_idx][0], uv_coords_float[v1_idx][1])
1140 uv2 =
vec2(uv_coords_float[v2_idx][0], uv_coords_float[v2_idx][1])
1143 material_id = material_ids[i]
1144 texture_file = texture_file_list[material_id]
1148 triangle_uuids.append(uuid)
1150 return triangle_uuids
1158 Set primitive data as signed 32-bit integer.
1161 uuid: UUID of the primitive
1162 label: String key for the data
1163 value: Signed integer value
1165 context_wrapper.setPrimitiveDataInt(self.
context, uuid, label, value)
1169 Set primitive data as unsigned 32-bit integer.
1171 Critical for properties like 'twosided_flag' which must be uint in C++.
1174 uuid: UUID of the primitive
1175 label: String key for the data
1176 value: Unsigned integer value (will be cast to uint32)
1178 context_wrapper.setPrimitiveDataUInt(self.
context, uuid, label, value)
1182 Set primitive data as 32-bit float.
1185 uuid: UUID of the primitive
1186 label: String key for the data
1189 context_wrapper.setPrimitiveDataFloat(self.
context, uuid, label, value)
1193 Set primitive data as string.
1196 uuid: UUID of the primitive
1197 label: String key for the data
1200 context_wrapper.setPrimitiveDataString(self.
context, uuid, label, value)
1204 Get primitive data for a specific primitive. If data_type is provided, it works like before.
1205 If data_type is None, it automatically detects the type and returns the appropriate value.
1208 uuid: UUID of the primitive
1209 label: String key for the data
1210 data_type: Optional. Python type to retrieve (int, uint, float, double, bool, str, vec2, vec3, vec4, int2, int3, int4, etc.)
1211 If None, auto-detects the type using C++ getPrimitiveDataType().
1214 The stored value of the specified or auto-detected type
1217 if data_type
is None:
1218 return context_wrapper.getPrimitiveDataAuto(self.
context, uuid, label)
1221 if data_type == int:
1222 return context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
1223 elif data_type == float:
1224 return context_wrapper.getPrimitiveDataFloat(self.
context, uuid, label)
1225 elif data_type == bool:
1227 int_value = context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
1228 return int_value != 0
1229 elif data_type == str:
1230 return context_wrapper.getPrimitiveDataString(self.
context, uuid, label)
1233 elif data_type == vec2:
1234 coords = context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
1235 return vec2(coords[0], coords[1])
1236 elif data_type == vec3:
1237 coords = context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
1238 return vec3(coords[0], coords[1], coords[2])
1239 elif data_type == vec4:
1240 coords = context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
1241 return vec4(coords[0], coords[1], coords[2], coords[3])
1242 elif data_type == int2:
1243 coords = context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
1244 return int2(coords[0], coords[1])
1245 elif data_type == int3:
1246 coords = context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
1247 return int3(coords[0], coords[1], coords[2])
1248 elif data_type == int4:
1249 coords = context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
1250 return int4(coords[0], coords[1], coords[2], coords[3])
1253 elif data_type ==
"uint":
1254 return context_wrapper.getPrimitiveDataUInt(self.
context, uuid, label)
1255 elif data_type ==
"double":
1256 return context_wrapper.getPrimitiveDataDouble(self.
context, uuid, label)
1259 elif data_type == list:
1261 return context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
1262 elif data_type ==
"list_vec2":
1263 return context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
1264 elif data_type ==
"list_vec4":
1265 return context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
1266 elif data_type ==
"list_int2":
1267 return context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
1268 elif data_type ==
"list_int3":
1269 return context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
1270 elif data_type ==
"list_int4":
1271 return context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
1274 raise ValueError(f
"Unsupported primitive data type: {data_type}. "
1275 f
"Supported types: int, float, bool, str, vec2, vec3, vec4, int2, int3, int4, "
1276 f
"'uint', 'double', list (for vec3), 'list_vec2', 'list_vec4', 'list_int2', 'list_int3', 'list_int4'")
1280 Check if primitive data exists for a specific primitive and label.
1283 uuid: UUID of the primitive
1284 label: String key for the data
1287 True if the data exists, False otherwise
1289 return context_wrapper.doesPrimitiveDataExistWrapper(self.
context, uuid, label)
1293 Convenience method to get float primitive data.
1296 uuid: UUID of the primitive
1297 label: String key for the data
1300 Float value stored for the primitive
1306 Get the Helios data type of primitive data.
1309 uuid: UUID of the primitive
1310 label: String key for the data
1313 HeliosDataType enum value as integer
1315 return context_wrapper.getPrimitiveDataTypeWrapper(self.
context, uuid, label)
1319 Get the size/length of primitive data (for vector data).
1322 uuid: UUID of the primitive
1323 label: String key for the data
1326 Size of data array, or 1 for scalar data
1328 return context_wrapper.getPrimitiveDataSizeWrapper(self.
context, uuid, label)
1332 Get primitive data values for multiple primitives as a NumPy array.
1334 This method retrieves primitive data for a list of UUIDs and returns the values
1335 as a NumPy array. The output array has the same length as the input UUID list,
1336 with each index corresponding to the primitive data value for that UUID.
1339 uuids: List of primitive UUIDs to get data for
1340 label: String key for the primitive data to retrieve
1343 NumPy array of primitive data values corresponding to each UUID.
1344 The array type depends on the data type:
1345 - int data: int32 array
1346 - uint data: uint32 array
1347 - float data: float32 array
1348 - double data: float64 array
1349 - vector data: float32 array with shape (N, vector_size)
1350 - string data: object array of strings
1353 ValueError: If UUID list is empty or UUIDs don't exist
1354 RuntimeError: If context is in mock mode or data doesn't exist for some UUIDs
1359 raise ValueError(
"UUID list cannot be empty")
1368 raise ValueError(f
"Primitive data '{label}' does not exist for UUID {uuid}")
1371 first_uuid = uuids[0]
1377 result = np.empty(len(uuids), dtype=np.int32)
1378 for i, uuid
in enumerate(uuids):
1381 elif data_type == 1:
1382 result = np.empty(len(uuids), dtype=np.uint32)
1383 for i, uuid
in enumerate(uuids):
1386 elif data_type == 2:
1387 result = np.empty(len(uuids), dtype=np.float32)
1388 for i, uuid
in enumerate(uuids):
1391 elif data_type == 3:
1392 result = np.empty(len(uuids), dtype=np.float64)
1393 for i, uuid
in enumerate(uuids):
1396 elif data_type == 4:
1397 result = np.empty((len(uuids), 2), dtype=np.float32)
1398 for i, uuid
in enumerate(uuids):
1400 result[i] = [vec_data.x, vec_data.y]
1402 elif data_type == 5:
1403 result = np.empty((len(uuids), 3), dtype=np.float32)
1404 for i, uuid
in enumerate(uuids):
1406 result[i] = [vec_data.x, vec_data.y, vec_data.z]
1408 elif data_type == 6:
1409 result = np.empty((len(uuids), 4), dtype=np.float32)
1410 for i, uuid
in enumerate(uuids):
1412 result[i] = [vec_data.x, vec_data.y, vec_data.z, vec_data.w]
1414 elif data_type == 7:
1415 result = np.empty((len(uuids), 2), dtype=np.int32)
1416 for i, uuid
in enumerate(uuids):
1418 result[i] = [int_data.x, int_data.y]
1420 elif data_type == 8:
1421 result = np.empty((len(uuids), 3), dtype=np.int32)
1422 for i, uuid
in enumerate(uuids):
1424 result[i] = [int_data.x, int_data.y, int_data.z]
1426 elif data_type == 9:
1427 result = np.empty((len(uuids), 4), dtype=np.int32)
1428 for i, uuid
in enumerate(uuids):
1430 result[i] = [int_data.x, int_data.y, int_data.z, int_data.w]
1432 elif data_type == 10:
1433 result = np.empty(len(uuids), dtype=object)
1434 for i, uuid
in enumerate(uuids):
1438 raise ValueError(f
"Unsupported primitive data type: {data_type}")
1444 colormap: str =
"hot", ncolors: int = 10,
1445 max_val: Optional[float] =
None, min_val: Optional[float] =
None):
1447 Color primitives based on primitive data values using pseudocolor mapping.
1449 This method applies a pseudocolor mapping to primitives based on the values
1450 of specified primitive data. The primitive colors are updated to reflect the
1451 data values using a color map.
1454 uuids: List of primitive UUIDs to color
1455 primitive_data: Name of primitive data to use for coloring (e.g., "radiation_flux_SW")
1456 colormap: Color map name - options include "hot", "cool", "parula", "rainbow", "gray", "lava"
1457 ncolors: Number of discrete colors in color map (default: 10)
1458 max_val: Maximum value for color scale (auto-determined if None)
1459 min_val: Minimum value for color scale (auto-determined if None)
1461 if max_val
is not None and min_val
is not None:
1462 context_wrapper.colorPrimitiveByDataPseudocolorWithRange(
1463 self.
context, uuids, primitive_data, colormap, ncolors, max_val, min_val)
1465 context_wrapper.colorPrimitiveByDataPseudocolor(
1466 self.
context, uuids, primitive_data, colormap, ncolors)
1469 def setTime(self, hour: int, minute: int = 0, second: int = 0):
1471 Set the simulation time.
1475 minute: Minute (0-59), defaults to 0
1476 second: Second (0-59), defaults to 0
1479 ValueError: If time values are out of range
1480 NotImplementedError: If time/date functions not available in current library build
1483 >>> context.setTime(14, 30) # Set to 2:30 PM
1484 >>> context.setTime(9, 15, 30) # Set to 9:15:30 AM
1486 context_wrapper.setTime(self.
context, hour, minute, second)
1488 def setDate(self, year: int, month: int, day: int):
1490 Set the simulation date.
1493 year: Year (1900-3000)
1498 ValueError: If date values are out of range
1499 NotImplementedError: If time/date functions not available in current library build
1502 >>> context.setDate(2023, 6, 21) # Set to June 21, 2023
1504 context_wrapper.setDate(self.
context, year, month, day)
1508 Set the simulation date using Julian day number.
1511 julian_day: Julian day (1-366)
1512 year: Year (1900-3000)
1515 ValueError: If values are out of range
1516 NotImplementedError: If time/date functions not available in current library build
1519 >>> context.setDateJulian(172, 2023) # Set to day 172 of 2023 (June 21)
1521 context_wrapper.setDateJulian(self.
context, julian_day, year)
1525 Get the current simulation time.
1528 Tuple of (hour, minute, second) as integers
1531 NotImplementedError: If time/date functions not available in current library build
1534 >>> hour, minute, second = context.getTime()
1535 >>> print(f"Current time: {hour:02d}:{minute:02d}:{second:02d}")
1537 return context_wrapper.getTime(self.
context)
1541 Get the current simulation date.
1544 Tuple of (year, month, day) as integers
1547 NotImplementedError: If time/date functions not available in current library build
1550 >>> year, month, day = context.getDate()
1551 >>> print(f"Current date: {year}-{month:02d}-{day:02d}")
1553 return context_wrapper.getDate(self.
context)
1558 Get list of available plugins for this PyHelios instance.
1561 List of available plugin names
1567 Check if a specific plugin is available.
1570 plugin_name: Name of the plugin to check
1573 True if plugin is available, False otherwise
1579 Get detailed information about available plugin capabilities.
1582 Dictionary mapping plugin names to capability information
1587 """Print detailed plugin status information."""
1592 Get list of requested plugins that are not available.
1595 requested_plugins: List of plugin names to check
1598 List of missing plugin names
Central simulation environment for PyHelios that manages 3D primitives and their data.
_validate_uuid(self, int uuid)
Validate that a UUID exists in this context.
List[int] getAllUUIDs(self)
List[int] addBox(self, vec3 center=vec3(0, 0, 0), vec3 size=vec3(1, 1, 1), int3 subdiv=int3(1, 1, 1), Optional[RGBcolor] color=None)
Add a rectangular box to the context.
str _validate_output_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize output file path for security.
np.ndarray getPrimitiveDataArray(self, List[int] uuids, str label)
Get primitive data values for multiple primitives as a NumPy array.
bool is_plugin_available(self, str plugin_name)
Check if a specific plugin is available.
List[int] getAllObjectIDs(self)
List[str] get_available_plugins(self)
Get list of available plugins for this PyHelios instance.
__exit__(self, exc_type, exc_value, traceback)
RGBcolor getPrimitiveColor(self, int uuid)
bool doesPrimitiveDataExist(self, int uuid, str label)
Check if primitive data exists for a specific primitive and label.
float getPrimitiveArea(self, int uuid)
int getPrimitiveCount(self)
List[PrimitiveInfo] getAllPrimitiveInfo(self)
Get physical properties and geometry information for all primitives in the context.
None writePLY(self, str filename, Optional[List[int]] UUIDs=None)
Write geometry to a PLY (Stanford Polygon) file.
List[PrimitiveInfo] getPrimitivesInfoForObject(self, int object_id)
Get physical properties and geometry information for all primitives belonging to a specific object.
print_plugin_status(self)
Print detailed plugin status information.
setDate(self, int year, int month, int day)
Set the simulation date.
PrimitiveType getPrimitiveType(self, int uuid)
getPrimitiveData(self, int uuid, str label, type data_type=None)
Get primitive data for a specific primitive.
List[str] get_missing_plugins(self, List[str] requested_plugins)
Get list of requested plugins that are not available.
bool isGeometryDirty(self)
colorPrimitiveByDataPseudocolor(self, List[int] uuids, str primitive_data, str colormap="hot", int ncolors=10, Optional[float] max_val=None, Optional[float] min_val=None)
Color primitives based on primitive data values using pseudocolor mapping.
List[int] addTile(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), Optional[SphericalCoord] rotation=None, int2 subdiv=int2(1, 1), Optional[RGBcolor] color=None)
Add a subdivided patch (tile) to the context.
_check_context_available(self)
Helper method to check if context is available with detailed error messages.
None setPrimitiveDataUInt(self, int uuid, str label, int value)
Set primitive data as unsigned 32-bit integer.
int addPatch(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), Optional[SphericalCoord] rotation=None, Optional[RGBcolor] color=None)
List[int] loadOBJ(self, str filename, Optional[vec3] origin=None, Optional[float] height=None, Optional[vec3] scale=None, Optional[SphericalCoord] rotation=None, Optional[RGBcolor] color=None, str upaxis="YUP", bool silent=False)
Load geometry from an OBJ (Wavefront) file.
List[int] addTube(self, List[vec3] nodes, Union[float, List[float]] radii, int ndivs=6, Optional[Union[RGBcolor, List[RGBcolor]]] colors=None)
Add a tube (pipe/cylinder) to the context.
str _validate_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize file path for security.
None setPrimitiveDataInt(self, int uuid, str label, int value)
Set primitive data as signed 32-bit integer.
None setPrimitiveDataString(self, int uuid, str label, str value)
Set primitive data as string.
None setPrimitiveDataFloat(self, int uuid, str label, float value)
Set primitive data as 32-bit float.
PrimitiveInfo getPrimitiveInfo(self, int uuid)
Get physical properties and geometry information for a single primitive.
int getPrimitiveDataSize(self, int uuid, str label)
Get the size/length of primitive data (for vector data).
vec3 getPrimitiveNormal(self, int uuid)
int addTriangleTextured(self, vec3 vertex0, vec3 vertex1, vec3 vertex2, str texture_file, vec2 uv0, vec2 uv1, vec2 uv2)
Add a textured triangle primitive to the context.
int getPrimitiveDataType(self, int uuid, str label)
Get the Helios data type of primitive data.
List[int] loadPLY(self, str filename, Optional[vec3] origin=None, Optional[float] height=None, Optional[SphericalCoord] rotation=None, Optional[RGBcolor] color=None, str upaxis="YUP", bool silent=False)
Load geometry from a PLY (Stanford Polygon) file.
None writeOBJ(self, str filename, Optional[List[int]] UUIDs=None, Optional[List[str]] primitive_data_fields=None, bool write_normals=False, bool silent=False)
Write geometry to an OBJ (Wavefront) file.
List[vec3] getPrimitiveVertices(self, int uuid)
float getPrimitiveDataFloat(self, int uuid, str label)
Convenience method to get float primitive data.
dict get_plugin_capabilities(self)
Get detailed information about available plugin capabilities.
getTime(self)
Get the current simulation time.
setDateJulian(self, int julian_day, int year)
Set the simulation date using Julian day number.
int addTriangle(self, vec3 vertex0, vec3 vertex1, vec3 vertex2, Optional[RGBcolor] color=None)
Add a triangle primitive to the context.
getDate(self)
Get the current simulation date.
List[int] addTrianglesFromArraysTextured(self, np.ndarray vertices, np.ndarray faces, np.ndarray uv_coords, Union[str, List[str]] texture_files, Optional[np.ndarray] material_ids=None)
Add textured triangles from NumPy arrays with support for multiple textures.
List[int] loadXML(self, str filename, bool quiet=False)
Load geometry from a Helios XML file.
List[int] addSphere(self, vec3 center=vec3(0, 0, 0), float radius=1.0, int ndivs=10, Optional[RGBcolor] color=None)
Add a sphere to the context.
List[int] addTrianglesFromArrays(self, np.ndarray vertices, np.ndarray faces, Optional[np.ndarray] colors=None)
Add triangles from NumPy arrays (compatible with trimesh, Open3D format).
setTime(self, int hour, int minute=0, int second=0)
Set the simulation time.
Physical properties and geometry information for a primitive.
__post_init__(self)
Calculate centroid from vertices if not provided.
Helios primitive type enumeration.