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)
311 @validate_patch_params
312 def addPatch(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1), rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None) -> int:
317 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
318 return context_wrapper.addPatchWithCenterSizeRotationAndColor(self.
context, center.to_list(), size.to_list(), rotation_list, color.to_list())
320 @validate_triangle_params
321 def addTriangle(self, vertex0: vec3, vertex1: vec3, vertex2: vec3, color: Optional[RGBcolor] =
None) -> int:
322 """Add a triangle primitive to the context
325 vertex0: First vertex of the triangle
326 vertex1: Second vertex of the triangle
327 vertex2: Third vertex of the triangle
328 color: Optional triangle color (defaults to white)
331 UUID of the created triangle primitive
335 return context_wrapper.addTriangle(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list())
337 return context_wrapper.addTriangleWithColor(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list(), color.to_list())
340 texture_file: str, uv0: vec2, uv1: vec2, uv2: vec2) -> int:
341 """Add a textured triangle primitive to the context
343 Creates a triangle with texture mapping. The texture image is mapped to the triangle
344 surface using UV coordinates, where (0,0) represents the top-left corner of the image
345 and (1,1) represents the bottom-right corner.
348 vertex0: First vertex of the triangle
349 vertex1: Second vertex of the triangle
350 vertex2: Third vertex of the triangle
351 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
352 uv0: UV texture coordinates for first vertex
353 uv1: UV texture coordinates for second vertex
354 uv2: UV texture coordinates for third vertex
357 UUID of the created textured triangle primitive
360 ValueError: If texture file path is invalid
361 FileNotFoundError: If texture file doesn't exist
362 RuntimeError: If context is in mock mode
365 >>> context = Context()
366 >>> # Create a textured triangle
367 >>> vertex0 = vec3(0, 0, 0)
368 >>> vertex1 = vec3(1, 0, 0)
369 >>> vertex2 = vec3(0.5, 1, 0)
370 >>> uv0 = vec2(0, 0) # Bottom-left of texture
371 >>> uv1 = vec2(1, 0) # Bottom-right of texture
372 >>> uv2 = vec2(0.5, 1) # Top-center of texture
373 >>> uuid = context.addTriangleTextured(vertex0, vertex1, vertex2,
374 ... "texture.png", uv0, uv1, uv2)
380 [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp'])
383 return context_wrapper.addTriangleWithTexture(
385 vertex0.to_list(), vertex1.to_list(), vertex2.to_list(),
386 validated_texture_file,
387 uv0.to_list(), uv1.to_list(), uv2.to_list()
392 primitive_type = context_wrapper.getPrimitiveType(self.
context, uuid)
397 return context_wrapper.getPrimitiveArea(self.
context, uuid)
401 normal_ptr = context_wrapper.getPrimitiveNormal(self.
context, uuid)
402 v =
vec3(normal_ptr[0], normal_ptr[1], normal_ptr[2])
407 size = ctypes.c_uint()
408 vertices_ptr = context_wrapper.getPrimitiveVertices(self.
context, uuid, ctypes.byref(size))
410 vertices_list = ctypes.cast(vertices_ptr, ctypes.POINTER(ctypes.c_float * size.value)).contents
411 vertices = [
vec3(vertices_list[i], vertices_list[i+1], vertices_list[i+2])
for i
in range(0, size.value, 3)]
416 color_ptr = context_wrapper.getPrimitiveColor(self.
context, uuid)
417 return RGBcolor(color_ptr[0], color_ptr[1], color_ptr[2])
421 return context_wrapper.getPrimitiveCount(self.
context)
425 size = ctypes.c_uint()
426 uuids_ptr = context_wrapper.getAllUUIDs(self.
context, ctypes.byref(size))
427 return list(uuids_ptr[:size.value])
431 return context_wrapper.getObjectCount(self.
context)
435 size = ctypes.c_uint()
436 objectids_ptr = context_wrapper.getAllObjectIDs(self.
context, ctypes.byref(size))
437 return list(objectids_ptr[:size.value])
441 Get physical properties and geometry information for a single primitive.
444 uuid: UUID of the primitive
447 PrimitiveInfo object containing physical properties and geometry
459 primitive_type=primitive_type,
468 Get physical properties and geometry information for all primitives in the context.
471 List of PrimitiveInfo objects for all primitives
478 Get physical properties and geometry information for all primitives belonging to a specific object.
481 object_id: ID of the object
484 List of PrimitiveInfo objects for primitives in the object
486 object_uuids = context_wrapper.getObjectPrimitiveUUIDs(self.
context, object_id)
490 def addTile(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
491 rotation: Optional[SphericalCoord] =
None, subdiv: int2 =
int2(1, 1),
492 color: Optional[RGBcolor] =
None) -> List[int]:
494 Add a subdivided patch (tile) to the context.
496 A tile is a patch subdivided into a regular grid of smaller patches,
497 useful for creating detailed surfaces or terrain.
500 center: 3D coordinates of tile center (default: origin)
501 size: Width and height of the tile (default: 1x1)
502 rotation: Orientation of the tile (default: no rotation)
503 subdiv: Number of subdivisions in x and y directions (default: 1x1)
504 color: Color of the tile (default: white)
507 List of UUIDs for all patches created in the tile
510 >>> context = Context()
511 >>> # Create a 2x2 meter tile subdivided into 4x4 patches
512 >>> tile_uuids = context.addTile(
513 ... center=vec3(0, 0, 1),
515 ... subdiv=int2(4, 4),
516 ... color=RGBcolor(0.5, 0.8, 0.2)
518 >>> print(f"Created {len(tile_uuids)} patches")
523 if not isinstance(center, vec3):
524 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
525 if not isinstance(size, vec2):
526 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
527 if rotation
is not None and not isinstance(rotation, SphericalCoord):
528 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
529 if not isinstance(subdiv, int2):
530 raise ValueError(f
"Subdiv must be an int2, got {type(subdiv).__name__}")
531 if color
is not None and not isinstance(color, RGBcolor):
532 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
535 if any(s <= 0
for s
in size.to_list()):
536 raise ValueError(
"All size dimensions must be positive")
537 if any(s <= 0
for s
in subdiv.to_list()):
538 raise ValueError(
"All subdivision counts must be positive")
544 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
546 if color
and not (color.r == 1.0
and color.g == 1.0
and color.b == 1.0):
547 return context_wrapper.addTileWithColor(
548 self.
context, center.to_list(), size.to_list(),
549 rotation_list, subdiv.to_list(), color.to_list()
552 return context_wrapper.addTile(
553 self.
context, center.to_list(), size.to_list(),
554 rotation_list, subdiv.to_list()
557 @validate_sphere_params
558 def addSphere(self, center: vec3 =
vec3(0, 0, 0), radius: float = 1.0,
559 ndivs: int = 10, color: Optional[RGBcolor] =
None) -> List[int]:
561 Add a sphere to the context.
563 The sphere is tessellated into triangular faces based on the specified
567 center: 3D coordinates of sphere center (default: origin)
568 radius: Radius of the sphere (default: 1.0)
569 ndivs: Number of divisions for tessellation (default: 10)
570 Higher values create smoother spheres but more triangles
571 color: Color of the sphere (default: white)
574 List of UUIDs for all triangles created in the sphere
577 >>> context = Context()
578 >>> # Create a red sphere at (1, 2, 3) with radius 0.5
579 >>> sphere_uuids = context.addSphere(
580 ... center=vec3(1, 2, 3),
583 ... color=RGBcolor(1, 0, 0)
585 >>> print(f"Created sphere with {len(sphere_uuids)} triangles")
590 if not isinstance(center, vec3):
591 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
592 if not isinstance(radius, (int, float)):
593 raise ValueError(f
"Radius must be a number, got {type(radius).__name__}")
594 if not isinstance(ndivs, int):
595 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
596 if color
is not None and not isinstance(color, RGBcolor):
597 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
601 raise ValueError(
"Sphere radius must be positive")
603 raise ValueError(
"Number of divisions must be at least 3")
606 return context_wrapper.addSphereWithColor(
607 self.
context, ndivs, center.to_list(), radius, color.to_list()
610 return context_wrapper.addSphere(
611 self.
context, ndivs, center.to_list(), radius
614 @validate_tube_params
615 def addTube(self, nodes: List[vec3], radii: Union[float, List[float]],
616 ndivs: int = 6, colors: Optional[Union[RGBcolor, List[RGBcolor]]] =
None) -> List[int]:
618 Add a tube (pipe/cylinder) to the context.
620 The tube is defined by a series of nodes (path) with radius at each node.
621 It's tessellated into triangular faces based on the number of radial divisions.
624 nodes: List of 3D points defining the tube path (at least 2 nodes)
625 radii: Radius at each node. Can be:
626 - Single float: constant radius for all nodes
627 - List of floats: radius for each node (must match nodes length)
628 ndivs: Number of radial divisions (default: 6)
629 Higher values create smoother tubes but more triangles
630 colors: Colors at each node. Can be:
632 - Single RGBcolor: constant color for all nodes
633 - List of RGBcolor: color for each node (must match nodes length)
636 List of UUIDs for all triangles created in the tube
639 >>> context = Context()
640 >>> # Create a curved tube with varying radius
641 >>> nodes = [vec3(0, 0, 0), vec3(1, 0, 0), vec3(2, 1, 0)]
642 >>> radii = [0.1, 0.2, 0.1]
643 >>> colors = [RGBcolor(1, 0, 0), RGBcolor(0, 1, 0), RGBcolor(0, 0, 1)]
644 >>> tube_uuids = context.addTube(nodes, radii, ndivs=8, colors=colors)
645 >>> print(f"Created tube with {len(tube_uuids)} triangles")
650 if not isinstance(nodes, (list, tuple)):
651 raise ValueError(f
"Nodes must be a list or tuple, got {type(nodes).__name__}")
652 if not isinstance(ndivs, int):
653 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
654 if colors
is not None and not isinstance(colors, (RGBcolor, list, tuple)):
655 raise ValueError(f
"Colors must be RGBcolor, list, tuple, or None, got {type(colors).__name__}")
659 raise ValueError(
"Tube requires at least 2 nodes")
661 raise ValueError(
"Number of radial divisions must be at least 3")
664 if isinstance(radii, (int, float)):
665 radii_list = [float(radii)] * len(nodes)
667 radii_list = [float(r)
for r
in radii]
668 if len(radii_list) != len(nodes):
669 raise ValueError(f
"Number of radii ({len(radii_list)}) must match number of nodes ({len(nodes)})")
672 if any(r <= 0
for r
in radii_list):
673 raise ValueError(
"All radii must be positive")
678 nodes_flat.extend(node.to_list())
682 return context_wrapper.addTube(self.
context, ndivs, nodes_flat, radii_list)
683 elif isinstance(colors, RGBcolor):
685 colors_flat = colors.to_list() * len(nodes)
688 if len(colors) != len(nodes):
689 raise ValueError(f
"Number of colors ({len(colors)}) must match number of nodes ({len(nodes)})")
692 colors_flat.extend(color.to_list())
694 return context_wrapper.addTubeWithColor(self.
context, ndivs, nodes_flat, radii_list, colors_flat)
697 def addBox(self, center: vec3 =
vec3(0, 0, 0), size: vec3 =
vec3(1, 1, 1),
698 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None) -> List[int]:
700 Add a rectangular box to the context.
702 The box is subdivided into patches on each face based on the specified
706 center: 3D coordinates of box center (default: origin)
707 size: Width, height, and depth of the box (default: 1x1x1)
708 subdiv: Number of subdivisions in x, y, and z directions (default: 1x1x1)
709 Higher values create more detailed surfaces
710 color: Color of the box (default: white)
713 List of UUIDs for all patches created on the box faces
716 >>> context = Context()
717 >>> # Create a blue box subdivided for detail
718 >>> box_uuids = context.addBox(
719 ... center=vec3(0, 0, 2),
720 ... size=vec3(2, 1, 0.5),
721 ... subdiv=int3(4, 2, 1),
722 ... color=RGBcolor(0, 0, 1)
724 >>> print(f"Created box with {len(box_uuids)} patches")
729 if not isinstance(center, vec3):
730 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
731 if not isinstance(size, vec3):
732 raise ValueError(f
"Size must be a vec3, got {type(size).__name__}")
733 if not isinstance(subdiv, int3):
734 raise ValueError(f
"Subdiv must be an int3, got {type(subdiv).__name__}")
735 if color
is not None and not isinstance(color, RGBcolor):
736 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
739 if any(s <= 0
for s
in size.to_list()):
740 raise ValueError(
"All box dimensions must be positive")
741 if any(s < 1
for s
in subdiv.to_list()):
742 raise ValueError(
"All subdivision counts must be at least 1")
745 return context_wrapper.addBoxWithColor(
746 self.
context, center.to_list(), size.to_list(),
747 subdiv.to_list(), color.to_list()
750 return context_wrapper.addBox(
751 self.
context, center.to_list(), size.to_list(), subdiv.to_list()
754 def addDisk(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
755 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
756 color: Optional[Union[RGBcolor, RGBAcolor]] =
None) -> List[int]:
758 Add a disk (circular or elliptical surface) to the context.
760 A disk is a flat circular or elliptical surface tessellated into
761 triangular faces. Supports both uniform radial subdivisions and
762 separate radial/azimuthal subdivisions for finer control.
765 center: 3D coordinates of disk center (default: origin)
766 size: Semi-major and semi-minor radii of the disk (default: 1x1 circle)
767 ndivs: Number of radial divisions (int) or [radial, azimuthal] divisions (int2)
768 (default: 20). Higher values create smoother circles but more triangles.
769 rotation: Orientation of the disk (default: horizontal, normal = +z)
770 color: Color of the disk (default: white). Can be RGBcolor or RGBAcolor for transparency.
773 List of UUIDs for all triangles created in the disk
776 >>> context = Context()
777 >>> # Create a red disk at (0, 0, 1) with radius 0.5
778 >>> disk_uuids = context.addDisk(
779 ... center=vec3(0, 0, 1),
780 ... size=vec2(0.5, 0.5),
782 ... color=RGBcolor(1, 0, 0)
784 >>> print(f"Created disk with {len(disk_uuids)} triangles")
786 >>> # Create a semi-transparent blue elliptical disk
787 >>> disk_uuids = context.addDisk(
788 ... center=vec3(0, 0, 2),
789 ... size=vec2(1.0, 0.5),
791 ... rotation=SphericalCoord(1, 0.5, 0),
792 ... color=RGBAcolor(0, 0, 1, 0.5)
795 >>> # Create disk with polar/radial subdivisions for finer control
796 >>> disk_uuids = context.addDisk(
797 ... center=vec3(0, 0, 3),
799 ... ndivs=int2(10, 20), # 10 radial, 20 azimuthal divisions
800 ... color=RGBcolor(0, 1, 0)
806 if not isinstance(center, vec3):
807 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
808 if not isinstance(size, vec2):
809 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
810 if not isinstance(ndivs, (int, int2)):
811 raise ValueError(f
"Ndivs must be an int or int2, got {type(ndivs).__name__}")
812 if rotation
is not None and not isinstance(rotation, SphericalCoord):
813 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
814 if color
is not None and not isinstance(color, (RGBcolor, RGBAcolor)):
815 raise ValueError(f
"Color must be an RGBcolor, RGBAcolor, or None, got {type(color).__name__}")
818 if any(s <= 0
for s
in size.to_list()):
819 raise ValueError(
"Disk size must be positive")
822 if isinstance(ndivs, int):
824 raise ValueError(
"Number of divisions must be at least 3")
826 if any(n < 1
for n
in ndivs.to_list()):
827 raise ValueError(
"Radial and angular divisions must be at least 1")
835 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
838 if isinstance(ndivs, int2):
841 if isinstance(color, RGBAcolor):
842 return context_wrapper.addDiskPolarSubdivisionsRGBA(
843 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
844 rotation_list, color.to_list()
848 return context_wrapper.addDiskPolarSubdivisions(
849 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
850 rotation_list, color.to_list()
854 color_list = [1.0, 1.0, 1.0]
855 return context_wrapper.addDiskPolarSubdivisions(
856 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
857 rotation_list, color_list
862 if isinstance(color, RGBAcolor):
864 return context_wrapper.addDiskWithRGBAColor(
865 self.
context, ndivs, center.to_list(), size.to_list(),
866 rotation_list, color.to_list()
870 return context_wrapper.addDiskWithColor(
871 self.
context, ndivs, center.to_list(), size.to_list(),
872 rotation_list, color.to_list()
876 return context_wrapper.addDiskWithRotation(
877 self.
context, ndivs, center.to_list(), size.to_list(),
881 def addCone(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
882 ndivs: int = 20, color: Optional[RGBcolor] =
None) -> List[int]:
884 Add a cone (or cylinder/frustum) to the context.
886 A cone is a 3D shape connecting two circular cross-sections with
887 potentially different radii. When radii are equal, creates a cylinder.
888 When one radius is zero, creates a true cone.
891 node0: 3D coordinates of the base center
892 node1: 3D coordinates of the apex center
893 radius0: Radius at base (node0). Use 0 for pointed end.
894 radius1: Radius at apex (node1). Use 0 for pointed end.
895 ndivs: Number of radial divisions for tessellation (default: 20)
896 color: Color of the cone (default: white)
899 List of UUIDs for all triangles created in the cone
902 >>> context = Context()
903 >>> # Create a cylinder (equal radii)
904 >>> cylinder_uuids = context.addCone(
905 ... node0=vec3(0, 0, 0),
906 ... node1=vec3(0, 0, 2),
912 >>> # Create a true cone (one radius = 0)
913 >>> cone_uuids = context.addCone(
914 ... node0=vec3(1, 0, 0),
915 ... node1=vec3(1, 0, 1.5),
919 ... color=RGBcolor(1, 0, 0)
922 >>> # Create a frustum (different radii)
923 >>> frustum_uuids = context.addCone(
924 ... node0=vec3(2, 0, 0),
925 ... node1=vec3(2, 0, 1),
934 if not isinstance(node0, vec3):
935 raise ValueError(f
"node0 must be a vec3, got {type(node0).__name__}")
936 if not isinstance(node1, vec3):
937 raise ValueError(f
"node1 must be a vec3, got {type(node1).__name__}")
938 if not isinstance(ndivs, int):
939 raise ValueError(f
"ndivs must be an int, got {type(ndivs).__name__}")
940 if color
is not None and not isinstance(color, RGBcolor):
941 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
944 if radius0 < 0
or radius1 < 0:
945 raise ValueError(
"Radii must be non-negative")
947 raise ValueError(
"Number of radial divisions must be at least 3")
951 return context_wrapper.addConeWithColor(
952 self.
context, ndivs, node0.to_list(), node1.to_list(),
953 radius0, radius1, color.to_list()
956 return context_wrapper.addCone(
957 self.
context, ndivs, node0.to_list(), node1.to_list(),
962 radius: Union[float, vec3] = 1.0, ndivs: int = 20,
963 color: Optional[RGBcolor] =
None,
964 texturefile: Optional[str] =
None) -> int:
966 Add a spherical or ellipsoidal compound object to the context.
968 Creates a sphere or ellipsoid as a compound object with a trackable object ID.
969 Primitives within the object are registered as children of the object.
972 center: Center position of sphere/ellipsoid (default: origin)
973 radius: Radius as float (sphere) or vec3 (ellipsoid) (default: 1.0)
974 ndivs: Number of tessellation divisions (default: 20)
975 color: Optional RGB color
976 texturefile: Optional texture image file path
979 Object ID of the created compound object
982 ValueError: If parameters are invalid
983 NotImplementedError: If object-returning functions unavailable
986 >>> # Create a basic sphere at origin
987 >>> obj_id = ctx.addSphereObject()
989 >>> # Create a colored sphere
990 >>> obj_id = ctx.addSphereObject(
991 ... center=vec3(0, 0, 5),
993 ... color=RGBcolor(1, 0, 0)
996 >>> # Create an ellipsoid (stretched sphere)
997 >>> obj_id = ctx.addSphereObject(
998 ... center=vec3(10, 0, 0),
999 ... radius=vec3(2, 1, 1), # Elongated in x-direction
1007 raise ValueError(
"Number of divisions must be at least 3")
1010 is_ellipsoid = isinstance(radius, vec3)
1016 return context_wrapper.addSphereObject_ellipsoid_texture(
1017 self.
context, ndivs, center.to_list(), radius.to_list(), texturefile
1020 return context_wrapper.addSphereObject_ellipsoid_color(
1021 self.
context, ndivs, center.to_list(), radius.to_list(), color.to_list()
1024 return context_wrapper.addSphereObject_ellipsoid(
1025 self.
context, ndivs, center.to_list(), radius.to_list()
1030 return context_wrapper.addSphereObject_texture(
1031 self.
context, ndivs, center.to_list(), radius, texturefile
1034 return context_wrapper.addSphereObject_color(
1035 self.
context, ndivs, center.to_list(), radius, color.to_list()
1038 return context_wrapper.addSphereObject_basic(
1039 self.
context, ndivs, center.to_list(), radius
1044 subdiv: int2 =
int2(1, 1),
1045 color: Optional[RGBcolor] =
None,
1046 texturefile: Optional[str] =
None,
1047 texture_repeat: Optional[int2] =
None) -> int:
1049 Add a tiled patch (subdivided patch) as a compound object to the context.
1051 Creates a rectangular patch subdivided into a grid of smaller patches,
1052 registered as a compound object with a trackable object ID.
1055 center: Center position of tile (default: origin)
1056 size: Size in x and y directions (default: 1x1)
1057 rotation: Spherical rotation (default: no rotation)
1058 subdiv: Number of subdivisions in x and y (default: 1x1)
1059 color: Optional RGB color
1060 texturefile: Optional texture image file path
1061 texture_repeat: Optional texture repetitions in x and y
1064 Object ID of the created compound object
1067 ValueError: If parameters are invalid
1068 NotImplementedError: If object-returning functions unavailable
1071 >>> # Create a basic 2x2 tile
1072 >>> obj_id = ctx.addTileObject(
1073 ... center=vec3(0, 0, 0),
1074 ... size=vec2(10, 10),
1075 ... subdiv=int2(2, 2)
1078 >>> # Create a colored tile with rotation
1079 >>> obj_id = ctx.addTileObject(
1080 ... center=vec3(5, 0, 0),
1081 ... size=vec2(10, 5),
1082 ... rotation=SphericalCoord(1, 0, 45),
1083 ... subdiv=int2(4, 2),
1084 ... color=RGBcolor(0, 1, 0)
1090 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1093 if texture_repeat
is not None:
1094 if texturefile
is None:
1095 raise ValueError(
"texture_repeat requires texturefile")
1096 return context_wrapper.addTileObject_texture_repeat(
1097 self.
context, center.to_list(), size.to_list(), rotation_list,
1098 subdiv.to_list(), texturefile, texture_repeat.to_list()
1101 return context_wrapper.addTileObject_texture(
1102 self.
context, center.to_list(), size.to_list(), rotation_list,
1103 subdiv.to_list(), texturefile
1106 return context_wrapper.addTileObject_color(
1107 self.
context, center.to_list(), size.to_list(), rotation_list,
1108 subdiv.to_list(), color.to_list()
1111 return context_wrapper.addTileObject_basic(
1112 self.
context, center.to_list(), size.to_list(), rotation_list,
1117 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None,
1118 texturefile: Optional[str] =
None, reverse_normals: bool =
False) -> int:
1120 Add a rectangular box (prism) as a compound object to the context.
1123 center: Center position (default: origin)
1124 size: Size in x, y, z directions (default: 1x1x1)
1125 subdiv: Subdivisions in x, y, z (default: 1x1x1)
1126 color: Optional RGB color
1127 texturefile: Optional texture file path
1128 reverse_normals: Reverse normal directions (default: False)
1131 Object ID of the created compound object
1137 return context_wrapper.addBoxObject_texture_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile, reverse_normals)
1139 return context_wrapper.addBoxObject_color_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list(), reverse_normals)
1141 raise ValueError(
"reverse_normals requires either color or texturefile")
1143 return context_wrapper.addBoxObject_texture(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile)
1145 return context_wrapper.addBoxObject_color(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list())
1147 return context_wrapper.addBoxObject_basic(self.
context, center.to_list(), size.to_list(), subdiv.to_list())
1149 def addConeObject(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1150 ndivs: int = 20, color: Optional[RGBcolor] =
None,
1151 texturefile: Optional[str] =
None) -> int:
1153 Add a cone/cylinder/frustum as a compound object to the context.
1156 node0: Base position
1158 radius0: Radius at base
1159 radius1: Radius at top
1160 ndivs: Number of radial divisions (default: 20)
1161 color: Optional RGB color
1162 texturefile: Optional texture file path
1165 Object ID of the created compound object
1170 return context_wrapper.addConeObject_texture(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, texturefile)
1172 return context_wrapper.addConeObject_color(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, color.to_list())
1174 return context_wrapper.addConeObject_basic(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1)
1177 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
1178 color: Optional[Union[RGBcolor, RGBAcolor]] =
None,
1179 texturefile: Optional[str] =
None) -> int:
1181 Add a disk as a compound object to the context.
1184 center: Center position (default: origin)
1185 size: Semi-major and semi-minor radii (default: 1x1)
1186 ndivs: int (uniform) or int2 (polar/radial subdivisions) (default: 20)
1187 rotation: Optional spherical rotation
1188 color: Optional RGB or RGBA color
1189 texturefile: Optional texture file path
1192 Object ID of the created compound object
1196 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
if rotation
else [1, 0, 0]
1197 is_polar = isinstance(ndivs, int2)
1201 return context_wrapper.addDiskObject_polar_texture(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, texturefile)
1203 if isinstance(color, RGBAcolor):
1204 return context_wrapper.addDiskObject_polar_rgba(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1206 return context_wrapper.addDiskObject_polar_color(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1208 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())
1211 return context_wrapper.addDiskObject_texture(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, texturefile)
1213 if isinstance(color, RGBAcolor):
1214 return context_wrapper.addDiskObject_rgba(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1216 return context_wrapper.addDiskObject_color(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1218 return context_wrapper.addDiskObject_rotation(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list)
1220 return context_wrapper.addDiskObject_basic(self.
context, ndivs, center.to_list(), size.to_list())
1222 def addTubeObject(self, ndivs: int, nodes: List[vec3], radii: List[float],
1223 colors: Optional[List[RGBcolor]] =
None,
1224 texturefile: Optional[str] =
None,
1225 texture_uv: Optional[List[float]] =
None) -> int:
1227 Add a tube as a compound object to the context.
1230 ndivs: Number of radial subdivisions
1231 nodes: List of vec3 positions defining tube segments
1232 radii: List of radii at each node
1233 colors: Optional list of RGB colors for each segment
1234 texturefile: Optional texture file path
1235 texture_uv: Optional UV coordinates for texture mapping
1238 Object ID of the created compound object
1243 raise ValueError(
"Tube requires at least 2 nodes")
1244 if len(radii) != len(nodes):
1245 raise ValueError(
"Number of radii must match number of nodes")
1247 nodes_flat = [coord
for node
in nodes
for coord
in node.to_list()]
1249 if texture_uv
is not None:
1250 if texturefile
is None:
1251 raise ValueError(
"texture_uv requires texturefile")
1252 return context_wrapper.addTubeObject_texture_uv(self.
context, ndivs, nodes_flat, radii, texturefile, texture_uv)
1254 return context_wrapper.addTubeObject_texture(self.
context, ndivs, nodes_flat, radii, texturefile)
1256 if len(colors) != len(nodes):
1257 raise ValueError(
"Number of colors must match number of nodes")
1258 colors_flat = [c
for color
in colors
for c
in color.to_list()]
1259 return context_wrapper.addTubeObject_color(self.
context, ndivs, nodes_flat, radii, colors_flat)
1261 return context_wrapper.addTubeObject_basic(self.
context, ndivs, nodes_flat, radii)
1263 def copyPrimitive(self, UUID: Union[int, List[int]]) -> Union[int, List[int]]:
1265 Copy one or more primitives.
1267 Creates a duplicate of the specified primitive(s) with all associated data.
1268 The copy is placed at the same location as the original.
1271 UUID: Single primitive UUID or list of UUIDs to copy
1274 Single UUID of copied primitive (if UUID is int) or
1275 List of UUIDs of copied primitives (if UUID is list)
1278 >>> context = Context()
1279 >>> original_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1280 >>> # Copy single primitive
1281 >>> copied_uuid = context.copyPrimitive(original_uuid)
1282 >>> # Copy multiple primitives
1283 >>> copied_uuids = context.copyPrimitive([uuid1, uuid2, uuid3])
1287 if isinstance(UUID, int):
1288 return context_wrapper.copyPrimitive(self.
context, UUID)
1289 elif isinstance(UUID, list):
1290 return context_wrapper.copyPrimitives(self.
context, UUID)
1292 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1296 Copy all primitive data from source to destination primitive.
1298 Copies all associated data (primitive data fields) from the source
1299 primitive to the destination primitive. Both primitives must already exist.
1302 sourceUUID: UUID of the source primitive
1303 destinationUUID: UUID of the destination primitive
1306 >>> context = Context()
1307 >>> source_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1308 >>> dest_uuid = context.addPatch(center=vec3(1, 0, 0), size=vec2(1, 1))
1309 >>> context.setPrimitiveData(source_uuid, "temperature", 25.5)
1310 >>> context.copyPrimitiveData(source_uuid, dest_uuid)
1311 >>> # dest_uuid now has temperature data
1315 if not isinstance(sourceUUID, int):
1316 raise ValueError(f
"sourceUUID must be int, got {type(sourceUUID).__name__}")
1317 if not isinstance(destinationUUID, int):
1318 raise ValueError(f
"destinationUUID must be int, got {type(destinationUUID).__name__}")
1320 context_wrapper.copyPrimitiveData(self.
context, sourceUUID, destinationUUID)
1322 def copyObject(self, ObjID: Union[int, List[int]]) -> Union[int, List[int]]:
1324 Copy one or more compound objects.
1326 Creates a duplicate of the specified compound object(s) with all
1327 associated primitives and data. The copy is placed at the same location
1331 ObjID: Single object ID or list of object IDs to copy
1334 Single object ID of copied object (if ObjID is int) or
1335 List of object IDs of copied objects (if ObjID is list)
1338 >>> context = Context()
1339 >>> original_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1340 >>> # Copy single object
1341 >>> copied_obj = context.copyObject(original_obj)
1342 >>> # Copy multiple objects
1343 >>> copied_objs = context.copyObject([obj1, obj2, obj3])
1347 if isinstance(ObjID, int):
1348 return context_wrapper.copyObject(self.
context, ObjID)
1349 elif isinstance(ObjID, list):
1350 return context_wrapper.copyObjects(self.
context, ObjID)
1352 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1354 def copyObjectData(self, source_objID: int, destination_objID: int) ->
None:
1356 Copy all object data from source to destination compound object.
1358 Copies all associated data (object data fields) from the source
1359 compound object to the destination object. Both objects must already exist.
1362 source_objID: Object ID of the source compound object
1363 destination_objID: Object ID of the destination compound object
1366 >>> context = Context()
1367 >>> source_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1368 >>> dest_obj = context.addTile(center=vec3(2, 0, 0), size=vec2(2, 2))
1369 >>> context.setObjectData(source_obj, "material", "wood")
1370 >>> context.copyObjectData(source_obj, dest_obj)
1371 >>> # dest_obj now has material data
1375 if not isinstance(source_objID, int):
1376 raise ValueError(f
"source_objID must be int, got {type(source_objID).__name__}")
1377 if not isinstance(destination_objID, int):
1378 raise ValueError(f
"destination_objID must be int, got {type(destination_objID).__name__}")
1380 context_wrapper.copyObjectData(self.
context, source_objID, destination_objID)
1384 Translate one or more primitives by a shift vector.
1386 Moves the specified primitive(s) by the given shift vector without
1387 changing their orientation or size.
1390 UUID: Single primitive UUID or list of UUIDs to translate
1391 shift: 3D vector representing the translation [x, y, z]
1394 >>> context = Context()
1395 >>> patch_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1396 >>> # Translate single primitive
1397 >>> context.translatePrimitive(patch_uuid, vec3(1, 0, 0)) # Move 1 unit in x
1398 >>> # Translate multiple primitives
1399 >>> context.translatePrimitive([uuid1, uuid2, uuid3], vec3(0, 0, 1)) # Move 1 unit in z
1404 if not isinstance(shift, vec3):
1405 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
1407 if isinstance(UUID, int):
1408 context_wrapper.translatePrimitive(self.
context, UUID, shift.to_list())
1409 elif isinstance(UUID, list):
1410 context_wrapper.translatePrimitives(self.
context, UUID, shift.to_list())
1412 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1414 def translateObject(self, ObjID: Union[int, List[int]], shift: vec3) ->
None:
1416 Translate one or more compound objects by a shift vector.
1418 Moves the specified compound object(s) and all their constituent
1419 primitives by the given shift vector without changing orientation or size.
1422 ObjID: Single object ID or list of object IDs to translate
1423 shift: 3D vector representing the translation [x, y, z]
1426 >>> context = Context()
1427 >>> tile_uuids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1428 >>> obj_id = context.getPrimitiveParentObjectID(tile_uuids[0]) # Get object ID
1429 >>> # Translate single object
1430 >>> context.translateObject(obj_id, vec3(5, 0, 0)) # Move 5 units in x
1431 >>> # Translate multiple objects
1432 >>> context.translateObject([obj1, obj2, obj3], vec3(0, 2, 0)) # Move 2 units in y
1437 if not isinstance(shift, vec3):
1438 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
1440 if isinstance(ObjID, int):
1441 context_wrapper.translateObject(self.
context, ObjID, shift.to_list())
1442 elif isinstance(ObjID, list):
1443 context_wrapper.translateObjects(self.
context, ObjID, shift.to_list())
1445 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1448 axis: Union[str, vec3], origin: Optional[vec3] =
None) ->
None:
1450 Rotate one or more primitives.
1453 UUID: Single UUID or list of UUIDs to rotate
1454 angle: Rotation angle in radians
1455 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
1456 origin: Optional rotation origin point. If None, rotates about primitive center.
1457 If provided with string axis, raises ValueError.
1460 ValueError: If axis is invalid or if origin is provided with string axis
1465 if isinstance(axis, str):
1466 if axis
not in (
'x',
'y',
'z'):
1467 raise ValueError(
"axis must be 'x', 'y', or 'z'")
1468 if origin
is not None:
1469 raise ValueError(
"origin parameter cannot be used with string axis")
1472 if isinstance(UUID, int):
1473 context_wrapper.rotatePrimitive_axisString(self.
context, UUID, angle, axis)
1474 elif isinstance(UUID, list):
1475 context_wrapper.rotatePrimitives_axisString(self.
context, UUID, angle, axis)
1477 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1479 elif isinstance(axis, vec3):
1480 axis_list = axis.to_list()
1483 if all(abs(v) < 1e-10
for v
in axis_list):
1484 raise ValueError(
"axis vector cannot be zero")
1488 if isinstance(UUID, int):
1489 context_wrapper.rotatePrimitive_axisVector(self.
context, UUID, angle, axis_list)
1490 elif isinstance(UUID, list):
1491 context_wrapper.rotatePrimitives_axisVector(self.
context, UUID, angle, axis_list)
1493 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1496 if not isinstance(origin, vec3):
1497 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
1499 origin_list = origin.to_list()
1500 if isinstance(UUID, int):
1501 context_wrapper.rotatePrimitive_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
1502 elif isinstance(UUID, list):
1503 context_wrapper.rotatePrimitives_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
1505 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1507 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
1509 def rotateObject(self, ObjID: Union[int, List[int]], angle: float,
1510 axis: Union[str, vec3], origin: Optional[vec3] =
None,
1511 about_origin: bool =
False) ->
None:
1513 Rotate one or more objects.
1516 ObjID: Single object ID or list of object IDs to rotate
1517 angle: Rotation angle in radians
1518 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
1519 origin: Optional rotation origin point. If None, rotates about object center.
1520 If provided with string axis, raises ValueError.
1521 about_origin: If True, rotate about global origin (0,0,0). Cannot be used with origin parameter.
1524 ValueError: If axis is invalid or if origin and about_origin are both specified
1529 if origin
is not None and about_origin:
1530 raise ValueError(
"Cannot specify both origin and about_origin")
1533 if isinstance(axis, str):
1534 if axis
not in (
'x',
'y',
'z'):
1535 raise ValueError(
"axis must be 'x', 'y', or 'z'")
1536 if origin
is not None:
1537 raise ValueError(
"origin parameter cannot be used with string axis")
1539 raise ValueError(
"about_origin parameter cannot be used with string axis")
1542 if isinstance(ObjID, int):
1543 context_wrapper.rotateObject_axisString(self.
context, ObjID, angle, axis)
1544 elif isinstance(ObjID, list):
1545 context_wrapper.rotateObjects_axisString(self.
context, ObjID, angle, axis)
1547 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1549 elif isinstance(axis, vec3):
1550 axis_list = axis.to_list()
1553 if all(abs(v) < 1e-10
for v
in axis_list):
1554 raise ValueError(
"axis vector cannot be zero")
1558 if isinstance(ObjID, int):
1559 context_wrapper.rotateObjectAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
1560 elif isinstance(ObjID, list):
1561 context_wrapper.rotateObjectsAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
1563 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1564 elif origin
is None:
1566 if isinstance(ObjID, int):
1567 context_wrapper.rotateObject_axisVector(self.
context, ObjID, angle, axis_list)
1568 elif isinstance(ObjID, list):
1569 context_wrapper.rotateObjects_axisVector(self.
context, ObjID, angle, axis_list)
1571 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1574 if not isinstance(origin, vec3):
1575 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
1577 origin_list = origin.to_list()
1578 if isinstance(ObjID, int):
1579 context_wrapper.rotateObject_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
1580 elif isinstance(ObjID, list):
1581 context_wrapper.rotateObjects_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
1583 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1585 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
1587 def scalePrimitive(self, UUID: Union[int, List[int]], scale: vec3, point: Optional[vec3] =
None) ->
None:
1589 Scale one or more primitives.
1592 UUID: Single UUID or list of UUIDs to scale
1593 scale: Scale factors as vec3(x, y, z)
1594 point: Optional point to scale about. If None, scales about primitive center.
1597 ValueError: If scale or point parameters are invalid
1601 if not isinstance(scale, vec3):
1602 raise ValueError(f
"scale must be a vec3, got {type(scale).__name__}")
1604 scale_list = scale.to_list()
1608 if isinstance(UUID, int):
1609 context_wrapper.scalePrimitive(self.
context, UUID, scale_list)
1610 elif isinstance(UUID, list):
1611 context_wrapper.scalePrimitives(self.
context, UUID, scale_list)
1613 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1616 if not isinstance(point, vec3):
1617 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
1619 point_list = point.to_list()
1620 if isinstance(UUID, int):
1621 context_wrapper.scalePrimitiveAboutPoint(self.
context, UUID, scale_list, point_list)
1622 elif isinstance(UUID, list):
1623 context_wrapper.scalePrimitivesAboutPoint(self.
context, UUID, scale_list, point_list)
1625 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1627 def scaleObject(self, ObjID: Union[int, List[int]], scale: vec3,
1628 point: Optional[vec3] =
None, about_center: bool =
False,
1629 about_origin: bool =
False) ->
None:
1631 Scale one or more objects.
1634 ObjID: Single object ID or list of object IDs to scale
1635 scale: Scale factors as vec3(x, y, z)
1636 point: Optional point to scale about
1637 about_center: If True, scale about object center (default behavior)
1638 about_origin: If True, scale about global origin (0,0,0)
1641 ValueError: If parameters are invalid or conflicting options specified
1646 options_count = sum([point
is not None, about_center, about_origin])
1647 if options_count > 1:
1648 raise ValueError(
"Cannot specify multiple scaling options (point, about_center, about_origin)")
1650 if not isinstance(scale, vec3):
1651 raise ValueError(f
"scale must be a vec3, got {type(scale).__name__}")
1653 scale_list = scale.to_list()
1657 if isinstance(ObjID, int):
1658 context_wrapper.scaleObjectAboutOrigin(self.
context, ObjID, scale_list)
1659 elif isinstance(ObjID, list):
1660 context_wrapper.scaleObjectsAboutOrigin(self.
context, ObjID, scale_list)
1662 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1665 if isinstance(ObjID, int):
1666 context_wrapper.scaleObjectAboutCenter(self.
context, ObjID, scale_list)
1667 elif isinstance(ObjID, list):
1668 context_wrapper.scaleObjectsAboutCenter(self.
context, ObjID, scale_list)
1670 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1671 elif point
is not None:
1673 if not isinstance(point, vec3):
1674 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
1676 point_list = point.to_list()
1677 if isinstance(ObjID, int):
1678 context_wrapper.scaleObjectAboutPoint(self.
context, ObjID, scale_list, point_list)
1679 elif isinstance(ObjID, list):
1680 context_wrapper.scaleObjectsAboutPoint(self.
context, ObjID, scale_list, point_list)
1682 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1685 if isinstance(ObjID, int):
1686 context_wrapper.scaleObject(self.
context, ObjID, scale_list)
1687 elif isinstance(ObjID, list):
1688 context_wrapper.scaleObjects(self.
context, ObjID, scale_list)
1690 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1694 Scale the length of a Cone object by scaling the distance between its two nodes.
1697 ObjID: Object ID of the Cone to scale
1698 scale_factor: Factor by which to scale the cone length (e.g., 2.0 doubles length)
1701 ValueError: If ObjID is not an integer or scale_factor is invalid
1702 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
1705 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
1706 method, enforcing better encapsulation.
1709 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
1710 >>> context.scaleConeObjectLength(cone_id, 1.5) # Make cone 50% longer
1712 if not isinstance(ObjID, int):
1713 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
1714 if not isinstance(scale_factor, (int, float)):
1715 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
1716 if scale_factor <= 0:
1717 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
1719 context_wrapper.scaleConeObjectLength(self.
context, ObjID, float(scale_factor))
1723 Scale the girth of a Cone object by scaling the radii at both nodes.
1726 ObjID: Object ID of the Cone to scale
1727 scale_factor: Factor by which to scale the cone girth (e.g., 2.0 doubles girth)
1730 ValueError: If ObjID is not an integer or scale_factor is invalid
1731 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
1734 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
1735 method, enforcing better encapsulation.
1738 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
1739 >>> context.scaleConeObjectGirth(cone_id, 2.0) # Double the cone girth
1741 if not isinstance(ObjID, int):
1742 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
1743 if not isinstance(scale_factor, (int, float)):
1744 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
1745 if scale_factor <= 0:
1746 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
1748 context_wrapper.scaleConeObjectGirth(self.
context, ObjID, float(scale_factor))
1750 def loadPLY(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
1751 rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None,
1752 upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
1754 Load geometry from a PLY (Stanford Polygon) file.
1757 filename: Path to the PLY file to load
1758 origin: Origin point for positioning the geometry (optional)
1759 height: Height scaling factor (optional)
1760 rotation: Rotation to apply to the geometry (optional)
1761 color: Default color for geometry without color data (optional)
1762 upaxis: Up axis orientation ("YUP" or "ZUP")
1763 silent: If True, suppress loading output messages
1766 List of UUIDs for the loaded primitives
1772 if origin
is None and height
is None and rotation
is None and color
is None:
1774 return context_wrapper.loadPLY(self.
context, validated_filename, silent)
1776 elif origin
is not None and height
is not None and rotation
is None and color
is None:
1778 return context_wrapper.loadPLYWithOriginHeight(self.
context, validated_filename, origin.to_list(), height, upaxis, silent)
1780 elif origin
is not None and height
is not None and rotation
is not None and color
is None:
1782 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1783 return context_wrapper.loadPLYWithOriginHeightRotation(self.
context, validated_filename, origin.to_list(), height, rotation_list, upaxis, silent)
1785 elif origin
is not None and height
is not None and rotation
is None and color
is not None:
1787 return context_wrapper.loadPLYWithOriginHeightColor(self.
context, validated_filename, origin.to_list(), height, color.to_list(), upaxis, silent)
1789 elif origin
is not None and height
is not None and rotation
is not None and color
is not None:
1791 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1792 return context_wrapper.loadPLYWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation_list, color.to_list(), upaxis, silent)
1795 raise ValueError(
"Invalid parameter combination. When using transformations, both origin and height are required.")
1797 def loadOBJ(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
1798 scale: Optional[vec3] =
None, rotation: Optional[SphericalCoord] =
None,
1799 color: Optional[RGBcolor] =
None, upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
1801 Load geometry from an OBJ (Wavefront) file.
1804 filename: Path to the OBJ file to load
1805 origin: Origin point for positioning the geometry (optional)
1806 height: Height scaling factor (optional, alternative to scale)
1807 scale: Scale factor for all dimensions (optional, alternative to height)
1808 rotation: Rotation to apply to the geometry (optional)
1809 color: Default color for geometry without color data (optional)
1810 upaxis: Up axis orientation ("YUP" or "ZUP")
1811 silent: If True, suppress loading output messages
1814 List of UUIDs for the loaded primitives
1820 if origin
is None and height
is None and scale
is None and rotation
is None and color
is None:
1822 return context_wrapper.loadOBJ(self.
context, validated_filename, silent)
1824 elif origin
is not None and height
is not None and scale
is None and rotation
is not None and color
is not None:
1826 return context_wrapper.loadOBJWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), silent)
1828 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":
1830 return context_wrapper.loadOBJWithOriginHeightRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), upaxis, silent)
1832 elif origin
is not None and scale
is not None and rotation
is not None and color
is not None:
1834 return context_wrapper.loadOBJWithOriginScaleRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), scale.to_list(), rotation.to_list(), color.to_list(), upaxis, silent)
1837 raise ValueError(
"Invalid parameter combination. For OBJ loading, you must provide either: " +
1838 "1) No parameters (simple load), " +
1839 "2) origin + height + rotation + color, " +
1840 "3) origin + height + rotation + color + upaxis, or " +
1841 "4) origin + scale + rotation + color + upaxis")
1843 def loadXML(self, filename: str, quiet: bool =
False) -> List[int]:
1845 Load geometry from a Helios XML file.
1848 filename: Path to the XML file to load
1849 quiet: If True, suppress loading output messages
1852 List of UUIDs for the loaded primitives
1858 return context_wrapper.loadXML(self.
context, validated_filename, quiet)
1860 def writePLY(self, filename: str, UUIDs: Optional[List[int]] =
None) ->
None:
1862 Write geometry to a PLY (Stanford Polygon) file.
1865 filename: Path to the output PLY file
1866 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
1869 ValueError: If filename is invalid or UUIDs are invalid
1870 PermissionError: If output directory is not writable
1871 FileNotFoundError: If UUIDs do not exist in context
1872 RuntimeError: If Context is in mock mode
1875 >>> context.writePLY("output.ply") # Export all primitives
1876 >>> context.writePLY("subset.ply", [uuid1, uuid2]) # Export specific primitives
1885 context_wrapper.writePLY(self.
context, validated_filename)
1889 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
1896 context_wrapper.writePLYWithUUIDs(self.
context, validated_filename, UUIDs)
1898 def writeOBJ(self, filename: str, UUIDs: Optional[List[int]] =
None,
1899 primitive_data_fields: Optional[List[str]] =
None,
1900 write_normals: bool =
False, silent: bool =
False) ->
None:
1902 Write geometry to an OBJ (Wavefront) file.
1905 filename: Path to the output OBJ file
1906 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
1907 primitive_data_fields: Optional list of primitive data field names to export
1908 write_normals: Whether to include vertex normals in the output
1909 silent: Whether to suppress output messages during export
1912 ValueError: If filename is invalid, UUIDs are invalid, or data fields don't exist
1913 PermissionError: If output directory is not writable
1914 FileNotFoundError: If UUIDs do not exist in context
1915 RuntimeError: If Context is in mock mode
1918 >>> context.writeOBJ("output.obj") # Export all primitives
1919 >>> context.writeOBJ("subset.obj", [uuid1, uuid2]) # Export specific primitives
1920 >>> context.writeOBJ("with_data.obj", [uuid1], ["temperature", "area"]) # Export with data
1929 context_wrapper.writeOBJ(self.
context, validated_filename, write_normals, silent)
1930 elif primitive_data_fields
is None:
1933 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
1939 context_wrapper.writeOBJWithUUIDs(self.
context, validated_filename, UUIDs, write_normals, silent)
1943 raise ValueError(
"UUIDs list cannot be empty when exporting primitive data")
1944 if not primitive_data_fields:
1945 raise ValueError(
"primitive_data_fields list cannot be empty")
1954 context_wrapper.writeOBJWithPrimitiveData(self.
context, validated_filename, UUIDs, primitive_data_fields, write_normals, silent)
1957 UUIDs: Optional[List[int]] =
None,
1958 print_header: bool =
False) ->
None:
1960 Write primitive data to an ASCII text file.
1962 Outputs a space-separated text file where each row corresponds to a primitive
1963 and each column corresponds to a primitive data label.
1966 filename: Path to the output file
1967 column_labels: List of primitive data labels to include as columns.
1968 Use "UUID" to include primitive UUIDs as a column.
1969 The order determines the column order in the output file.
1970 UUIDs: Optional list of primitive UUIDs to include. If None, includes all primitives.
1971 print_header: If True, writes column labels as the first line of the file
1974 ValueError: If filename is invalid, column_labels is empty, or UUIDs list is empty when provided
1975 HeliosFileIOError: If file cannot be written
1976 HeliosRuntimeError: If a column label doesn't exist for any primitive
1979 >>> # Write temperature and area for all primitives
1980 >>> context.writePrimitiveData("output.txt", ["UUID", "temperature", "area"])
1982 >>> # Write with header row
1983 >>> context.writePrimitiveData("output.txt", ["UUID", "radiation_flux"], print_header=True)
1985 >>> # Write only for selected primitives
1986 >>> context.writePrimitiveData("subset.txt", ["temperature"], UUIDs=[uuid1, uuid2])
1991 if not column_labels:
1992 raise ValueError(
"column_labels list cannot be empty")
1999 context_wrapper.writePrimitiveData(self.
context, validated_filename, column_labels, print_header)
2003 raise ValueError(
"UUIDs list cannot be empty when provided. Use UUIDs=None to include all primitives")
2009 context_wrapper.writePrimitiveDataWithUUIDs(self.
context, validated_filename, column_labels, UUIDs, print_header)
2012 colors: Optional[np.ndarray] =
None) -> List[int]:
2014 Add triangles from NumPy arrays (compatible with trimesh, Open3D format).
2017 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2018 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2019 colors: Optional NumPy array of shape (N, 3) or (M, 3) containing RGB colors as float32/float64
2020 If shape (N, 3): per-vertex colors
2021 If shape (M, 3): per-triangle colors
2024 List of UUIDs for the added triangles
2027 ValueError: If array dimensions are invalid
2030 if vertices.ndim != 2
or vertices.shape[1] != 3:
2031 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2032 if faces.ndim != 2
or faces.shape[1] != 3:
2033 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2036 max_vertex_index = np.max(faces)
2037 if max_vertex_index >= vertices.shape[0]:
2038 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2041 per_vertex_colors =
False
2042 per_triangle_colors =
False
2043 if colors
is not None:
2044 if colors.ndim != 2
or colors.shape[1] != 3:
2045 raise ValueError(f
"Colors array must have shape (N, 3) or (M, 3), got {colors.shape}")
2046 if colors.shape[0] == vertices.shape[0]:
2047 per_vertex_colors =
True
2048 elif colors.shape[0] == faces.shape[0]:
2049 per_triangle_colors =
True
2051 raise ValueError(f
"Colors array shape {colors.shape} doesn't match vertices ({vertices.shape[0]},) or faces ({faces.shape[0]},)")
2054 vertices_float = vertices.astype(np.float32)
2055 faces_int = faces.astype(np.int32)
2056 if colors
is not None:
2057 colors_float = colors.astype(np.float32)
2061 for i
in range(faces.shape[0]):
2063 v0_idx, v1_idx, v2_idx = faces_int[i]
2066 vertex0 = vertices_float[v0_idx].tolist()
2067 vertex1 = vertices_float[v1_idx].tolist()
2068 vertex2 = vertices_float[v2_idx].tolist()
2073 uuid = context_wrapper.addTriangle(self.
context, vertex0, vertex1, vertex2)
2074 elif per_triangle_colors:
2076 color = colors_float[i].tolist()
2077 uuid = context_wrapper.addTriangleWithColor(self.
context, vertex0, vertex1, vertex2, color)
2078 elif per_vertex_colors:
2080 color = np.mean([colors_float[v0_idx], colors_float[v1_idx], colors_float[v2_idx]], axis=0).tolist()
2081 uuid = context_wrapper.addTriangleWithColor(self.
context, vertex0, vertex1, vertex2, color)
2083 triangle_uuids.append(uuid)
2085 return triangle_uuids
2088 uv_coords: np.ndarray, texture_files: Union[str, List[str]],
2089 material_ids: Optional[np.ndarray] =
None) -> List[int]:
2091 Add textured triangles from NumPy arrays with support for multiple textures.
2093 This method supports both single-texture and multi-texture workflows:
2094 - Single texture: Pass a single texture file string, all faces use the same texture
2095 - Multiple textures: Pass a list of texture files and material_ids array specifying which texture each face uses
2098 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2099 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2100 uv_coords: NumPy array of shape (N, 2) containing UV texture coordinates as float32/float64
2101 texture_files: Single texture file path (str) or list of texture file paths (List[str])
2102 material_ids: Optional NumPy array of shape (M,) containing material ID for each face.
2103 If None and texture_files is a list, all faces use texture 0.
2104 If None and texture_files is a string, this parameter is ignored.
2107 List of UUIDs for the added textured triangles
2110 ValueError: If array dimensions are invalid or material IDs are out of range
2113 # Single texture usage (backward compatible)
2114 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, "texture.png")
2116 # Multi-texture usage (Open3D style)
2117 >>> texture_files = ["wood.png", "metal.png", "glass.png"]
2118 >>> material_ids = np.array([0, 0, 1, 1, 2, 2]) # 6 faces using different textures
2119 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, texture_files, material_ids)
2124 if vertices.ndim != 2
or vertices.shape[1] != 3:
2125 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2126 if faces.ndim != 2
or faces.shape[1] != 3:
2127 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2128 if uv_coords.ndim != 2
or uv_coords.shape[1] != 2:
2129 raise ValueError(f
"UV coordinates array must have shape (N, 2), got {uv_coords.shape}")
2132 if uv_coords.shape[0] != vertices.shape[0]:
2133 raise ValueError(f
"UV coordinates count ({uv_coords.shape[0]}) must match vertices count ({vertices.shape[0]})")
2136 max_vertex_index = np.max(faces)
2137 if max_vertex_index >= vertices.shape[0]:
2138 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2141 if isinstance(texture_files, str):
2143 texture_file_list = [texture_files]
2144 if material_ids
is None:
2145 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2148 if not np.all(material_ids == 0):
2149 raise ValueError(
"When using single texture file, all material IDs must be 0")
2152 texture_file_list = list(texture_files)
2153 if len(texture_file_list) == 0:
2154 raise ValueError(
"Texture files list cannot be empty")
2156 if material_ids
is None:
2158 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2161 if material_ids.ndim != 1
or material_ids.shape[0] != faces.shape[0]:
2162 raise ValueError(f
"Material IDs must have shape ({faces.shape[0]},), got {material_ids.shape}")
2165 max_material_id = np.max(material_ids)
2166 if max_material_id >= len(texture_file_list):
2167 raise ValueError(f
"Material ID {max_material_id} exceeds texture count {len(texture_file_list)}")
2170 for i, texture_file
in enumerate(texture_file_list):
2173 except (FileNotFoundError, ValueError)
as e:
2174 raise ValueError(f
"Texture file {i} ({texture_file}): {e}")
2177 if 'addTrianglesFromArraysMultiTextured' in context_wrapper._AVAILABLE_TRIANGLE_FUNCTIONS:
2178 return context_wrapper.addTrianglesFromArraysMultiTextured(
2179 self.
context, vertices, faces, uv_coords, texture_file_list, material_ids
2183 from .wrappers.DataTypes
import vec3, vec2
2185 vertices_float = vertices.astype(np.float32)
2186 faces_int = faces.astype(np.int32)
2187 uv_coords_float = uv_coords.astype(np.float32)
2190 for i
in range(faces.shape[0]):
2192 v0_idx, v1_idx, v2_idx = faces_int[i]
2195 vertex0 =
vec3(vertices_float[v0_idx][0], vertices_float[v0_idx][1], vertices_float[v0_idx][2])
2196 vertex1 =
vec3(vertices_float[v1_idx][0], vertices_float[v1_idx][1], vertices_float[v1_idx][2])
2197 vertex2 =
vec3(vertices_float[v2_idx][0], vertices_float[v2_idx][1], vertices_float[v2_idx][2])
2200 uv0 =
vec2(uv_coords_float[v0_idx][0], uv_coords_float[v0_idx][1])
2201 uv1 =
vec2(uv_coords_float[v1_idx][0], uv_coords_float[v1_idx][1])
2202 uv2 =
vec2(uv_coords_float[v2_idx][0], uv_coords_float[v2_idx][1])
2205 material_id = material_ids[i]
2206 texture_file = texture_file_list[material_id]
2210 triangle_uuids.append(uuid)
2212 return triangle_uuids
2220 Set primitive data as signed 32-bit integer for one or multiple primitives.
2223 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2224 label: String key for the data
2225 value: Signed integer value (broadcast to all UUIDs if list provided)
2227 if isinstance(uuids_or_uuid, (list, tuple)):
2228 context_wrapper.setBroadcastPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2230 context_wrapper.setPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2234 Set primitive data as unsigned 32-bit integer for one or multiple primitives.
2236 Critical for properties like 'twosided_flag' which must be uint in C++.
2239 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2240 label: String key for the data
2241 value: Unsigned integer value (broadcast to all UUIDs if list provided)
2243 if isinstance(uuids_or_uuid, (list, tuple)):
2244 context_wrapper.setBroadcastPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2246 context_wrapper.setPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2250 Set primitive data as 32-bit float for one or multiple primitives.
2253 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2254 label: String key for the data
2255 value: Float value (broadcast to all UUIDs if list provided)
2257 if isinstance(uuids_or_uuid, (list, tuple)):
2258 context_wrapper.setBroadcastPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2260 context_wrapper.setPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2264 Set primitive data as 64-bit double 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: Double value (broadcast to all UUIDs if list provided)
2271 if isinstance(uuids_or_uuid, (list, tuple)):
2272 context_wrapper.setBroadcastPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2274 context_wrapper.setPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2278 Set primitive data as string 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: String value (broadcast to all UUIDs if list provided)
2285 if isinstance(uuids_or_uuid, (list, tuple)):
2286 context_wrapper.setBroadcastPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2288 context_wrapper.setPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2292 Set primitive data as vec2 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 x_or_vec: Either x component (float) or vec2 object
2298 y: Y component (if x_or_vec is float)
2300 if hasattr(x_or_vec,
'x'):
2301 x, y = x_or_vec.x, x_or_vec.y
2304 if isinstance(uuids_or_uuid, (list, tuple)):
2305 context_wrapper.setBroadcastPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2307 context_wrapper.setPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2309 def setPrimitiveDataVec3(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
2311 Set primitive data as vec3 for one or multiple primitives.
2314 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2315 label: String key for the data
2316 x_or_vec: Either x component (float) or vec3 object
2317 y: Y component (if x_or_vec is float)
2318 z: Z component (if x_or_vec is float)
2320 if hasattr(x_or_vec,
'x'):
2321 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2324 if isinstance(uuids_or_uuid, (list, tuple)):
2325 context_wrapper.setBroadcastPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2327 context_wrapper.setPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2329 def setPrimitiveDataVec4(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
2331 Set primitive data as vec4 for one or multiple primitives.
2334 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2335 label: String key for the data
2336 x_or_vec: Either x component (float) or vec4 object
2337 y: Y component (if x_or_vec is float)
2338 z: Z component (if x_or_vec is float)
2339 w: W component (if x_or_vec is float)
2341 if hasattr(x_or_vec,
'x'):
2342 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2345 if isinstance(uuids_or_uuid, (list, tuple)):
2346 context_wrapper.setBroadcastPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
2348 context_wrapper.setPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
2352 Set primitive data as int2 for one or multiple primitives.
2355 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2356 label: String key for the data
2357 x_or_vec: Either x component (int) or int2 object
2358 y: Y component (if x_or_vec is int)
2360 if hasattr(x_or_vec,
'x'):
2361 x, y = x_or_vec.x, x_or_vec.y
2364 if isinstance(uuids_or_uuid, (list, tuple)):
2365 context_wrapper.setBroadcastPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
2367 context_wrapper.setPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
2369 def setPrimitiveDataInt3(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
2371 Set primitive data as int3 for one or multiple primitives.
2374 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2375 label: String key for the data
2376 x_or_vec: Either x component (int) or int3 object
2377 y: Y component (if x_or_vec is int)
2378 z: Z component (if x_or_vec is int)
2380 if hasattr(x_or_vec,
'x'):
2381 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2384 if isinstance(uuids_or_uuid, (list, tuple)):
2385 context_wrapper.setBroadcastPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
2387 context_wrapper.setPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
2389 def setPrimitiveDataInt4(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
2391 Set primitive data as int4 for one or multiple primitives.
2394 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2395 label: String key for the data
2396 x_or_vec: Either x component (int) or int4 object
2397 y: Y component (if x_or_vec is int)
2398 z: Z component (if x_or_vec is int)
2399 w: W component (if x_or_vec is int)
2401 if hasattr(x_or_vec,
'x'):
2402 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2405 if isinstance(uuids_or_uuid, (list, tuple)):
2406 context_wrapper.setBroadcastPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
2408 context_wrapper.setPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
2412 Get primitive data for a specific primitive. If data_type is provided, it works like before.
2413 If data_type is None, it automatically detects the type and returns the appropriate value.
2416 uuid: UUID of the primitive
2417 label: String key for the data
2418 data_type: Optional. Python type to retrieve (int, uint, float, double, bool, str, vec2, vec3, vec4, int2, int3, int4, etc.)
2419 If None, auto-detects the type using C++ getPrimitiveDataType().
2422 The stored value of the specified or auto-detected type
2425 if data_type
is None:
2426 return context_wrapper.getPrimitiveDataAuto(self.
context, uuid, label)
2429 if data_type == int:
2430 return context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
2431 elif data_type == float:
2432 return context_wrapper.getPrimitiveDataFloat(self.
context, uuid, label)
2433 elif data_type == bool:
2435 int_value = context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
2436 return int_value != 0
2437 elif data_type == str:
2438 return context_wrapper.getPrimitiveDataString(self.
context, uuid, label)
2441 elif data_type == vec2:
2442 coords = context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
2443 return vec2(coords[0], coords[1])
2444 elif data_type == vec3:
2445 coords = context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
2446 return vec3(coords[0], coords[1], coords[2])
2447 elif data_type == vec4:
2448 coords = context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
2449 return vec4(coords[0], coords[1], coords[2], coords[3])
2450 elif data_type == int2:
2451 coords = context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
2452 return int2(coords[0], coords[1])
2453 elif data_type == int3:
2454 coords = context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
2455 return int3(coords[0], coords[1], coords[2])
2456 elif data_type == int4:
2457 coords = context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
2458 return int4(coords[0], coords[1], coords[2], coords[3])
2461 elif data_type ==
"uint":
2462 return context_wrapper.getPrimitiveDataUInt(self.
context, uuid, label)
2463 elif data_type ==
"double":
2464 return context_wrapper.getPrimitiveDataDouble(self.
context, uuid, label)
2467 elif data_type == list:
2469 return context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
2470 elif data_type ==
"list_vec2":
2471 return context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
2472 elif data_type ==
"list_vec4":
2473 return context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
2474 elif data_type ==
"list_int2":
2475 return context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
2476 elif data_type ==
"list_int3":
2477 return context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
2478 elif data_type ==
"list_int4":
2479 return context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
2482 raise ValueError(f
"Unsupported primitive data type: {data_type}. "
2483 f
"Supported types: int, float, bool, str, vec2, vec3, vec4, int2, int3, int4, "
2484 f
"'uint', 'double', list (for vec3), 'list_vec2', 'list_vec4', 'list_int2', 'list_int3', 'list_int4'")
2488 Check if primitive data exists for a specific primitive and label.
2491 uuid: UUID of the primitive
2492 label: String key for the data
2495 True if the data exists, False otherwise
2497 return context_wrapper.doesPrimitiveDataExistWrapper(self.
context, uuid, label)
2501 Convenience method to get float primitive data.
2504 uuid: UUID of the primitive
2505 label: String key for the data
2508 Float value stored for the primitive
2514 Get the Helios data type of primitive data.
2517 uuid: UUID of the primitive
2518 label: String key for the data
2521 HeliosDataType enum value as integer
2523 return context_wrapper.getPrimitiveDataTypeWrapper(self.
context, uuid, label)
2527 Get the size/length of primitive data (for vector data).
2530 uuid: UUID of the primitive
2531 label: String key for the data
2534 Size of data array, or 1 for scalar data
2536 return context_wrapper.getPrimitiveDataSizeWrapper(self.
context, uuid, label)
2540 Get primitive data values for multiple primitives as a NumPy array.
2542 This method retrieves primitive data for a list of UUIDs and returns the values
2543 as a NumPy array. The output array has the same length as the input UUID list,
2544 with each index corresponding to the primitive data value for that UUID.
2547 uuids: List of primitive UUIDs to get data for
2548 label: String key for the primitive data to retrieve
2551 NumPy array of primitive data values corresponding to each UUID.
2552 The array type depends on the data type:
2553 - int data: int32 array
2554 - uint data: uint32 array
2555 - float data: float32 array
2556 - double data: float64 array
2557 - vector data: float32 array with shape (N, vector_size)
2558 - string data: object array of strings
2561 ValueError: If UUID list is empty or UUIDs don't exist
2562 RuntimeError: If context is in mock mode or data doesn't exist for some UUIDs
2567 raise ValueError(
"UUID list cannot be empty")
2576 raise ValueError(f
"Primitive data '{label}' does not exist for UUID {uuid}")
2579 first_uuid = uuids[0]
2585 result = np.empty(len(uuids), dtype=np.int32)
2586 for i, uuid
in enumerate(uuids):
2589 elif data_type == 1:
2590 result = np.empty(len(uuids), dtype=np.uint32)
2591 for i, uuid
in enumerate(uuids):
2594 elif data_type == 2:
2595 result = np.empty(len(uuids), dtype=np.float32)
2596 for i, uuid
in enumerate(uuids):
2599 elif data_type == 3:
2600 result = np.empty(len(uuids), dtype=np.float64)
2601 for i, uuid
in enumerate(uuids):
2604 elif data_type == 4:
2605 result = np.empty((len(uuids), 2), dtype=np.float32)
2606 for i, uuid
in enumerate(uuids):
2608 result[i] = [vec_data.x, vec_data.y]
2610 elif data_type == 5:
2611 result = np.empty((len(uuids), 3), dtype=np.float32)
2612 for i, uuid
in enumerate(uuids):
2614 result[i] = [vec_data.x, vec_data.y, vec_data.z]
2616 elif data_type == 6:
2617 result = np.empty((len(uuids), 4), dtype=np.float32)
2618 for i, uuid
in enumerate(uuids):
2620 result[i] = [vec_data.x, vec_data.y, vec_data.z, vec_data.w]
2622 elif data_type == 7:
2623 result = np.empty((len(uuids), 2), dtype=np.int32)
2624 for i, uuid
in enumerate(uuids):
2626 result[i] = [int_data.x, int_data.y]
2628 elif data_type == 8:
2629 result = np.empty((len(uuids), 3), dtype=np.int32)
2630 for i, uuid
in enumerate(uuids):
2632 result[i] = [int_data.x, int_data.y, int_data.z]
2634 elif data_type == 9:
2635 result = np.empty((len(uuids), 4), dtype=np.int32)
2636 for i, uuid
in enumerate(uuids):
2638 result[i] = [int_data.x, int_data.y, int_data.z, int_data.w]
2640 elif data_type == 10:
2641 result = np.empty(len(uuids), dtype=object)
2642 for i, uuid
in enumerate(uuids):
2646 raise ValueError(f
"Unsupported primitive data type: {data_type}")
2652 colormap: str =
"hot", ncolors: int = 10,
2653 max_val: Optional[float] =
None, min_val: Optional[float] =
None):
2655 Color primitives based on primitive data values using pseudocolor mapping.
2657 This method applies a pseudocolor mapping to primitives based on the values
2658 of specified primitive data. The primitive colors are updated to reflect the
2659 data values using a color map.
2662 uuids: List of primitive UUIDs to color
2663 primitive_data: Name of primitive data to use for coloring (e.g., "radiation_flux_SW")
2664 colormap: Color map name - options include "hot", "cool", "parula", "rainbow", "gray", "lava"
2665 ncolors: Number of discrete colors in color map (default: 10)
2666 max_val: Maximum value for color scale (auto-determined if None)
2667 min_val: Minimum value for color scale (auto-determined if None)
2669 if max_val
is not None and min_val
is not None:
2670 context_wrapper.colorPrimitiveByDataPseudocolorWithRange(
2671 self.
context, uuids, primitive_data, colormap, ncolors, max_val, min_val)
2673 context_wrapper.colorPrimitiveByDataPseudocolor(
2674 self.
context, uuids, primitive_data, colormap, ncolors)
2677 def setTime(self, hour: int, minute: int = 0, second: int = 0):
2679 Set the simulation time.
2683 minute: Minute (0-59), defaults to 0
2684 second: Second (0-59), defaults to 0
2687 ValueError: If time values are out of range
2688 NotImplementedError: If time/date functions not available in current library build
2691 >>> context.setTime(14, 30) # Set to 2:30 PM
2692 >>> context.setTime(9, 15, 30) # Set to 9:15:30 AM
2694 context_wrapper.setTime(self.
context, hour, minute, second)
2696 def setDate(self, year: int, month: int, day: int):
2698 Set the simulation date.
2701 year: Year (1900-3000)
2706 ValueError: If date values are out of range
2707 NotImplementedError: If time/date functions not available in current library build
2710 >>> context.setDate(2023, 6, 21) # Set to June 21, 2023
2712 context_wrapper.setDate(self.
context, year, month, day)
2716 Set the simulation date using Julian day number.
2719 julian_day: Julian day (1-366)
2720 year: Year (1900-3000)
2723 ValueError: If values are out of range
2724 NotImplementedError: If time/date functions not available in current library build
2727 >>> context.setDateJulian(172, 2023) # Set to day 172 of 2023 (June 21)
2729 context_wrapper.setDateJulian(self.
context, julian_day, year)
2733 Get the current simulation time.
2736 Tuple of (hour, minute, second) as integers
2739 NotImplementedError: If time/date functions not available in current library build
2742 >>> hour, minute, second = context.getTime()
2743 >>> print(f"Current time: {hour:02d}:{minute:02d}:{second:02d}")
2745 return context_wrapper.getTime(self.
context)
2749 Get the current simulation date.
2752 Tuple of (year, month, day) as integers
2755 NotImplementedError: If time/date functions not available in current library build
2758 >>> year, month, day = context.getDate()
2759 >>> print(f"Current date: {year}-{month:02d}-{day:02d}")
2761 return context_wrapper.getDate(self.
context)
2767 def deletePrimitive(self, uuids_or_uuid: Union[int, List[int]]) ->
None:
2769 Delete one or more primitives from the context.
2771 This removes the primitive(s) entirely from the context. If a primitive
2772 belongs to a compound object, it will be removed from that object. If the
2773 object becomes empty after removal, it is automatically deleted.
2776 uuids_or_uuid: Single UUID (int) or list of UUIDs to delete
2779 RuntimeError: If any UUID doesn't exist in the context
2780 ValueError: If UUID is invalid (negative)
2781 NotImplementedError: If delete functions not available in current library build
2784 >>> context = Context()
2785 >>> patch_id = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
2786 >>> context.deletePrimitive(patch_id) # Single deletion
2788 >>> # Multiple deletion
2789 >>> ids = [context.addPatch() for _ in range(5)]
2790 >>> context.deletePrimitive(ids) # Delete all at once
2794 if isinstance(uuids_or_uuid, (list, tuple)):
2795 for uuid
in uuids_or_uuid:
2797 raise ValueError(f
"UUID must be non-negative, got {uuid}")
2798 context_wrapper.deletePrimitives(self.
context, list(uuids_or_uuid))
2800 if uuids_or_uuid < 0:
2801 raise ValueError(f
"UUID must be non-negative, got {uuids_or_uuid}")
2802 context_wrapper.deletePrimitive(self.
context, uuids_or_uuid)
2804 def deleteObject(self, objIDs_or_objID: Union[int, List[int]]) ->
None:
2806 Delete one or more compound objects from the context.
2808 This removes the compound object(s) AND all their child primitives.
2809 Use this when you want to delete an entire object hierarchy at once.
2812 objIDs_or_objID: Single object ID (int) or list of object IDs to delete
2815 RuntimeError: If any object ID doesn't exist in the context
2816 ValueError: If object ID is invalid (negative)
2817 NotImplementedError: If delete functions not available in current library build
2820 >>> context = Context()
2821 >>> # Create a compound object (e.g., a tile with multiple patches)
2822 >>> patch_ids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2),
2823 ... tile_divisions=int2(2, 2))
2824 >>> obj_id = context.getPrimitiveParentObjectID(patch_ids[0])
2825 >>> context.deleteObject(obj_id) # Deletes tile and all its patches
2829 if isinstance(objIDs_or_objID, (list, tuple)):
2830 for objID
in objIDs_or_objID:
2832 raise ValueError(f
"Object ID must be non-negative, got {objID}")
2833 context_wrapper.deleteObjects(self.
context, list(objIDs_or_objID))
2835 if objIDs_or_objID < 0:
2836 raise ValueError(f
"Object ID must be non-negative, got {objIDs_or_objID}")
2837 context_wrapper.deleteObject(self.
context, objIDs_or_objID)
2842 Get list of available plugins for this PyHelios instance.
2845 List of available plugin names
2851 Check if a specific plugin is available.
2854 plugin_name: Name of the plugin to check
2857 True if plugin is available, False otherwise
2863 Get detailed information about available plugin capabilities.
2866 Dictionary mapping plugin names to capability information
2871 """Print detailed plugin status information."""
2876 Get list of requested plugins that are not available.
2879 requested_plugins: List of plugin names to check
2882 List of missing plugin names
2892 Create a new material for sharing visual properties across primitives.
2894 Materials enable efficient memory usage by allowing multiple primitives to
2895 share rendering properties. Changes to a material affect all primitives using it.
2898 material_label: Unique label for the material
2901 RuntimeError: If material label already exists
2904 >>> context.addMaterial("wood_oak")
2905 >>> context.setMaterialColor("wood_oak", (0.6, 0.4, 0.2, 1.0))
2906 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
2908 context_wrapper.addMaterial(self.
context, material_label)
2911 """Check if a material with the given label exists."""
2912 return context_wrapper.doesMaterialExist(self.
context, material_label)
2915 """Get list of all material labels in the context."""
2916 return context_wrapper.listMaterials(self.
context)
2920 Delete a material from the context.
2922 Primitives using this material will be reassigned to the default material.
2925 material_label: Label of the material to delete
2928 RuntimeError: If material doesn't exist
2930 context_wrapper.deleteMaterial(self.
context, material_label)
2934 Get the RGBA color of a material.
2937 material_label: Label of the material
2943 RuntimeError: If material doesn't exist
2945 from .wrappers.DataTypes
import RGBAcolor
2946 color_list = context_wrapper.getMaterialColor(self.
context, material_label)
2947 return RGBAcolor(color_list[0], color_list[1], color_list[2], color_list[3])
2951 Set the RGBA color of a material.
2953 This affects all primitives that reference this material.
2956 material_label: Label of the material
2957 color: RGBAcolor object or tuple/list of (r, g, b, a) values
2960 RuntimeError: If material doesn't exist
2963 >>> from pyhelios.types import RGBAcolor
2964 >>> context.setMaterialColor("wood", RGBAcolor(0.6, 0.4, 0.2, 1.0))
2965 >>> context.setMaterialColor("wood", (0.6, 0.4, 0.2, 1.0))
2967 if hasattr(color,
'r'):
2968 r, g, b, a = color.r, color.g, color.b, color.a
2970 r, g, b, a = color[0], color[1], color[2], color[3]
2971 context_wrapper.setMaterialColor(self.
context, material_label, r, g, b, a)
2975 Get the texture file path for a material.
2978 material_label: Label of the material
2981 Texture file path, or empty string if no texture
2984 RuntimeError: If material doesn't exist
2986 return context_wrapper.getMaterialTexture(self.
context, material_label)
2990 Set the texture file for a material.
2992 This affects all primitives that reference this material.
2995 material_label: Label of the material
2996 texture_file: Path to texture image file
2999 RuntimeError: If material doesn't exist or texture file not found
3001 context_wrapper.setMaterialTexture(self.
context, material_label, texture_file)
3004 """Check if material texture color is overridden by material color."""
3005 return context_wrapper.isMaterialTextureColorOverridden(self.
context, material_label)
3008 """Set whether material color overrides texture color."""
3009 context_wrapper.setMaterialTextureColorOverride(self.
context, material_label, override)
3012 """Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3013 return context_wrapper.getMaterialTwosidedFlag(self.
context, material_label)
3016 """Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3017 context_wrapper.setMaterialTwosidedFlag(self.
context, material_label, twosided_flag)
3021 Assign a material to primitive(s).
3024 uuid: Single UUID (int) or list of UUIDs (List[int])
3025 material_label: Label of the material to assign
3028 RuntimeError: If primitive or material doesn't exist
3031 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3032 >>> context.assignMaterialToPrimitive([uuid1, uuid2, uuid3], "wood_oak")
3034 if isinstance(uuid, (list, tuple)):
3035 context_wrapper.assignMaterialToPrimitives(self.
context, uuid, material_label)
3037 context_wrapper.assignMaterialToPrimitive(self.
context, uuid, material_label)
3041 Assign a material to all primitives in compound object(s).
3044 objID: Single object ID (int) or list of object IDs (List[int])
3045 material_label: Label of the material to assign
3048 RuntimeError: If object or material doesn't exist
3051 >>> tree_id = wpt.buildTree(WPTType.LEMON)
3052 >>> context.assignMaterialToObject(tree_id, "tree_bark")
3053 >>> context.assignMaterialToObject([id1, id2], "grass")
3055 if isinstance(objID, (list, tuple)):
3056 context_wrapper.assignMaterialToObjects(self.
context, objID, material_label)
3058 context_wrapper.assignMaterialToObject(self.
context, objID, material_label)
3062 Get the material label assigned to a primitive.
3065 uuid: UUID of the primitive
3068 Material label, or empty string if no material assigned
3071 RuntimeError: If primitive doesn't exist
3073 return context_wrapper.getPrimitiveMaterialLabel(self.
context, uuid)
3077 Get two-sided rendering flag for a primitive.
3079 Checks material first, then primitive data if no material assigned.
3082 uuid: UUID of the primitive
3083 default_value: Default value if no material/data (default 1 = two-sided)
3086 Two-sided flag (0 = one-sided, 1 = two-sided)
3088 return context_wrapper.getPrimitiveTwosidedFlag(self.
context, uuid, default_value)
3092 Get all primitive UUIDs that use a specific material.
3095 material_label: Label of the material
3098 List of primitive UUIDs using the material
3101 RuntimeError: If material doesn't exist
3103 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.
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.