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, RGBAcolor, 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)
284 """Destructor to ensure C++ resources freed even without 'with' statement."""
285 if hasattr(self,
'context')
and self.
context is not None:
287 context_wrapper.destroyContext(self.
context)
290 except Exception
as e:
292 warnings.warn(f
"Error in Context.__del__: {e}")
300 context_wrapper.markGeometryClean(self.
context)
304 context_wrapper.markGeometryDirty(self.
context)
309 return context_wrapper.isGeometryDirty(self.
context)
313 Seed the random number generator for reproducible stochastic results.
316 seed: Integer seed value for random number generation
319 This is critical for reproducible results in stochastic simulations
320 (e.g., LiDAR scans with beam divergence, random perturbations).
323 context_wrapper.helios_lib.seedRandomGenerator(self.
context, seed)
325 @validate_patch_params
326 def addPatch(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1), rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None) -> int:
331 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
332 return context_wrapper.addPatchWithCenterSizeRotationAndColor(self.
context, center.to_list(), size.to_list(), rotation_list, color.to_list())
334 @validate_triangle_params
335 def addTriangle(self, vertex0: vec3, vertex1: vec3, vertex2: vec3, color: Optional[RGBcolor] =
None) -> int:
336 """Add a triangle primitive to the context
339 vertex0: First vertex of the triangle
340 vertex1: Second vertex of the triangle
341 vertex2: Third vertex of the triangle
342 color: Optional triangle color (defaults to white)
345 UUID of the created triangle primitive
349 return context_wrapper.addTriangle(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list())
351 return context_wrapper.addTriangleWithColor(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list(), color.to_list())
354 texture_file: str, uv0: vec2, uv1: vec2, uv2: vec2) -> int:
355 """Add a textured triangle primitive to the context
357 Creates a triangle with texture mapping. The texture image is mapped to the triangle
358 surface using UV coordinates, where (0,0) represents the top-left corner of the image
359 and (1,1) represents the bottom-right corner.
362 vertex0: First vertex of the triangle
363 vertex1: Second vertex of the triangle
364 vertex2: Third vertex of the triangle
365 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
366 uv0: UV texture coordinates for first vertex
367 uv1: UV texture coordinates for second vertex
368 uv2: UV texture coordinates for third vertex
371 UUID of the created textured triangle primitive
374 ValueError: If texture file path is invalid
375 FileNotFoundError: If texture file doesn't exist
376 RuntimeError: If context is in mock mode
379 >>> context = Context()
380 >>> # Create a textured triangle
381 >>> vertex0 = vec3(0, 0, 0)
382 >>> vertex1 = vec3(1, 0, 0)
383 >>> vertex2 = vec3(0.5, 1, 0)
384 >>> uv0 = vec2(0, 0) # Bottom-left of texture
385 >>> uv1 = vec2(1, 0) # Bottom-right of texture
386 >>> uv2 = vec2(0.5, 1) # Top-center of texture
387 >>> uuid = context.addTriangleTextured(vertex0, vertex1, vertex2,
388 ... "texture.png", uv0, uv1, uv2)
394 [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp'])
397 return context_wrapper.addTriangleWithTexture(
399 vertex0.to_list(), vertex1.to_list(), vertex2.to_list(),
400 validated_texture_file,
401 uv0.to_list(), uv1.to_list(), uv2.to_list()
406 primitive_type = context_wrapper.getPrimitiveType(self.
context, uuid)
411 return context_wrapper.getPrimitiveArea(self.
context, uuid)
415 normal_ptr = context_wrapper.getPrimitiveNormal(self.
context, uuid)
416 v =
vec3(normal_ptr[0], normal_ptr[1], normal_ptr[2])
421 size = ctypes.c_uint()
422 vertices_ptr = context_wrapper.getPrimitiveVertices(self.
context, uuid, ctypes.byref(size))
424 vertices_list = ctypes.cast(vertices_ptr, ctypes.POINTER(ctypes.c_float * size.value)).contents
425 vertices = [
vec3(vertices_list[i], vertices_list[i+1], vertices_list[i+2])
for i
in range(0, size.value, 3)]
430 color_ptr = context_wrapper.getPrimitiveColor(self.
context, uuid)
431 return RGBcolor(color_ptr[0], color_ptr[1], color_ptr[2])
435 return context_wrapper.getPrimitiveCount(self.
context)
439 size = ctypes.c_uint()
440 uuids_ptr = context_wrapper.getAllUUIDs(self.
context, ctypes.byref(size))
441 return list(uuids_ptr[:size.value])
445 return context_wrapper.getObjectCount(self.
context)
449 size = ctypes.c_uint()
450 objectids_ptr = context_wrapper.getAllObjectIDs(self.
context, ctypes.byref(size))
451 return list(objectids_ptr[:size.value])
455 Get physical properties and geometry information for a single primitive.
458 uuid: UUID of the primitive
461 PrimitiveInfo object containing physical properties and geometry
473 primitive_type=primitive_type,
482 Get physical properties and geometry information for all primitives in the context.
485 List of PrimitiveInfo objects for all primitives
492 Get physical properties and geometry information for all primitives belonging to a specific object.
495 object_id: ID of the object
498 List of PrimitiveInfo objects for primitives in the object
500 object_uuids = context_wrapper.getObjectPrimitiveUUIDs(self.
context, object_id)
504 def addTile(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
505 rotation: Optional[SphericalCoord] =
None, subdiv: int2 =
int2(1, 1),
506 color: Optional[RGBcolor] =
None) -> List[int]:
508 Add a subdivided patch (tile) to the context.
510 A tile is a patch subdivided into a regular grid of smaller patches,
511 useful for creating detailed surfaces or terrain.
514 center: 3D coordinates of tile center (default: origin)
515 size: Width and height of the tile (default: 1x1)
516 rotation: Orientation of the tile (default: no rotation)
517 subdiv: Number of subdivisions in x and y directions (default: 1x1)
518 color: Color of the tile (default: white)
521 List of UUIDs for all patches created in the tile
524 >>> context = Context()
525 >>> # Create a 2x2 meter tile subdivided into 4x4 patches
526 >>> tile_uuids = context.addTile(
527 ... center=vec3(0, 0, 1),
529 ... subdiv=int2(4, 4),
530 ... color=RGBcolor(0.5, 0.8, 0.2)
532 >>> print(f"Created {len(tile_uuids)} patches")
537 if not isinstance(center, vec3):
538 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
539 if not isinstance(size, vec2):
540 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
541 if rotation
is not None and not isinstance(rotation, SphericalCoord):
542 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
543 if not isinstance(subdiv, int2):
544 raise ValueError(f
"Subdiv must be an int2, got {type(subdiv).__name__}")
545 if color
is not None and not isinstance(color, RGBcolor):
546 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
549 if any(s <= 0
for s
in size.to_list()):
550 raise ValueError(
"All size dimensions must be positive")
551 if any(s <= 0
for s
in subdiv.to_list()):
552 raise ValueError(
"All subdivision counts must be positive")
558 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
560 if color
and not (color.r == 1.0
and color.g == 1.0
and color.b == 1.0):
561 return context_wrapper.addTileWithColor(
562 self.
context, center.to_list(), size.to_list(),
563 rotation_list, subdiv.to_list(), color.to_list()
566 return context_wrapper.addTile(
567 self.
context, center.to_list(), size.to_list(),
568 rotation_list, subdiv.to_list()
571 @validate_sphere_params
572 def addSphere(self, center: vec3 =
vec3(0, 0, 0), radius: float = 1.0,
573 ndivs: int = 10, color: Optional[RGBcolor] =
None) -> List[int]:
575 Add a sphere to the context.
577 The sphere is tessellated into triangular faces based on the specified
581 center: 3D coordinates of sphere center (default: origin)
582 radius: Radius of the sphere (default: 1.0)
583 ndivs: Number of divisions for tessellation (default: 10)
584 Higher values create smoother spheres but more triangles
585 color: Color of the sphere (default: white)
588 List of UUIDs for all triangles created in the sphere
591 >>> context = Context()
592 >>> # Create a red sphere at (1, 2, 3) with radius 0.5
593 >>> sphere_uuids = context.addSphere(
594 ... center=vec3(1, 2, 3),
597 ... color=RGBcolor(1, 0, 0)
599 >>> print(f"Created sphere with {len(sphere_uuids)} triangles")
604 if not isinstance(center, vec3):
605 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
606 if not isinstance(radius, (int, float)):
607 raise ValueError(f
"Radius must be a number, got {type(radius).__name__}")
608 if not isinstance(ndivs, int):
609 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
610 if color
is not None and not isinstance(color, RGBcolor):
611 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
615 raise ValueError(
"Sphere radius must be positive")
617 raise ValueError(
"Number of divisions must be at least 3")
620 return context_wrapper.addSphereWithColor(
621 self.
context, ndivs, center.to_list(), radius, color.to_list()
624 return context_wrapper.addSphere(
625 self.
context, ndivs, center.to_list(), radius
628 @validate_tube_params
629 def addTube(self, nodes: List[vec3], radii: Union[float, List[float]],
630 ndivs: int = 6, colors: Optional[Union[RGBcolor, List[RGBcolor]]] =
None) -> List[int]:
632 Add a tube (pipe/cylinder) to the context.
634 The tube is defined by a series of nodes (path) with radius at each node.
635 It's tessellated into triangular faces based on the number of radial divisions.
638 nodes: List of 3D points defining the tube path (at least 2 nodes)
639 radii: Radius at each node. Can be:
640 - Single float: constant radius for all nodes
641 - List of floats: radius for each node (must match nodes length)
642 ndivs: Number of radial divisions (default: 6)
643 Higher values create smoother tubes but more triangles
644 colors: Colors at each node. Can be:
646 - Single RGBcolor: constant color for all nodes
647 - List of RGBcolor: color for each node (must match nodes length)
650 List of UUIDs for all triangles created in the tube
653 >>> context = Context()
654 >>> # Create a curved tube with varying radius
655 >>> nodes = [vec3(0, 0, 0), vec3(1, 0, 0), vec3(2, 1, 0)]
656 >>> radii = [0.1, 0.2, 0.1]
657 >>> colors = [RGBcolor(1, 0, 0), RGBcolor(0, 1, 0), RGBcolor(0, 0, 1)]
658 >>> tube_uuids = context.addTube(nodes, radii, ndivs=8, colors=colors)
659 >>> print(f"Created tube with {len(tube_uuids)} triangles")
664 if not isinstance(nodes, (list, tuple)):
665 raise ValueError(f
"Nodes must be a list or tuple, got {type(nodes).__name__}")
666 if not isinstance(ndivs, int):
667 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
668 if colors
is not None and not isinstance(colors, (RGBcolor, list, tuple)):
669 raise ValueError(f
"Colors must be RGBcolor, list, tuple, or None, got {type(colors).__name__}")
673 raise ValueError(
"Tube requires at least 2 nodes")
675 raise ValueError(
"Number of radial divisions must be at least 3")
678 if isinstance(radii, (int, float)):
679 radii_list = [float(radii)] * len(nodes)
681 radii_list = [float(r)
for r
in radii]
682 if len(radii_list) != len(nodes):
683 raise ValueError(f
"Number of radii ({len(radii_list)}) must match number of nodes ({len(nodes)})")
686 if any(r <= 0
for r
in radii_list):
687 raise ValueError(
"All radii must be positive")
692 nodes_flat.extend(node.to_list())
696 return context_wrapper.addTube(self.
context, ndivs, nodes_flat, radii_list)
697 elif isinstance(colors, RGBcolor):
699 colors_flat = colors.to_list() * len(nodes)
702 if len(colors) != len(nodes):
703 raise ValueError(f
"Number of colors ({len(colors)}) must match number of nodes ({len(nodes)})")
706 colors_flat.extend(color.to_list())
708 return context_wrapper.addTubeWithColor(self.
context, ndivs, nodes_flat, radii_list, colors_flat)
711 def addBox(self, center: vec3 =
vec3(0, 0, 0), size: vec3 =
vec3(1, 1, 1),
712 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None) -> List[int]:
714 Add a rectangular box to the context.
716 The box is subdivided into patches on each face based on the specified
720 center: 3D coordinates of box center (default: origin)
721 size: Width, height, and depth of the box (default: 1x1x1)
722 subdiv: Number of subdivisions in x, y, and z directions (default: 1x1x1)
723 Higher values create more detailed surfaces
724 color: Color of the box (default: white)
727 List of UUIDs for all patches created on the box faces
730 >>> context = Context()
731 >>> # Create a blue box subdivided for detail
732 >>> box_uuids = context.addBox(
733 ... center=vec3(0, 0, 2),
734 ... size=vec3(2, 1, 0.5),
735 ... subdiv=int3(4, 2, 1),
736 ... color=RGBcolor(0, 0, 1)
738 >>> print(f"Created box with {len(box_uuids)} patches")
743 if not isinstance(center, vec3):
744 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
745 if not isinstance(size, vec3):
746 raise ValueError(f
"Size must be a vec3, got {type(size).__name__}")
747 if not isinstance(subdiv, int3):
748 raise ValueError(f
"Subdiv must be an int3, got {type(subdiv).__name__}")
749 if color
is not None and not isinstance(color, RGBcolor):
750 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
753 if any(s <= 0
for s
in size.to_list()):
754 raise ValueError(
"All box dimensions must be positive")
755 if any(s < 1
for s
in subdiv.to_list()):
756 raise ValueError(
"All subdivision counts must be at least 1")
759 return context_wrapper.addBoxWithColor(
760 self.
context, center.to_list(), size.to_list(),
761 subdiv.to_list(), color.to_list()
764 return context_wrapper.addBox(
765 self.
context, center.to_list(), size.to_list(), subdiv.to_list()
768 def addDisk(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
769 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
770 color: Optional[Union[RGBcolor, RGBAcolor]] =
None) -> List[int]:
772 Add a disk (circular or elliptical surface) to the context.
774 A disk is a flat circular or elliptical surface tessellated into
775 triangular faces. Supports both uniform radial subdivisions and
776 separate radial/azimuthal subdivisions for finer control.
779 center: 3D coordinates of disk center (default: origin)
780 size: Semi-major and semi-minor radii of the disk (default: 1x1 circle)
781 ndivs: Number of radial divisions (int) or [radial, azimuthal] divisions (int2)
782 (default: 20). Higher values create smoother circles but more triangles.
783 rotation: Orientation of the disk (default: horizontal, normal = +z)
784 color: Color of the disk (default: white). Can be RGBcolor or RGBAcolor for transparency.
787 List of UUIDs for all triangles created in the disk
790 >>> context = Context()
791 >>> # Create a red disk at (0, 0, 1) with radius 0.5
792 >>> disk_uuids = context.addDisk(
793 ... center=vec3(0, 0, 1),
794 ... size=vec2(0.5, 0.5),
796 ... color=RGBcolor(1, 0, 0)
798 >>> print(f"Created disk with {len(disk_uuids)} triangles")
800 >>> # Create a semi-transparent blue elliptical disk
801 >>> disk_uuids = context.addDisk(
802 ... center=vec3(0, 0, 2),
803 ... size=vec2(1.0, 0.5),
805 ... rotation=SphericalCoord(1, 0.5, 0),
806 ... color=RGBAcolor(0, 0, 1, 0.5)
809 >>> # Create disk with polar/radial subdivisions for finer control
810 >>> disk_uuids = context.addDisk(
811 ... center=vec3(0, 0, 3),
813 ... ndivs=int2(10, 20), # 10 radial, 20 azimuthal divisions
814 ... color=RGBcolor(0, 1, 0)
820 if not isinstance(center, vec3):
821 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
822 if not isinstance(size, vec2):
823 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
824 if not isinstance(ndivs, (int, int2)):
825 raise ValueError(f
"Ndivs must be an int or int2, got {type(ndivs).__name__}")
826 if rotation
is not None and not isinstance(rotation, SphericalCoord):
827 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
828 if color
is not None and not isinstance(color, (RGBcolor, RGBAcolor)):
829 raise ValueError(f
"Color must be an RGBcolor, RGBAcolor, or None, got {type(color).__name__}")
832 if any(s <= 0
for s
in size.to_list()):
833 raise ValueError(
"Disk size must be positive")
836 if isinstance(ndivs, int):
838 raise ValueError(
"Number of divisions must be at least 3")
840 if any(n < 1
for n
in ndivs.to_list()):
841 raise ValueError(
"Radial and angular divisions must be at least 1")
849 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
852 if isinstance(ndivs, int2):
855 if isinstance(color, RGBAcolor):
856 return context_wrapper.addDiskPolarSubdivisionsRGBA(
857 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
858 rotation_list, color.to_list()
862 return context_wrapper.addDiskPolarSubdivisions(
863 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
864 rotation_list, color.to_list()
868 color_list = [1.0, 1.0, 1.0]
869 return context_wrapper.addDiskPolarSubdivisions(
870 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
871 rotation_list, color_list
876 if isinstance(color, RGBAcolor):
878 return context_wrapper.addDiskWithRGBAColor(
879 self.
context, ndivs, center.to_list(), size.to_list(),
880 rotation_list, color.to_list()
884 return context_wrapper.addDiskWithColor(
885 self.
context, ndivs, center.to_list(), size.to_list(),
886 rotation_list, color.to_list()
890 return context_wrapper.addDiskWithRotation(
891 self.
context, ndivs, center.to_list(), size.to_list(),
895 def addCone(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
896 ndivs: int = 20, color: Optional[RGBcolor] =
None) -> List[int]:
898 Add a cone (or cylinder/frustum) to the context.
900 A cone is a 3D shape connecting two circular cross-sections with
901 potentially different radii. When radii are equal, creates a cylinder.
902 When one radius is zero, creates a true cone.
905 node0: 3D coordinates of the base center
906 node1: 3D coordinates of the apex center
907 radius0: Radius at base (node0). Use 0 for pointed end.
908 radius1: Radius at apex (node1). Use 0 for pointed end.
909 ndivs: Number of radial divisions for tessellation (default: 20)
910 color: Color of the cone (default: white)
913 List of UUIDs for all triangles created in the cone
916 >>> context = Context()
917 >>> # Create a cylinder (equal radii)
918 >>> cylinder_uuids = context.addCone(
919 ... node0=vec3(0, 0, 0),
920 ... node1=vec3(0, 0, 2),
926 >>> # Create a true cone (one radius = 0)
927 >>> cone_uuids = context.addCone(
928 ... node0=vec3(1, 0, 0),
929 ... node1=vec3(1, 0, 1.5),
933 ... color=RGBcolor(1, 0, 0)
936 >>> # Create a frustum (different radii)
937 >>> frustum_uuids = context.addCone(
938 ... node0=vec3(2, 0, 0),
939 ... node1=vec3(2, 0, 1),
948 if not isinstance(node0, vec3):
949 raise ValueError(f
"node0 must be a vec3, got {type(node0).__name__}")
950 if not isinstance(node1, vec3):
951 raise ValueError(f
"node1 must be a vec3, got {type(node1).__name__}")
952 if not isinstance(ndivs, int):
953 raise ValueError(f
"ndivs must be an int, got {type(ndivs).__name__}")
954 if color
is not None and not isinstance(color, RGBcolor):
955 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
958 if radius0 < 0
or radius1 < 0:
959 raise ValueError(
"Radii must be non-negative")
961 raise ValueError(
"Number of radial divisions must be at least 3")
965 return context_wrapper.addConeWithColor(
966 self.
context, ndivs, node0.to_list(), node1.to_list(),
967 radius0, radius1, color.to_list()
970 return context_wrapper.addCone(
971 self.
context, ndivs, node0.to_list(), node1.to_list(),
976 radius: Union[float, vec3] = 1.0, ndivs: int = 20,
977 color: Optional[RGBcolor] =
None,
978 texturefile: Optional[str] =
None) -> int:
980 Add a spherical or ellipsoidal compound object to the context.
982 Creates a sphere or ellipsoid as a compound object with a trackable object ID.
983 Primitives within the object are registered as children of the object.
986 center: Center position of sphere/ellipsoid (default: origin)
987 radius: Radius as float (sphere) or vec3 (ellipsoid) (default: 1.0)
988 ndivs: Number of tessellation divisions (default: 20)
989 color: Optional RGB color
990 texturefile: Optional texture image file path
993 Object ID of the created compound object
996 ValueError: If parameters are invalid
997 NotImplementedError: If object-returning functions unavailable
1000 >>> # Create a basic sphere at origin
1001 >>> obj_id = ctx.addSphereObject()
1003 >>> # Create a colored sphere
1004 >>> obj_id = ctx.addSphereObject(
1005 ... center=vec3(0, 0, 5),
1007 ... color=RGBcolor(1, 0, 0)
1010 >>> # Create an ellipsoid (stretched sphere)
1011 >>> obj_id = ctx.addSphereObject(
1012 ... center=vec3(10, 0, 0),
1013 ... radius=vec3(2, 1, 1), # Elongated in x-direction
1021 raise ValueError(
"Number of divisions must be at least 3")
1024 is_ellipsoid = isinstance(radius, vec3)
1030 return context_wrapper.addSphereObject_ellipsoid_texture(
1031 self.
context, ndivs, center.to_list(), radius.to_list(), texturefile
1034 return context_wrapper.addSphereObject_ellipsoid_color(
1035 self.
context, ndivs, center.to_list(), radius.to_list(), color.to_list()
1038 return context_wrapper.addSphereObject_ellipsoid(
1039 self.
context, ndivs, center.to_list(), radius.to_list()
1044 return context_wrapper.addSphereObject_texture(
1045 self.
context, ndivs, center.to_list(), radius, texturefile
1048 return context_wrapper.addSphereObject_color(
1049 self.
context, ndivs, center.to_list(), radius, color.to_list()
1052 return context_wrapper.addSphereObject_basic(
1053 self.
context, ndivs, center.to_list(), radius
1058 subdiv: int2 =
int2(1, 1),
1059 color: Optional[RGBcolor] =
None,
1060 texturefile: Optional[str] =
None,
1061 texture_repeat: Optional[int2] =
None) -> int:
1063 Add a tiled patch (subdivided patch) as a compound object to the context.
1065 Creates a rectangular patch subdivided into a grid of smaller patches,
1066 registered as a compound object with a trackable object ID.
1069 center: Center position of tile (default: origin)
1070 size: Size in x and y directions (default: 1x1)
1071 rotation: Spherical rotation (default: no rotation)
1072 subdiv: Number of subdivisions in x and y (default: 1x1)
1073 color: Optional RGB color
1074 texturefile: Optional texture image file path
1075 texture_repeat: Optional texture repetitions in x and y
1078 Object ID of the created compound object
1081 ValueError: If parameters are invalid
1082 NotImplementedError: If object-returning functions unavailable
1085 >>> # Create a basic 2x2 tile
1086 >>> obj_id = ctx.addTileObject(
1087 ... center=vec3(0, 0, 0),
1088 ... size=vec2(10, 10),
1089 ... subdiv=int2(2, 2)
1092 >>> # Create a colored tile with rotation
1093 >>> obj_id = ctx.addTileObject(
1094 ... center=vec3(5, 0, 0),
1095 ... size=vec2(10, 5),
1096 ... rotation=SphericalCoord(1, 0, 45),
1097 ... subdiv=int2(4, 2),
1098 ... color=RGBcolor(0, 1, 0)
1104 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1107 if texture_repeat
is not None:
1108 if texturefile
is None:
1109 raise ValueError(
"texture_repeat requires texturefile")
1110 return context_wrapper.addTileObject_texture_repeat(
1111 self.
context, center.to_list(), size.to_list(), rotation_list,
1112 subdiv.to_list(), texturefile, texture_repeat.to_list()
1115 return context_wrapper.addTileObject_texture(
1116 self.
context, center.to_list(), size.to_list(), rotation_list,
1117 subdiv.to_list(), texturefile
1120 return context_wrapper.addTileObject_color(
1121 self.
context, center.to_list(), size.to_list(), rotation_list,
1122 subdiv.to_list(), color.to_list()
1125 return context_wrapper.addTileObject_basic(
1126 self.
context, center.to_list(), size.to_list(), rotation_list,
1131 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None,
1132 texturefile: Optional[str] =
None, reverse_normals: bool =
False) -> int:
1134 Add a rectangular box (prism) as a compound object to the context.
1137 center: Center position (default: origin)
1138 size: Size in x, y, z directions (default: 1x1x1)
1139 subdiv: Subdivisions in x, y, z (default: 1x1x1)
1140 color: Optional RGB color
1141 texturefile: Optional texture file path
1142 reverse_normals: Reverse normal directions (default: False)
1145 Object ID of the created compound object
1151 return context_wrapper.addBoxObject_texture_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile, reverse_normals)
1153 return context_wrapper.addBoxObject_color_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list(), reverse_normals)
1155 raise ValueError(
"reverse_normals requires either color or texturefile")
1157 return context_wrapper.addBoxObject_texture(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile)
1159 return context_wrapper.addBoxObject_color(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list())
1161 return context_wrapper.addBoxObject_basic(self.
context, center.to_list(), size.to_list(), subdiv.to_list())
1163 def addConeObject(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1164 ndivs: int = 20, color: Optional[RGBcolor] =
None,
1165 texturefile: Optional[str] =
None) -> int:
1167 Add a cone/cylinder/frustum as a compound object to the context.
1170 node0: Base position
1172 radius0: Radius at base
1173 radius1: Radius at top
1174 ndivs: Number of radial divisions (default: 20)
1175 color: Optional RGB color
1176 texturefile: Optional texture file path
1179 Object ID of the created compound object
1184 return context_wrapper.addConeObject_texture(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, texturefile)
1186 return context_wrapper.addConeObject_color(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, color.to_list())
1188 return context_wrapper.addConeObject_basic(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1)
1191 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
1192 color: Optional[Union[RGBcolor, RGBAcolor]] =
None,
1193 texturefile: Optional[str] =
None) -> int:
1195 Add a disk as a compound object to the context.
1198 center: Center position (default: origin)
1199 size: Semi-major and semi-minor radii (default: 1x1)
1200 ndivs: int (uniform) or int2 (polar/radial subdivisions) (default: 20)
1201 rotation: Optional spherical rotation
1202 color: Optional RGB or RGBA color
1203 texturefile: Optional texture file path
1206 Object ID of the created compound object
1210 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
if rotation
else [1, 0, 0]
1211 is_polar = isinstance(ndivs, int2)
1215 return context_wrapper.addDiskObject_polar_texture(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, texturefile)
1217 if isinstance(color, RGBAcolor):
1218 return context_wrapper.addDiskObject_polar_rgba(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1220 return context_wrapper.addDiskObject_polar_color(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1222 return context_wrapper.addDiskObject_polar_color(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list,
RGBcolor(0.5, 0.5, 0.5).to_list())
1225 return context_wrapper.addDiskObject_texture(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, texturefile)
1227 if isinstance(color, RGBAcolor):
1228 return context_wrapper.addDiskObject_rgba(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1230 return context_wrapper.addDiskObject_color(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1232 return context_wrapper.addDiskObject_rotation(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list)
1234 return context_wrapper.addDiskObject_basic(self.
context, ndivs, center.to_list(), size.to_list())
1236 def addTubeObject(self, ndivs: int, nodes: List[vec3], radii: List[float],
1237 colors: Optional[List[RGBcolor]] =
None,
1238 texturefile: Optional[str] =
None,
1239 texture_uv: Optional[List[float]] =
None) -> int:
1241 Add a tube as a compound object to the context.
1244 ndivs: Number of radial subdivisions
1245 nodes: List of vec3 positions defining tube segments
1246 radii: List of radii at each node
1247 colors: Optional list of RGB colors for each segment
1248 texturefile: Optional texture file path
1249 texture_uv: Optional UV coordinates for texture mapping
1252 Object ID of the created compound object
1257 raise ValueError(
"Tube requires at least 2 nodes")
1258 if len(radii) != len(nodes):
1259 raise ValueError(
"Number of radii must match number of nodes")
1261 nodes_flat = [coord
for node
in nodes
for coord
in node.to_list()]
1263 if texture_uv
is not None:
1264 if texturefile
is None:
1265 raise ValueError(
"texture_uv requires texturefile")
1266 return context_wrapper.addTubeObject_texture_uv(self.
context, ndivs, nodes_flat, radii, texturefile, texture_uv)
1268 return context_wrapper.addTubeObject_texture(self.
context, ndivs, nodes_flat, radii, texturefile)
1270 if len(colors) != len(nodes):
1271 raise ValueError(
"Number of colors must match number of nodes")
1272 colors_flat = [c
for color
in colors
for c
in color.to_list()]
1273 return context_wrapper.addTubeObject_color(self.
context, ndivs, nodes_flat, radii, colors_flat)
1275 return context_wrapper.addTubeObject_basic(self.
context, ndivs, nodes_flat, radii)
1277 def copyPrimitive(self, UUID: Union[int, List[int]]) -> Union[int, List[int]]:
1279 Copy one or more primitives.
1281 Creates a duplicate of the specified primitive(s) with all associated data.
1282 The copy is placed at the same location as the original.
1285 UUID: Single primitive UUID or list of UUIDs to copy
1288 Single UUID of copied primitive (if UUID is int) or
1289 List of UUIDs of copied primitives (if UUID is list)
1292 >>> context = Context()
1293 >>> original_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1294 >>> # Copy single primitive
1295 >>> copied_uuid = context.copyPrimitive(original_uuid)
1296 >>> # Copy multiple primitives
1297 >>> copied_uuids = context.copyPrimitive([uuid1, uuid2, uuid3])
1301 if isinstance(UUID, int):
1302 return context_wrapper.copyPrimitive(self.
context, UUID)
1303 elif isinstance(UUID, list):
1304 return context_wrapper.copyPrimitives(self.
context, UUID)
1306 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1310 Copy all primitive data from source to destination primitive.
1312 Copies all associated data (primitive data fields) from the source
1313 primitive to the destination primitive. Both primitives must already exist.
1316 sourceUUID: UUID of the source primitive
1317 destinationUUID: UUID of the destination primitive
1320 >>> context = Context()
1321 >>> source_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1322 >>> dest_uuid = context.addPatch(center=vec3(1, 0, 0), size=vec2(1, 1))
1323 >>> context.setPrimitiveData(source_uuid, "temperature", 25.5)
1324 >>> context.copyPrimitiveData(source_uuid, dest_uuid)
1325 >>> # dest_uuid now has temperature data
1329 if not isinstance(sourceUUID, int):
1330 raise ValueError(f
"sourceUUID must be int, got {type(sourceUUID).__name__}")
1331 if not isinstance(destinationUUID, int):
1332 raise ValueError(f
"destinationUUID must be int, got {type(destinationUUID).__name__}")
1334 context_wrapper.copyPrimitiveData(self.
context, sourceUUID, destinationUUID)
1336 def copyObject(self, ObjID: Union[int, List[int]]) -> Union[int, List[int]]:
1338 Copy one or more compound objects.
1340 Creates a duplicate of the specified compound object(s) with all
1341 associated primitives and data. The copy is placed at the same location
1345 ObjID: Single object ID or list of object IDs to copy
1348 Single object ID of copied object (if ObjID is int) or
1349 List of object IDs of copied objects (if ObjID is list)
1352 >>> context = Context()
1353 >>> original_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1354 >>> # Copy single object
1355 >>> copied_obj = context.copyObject(original_obj)
1356 >>> # Copy multiple objects
1357 >>> copied_objs = context.copyObject([obj1, obj2, obj3])
1361 if isinstance(ObjID, int):
1362 return context_wrapper.copyObject(self.
context, ObjID)
1363 elif isinstance(ObjID, list):
1364 return context_wrapper.copyObjects(self.
context, ObjID)
1366 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1368 def copyObjectData(self, source_objID: int, destination_objID: int) ->
None:
1370 Copy all object data from source to destination compound object.
1372 Copies all associated data (object data fields) from the source
1373 compound object to the destination object. Both objects must already exist.
1376 source_objID: Object ID of the source compound object
1377 destination_objID: Object ID of the destination compound object
1380 >>> context = Context()
1381 >>> source_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1382 >>> dest_obj = context.addTile(center=vec3(2, 0, 0), size=vec2(2, 2))
1383 >>> context.setObjectData(source_obj, "material", "wood")
1384 >>> context.copyObjectData(source_obj, dest_obj)
1385 >>> # dest_obj now has material data
1389 if not isinstance(source_objID, int):
1390 raise ValueError(f
"source_objID must be int, got {type(source_objID).__name__}")
1391 if not isinstance(destination_objID, int):
1392 raise ValueError(f
"destination_objID must be int, got {type(destination_objID).__name__}")
1394 context_wrapper.copyObjectData(self.
context, source_objID, destination_objID)
1398 Translate one or more primitives by a shift vector.
1400 Moves the specified primitive(s) by the given shift vector without
1401 changing their orientation or size.
1404 UUID: Single primitive UUID or list of UUIDs to translate
1405 shift: 3D vector representing the translation [x, y, z]
1408 >>> context = Context()
1409 >>> patch_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1410 >>> # Translate single primitive
1411 >>> context.translatePrimitive(patch_uuid, vec3(1, 0, 0)) # Move 1 unit in x
1412 >>> # Translate multiple primitives
1413 >>> context.translatePrimitive([uuid1, uuid2, uuid3], vec3(0, 0, 1)) # Move 1 unit in z
1418 if not isinstance(shift, vec3):
1419 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
1421 if isinstance(UUID, int):
1422 context_wrapper.translatePrimitive(self.
context, UUID, shift.to_list())
1423 elif isinstance(UUID, list):
1424 context_wrapper.translatePrimitives(self.
context, UUID, shift.to_list())
1426 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1428 def translateObject(self, ObjID: Union[int, List[int]], shift: vec3) ->
None:
1430 Translate one or more compound objects by a shift vector.
1432 Moves the specified compound object(s) and all their constituent
1433 primitives by the given shift vector without changing orientation or size.
1436 ObjID: Single object ID or list of object IDs to translate
1437 shift: 3D vector representing the translation [x, y, z]
1440 >>> context = Context()
1441 >>> tile_uuids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1442 >>> obj_id = context.getPrimitiveParentObjectID(tile_uuids[0]) # Get object ID
1443 >>> # Translate single object
1444 >>> context.translateObject(obj_id, vec3(5, 0, 0)) # Move 5 units in x
1445 >>> # Translate multiple objects
1446 >>> context.translateObject([obj1, obj2, obj3], vec3(0, 2, 0)) # Move 2 units in y
1451 if not isinstance(shift, vec3):
1452 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
1454 if isinstance(ObjID, int):
1455 context_wrapper.translateObject(self.
context, ObjID, shift.to_list())
1456 elif isinstance(ObjID, list):
1457 context_wrapper.translateObjects(self.
context, ObjID, shift.to_list())
1459 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1462 axis: Union[str, vec3], origin: Optional[vec3] =
None) ->
None:
1464 Rotate one or more primitives.
1467 UUID: Single UUID or list of UUIDs to rotate
1468 angle: Rotation angle in radians
1469 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
1470 origin: Optional rotation origin point. If None, rotates about primitive center.
1471 If provided with string axis, raises ValueError.
1474 ValueError: If axis is invalid or if origin is provided with string axis
1479 if isinstance(axis, str):
1480 if axis
not in (
'x',
'y',
'z'):
1481 raise ValueError(
"axis must be 'x', 'y', or 'z'")
1482 if origin
is not None:
1483 raise ValueError(
"origin parameter cannot be used with string axis")
1486 if isinstance(UUID, int):
1487 context_wrapper.rotatePrimitive_axisString(self.
context, UUID, angle, axis)
1488 elif isinstance(UUID, list):
1489 context_wrapper.rotatePrimitives_axisString(self.
context, UUID, angle, axis)
1491 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1493 elif isinstance(axis, vec3):
1494 axis_list = axis.to_list()
1497 if all(abs(v) < 1e-10
for v
in axis_list):
1498 raise ValueError(
"axis vector cannot be zero")
1502 if isinstance(UUID, int):
1503 context_wrapper.rotatePrimitive_axisVector(self.
context, UUID, angle, axis_list)
1504 elif isinstance(UUID, list):
1505 context_wrapper.rotatePrimitives_axisVector(self.
context, UUID, angle, axis_list)
1507 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1510 if not isinstance(origin, vec3):
1511 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
1513 origin_list = origin.to_list()
1514 if isinstance(UUID, int):
1515 context_wrapper.rotatePrimitive_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
1516 elif isinstance(UUID, list):
1517 context_wrapper.rotatePrimitives_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
1519 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1521 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
1523 def rotateObject(self, ObjID: Union[int, List[int]], angle: float,
1524 axis: Union[str, vec3], origin: Optional[vec3] =
None,
1525 about_origin: bool =
False) ->
None:
1527 Rotate one or more objects.
1530 ObjID: Single object ID or list of object IDs to rotate
1531 angle: Rotation angle in radians
1532 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
1533 origin: Optional rotation origin point. If None, rotates about object center.
1534 If provided with string axis, raises ValueError.
1535 about_origin: If True, rotate about global origin (0,0,0). Cannot be used with origin parameter.
1538 ValueError: If axis is invalid or if origin and about_origin are both specified
1543 if origin
is not None and about_origin:
1544 raise ValueError(
"Cannot specify both origin and about_origin")
1547 if isinstance(axis, str):
1548 if axis
not in (
'x',
'y',
'z'):
1549 raise ValueError(
"axis must be 'x', 'y', or 'z'")
1550 if origin
is not None:
1551 raise ValueError(
"origin parameter cannot be used with string axis")
1553 raise ValueError(
"about_origin parameter cannot be used with string axis")
1556 if isinstance(ObjID, int):
1557 context_wrapper.rotateObject_axisString(self.
context, ObjID, angle, axis)
1558 elif isinstance(ObjID, list):
1559 context_wrapper.rotateObjects_axisString(self.
context, ObjID, angle, axis)
1561 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1563 elif isinstance(axis, vec3):
1564 axis_list = axis.to_list()
1567 if all(abs(v) < 1e-10
for v
in axis_list):
1568 raise ValueError(
"axis vector cannot be zero")
1572 if isinstance(ObjID, int):
1573 context_wrapper.rotateObjectAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
1574 elif isinstance(ObjID, list):
1575 context_wrapper.rotateObjectsAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
1577 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1578 elif origin
is None:
1580 if isinstance(ObjID, int):
1581 context_wrapper.rotateObject_axisVector(self.
context, ObjID, angle, axis_list)
1582 elif isinstance(ObjID, list):
1583 context_wrapper.rotateObjects_axisVector(self.
context, ObjID, angle, axis_list)
1585 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1588 if not isinstance(origin, vec3):
1589 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
1591 origin_list = origin.to_list()
1592 if isinstance(ObjID, int):
1593 context_wrapper.rotateObject_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
1594 elif isinstance(ObjID, list):
1595 context_wrapper.rotateObjects_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
1597 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1599 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
1601 def scalePrimitive(self, UUID: Union[int, List[int]], scale: vec3, point: Optional[vec3] =
None) ->
None:
1603 Scale one or more primitives.
1606 UUID: Single UUID or list of UUIDs to scale
1607 scale: Scale factors as vec3(x, y, z)
1608 point: Optional point to scale about. If None, scales about primitive center.
1611 ValueError: If scale or point parameters are invalid
1615 if not isinstance(scale, vec3):
1616 raise ValueError(f
"scale must be a vec3, got {type(scale).__name__}")
1618 scale_list = scale.to_list()
1622 if isinstance(UUID, int):
1623 context_wrapper.scalePrimitive(self.
context, UUID, scale_list)
1624 elif isinstance(UUID, list):
1625 context_wrapper.scalePrimitives(self.
context, UUID, scale_list)
1627 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1630 if not isinstance(point, vec3):
1631 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
1633 point_list = point.to_list()
1634 if isinstance(UUID, int):
1635 context_wrapper.scalePrimitiveAboutPoint(self.
context, UUID, scale_list, point_list)
1636 elif isinstance(UUID, list):
1637 context_wrapper.scalePrimitivesAboutPoint(self.
context, UUID, scale_list, point_list)
1639 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1641 def scaleObject(self, ObjID: Union[int, List[int]], scale: vec3,
1642 point: Optional[vec3] =
None, about_center: bool =
False,
1643 about_origin: bool =
False) ->
None:
1645 Scale one or more objects.
1648 ObjID: Single object ID or list of object IDs to scale
1649 scale: Scale factors as vec3(x, y, z)
1650 point: Optional point to scale about
1651 about_center: If True, scale about object center (default behavior)
1652 about_origin: If True, scale about global origin (0,0,0)
1655 ValueError: If parameters are invalid or conflicting options specified
1660 options_count = sum([point
is not None, about_center, about_origin])
1661 if options_count > 1:
1662 raise ValueError(
"Cannot specify multiple scaling options (point, about_center, about_origin)")
1664 if not isinstance(scale, vec3):
1665 raise ValueError(f
"scale must be a vec3, got {type(scale).__name__}")
1667 scale_list = scale.to_list()
1671 if isinstance(ObjID, int):
1672 context_wrapper.scaleObjectAboutOrigin(self.
context, ObjID, scale_list)
1673 elif isinstance(ObjID, list):
1674 context_wrapper.scaleObjectsAboutOrigin(self.
context, ObjID, scale_list)
1676 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1679 if isinstance(ObjID, int):
1680 context_wrapper.scaleObjectAboutCenter(self.
context, ObjID, scale_list)
1681 elif isinstance(ObjID, list):
1682 context_wrapper.scaleObjectsAboutCenter(self.
context, ObjID, scale_list)
1684 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1685 elif point
is not None:
1687 if not isinstance(point, vec3):
1688 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
1690 point_list = point.to_list()
1691 if isinstance(ObjID, int):
1692 context_wrapper.scaleObjectAboutPoint(self.
context, ObjID, scale_list, point_list)
1693 elif isinstance(ObjID, list):
1694 context_wrapper.scaleObjectsAboutPoint(self.
context, ObjID, scale_list, point_list)
1696 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1699 if isinstance(ObjID, int):
1700 context_wrapper.scaleObject(self.
context, ObjID, scale_list)
1701 elif isinstance(ObjID, list):
1702 context_wrapper.scaleObjects(self.
context, ObjID, scale_list)
1704 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1708 Scale the length of a Cone object by scaling the distance between its two nodes.
1711 ObjID: Object ID of the Cone to scale
1712 scale_factor: Factor by which to scale the cone length (e.g., 2.0 doubles length)
1715 ValueError: If ObjID is not an integer or scale_factor is invalid
1716 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
1719 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
1720 method, enforcing better encapsulation.
1723 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
1724 >>> context.scaleConeObjectLength(cone_id, 1.5) # Make cone 50% longer
1726 if not isinstance(ObjID, int):
1727 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
1728 if not isinstance(scale_factor, (int, float)):
1729 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
1730 if scale_factor <= 0:
1731 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
1733 context_wrapper.scaleConeObjectLength(self.
context, ObjID, float(scale_factor))
1737 Scale the girth of a Cone object by scaling the radii at both nodes.
1740 ObjID: Object ID of the Cone to scale
1741 scale_factor: Factor by which to scale the cone girth (e.g., 2.0 doubles girth)
1744 ValueError: If ObjID is not an integer or scale_factor is invalid
1745 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
1748 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
1749 method, enforcing better encapsulation.
1752 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
1753 >>> context.scaleConeObjectGirth(cone_id, 2.0) # Double the cone girth
1755 if not isinstance(ObjID, int):
1756 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
1757 if not isinstance(scale_factor, (int, float)):
1758 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
1759 if scale_factor <= 0:
1760 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
1762 context_wrapper.scaleConeObjectGirth(self.
context, ObjID, float(scale_factor))
1764 def loadPLY(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
1765 rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None,
1766 upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
1768 Load geometry from a PLY (Stanford Polygon) file.
1771 filename: Path to the PLY file to load
1772 origin: Origin point for positioning the geometry (optional)
1773 height: Height scaling factor (optional)
1774 rotation: Rotation to apply to the geometry (optional)
1775 color: Default color for geometry without color data (optional)
1776 upaxis: Up axis orientation ("YUP" or "ZUP")
1777 silent: If True, suppress loading output messages
1780 List of UUIDs for the loaded primitives
1786 if origin
is None and height
is None and rotation
is None and color
is None:
1788 return context_wrapper.loadPLY(self.
context, validated_filename, silent)
1790 elif origin
is not None and height
is not None and rotation
is None and color
is None:
1792 return context_wrapper.loadPLYWithOriginHeight(self.
context, validated_filename, origin.to_list(), height, upaxis, silent)
1794 elif origin
is not None and height
is not None and rotation
is not None and color
is None:
1796 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1797 return context_wrapper.loadPLYWithOriginHeightRotation(self.
context, validated_filename, origin.to_list(), height, rotation_list, upaxis, silent)
1799 elif origin
is not None and height
is not None and rotation
is None and color
is not None:
1801 return context_wrapper.loadPLYWithOriginHeightColor(self.
context, validated_filename, origin.to_list(), height, color.to_list(), upaxis, silent)
1803 elif origin
is not None and height
is not None and rotation
is not None and color
is not None:
1805 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1806 return context_wrapper.loadPLYWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation_list, color.to_list(), upaxis, silent)
1809 raise ValueError(
"Invalid parameter combination. When using transformations, both origin and height are required.")
1811 def loadOBJ(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
1812 scale: Optional[vec3] =
None, rotation: Optional[SphericalCoord] =
None,
1813 color: Optional[RGBcolor] =
None, upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
1815 Load geometry from an OBJ (Wavefront) file.
1818 filename: Path to the OBJ file to load
1819 origin: Origin point for positioning the geometry (optional)
1820 height: Height scaling factor (optional, alternative to scale)
1821 scale: Scale factor for all dimensions (optional, alternative to height)
1822 rotation: Rotation to apply to the geometry (optional)
1823 color: Default color for geometry without color data (optional)
1824 upaxis: Up axis orientation ("YUP" or "ZUP")
1825 silent: If True, suppress loading output messages
1828 List of UUIDs for the loaded primitives
1834 if origin
is None and height
is None and scale
is None and rotation
is None and color
is None:
1836 return context_wrapper.loadOBJ(self.
context, validated_filename, silent)
1838 elif origin
is not None and height
is not None and scale
is None and rotation
is not None and color
is not None:
1840 return context_wrapper.loadOBJWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), silent)
1842 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":
1844 return context_wrapper.loadOBJWithOriginHeightRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), upaxis, silent)
1846 elif origin
is not None and scale
is not None and rotation
is not None and color
is not None:
1848 return context_wrapper.loadOBJWithOriginScaleRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), scale.to_list(), rotation.to_list(), color.to_list(), upaxis, silent)
1851 raise ValueError(
"Invalid parameter combination. For OBJ loading, you must provide either: " +
1852 "1) No parameters (simple load), " +
1853 "2) origin + height + rotation + color, " +
1854 "3) origin + height + rotation + color + upaxis, or " +
1855 "4) origin + scale + rotation + color + upaxis")
1857 def loadXML(self, filename: str, quiet: bool =
False) -> List[int]:
1859 Load geometry from a Helios XML file.
1862 filename: Path to the XML file to load
1863 quiet: If True, suppress loading output messages
1866 List of UUIDs for the loaded primitives
1872 return context_wrapper.loadXML(self.
context, validated_filename, quiet)
1874 def writePLY(self, filename: str, UUIDs: Optional[List[int]] =
None) ->
None:
1876 Write geometry to a PLY (Stanford Polygon) file.
1879 filename: Path to the output PLY file
1880 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
1883 ValueError: If filename is invalid or UUIDs are invalid
1884 PermissionError: If output directory is not writable
1885 FileNotFoundError: If UUIDs do not exist in context
1886 RuntimeError: If Context is in mock mode
1889 >>> context.writePLY("output.ply") # Export all primitives
1890 >>> context.writePLY("subset.ply", [uuid1, uuid2]) # Export specific primitives
1899 context_wrapper.writePLY(self.
context, validated_filename)
1903 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
1910 context_wrapper.writePLYWithUUIDs(self.
context, validated_filename, UUIDs)
1912 def writeOBJ(self, filename: str, UUIDs: Optional[List[int]] =
None,
1913 primitive_data_fields: Optional[List[str]] =
None,
1914 write_normals: bool =
False, silent: bool =
False) ->
None:
1916 Write geometry to an OBJ (Wavefront) file.
1919 filename: Path to the output OBJ file
1920 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
1921 primitive_data_fields: Optional list of primitive data field names to export
1922 write_normals: Whether to include vertex normals in the output
1923 silent: Whether to suppress output messages during export
1926 ValueError: If filename is invalid, UUIDs are invalid, or data fields don't exist
1927 PermissionError: If output directory is not writable
1928 FileNotFoundError: If UUIDs do not exist in context
1929 RuntimeError: If Context is in mock mode
1932 >>> context.writeOBJ("output.obj") # Export all primitives
1933 >>> context.writeOBJ("subset.obj", [uuid1, uuid2]) # Export specific primitives
1934 >>> context.writeOBJ("with_data.obj", [uuid1], ["temperature", "area"]) # Export with data
1943 context_wrapper.writeOBJ(self.
context, validated_filename, write_normals, silent)
1944 elif primitive_data_fields
is None:
1947 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
1953 context_wrapper.writeOBJWithUUIDs(self.
context, validated_filename, UUIDs, write_normals, silent)
1957 raise ValueError(
"UUIDs list cannot be empty when exporting primitive data")
1958 if not primitive_data_fields:
1959 raise ValueError(
"primitive_data_fields list cannot be empty")
1968 context_wrapper.writeOBJWithPrimitiveData(self.
context, validated_filename, UUIDs, primitive_data_fields, write_normals, silent)
1971 UUIDs: Optional[List[int]] =
None,
1972 print_header: bool =
False) ->
None:
1974 Write primitive data to an ASCII text file.
1976 Outputs a space-separated text file where each row corresponds to a primitive
1977 and each column corresponds to a primitive data label.
1980 filename: Path to the output file
1981 column_labels: List of primitive data labels to include as columns.
1982 Use "UUID" to include primitive UUIDs as a column.
1983 The order determines the column order in the output file.
1984 UUIDs: Optional list of primitive UUIDs to include. If None, includes all primitives.
1985 print_header: If True, writes column labels as the first line of the file
1988 ValueError: If filename is invalid, column_labels is empty, or UUIDs list is empty when provided
1989 HeliosFileIOError: If file cannot be written
1990 HeliosRuntimeError: If a column label doesn't exist for any primitive
1993 >>> # Write temperature and area for all primitives
1994 >>> context.writePrimitiveData("output.txt", ["UUID", "temperature", "area"])
1996 >>> # Write with header row
1997 >>> context.writePrimitiveData("output.txt", ["UUID", "radiation_flux"], print_header=True)
1999 >>> # Write only for selected primitives
2000 >>> context.writePrimitiveData("subset.txt", ["temperature"], UUIDs=[uuid1, uuid2])
2005 if not column_labels:
2006 raise ValueError(
"column_labels list cannot be empty")
2013 context_wrapper.writePrimitiveData(self.
context, validated_filename, column_labels, print_header)
2017 raise ValueError(
"UUIDs list cannot be empty when provided. Use UUIDs=None to include all primitives")
2023 context_wrapper.writePrimitiveDataWithUUIDs(self.
context, validated_filename, column_labels, UUIDs, print_header)
2026 colors: Optional[np.ndarray] =
None) -> List[int]:
2028 Add triangles from NumPy arrays (compatible with trimesh, Open3D format).
2031 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2032 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2033 colors: Optional NumPy array of shape (N, 3) or (M, 3) containing RGB colors as float32/float64
2034 If shape (N, 3): per-vertex colors
2035 If shape (M, 3): per-triangle colors
2038 List of UUIDs for the added triangles
2041 ValueError: If array dimensions are invalid
2044 if vertices.ndim != 2
or vertices.shape[1] != 3:
2045 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2046 if faces.ndim != 2
or faces.shape[1] != 3:
2047 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2050 max_vertex_index = np.max(faces)
2051 if max_vertex_index >= vertices.shape[0]:
2052 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2055 per_vertex_colors =
False
2056 per_triangle_colors =
False
2057 if colors
is not None:
2058 if colors.ndim != 2
or colors.shape[1] != 3:
2059 raise ValueError(f
"Colors array must have shape (N, 3) or (M, 3), got {colors.shape}")
2060 if colors.shape[0] == vertices.shape[0]:
2061 per_vertex_colors =
True
2062 elif colors.shape[0] == faces.shape[0]:
2063 per_triangle_colors =
True
2065 raise ValueError(f
"Colors array shape {colors.shape} doesn't match vertices ({vertices.shape[0]},) or faces ({faces.shape[0]},)")
2068 vertices_float = vertices.astype(np.float32)
2069 faces_int = faces.astype(np.int32)
2070 if colors
is not None:
2071 colors_float = colors.astype(np.float32)
2075 for i
in range(faces.shape[0]):
2077 v0_idx, v1_idx, v2_idx = faces_int[i]
2080 vertex0 = vertices_float[v0_idx].tolist()
2081 vertex1 = vertices_float[v1_idx].tolist()
2082 vertex2 = vertices_float[v2_idx].tolist()
2087 uuid = context_wrapper.addTriangle(self.
context, vertex0, vertex1, vertex2)
2088 elif per_triangle_colors:
2090 color = colors_float[i].tolist()
2091 uuid = context_wrapper.addTriangleWithColor(self.
context, vertex0, vertex1, vertex2, color)
2092 elif per_vertex_colors:
2094 color = np.mean([colors_float[v0_idx], colors_float[v1_idx], colors_float[v2_idx]], axis=0).tolist()
2095 uuid = context_wrapper.addTriangleWithColor(self.
context, vertex0, vertex1, vertex2, color)
2097 triangle_uuids.append(uuid)
2099 return triangle_uuids
2102 uv_coords: np.ndarray, texture_files: Union[str, List[str]],
2103 material_ids: Optional[np.ndarray] =
None) -> List[int]:
2105 Add textured triangles from NumPy arrays with support for multiple textures.
2107 This method supports both single-texture and multi-texture workflows:
2108 - Single texture: Pass a single texture file string, all faces use the same texture
2109 - Multiple textures: Pass a list of texture files and material_ids array specifying which texture each face uses
2112 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2113 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2114 uv_coords: NumPy array of shape (N, 2) containing UV texture coordinates as float32/float64
2115 texture_files: Single texture file path (str) or list of texture file paths (List[str])
2116 material_ids: Optional NumPy array of shape (M,) containing material ID for each face.
2117 If None and texture_files is a list, all faces use texture 0.
2118 If None and texture_files is a string, this parameter is ignored.
2121 List of UUIDs for the added textured triangles
2124 ValueError: If array dimensions are invalid or material IDs are out of range
2127 # Single texture usage (backward compatible)
2128 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, "texture.png")
2130 # Multi-texture usage (Open3D style)
2131 >>> texture_files = ["wood.png", "metal.png", "glass.png"]
2132 >>> material_ids = np.array([0, 0, 1, 1, 2, 2]) # 6 faces using different textures
2133 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, texture_files, material_ids)
2138 if vertices.ndim != 2
or vertices.shape[1] != 3:
2139 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2140 if faces.ndim != 2
or faces.shape[1] != 3:
2141 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2142 if uv_coords.ndim != 2
or uv_coords.shape[1] != 2:
2143 raise ValueError(f
"UV coordinates array must have shape (N, 2), got {uv_coords.shape}")
2146 if uv_coords.shape[0] != vertices.shape[0]:
2147 raise ValueError(f
"UV coordinates count ({uv_coords.shape[0]}) must match vertices count ({vertices.shape[0]})")
2150 max_vertex_index = np.max(faces)
2151 if max_vertex_index >= vertices.shape[0]:
2152 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2155 if isinstance(texture_files, str):
2157 texture_file_list = [texture_files]
2158 if material_ids
is None:
2159 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2162 if not np.all(material_ids == 0):
2163 raise ValueError(
"When using single texture file, all material IDs must be 0")
2166 texture_file_list = list(texture_files)
2167 if len(texture_file_list) == 0:
2168 raise ValueError(
"Texture files list cannot be empty")
2170 if material_ids
is None:
2172 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2175 if material_ids.ndim != 1
or material_ids.shape[0] != faces.shape[0]:
2176 raise ValueError(f
"Material IDs must have shape ({faces.shape[0]},), got {material_ids.shape}")
2179 max_material_id = np.max(material_ids)
2180 if max_material_id >= len(texture_file_list):
2181 raise ValueError(f
"Material ID {max_material_id} exceeds texture count {len(texture_file_list)}")
2184 for i, texture_file
in enumerate(texture_file_list):
2187 except (FileNotFoundError, ValueError)
as e:
2188 raise ValueError(f
"Texture file {i} ({texture_file}): {e}")
2191 if 'addTrianglesFromArraysMultiTextured' in context_wrapper._AVAILABLE_TRIANGLE_FUNCTIONS:
2192 return context_wrapper.addTrianglesFromArraysMultiTextured(
2193 self.
context, vertices, faces, uv_coords, texture_file_list, material_ids
2197 from .wrappers.DataTypes
import vec3, vec2
2199 vertices_float = vertices.astype(np.float32)
2200 faces_int = faces.astype(np.int32)
2201 uv_coords_float = uv_coords.astype(np.float32)
2204 for i
in range(faces.shape[0]):
2206 v0_idx, v1_idx, v2_idx = faces_int[i]
2209 vertex0 =
vec3(vertices_float[v0_idx][0], vertices_float[v0_idx][1], vertices_float[v0_idx][2])
2210 vertex1 =
vec3(vertices_float[v1_idx][0], vertices_float[v1_idx][1], vertices_float[v1_idx][2])
2211 vertex2 =
vec3(vertices_float[v2_idx][0], vertices_float[v2_idx][1], vertices_float[v2_idx][2])
2214 uv0 =
vec2(uv_coords_float[v0_idx][0], uv_coords_float[v0_idx][1])
2215 uv1 =
vec2(uv_coords_float[v1_idx][0], uv_coords_float[v1_idx][1])
2216 uv2 =
vec2(uv_coords_float[v2_idx][0], uv_coords_float[v2_idx][1])
2219 material_id = material_ids[i]
2220 texture_file = texture_file_list[material_id]
2224 triangle_uuids.append(uuid)
2226 return triangle_uuids
2234 Set primitive data as signed 32-bit integer for one or multiple primitives.
2237 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2238 label: String key for the data
2239 value: Signed integer value (broadcast to all UUIDs if list provided)
2241 if isinstance(uuids_or_uuid, (list, tuple)):
2242 context_wrapper.setBroadcastPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2244 context_wrapper.setPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2248 Set primitive data as unsigned 32-bit integer for one or multiple primitives.
2250 Critical for properties like 'twosided_flag' which must be uint in C++.
2253 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2254 label: String key for the data
2255 value: Unsigned integer value (broadcast to all UUIDs if list provided)
2257 if isinstance(uuids_or_uuid, (list, tuple)):
2258 context_wrapper.setBroadcastPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2260 context_wrapper.setPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2264 Set primitive data as 32-bit float for one or multiple primitives.
2267 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2268 label: String key for the data
2269 value: Float value (broadcast to all UUIDs if list provided)
2271 if isinstance(uuids_or_uuid, (list, tuple)):
2272 context_wrapper.setBroadcastPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2274 context_wrapper.setPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2278 Set primitive data as 64-bit double for one or multiple primitives.
2281 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2282 label: String key for the data
2283 value: Double value (broadcast to all UUIDs if list provided)
2285 if isinstance(uuids_or_uuid, (list, tuple)):
2286 context_wrapper.setBroadcastPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2288 context_wrapper.setPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2292 Set primitive data as string for one or multiple primitives.
2295 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2296 label: String key for the data
2297 value: String value (broadcast to all UUIDs if list provided)
2299 if isinstance(uuids_or_uuid, (list, tuple)):
2300 context_wrapper.setBroadcastPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2302 context_wrapper.setPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2306 Set primitive data as vec2 for one or multiple primitives.
2309 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2310 label: String key for the data
2311 x_or_vec: Either x component (float) or vec2 object
2312 y: Y component (if x_or_vec is float)
2314 if hasattr(x_or_vec,
'x'):
2315 x, y = x_or_vec.x, x_or_vec.y
2318 if isinstance(uuids_or_uuid, (list, tuple)):
2319 context_wrapper.setBroadcastPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2321 context_wrapper.setPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2323 def setPrimitiveDataVec3(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
2325 Set primitive data as vec3 for one or multiple primitives.
2328 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2329 label: String key for the data
2330 x_or_vec: Either x component (float) or vec3 object
2331 y: Y component (if x_or_vec is float)
2332 z: Z component (if x_or_vec is float)
2334 if hasattr(x_or_vec,
'x'):
2335 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2338 if isinstance(uuids_or_uuid, (list, tuple)):
2339 context_wrapper.setBroadcastPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2341 context_wrapper.setPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2343 def setPrimitiveDataVec4(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
2345 Set primitive data as vec4 for one or multiple primitives.
2348 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2349 label: String key for the data
2350 x_or_vec: Either x component (float) or vec4 object
2351 y: Y component (if x_or_vec is float)
2352 z: Z component (if x_or_vec is float)
2353 w: W component (if x_or_vec is float)
2355 if hasattr(x_or_vec,
'x'):
2356 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2359 if isinstance(uuids_or_uuid, (list, tuple)):
2360 context_wrapper.setBroadcastPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
2362 context_wrapper.setPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
2366 Set primitive data as int2 for one or multiple primitives.
2369 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2370 label: String key for the data
2371 x_or_vec: Either x component (int) or int2 object
2372 y: Y component (if x_or_vec is int)
2374 if hasattr(x_or_vec,
'x'):
2375 x, y = x_or_vec.x, x_or_vec.y
2378 if isinstance(uuids_or_uuid, (list, tuple)):
2379 context_wrapper.setBroadcastPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
2381 context_wrapper.setPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
2383 def setPrimitiveDataInt3(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
2385 Set primitive data as int3 for one or multiple primitives.
2388 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2389 label: String key for the data
2390 x_or_vec: Either x component (int) or int3 object
2391 y: Y component (if x_or_vec is int)
2392 z: Z component (if x_or_vec is int)
2394 if hasattr(x_or_vec,
'x'):
2395 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2398 if isinstance(uuids_or_uuid, (list, tuple)):
2399 context_wrapper.setBroadcastPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
2401 context_wrapper.setPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
2403 def setPrimitiveDataInt4(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
2405 Set primitive data as int4 for one or multiple primitives.
2408 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2409 label: String key for the data
2410 x_or_vec: Either x component (int) or int4 object
2411 y: Y component (if x_or_vec is int)
2412 z: Z component (if x_or_vec is int)
2413 w: W component (if x_or_vec is int)
2415 if hasattr(x_or_vec,
'x'):
2416 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2419 if isinstance(uuids_or_uuid, (list, tuple)):
2420 context_wrapper.setBroadcastPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
2422 context_wrapper.setPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
2426 Get primitive data for a specific primitive. If data_type is provided, it works like before.
2427 If data_type is None, it automatically detects the type and returns the appropriate value.
2430 uuid: UUID of the primitive
2431 label: String key for the data
2432 data_type: Optional. Python type to retrieve (int, uint, float, double, bool, str, vec2, vec3, vec4, int2, int3, int4, etc.)
2433 If None, auto-detects the type using C++ getPrimitiveDataType().
2436 The stored value of the specified or auto-detected type
2439 if data_type
is None:
2440 return context_wrapper.getPrimitiveDataAuto(self.
context, uuid, label)
2443 if data_type == int:
2444 return context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
2445 elif data_type == float:
2446 return context_wrapper.getPrimitiveDataFloat(self.
context, uuid, label)
2447 elif data_type == bool:
2449 int_value = context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
2450 return int_value != 0
2451 elif data_type == str:
2452 return context_wrapper.getPrimitiveDataString(self.
context, uuid, label)
2455 elif data_type == vec2:
2456 coords = context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
2457 return vec2(coords[0], coords[1])
2458 elif data_type == vec3:
2459 coords = context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
2460 return vec3(coords[0], coords[1], coords[2])
2461 elif data_type == vec4:
2462 coords = context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
2463 return vec4(coords[0], coords[1], coords[2], coords[3])
2464 elif data_type == int2:
2465 coords = context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
2466 return int2(coords[0], coords[1])
2467 elif data_type == int3:
2468 coords = context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
2469 return int3(coords[0], coords[1], coords[2])
2470 elif data_type == int4:
2471 coords = context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
2472 return int4(coords[0], coords[1], coords[2], coords[3])
2475 elif data_type ==
"uint":
2476 return context_wrapper.getPrimitiveDataUInt(self.
context, uuid, label)
2477 elif data_type ==
"double":
2478 return context_wrapper.getPrimitiveDataDouble(self.
context, uuid, label)
2481 elif data_type == list:
2483 return context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
2484 elif data_type ==
"list_vec2":
2485 return context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
2486 elif data_type ==
"list_vec4":
2487 return context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
2488 elif data_type ==
"list_int2":
2489 return context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
2490 elif data_type ==
"list_int3":
2491 return context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
2492 elif data_type ==
"list_int4":
2493 return context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
2496 raise ValueError(f
"Unsupported primitive data type: {data_type}. "
2497 f
"Supported types: int, float, bool, str, vec2, vec3, vec4, int2, int3, int4, "
2498 f
"'uint', 'double', list (for vec3), 'list_vec2', 'list_vec4', 'list_int2', 'list_int3', 'list_int4'")
2502 Check if primitive data exists for a specific primitive and label.
2505 uuid: UUID of the primitive
2506 label: String key for the data
2509 True if the data exists, False otherwise
2511 return context_wrapper.doesPrimitiveDataExistWrapper(self.
context, uuid, label)
2515 Convenience method to get float primitive data.
2518 uuid: UUID of the primitive
2519 label: String key for the data
2522 Float value stored for the primitive
2528 Get the Helios data type of primitive data.
2531 uuid: UUID of the primitive
2532 label: String key for the data
2535 HeliosDataType enum value as integer
2537 return context_wrapper.getPrimitiveDataTypeWrapper(self.
context, uuid, label)
2541 Get the size/length of primitive data (for vector data).
2544 uuid: UUID of the primitive
2545 label: String key for the data
2548 Size of data array, or 1 for scalar data
2550 return context_wrapper.getPrimitiveDataSizeWrapper(self.
context, uuid, label)
2554 Get primitive data values for multiple primitives as a NumPy array.
2556 This method retrieves primitive data for a list of UUIDs and returns the values
2557 as a NumPy array. The output array has the same length as the input UUID list,
2558 with each index corresponding to the primitive data value for that UUID.
2561 uuids: List of primitive UUIDs to get data for
2562 label: String key for the primitive data to retrieve
2565 NumPy array of primitive data values corresponding to each UUID.
2566 The array type depends on the data type:
2567 - int data: int32 array
2568 - uint data: uint32 array
2569 - float data: float32 array
2570 - double data: float64 array
2571 - vector data: float32 array with shape (N, vector_size)
2572 - string data: object array of strings
2575 ValueError: If UUID list is empty or UUIDs don't exist
2576 RuntimeError: If context is in mock mode or data doesn't exist for some UUIDs
2581 raise ValueError(
"UUID list cannot be empty")
2590 raise ValueError(f
"Primitive data '{label}' does not exist for UUID {uuid}")
2593 first_uuid = uuids[0]
2599 result = np.empty(len(uuids), dtype=np.int32)
2600 for i, uuid
in enumerate(uuids):
2603 elif data_type == 1:
2604 result = np.empty(len(uuids), dtype=np.uint32)
2605 for i, uuid
in enumerate(uuids):
2608 elif data_type == 2:
2609 result = np.empty(len(uuids), dtype=np.float32)
2610 for i, uuid
in enumerate(uuids):
2613 elif data_type == 3:
2614 result = np.empty(len(uuids), dtype=np.float64)
2615 for i, uuid
in enumerate(uuids):
2618 elif data_type == 4:
2619 result = np.empty((len(uuids), 2), dtype=np.float32)
2620 for i, uuid
in enumerate(uuids):
2622 result[i] = [vec_data.x, vec_data.y]
2624 elif data_type == 5:
2625 result = np.empty((len(uuids), 3), dtype=np.float32)
2626 for i, uuid
in enumerate(uuids):
2628 result[i] = [vec_data.x, vec_data.y, vec_data.z]
2630 elif data_type == 6:
2631 result = np.empty((len(uuids), 4), dtype=np.float32)
2632 for i, uuid
in enumerate(uuids):
2634 result[i] = [vec_data.x, vec_data.y, vec_data.z, vec_data.w]
2636 elif data_type == 7:
2637 result = np.empty((len(uuids), 2), dtype=np.int32)
2638 for i, uuid
in enumerate(uuids):
2640 result[i] = [int_data.x, int_data.y]
2642 elif data_type == 8:
2643 result = np.empty((len(uuids), 3), dtype=np.int32)
2644 for i, uuid
in enumerate(uuids):
2646 result[i] = [int_data.x, int_data.y, int_data.z]
2648 elif data_type == 9:
2649 result = np.empty((len(uuids), 4), dtype=np.int32)
2650 for i, uuid
in enumerate(uuids):
2652 result[i] = [int_data.x, int_data.y, int_data.z, int_data.w]
2654 elif data_type == 10:
2655 result = np.empty(len(uuids), dtype=object)
2656 for i, uuid
in enumerate(uuids):
2660 raise ValueError(f
"Unsupported primitive data type: {data_type}")
2666 colormap: str =
"hot", ncolors: int = 10,
2667 max_val: Optional[float] =
None, min_val: Optional[float] =
None):
2669 Color primitives based on primitive data values using pseudocolor mapping.
2671 This method applies a pseudocolor mapping to primitives based on the values
2672 of specified primitive data. The primitive colors are updated to reflect the
2673 data values using a color map.
2676 uuids: List of primitive UUIDs to color
2677 primitive_data: Name of primitive data to use for coloring (e.g., "radiation_flux_SW")
2678 colormap: Color map name - options include "hot", "cool", "parula", "rainbow", "gray", "lava"
2679 ncolors: Number of discrete colors in color map (default: 10)
2680 max_val: Maximum value for color scale (auto-determined if None)
2681 min_val: Minimum value for color scale (auto-determined if None)
2683 if max_val
is not None and min_val
is not None:
2684 context_wrapper.colorPrimitiveByDataPseudocolorWithRange(
2685 self.
context, uuids, primitive_data, colormap, ncolors, max_val, min_val)
2687 context_wrapper.colorPrimitiveByDataPseudocolor(
2688 self.
context, uuids, primitive_data, colormap, ncolors)
2691 def setTime(self, hour: int, minute: int = 0, second: int = 0):
2693 Set the simulation time.
2697 minute: Minute (0-59), defaults to 0
2698 second: Second (0-59), defaults to 0
2701 ValueError: If time values are out of range
2702 NotImplementedError: If time/date functions not available in current library build
2705 >>> context.setTime(14, 30) # Set to 2:30 PM
2706 >>> context.setTime(9, 15, 30) # Set to 9:15:30 AM
2708 context_wrapper.setTime(self.
context, hour, minute, second)
2710 def setDate(self, year: int, month: int, day: int):
2712 Set the simulation date.
2715 year: Year (1900-3000)
2720 ValueError: If date values are out of range
2721 NotImplementedError: If time/date functions not available in current library build
2724 >>> context.setDate(2023, 6, 21) # Set to June 21, 2023
2726 context_wrapper.setDate(self.
context, year, month, day)
2730 Set the simulation date using Julian day number.
2733 julian_day: Julian day (1-366)
2734 year: Year (1900-3000)
2737 ValueError: If values are out of range
2738 NotImplementedError: If time/date functions not available in current library build
2741 >>> context.setDateJulian(172, 2023) # Set to day 172 of 2023 (June 21)
2743 context_wrapper.setDateJulian(self.
context, julian_day, year)
2747 Get the current simulation time.
2750 Tuple of (hour, minute, second) as integers
2753 NotImplementedError: If time/date functions not available in current library build
2756 >>> hour, minute, second = context.getTime()
2757 >>> print(f"Current time: {hour:02d}:{minute:02d}:{second:02d}")
2759 return context_wrapper.getTime(self.
context)
2763 Get the current simulation date.
2766 Tuple of (year, month, day) as integers
2769 NotImplementedError: If time/date functions not available in current library build
2772 >>> year, month, day = context.getDate()
2773 >>> print(f"Current date: {year}-{month:02d}-{day:02d}")
2775 return context_wrapper.getDate(self.
context)
2781 def deletePrimitive(self, uuids_or_uuid: Union[int, List[int]]) ->
None:
2783 Delete one or more primitives from the context.
2785 This removes the primitive(s) entirely from the context. If a primitive
2786 belongs to a compound object, it will be removed from that object. If the
2787 object becomes empty after removal, it is automatically deleted.
2790 uuids_or_uuid: Single UUID (int) or list of UUIDs to delete
2793 RuntimeError: If any UUID doesn't exist in the context
2794 ValueError: If UUID is invalid (negative)
2795 NotImplementedError: If delete functions not available in current library build
2798 >>> context = Context()
2799 >>> patch_id = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
2800 >>> context.deletePrimitive(patch_id) # Single deletion
2802 >>> # Multiple deletion
2803 >>> ids = [context.addPatch() for _ in range(5)]
2804 >>> context.deletePrimitive(ids) # Delete all at once
2808 if isinstance(uuids_or_uuid, (list, tuple)):
2809 for uuid
in uuids_or_uuid:
2811 raise ValueError(f
"UUID must be non-negative, got {uuid}")
2812 context_wrapper.deletePrimitives(self.
context, list(uuids_or_uuid))
2814 if uuids_or_uuid < 0:
2815 raise ValueError(f
"UUID must be non-negative, got {uuids_or_uuid}")
2816 context_wrapper.deletePrimitive(self.
context, uuids_or_uuid)
2818 def deleteObject(self, objIDs_or_objID: Union[int, List[int]]) ->
None:
2820 Delete one or more compound objects from the context.
2822 This removes the compound object(s) AND all their child primitives.
2823 Use this when you want to delete an entire object hierarchy at once.
2826 objIDs_or_objID: Single object ID (int) or list of object IDs to delete
2829 RuntimeError: If any object ID doesn't exist in the context
2830 ValueError: If object ID is invalid (negative)
2831 NotImplementedError: If delete functions not available in current library build
2834 >>> context = Context()
2835 >>> # Create a compound object (e.g., a tile with multiple patches)
2836 >>> patch_ids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2),
2837 ... tile_divisions=int2(2, 2))
2838 >>> obj_id = context.getPrimitiveParentObjectID(patch_ids[0])
2839 >>> context.deleteObject(obj_id) # Deletes tile and all its patches
2843 if isinstance(objIDs_or_objID, (list, tuple)):
2844 for objID
in objIDs_or_objID:
2846 raise ValueError(f
"Object ID must be non-negative, got {objID}")
2847 context_wrapper.deleteObjects(self.
context, list(objIDs_or_objID))
2849 if objIDs_or_objID < 0:
2850 raise ValueError(f
"Object ID must be non-negative, got {objIDs_or_objID}")
2851 context_wrapper.deleteObject(self.
context, objIDs_or_objID)
2856 Get list of available plugins for this PyHelios instance.
2859 List of available plugin names
2865 Check if a specific plugin is available.
2868 plugin_name: Name of the plugin to check
2871 True if plugin is available, False otherwise
2877 Get detailed information about available plugin capabilities.
2880 Dictionary mapping plugin names to capability information
2885 """Print detailed plugin status information."""
2890 Get list of requested plugins that are not available.
2893 requested_plugins: List of plugin names to check
2896 List of missing plugin names
2906 Create a new material for sharing visual properties across primitives.
2908 Materials enable efficient memory usage by allowing multiple primitives to
2909 share rendering properties. Changes to a material affect all primitives using it.
2912 material_label: Unique label for the material
2915 RuntimeError: If material label already exists
2918 >>> context.addMaterial("wood_oak")
2919 >>> context.setMaterialColor("wood_oak", (0.6, 0.4, 0.2, 1.0))
2920 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
2922 context_wrapper.addMaterial(self.
context, material_label)
2925 """Check if a material with the given label exists."""
2926 return context_wrapper.doesMaterialExist(self.
context, material_label)
2929 """Get list of all material labels in the context."""
2930 return context_wrapper.listMaterials(self.
context)
2934 Delete a material from the context.
2936 Primitives using this material will be reassigned to the default material.
2939 material_label: Label of the material to delete
2942 RuntimeError: If material doesn't exist
2944 context_wrapper.deleteMaterial(self.
context, material_label)
2948 Get the RGBA color of a material.
2951 material_label: Label of the material
2957 RuntimeError: If material doesn't exist
2959 from .wrappers.DataTypes
import RGBAcolor
2960 color_list = context_wrapper.getMaterialColor(self.
context, material_label)
2961 return RGBAcolor(color_list[0], color_list[1], color_list[2], color_list[3])
2965 Set the RGBA color of a material.
2967 This affects all primitives that reference this material.
2970 material_label: Label of the material
2971 color: RGBAcolor object or tuple/list of (r, g, b, a) values
2974 RuntimeError: If material doesn't exist
2977 >>> from pyhelios.types import RGBAcolor
2978 >>> context.setMaterialColor("wood", RGBAcolor(0.6, 0.4, 0.2, 1.0))
2979 >>> context.setMaterialColor("wood", (0.6, 0.4, 0.2, 1.0))
2981 if hasattr(color,
'r'):
2982 r, g, b, a = color.r, color.g, color.b, color.a
2984 r, g, b, a = color[0], color[1], color[2], color[3]
2985 context_wrapper.setMaterialColor(self.
context, material_label, r, g, b, a)
2989 Get the texture file path for a material.
2992 material_label: Label of the material
2995 Texture file path, or empty string if no texture
2998 RuntimeError: If material doesn't exist
3000 return context_wrapper.getMaterialTexture(self.
context, material_label)
3004 Set the texture file for a material.
3006 This affects all primitives that reference this material.
3009 material_label: Label of the material
3010 texture_file: Path to texture image file
3013 RuntimeError: If material doesn't exist or texture file not found
3015 context_wrapper.setMaterialTexture(self.
context, material_label, texture_file)
3018 """Check if material texture color is overridden by material color."""
3019 return context_wrapper.isMaterialTextureColorOverridden(self.
context, material_label)
3022 """Set whether material color overrides texture color."""
3023 context_wrapper.setMaterialTextureColorOverride(self.
context, material_label, override)
3026 """Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3027 return context_wrapper.getMaterialTwosidedFlag(self.
context, material_label)
3030 """Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3031 context_wrapper.setMaterialTwosidedFlag(self.
context, material_label, twosided_flag)
3035 Assign a material to primitive(s).
3038 uuid: Single UUID (int) or list of UUIDs (List[int])
3039 material_label: Label of the material to assign
3042 RuntimeError: If primitive or material doesn't exist
3045 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3046 >>> context.assignMaterialToPrimitive([uuid1, uuid2, uuid3], "wood_oak")
3048 if isinstance(uuid, (list, tuple)):
3049 context_wrapper.assignMaterialToPrimitives(self.
context, uuid, material_label)
3051 context_wrapper.assignMaterialToPrimitive(self.
context, uuid, material_label)
3055 Assign a material to all primitives in compound object(s).
3058 objID: Single object ID (int) or list of object IDs (List[int])
3059 material_label: Label of the material to assign
3062 RuntimeError: If object or material doesn't exist
3065 >>> tree_id = wpt.buildTree(WPTType.LEMON)
3066 >>> context.assignMaterialToObject(tree_id, "tree_bark")
3067 >>> context.assignMaterialToObject([id1, id2], "grass")
3069 if isinstance(objID, (list, tuple)):
3070 context_wrapper.assignMaterialToObjects(self.
context, objID, material_label)
3072 context_wrapper.assignMaterialToObject(self.
context, objID, material_label)
3076 Get the material label assigned to a primitive.
3079 uuid: UUID of the primitive
3082 Material label, or empty string if no material assigned
3085 RuntimeError: If primitive doesn't exist
3087 return context_wrapper.getPrimitiveMaterialLabel(self.
context, uuid)
3091 Get two-sided rendering flag for a primitive.
3093 Checks material first, then primitive data if no material assigned.
3096 uuid: UUID of the primitive
3097 default_value: Default value if no material/data (default 1 = two-sided)
3100 Two-sided flag (0 = one-sided, 1 = two-sided)
3102 return context_wrapper.getPrimitiveTwosidedFlag(self.
context, uuid, default_value)
3106 Get all primitive UUIDs that use a specific material.
3109 material_label: Label of the material
3112 List of primitive UUIDs using the material
3115 RuntimeError: If material doesn't exist
3117 return context_wrapper.getPrimitivesUsingMaterial(self.
context, material_label)
Central simulation environment for PyHelios that manages 3D primitives and their data.
int addTileObject(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), SphericalCoord rotation=SphericalCoord(1, 0, 0), int2 subdiv=int2(1, 1), Optional[RGBcolor] color=None, Optional[str] texturefile=None, Optional[int2] texture_repeat=None)
Add a tiled patch (subdivided patch) as a compound object to the context.
None scaleConeObjectGirth(self, int ObjID, float scale_factor)
Scale the girth of a Cone object by scaling the radii at both nodes.
str getMaterialTexture(self, str material_label)
Get the texture file path for a material.
getMaterialColor(self, str material_label)
Get the RGBA color of a material.
Union[int, List[int]] copyObject(self, Union[int, List[int]] ObjID)
Copy one or more compound objects.
int getMaterialTwosidedFlag(self, str material_label)
Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided).
None deletePrimitive(self, Union[int, List[int]] uuids_or_uuid)
Delete one or more primitives from the context.
_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.
int getPrimitiveTwosidedFlag(self, int uuid, int default_value=1)
Get two-sided rendering flag for a primitive.
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)
None setPrimitiveDataInt4(self, uuids_or_uuid, str label, x_or_vec, int y=None, int z=None, int w=None)
Set primitive data as int4 for one or multiple primitives.
RGBcolor getPrimitiveColor(self, int uuid)
int addSphereObject(self, vec3 center=vec3(0, 0, 0), Union[float, vec3] radius=1.0, int ndivs=20, Optional[RGBcolor] color=None, Optional[str] texturefile=None)
Add a spherical or ellipsoidal compound object to the context.
bool doesPrimitiveDataExist(self, int uuid, str label)
Check if primitive data exists for a specific primitive and label.
List[str] listMaterials(self)
Get list of all material labels in the context.
float getPrimitiveArea(self, int uuid)
None setPrimitiveDataInt(self, uuids_or_uuid, str label, int value)
Set primitive data as signed 32-bit integer for one or multiple primitives.
None setPrimitiveDataDouble(self, uuids_or_uuid, str label, float value)
Set primitive data as 64-bit double for one or multiple primitives.
None setPrimitiveDataVec2(self, uuids_or_uuid, str label, x_or_vec, float y=None)
Set primitive data as vec2 for one or multiple primitives.
int getPrimitiveCount(self)
int addConeObject(self, vec3 node0, vec3 node1, float radius0, float radius1, int ndivs=20, Optional[RGBcolor] color=None, Optional[str] texturefile=None)
Add a cone/cylinder/frustum as a compound object to the context.
List[PrimitiveInfo] getAllPrimitiveInfo(self)
Get physical properties and geometry information for all primitives in the context.
None copyObjectData(self, int source_objID, int destination_objID)
Copy all object data from source to destination compound object.
None setPrimitiveDataInt3(self, uuids_or_uuid, str label, x_or_vec, int y=None, int z=None)
Set primitive data as int3 for one or multiple primitives.
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.
None setPrimitiveDataInt2(self, uuids_or_uuid, str label, x_or_vec, int y=None)
Set primitive data as int2 for one or multiple primitives.
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)
None copyPrimitiveData(self, int sourceUUID, int destinationUUID)
Copy all primitive data from source to destination primitive.
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.
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
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.
int addPatch(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), Optional[SphericalCoord] rotation=None, Optional[RGBcolor] color=None)
addMaterial(self, str material_label)
Create a new material for sharing visual properties across primitives.
bool isMaterialTextureColorOverridden(self, str material_label)
Check if material texture color is overridden by material color.
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.
None setPrimitiveDataVec4(self, uuids_or_uuid, str label, x_or_vec, float y=None, float z=None, float w=None)
Set primitive data as vec4 for one or multiple primitives.
None setPrimitiveDataFloat(self, uuids_or_uuid, str label, float value)
Set primitive data as 32-bit float for one or multiple primitives.
int addDiskObject(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), Union[int, int2] ndivs=20, Optional[SphericalCoord] rotation=None, Optional[Union[RGBcolor, RGBAcolor]] color=None, Optional[str] texturefile=None)
Add a disk as a compound object to the context.
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.
List[int] addCone(self, vec3 node0, vec3 node1, float radius0, float radius1, int ndivs=20, Optional[RGBcolor] color=None)
Add a cone (or cylinder/frustum) to the context.
str _validate_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize file path for security.
List[int] addDisk(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), Union[int, int2] ndivs=20, Optional[SphericalCoord] rotation=None, Optional[Union[RGBcolor, RGBAcolor]] color=None)
Add a disk (circular or elliptical surface) to the context.
assignMaterialToPrimitive(self, uuid, str material_label)
Assign a material to primitive(s).
None scaleObject(self, Union[int, List[int]] ObjID, vec3 scale, Optional[vec3] point=None, bool about_center=False, bool about_origin=False)
Scale one or more objects.
seedRandomGenerator(self, int seed)
Seed the random number generator for reproducible stochastic results.
None deleteObject(self, Union[int, List[int]] objIDs_or_objID)
Delete one or more compound objects from the context.
None translatePrimitive(self, Union[int, List[int]] UUID, vec3 shift)
Translate one or more primitives by a shift vector.
Union[int, List[int]] copyPrimitive(self, Union[int, List[int]] UUID)
Copy one or more primitives.
setMaterialTwosidedFlag(self, str material_label, int twosided_flag)
Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided).
None setPrimitiveDataString(self, uuids_or_uuid, str label, str value)
Set primitive data as string for one or multiple primitives.
None rotateObject(self, Union[int, List[int]] ObjID, float angle, Union[str, vec3] axis, Optional[vec3] origin=None, bool about_origin=False)
Rotate one or more objects.
None writePrimitiveData(self, str filename, List[str] column_labels, Optional[List[int]] UUIDs=None, bool print_header=False)
Write primitive data to an ASCII text file.
None rotatePrimitive(self, Union[int, List[int]] UUID, float angle, Union[str, vec3] axis, Optional[vec3] origin=None)
Rotate one or more primitives.
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 addBoxObject(self, vec3 center=vec3(0, 0, 0), vec3 size=vec3(1, 1, 1), int3 subdiv=int3(1, 1, 1), Optional[RGBcolor] color=None, Optional[str] texturefile=None, bool reverse_normals=False)
Add a rectangular box (prism) as a compound object to the context.
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.
None setPrimitiveDataUInt(self, uuids_or_uuid, str label, int value)
Set primitive data as unsigned 32-bit integer for one or multiple primitives.
str getPrimitiveMaterialLabel(self, int uuid)
Get the material label assigned to a primitive.
int addTubeObject(self, int ndivs, List[vec3] nodes, List[float] radii, Optional[List[RGBcolor]] colors=None, Optional[str] texturefile=None, Optional[List[float]] texture_uv=None)
Add a tube as a compound object to the context.
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.
None translateObject(self, Union[int, List[int]] ObjID, vec3 shift)
Translate one or more compound objects by a shift vector.
List[vec3] getPrimitiveVertices(self, int uuid)
float getPrimitiveDataFloat(self, int uuid, str label)
Convenience method to get float primitive data.
deleteMaterial(self, str material_label)
Delete a material from the context.
dict get_plugin_capabilities(self)
Get detailed information about available plugin capabilities.
getTime(self)
Get the current simulation time.
None scalePrimitive(self, Union[int, List[int]] UUID, vec3 scale, Optional[vec3] point=None)
Scale one or more primitives.
List[int] getPrimitivesUsingMaterial(self, str material_label)
Get all primitive UUIDs that use a specific material.
setDateJulian(self, int julian_day, int year)
Set the simulation date using Julian day number.
setMaterialTextureColorOverride(self, str material_label, bool override)
Set whether material color overrides texture color.
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.
setMaterialColor(self, str material_label, color)
Set the RGBA color of a material.
bool doesMaterialExist(self, str material_label)
Check if a material with the given label exists.
None scaleConeObjectLength(self, int ObjID, float scale_factor)
Scale the length of a Cone object by scaling the distance between its two nodes.
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.
assignMaterialToObject(self, objID, str material_label)
Assign a material to all primitives in compound object(s).
None setPrimitiveDataVec3(self, uuids_or_uuid, str label, x_or_vec, float y=None, float z=None)
Set primitive data as vec3 for one or multiple primitives.
setMaterialTexture(self, str material_label, str texture_file)
Set the texture file for a material.
Physical properties and geometry information for a primitive.
__post_init__(self)
Calculate centroid from vertices if not provided.
Helios primitive type enumeration.