3from dataclasses
import dataclass
4from typing
import List, Optional, Union
9from .wrappers
import UContextWrapper
as context_wrapper
10from .wrappers.DataTypes
import vec2, vec3, vec4, int2, int3, int4, SphericalCoord, RGBcolor, RGBAcolor, PrimitiveType, Date, Time, Location, AdaptiveTileRefinement, VertexNormalSource, VertexWeldMode
11from .exceptions
import HeliosError
12from .plugins.loader
import LibraryLoadError, validate_library, get_library_info
13from .plugins.registry
import get_plugin_registry
14from .validation.geometry
import (
15 validate_patch_params, validate_triangle_params, validate_sphere_params,
16 validate_tube_params, validate_box_params
21_BULK_PRIMITIVE_DATA_TYPES = frozenset({0, 1, 3, 4, 5, 6, 7, 8, 9})
27 Physical properties and geometry information for a primitive.
28 This is separate from primitive data (user-defined key-value pairs).
31 primitive_type: PrimitiveType
36 centroid: Optional[vec3] =
None
37 texture_file: Optional[str] =
None
38 texture_uv: Optional[List[vec2]] =
None
39 solid_fraction: Optional[float] =
None
42 """Calculate centroid from vertices if not provided."""
46 total_y = sum(v.y
for v
in self.
vertices)
47 total_z = sum(v.z
for v
in self.
vertices)
49 self.
centroid =
vec3(total_x / count, total_y / count, total_z / count)
54 Central simulation environment for PyHelios that manages 3D primitives and their data.
56 The Context class provides methods for:
57 - Creating geometric primitives (patches, triangles)
58 - Creating compound geometry (tiles, spheres, tubes, boxes)
59 - Loading 3D models from files (PLY, OBJ, XML)
60 - Managing primitive data (flexible key-value storage)
61 - Querying primitive properties and collections
62 - Batch operations on multiple primitives
65 - UUID-based primitive tracking
66 - Comprehensive primitive data system with auto-type detection
67 - Efficient array-based data retrieval via getPrimitiveDataArray()
68 - Cross-platform compatibility with mock mode support
69 - Context manager protocol for resource cleanup
72 >>> with Context() as context:
73 ... # Create primitives
74 ... patch_uuid = context.addPatch(center=vec3(0, 0, 0))
75 ... triangle_uuid = context.addTriangle(vec3(0,0,0), vec3(1,0,0), vec3(0.5,1,0))
77 ... # Set primitive data
78 ... context.setPrimitiveDataFloat(patch_uuid, "temperature", 25.5)
79 ... context.setPrimitiveDataFloat(triangle_uuid, "temperature", 30.2)
81 ... # Get data efficiently as NumPy array
82 ... temps = context.getPrimitiveDataArray([patch_uuid, triangle_uuid], "temperature")
83 ... print(temps) # [25.5 30.2]
94 library_info = get_library_info()
95 if library_info.get(
'is_mock',
False):
97 print(
"Warning: PyHelios running in development mock mode - functionality is limited")
98 print(
"Available plugins: None (mock mode)")
105 if not validate_library():
106 raise LibraryLoadError(
107 "Native Helios library validation failed. Some required functions are missing. "
108 "Try rebuilding the native library: build_scripts/build_helios"
110 except LibraryLoadError:
112 except Exception
as e:
113 raise LibraryLoadError(
114 f
"Failed to validate native Helios library: {e}. "
115 f
"To enable development mode without native libraries, set PYHELIOS_DEV_MODE=1"
120 self.
context = context_wrapper.createContext()
123 raise LibraryLoadError(
124 "Failed to create Helios context. Native library may not be functioning correctly."
129 except Exception
as e:
131 raise LibraryLoadError(
132 f
"Failed to create Helios context: {e}. "
133 f
"Ensure native libraries are built and accessible."
137 """Helper method to check if context is available with detailed error messages."""
142 "Context is in mock mode - native functionality not available.\n"
143 "Build native libraries with 'python build_scripts/build_helios.py' or set PYHELIOS_DEV_MODE=1 for development."
147 "Context has been cleaned up and is no longer usable.\n"
148 "This usually means you're trying to use a Context outside its 'with' statement scope.\n"
150 "Fix: Ensure all Context usage is inside the 'with Context() as context:' block:\n"
151 " with Context() as context:\n"
152 " # All context operations must be here\n"
153 " with SomePlugin(context) as plugin:\n"
154 " plugin.do_something()\n"
155 " with Visualizer() as vis:\n"
156 " vis.buildContextGeometry(context) # Still inside Context scope\n"
157 " # Context is cleaned up here - cannot use context after this point"
161 "Context creation failed - native functionality not available.\n"
162 "Build native libraries with 'python build_scripts/build_helios.py'"
167 f
"Context is not available (state: {self._lifecycle_state}).\n"
168 "Build native libraries with 'python build_scripts/build_helios.py' or set PYHELIOS_DEV_MODE=1 for development."
172 """Validate that a UUID exists in this context.
175 uuid: The UUID to validate
178 RuntimeError: If UUID is invalid or doesn't exist in context
183 """Validate that every UUID in ``uuids`` exists in this context.
185 Use this for any UUID list rather than calling :meth:`_validate_uuid` in a
186 loop. Existence is checked against the context's UUID list, which must be
187 fetched from the native layer and searched; doing that per element makes
188 validating N UUIDs O(N^2) with N native round-trips. At 20,000 primitives
189 that cost ~15 s, swamping the work being validated. Fetching once and
190 testing set membership makes it linear.
192 Checks run per element in order -- type first, then existence -- so the
193 error raised for a given list is the same one the per-element loop raised.
194 The context's UUID list is fetched lazily, on the first element that needs
195 it, and reused for the rest of the call.
198 uuids: Iterable of UUIDs to validate
201 RuntimeError: If any UUID is invalid or doesn't exist in context
209 if not isinstance(uuid, int)
or uuid < 0:
210 raise RuntimeError(f
"Invalid UUID: {uuid}. UUIDs must be non-negative integers.")
218 valid_set = set(valid_uuids)
227 if valid_set
is not None and uuid
not in valid_set:
228 raise RuntimeError(f
"UUID {uuid} does not exist in context. Valid UUIDs: {valid_uuids[:10]}{'...' if len(valid_uuids) > 10 else ''}")
231 def _validate_file_path(self, filename: str, expected_extensions: List[str] =
None) -> str:
232 """Validate and normalize file path for security.
235 filename: File path to validate
236 expected_extensions: List of allowed file extensions (e.g., ['.ply', '.obj'])
239 Normalized absolute path
243 ValueError: If path is invalid or potentially dangerous
244 FileNotFoundError: If file does not exist
249 abs_path = os.path.abspath(filename)
253 normalized_path = os.path.normpath(abs_path)
254 if abs_path != normalized_path:
255 raise ValueError(f
"Invalid file path (potential path traversal): {filename}")
258 if expected_extensions:
259 file_ext = os.path.splitext(abs_path)[1].lower()
260 if file_ext
not in [ext.lower()
for ext
in expected_extensions]:
261 raise ValueError(f
"Invalid file extension '{file_ext}'. Expected one of: {expected_extensions}")
264 if not os.path.exists(abs_path):
265 raise FileNotFoundError(f
"File not found: {abs_path}")
268 if not os.path.isfile(abs_path):
269 raise ValueError(f
"Path is not a file: {abs_path}")
274 """Validate and normalize output file path for security.
277 filename: Output file path to validate
278 expected_extensions: List of allowed file extensions (e.g., ['.ply', '.obj'])
281 Normalized absolute path
284 ValueError: If path is invalid or potentially dangerous
285 PermissionError: If output directory is not writable
290 if not filename
or not filename.strip():
291 raise ValueError(
"Filename cannot be empty")
294 abs_path = os.path.abspath(filename)
297 normalized_path = os.path.normpath(abs_path)
298 if abs_path != normalized_path:
299 raise ValueError(f
"Invalid file path (potential path traversal): {filename}")
302 if expected_extensions:
303 file_ext = os.path.splitext(abs_path)[1].lower()
304 if file_ext
not in [ext.lower()
for ext
in expected_extensions]:
305 raise ValueError(f
"Invalid file extension '{file_ext}'. Expected one of: {expected_extensions}")
308 output_dir = os.path.dirname(abs_path)
309 if not os.path.exists(output_dir):
310 raise ValueError(f
"Output directory does not exist: {output_dir}")
311 if not os.access(output_dir, os.W_OK):
312 raise PermissionError(f
"Output directory is not writable: {output_dir}")
319 def __exit__(self, exc_type, exc_value, traceback):
321 context_wrapper.destroyContext(self.
context)
326 """Destructor to ensure C++ resources freed even without 'with' statement."""
327 if hasattr(self,
'context')
and self.
context is not None:
332 except Exception
as e:
340 warnings.warn(f
"Error in Context.__del__: {e}")
341 except BaseException:
350 context_wrapper.markGeometryClean(self.
context)
354 context_wrapper.markGeometryDirty(self.
context)
359 return context_wrapper.isGeometryDirty(self.
context)
363 Seed the random number generator for reproducible stochastic results.
366 seed: Integer seed value for random number generation
369 This is critical for reproducible results in stochastic simulations
370 (e.g., LiDAR scans with beam divergence, random perturbations).
373 context_wrapper.helios_lib.seedRandomGenerator(self.
context, seed)
375 @validate_patch_params
376 def addPatch(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1), rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None) -> int:
381 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
382 return context_wrapper.addPatchWithCenterSizeRotationAndColor(self.
context, center.to_list(), size.to_list(), rotation_list, color.to_list())
385 rotation: Optional[SphericalCoord] =
None,
386 uv_center: Optional[vec2] =
None,
387 uv_size: Optional[vec2] =
None) -> int:
388 """Add a textured patch primitive to the context.
390 Creates a rectangular patch with a texture image mapped to its surface.
393 center: 3D position of the patch center
394 size: Width and height of the patch
395 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
396 rotation: Optional spherical rotation (defaults to no rotation)
397 uv_center: Optional UV center of texture map (required if uv_size is provided)
398 uv_size: Optional UV size of texture map (required if uv_center is provided)
401 UUID of the created textured patch primitive
404 ValueError: If arguments have wrong types or UV params are partially specified
405 FileNotFoundError: If texture file doesn't exist
406 RuntimeError: If context is in mock mode
409 >>> context = Context()
410 >>> uuid = context.addPatchTextured(
411 ... center=vec3(0, 0, 0),
413 ... texture_file="texture.png"
418 if not isinstance(center, vec3):
419 raise ValueError(f
"center must be a vec3, got {type(center).__name__}")
420 if not isinstance(size, vec2):
421 raise ValueError(f
"size must be a vec2, got {type(size).__name__}")
422 if not isinstance(texture_file, str):
423 raise ValueError(f
"texture_file must be a str, got {type(texture_file).__name__}")
424 if rotation
is not None and not isinstance(rotation, SphericalCoord):
425 raise ValueError(f
"rotation must be a SphericalCoord, got {type(rotation).__name__}")
427 if (uv_center
is None) != (uv_size
is None):
428 raise ValueError(
"uv_center and uv_size must both be provided or both omitted")
429 if uv_center
is not None and not isinstance(uv_center, vec2):
430 raise ValueError(f
"uv_center must be a vec2, got {type(uv_center).__name__}")
431 if uv_size
is not None and not isinstance(uv_size, vec2):
432 raise ValueError(f
"uv_size must be a vec2, got {type(uv_size).__name__}")
435 [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp'])
438 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
440 if uv_center
is not None:
441 return context_wrapper.addPatchWithTextureAndUV(
442 self.
context, center.to_list(), size.to_list(), rotation_list,
443 validated_texture_file, uv_center.to_list(), uv_size.to_list()
446 return context_wrapper.addPatchWithTexture(
447 self.
context, center.to_list(), size.to_list(), rotation_list,
448 validated_texture_file
451 @validate_triangle_params
452 def addTriangle(self, vertex0: vec3, vertex1: vec3, vertex2: vec3, color: Optional[RGBcolor] =
None) -> int:
453 """Add a triangle primitive to the context
456 vertex0: First vertex of the triangle
457 vertex1: Second vertex of the triangle
458 vertex2: Third vertex of the triangle
459 color: Optional triangle color (defaults to white)
462 UUID of the created triangle primitive
466 return context_wrapper.addTriangle(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list())
468 return context_wrapper.addTriangleWithColor(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list(), color.to_list())
471 texture_file: str, uv0: vec2, uv1: vec2, uv2: vec2) -> int:
472 """Add a textured triangle primitive to the context
474 Creates a triangle with texture mapping. The texture image is mapped to the triangle
475 surface using UV coordinates, where (0,0) represents the top-left corner of the image
476 and (1,1) represents the bottom-right corner.
479 vertex0: First vertex of the triangle
480 vertex1: Second vertex of the triangle
481 vertex2: Third vertex of the triangle
482 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
483 uv0: UV texture coordinates for first vertex
484 uv1: UV texture coordinates for second vertex
485 uv2: UV texture coordinates for third vertex
488 UUID of the created textured triangle primitive
491 ValueError: If texture file path is invalid
492 FileNotFoundError: If texture file doesn't exist
493 RuntimeError: If context is in mock mode
496 >>> context = Context()
497 >>> # Create a textured triangle
498 >>> vertex0 = vec3(0, 0, 0)
499 >>> vertex1 = vec3(1, 0, 0)
500 >>> vertex2 = vec3(0.5, 1, 0)
501 >>> uv0 = vec2(0, 0) # Bottom-left of texture
502 >>> uv1 = vec2(1, 0) # Bottom-right of texture
503 >>> uv2 = vec2(0.5, 1) # Top-center of texture
504 >>> uuid = context.addTriangleTextured(vertex0, vertex1, vertex2,
505 ... "texture.png", uv0, uv1, uv2)
510 for name, val
in [(
"vertex0", vertex0), (
"vertex1", vertex1), (
"vertex2", vertex2)]:
511 if not isinstance(val, vec3):
512 raise ValueError(f
"{name} must be a vec3, got {type(val).__name__}")
513 for name, val
in [(
"uv0", uv0), (
"uv1", uv1), (
"uv2", uv2)]:
514 if not isinstance(val, vec2):
515 raise ValueError(f
"{name} must be a vec2, got {type(val).__name__}")
519 [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp'])
522 return context_wrapper.addTriangleWithTexture(
524 vertex0.to_list(), vertex1.to_list(), vertex2.to_list(),
525 validated_texture_file,
526 uv0.to_list(), uv1.to_list(), uv2.to_list()
530 """Get the type of a primitive or multiple primitives.
533 uuid: Single UUID (int) or list of UUIDs
536 PrimitiveType for single UUID, or np.ndarray of shape (N,) uint32 for list
539 if isinstance(uuid, (list, tuple)):
541 return np.empty((0,), dtype=np.uint32)
542 ptr, size = context_wrapper.getBatchPrimitiveTypes(self.
context, uuid)
543 if size == 0
or not ptr:
544 return np.empty((0,), dtype=np.uint32)
545 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
546 primitive_type = context_wrapper.getPrimitiveType(self.
context, uuid)
550 """Get the area of a primitive or multiple primitives.
553 uuid: Single UUID (int) or list of UUIDs
556 float for single UUID, or np.ndarray of shape (N,) for list
559 if isinstance(uuid, (list, tuple)):
561 return np.empty((0,), dtype=np.float32)
562 ptr, size = context_wrapper.getBatchPrimitiveAreas(self.
context, uuid)
563 if size == 0
or not ptr:
564 return np.empty((0,), dtype=np.float32)
565 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
566 return context_wrapper.getPrimitiveArea(self.
context, uuid)
569 """Get the normal vector of a primitive or multiple primitives.
572 uuid: Single UUID (int) or list of UUIDs
575 vec3 for single UUID, or np.ndarray of shape (N, 3) for list
578 if isinstance(uuid, (list, tuple)):
580 return np.empty((0, 3), dtype=np.float32)
581 ptr, size = context_wrapper.getBatchPrimitiveNormals(self.
context, uuid)
582 if size == 0
or not ptr:
583 return np.empty((0, 3), dtype=np.float32)
584 return np.ctypeslib.as_array(ptr, shape=(size,)).copy().reshape(-1, 3)
585 normal_ptr = context_wrapper.getPrimitiveNormal(self.
context, uuid)
586 return vec3(normal_ptr[0], normal_ptr[1], normal_ptr[2])
589 """Get vertices of a primitive or multiple primitives.
592 uuid: Single UUID (int) or list of UUIDs
595 List[vec3] for single UUID, or tuple of (flat_data, offsets) for list
596 where flat_data is a float32 ndarray and offsets is a uint32 ndarray
597 of length N+1. Vertices for primitive i are at
598 flat_data[offsets[i]:offsets[i+1]].
601 if isinstance(uuid, (list, tuple)):
603 return (np.empty((0,), dtype=np.float32), np.zeros((1,), dtype=np.uint32))
604 ptr, offsets, total = context_wrapper.getBatchPrimitiveVertices(self.
context, uuid)
605 offsets_arr = np.asarray(offsets, dtype=np.uint32)
606 if total == 0
or not ptr:
607 return (np.empty((0,), dtype=np.float32), offsets_arr)
608 data = np.ctypeslib.as_array(ptr, shape=(total,)).copy()
609 return (data, offsets_arr)
610 size = ctypes.c_uint()
611 vertices_ptr = context_wrapper.getPrimitiveVertices(self.
context, uuid, ctypes.byref(size))
613 vertices_list = ctypes.cast(vertices_ptr, ctypes.POINTER(ctypes.c_float * size.value)).contents
614 vertices = [
vec3(vertices_list[i], vertices_list[i+1], vertices_list[i+2])
for i
in range(0, size.value, 3)]
618 """Get the color of a primitive or multiple primitives.
621 uuid: Single UUID (int) or list of UUIDs
624 RGBcolor for single UUID, or np.ndarray of shape (N, 3) for list
627 if isinstance(uuid, (list, tuple)):
629 return np.empty((0, 3), dtype=np.float32)
630 ptr, size = context_wrapper.getBatchPrimitiveColors(self.
context, uuid)
631 if size == 0
or not ptr:
632 return np.empty((0, 3), dtype=np.float32)
633 return np.ctypeslib.as_array(ptr, shape=(size,)).copy().reshape(-1, 3)
634 color_ptr = context_wrapper.getPrimitiveColor(self.
context, uuid)
635 return RGBcolor(color_ptr[0], color_ptr[1], color_ptr[2])
639 return context_wrapper.getPrimitiveCount(self.
context)
642 """Check if a primitive exists for a given UUID or list of UUIDs.
645 uuid: A single UUID (int) or a list of UUIDs.
648 True if the primitive(s) exist, False otherwise.
649 For a list, returns True only if ALL primitives exist.
652 if isinstance(uuid, (list, tuple)):
653 arr = (ctypes.c_uint * len(uuid))(*uuid)
654 return context_wrapper.doesPrimitiveExistBatch(self.
context, arr, len(uuid))
655 return context_wrapper.doesPrimitiveExist(self.
context, uuid)
659 size = ctypes.c_uint()
660 uuids_ptr = context_wrapper.getAllUUIDs(self.
context, ctypes.byref(size))
661 return list(uuids_ptr[:size.value])
665 return context_wrapper.getObjectCount(self.
context)
669 size = ctypes.c_uint()
670 objectids_ptr = context_wrapper.getAllObjectIDs(self.
context, ctypes.byref(size))
671 return list(objectids_ptr[:size.value])
675 Get physical properties and geometry information for a single primitive.
678 uuid: UUID of the primitive
681 PrimitiveInfo object containing physical properties and geometry
696 solid_fraction =
None
701 except NotImplementedError:
707 except NotImplementedError:
711 except NotImplementedError:
712 solid_fraction =
None
716 primitive_type=primitive_type,
721 texture_file=texture_file,
722 texture_uv=texture_uv,
723 solid_fraction=solid_fraction,
727 """Build PrimitiveInfo for many primitives with a fixed number of native calls.
729 Field-for-field identical to calling :meth:`getPrimitiveInfo` on each UUID,
730 but each of the eight fields has a list-accepting getter backed by a native
731 ``getBatch*`` call. Doing it per primitive costs eight native round-trips
732 each — 77.5 ms for 5,000 primitives against 4.6 ms batched.
735 uuids: Primitives to describe, in the order to return them
738 List of PrimitiveInfo, one per UUID, in the order given
754 except NotImplementedError:
760 if uv_offsets
is None or len(uv_offsets) < len(uuids) + 1:
761 uv_data, uv_offsets =
None,
None
762 except NotImplementedError:
763 uv_data, uv_offsets =
None,
None
766 except NotImplementedError:
767 solid_fractions =
None
770 for i, uuid
in enumerate(uuids):
771 segment = vertex_data[vertex_offsets[i]:vertex_offsets[i + 1]]
772 vertices = [
vec3(float(segment[j]), float(segment[j + 1]), float(segment[j + 2]))
773 for j
in range(0, len(segment), 3)]
776 if uv_data
is not None:
777 uv_segment = uv_data[uv_offsets[i]:uv_offsets[i + 1]]
779 texture_uv = [
vec2(float(uv_segment[j]), float(uv_segment[j + 1]))
780 for j
in range(0, len(uv_segment), 2)]
783 if texture_files
is not None and texture_files[i]:
784 texture_file = texture_files[i]
789 area=float(areas[i]),
790 normal=
vec3(float(normals[i][0]), float(normals[i][1]), float(normals[i][2])),
792 color=
RGBcolor(float(colors[i][0]), float(colors[i][1]), float(colors[i][2])),
793 texture_file=texture_file,
794 texture_uv=texture_uv,
795 solid_fraction=(
None if solid_fractions
is None
796 else float(solid_fractions[i])),
802 Get physical properties and geometry information for all primitives in the context.
805 List of PrimitiveInfo objects for all primitives
811 Get physical properties and geometry information for all primitives belonging to a specific object.
814 object_id: ID of the object
817 List of PrimitiveInfo objects for primitives in the object
819 object_uuids = context_wrapper.getObjectPrimitiveUUIDs(self.
context, object_id)
823 def addTile(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
824 rotation: Optional[SphericalCoord] =
None, subdiv: int2 =
int2(1, 1),
825 color: Optional[RGBcolor] =
None) -> List[int]:
827 Add a subdivided patch (tile) to the context.
829 A tile is a patch subdivided into a regular grid of smaller patches,
830 useful for creating detailed surfaces or terrain.
833 center: 3D coordinates of tile center (default: origin)
834 size: Width and height of the tile (default: 1x1)
835 rotation: Orientation of the tile (default: no rotation)
836 subdiv: Number of subdivisions in x and y directions (default: 1x1)
837 color: Color of the tile (default: white)
840 List of UUIDs for all patches created in the tile
843 >>> context = Context()
844 >>> # Create a 2x2 meter tile subdivided into 4x4 patches
845 >>> tile_uuids = context.addTile(
846 ... center=vec3(0, 0, 1),
848 ... subdiv=int2(4, 4),
849 ... color=RGBcolor(0.5, 0.8, 0.2)
851 >>> print(f"Created {len(tile_uuids)} patches")
856 if not isinstance(center, vec3):
857 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
858 if not isinstance(size, vec2):
859 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
860 if rotation
is not None and not isinstance(rotation, SphericalCoord):
861 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
862 if not isinstance(subdiv, int2):
863 raise ValueError(f
"Subdiv must be an int2, got {type(subdiv).__name__}")
864 if color
is not None and not isinstance(color, RGBcolor):
865 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
868 if any(s <= 0
for s
in size.to_list()):
869 raise ValueError(
"All size dimensions must be positive")
870 if any(s <= 0
for s
in subdiv.to_list()):
871 raise ValueError(
"All subdivision counts must be positive")
877 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
879 if color
and not (color.r == 1.0
and color.g == 1.0
and color.b == 1.0):
880 return context_wrapper.addTileWithColor(
881 self.
context, center.to_list(), size.to_list(),
882 rotation_list, subdiv.to_list(), color.to_list()
885 return context_wrapper.addTile(
886 self.
context, center.to_list(), size.to_list(),
887 rotation_list, subdiv.to_list()
890 @validate_sphere_params
891 def addSphere(self, center: vec3 =
vec3(0, 0, 0), radius: float = 1.0,
892 ndivs: int = 10, color: Optional[RGBcolor] =
None) -> List[int]:
894 Add a sphere to the context.
896 The sphere is tessellated into triangular faces based on the specified
900 center: 3D coordinates of sphere center (default: origin)
901 radius: Radius of the sphere (default: 1.0)
902 ndivs: Number of divisions for tessellation (default: 10)
903 Higher values create smoother spheres but more triangles
904 color: Color of the sphere (default: white)
907 List of UUIDs for all triangles created in the sphere
910 >>> context = Context()
911 >>> # Create a red sphere at (1, 2, 3) with radius 0.5
912 >>> sphere_uuids = context.addSphere(
913 ... center=vec3(1, 2, 3),
916 ... color=RGBcolor(1, 0, 0)
918 >>> print(f"Created sphere with {len(sphere_uuids)} triangles")
923 if not isinstance(center, vec3):
924 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
925 if not isinstance(radius, (int, float)):
926 raise ValueError(f
"Radius must be a number, got {type(radius).__name__}")
927 if not isinstance(ndivs, int):
928 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
929 if color
is not None and not isinstance(color, RGBcolor):
930 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
934 raise ValueError(
"Sphere radius must be positive")
936 raise ValueError(
"Number of divisions must be at least 3")
939 return context_wrapper.addSphereWithColor(
940 self.
context, ndivs, center.to_list(), radius, color.to_list()
943 return context_wrapper.addSphere(
944 self.
context, ndivs, center.to_list(), radius
947 @validate_tube_params
948 def addTube(self, nodes: List[vec3], radii: Union[float, List[float]],
949 ndivs: int = 6, colors: Optional[Union[RGBcolor, List[RGBcolor]]] =
None) -> List[int]:
951 Add a tube (pipe/cylinder) to the context.
953 The tube is defined by a series of nodes (path) with radius at each node.
954 It's tessellated into triangular faces based on the number of radial divisions.
957 nodes: List of 3D points defining the tube path (at least 2 nodes)
958 radii: Radius at each node. Can be:
959 - Single float: constant radius for all nodes
960 - List of floats: radius for each node (must match nodes length)
961 ndivs: Number of radial divisions (default: 6)
962 Higher values create smoother tubes but more triangles
963 colors: Colors at each node. Can be:
965 - Single RGBcolor: constant color for all nodes
966 - List of RGBcolor: color for each node (must match nodes length)
969 List of UUIDs for all triangles created in the tube
972 >>> context = Context()
973 >>> # Create a curved tube with varying radius
974 >>> nodes = [vec3(0, 0, 0), vec3(1, 0, 0), vec3(2, 1, 0)]
975 >>> radii = [0.1, 0.2, 0.1]
976 >>> colors = [RGBcolor(1, 0, 0), RGBcolor(0, 1, 0), RGBcolor(0, 0, 1)]
977 >>> tube_uuids = context.addTube(nodes, radii, ndivs=8, colors=colors)
978 >>> print(f"Created tube with {len(tube_uuids)} triangles")
983 if not isinstance(nodes, (list, tuple)):
984 raise ValueError(f
"Nodes must be a list or tuple, got {type(nodes).__name__}")
985 if not isinstance(ndivs, int):
986 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
987 if colors
is not None and not isinstance(colors, (RGBcolor, list, tuple)):
988 raise ValueError(f
"Colors must be RGBcolor, list, tuple, or None, got {type(colors).__name__}")
992 raise ValueError(
"Tube requires at least 2 nodes")
994 raise ValueError(
"Number of radial divisions must be at least 3")
997 if isinstance(radii, (int, float)):
998 radii_list = [float(radii)] * len(nodes)
1000 radii_list = [float(r)
for r
in radii]
1001 if len(radii_list) != len(nodes):
1002 raise ValueError(f
"Number of radii ({len(radii_list)}) must match number of nodes ({len(nodes)})")
1005 if any(r <= 0
for r
in radii_list):
1006 raise ValueError(
"All radii must be positive")
1011 nodes_flat.extend(node.to_list())
1015 return context_wrapper.addTube(self.
context, ndivs, nodes_flat, radii_list)
1016 elif isinstance(colors, RGBcolor):
1018 colors_flat = colors.to_list() * len(nodes)
1021 if len(colors) != len(nodes):
1022 raise ValueError(f
"Number of colors ({len(colors)}) must match number of nodes ({len(nodes)})")
1024 for color
in colors:
1025 colors_flat.extend(color.to_list())
1027 return context_wrapper.addTubeWithColor(self.
context, ndivs, nodes_flat, radii_list, colors_flat)
1029 @validate_box_params
1030 def addBox(self, center: vec3 =
vec3(0, 0, 0), size: vec3 =
vec3(1, 1, 1),
1031 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None) -> List[int]:
1033 Add a rectangular box to the context.
1035 The box is subdivided into patches on each face based on the specified
1039 center: 3D coordinates of box center (default: origin)
1040 size: Width, height, and depth of the box (default: 1x1x1)
1041 subdiv: Number of subdivisions in x, y, and z directions (default: 1x1x1)
1042 Higher values create more detailed surfaces
1043 color: Color of the box (default: white)
1046 List of UUIDs for all patches created on the box faces
1049 >>> context = Context()
1050 >>> # Create a blue box subdivided for detail
1051 >>> box_uuids = context.addBox(
1052 ... center=vec3(0, 0, 2),
1053 ... size=vec3(2, 1, 0.5),
1054 ... subdiv=int3(4, 2, 1),
1055 ... color=RGBcolor(0, 0, 1)
1057 >>> print(f"Created box with {len(box_uuids)} patches")
1062 if not isinstance(center, vec3):
1063 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1064 if not isinstance(size, vec3):
1065 raise ValueError(f
"Size must be a vec3, got {type(size).__name__}")
1066 if not isinstance(subdiv, int3):
1067 raise ValueError(f
"Subdiv must be an int3, got {type(subdiv).__name__}")
1068 if color
is not None and not isinstance(color, RGBcolor):
1069 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1072 if any(s <= 0
for s
in size.to_list()):
1073 raise ValueError(
"All box dimensions must be positive")
1074 if any(s < 1
for s
in subdiv.to_list()):
1075 raise ValueError(
"All subdivision counts must be at least 1")
1078 return context_wrapper.addBoxWithColor(
1079 self.
context, center.to_list(), size.to_list(),
1080 subdiv.to_list(), color.to_list()
1083 return context_wrapper.addBox(
1084 self.
context, center.to_list(), size.to_list(), subdiv.to_list()
1087 def addDisk(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
1088 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
1089 color: Optional[Union[RGBcolor, RGBAcolor]] =
None) -> List[int]:
1091 Add a disk (circular or elliptical surface) to the context.
1093 A disk is a flat circular or elliptical surface tessellated into
1094 triangular faces. Supports both uniform radial subdivisions and
1095 separate radial/azimuthal subdivisions for finer control.
1098 center: 3D coordinates of disk center (default: origin)
1099 size: Semi-major and semi-minor radii of the disk (default: 1x1 circle)
1100 ndivs: Number of radial divisions (int) or [radial, azimuthal] divisions (int2)
1101 (default: 20). Higher values create smoother circles but more triangles.
1102 rotation: Orientation of the disk (default: horizontal, normal = +z)
1103 color: Color of the disk (default: white). Can be RGBcolor or RGBAcolor for transparency.
1106 List of UUIDs for all triangles created in the disk
1109 >>> context = Context()
1110 >>> # Create a red disk at (0, 0, 1) with radius 0.5
1111 >>> disk_uuids = context.addDisk(
1112 ... center=vec3(0, 0, 1),
1113 ... size=vec2(0.5, 0.5),
1115 ... color=RGBcolor(1, 0, 0)
1117 >>> print(f"Created disk with {len(disk_uuids)} triangles")
1119 >>> # Create a semi-transparent blue elliptical disk
1120 >>> disk_uuids = context.addDisk(
1121 ... center=vec3(0, 0, 2),
1122 ... size=vec2(1.0, 0.5),
1124 ... rotation=SphericalCoord(1, 0.5, 0),
1125 ... color=RGBAcolor(0, 0, 1, 0.5)
1128 >>> # Create disk with polar/radial subdivisions for finer control
1129 >>> disk_uuids = context.addDisk(
1130 ... center=vec3(0, 0, 3),
1131 ... size=vec2(1, 1),
1132 ... ndivs=int2(10, 20), # 10 radial, 20 azimuthal divisions
1133 ... color=RGBcolor(0, 1, 0)
1139 if not isinstance(center, vec3):
1140 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1141 if not isinstance(size, vec2):
1142 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1143 if not isinstance(ndivs, (int, int2)):
1144 raise ValueError(f
"Ndivs must be an int or int2, got {type(ndivs).__name__}")
1145 if rotation
is not None and not isinstance(rotation, SphericalCoord):
1146 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
1147 if color
is not None and not isinstance(color, (RGBcolor, RGBAcolor)):
1148 raise ValueError(f
"Color must be an RGBcolor, RGBAcolor, or None, got {type(color).__name__}")
1151 if any(s <= 0
for s
in size.to_list()):
1152 raise ValueError(
"Disk size must be positive")
1155 if isinstance(ndivs, int):
1157 raise ValueError(
"Number of divisions must be at least 3")
1159 if any(n < 1
for n
in ndivs.to_list()):
1160 raise ValueError(
"Radial and angular divisions must be at least 1")
1163 if rotation
is None:
1168 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1171 if isinstance(ndivs, int2):
1174 if isinstance(color, RGBAcolor):
1175 return context_wrapper.addDiskPolarSubdivisionsRGBA(
1176 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
1177 rotation_list, color.to_list()
1181 return context_wrapper.addDiskPolarSubdivisions(
1182 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
1183 rotation_list, color.to_list()
1187 color_list = [1.0, 1.0, 1.0]
1188 return context_wrapper.addDiskPolarSubdivisions(
1189 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
1190 rotation_list, color_list
1195 if isinstance(color, RGBAcolor):
1197 return context_wrapper.addDiskWithRGBAColor(
1198 self.
context, ndivs, center.to_list(), size.to_list(),
1199 rotation_list, color.to_list()
1203 return context_wrapper.addDiskWithColor(
1204 self.
context, ndivs, center.to_list(), size.to_list(),
1205 rotation_list, color.to_list()
1209 return context_wrapper.addDiskWithRotation(
1210 self.
context, ndivs, center.to_list(), size.to_list(),
1214 def addCone(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1215 ndivs: int = 20, color: Optional[RGBcolor] =
None) -> List[int]:
1217 Add a cone (or cylinder/frustum) to the context.
1219 A cone is a 3D shape connecting two circular cross-sections with
1220 potentially different radii. When radii are equal, creates a cylinder.
1221 When one radius is zero, creates a true cone.
1224 node0: 3D coordinates of the base center
1225 node1: 3D coordinates of the apex center
1226 radius0: Radius at base (node0). Use 0 for pointed end.
1227 radius1: Radius at apex (node1). Use 0 for pointed end.
1228 ndivs: Number of radial divisions for tessellation (default: 20)
1229 color: Color of the cone (default: white)
1232 List of UUIDs for all triangles created in the cone
1235 >>> context = Context()
1236 >>> # Create a cylinder (equal radii)
1237 >>> cylinder_uuids = context.addCone(
1238 ... node0=vec3(0, 0, 0),
1239 ... node1=vec3(0, 0, 2),
1245 >>> # Create a true cone (one radius = 0)
1246 >>> cone_uuids = context.addCone(
1247 ... node0=vec3(1, 0, 0),
1248 ... node1=vec3(1, 0, 1.5),
1252 ... color=RGBcolor(1, 0, 0)
1255 >>> # Create a frustum (different radii)
1256 >>> frustum_uuids = context.addCone(
1257 ... node0=vec3(2, 0, 0),
1258 ... node1=vec3(2, 0, 1),
1267 if not isinstance(node0, vec3):
1268 raise ValueError(f
"node0 must be a vec3, got {type(node0).__name__}")
1269 if not isinstance(node1, vec3):
1270 raise ValueError(f
"node1 must be a vec3, got {type(node1).__name__}")
1271 if not isinstance(ndivs, int):
1272 raise ValueError(f
"ndivs must be an int, got {type(ndivs).__name__}")
1273 if color
is not None and not isinstance(color, RGBcolor):
1274 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1277 if radius0 < 0
or radius1 < 0:
1278 raise ValueError(
"Radii must be non-negative")
1280 raise ValueError(
"Number of radial divisions must be at least 3")
1284 return context_wrapper.addConeWithColor(
1285 self.
context, ndivs, node0.to_list(), node1.to_list(),
1286 radius0, radius1, color.to_list()
1289 return context_wrapper.addCone(
1290 self.
context, ndivs, node0.to_list(), node1.to_list(),
1295 radius: Union[float, vec3] = 1.0, ndivs: int = 20,
1296 color: Optional[RGBcolor] =
None,
1297 texturefile: Optional[str] =
None) -> int:
1299 Add a spherical or ellipsoidal compound object to the context.
1301 Creates a sphere or ellipsoid as a compound object with a trackable object ID.
1302 Primitives within the object are registered as children of the object.
1305 center: Center position of sphere/ellipsoid (default: origin)
1306 radius: Radius as float (sphere) or vec3 (ellipsoid) (default: 1.0)
1307 ndivs: Number of tessellation divisions (default: 20)
1308 color: Optional RGB color
1309 texturefile: Optional texture image file path
1312 Object ID of the created compound object
1315 ValueError: If parameters are invalid
1316 NotImplementedError: If object-returning functions unavailable
1319 >>> # Create a basic sphere at origin
1320 >>> obj_id = ctx.addSphereObject()
1322 >>> # Create a colored sphere
1323 >>> obj_id = ctx.addSphereObject(
1324 ... center=vec3(0, 0, 5),
1326 ... color=RGBcolor(1, 0, 0)
1329 >>> # Create an ellipsoid (stretched sphere)
1330 >>> obj_id = ctx.addSphereObject(
1331 ... center=vec3(10, 0, 0),
1332 ... radius=vec3(2, 1, 1), # Elongated in x-direction
1339 if not isinstance(center, vec3):
1340 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1341 if not isinstance(radius, (int, float, vec3)):
1342 raise ValueError(f
"Radius must be a number or vec3, got {type(radius).__name__}")
1343 if color
is not None and not isinstance(color, RGBcolor):
1344 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1348 raise ValueError(
"Number of divisions must be at least 3")
1351 is_ellipsoid = isinstance(radius, vec3)
1357 return context_wrapper.addSphereObject_ellipsoid_texture(
1358 self.
context, ndivs, center.to_list(), radius.to_list(), texturefile
1361 return context_wrapper.addSphereObject_ellipsoid_color(
1362 self.
context, ndivs, center.to_list(), radius.to_list(), color.to_list()
1365 return context_wrapper.addSphereObject_ellipsoid(
1366 self.
context, ndivs, center.to_list(), radius.to_list()
1371 return context_wrapper.addSphereObject_texture(
1372 self.
context, ndivs, center.to_list(), radius, texturefile
1375 return context_wrapper.addSphereObject_color(
1376 self.
context, ndivs, center.to_list(), radius, color.to_list()
1379 return context_wrapper.addSphereObject_basic(
1380 self.
context, ndivs, center.to_list(), radius
1385 subdiv: int2 =
int2(1, 1),
1386 color: Optional[RGBcolor] =
None,
1387 texturefile: Optional[str] =
None,
1388 texture_repeat: Optional[int2] =
None) -> int:
1390 Add a tiled patch (subdivided patch) as a compound object to the context.
1392 Creates a rectangular patch subdivided into a grid of smaller patches,
1393 registered as a compound object with a trackable object ID.
1396 center: Center position of tile (default: origin)
1397 size: Size in x and y directions (default: 1x1)
1398 rotation: Spherical rotation (default: no rotation)
1399 subdiv: Number of subdivisions in x and y (default: 1x1)
1400 color: Optional RGB color
1401 texturefile: Optional texture image file path
1402 texture_repeat: Optional texture repetitions in x and y
1405 Object ID of the created compound object
1408 ValueError: If parameters are invalid
1409 NotImplementedError: If object-returning functions unavailable
1412 >>> # Create a basic 2x2 tile
1413 >>> obj_id = ctx.addTileObject(
1414 ... center=vec3(0, 0, 0),
1415 ... size=vec2(10, 10),
1416 ... subdiv=int2(2, 2)
1419 >>> # Create a colored tile with rotation
1420 >>> obj_id = ctx.addTileObject(
1421 ... center=vec3(5, 0, 0),
1422 ... size=vec2(10, 5),
1423 ... rotation=SphericalCoord(1, 0, 45),
1424 ... subdiv=int2(4, 2),
1425 ... color=RGBcolor(0, 1, 0)
1431 if not isinstance(center, vec3):
1432 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1433 if not isinstance(size, vec2):
1434 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1435 if not isinstance(rotation, SphericalCoord):
1436 raise ValueError(f
"Rotation must be a SphericalCoord, got {type(rotation).__name__}")
1437 if not isinstance(subdiv, int2):
1438 raise ValueError(f
"Subdiv must be an int2, got {type(subdiv).__name__}")
1439 if color
is not None and not isinstance(color, RGBcolor):
1440 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1441 if texture_repeat
is not None and not isinstance(texture_repeat, int2):
1442 raise ValueError(f
"texture_repeat must be an int2 or None, got {type(texture_repeat).__name__}")
1445 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1448 if texture_repeat
is not None:
1449 if texturefile
is None:
1450 raise ValueError(
"texture_repeat requires texturefile")
1451 return context_wrapper.addTileObject_texture_repeat(
1452 self.
context, center.to_list(), size.to_list(), rotation_list,
1453 subdiv.to_list(), texturefile, texture_repeat.to_list()
1456 return context_wrapper.addTileObject_texture(
1457 self.
context, center.to_list(), size.to_list(), rotation_list,
1458 subdiv.to_list(), texturefile
1461 return context_wrapper.addTileObject_color(
1462 self.
context, center.to_list(), size.to_list(), rotation_list,
1463 subdiv.to_list(), color.to_list()
1466 return context_wrapper.addTileObject_basic(
1467 self.
context, center.to_list(), size.to_list(), rotation_list,
1473 refinement: Optional[AdaptiveTileRefinement] =
None,
1474 color: Optional[RGBcolor] =
None,
1475 texturefile: Optional[str] =
None,
1476 texture_repeat: Optional[int2] =
None) -> int:
1478 Add a patch subdivided into sub-patches whose size adapts with distance from a target point.
1480 Sub-patches are generated by recursive quadtree subdivision, so they are fine near
1481 ``refinement.target`` and progressively coarser away from it. This is intended for ground
1482 planes, where fine resolution is needed to resolve shadows cast near an object of interest
1483 but the remainder of the domain only needs to occlude the horizon. The sub-patches exactly
1484 partition the tile with no gaps and no overlaps, and the achieved sizes are typically
1485 within about 20% of those requested.
1488 center: Center position of tile (default: origin)
1489 size: Size in x and y directions (default: 1x1)
1490 rotation: Spherical rotation (default: no rotation)
1491 refinement: Parameters controlling the adaptive sub-patch resolution
1492 (default: ``AdaptiveTileRefinement()``)
1493 color: Optional RGB color
1494 texturefile: Optional texture image file path
1495 texture_repeat: Optional texture repetitions in x and y. Unlike
1496 :meth:`addTileObject`, the count is applied exactly -- the base grid is snapped to
1497 a multiple of it rather than the count being reduced.
1500 Object ID of the created compound object
1503 ValueError: If parameters are invalid
1504 RuntimeError: If the refinement is rejected by helios, e.g. because
1505 ``subpatch_size_max`` exceeds half the smaller tile dimension, or because the
1506 requested refinement would generate more than 2,000,000 sub-patches
1507 NotImplementedError: If object-returning functions unavailable
1510 Sub-patches are ordered by quadtree traversal, not row-major, so they cannot be
1511 indexed by grid position. Adaptive tiles have no subdivision count, so
1512 :meth:`getTileObjectSubdivisionCount` and :meth:`setTileObjectSubdivisionCount` do
1516 >>> # Ground plane resolved finely beneath a plant at the origin
1517 >>> refinement = AdaptiveTileRefinement(
1518 ... target=vec2(0, 0), subpatch_size_min=0.02, subpatch_size_max=2.0
1520 >>> obj_id = ctx.addAdaptiveTileObject(
1521 ... center=vec3(0, 0, 0), size=vec2(50, 50), refinement=refinement
1524 >>> # Check the cost before committing to it
1525 >>> ctx.predictAdaptiveTileObjectSubpatchCount(vec2(50, 50), refinement) # doctest: +SKIP
1530 if refinement
is None:
1534 if not isinstance(center, vec3):
1535 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1536 if not isinstance(size, vec2):
1537 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1538 if not isinstance(rotation, SphericalCoord):
1539 raise ValueError(f
"Rotation must be a SphericalCoord, got {type(rotation).__name__}")
1540 if not isinstance(refinement, AdaptiveTileRefinement):
1541 raise ValueError(f
"Refinement must be an AdaptiveTileRefinement or None, got {type(refinement).__name__}")
1542 if color
is not None and not isinstance(color, RGBcolor):
1543 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1544 if texture_repeat
is not None and not isinstance(texture_repeat, int2):
1545 raise ValueError(f
"texture_repeat must be an int2 or None, got {type(texture_repeat).__name__}")
1548 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1551 if texture_repeat
is not None:
1552 if texturefile
is None:
1553 raise ValueError(
"texture_repeat requires texturefile")
1554 return context_wrapper.addAdaptiveTileObject_texture_repeat(
1555 self.
context, center.to_list(), size.to_list(), rotation_list,
1556 refinement.to_list(), texturefile, texture_repeat.to_list()
1559 return context_wrapper.addAdaptiveTileObject_texture(
1560 self.
context, center.to_list(), size.to_list(), rotation_list,
1561 refinement.to_list(), texturefile
1564 return context_wrapper.addAdaptiveTileObject_color(
1565 self.
context, center.to_list(), size.to_list(), rotation_list,
1566 refinement.to_list(), color.to_list()
1569 return context_wrapper.addAdaptiveTileObject_basic(
1570 self.
context, center.to_list(), size.to_list(), rotation_list,
1571 refinement.to_list()
1575 refinement: Optional[AdaptiveTileRefinement] =
None,
1576 texture_repeat: Optional[int2] =
None) -> int:
1578 Determine how many sub-patches an adaptive tile object would contain, without building geometry.
1580 Runs the same quadtree traversal as :meth:`addAdaptiveTileObject` but counts cells instead
1581 of creating primitives. Useful for checking the cost of a set of refinement parameters
1582 before committing to it, since the sub-patch count is highly sensitive to
1583 ``refinement.transition_exponent``.
1586 size: Size of the tile in the x- and y-directions
1587 refinement: Parameters controlling the adaptive sub-patch resolution
1588 (default: ``AdaptiveTileRefinement()``)
1589 texture_repeat: Texture repetitions the tile would use (default: 1x1)
1592 Number of sub-patches that would be created
1595 ValueError: If parameters are invalid
1596 RuntimeError: Raised for a refinement that would generate too many sub-patches, rather
1597 than reporting a count, so that this and :meth:`addAdaptiveTileObject` answer the
1598 question the same way
1601 >>> refinement = AdaptiveTileRefinement(subpatch_size_min=0.02, subpatch_size_max=2.0)
1602 >>> ctx.predictAdaptiveTileObjectSubpatchCount(vec2(50, 50), refinement) # doctest: +SKIP
1607 if refinement
is None:
1609 if texture_repeat
is None:
1610 texture_repeat =
int2(1, 1)
1612 if not isinstance(size, vec2):
1613 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1614 if not isinstance(refinement, AdaptiveTileRefinement):
1615 raise ValueError(f
"Refinement must be an AdaptiveTileRefinement or None, got {type(refinement).__name__}")
1616 if not isinstance(texture_repeat, int2):
1617 raise ValueError(f
"texture_repeat must be an int2 or None, got {type(texture_repeat).__name__}")
1619 return context_wrapper.predictAdaptiveTileObjectSubpatchCount(
1620 self.
context, size.to_list(), refinement.to_list(), texture_repeat.to_list()
1624 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None,
1625 texturefile: Optional[str] =
None, reverse_normals: bool =
False) -> int:
1627 Add a rectangular box (prism) as a compound object to the context.
1630 center: Center position (default: origin)
1631 size: Size in x, y, z directions (default: 1x1x1)
1632 subdiv: Subdivisions in x, y, z (default: 1x1x1)
1633 color: Optional RGB color
1634 texturefile: Optional texture file path
1635 reverse_normals: Reverse normal directions (default: False)
1638 Object ID of the created compound object
1643 if not isinstance(center, vec3):
1644 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1645 if not isinstance(size, vec3):
1646 raise ValueError(f
"Size must be a vec3, got {type(size).__name__}")
1647 if not isinstance(subdiv, int3):
1648 raise ValueError(f
"Subdiv must be an int3, got {type(subdiv).__name__}")
1649 if color
is not None and not isinstance(color, RGBcolor):
1650 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1654 return context_wrapper.addBoxObject_texture_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile, reverse_normals)
1656 return context_wrapper.addBoxObject_color_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list(), reverse_normals)
1658 raise ValueError(
"reverse_normals requires either color or texturefile")
1660 return context_wrapper.addBoxObject_texture(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile)
1662 return context_wrapper.addBoxObject_color(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list())
1664 return context_wrapper.addBoxObject_basic(self.
context, center.to_list(), size.to_list(), subdiv.to_list())
1666 def addConeObject(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1667 ndivs: int = 20, color: Optional[RGBcolor] =
None,
1668 texturefile: Optional[str] =
None) -> int:
1670 Add a cone/cylinder/frustum as a compound object to the context.
1673 node0: Base position
1675 radius0: Radius at base
1676 radius1: Radius at top
1677 ndivs: Number of radial divisions (default: 20)
1678 color: Optional RGB color
1679 texturefile: Optional texture file path
1682 Object ID of the created compound object
1687 if not isinstance(node0, vec3):
1688 raise ValueError(f
"node0 must be a vec3, got {type(node0).__name__}")
1689 if not isinstance(node1, vec3):
1690 raise ValueError(f
"node1 must be a vec3, got {type(node1).__name__}")
1691 if not isinstance(radius0, (int, float)):
1692 raise ValueError(f
"radius0 must be a number, got {type(radius0).__name__}")
1693 if not isinstance(radius1, (int, float)):
1694 raise ValueError(f
"radius1 must be a number, got {type(radius1).__name__}")
1695 if color
is not None and not isinstance(color, RGBcolor):
1696 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1699 return context_wrapper.addConeObject_texture(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, texturefile)
1701 return context_wrapper.addConeObject_color(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, color.to_list())
1703 return context_wrapper.addConeObject_basic(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1)
1706 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
1707 color: Optional[Union[RGBcolor, RGBAcolor]] =
None,
1708 texturefile: Optional[str] =
None) -> int:
1710 Add a disk as a compound object to the context.
1713 center: Center position (default: origin)
1714 size: Semi-major and semi-minor radii (default: 1x1)
1715 ndivs: int (uniform) or int2 (polar/radial subdivisions) (default: 20)
1716 rotation: Optional spherical rotation
1717 color: Optional RGB or RGBA color
1718 texturefile: Optional texture file path
1721 Object ID of the created compound object
1725 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
if rotation
else [1, 0, 0]
1726 is_polar = isinstance(ndivs, int2)
1730 return context_wrapper.addDiskObject_polar_texture(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, texturefile)
1732 if isinstance(color, RGBAcolor):
1733 return context_wrapper.addDiskObject_polar_rgba(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1735 return context_wrapper.addDiskObject_polar_color(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1737 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())
1740 return context_wrapper.addDiskObject_texture(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, texturefile)
1742 if isinstance(color, RGBAcolor):
1743 return context_wrapper.addDiskObject_rgba(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1745 return context_wrapper.addDiskObject_color(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1747 return context_wrapper.addDiskObject_rotation(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list)
1749 return context_wrapper.addDiskObject_basic(self.
context, ndivs, center.to_list(), size.to_list())
1751 def addTubeObject(self, ndivs: int, nodes: List[vec3], radii: List[float],
1752 colors: Optional[List[RGBcolor]] =
None,
1753 texturefile: Optional[str] =
None,
1754 texture_uv: Optional[List[float]] =
None) -> int:
1756 Add a tube as a compound object to the context.
1759 ndivs: Number of radial subdivisions
1760 nodes: List of vec3 positions defining tube segments
1761 radii: List of radii at each node
1762 colors: Optional list of RGB colors for each segment
1763 texturefile: Optional texture file path
1764 texture_uv: Optional UV coordinates for texture mapping
1767 Object ID of the created compound object
1772 if not isinstance(nodes, (list, tuple)):
1773 raise ValueError(f
"Nodes must be a list, got {type(nodes).__name__}")
1774 for i, node
in enumerate(nodes):
1775 if not isinstance(node, vec3):
1776 raise ValueError(f
"nodes[{i}] must be a vec3, got {type(node).__name__}")
1777 if not isinstance(radii, (list, tuple)):
1778 raise ValueError(f
"Radii must be a list, got {type(radii).__name__}")
1779 if colors
is not None:
1780 if not isinstance(colors, (list, tuple)):
1781 raise ValueError(f
"Colors must be a list or None, got {type(colors).__name__}")
1782 for i, c
in enumerate(colors):
1783 if not isinstance(c, RGBcolor):
1784 raise ValueError(f
"colors[{i}] must be an RGBcolor, got {type(c).__name__}")
1787 raise ValueError(
"Tube requires at least 2 nodes")
1788 if len(radii) != len(nodes):
1789 raise ValueError(
"Number of radii must match number of nodes")
1791 nodes_flat = [coord
for node
in nodes
for coord
in node.to_list()]
1793 if texture_uv
is not None:
1794 if texturefile
is None:
1795 raise ValueError(
"texture_uv requires texturefile")
1796 return context_wrapper.addTubeObject_texture_uv(self.
context, ndivs, nodes_flat, radii, texturefile, texture_uv)
1798 return context_wrapper.addTubeObject_texture(self.
context, ndivs, nodes_flat, radii, texturefile)
1800 if len(colors) != len(nodes):
1801 raise ValueError(
"Number of colors must match number of nodes")
1802 colors_flat = [c
for color
in colors
for c
in color.to_list()]
1803 return context_wrapper.addTubeObject_color(self.
context, ndivs, nodes_flat, radii, colors_flat)
1805 return context_wrapper.addTubeObject_basic(self.
context, ndivs, nodes_flat, radii)
1807 def copyPrimitive(self, UUID: Union[int, List[int]]) -> Union[int, List[int]]:
1809 Copy one or more primitives.
1811 Creates a duplicate of the specified primitive(s) with all associated data.
1812 The copy is placed at the same location as the original.
1815 UUID: Single primitive UUID or list of UUIDs to copy
1818 Single UUID of copied primitive (if UUID is int) or
1819 List of UUIDs of copied primitives (if UUID is list)
1822 >>> context = Context()
1823 >>> original_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1824 >>> # Copy single primitive
1825 >>> copied_uuid = context.copyPrimitive(original_uuid)
1826 >>> # Copy multiple primitives
1827 >>> copied_uuids = context.copyPrimitive([uuid1, uuid2, uuid3])
1831 if isinstance(UUID, int):
1832 return context_wrapper.copyPrimitive(self.
context, UUID)
1833 elif isinstance(UUID, list):
1834 return context_wrapper.copyPrimitives(self.
context, UUID)
1836 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1840 Copy all primitive data from source to destination primitive.
1842 Copies all associated data (primitive data fields) from the source
1843 primitive to the destination primitive. Both primitives must already exist.
1846 sourceUUID: UUID of the source primitive
1847 destinationUUID: UUID of the destination primitive
1850 >>> context = Context()
1851 >>> source_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1852 >>> dest_uuid = context.addPatch(center=vec3(1, 0, 0), size=vec2(1, 1))
1853 >>> context.setPrimitiveDataFloat(source_uuid, "temperature", 25.5)
1854 >>> context.copyPrimitiveData(source_uuid, dest_uuid)
1855 >>> # dest_uuid now has temperature data
1859 if not isinstance(sourceUUID, int):
1860 raise ValueError(f
"sourceUUID must be int, got {type(sourceUUID).__name__}")
1861 if not isinstance(destinationUUID, int):
1862 raise ValueError(f
"destinationUUID must be int, got {type(destinationUUID).__name__}")
1864 context_wrapper.copyPrimitiveData(self.
context, sourceUUID, destinationUUID)
1866 def copyObject(self, ObjID: Union[int, List[int]]) -> Union[int, List[int]]:
1868 Copy one or more compound objects.
1870 Creates a duplicate of the specified compound object(s) with all
1871 associated primitives and data. The copy is placed at the same location
1875 ObjID: Single object ID or list of object IDs to copy
1878 Single object ID of copied object (if ObjID is int) or
1879 List of object IDs of copied objects (if ObjID is list)
1882 >>> context = Context()
1883 >>> original_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1884 >>> # Copy single object
1885 >>> copied_obj = context.copyObject(original_obj)
1886 >>> # Copy multiple objects
1887 >>> copied_objs = context.copyObject([obj1, obj2, obj3])
1891 if isinstance(ObjID, int):
1892 return context_wrapper.copyObject(self.
context, ObjID)
1893 elif isinstance(ObjID, list):
1894 return context_wrapper.copyObjects(self.
context, ObjID)
1896 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1898 def copyObjectData(self, source_objID: int, destination_objID: int) ->
None:
1900 Copy all object data from source to destination compound object.
1902 Copies all associated data (object data fields) from the source
1903 compound object to the destination object. Both objects must already exist.
1906 source_objID: Object ID of the source compound object
1907 destination_objID: Object ID of the destination compound object
1910 >>> context = Context()
1911 >>> source_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1912 >>> dest_obj = context.addTile(center=vec3(2, 0, 0), size=vec2(2, 2))
1913 >>> context.setObjectData(source_obj, "material", "wood")
1914 >>> context.copyObjectData(source_obj, dest_obj)
1915 >>> # dest_obj now has material data
1919 if not isinstance(source_objID, int):
1920 raise ValueError(f
"source_objID must be int, got {type(source_objID).__name__}")
1921 if not isinstance(destination_objID, int):
1922 raise ValueError(f
"destination_objID must be int, got {type(destination_objID).__name__}")
1924 context_wrapper.copyObjectData(self.
context, source_objID, destination_objID)
1928 Translate one or more primitives by a shift vector.
1930 Moves the specified primitive(s) by the given shift vector without
1931 changing their orientation or size.
1934 UUID: Single primitive UUID or list of UUIDs to translate
1935 shift: 3D vector representing the translation [x, y, z]
1938 >>> context = Context()
1939 >>> patch_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1940 >>> # Translate single primitive
1941 >>> context.translatePrimitive(patch_uuid, vec3(1, 0, 0)) # Move 1 unit in x
1942 >>> # Translate multiple primitives
1943 >>> context.translatePrimitive([uuid1, uuid2, uuid3], vec3(0, 0, 1)) # Move 1 unit in z
1948 if not isinstance(shift, vec3):
1949 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
1951 if isinstance(UUID, int):
1952 context_wrapper.translatePrimitive(self.
context, UUID, shift.to_list())
1953 elif isinstance(UUID, list):
1954 context_wrapper.translatePrimitives(self.
context, UUID, shift.to_list())
1956 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1958 def translateObject(self, ObjID: Union[int, List[int]], shift: vec3) ->
None:
1960 Translate one or more compound objects by a shift vector.
1962 Moves the specified compound object(s) and all their constituent
1963 primitives by the given shift vector without changing orientation or size.
1966 ObjID: Single object ID or list of object IDs to translate
1967 shift: 3D vector representing the translation [x, y, z]
1970 >>> context = Context()
1971 >>> tile_uuids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1972 >>> obj_id = context.getPrimitiveParentObjectID(tile_uuids[0]) # Get object ID
1973 >>> # Translate single object
1974 >>> context.translateObject(obj_id, vec3(5, 0, 0)) # Move 5 units in x
1975 >>> # Translate multiple objects
1976 >>> context.translateObject([obj1, obj2, obj3], vec3(0, 2, 0)) # Move 2 units in y
1981 if not isinstance(shift, vec3):
1982 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
1984 if isinstance(ObjID, int):
1985 context_wrapper.translateObject(self.
context, ObjID, shift.to_list())
1986 elif isinstance(ObjID, list):
1987 context_wrapper.translateObjects(self.
context, ObjID, shift.to_list())
1989 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1992 axis: Union[str, vec3], origin: Optional[vec3] =
None) ->
None:
1994 Rotate one or more primitives.
1997 UUID: Single UUID or list of UUIDs to rotate
1998 angle: Rotation angle in radians
1999 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
2000 origin: Optional rotation origin point. If None, rotates about primitive center.
2001 If provided with string axis, raises ValueError.
2004 ValueError: If axis is invalid or if origin is provided with string axis
2009 if isinstance(axis, str):
2010 if axis
not in (
'x',
'y',
'z'):
2011 raise ValueError(
"axis must be 'x', 'y', or 'z'")
2012 if origin
is not None:
2013 raise ValueError(
"origin parameter cannot be used with string axis")
2016 if isinstance(UUID, int):
2017 context_wrapper.rotatePrimitive_axisString(self.
context, UUID, angle, axis)
2018 elif isinstance(UUID, list):
2019 context_wrapper.rotatePrimitives_axisString(self.
context, UUID, angle, axis)
2021 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2023 elif isinstance(axis, vec3):
2024 axis_list = axis.to_list()
2027 if all(abs(v) < 1e-10
for v
in axis_list):
2028 raise ValueError(
"axis vector cannot be zero")
2032 if isinstance(UUID, int):
2033 context_wrapper.rotatePrimitive_axisVector(self.
context, UUID, angle, axis_list)
2034 elif isinstance(UUID, list):
2035 context_wrapper.rotatePrimitives_axisVector(self.
context, UUID, angle, axis_list)
2037 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2040 if not isinstance(origin, vec3):
2041 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
2043 origin_list = origin.to_list()
2044 if isinstance(UUID, int):
2045 context_wrapper.rotatePrimitive_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
2046 elif isinstance(UUID, list):
2047 context_wrapper.rotatePrimitives_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
2049 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2051 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
2053 def rotateObject(self, ObjID: Union[int, List[int]], angle: float,
2054 axis: Union[str, vec3], origin: Optional[vec3] =
None,
2055 about_origin: bool =
False) ->
None:
2057 Rotate one or more objects.
2060 ObjID: Single object ID or list of object IDs to rotate
2061 angle: Rotation angle in radians
2062 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
2063 origin: Optional rotation origin point. If None, rotates about object center.
2064 If provided with string axis, raises ValueError.
2065 about_origin: If True, rotate about the object's own stored origin point
2066 (``object_origin``), which for most objects is its construction center —
2067 NOT the global origin (0,0,0). An object built away from the world origin
2068 therefore spins in place rather than orbiting the world origin. To orbit a
2069 specific point, pass that point as ``origin`` instead. Cannot be used with
2070 the origin parameter.
2073 ValueError: If axis is invalid or if origin and about_origin are both specified
2078 if origin
is not None and about_origin:
2079 raise ValueError(
"Cannot specify both origin and about_origin")
2082 if isinstance(axis, str):
2083 if axis
not in (
'x',
'y',
'z'):
2084 raise ValueError(
"axis must be 'x', 'y', or 'z'")
2085 if origin
is not None:
2086 raise ValueError(
"origin parameter cannot be used with string axis")
2088 raise ValueError(
"about_origin parameter cannot be used with string axis")
2091 if isinstance(ObjID, int):
2092 context_wrapper.rotateObject_axisString(self.
context, ObjID, angle, axis)
2093 elif isinstance(ObjID, list):
2094 context_wrapper.rotateObjects_axisString(self.
context, ObjID, angle, axis)
2096 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2098 elif isinstance(axis, vec3):
2099 axis_list = axis.to_list()
2102 if all(abs(v) < 1e-10
for v
in axis_list):
2103 raise ValueError(
"axis vector cannot be zero")
2107 if isinstance(ObjID, int):
2108 context_wrapper.rotateObjectAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
2109 elif isinstance(ObjID, list):
2110 context_wrapper.rotateObjectsAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
2112 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2113 elif origin
is None:
2115 if isinstance(ObjID, int):
2116 context_wrapper.rotateObject_axisVector(self.
context, ObjID, angle, axis_list)
2117 elif isinstance(ObjID, list):
2118 context_wrapper.rotateObjects_axisVector(self.
context, ObjID, angle, axis_list)
2120 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2123 if not isinstance(origin, vec3):
2124 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
2126 origin_list = origin.to_list()
2127 if isinstance(ObjID, int):
2128 context_wrapper.rotateObject_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
2129 elif isinstance(ObjID, list):
2130 context_wrapper.rotateObjects_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
2132 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2134 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
2136 def scalePrimitive(self, UUID: Union[int, List[int]], scale: vec3, point: Optional[vec3] =
None) ->
None:
2138 Scale one or more primitives.
2141 UUID: Single UUID or list of UUIDs to scale
2142 scale: Scale factors as vec3(x, y, z)
2143 point: Optional point to scale about. If None, scales about primitive center.
2146 ValueError: If scale or point parameters are invalid
2150 if not isinstance(scale, vec3):
2151 raise ValueError(f
"scale must be a vec3, got {type(scale).__name__}")
2153 scale_list = scale.to_list()
2157 if isinstance(UUID, int):
2158 context_wrapper.scalePrimitive(self.
context, UUID, scale_list)
2159 elif isinstance(UUID, list):
2160 context_wrapper.scalePrimitives(self.
context, UUID, scale_list)
2162 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2165 if not isinstance(point, vec3):
2166 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
2168 point_list = point.to_list()
2169 if isinstance(UUID, int):
2170 context_wrapper.scalePrimitiveAboutPoint(self.
context, UUID, scale_list, point_list)
2171 elif isinstance(UUID, list):
2172 context_wrapper.scalePrimitivesAboutPoint(self.
context, UUID, scale_list, point_list)
2174 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2176 def scaleObject(self, ObjID: Union[int, List[int]], scale: vec3,
2177 point: Optional[vec3] =
None, about_center: bool =
False,
2178 about_origin: bool =
False) ->
None:
2180 Scale one or more objects.
2183 ObjID: Single object ID or list of object IDs to scale
2184 scale: Scale factors as vec3(x, y, z)
2185 point: Optional point to scale about
2186 about_center: If True, scale about object center (default behavior)
2187 about_origin: If True, scale about the object's own stored origin point
2188 (``object_origin``), not the global origin (0,0,0). Pass ``point`` to
2189 scale about a specific location instead.
2192 ValueError: If parameters are invalid or conflicting options specified
2197 options_count = sum([point
is not None, about_center, about_origin])
2198 if options_count > 1:
2199 raise ValueError(
"Cannot specify multiple scaling options (point, about_center, about_origin)")
2201 if not isinstance(scale, vec3):
2202 raise ValueError(f
"scale must be a vec3, got {type(scale).__name__}")
2204 scale_list = scale.to_list()
2208 if isinstance(ObjID, int):
2209 context_wrapper.scaleObjectAboutOrigin(self.
context, ObjID, scale_list)
2210 elif isinstance(ObjID, list):
2211 context_wrapper.scaleObjectsAboutOrigin(self.
context, ObjID, scale_list)
2213 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2216 if isinstance(ObjID, int):
2217 context_wrapper.scaleObjectAboutCenter(self.
context, ObjID, scale_list)
2218 elif isinstance(ObjID, list):
2219 context_wrapper.scaleObjectsAboutCenter(self.
context, ObjID, scale_list)
2221 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2222 elif point
is not None:
2224 if not isinstance(point, vec3):
2225 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
2227 point_list = point.to_list()
2228 if isinstance(ObjID, int):
2229 context_wrapper.scaleObjectAboutPoint(self.
context, ObjID, scale_list, point_list)
2230 elif isinstance(ObjID, list):
2231 context_wrapper.scaleObjectsAboutPoint(self.
context, ObjID, scale_list, point_list)
2233 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2236 if isinstance(ObjID, int):
2237 context_wrapper.scaleObject(self.
context, ObjID, scale_list)
2238 elif isinstance(ObjID, list):
2239 context_wrapper.scaleObjects(self.
context, ObjID, scale_list)
2241 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2245 Scale the length of a Cone object by scaling the distance between its two nodes.
2248 ObjID: Object ID of the Cone to scale
2249 scale_factor: Factor by which to scale the cone length (e.g., 2.0 doubles length)
2252 ValueError: If ObjID is not an integer or scale_factor is invalid
2253 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
2256 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
2257 method, enforcing better encapsulation.
2260 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
2261 >>> context.scaleConeObjectLength(cone_id, 1.5) # Make cone 50% longer
2263 if not isinstance(ObjID, int):
2264 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
2265 if not isinstance(scale_factor, (int, float)):
2266 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
2267 if scale_factor <= 0:
2268 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
2270 context_wrapper.scaleConeObjectLength(self.
context, ObjID, float(scale_factor))
2274 Scale the girth of a Cone object by scaling the radii at both nodes.
2277 ObjID: Object ID of the Cone to scale
2278 scale_factor: Factor by which to scale the cone girth (e.g., 2.0 doubles girth)
2281 ValueError: If ObjID is not an integer or scale_factor is invalid
2282 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
2285 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
2286 method, enforcing better encapsulation.
2289 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
2290 >>> context.scaleConeObjectGirth(cone_id, 2.0) # Double the cone girth
2292 if not isinstance(ObjID, int):
2293 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
2294 if not isinstance(scale_factor, (int, float)):
2295 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
2296 if scale_factor <= 0:
2297 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
2299 context_wrapper.scaleConeObjectGirth(self.
context, ObjID, float(scale_factor))
2301 def loadPLY(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
2302 rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None,
2303 upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
2305 Load geometry from a PLY (Stanford Polygon) file.
2308 filename: Path to the PLY file to load
2309 origin: Origin point for positioning the geometry (optional)
2310 height: Absolute height in metres of the loaded geometry after scaling -- not a multiplier; the model is uniformly scaled so its vertical extent equals this value (optional)
2311 rotation: Rotation to apply to the geometry (optional)
2312 color: Default color for geometry without color data (optional)
2313 upaxis: Up axis orientation ("YUP" or "ZUP")
2314 silent: If True, suppress loading output messages
2317 List of UUIDs for the loaded primitives
2322 if origin
is not None and not isinstance(origin, vec3):
2323 raise ValueError(f
"Origin must be a vec3 or None, got {type(origin).__name__}")
2324 if rotation
is not None and not isinstance(rotation, SphericalCoord):
2325 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
2326 if color
is not None and not isinstance(color, RGBcolor):
2327 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
2332 if origin
is None and height
is None and rotation
is None and color
is None:
2334 return context_wrapper.loadPLY(self.
context, validated_filename, silent)
2336 elif origin
is not None and height
is not None and rotation
is None and color
is None:
2338 return context_wrapper.loadPLYWithOriginHeight(self.
context, validated_filename, origin.to_list(), height, upaxis, silent)
2340 elif origin
is not None and height
is not None and rotation
is not None and color
is None:
2342 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
2343 return context_wrapper.loadPLYWithOriginHeightRotation(self.
context, validated_filename, origin.to_list(), height, rotation_list, upaxis, silent)
2345 elif origin
is not None and height
is not None and rotation
is None and color
is not None:
2347 return context_wrapper.loadPLYWithOriginHeightColor(self.
context, validated_filename, origin.to_list(), height, color.to_list(), upaxis, silent)
2349 elif origin
is not None and height
is not None and rotation
is not None and color
is not None:
2351 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
2352 return context_wrapper.loadPLYWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation_list, color.to_list(), upaxis, silent)
2355 raise ValueError(
"Invalid parameter combination. When using transformations, both origin and height are required.")
2357 def loadOBJ(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
2358 scale: Optional[vec3] =
None, rotation: Optional[SphericalCoord] =
None,
2359 color: Optional[RGBcolor] =
None, upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
2361 Load geometry from an OBJ (Wavefront) file.
2364 filename: Path to the OBJ file to load
2365 origin: Origin point for positioning the geometry (optional)
2366 height: Absolute height in metres of the loaded geometry after scaling -- not a multiplier (optional, alternative to scale)
2367 scale: Scale factor for all dimensions (optional, alternative to height)
2368 rotation: Rotation to apply to the geometry (optional)
2369 color: Default color for geometry without color data (optional)
2370 upaxis: Up axis orientation ("YUP" or "ZUP")
2371 silent: If True, suppress loading output messages
2374 List of UUIDs for the loaded primitives
2379 if origin
is not None and not isinstance(origin, vec3):
2380 raise ValueError(f
"Origin must be a vec3 or None, got {type(origin).__name__}")
2381 if scale
is not None and not isinstance(scale, vec3):
2382 raise ValueError(f
"Scale must be a vec3 or None, got {type(scale).__name__}")
2383 if rotation
is not None and not isinstance(rotation, SphericalCoord):
2384 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
2385 if color
is not None and not isinstance(color, RGBcolor):
2386 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
2391 if origin
is None and height
is None and scale
is None and rotation
is None and color
is None:
2393 return context_wrapper.loadOBJ(self.
context, validated_filename, silent)
2395 elif origin
is not None and height
is not None and scale
is None and rotation
is not None and color
is not None:
2397 return context_wrapper.loadOBJWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), silent)
2399 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":
2401 return context_wrapper.loadOBJWithOriginHeightRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), upaxis, silent)
2403 elif origin
is not None and scale
is not None and rotation
is not None and color
is not None:
2405 return context_wrapper.loadOBJWithOriginScaleRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), scale.to_list(), rotation.to_list(), color.to_list(), upaxis, silent)
2408 raise ValueError(
"Invalid parameter combination. For OBJ loading, you must provide either: " +
2409 "1) No parameters (simple load), " +
2410 "2) origin + height + rotation + color, " +
2411 "3) origin + height + rotation + color + upaxis, or " +
2412 "4) origin + scale + rotation + color + upaxis")
2414 def loadXML(self, filename: str, quiet: bool =
False) -> List[int]:
2416 Load geometry from a Helios XML file.
2419 filename: Path to the XML file to load
2420 quiet: If True, suppress loading output messages
2423 List of UUIDs for the loaded primitives
2429 return context_wrapper.loadXML(self.
context, validated_filename, quiet)
2431 def writePLY(self, filename: str, UUIDs: Optional[List[int]] =
None) ->
None:
2433 Write geometry to a PLY (Stanford Polygon) file.
2436 filename: Path to the output PLY file
2437 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
2440 ValueError: If filename is invalid or UUIDs are invalid
2441 PermissionError: If output directory is not writable
2442 FileNotFoundError: If UUIDs do not exist in context
2443 RuntimeError: If Context is in mock mode
2446 >>> context.writePLY("output.ply") # Export all primitives
2447 >>> context.writePLY("subset.ply", [uuid1, uuid2]) # Export specific primitives
2456 context_wrapper.writePLY(self.
context, validated_filename)
2460 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
2466 context_wrapper.writePLYWithUUIDs(self.
context, validated_filename, UUIDs)
2468 def writeOBJ(self, filename: str, UUIDs: Optional[List[int]] =
None,
2469 primitive_data_fields: Optional[List[str]] =
None,
2470 write_normals: bool =
False, silent: bool =
False) ->
None:
2472 Write geometry to an OBJ (Wavefront) file.
2475 filename: Path to the output OBJ file
2476 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
2477 primitive_data_fields: Optional list of primitive data field names to export
2478 write_normals: Whether to include vertex normals in the output
2479 silent: Whether to suppress output messages during export
2482 ValueError: If filename is invalid, UUIDs are invalid, or data fields don't exist
2483 PermissionError: If output directory is not writable
2484 FileNotFoundError: If UUIDs do not exist in context
2485 RuntimeError: If Context is in mock mode
2488 >>> context.writeOBJ("output.obj") # Export all primitives
2489 >>> context.writeOBJ("subset.obj", [uuid1, uuid2]) # Export specific primitives
2490 >>> context.writeOBJ("with_data.obj", [uuid1], ["temperature", "area"]) # Export with data
2499 context_wrapper.writeOBJ(self.
context, validated_filename, write_normals, silent)
2500 elif primitive_data_fields
is None:
2503 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
2508 context_wrapper.writeOBJWithUUIDs(self.
context, validated_filename, UUIDs, write_normals, silent)
2512 raise ValueError(
"UUIDs list cannot be empty when exporting primitive data")
2513 if not primitive_data_fields:
2514 raise ValueError(
"primitive_data_fields list cannot be empty")
2522 context_wrapper.writeOBJWithPrimitiveData(self.
context, validated_filename, UUIDs, primitive_data_fields, write_normals, silent)
2525 UUIDs: Optional[List[int]] =
None,
2526 print_header: bool =
False) ->
None:
2528 Write primitive data to an ASCII text file.
2530 Outputs a space-separated text file where each row corresponds to a primitive
2531 and each column corresponds to a primitive data label.
2534 filename: Path to the output file
2535 column_labels: List of primitive data labels to include as columns.
2536 Use "UUID" to include primitive UUIDs as a column.
2537 The order determines the column order in the output file.
2538 UUIDs: Optional list of primitive UUIDs to include. If None, includes all primitives.
2539 print_header: If True, writes column labels as the first line of the file
2542 ValueError: If filename is invalid, column_labels is empty, or UUIDs list is empty when provided
2543 HeliosFileIOError: If file cannot be written
2544 HeliosRuntimeError: If a column label doesn't exist for any primitive
2547 >>> # Write temperature and area for all primitives
2548 >>> context.writePrimitiveData("output.txt", ["UUID", "temperature", "area"])
2550 >>> # Write with header row
2551 >>> context.writePrimitiveData("output.txt", ["UUID", "radiation_flux"], print_header=True)
2553 >>> # Write only for selected primitives
2554 >>> context.writePrimitiveData("subset.txt", ["temperature"], UUIDs=[uuid1, uuid2])
2559 if not column_labels:
2560 raise ValueError(
"column_labels list cannot be empty")
2567 context_wrapper.writePrimitiveData(self.
context, validated_filename, column_labels, print_header)
2571 raise ValueError(
"UUIDs list cannot be empty when provided. Use UUIDs=None to include all primitives")
2576 context_wrapper.writePrimitiveDataWithUUIDs(self.
context, validated_filename, column_labels, UUIDs, print_header)
2579 colors: Optional[np.ndarray] =
None) -> List[int]:
2581 Add triangles from NumPy arrays (compatible with trimesh, Open3D format).
2584 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2585 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2586 colors: Optional NumPy array of shape (N, 3) or (M, 3) containing RGB colors as float32/float64
2587 If shape (N, 3): per-vertex colors
2588 If shape (M, 3): per-triangle colors
2591 List of UUIDs for the added triangles
2594 ValueError: If array dimensions are invalid
2597 if vertices.ndim != 2
or vertices.shape[1] != 3:
2598 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2599 if faces.ndim != 2
or faces.shape[1] != 3:
2600 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2603 max_vertex_index = np.max(faces)
2604 if max_vertex_index >= vertices.shape[0]:
2605 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2608 per_vertex_colors =
False
2609 per_triangle_colors =
False
2610 if colors
is not None:
2611 if colors.ndim != 2
or colors.shape[1] != 3:
2612 raise ValueError(f
"Colors array must have shape (N, 3) or (M, 3), got {colors.shape}")
2613 if colors.shape[0] == vertices.shape[0]:
2614 per_vertex_colors =
True
2615 elif colors.shape[0] == faces.shape[0]:
2616 per_triangle_colors =
True
2618 raise ValueError(f
"Colors array shape {colors.shape} doesn't match vertices ({vertices.shape[0]},) or faces ({faces.shape[0]},)")
2621 vertices_float = vertices.astype(np.float32)
2622 faces_int = faces.astype(np.int32)
2623 if colors
is not None:
2624 colors_float = colors.astype(np.float32)
2629 corner0 = vertices_float[faces_int[:, 0]].tolist()
2630 corner1 = vertices_float[faces_int[:, 1]].tolist()
2631 corner2 = vertices_float[faces_int[:, 2]].tolist()
2638 elif per_triangle_colors:
2639 face_colors = colors_float.tolist()
2641 averaged = (colors_float[faces_int[:, 0]]
2642 + colors_float[faces_int[:, 1]]
2643 + colors_float[faces_int[:, 2]]) / 3.0
2644 face_colors = averaged.tolist()
2648 if face_colors
is None:
2649 for vertex0, vertex1, vertex2
in zip(corner0, corner1, corner2):
2650 triangle_uuids.append(
2651 context_wrapper.addTriangle(self.
context, vertex0, vertex1, vertex2))
2653 for vertex0, vertex1, vertex2, color
in zip(corner0, corner1, corner2, face_colors):
2654 triangle_uuids.append(
2655 context_wrapper.addTriangleWithColor(
2656 self.
context, vertex0, vertex1, vertex2, color))
2658 return triangle_uuids
2661 uv_coords: np.ndarray, texture_files: Union[str, List[str]],
2662 material_ids: Optional[np.ndarray] =
None) -> List[int]:
2664 Add textured triangles from NumPy arrays with support for multiple textures.
2666 This method supports both single-texture and multi-texture workflows:
2667 - Single texture: Pass a single texture file string, all faces use the same texture
2668 - Multiple textures: Pass a list of texture files and material_ids array specifying which texture each face uses
2671 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2672 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2673 uv_coords: NumPy array of shape (N, 2) containing UV texture coordinates as float32/float64
2674 texture_files: Single texture file path (str) or list of texture file paths (List[str])
2675 material_ids: Optional NumPy array of shape (M,) containing material ID for each face.
2676 If None and texture_files is a list, all faces use texture 0.
2677 If None and texture_files is a string, this parameter is ignored.
2680 List of UUIDs for the added textured triangles
2683 ValueError: If array dimensions are invalid or material IDs are out of range
2686 # Single texture usage (backward compatible)
2687 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, "texture.png")
2689 # Multi-texture usage (Open3D style)
2690 >>> texture_files = ["wood.png", "metal.png", "glass.png"]
2691 >>> material_ids = np.array([0, 0, 1, 1, 2, 2]) # 6 faces using different textures
2692 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, texture_files, material_ids)
2697 if vertices.ndim != 2
or vertices.shape[1] != 3:
2698 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2699 if faces.ndim != 2
or faces.shape[1] != 3:
2700 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2701 if uv_coords.ndim != 2
or uv_coords.shape[1] != 2:
2702 raise ValueError(f
"UV coordinates array must have shape (N, 2), got {uv_coords.shape}")
2705 if uv_coords.shape[0] != vertices.shape[0]:
2706 raise ValueError(f
"UV coordinates count ({uv_coords.shape[0]}) must match vertices count ({vertices.shape[0]})")
2709 max_vertex_index = np.max(faces)
2710 if max_vertex_index >= vertices.shape[0]:
2711 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2714 if isinstance(texture_files, str):
2716 texture_file_list = [texture_files]
2717 if material_ids
is None:
2718 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2721 if not np.all(material_ids == 0):
2722 raise ValueError(
"When using single texture file, all material IDs must be 0")
2725 texture_file_list = list(texture_files)
2726 if len(texture_file_list) == 0:
2727 raise ValueError(
"Texture files list cannot be empty")
2729 if material_ids
is None:
2731 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2734 if material_ids.ndim != 1
or material_ids.shape[0] != faces.shape[0]:
2735 raise ValueError(f
"Material IDs must have shape ({faces.shape[0]},), got {material_ids.shape}")
2738 max_material_id = np.max(material_ids)
2739 if max_material_id >= len(texture_file_list):
2740 raise ValueError(f
"Material ID {max_material_id} exceeds texture count {len(texture_file_list)}")
2743 for i, texture_file
in enumerate(texture_file_list):
2746 except (FileNotFoundError, ValueError)
as e:
2747 raise ValueError(f
"Texture file {i} ({texture_file}): {e}")
2750 if 'addTrianglesFromArraysMultiTextured' in context_wrapper._AVAILABLE_TRIANGLE_FUNCTIONS:
2751 return context_wrapper.addTrianglesFromArraysMultiTextured(
2752 self.
context, vertices, faces, uv_coords, texture_file_list, material_ids
2756 from .wrappers.DataTypes
import vec3, vec2
2758 vertices_float = vertices.astype(np.float32)
2759 faces_int = faces.astype(np.int32)
2760 uv_coords_float = uv_coords.astype(np.float32)
2763 for i
in range(faces.shape[0]):
2765 v0_idx, v1_idx, v2_idx = faces_int[i]
2768 vertex0 =
vec3(vertices_float[v0_idx][0], vertices_float[v0_idx][1], vertices_float[v0_idx][2])
2769 vertex1 =
vec3(vertices_float[v1_idx][0], vertices_float[v1_idx][1], vertices_float[v1_idx][2])
2770 vertex2 =
vec3(vertices_float[v2_idx][0], vertices_float[v2_idx][1], vertices_float[v2_idx][2])
2773 uv0 =
vec2(uv_coords_float[v0_idx][0], uv_coords_float[v0_idx][1])
2774 uv1 =
vec2(uv_coords_float[v1_idx][0], uv_coords_float[v1_idx][1])
2775 uv2 =
vec2(uv_coords_float[v2_idx][0], uv_coords_float[v2_idx][1])
2778 material_id = material_ids[i]
2779 texture_file = texture_file_list[material_id]
2783 triangle_uuids.append(uuid)
2785 return triangle_uuids
2793 Set primitive data as signed 32-bit integer for one or multiple primitives.
2796 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2797 label: String key for the data
2798 value: Signed integer scalar (broadcast to all UUIDs), or a list of
2799 values (one per UUID) to set a distinct value on each primitive.
2801 if isinstance(uuids_or_uuid, (list, tuple)):
2802 if isinstance(value, (list, tuple, np.ndarray)):
2803 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int', value)
2805 context_wrapper.setBroadcastPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2807 context_wrapper.setPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2811 Set primitive data as unsigned 32-bit integer for one or multiple primitives.
2813 Critical for properties like 'twosided_flag' which must be uint in C++.
2816 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2817 label: String key for the data
2818 value: Unsigned integer scalar (broadcast to all UUIDs), or a list of
2819 values (one per UUID) to set a distinct value on each primitive.
2821 if isinstance(uuids_or_uuid, (list, tuple)):
2822 if isinstance(value, (list, tuple, np.ndarray)):
2823 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'UInt', value)
2825 context_wrapper.setBroadcastPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2827 context_wrapper.setPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2831 Set primitive data as 32-bit float for one or multiple primitives.
2834 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2835 label: String key for the data
2836 value: Float scalar (broadcast to all UUIDs), or a list of values
2837 (one per UUID) to set a distinct value on each primitive.
2839 if isinstance(uuids_or_uuid, (list, tuple)):
2840 if isinstance(value, (list, tuple, np.ndarray)):
2841 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Float', value)
2843 context_wrapper.setBroadcastPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2845 context_wrapper.setPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2849 Set primitive data as 64-bit double for one or multiple primitives.
2852 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2853 label: String key for the data
2854 value: Double scalar (broadcast to all UUIDs), or a list of values
2855 (one per UUID) to set a distinct value on each primitive.
2857 if isinstance(uuids_or_uuid, (list, tuple)):
2858 if isinstance(value, (list, tuple, np.ndarray)):
2859 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Double', value)
2861 context_wrapper.setBroadcastPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2863 context_wrapper.setPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2867 Set primitive data as string for one or multiple primitives.
2870 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2871 label: String key for the data
2872 value: String scalar (broadcast to all UUIDs), or a list of strings
2873 (one per UUID) to set a distinct value on each primitive.
2875 if isinstance(uuids_or_uuid, (list, tuple)):
2876 if isinstance(value, (list, tuple, np.ndarray)):
2877 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'String', value)
2879 context_wrapper.setBroadcastPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2881 context_wrapper.setPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2885 Set primitive data as vec2 for one or multiple primitives.
2888 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2889 label: String key for the data
2890 x_or_vec: Either x component (float) or vec2 object
2891 y: Y component (if x_or_vec is float)
2893 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2894 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Vec2', x_or_vec)
2896 if hasattr(x_or_vec,
'x'):
2897 x, y = x_or_vec.x, x_or_vec.y
2900 if isinstance(uuids_or_uuid, (list, tuple)):
2901 context_wrapper.setBroadcastPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2903 context_wrapper.setPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2905 def setPrimitiveDataVec3(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
2907 Set primitive data as vec3 for one or multiple primitives.
2910 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2911 label: String key for the data
2912 x_or_vec: Either x component (float) or vec3 object
2913 y: Y component (if x_or_vec is float)
2914 z: Z component (if x_or_vec is float)
2916 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2917 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Vec3', x_or_vec)
2919 if hasattr(x_or_vec,
'x'):
2920 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2923 if isinstance(uuids_or_uuid, (list, tuple)):
2924 context_wrapper.setBroadcastPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2926 context_wrapper.setPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2928 def setPrimitiveDataVec4(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
2930 Set primitive data as vec4 for one or multiple primitives.
2933 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2934 label: String key for the data
2935 x_or_vec: Either x component (float) or vec4 object
2936 y: Y component (if x_or_vec is float)
2937 z: Z component (if x_or_vec is float)
2938 w: W component (if x_or_vec is float)
2940 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2941 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Vec4', x_or_vec)
2943 if hasattr(x_or_vec,
'x'):
2944 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2947 if isinstance(uuids_or_uuid, (list, tuple)):
2948 context_wrapper.setBroadcastPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
2950 context_wrapper.setPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
2954 Set primitive data as int2 for one or multiple primitives.
2957 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2958 label: String key for the data
2959 x_or_vec: Either x component (int) or int2 object
2960 y: Y component (if x_or_vec is int)
2962 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2963 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int2', x_or_vec)
2965 if hasattr(x_or_vec,
'x'):
2966 x, y = x_or_vec.x, x_or_vec.y
2969 if isinstance(uuids_or_uuid, (list, tuple)):
2970 context_wrapper.setBroadcastPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
2972 context_wrapper.setPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
2974 def setPrimitiveDataInt3(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
2976 Set primitive data as int3 for one or multiple primitives.
2979 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2980 label: String key for the data
2981 x_or_vec: Either x component (int) or int3 object
2982 y: Y component (if x_or_vec is int)
2983 z: Z component (if x_or_vec is int)
2985 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2986 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int3', x_or_vec)
2988 if hasattr(x_or_vec,
'x'):
2989 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2992 if isinstance(uuids_or_uuid, (list, tuple)):
2993 context_wrapper.setBroadcastPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
2995 context_wrapper.setPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
2997 def setPrimitiveDataInt4(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
2999 Set primitive data as int4 for one or multiple primitives.
3002 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
3003 label: String key for the data
3004 x_or_vec: Either x component (int) or int4 object
3005 y: Y component (if x_or_vec is int)
3006 z: Z component (if x_or_vec is int)
3007 w: W component (if x_or_vec is int)
3009 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
3010 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int4', x_or_vec)
3012 if hasattr(x_or_vec,
'x'):
3013 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
3016 if isinstance(uuids_or_uuid, (list, tuple)):
3017 context_wrapper.setBroadcastPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
3019 context_wrapper.setPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
3023 Get primitive data for a specific primitive. If data_type is provided, it works like before.
3024 If data_type is None, it automatically detects the type and returns the appropriate value.
3027 uuid: UUID of the primitive
3028 label: String key for the data
3029 data_type: Optional. Python type to retrieve (int, uint, float, double, bool, str, vec2, vec3, vec4, int2, int3, int4, etc.)
3030 If None, auto-detects the type using C++ getPrimitiveDataType().
3033 The stored value of the specified or auto-detected type
3036 if data_type
is None:
3037 return context_wrapper.getPrimitiveDataAuto(self.
context, uuid, label)
3040 if data_type == int:
3041 return context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
3042 elif data_type == float:
3043 return context_wrapper.getPrimitiveDataFloat(self.
context, uuid, label)
3044 elif data_type == bool:
3046 int_value = context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
3047 return int_value != 0
3048 elif data_type == str:
3049 return context_wrapper.getPrimitiveDataString(self.
context, uuid, label)
3052 elif data_type == vec2:
3053 coords = context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
3054 return vec2(coords[0], coords[1])
3055 elif data_type == vec3:
3056 coords = context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
3057 return vec3(coords[0], coords[1], coords[2])
3058 elif data_type == vec4:
3059 coords = context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
3060 return vec4(coords[0], coords[1], coords[2], coords[3])
3061 elif data_type == int2:
3062 coords = context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
3063 return int2(coords[0], coords[1])
3064 elif data_type == int3:
3065 coords = context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
3066 return int3(coords[0], coords[1], coords[2])
3067 elif data_type == int4:
3068 coords = context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
3069 return int4(coords[0], coords[1], coords[2], coords[3])
3072 elif data_type ==
"uint":
3073 return context_wrapper.getPrimitiveDataUInt(self.
context, uuid, label)
3074 elif data_type ==
"double":
3075 return context_wrapper.getPrimitiveDataDouble(self.
context, uuid, label)
3078 elif data_type == list:
3080 return context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
3081 elif data_type ==
"list_vec2":
3082 return context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
3083 elif data_type ==
"list_vec4":
3084 return context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
3085 elif data_type ==
"list_int2":
3086 return context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
3087 elif data_type ==
"list_int3":
3088 return context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
3089 elif data_type ==
"list_int4":
3090 return context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
3093 raise ValueError(f
"Unsupported primitive data type: {data_type}. "
3094 f
"Supported types: int, float, bool, str, vec2, vec3, vec4, int2, int3, int4, "
3095 f
"'uint', 'double', list (for vec3), 'list_vec2', 'list_vec4', 'list_int2', 'list_int3', 'list_int4'")
3099 Check if primitive data exists for a specific primitive and label.
3102 uuid: UUID of the primitive
3103 label: String key for the data
3106 True if the data exists, False otherwise
3108 return context_wrapper.doesPrimitiveDataExistWrapper(self.
context, uuid, label)
3112 Convenience method to get float primitive data.
3115 uuid: UUID of the primitive
3116 label: String key for the data
3119 Float value stored for the primitive
3125 Get the Helios data type of primitive data.
3128 uuid: UUID of the primitive
3129 label: String key for the data
3132 HeliosDataType enum value as integer
3134 return context_wrapper.getPrimitiveDataTypeWrapper(self.
context, uuid, label)
3138 Get the size/length of primitive data (for vector data).
3141 uuid: UUID of the primitive
3142 label: String key for the data
3145 Size of data array, or 1 for scalar data
3147 return context_wrapper.getPrimitiveDataSizeWrapper(self.
context, uuid, label)
3150 """Raise if any of ``uuids`` lacks primitive data ``label``.
3152 Costs one native call per primitive, so call it only where the read that
3153 follows is itself per-primitive, or to diagnose a failure that has already
3157 ValueError: naming the first primitive that lacks the data
3161 raise ValueError(f
"Primitive data '{label}' does not exist for UUID {uuid}")
3165 Get primitive data values for multiple primitives as a NumPy array.
3167 This method retrieves primitive data for a list of UUIDs and returns the values
3168 as a NumPy array. The output array has the same length as the input UUID list,
3169 with each index corresponding to the primitive data value for that UUID.
3172 uuids: List of primitive UUIDs to get data for
3173 label: String key for the primitive data to retrieve
3176 NumPy array of primitive data values corresponding to each UUID.
3177 The array type depends on the data type:
3178 - int data: int32 array
3179 - uint data: uint32 array
3180 - float data: float32 array
3181 - double data: float64 array
3182 - vector data: float32 array with shape (N, vector_size)
3183 - string data: object array of strings
3186 ValueError: If UUID list is empty or UUIDs don't exist
3187 RuntimeError: If context is in mock mode or data doesn't exist for some UUIDs
3192 raise ValueError(
"UUID list cannot be empty")
3201 first_uuid = uuids[0]
3203 raise ValueError(f
"Primitive data '{label}' does not exist for UUID {first_uuid}")
3214 result = context_wrapper.getPrimitiveDataFloatArray(
3224 elif data_type
in _BULK_PRIMITIVE_DATA_TYPES:
3229 result = context_wrapper.getPrimitiveDataArrayBulk(
3230 self.
context, uuids, label, data_type)
3235 elif data_type == 10:
3239 result = context_wrapper.getPrimitiveDataStringArrayBulk(
3246 raise ValueError(f
"Unsupported primitive data type: {data_type}")
3252 colormap: str =
"hot", ncolors: int = 10,
3253 max_val: Optional[float] =
None, min_val: Optional[float] =
None):
3255 Color primitives based on primitive data values using pseudocolor mapping.
3257 This method applies a pseudocolor mapping to primitives based on the values
3258 of specified primitive data. The primitive colors are updated to reflect the
3259 data values using a color map.
3262 uuids: List of primitive UUIDs to color
3263 primitive_data: Name of primitive data to use for coloring (e.g., "radiation_flux_SW")
3264 colormap: Color map name - options include "hot", "cool", "parula", "rainbow", "gray", "lava"
3265 ncolors: Number of discrete colors in color map (default: 10)
3266 max_val: Maximum value for color scale (auto-determined if None)
3267 min_val: Minimum value for color scale (auto-determined if None)
3269 if max_val
is not None and min_val
is not None:
3270 context_wrapper.colorPrimitiveByDataPseudocolorWithRange(
3271 self.
context, uuids, primitive_data, colormap, ncolors, max_val, min_val)
3273 context_wrapper.colorPrimitiveByDataPseudocolor(
3274 self.
context, uuids, primitive_data, colormap, ncolors)
3277 def setTime(self, hour: int, minute: int = 0, second: int = 0):
3279 Set the simulation time.
3283 minute: Minute (0-59), defaults to 0
3284 second: Second (0-59), defaults to 0
3287 ValueError: If time values are out of range
3288 NotImplementedError: If time/date functions not available in current library build
3291 >>> context.setTime(14, 30) # Set to 2:30 PM
3292 >>> context.setTime(9, 15, 30) # Set to 9:15:30 AM
3294 context_wrapper.setTime(self.
context, hour, minute, second)
3296 def setDate(self, year: int, month: int, day: int):
3298 Set the simulation date.
3301 year: Year (1900-3000)
3306 ValueError: If date values are out of range
3307 NotImplementedError: If time/date functions not available in current library build
3310 >>> context.setDate(2023, 6, 21) # Set to June 21, 2023
3312 context_wrapper.setDate(self.
context, year, month, day)
3316 Set the simulation date using Julian day number.
3319 julian_day: Julian day (1-366)
3320 year: Year (1900-3000)
3323 ValueError: If values are out of range
3324 NotImplementedError: If time/date functions not available in current library build
3327 >>> context.setDateJulian(172, 2023) # Set to day 172 of 2023 (June 21)
3329 context_wrapper.setDateJulian(self.
context, julian_day, year)
3333 Get the current simulation time.
3336 Tuple of (hour, minute, second) as integers
3339 NotImplementedError: If time/date functions not available in current library build
3342 >>> hour, minute, second = context.getTime()
3343 >>> print(f"Current time: {hour:02d}:{minute:02d}:{second:02d}")
3349 Get the current simulation date.
3352 Tuple of (year, month, day) as integers
3355 NotImplementedError: If time/date functions not available in current library build
3358 >>> year, month, day = context.getDate()
3359 >>> print(f"Current date: {year}-{month:02d}-{day:02d}")
3367 def addTimeseriesData(self, label: str, value: float, date:
'Date', time:
'Time'):
3369 Add a data point to a timeseries variable.
3372 label: Name of the timeseries variable (e.g., "temperature")
3373 value: Value of the data point
3374 date (Date): Date of the data point
3375 time: Time of the data point
3378 ValueError: If label is empty, or date/time are wrong types
3379 NotImplementedError: If timeseries functions not available
3382 >>> from pyhelios.types import Date, Time
3383 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3386 if not isinstance(label, str)
or not label:
3387 raise ValueError(
"Label must be a non-empty string")
3388 if not isinstance(date, Date):
3389 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3390 if not isinstance(time, Time):
3391 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3393 context_wrapper.addTimeseriesData(
3394 self.
context, label, float(value),
3395 date.day, date.month, date.year,
3396 time.hour, time.minute, time.second
3401 Update the value of an existing timeseries data point.
3404 label: Name of the timeseries variable (must already exist)
3405 date (Date): Date of the existing point (must match exactly)
3406 time: Time of the existing point (must match exactly)
3407 new_value: Replacement value
3410 ValueError: If label is empty, or date/time are wrong types
3411 HeliosRuntimeError: If the variable does not exist or no point matches the (date, time)
3412 NotImplementedError: If timeseries functions not available
3415 >>> from pyhelios.types import Date, Time
3416 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3417 >>> context.updateTimeseriesData("temperature", Date(2024, 6, 15), Time(12, 0, 0), 26.5)
3420 if not isinstance(label, str)
or not label:
3421 raise ValueError(
"Label must be a non-empty string")
3422 if not isinstance(date, Date):
3423 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3424 if not isinstance(time, Time):
3425 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3427 context_wrapper.updateTimeseriesData(
3429 date.day, date.month, date.year,
3430 time.hour, time.minute, time.second,
3436 Set the Context date and time from a timeseries data point index.
3439 label: Name of the timeseries variable
3440 index: Index of the data point (0 = earliest, chronologically ordered)
3443 ValueError: If label is empty or index is negative
3444 NotImplementedError: If timeseries functions not available
3447 >>> context.setCurrentTimeseriesPoint("temperature", 0)
3450 if not isinstance(label, str)
or not label:
3451 raise ValueError(
"Label must be a non-empty string")
3452 if not isinstance(index, int)
or index < 0:
3453 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3455 context_wrapper.setCurrentTimeseriesPoint(self.
context, label, index)
3458 index: int =
None) -> float:
3460 Query a timeseries data value.
3462 Three modes of operation:
3463 - With date and time: returns interpolated value at the specified date/time
3464 - With index: returns value at the specified data point index
3465 - With neither: returns value at the current Context date/time
3468 label: Name of the timeseries variable
3469 date (Date): Date to query at (requires time as well)
3470 time: Time to query at (requires date as well)
3471 index: Index of the data point (0 = earliest)
3474 The timeseries value as a float
3477 ValueError: If both date/time and index are provided, or if date without time
3478 NotImplementedError: If timeseries functions not available
3481 >>> # Query at specific date/time
3482 >>> val = context.queryTimeseriesData("temperature", date=Date(2024, 6, 15), time=Time(12, 0, 0))
3483 >>> # Query by index
3484 >>> val = context.queryTimeseriesData("temperature", index=0)
3485 >>> # Query at current context time
3486 >>> val = context.queryTimeseriesData("temperature")
3489 if not isinstance(label, str)
or not label:
3490 raise ValueError(
"Label must be a non-empty string")
3492 has_datetime = date
is not None or time
is not None
3493 has_index = index
is not None
3495 if has_datetime
and has_index:
3496 raise ValueError(
"Cannot specify both date/time and index. Use one or the other.")
3499 if date
is None or time
is None:
3500 raise ValueError(
"Both date and time must be provided together")
3501 if not isinstance(date, Date):
3502 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3503 if not isinstance(time, Time):
3504 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3505 return context_wrapper.queryTimeseriesDataDateTime(
3507 date.day, date.month, date.year,
3508 time.hour, time.minute, time.second
3512 if not isinstance(index, int)
or index < 0:
3513 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3514 return context_wrapper.queryTimeseriesDataIndex(self.
context, label, index)
3516 return context_wrapper.queryTimeseriesDataCurrent(self.
context, label)
3520 Get the Time associated with a timeseries data point.
3523 label: Name of the timeseries variable
3524 index: Index of the data point (0 = earliest)
3527 Time object for the data point
3530 ValueError: If label is empty or index is negative
3531 NotImplementedError: If timeseries functions not available
3534 >>> t = context.queryTimeseriesTime("temperature", 0)
3535 >>> print(f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}")
3538 if not isinstance(label, str)
or not label:
3539 raise ValueError(
"Label must be a non-empty string")
3540 if not isinstance(index, int)
or index < 0:
3541 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3543 hour, minute, second = context_wrapper.queryTimeseriesTime(self.
context, label, index)
3544 return Time(hour=hour, minute=minute, second=second)
3548 Get the Date associated with a timeseries data point.
3551 label: Name of the timeseries variable
3552 index: Index of the data point (0 = earliest)
3555 Date object for the data point
3558 ValueError: If label is empty or index is negative
3559 NotImplementedError: If timeseries functions not available
3562 >>> d = context.queryTimeseriesDate("temperature", 0)
3563 >>> print(f"{d.year}-{d.month:02d}-{d.day:02d}")
3566 if not isinstance(label, str)
or not label:
3567 raise ValueError(
"Label must be a non-empty string")
3568 if not isinstance(index, int)
or index < 0:
3569 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3571 year, month, day = context_wrapper.queryTimeseriesDate(self.
context, label, index)
3572 return Date(year=year, month=month, day=day)
3576 Get the number of data points in a timeseries variable.
3579 label: Name of the timeseries variable
3582 Number of data points
3585 ValueError: If label is empty
3586 NotImplementedError: If timeseries functions not available
3589 >>> n = context.getTimeseriesLength("temperature")
3590 >>> print(f"Timeseries has {n} data points")
3593 if not isinstance(label, str)
or not label:
3594 raise ValueError(
"Label must be a non-empty string")
3596 return context_wrapper.getTimeseriesLength(self.
context, label)
3600 Check whether a timeseries variable exists.
3603 label: Name of the timeseries variable
3606 True if the variable exists, False otherwise
3609 ValueError: If label is empty
3610 NotImplementedError: If timeseries functions not available
3613 >>> if context.doesTimeseriesVariableExist("temperature"):
3614 ... print("Temperature data loaded")
3617 if not isinstance(label, str)
or not label:
3618 raise ValueError(
"Label must be a non-empty string")
3620 return context_wrapper.doesTimeseriesVariableExist(self.
context, label)
3624 List all existing timeseries variables.
3627 List of timeseries variable names
3630 NotImplementedError: If timeseries functions not available
3633 >>> variables = context.listTimeseriesVariables()
3634 >>> for var in variables:
3635 ... print(f" {var}: {context.getTimeseriesLength(var)} points")
3639 return context_wrapper.listTimeseriesVariables(self.
context)
3642 """Clear all timeseries data from the Context.
3644 Removes all timeseries variables and their associated date/time values.
3647 NotImplementedError: If timeseries functions not available
3650 >>> context.clearTimeseriesData()
3651 >>> context.listTimeseriesVariables()
3655 context_wrapper.clearTimeseriesData(self.
context)
3658 """Delete a single timeseries variable and all of its data points.
3660 Complements :meth:`clearTimeseriesData` (which removes all variables) and
3661 :meth:`updateTimeseriesData` (which modifies a single point).
3664 label: Name of the timeseries variable to delete.
3667 ValueError: If ``label`` is empty.
3668 NotImplementedError: If running against helios-core older than v1.3.72.
3671 If the variable does not exist, the underlying Helios API issues a
3672 non-fatal warning to stderr and the call is otherwise a no-op.
3675 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3676 >>> context.deleteTimeseriesVariable("temperature")
3677 >>> context.doesTimeseriesVariableExist("temperature")
3681 if not isinstance(label, str)
or not label:
3682 raise ValueError(
"Label must be a non-empty string")
3683 context_wrapper.deleteTimeseriesVariable(self.
context, label)
3686 """Delete a single timeseries data point at the given date and time.
3688 If ``label`` is provided, only that variable's matching point is removed. If ``label``
3689 is omitted (None), the matching point is removed from every timeseries variable.
3692 date (Date): Date of the data point to delete.
3693 time: Time of the data point to delete.
3694 label: Optional name of the timeseries variable. None applies to all variables.
3697 ValueError: If date/time are wrong types, or label is an empty string.
3698 NotImplementedError: If running against helios-core older than v1.3.73.
3701 If no matching data point exists, the underlying Helios API issues a non-fatal
3702 warning to stderr and the call is otherwise a no-op. Matching uses the same
3703 (date, time) encoding as :meth:`addTimeseriesData`.
3706 >>> from pyhelios.types import Date, Time
3707 >>> context.deleteTimeseriesDataPoint(Date(2024, 6, 15), Time(12, 0, 0), "temperature")
3710 if not isinstance(date, Date):
3711 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3712 if not isinstance(time, Time):
3713 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3714 if label
is not None and (
not isinstance(label, str)
or not label):
3715 raise ValueError(
"label must be a non-empty string or None")
3718 context_wrapper.deleteTimeseriesDataPointAll(
3720 date.day, date.month, date.year,
3721 time.hour, time.minute, time.second
3724 context_wrapper.deleteTimeseriesDataPoint(
3726 date.day, date.month, date.year,
3727 time.hour, time.minute, time.second
3731 delimiter: str =
",", date_string_format: str =
"YYYYMMDD",
3732 headerlines: int = 0):
3734 Load tabular timeseries data from a text file.
3736 The file should contain columns of data with dates/times and measured values.
3737 Column labels specify how each column should be interpreted. Special labels
3738 include "year", "DOY", "date", "datetime", "hour", "minute", "second", "time".
3739 Other labels become timeseries variable names.
3742 data_file: Path to the text file containing tabular data
3743 column_labels: List of column label strings specifying what each column contains
3744 delimiter: Column delimiter string (default: ",")
3745 date_string_format: Format of date strings in the file. Supported formats:
3746 "YYYYMMDD", "YYYYMMDDHH", "YYYYMMDDHHMM", "DD/MM/YYYY",
3747 "MM/DD/YYYY", "DDMMYYYY", "YYYY-MM-DD", "DD/MM/YYYY HH:MM",
3748 "MM/DD/YYYY HH:MM", "ISO8601" (default: "YYYYMMDD")
3749 headerlines: Number of header lines to skip (default: 0)
3752 ValueError: If data_file is empty, column_labels is empty, or delimiter is empty
3753 RuntimeError: If the file cannot be read or parsed
3754 NotImplementedError: If timeseries functions not available
3757 >>> context.loadTabularTimeseriesData(
3758 ... "weather_data.csv",
3759 ... column_labels=["date", "hour", "temperature", "humidity"],
3763 >>> temp = context.queryTimeseriesData("temperature", index=0)
3766 if not isinstance(data_file, str)
or not data_file:
3767 raise ValueError(
"data_file must be a non-empty string")
3768 if not isinstance(column_labels, list)
or not column_labels:
3769 raise ValueError(
"column_labels must be a non-empty list of strings")
3770 for i, label
in enumerate(column_labels):
3771 if not isinstance(label, str):
3772 raise ValueError(f
"column_labels[{i}] must be a string, got {type(label).__name__}")
3773 if not isinstance(delimiter, str)
or not delimiter:
3774 raise ValueError(
"delimiter must be a non-empty string")
3776 context_wrapper.loadTabularTimeseriesData(
3777 self.
context, data_file, column_labels, delimiter,
3778 date_string_format, headerlines
3785 def deletePrimitive(self, uuids_or_uuid: Union[int, List[int]]) ->
None:
3787 Delete one or more primitives from the context.
3789 This removes the primitive(s) entirely from the context. If a primitive
3790 belongs to a compound object, it will be removed from that object. If the
3791 object becomes empty after removal, it is automatically deleted.
3794 uuids_or_uuid: Single UUID (int) or list of UUIDs to delete
3797 RuntimeError: If any UUID doesn't exist in the context
3798 ValueError: If UUID is invalid (negative)
3799 NotImplementedError: If delete functions not available in current library build
3802 >>> context = Context()
3803 >>> patch_id = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
3804 >>> context.deletePrimitive(patch_id) # Single deletion
3806 >>> # Multiple deletion
3807 >>> ids = [context.addPatch() for _ in range(5)]
3808 >>> context.deletePrimitive(ids) # Delete all at once
3812 if isinstance(uuids_or_uuid, (list, tuple)):
3813 for uuid
in uuids_or_uuid:
3815 raise ValueError(f
"UUID must be non-negative, got {uuid}")
3816 context_wrapper.deletePrimitives(self.
context, list(uuids_or_uuid))
3818 if uuids_or_uuid < 0:
3819 raise ValueError(f
"UUID must be non-negative, got {uuids_or_uuid}")
3820 context_wrapper.deletePrimitive(self.
context, uuids_or_uuid)
3822 def deleteObject(self, objIDs_or_objID: Union[int, List[int]]) ->
None:
3824 Delete one or more compound objects from the context.
3826 This removes the compound object(s) AND all their child primitives.
3827 Use this when you want to delete an entire object hierarchy at once.
3830 objIDs_or_objID: Single object ID (int) or list of object IDs to delete
3833 RuntimeError: If any object ID doesn't exist in the context
3834 ValueError: If object ID is invalid (negative)
3835 NotImplementedError: If delete functions not available in current library build
3838 >>> context = Context()
3839 >>> # Create a compound object (e.g., a tile with multiple patches)
3840 >>> patch_ids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2),
3841 ... tile_divisions=int2(2, 2))
3842 >>> obj_id = context.getPrimitiveParentObjectID(patch_ids[0])
3843 >>> context.deleteObject(obj_id) # Deletes tile and all its patches
3847 if isinstance(objIDs_or_objID, (list, tuple)):
3848 for objID
in objIDs_or_objID:
3850 raise ValueError(f
"Object ID must be non-negative, got {objID}")
3851 context_wrapper.deleteObjects(self.
context, list(objIDs_or_objID))
3853 if objIDs_or_objID < 0:
3854 raise ValueError(f
"Object ID must be non-negative, got {objIDs_or_objID}")
3855 context_wrapper.deleteObject(self.
context, objIDs_or_objID)
3860 Get list of available plugins for this PyHelios instance.
3863 List of available plugin names
3869 Check if a specific plugin is available.
3872 plugin_name: Name of the plugin to check
3875 True if plugin is available, False otherwise
3881 Get detailed information about available plugin capabilities.
3884 Dictionary mapping plugin names to capability information
3889 """Print detailed plugin status information."""
3894 Get list of requested plugins that are not available.
3897 requested_plugins: List of plugin names to check
3900 List of missing plugin names
3910 Create a new material for sharing visual properties across primitives.
3912 Materials enable efficient memory usage by allowing multiple primitives to
3913 share rendering properties. Changes to a material affect all primitives using it.
3916 material_label: Unique label for the material
3919 RuntimeError: If material label already exists
3922 >>> context.addMaterial("wood_oak")
3923 >>> context.setMaterialColor("wood_oak", (0.6, 0.4, 0.2, 1.0))
3924 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3926 context_wrapper.addMaterial(self.
context, material_label)
3929 """Check if a material with the given label exists."""
3930 return context_wrapper.doesMaterialExist(self.
context, material_label)
3933 """Get list of all material labels in the context."""
3934 return context_wrapper.listMaterials(self.
context)
3938 Delete a material from the context.
3940 Primitives using this material will be reassigned to the default material.
3943 material_label: Label of the material to delete
3946 RuntimeError: If material doesn't exist
3948 context_wrapper.deleteMaterial(self.
context, material_label)
3952 Get the RGBA color of a material.
3955 material_label: Label of the material
3961 RuntimeError: If material doesn't exist
3963 from .wrappers.DataTypes
import RGBAcolor
3964 color_list = context_wrapper.getMaterialColor(self.
context, material_label)
3965 return RGBAcolor(color_list[0], color_list[1], color_list[2], color_list[3])
3969 Set the RGBA color of a material.
3971 This affects all primitives that reference this material.
3974 material_label: Label of the material
3975 color: RGBAcolor object or tuple/list of (r, g, b, a) values
3978 RuntimeError: If material doesn't exist
3981 >>> from pyhelios.types import RGBAcolor
3982 >>> context.setMaterialColor("wood", RGBAcolor(0.6, 0.4, 0.2, 1.0))
3983 >>> context.setMaterialColor("wood", (0.6, 0.4, 0.2, 1.0))
3985 if isinstance(color, RGBAcolor):
3986 r, g, b, a = color.r, color.g, color.b, color.a
3987 elif isinstance(color, (list, tuple))
and len(color) == 4:
3988 r, g, b, a = color[0], color[1], color[2], color[3]
3990 raise ValueError(f
"Color must be an RGBAcolor or a 4-element list/tuple, got {type(color).__name__}")
3991 context_wrapper.setMaterialColor(self.
context, material_label, r, g, b, a)
3995 Get the texture file path for a material.
3998 material_label: Label of the material
4001 Texture file path, or empty string if no texture
4004 RuntimeError: If material doesn't exist
4006 return context_wrapper.getMaterialTexture(self.
context, material_label)
4010 Set the texture file for a material.
4012 This affects all primitives that reference this material.
4015 material_label: Label of the material
4016 texture_file: Path to texture image file
4019 RuntimeError: If material doesn't exist or texture file not found
4021 context_wrapper.setMaterialTexture(self.
context, material_label, texture_file)
4024 """Check if material texture color is overridden by material color."""
4025 return context_wrapper.isMaterialTextureColorOverridden(self.
context, material_label)
4028 """Set whether material color overrides texture color."""
4029 context_wrapper.setMaterialTextureColorOverride(self.
context, material_label, override)
4032 """Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
4033 return context_wrapper.getMaterialTwosidedFlag(self.
context, material_label)
4036 """Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
4037 context_wrapper.setMaterialTwosidedFlag(self.
context, material_label, twosided_flag)
4041 Assign a material to primitive(s).
4044 uuid: Single UUID (int) or list of UUIDs (List[int])
4045 material_label: Label of the material to assign
4048 RuntimeError: If primitive or material doesn't exist
4051 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
4052 >>> context.assignMaterialToPrimitive([uuid1, uuid2, uuid3], "wood_oak")
4054 if isinstance(uuid, (list, tuple)):
4055 context_wrapper.assignMaterialToPrimitives(self.
context, uuid, material_label)
4057 context_wrapper.assignMaterialToPrimitive(self.
context, uuid, material_label)
4061 Assign a material to all primitives in compound object(s).
4064 objID: Single object ID (int) or list of object IDs (List[int])
4065 material_label: Label of the material to assign
4068 RuntimeError: If object or material doesn't exist
4071 >>> tree_id = wpt.buildTree(WPTType.LEMON)
4072 >>> context.assignMaterialToObject(tree_id, "tree_bark")
4073 >>> context.assignMaterialToObject([id1, id2], "grass")
4075 if isinstance(objID, (list, tuple)):
4076 context_wrapper.assignMaterialToObjects(self.
context, objID, material_label)
4078 context_wrapper.assignMaterialToObject(self.
context, objID, material_label)
4081 """Get the material label assigned to a primitive or multiple primitives.
4084 uuid: Single UUID (int) or list of UUIDs
4087 str for single UUID, or List[str] for list
4090 RuntimeError: If primitive doesn't exist
4092 if isinstance(uuid, (list, tuple)):
4096 ptr, offsets, total = context_wrapper.getBatchPrimitiveMaterialLabels(self.
context, uuid)
4097 if total == 0
or not ptr:
4098 return [
"" for _
in uuid]
4099 full_str = ptr.decode(
'utf-8')
if isinstance(ptr, bytes)
else ptr
4100 return [full_str[offsets[i]:offsets[i+1]]
for i
in range(len(uuid))]
4101 return context_wrapper.getPrimitiveMaterialLabel(self.
context, uuid)
4105 Get two-sided rendering flag for a primitive.
4107 Checks material first, then primitive data if no material assigned.
4110 uuid: UUID of the primitive
4111 default_value: Default value if no material/data (default 1 = two-sided)
4114 Two-sided flag (0 = one-sided, 1 = two-sided)
4116 return context_wrapper.getPrimitiveTwosidedFlag(self.
context, uuid, default_value)
4120 Get all primitive UUIDs that use a specific material.
4123 material_label: Label of the material
4126 List of primitive UUIDs using the material
4129 RuntimeError: If material doesn't exist
4131 return context_wrapper.getPrimitivesUsingMaterial(self.
context, material_label)
4138 """Get the texture file path of a primitive or multiple primitives.
4141 uuid: Single UUID (int) or list of UUIDs
4144 str for single UUID, or List[str] for list
4147 if isinstance(uuid, (list, tuple)):
4150 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureFiles(self.
context, uuid)
4151 if total == 0
or not ptr:
4152 return [
"" for _
in uuid]
4153 full_str = ptr.decode(
'utf-8')
if isinstance(ptr, bytes)
else ptr
4154 return [full_str[offsets[i]:offsets[i+1]]
for i
in range(len(uuid))]
4155 return context_wrapper.getPrimitiveTextureFile(self.
context, uuid)
4158 """Resolve material texture suppression for export.
4160 For each primitive, applies material-based texture suppression rules:
4161 1. If primitive has texture but material has no texture -> suppress texture, use material color
4162 2. If both have texture and textureColorOverride -> prefix "mask:", use material color
4163 3. Otherwise -> leave unchanged
4166 uuids: List of primitive UUIDs
4167 colors_np: numpy float32 array of shape (N, 3), modified IN-PLACE
4170 List[str] of resolved texture file paths
4175 return context_wrapper.resolveMaterialTextures(self.
context, uuids, colors_np)
4178 """Pack GPU-ready geometry buffers for a set of primitives in a single C++ pass.
4180 Produces a binary blob containing contiguous typed arrays (positions,
4181 colors, uvs, indices, faceToUuid) grouped by texture, ready for
4182 zero-copy loading into Three.js BufferGeometry attributes.
4185 uuids: List of primitive UUIDs
4188 bytes: Raw binary blob (see wire format v2 spec)
4193 return context_wrapper.packGPUBuffers(self.
context, uuids)
4196 """Set the texture file path of a primitive.
4199 uuid: UUID of the primitive
4200 texture_file: Path to the texture file
4203 context_wrapper.setPrimitiveTextureFile(self.
context, uuid, texture_file)
4206 """Get the texture size (width, height) of a primitive.
4209 uuid: UUID of the primitive
4212 int2 with width and height of the texture
4215 w, h = context_wrapper.getPrimitiveTextureSize(self.
context, uuid)
4219 """Get the texture UV coordinates of a primitive or multiple primitives.
4222 uuid: Single UUID (int) or list of UUIDs
4225 List[vec2] for single UUID, or tuple of (flat_data, offsets) for list
4228 if isinstance(uuid, (list, tuple)):
4230 return (np.empty((0,), dtype=np.float32), np.zeros((1,), dtype=np.uint32))
4231 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureUV(self.
context, uuid)
4232 offsets_arr = np.asarray(offsets, dtype=np.uint32)
4233 if total == 0
or not ptr:
4234 return (np.empty((0,), dtype=np.float32), offsets_arr)
4235 data = np.ctypeslib.as_array(ptr, shape=(total,)).copy()
4236 return (data, offsets_arr)
4237 uv_pairs = context_wrapper.getPrimitiveTextureUV(self.
context, uuid)
4238 return [
vec2(u, v)
for u, v
in uv_pairs]
4241 """Check if primitive texture has a transparency channel.
4244 uuid: UUID of the primitive
4247 True if texture has transparency channel
4250 return context_wrapper.primitiveTextureHasTransparencyChannel(self.
context, uuid)
4253 """Get the solid fraction of a primitive or multiple primitives.
4256 uuid: Single UUID (int) or list of UUIDs
4259 float for single UUID, or np.ndarray of shape (N,) for list
4262 if isinstance(uuid, (list, tuple)):
4264 return np.empty((0,), dtype=np.float32)
4265 ptr, size = context_wrapper.getBatchPrimitiveSolidFractions(self.
context, uuid)
4266 if size == 0
or not ptr:
4267 return np.empty((0,), dtype=np.float32)
4268 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
4269 return context_wrapper.getPrimitiveSolidFraction(self.
context, uuid)
4272 """Override texture color with the primitive's constant RGB color.
4275 uuids_or_uuid: A single UUID (int) or a list of UUIDs. When a list is
4276 given, the override is applied to all of them in a single bulk call.
4279 if isinstance(uuids_or_uuid, (list, tuple)):
4280 context_wrapper.overridePrimitiveTextureColorBatchWrapper(self.
context, list(uuids_or_uuid))
4282 context_wrapper.overridePrimitiveTextureColor(self.
context, uuids_or_uuid)
4285 """Use texture-map color instead of the constant RGB color.
4288 uuids_or_uuid: A single UUID (int) or a list of UUIDs. When a list is
4289 given, all of them are restored in a single bulk call.
4292 if isinstance(uuids_or_uuid, (list, tuple)):
4293 context_wrapper.usePrimitiveTextureColorBatchWrapper(self.
context, list(uuids_or_uuid))
4295 context_wrapper.usePrimitiveTextureColor(self.
context, uuids_or_uuid)
4298 """Check if primitive texture color is overridden.
4301 uuid: UUID of the primitive
4304 True if texture color is overridden with constant RGB
4307 return context_wrapper.isPrimitiveTextureColorOverridden(self.
context, uuid)
4314 """Get normals for all primitives. Returns ndarray of shape (N, 3)."""
4318 """Get colors for all primitives. Returns ndarray of shape (N, 3)."""
4322 """Get areas for all primitives. Returns ndarray of shape (N,)."""
4326 """Get types for all primitives. Returns ndarray of shape (N,) uint32."""
4330 """Get solid fractions for all primitives. Returns ndarray of shape (N,)."""
4334 """Get vertices for all primitives. Returns (flat_data, offsets) tuple."""
4338 """Get texture files for all primitives. Returns list of strings."""
4342 """Get material labels for all primitives. Returns list of strings."""
4348 """Hide one or more primitives. Hidden primitives are excluded from getAllUUIDs().
4351 uuids_or_uuid: Single UUID (int) or list of UUIDs to hide.
4353 if isinstance(uuids_or_uuid, (list, tuple)):
4354 context_wrapper.hidePrimitivesWrapper(self.
context, list(uuids_or_uuid))
4356 context_wrapper.hidePrimitiveWrapper(self.
context, uuids_or_uuid)
4359 """Show one or more previously hidden primitives.
4362 uuids_or_uuid: Single UUID (int) or list of UUIDs to show.
4364 if isinstance(uuids_or_uuid, (list, tuple)):
4365 context_wrapper.showPrimitivesWrapper(self.
context, list(uuids_or_uuid))
4367 context_wrapper.showPrimitiveWrapper(self.
context, uuids_or_uuid)
4370 """Check if a primitive is hidden.
4373 uuid: UUID of the primitive.
4376 True if the primitive is hidden.
4378 return context_wrapper.isPrimitiveHiddenWrapper(self.
context, uuid)
4381 """Hide one or more compound objects (and all their primitives).
4384 objids_or_objid: Single object ID (int) or list of object IDs to hide.
4386 if isinstance(objids_or_objid, (list, tuple)):
4387 context_wrapper.hideObjectsWrapper(self.
context, list(objids_or_objid))
4389 context_wrapper.hideObjectWrapper(self.
context, objids_or_objid)
4391 def showObject(self, objids_or_objid) -> None:
4392 """Show one or more previously hidden compound objects.
4395 objids_or_objid: Single object ID (int) or list of object IDs to show.
4397 if isinstance(objids_or_objid, (list, tuple)):
4398 context_wrapper.showObjectsWrapper(self.
context, list(objids_or_objid))
4400 context_wrapper.showObjectWrapper(self.
context, objids_or_objid)
4403 """Check if a compound object is hidden.
4409 True if the object is hidden.
4411 return context_wrapper.isObjectHiddenWrapper(self.
context, objID)
4415 def setObjectDataInt(self, objids_or_objid, label: str, value: int) ->
None:
4416 """Set object data as signed 32-bit integer. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4417 if isinstance(objids_or_objid, (list, tuple)):
4418 if isinstance(value, (list, tuple, np.ndarray)):
4419 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int', value)
4421 context_wrapper.setBroadcastObjectDataInt(self.
context, objids_or_objid, label, value)
4423 context_wrapper.setObjectDataInt(self.
context, objids_or_objid, label, value)
4426 """Set object data as unsigned 32-bit integer. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4427 if isinstance(objids_or_objid, (list, tuple)):
4428 if isinstance(value, (list, tuple, np.ndarray)):
4429 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'UInt', value)
4431 context_wrapper.setBroadcastObjectDataUInt(self.
context, objids_or_objid, label, value)
4433 context_wrapper.setObjectDataUInt(self.
context, objids_or_objid, label, value)
4436 """Set object data as 32-bit float. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4437 if isinstance(objids_or_objid, (list, tuple)):
4438 if isinstance(value, (list, tuple, np.ndarray)):
4439 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Float', value)
4441 context_wrapper.setBroadcastObjectDataFloat(self.
context, objids_or_objid, label, value)
4443 context_wrapper.setObjectDataFloat(self.
context, objids_or_objid, label, value)
4446 """Set object data as 64-bit double. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4447 if isinstance(objids_or_objid, (list, tuple)):
4448 if isinstance(value, (list, tuple, np.ndarray)):
4449 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Double', value)
4451 context_wrapper.setBroadcastObjectDataDouble(self.
context, objids_or_objid, label, value)
4453 context_wrapper.setObjectDataDouble(self.
context, objids_or_objid, label, value)
4456 """Set object data as string. Scalar broadcasts to all objIDs; a list of strings sets a distinct value per objID."""
4457 if isinstance(objids_or_objid, (list, tuple)):
4458 if isinstance(value, (list, tuple, np.ndarray)):
4459 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'String', value)
4461 context_wrapper.setBroadcastObjectDataString(self.
context, objids_or_objid, label, value)
4463 context_wrapper.setObjectDataString(self.
context, objids_or_objid, label, value)
4465 def setObjectDataVec2(self, objids_or_objid, label: str, x_or_vec, y: float =
None) ->
None:
4466 """Set object data as vec2. Accepts a vec2 / x,y components, or a list of vec2 (one per objID)."""
4467 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4468 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Vec2', x_or_vec)
4470 if hasattr(x_or_vec,
'x')
and y
is None:
4471 x, y = x_or_vec.x, x_or_vec.y
4474 if isinstance(objids_or_objid, (list, tuple)):
4475 context_wrapper.setBroadcastObjectDataVec2(self.
context, objids_or_objid, label, x, y)
4477 context_wrapper.setObjectDataVec2(self.
context, objids_or_objid, label, x, y)
4479 def setObjectDataVec3(self, objids_or_objid, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
4480 """Set object data as vec3. Accepts a vec3 / x,y,z components, or a list of vec3 (one per objID)."""
4481 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4482 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Vec3', x_or_vec)
4484 if hasattr(x_or_vec,
'x')
and y
is None:
4485 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4488 if isinstance(objids_or_objid, (list, tuple)):
4489 context_wrapper.setBroadcastObjectDataVec3(self.
context, objids_or_objid, label, x, y, z)
4491 context_wrapper.setObjectDataVec3(self.
context, objids_or_objid, label, x, y, z)
4493 def setObjectDataVec4(self, objids_or_objid, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
4494 """Set object data as vec4. Accepts a vec4 / x,y,z,w components, or a list of vec4 (one per objID)."""
4495 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4496 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Vec4', x_or_vec)
4498 if hasattr(x_or_vec,
'x')
and y
is None:
4499 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4502 if isinstance(objids_or_objid, (list, tuple)):
4503 context_wrapper.setBroadcastObjectDataVec4(self.
context, objids_or_objid, label, x, y, z, w)
4505 context_wrapper.setObjectDataVec4(self.
context, objids_or_objid, label, x, y, z, w)
4507 def setObjectDataInt2(self, objids_or_objid, label: str, x_or_vec, y: int =
None) ->
None:
4508 """Set object data as int2. Accepts an int2 / x,y components, or a list of int2 (one per objID)."""
4509 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4510 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int2', x_or_vec)
4512 if hasattr(x_or_vec,
'x')
and y
is None:
4513 x, y = x_or_vec.x, x_or_vec.y
4516 if isinstance(objids_or_objid, (list, tuple)):
4517 context_wrapper.setBroadcastObjectDataInt2(self.
context, objids_or_objid, label, x, y)
4519 context_wrapper.setObjectDataInt2(self.
context, objids_or_objid, label, x, y)
4521 def setObjectDataInt3(self, objids_or_objid, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
4522 """Set object data as int3. Accepts an int3 / x,y,z components, or a list of int3 (one per objID)."""
4523 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4524 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int3', x_or_vec)
4526 if hasattr(x_or_vec,
'x')
and y
is None:
4527 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4530 if isinstance(objids_or_objid, (list, tuple)):
4531 context_wrapper.setBroadcastObjectDataInt3(self.
context, objids_or_objid, label, x, y, z)
4533 context_wrapper.setObjectDataInt3(self.
context, objids_or_objid, label, x, y, z)
4535 def setObjectDataInt4(self, objids_or_objid, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
4536 """Set object data as int4. Accepts an int4 / x,y,z,w components, or a list of int4 (one per objID)."""
4537 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4538 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int4', x_or_vec)
4540 if hasattr(x_or_vec,
'x')
and y
is None:
4541 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4544 if isinstance(objids_or_objid, (list, tuple)):
4545 context_wrapper.setBroadcastObjectDataInt4(self.
context, objids_or_objid, label, x, y, z, w)
4547 context_wrapper.setObjectDataInt4(self.
context, objids_or_objid, label, x, y, z, w)
4549 def getObjectData(self, objID: int, label: str, data_type: type =
None):
4550 """Get object data with optional type specification. Auto-detects type if not specified."""
4551 if data_type
is None:
4552 return context_wrapper.getObjectDataAuto(self.
context, objID, label)
4553 if data_type == int:
4554 return context_wrapper.getObjectDataInt(self.
context, objID, label)
4555 elif data_type == float:
4556 return context_wrapper.getObjectDataFloat(self.
context, objID, label)
4557 elif data_type == str:
4558 return context_wrapper.getObjectDataString(self.
context, objID, label)
4559 elif data_type == vec3:
4560 coords = context_wrapper.getObjectDataVec3(self.
context, objID, label)
4561 return vec3(coords[0], coords[1], coords[2])
4562 elif data_type == vec2:
4563 coords = context_wrapper.getObjectDataVec2(self.
context, objID, label)
4564 return vec2(coords[0], coords[1])
4565 elif data_type == vec4:
4566 coords = context_wrapper.getObjectDataVec4(self.
context, objID, label)
4567 return vec4(coords[0], coords[1], coords[2], coords[3])
4568 elif data_type == int2:
4569 coords = context_wrapper.getObjectDataInt2(self.
context, objID, label)
4570 return int2(coords[0], coords[1])
4571 elif data_type == int3:
4572 coords = context_wrapper.getObjectDataInt3(self.
context, objID, label)
4573 return int3(coords[0], coords[1], coords[2])
4574 elif data_type == int4:
4575 coords = context_wrapper.getObjectDataInt4(self.
context, objID, label)
4576 return int4(coords[0], coords[1], coords[2], coords[3])
4577 elif data_type ==
"uint":
4578 return context_wrapper.getObjectDataUInt(self.
context, objID, label)
4579 elif data_type ==
"double":
4580 return context_wrapper.getObjectDataDouble(self.
context, objID, label)
4582 raise ValueError(f
"Unsupported object data type: {data_type}")
4585 """Get float object data."""
4586 return context_wrapper.getObjectDataFloat(self.
context, objID, label)
4589 """Get int object data."""
4590 return context_wrapper.getObjectDataInt(self.
context, objID, label)
4593 """Get string object data."""
4594 return context_wrapper.getObjectDataString(self.
context, objID, label)
4597 """Get the HeliosDataType enum for object data."""
4598 return context_wrapper.getObjectDataTypeWrapper(self.
context, objID, label)
4601 """Get the size of object data array."""
4602 return context_wrapper.getObjectDataSizeWrapper(self.
context, objID, label)
4605 """Check if object data exists."""
4606 return context_wrapper.doesObjectDataExistWrapper(self.
context, objID, label)
4609 """Get object data values for multiple objects as a NumPy array.
4611 Reads one label across every object in a single native call, in the
4612 order the IDs were given. Reading them one at a time costs a ctypes
4613 round-trip per object.
4616 objids: Object IDs to read, controlling the result order
4617 label: Object data label to retrieve
4620 NumPy array with one entry per object: int32, uint32, float32 or
4621 float64 for the scalar types, and shape (N, components) for the
4622 vec2/3/4 and int2/3/4 types.
4625 ValueError: If the ID list is empty or the label does not exist
4626 NotImplementedError: If the data type has no bulk getter
4629 if not isinstance(objids, (list, tuple)):
4631 f
"objids must be a list of object IDs, got {type(objids).__name__}")
4633 raise ValueError(
"Object ID list cannot be empty")
4638 f
"Object data '{label}' does not exist for object {first}")
4642 return context_wrapper.getObjectDataStringArrayBulk(
4644 return context_wrapper.getObjectDataArrayBulk(
4645 self.
context, objids, label, data_type)
4648 """Clear object data. Accepts single ID or list."""
4649 if isinstance(objids_or_objid, (list, tuple)):
4650 context_wrapper.clearObjectDataBatchWrapper(self.
context, objids_or_objid, label)
4652 context_wrapper.clearObjectDataWrapper(self.
context, objids_or_objid, label)
4655 """Remove a named data field from every compound object in the Context.
4657 Clears the data with the given label from all objects (including hidden ones) and
4658 releases the registered data type for the label, so it may subsequently be
4659 re-registered with a different type. Requires helios-core v1.3.73 or newer.
4662 context_wrapper.clearAllObjectDataByLabelWrapper(self.
context, label)
4665 """List all data labels on a specific object."""
4666 return context_wrapper.listObjectDataWrapper(self.
context, objID)
4669 """List all object data labels in context."""
4670 return context_wrapper.listAllObjectDataLabelsWrapper(self.
context)
4673 """Copy object data to a new label."""
4674 context_wrapper.duplicateObjectDataWrapper(self.
context, objID, old_label, new_label)
4676 def renameObjectData(self, objID: int, old_label: str, new_label: str) ->
None:
4677 """Rename an object data label."""
4678 context_wrapper.renameObjectDataWrapper(self.
context, objID, old_label, new_label)
4680 def filterObjectsByData(self, objIDs: List[int], label: str, value, comparator: str =
"=") -> List[int]:
4681 """Filter objects by data value. Auto-dispatches based on value type."""
4682 if isinstance(value, str):
4683 return context_wrapper.filterObjectsByDataStringWrapper(self.
context, objIDs, label, value)
4684 elif isinstance(value, float):
4685 return context_wrapper.filterObjectsByDataFloatWrapper(self.
context, objIDs, label, value, comparator)
4686 elif isinstance(value, int):
4687 return context_wrapper.filterObjectsByDataIntWrapper(self.
context, objIDs, label, value, comparator)
4689 raise ValueError(f
"Unsupported filter value type: {type(value).__name__}")
4694 """Set global data as signed 32-bit integer."""
4695 context_wrapper.setGlobalDataInt(self.
context, label, value)
4698 """Set global data as unsigned 32-bit integer."""
4699 context_wrapper.setGlobalDataUInt(self.
context, label, value)
4702 """Set global data as 32-bit float."""
4703 context_wrapper.setGlobalDataFloat(self.
context, label, value)
4706 """Set global data as 64-bit double."""
4707 context_wrapper.setGlobalDataDouble(self.
context, label, value)
4710 """Set global data as string."""
4711 context_wrapper.setGlobalDataString(self.
context, label, value)
4714 """Set global data as vec2."""
4715 if hasattr(x_or_vec,
'x')
and y
is None:
4716 x, y = x_or_vec.x, x_or_vec.y
4719 context_wrapper.setGlobalDataVec2(self.
context, label, x, y)
4721 def setGlobalDataVec3(self, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
4722 """Set global data as vec3."""
4723 if hasattr(x_or_vec,
'x')
and y
is None:
4724 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4727 context_wrapper.setGlobalDataVec3(self.
context, label, x, y, z)
4729 def setGlobalDataVec4(self, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
4730 """Set global data as vec4."""
4731 if hasattr(x_or_vec,
'x')
and y
is None:
4732 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4735 context_wrapper.setGlobalDataVec4(self.
context, label, x, y, z, w)
4738 """Set global data as int2."""
4739 if hasattr(x_or_vec,
'x')
and y
is None:
4740 x, y = x_or_vec.x, x_or_vec.y
4743 context_wrapper.setGlobalDataInt2(self.
context, label, x, y)
4745 def setGlobalDataInt3(self, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
4746 """Set global data as int3."""
4747 if hasattr(x_or_vec,
'x')
and y
is None:
4748 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4751 context_wrapper.setGlobalDataInt3(self.
context, label, x, y, z)
4753 def setGlobalDataInt4(self, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
4754 """Set global data as int4."""
4755 if hasattr(x_or_vec,
'x')
and y
is None:
4756 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4759 context_wrapper.setGlobalDataInt4(self.
context, label, x, y, z, w)
4761 def getGlobalData(self, label: str, data_type: type =
None):
4762 """Get global data with optional type specification. Auto-detects type if not specified."""
4763 if data_type
is None:
4764 return context_wrapper.getGlobalDataAuto(self.
context, label)
4765 if data_type == int:
4766 return context_wrapper.getGlobalDataInt(self.
context, label)
4767 elif data_type == float:
4768 return context_wrapper.getGlobalDataFloat(self.
context, label)
4769 elif data_type == str:
4770 return context_wrapper.getGlobalDataString(self.
context, label)
4771 elif data_type == vec3:
4772 coords = context_wrapper.getGlobalDataVec3(self.
context, label)
4773 return vec3(coords[0], coords[1], coords[2])
4774 elif data_type == vec2:
4775 coords = context_wrapper.getGlobalDataVec2(self.
context, label)
4776 return vec2(coords[0], coords[1])
4777 elif data_type == vec4:
4778 coords = context_wrapper.getGlobalDataVec4(self.
context, label)
4779 return vec4(coords[0], coords[1], coords[2], coords[3])
4780 elif data_type == int2:
4781 coords = context_wrapper.getGlobalDataInt2(self.
context, label)
4782 return int2(coords[0], coords[1])
4783 elif data_type == int3:
4784 coords = context_wrapper.getGlobalDataInt3(self.
context, label)
4785 return int3(coords[0], coords[1], coords[2])
4786 elif data_type == int4:
4787 coords = context_wrapper.getGlobalDataInt4(self.
context, label)
4788 return int4(coords[0], coords[1], coords[2], coords[3])
4789 elif data_type ==
"uint":
4790 return context_wrapper.getGlobalDataUInt(self.
context, label)
4791 elif data_type ==
"double":
4792 return context_wrapper.getGlobalDataDouble(self.
context, label)
4794 raise ValueError(f
"Unsupported global data type: {data_type}")
4797 """Get float global data."""
4798 return context_wrapper.getGlobalDataFloat(self.
context, label)
4801 """Get int global data."""
4802 return context_wrapper.getGlobalDataInt(self.
context, label)
4805 """Get string global data."""
4806 return context_wrapper.getGlobalDataString(self.
context, label)
4809 """Get the HeliosDataType enum for global data."""
4810 return context_wrapper.getGlobalDataTypeWrapper(self.
context, label)
4813 """Get the size of global data array."""
4814 return context_wrapper.getGlobalDataSizeWrapper(self.
context, label)
4817 """Check if global data exists."""
4818 return context_wrapper.doesGlobalDataExistWrapper(self.
context, label)
4821 """Clear global data."""
4822 context_wrapper.clearGlobalDataWrapper(self.
context, label)
4825 """Rename a global data label."""
4826 context_wrapper.renameGlobalDataWrapper(self.
context, old_label, new_label)
4829 """Duplicate global data to a new label."""
4830 context_wrapper.duplicateGlobalDataWrapper(self.
context, old_label, new_label)
4833 """List all global data labels."""
4834 return context_wrapper.listGlobalDataWrapper(self.
context)
4837 """Increment global data. Auto-dispatches based on increment type."""
4838 if isinstance(increment, float):
4839 context_wrapper.incrementGlobalDataFloatWrapper(self.
context, label, increment)
4840 elif isinstance(increment, int):
4841 context_wrapper.incrementGlobalDataIntWrapper(self.
context, label, increment)
4843 raise ValueError(f
"Unsupported increment type: {type(increment).__name__}")
4848 """Calculate arithmetic mean of primitive data across UUIDs.
4851 uuids: List of primitive UUIDs.
4853 return_type: float (default), "double", or vec3.
4855 if return_type == float:
4856 return context_wrapper.calculatePrimitiveDataMeanFloatWrapper(self.
context, uuids, label)
4857 elif return_type ==
"double":
4858 return context_wrapper.calculatePrimitiveDataMeanDoubleWrapper(self.
context, uuids, label)
4859 elif return_type == vec3:
4860 coords = context_wrapper.calculatePrimitiveDataMeanVec3Wrapper(self.
context, uuids, label)
4861 return vec3(coords[0], coords[1], coords[2])
4863 raise ValueError(f
"Unsupported return type: {return_type}")
4866 """Calculate area-weighted mean of primitive data."""
4867 if return_type == float:
4868 return context_wrapper.calculatePrimitiveDataAreaWeightedMeanFloatWrapper(self.
context, uuids, label)
4870 raise ValueError(f
"Unsupported return type: {return_type}")
4873 """Calculate sum of primitive data across UUIDs."""
4874 if return_type == float:
4875 return context_wrapper.calculatePrimitiveDataSumFloatWrapper(self.
context, uuids, label)
4876 elif return_type ==
"double":
4877 return context_wrapper.calculatePrimitiveDataSumDoubleWrapper(self.
context, uuids, label)
4879 raise ValueError(f
"Unsupported return type: {return_type}")
4882 """Calculate area-weighted sum of primitive data."""
4883 if return_type == float:
4884 return context_wrapper.calculatePrimitiveDataAreaWeightedSumFloatWrapper(self.
context, uuids, label)
4886 raise ValueError(f
"Unsupported return type: {return_type}")
4889 """Scale primitive data by a factor.
4892 scalePrimitiveData(uuids, label, factor) - scale for specific UUIDs
4893 scalePrimitiveData(label, factor) - scale for ALL primitives
4895 if isinstance(uuids_or_label, str):
4896 context_wrapper.scalePrimitiveDataAllWrapper(self.
context, uuids_or_label, label_or_factor)
4898 context_wrapper.scalePrimitiveDataWithUUIDsWrapper(self.
context, uuids_or_label, label_or_factor, factor)
4900 def incrementPrimitiveData(self, uuids: List[int], label: str, increment, data_type: str =
None) ->
None:
4901 """Increment primitive data for the given UUIDs.
4903 Each Helios increment overload only acts on fields whose stored type matches;
4904 fields of a different type are left unchanged. By default the overload is
4905 inferred from the Python type of ``increment`` (``int`` -> int, ``float`` ->
4906 float). To target an unsigned-int or double field, pass ``data_type``
4907 explicitly as one of ``'int'``, ``'uint'``, ``'float'``, ``'double'``.
4910 uuids: UUIDs whose data field to increment.
4911 label: Data field label.
4912 increment: Amount to add.
4913 data_type: Optional explicit field type to target.
4915 if data_type
is not None:
4916 dt = data_type.lower()
4918 context_wrapper.incrementPrimitiveDataIntWrapper(self.
context, uuids, label, int(increment))
4919 elif dt
in (
'uint',
'unsigned',
'unsigned int'):
4920 context_wrapper.incrementPrimitiveDataUIntWrapper(self.
context, uuids, label, int(increment))
4922 context_wrapper.incrementPrimitiveDataFloatWrapper(self.
context, uuids, label, float(increment))
4923 elif dt ==
'double':
4924 context_wrapper.incrementPrimitiveDataDoubleWrapper(self.
context, uuids, label, float(increment))
4926 raise ValueError(f
"Unsupported data_type: {data_type!r}. Expected one of 'int', 'uint', 'float', 'double'.")
4928 if isinstance(increment, float):
4929 context_wrapper.incrementPrimitiveDataFloatWrapper(self.
context, uuids, label, increment)
4930 elif isinstance(increment, int):
4931 context_wrapper.incrementPrimitiveDataIntWrapper(self.
context, uuids, label, increment)
4933 raise ValueError(f
"Unsupported increment type: {type(increment).__name__}")
4936 """Sum multiple primitive data fields into a new field."""
4937 context_wrapper.aggregatePrimitiveDataSumWrapper(self.
context, uuids, labels, result_label)
4940 """Multiply multiple primitive data fields into a new field."""
4941 context_wrapper.aggregatePrimitiveDataProductWrapper(self.
context, uuids, labels, result_label)
4944 """Calculate total one-sided surface area for a set of primitives."""
4945 return context_wrapper.sumPrimitiveSurfaceAreaWrapper(self.
context, uuids)
4947 def calculateAreaIndex(self, leaf_uuids: List[int], wood_uuids: Optional[List[int]] =
None,
4948 ground_area: Optional[float] =
None) -> float:
4949 """Calculate the one-sided area index on a ground-area basis.
4951 This is the quantity :math:`L` appearing in Beer's law,
4952 :math:`\\exp(-G L / \\cos\\theta)`: total one-sided area divided by the ground
4953 area over which it is distributed.
4956 leaf_uuids: UUIDs of leaf primitives.
4957 wood_uuids: Optional UUIDs of woody (branch, trunk, stem) primitives. When
4958 given, the result is a *plant* area index rather than a leaf area index.
4959 Woody area is counted as one half of its summed one-sided area, because a
4960 tube or cone encloses the branch and so sums to the full cylinder surface
4961 rather than the projected area Beer's law requires. If woody elements are
4962 instead represented by non-enclosing planar primitives that are already
4963 one-sided silhouettes, this halving underestimates them by a factor of two.
4964 ground_area: Optional ground area basis in m^2. When omitted, the basis is the
4965 horizontal (x-y) footprint of the bounding box of *all* primitives in the
4966 Context -- so a ground primitive extending beyond the canopy enlarges the
4967 basis and lowers the reported index. Supply this explicitly whenever the
4968 domain is not cropped tightly to the canopy.
4971 One-sided area index (m^2 area per m^2 ground area).
4974 ValueError: If ``leaf_uuids`` is empty or ``ground_area`` is not positive.
4975 RuntimeError: If any primitive is a voxel, whose area is its total enclosing
4976 surface area rather than a one-sided area.
4979 >>> lai = context.calculateAreaIndex(leaf_uuids)
4980 >>> pai = context.calculateAreaIndex(leaf_uuids, wood_uuids)
4981 >>> lai = context.calculateAreaIndex(leaf_uuids, ground_area=100.0)
4984 raise ValueError(
"leaf_uuids must contain at least one UUID")
4985 if ground_area
is not None and ground_area <= 0:
4986 raise ValueError(f
"Ground area must be positive, got {ground_area}")
4988 if wood_uuids
is not None and ground_area
is not None:
4989 return context_wrapper.calculateAreaIndexLeafWoodGroundAreaWrapper(
4990 self.
context, leaf_uuids, wood_uuids, ground_area)
4991 elif wood_uuids
is not None:
4992 return context_wrapper.calculateAreaIndexLeafWoodWrapper(self.
context, leaf_uuids, wood_uuids)
4993 elif ground_area
is not None:
4994 return context_wrapper.calculateAreaIndexLeafGroundAreaWrapper(self.
context, leaf_uuids, ground_area)
4996 return context_wrapper.calculateAreaIndexLeafWrapper(self.
context, leaf_uuids)
4999 """Filter primitives by data value. Auto-dispatches based on value type.
5002 uuids: UUIDs to filter.
5003 label: Data label to compare.
5004 value: Filter value (float, int, or str).
5005 comparator: Comparison operator ("=", "<", ">", "<=", ">="). Not used for strings.
5007 if isinstance(value, str):
5008 return context_wrapper.filterPrimitivesByDataStringWrapper(self.
context, uuids, label, value)
5009 elif isinstance(value, float):
5010 return context_wrapper.filterPrimitivesByDataFloatWrapper(self.
context, uuids, label, value, comparator)
5011 elif isinstance(value, int):
5012 return context_wrapper.filterPrimitivesByDataIntWrapper(self.
context, uuids, label, value, comparator)
5014 raise ValueError(f
"Unsupported filter value type: {type(value).__name__}")
5019 """Return the integer-coded `helios::ObjectType` of a compound object.
5021 Values follow the C++ `helios::ObjectType` enum
5022 (0=tile, 1=sphere, 2=tube, 3=box, 4=disk, 5=polymesh, 6=cone,
5026 return context_wrapper.getObjectTypeWrapper(self.
context, objID)
5030 x, y, z = context_wrapper.getObjectCenterWrapper(self.
context, objID)
5031 return vec3(x, y, z)
5034 """Get axis-aligned bounding box for one object or a list of objects.
5036 The box encloses every vertex of every primitive belonging to the given
5040 objIDs: Single object ID (int) or list of object IDs.
5043 Tuple of (min_corner: vec3, max_corner: vec3).
5046 HeliosRuntimeError: If an object ID does not exist, or if the given
5047 object(s) contain no primitives at all (a bounding box would be
5048 undefined; this previously returned a misleading box at the origin).
5051 if isinstance(objIDs, (list, tuple)):
5052 mn, mx = context_wrapper.getObjectBoundingBoxBatchWrapper(self.
context, list(objIDs))
5054 mn, mx = context_wrapper.getObjectBoundingBoxWrapper(self.
context, objIDs)
5055 return (
vec3(mn[0], mn[1], mn[2]),
vec3(mx[0], mx[1], mx[2]))
5058 """Get flattened primitive UUIDs for one object, a list of objects, or a list-of-lists.
5061 objIDs: int, List[int], or List[List[int]].
5064 Flat list of primitive UUIDs (union across all objects).
5067 if isinstance(objIDs, (list, tuple))
and objIDs
and isinstance(objIDs[0], (list, tuple)):
5068 return context_wrapper.getObjectPrimitiveUUIDsNestedWrapper(self.
context, [list(x)
for x
in objIDs])
5069 if isinstance(objIDs, (list, tuple)):
5070 return context_wrapper.getObjectPrimitiveUUIDsBatchWrapper(self.
context, list(objIDs))
5071 return context_wrapper.getObjectPrimitiveUUIDs(self.
context, int(objIDs))
5075 """Get tile-object area ratio for one or multiple tile objects."""
5077 if isinstance(objIDs, (list, tuple)):
5078 return context_wrapper.getTileObjectAreaRatioBatchWrapper(self.
context, list(objIDs))
5079 return context_wrapper.getTileObjectAreaRatioWrapper(self.
context, objIDs)
5083 x, y, z = context_wrapper.getTileObjectCenterWrapper(self.
context, objID)
5084 return vec3(x, y, z)
5088 x, y = context_wrapper.getTileObjectSizeWrapper(self.
context, objID)
5093 x, y = context_wrapper.getTileObjectSubdivisionCountWrapper(self.
context, objID)
5098 x, y, z = context_wrapper.getTileObjectNormalWrapper(self.
context, objID)
5099 return vec3(x, y, z)
5103 pairs = context_wrapper.getTileObjectTextureUVWrapper(self.
context, objID)
5104 return [
vec2(u, v)
for u, v
in pairs]
5108 triples = context_wrapper.getTileObjectVerticesWrapper(self.
context, objID)
5109 return [
vec3(x, y, z)
for x, y, z
in triples]
5112 """Get the texture repeat count requested when the tile object was created.
5114 The repeat actually applied to the sub-patch texture coordinates is reduced whenever the
5115 requested count does not evenly divide the subdivision count; use
5116 :meth:`getTileObjectEffectiveTextureRepeat` to query that value.
5118 It is the requested rather than the reduced count that is retained on the object and
5119 re-applied when :meth:`setTileObjectSubdivisionCount` changes the subdivision count.
5122 x, y = context_wrapper.getTileObjectTextureRepeatWrapper(self.
context, objID)
5126 """Get the texture repeat count actually applied to the sub-patches of a tile object.
5128 This is the requested count (see :meth:`getTileObjectTextureRepeat`) reduced so that it
5129 evenly divides the subdivision count.
5132 x, y = context_wrapper.getTileObjectEffectiveTextureRepeatWrapper(self.
context, objID)
5137 """Get the Cartesian coordinates of the center of an adaptive tile object."""
5139 x, y, z = context_wrapper.getAdaptiveTileObjectCenterWrapper(self.
context, objID)
5140 return vec3(x, y, z)
5143 """Get the dimensions of an entire adaptive tile object."""
5145 x, y = context_wrapper.getAdaptiveTileObjectSizeWrapper(self.
context, objID)
5149 """Get a unit vector normal to an adaptive tile object surface."""
5151 x, y, z = context_wrapper.getAdaptiveTileObjectNormalWrapper(self.
context, objID)
5152 return vec3(x, y, z)
5155 """Get the Cartesian coordinates of each of the four corners of an adaptive tile object."""
5157 triples = context_wrapper.getAdaptiveTileObjectVerticesWrapper(self.
context, objID)
5158 return [
vec3(x, y, z)
for x, y, z
in triples]
5161 """Get the refinement parameters that were requested when the object was created."""
5163 tx, ty, smin, smax, exponent = context_wrapper.getAdaptiveTileObjectRefinementWrapper(self.
context, objID)
5165 subpatch_size_max=smax, transition_exponent=exponent)
5168 """Get the number of coarsest-level cells spanning an adaptive tile in x and y.
5170 The base grid is the uniform grid of unrefined cells that the quadtree refines within. It
5171 is derived from the requested sub-patch size range and the tile dimensions.
5174 x, y = context_wrapper.getAdaptiveTileObjectBaseSubdivisionCountWrapper(self.
context, objID)
5178 """Get the maximum quadtree refinement level, i.e. the number of times a base cell may be subdivided."""
5180 return context_wrapper.getAdaptiveTileObjectMaxRefinementLevelWrapper(self.
context, objID)
5183 """Get the sub-patch edge lengths actually achieved, as opposed to those requested.
5186 A vec2 holding the achieved minimum edge length in ``x`` and the achieved maximum edge
5190 x, y = context_wrapper.getAdaptiveTileObjectSubpatchSizeRangeWrapper(self.
context, objID)
5194 """Get the texture repeat count of an adaptive tile object.
5196 The base grid is snapped to a multiple of the requested repeat count when the object is
5197 created, so unlike :meth:`getTileObjectTextureRepeat` the requested count is always applied
5198 exactly and there is no effective-repeat variant.
5201 x, y = context_wrapper.getAdaptiveTileObjectTextureRepeatWrapper(self.
context, objID)
5207 x, y, z = context_wrapper.getSphereObjectCenterWrapper(self.
context, objID)
5208 return vec3(x, y, z)
5211 """Get per-axis radii of a sphere object.
5213 Note: Helios spheres are spheroids with three independent radii (rx, ry, rz).
5214 Returns a vec3 (not a scalar).
5217 x, y, z = context_wrapper.getSphereObjectRadiusWrapper(self.
context, objID)
5218 return vec3(x, y, z)
5222 return context_wrapper.getSphereObjectSubdivisionCountWrapper(self.
context, objID)
5226 return context_wrapper.getSphereObjectVolumeWrapper(self.
context, objID)
5231 x, y, z = context_wrapper.getBoxObjectCenterWrapper(self.
context, objID)
5236 x, y, z = context_wrapper.getBoxObjectSizeWrapper(self.
context, objID)
5241 x, y, z = context_wrapper.getBoxObjectSubdivisionCountWrapper(self.
context, objID)
5242 return int3(x, y, z)
5246 return context_wrapper.getBoxObjectVolumeWrapper(self.
context, objID)
5251 x, y, z = context_wrapper.getDiskObjectCenterWrapper(self.
context, objID)
5252 return vec3(x, y, z)
5256 x, y = context_wrapper.getDiskObjectSizeWrapper(self.
context, objID)
5261 return context_wrapper.getDiskObjectSubdivisionCountWrapper(self.
context, objID)
5266 return context_wrapper.getTubeObjectSubdivisionCountWrapper(self.
context, objID)
5270 return context_wrapper.getTubeObjectNodeCountWrapper(self.
context, objID)
5274 triples = context_wrapper.getTubeObjectNodesWrapper(self.
context, objID)
5275 return [
vec3(x, y, z)
for x, y, z
in triples]
5279 return context_wrapper.getTubeObjectNodeRadiiWrapper(self.
context, objID)
5283 triples = context_wrapper.getTubeObjectNodeColorsWrapper(self.
context, objID)
5284 return [
RGBcolor(r, g, b)
for r, g, b
in triples]
5288 return context_wrapper.getTubeObjectVolumeWrapper(self.
context, objID)
5292 return context_wrapper.getTubeObjectSegmentVolumeWrapper(self.
context, objID, segment_index)
5297 return context_wrapper.getConeObjectSubdivisionCountWrapper(self.
context, objID)
5301 triples = context_wrapper.getConeObjectNodesWrapper(self.
context, objID)
5302 return [
vec3(x, y, z)
for x, y, z
in triples]
5306 return context_wrapper.getConeObjectNodeRadiiWrapper(self.
context, objID)
5310 x, y, z = context_wrapper.getConeObjectNodeWrapper(self.
context, objID, number)
5311 return vec3(x, y, z)
5315 return context_wrapper.getConeObjectNodeRadiusWrapper(self.
context, objID, number)
5319 x, y, z = context_wrapper.getConeObjectAxisUnitVectorWrapper(self.
context, objID)
5320 return vec3(x, y, z)
5324 return context_wrapper.getConeObjectLengthWrapper(self.
context, objID)
5328 return context_wrapper.getConeObjectVolumeWrapper(self.
context, objID)
5334 x, y, z = context_wrapper.getPatchCenterWrapper(self.
context, uuid)
5335 return vec3(x, y, z)
5339 x, y = context_wrapper.getPatchSizeWrapper(self.
context, uuid)
5344 x, y, z = context_wrapper.getTriangleVertexWrapper(self.
context, uuid, number)
5345 return vec3(x, y, z)
5349 x, y, z = context_wrapper.getVoxelCenterWrapper(self.
context, uuid)
5350 return vec3(x, y, z)
5354 x, y, z = context_wrapper.getVoxelSizeWrapper(self.
context, uuid)
5355 return vec3(x, y, z)
5357 def getPatchCount(self, include_hidden: bool =
True) -> int:
5359 return context_wrapper.getPatchCountWrapper(self.
context, include_hidden)
5363 return context_wrapper.getTriangleCountWrapper(self.
context, include_hidden)
5366 """Get axis-aligned bounding box for one primitive or a list of primitives.
5369 uuids: Single UUID (int) or list of UUIDs.
5372 Tuple of (min_corner: vec3, max_corner: vec3).
5375 if isinstance(uuids, (list, tuple)):
5376 mn, mx = context_wrapper.getPrimitiveBoundingBoxBatchWrapper(self.
context, list(uuids))
5378 mn, mx = context_wrapper.getPrimitiveBoundingBoxWrapper(self.
context, uuids)
5379 return (
vec3(mn[0], mn[1], mn[2]),
vec3(mx[0], mx[1], mx[2]))
5384 """Set the RGB or RGBA color of one primitive or a list of primitives.
5387 uuids: Single UUID (int) or list of UUIDs.
5388 color: RGBcolor or RGBAcolor.
5391 if isinstance(color, RGBAcolor):
5392 rgba = [color.r, color.g, color.b, color.a]
5393 if isinstance(uuids, (list, tuple)):
5394 context_wrapper.setPrimitiveColorRGBABatchWrapper(self.
context, list(uuids), rgba)
5396 context_wrapper.setPrimitiveColorRGBAWrapper(self.
context, uuids, rgba)
5397 elif isinstance(color, RGBcolor):
5398 rgb = [color.r, color.g, color.b]
5399 if isinstance(uuids, (list, tuple)):
5400 context_wrapper.setPrimitiveColorBatchWrapper(self.
context, list(uuids), rgb)
5402 context_wrapper.setPrimitiveColorWrapper(self.
context, uuids, rgb)
5404 raise ValueError(f
"color must be RGBcolor or RGBAcolor, got {type(color).__name__}")
5409 """Remove a named data field from one primitive or a list of primitives."""
5411 if isinstance(uuids, (list, tuple)):
5412 context_wrapper.clearPrimitiveDataByLabelBatchWrapper(self.
context, list(uuids), label)
5414 context_wrapper.clearPrimitiveDataByLabelWrapper(self.
context, uuids, label)
5417 """Remove a named data field from every primitive in the Context.
5419 Clears the data with the given label from all primitives (including hidden ones)
5420 and releases the registered data type for the label, so it may subsequently be
5421 re-registered with a different type. Requires helios-core v1.3.73 or newer.
5424 context_wrapper.clearAllPrimitiveDataByLabelWrapper(self.
context, label)
5427 """List all data labels attached to a primitive."""
5429 return context_wrapper.listPrimitiveDataWrapper(self.
context, uuid)
5435 if not isinstance(xbounds, vec2):
5436 raise ValueError(f
"xbounds must be a vec2, got {type(xbounds).__name__}")
5437 context_wrapper.cropDomainXWrapper(self.
context, xbounds.to_list())
5441 if not isinstance(ybounds, vec2):
5442 raise ValueError(f
"ybounds must be a vec2, got {type(ybounds).__name__}")
5443 context_wrapper.cropDomainYWrapper(self.
context, ybounds.to_list())
5447 if not isinstance(zbounds, vec2):
5448 raise ValueError(f
"zbounds must be a vec2, got {type(zbounds).__name__}")
5449 context_wrapper.cropDomainZWrapper(self.
context, zbounds.to_list())
5451 def cropDomain(self, *args) -> Optional[List[int]]:
5452 """Crop the context domain to the given XYZ bounds.
5455 cropDomain(xbounds: vec2, ybounds: vec2, zbounds: vec2)
5456 -> crop ALL primitives; returns None.
5457 cropDomain(uuids: List[int], xbounds: vec2, ybounds: vec2, zbounds: vec2)
5458 -> crop only the given primitives; returns the list of primitives
5459 that survived (in-bounds UUIDs). The input list is NOT mutated.
5464 for name, b
in ((
"xbounds", xb), (
"ybounds", yb), (
"zbounds", zb)):
5465 if not isinstance(b, vec2):
5466 raise ValueError(f
"{name} must be a vec2, got {type(b).__name__}")
5467 context_wrapper.cropDomainXYZWrapper(self.
context, xb.to_list(), yb.to_list(), zb.to_list())
5470 uuids, xb, yb, zb = args
5471 if not isinstance(uuids, (list, tuple)):
5472 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
5473 for name, b
in ((
"xbounds", xb), (
"ybounds", yb), (
"zbounds", zb)):
5474 if not isinstance(b, vec2):
5475 raise ValueError(f
"{name} must be a vec2, got {type(b).__name__}")
5476 return context_wrapper.cropDomainByUUIDsWrapper(self.
context, list(uuids), xb.to_list(), yb.to_list(), zb.to_list())
5477 raise TypeError(f
"cropDomain() takes 3 or 4 positional arguments, got {len(args)}")
5486 """Return True if a compound object with the given ID exists."""
5488 return context_wrapper.doesObjectExistWrapper(self.
context, objID)
5491 """Return True if the given primitive UUID belongs to the given object."""
5493 return context_wrapper.doesObjectContainPrimitiveWrapper(self.
context, objID, uuid)
5496 """Return True if the named material has data stored under data_label."""
5498 return context_wrapper.doesMaterialDataExistWrapper(self.
context, material_label, data_label)
5501 """Return True if the compound object has a texture assigned."""
5503 return context_wrapper.objectHasTextureWrapper(self.
context, objID)
5506 """Return True if the primitive's geometry has been modified since the last clean mark."""
5508 return context_wrapper.isPrimitiveDirtyWrapper(self.
context, uuid)
5511 """Return True if value caching is enabled for the given object-data label."""
5513 return context_wrapper.isObjectDataValueCachingEnabledWrapper(self.
context, label)
5516 """Return True if value caching is enabled for the given primitive-data label."""
5518 return context_wrapper.isPrimitiveDataValueCachingEnabledWrapper(self.
context, label)
5521 """Return True if all primitives originally belonging to this object still exist
5522 (i.e., none have been deleted)."""
5524 return context_wrapper.areObjectPrimitivesCompleteWrapper(self.
context, objID)
5529 """Get the current simulation date as Julian day (1-366)."""
5531 return context_wrapper.getJulianDateWrapper(self.
context)
5534 """Return the total number of materials registered in the context."""
5536 return context_wrapper.getMaterialCountWrapper(self.
context)
5539 """Return the total surface area (one-sided) of all primitives in the object."""
5541 return context_wrapper.getObjectAreaWrapper(self.
context, objID)
5544 """Return the number of primitives currently belonging to the object."""
5546 return context_wrapper.getObjectPrimitiveCountWrapper(self.
context, objID)
5549 """Return the enclosed volume of a polymesh object.
5551 Since helios-core 1.3.84 a mesh carrying a face table is separated into its
5552 connected pieces and the volume of those that are closed is summed, so a solid
5553 shape modelled with an open stalk reports the shape's volume. An error is raised
5554 only when no piece is closed. A mesh carrying no face table has its closure
5555 checked by matching facets on coincident corners.
5558 return context_wrapper.getPolymeshObjectVolumeWrapper(self.
context, objID)
5561 """Return the total surface area of a polymesh object, summed over every face."""
5563 return context_wrapper.getPolymeshObjectSurfaceAreaWrapper(self.
context, objID)
5567 Return True if a polymesh object is a closed surface, i.e. has no boundary edges.
5569 Only a closed mesh has a well-defined enclosed volume. Since helios-core 1.3.84
5570 :meth:`getPolymeshObjectVolume` splits a mesh into its connected pieces and sums
5571 the volume of those that are closed, so it raises only when no piece is closed --
5572 a solid fruit modelled with an open stalk reports the fruit's volume.
5575 return context_wrapper.isPolymeshObjectClosedWrapper(self.
context, objID)
5579 Move every shared vertex of a polymesh object, deforming the mesh.
5581 Writes every shared vertex in one pass and pushes the new positions out to the
5582 member primitives, so faces that meet at a vertex stay welded. This is the
5583 supported way to deform a mesh: transforming the member primitives individually
5584 leaves each shared vertex wherever the last facet processed put it.
5586 The topology is unchanged, so ``vertices`` must be parallel to and the same length
5587 as the list returned by :meth:`getPolymeshObjectVertices`. Texture coordinates are
5588 not touched, and neither is the solid fraction of the member primitives -- so
5589 deforming a textured mesh does not re-rasterize its alpha mask.
5592 objID: Object ID of the polymesh object
5593 vertices: New vertex positions in global Cartesian coordinates
5596 RuntimeError: If the native library predates helios-core v1.3.84
5597 ValueError: If a vertex is not a vec3
5600 Vertex normals are NOT recomputed and no longer describe the deformed surface;
5601 call :meth:`computePolymeshObjectVertexNormals` again if exact normals matter.
5604 >>> verts = context.getPolymeshObjectVertices(objID)
5605 >>> stretched = [vec3(v.x, v.y, v.z * 2.0) for v in verts]
5606 >>> context.setPolymeshObjectVertices(objID, stretched)
5609 for i, v
in enumerate(vertices):
5610 if not isinstance(v, vec3):
5611 raise ValueError(f
"vertices[{i}] must be a vec3, got {type(v).__name__}")
5612 context_wrapper.setPolymeshObjectVerticesWrapper(
5613 self.
context, objID, [(v.x, v.y, v.z)
for v
in vertices]
5618 Return True if a compound object reports which member primitives meet at each vertex.
5620 True for Tile, AdaptiveTile, Sphere, Tube and Cone objects, and for a Polymesh that
5621 carries a face table. False for a Box, a Disk, and for a polymesh assembled from
5622 loose primitives by :meth:`addPolymeshObject`, which has no topology.
5625 RuntimeError: If the native library predates helios-core v1.3.84
5628 return context_wrapper.doesObjectHaveSharedVertexTopologyWrapper(self.
context, objID)
5631 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL) -> int:
5633 Return the number of distinct shared vertices in a compound object's mesh.
5635 This is one greater than the largest index
5636 :meth:`getObjectPrimitiveSharedVertexIndices` can return, and zero if the object
5637 exposes no topology.
5640 objID: Object ID of the compound object
5641 weld_mode: Granularity at which coincident vertices are treated as the same
5642 shared vertex. See :class:`VertexWeldMode`.
5645 RuntimeError: If the native library predates helios-core v1.3.84
5648 return context_wrapper.getObjectSharedVertexCountWrapper(
5649 self.
context, objID, int(weld_mode)
5653 self, objID: int, uuid: int,
5654 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL
5657 Return the shared mesh vertex each vertex of a primitive belongs to.
5659 Indices are in the same order as :meth:`getPrimitiveVertices`, and two primitives
5660 meeting at a corner report the same index there. This is what lets a per-face
5661 quantity be averaged onto the vertices neighbouring faces have in common.
5664 objID: Object ID of the compound object the primitive belongs to
5665 uuid: UUID of the primitive
5666 weld_mode: See :class:`VertexWeldMode`
5669 One index per vertex of the primitive; empty if the object exposes no topology.
5672 RuntimeError: If the native library predates helios-core v1.3.84
5675 When walking a whole object, prefer
5676 :meth:`getObjectPrimitiveSharedVertexIndicesMulti` -- this per-primitive form is
5677 O(n) per call on Sphere, Tube and Cone objects, so a full walk is O(n^2).
5680 return context_wrapper.getObjectPrimitiveSharedVertexIndicesWrapper(
5681 self.
context, objID, uuid, int(weld_mode)
5685 self, objID: int, uuids: List[int],
5686 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL
5687 ) -> List[List[int]]:
5689 Return shared mesh vertex indices for many primitives of a compound object at once.
5691 Equivalent to calling :meth:`getObjectPrimitiveSharedVertexIndices` for each UUID,
5692 except that any per-object quantity needed to locate the vertices is prepared once
5693 for the whole batch. Prefer this overload when walking an entire object.
5696 objID: Object ID of the compound object the primitives belong to
5697 uuids: UUIDs of the primitives
5698 weld_mode: See :class:`VertexWeldMode`
5701 A list parallel to ``uuids``, each entry holding one shared vertex index per
5702 vertex of the corresponding primitive.
5705 RuntimeError: If the native library predates helios-core v1.3.84
5708 return context_wrapper.getObjectPrimitiveSharedVertexIndicesMultiWrapper(
5709 self.
context, objID, uuids, int(weld_mode)
5714 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL
5717 Return a primitive's shared mesh vertex indices without naming its parent object.
5719 Resolves the primitive's parent object and forwards to it.
5722 uuid: UUID of the primitive
5723 weld_mode: See :class:`VertexWeldMode`
5726 One index per vertex of the primitive. Empty if the primitive belongs to no
5727 object, or to one that exposes no topology.
5730 RuntimeError: If the native library predates helios-core v1.3.84
5733 return context_wrapper.getPrimitiveSharedVertexIndicesWrapper(
5734 self.
context, uuid, int(weld_mode)
5739 Return the deduplicated shared vertex positions of a polymesh object.
5741 Returns an empty list for a mesh with no retained topology, such as one built
5742 with :meth:`addPolymeshObject` and not given a face set.
5745 triples = context_wrapper.getPolymeshObjectVerticesWrapper(self.
context, objID)
5746 return [
vec3(x, y, z)
for x, y, z
in triples]
5749 """Return the vertex index triples defining each face of a polymesh object."""
5751 triples = context_wrapper.getPolymeshObjectFacesWrapper(self.
context, objID)
5752 return [
int3(a, b, c)
for a, b, c
in triples]
5756 Return the per-vertex normals of a polymesh object.
5758 Returns an empty list if the mesh carries none. A mesh loaded by :meth:`loadOBJ`
5759 or :meth:`loadPLY` always has them (helios-core v1.3.85+): normals authored in the
5760 file are kept, and a file that supplies none has them generated from the mesh
5761 connectivity. Only a mesh assembled programmatically through
5762 :meth:`setPolymeshObjectTopology` without normals carries none; call
5763 :meth:`computePolymeshObjectVertexNormals` to generate them.
5766 triples = context_wrapper.getPolymeshObjectVertexNormalsWrapper(self.
context, objID)
5767 return [
vec3(x, y, z)
for x, y, z
in triples]
5770 """Return the per-vertex texture coordinates of a polymesh object, or an empty list if it has none."""
5772 pairs = context_wrapper.getPolymeshObjectVertexUVWrapper(self.
context, objID)
5773 return [
vec2(u, v)
for u, v
in pairs]
5776 """Return True if a polymesh object carries per-vertex normals."""
5778 return context_wrapper.doesPolymeshObjectHaveVertexNormalsWrapper(self.
context, objID)
5782 Return where a polymesh object's vertex normals came from.
5784 ``AUTHORED`` means they were read from the source file (OBJ ``vn`` records, or
5785 PLY ``nx``/``ny``/``nz`` properties); ``COMPUTED`` means they were generated,
5786 either by the file loader because the file supplied none (helios-core v1.3.85+)
5787 or by :meth:`computePolymeshObjectVertexNormals`; ``NONE`` means the mesh has no
5788 vertex normals, which only arises for a mesh assembled programmatically through
5789 :meth:`setPolymeshObjectTopology`.
5793 context_wrapper.getPolymeshObjectVertexNormalSourceWrapper(self.
context, objID)
5797 """Return the number of shared vertices in a polymesh object."""
5799 return context_wrapper.getPolymeshObjectVertexCountWrapper(self.
context, objID)
5802 """Return the number of faces in a polymesh object."""
5804 return context_wrapper.getPolymeshObjectFaceCountWrapper(self.
context, objID)
5807 """Return the index into :meth:`getPolymeshObjectFaces` of the face made up by a member primitive."""
5809 return context_wrapper.getPolymeshObjectFaceIndexForPrimitiveWrapper(self.
context, objID, uuid)
5812 """Return the UUID of the primitive making up a given face of a polymesh object."""
5814 return context_wrapper.getPolymeshObjectPrimitiveUUIDForFaceWrapper(self.
context, objID, face_index)
5818 Compute per-vertex normals for a polymesh object by area-weighted averaging.
5820 Vertices are split across edges whose dihedral angle exceeds
5821 ``crease_angle_degrees``, so hard edges stay hard rather than being smoothed
5822 away. The resulting normals are reported as
5823 :attr:`VertexNormalSource.COMPUTED`.
5825 The file loaders call this automatically for a mesh whose source file supplied
5826 no normals (helios-core v1.3.85+), so it is only needed for a mesh assembled
5827 programmatically through :meth:`setPolymeshObjectTopology`, or to regenerate
5828 normals at a different crease angle or after the mesh has been deformed with
5829 :meth:`setPolymeshObjectVertices`.
5832 objID: Object ID of the polymesh object
5833 crease_angle_degrees: Dihedral angle above which an edge is kept hard
5836 context_wrapper.computePolymeshObjectVertexNormalsWrapper(
5837 self.
context, objID, float(crease_angle_degrees)
5842 Return the boundary edges of a polymesh object as vertex index pairs.
5844 A boundary edge is one referenced by exactly one face. An empty list means the
5848 pairs = context_wrapper.getPolymeshObjectBoundaryEdgesWrapper(self.
context, objID)
5849 return [
int2(a, b)
for a, b
in pairs]
5853 Return the connected components of a polymesh object.
5855 Each component is a list of face indices into :meth:`getPolymeshObjectFaces`.
5856 More than one component means the mesh is made of separate disjoint pieces.
5859 return context_wrapper.getPolymeshObjectConnectedComponentsWrapper(self.
context, objID)
5862 face_UUIDs: List[int],
5863 vertex_normals: Optional[List[vec3]] =
None,
5864 vertex_uv: Optional[List[vec2]] =
None,
5865 normal_source: VertexNormalSource = VertexNormalSource.NONE) ->
None:
5867 Attach an indexed face set to a polymesh object built programmatically.
5869 Meshes loaded from an OBJ or PLY file retain the connectivity of the source file
5870 automatically. This is for a mesh assembled with :meth:`addPolymeshObject`, which
5871 otherwise has no topology and behaves as a triangle soup: its face count is zero,
5872 it cannot report a volume, and it is written out as independent per-triangle
5876 objID: Object ID of the polymesh object
5877 vertices: Shared vertex positions in global Cartesian coordinates
5878 faces: Vertex index triples defining each face
5879 face_UUIDs: UUID of the primitive corresponding to each face, parallel to ``faces``
5880 vertex_normals: Per-vertex normals, or None if the mesh has none
5881 vertex_uv: Per-vertex texture coordinates, or None if the mesh has none
5882 normal_source: Provenance of the supplied vertex normals
5885 ValueError: If an argument has the wrong type or ``face_UUIDs`` is not
5886 parallel to ``faces``
5889 for i, v
in enumerate(vertices):
5890 if not isinstance(v, vec3):
5891 raise ValueError(f
"vertices[{i}] must be a vec3, got {type(v).__name__}")
5892 for i, f
in enumerate(faces):
5893 if not isinstance(f, int3):
5894 raise ValueError(f
"faces[{i}] must be an int3, got {type(f).__name__}")
5895 if vertex_normals
is not None:
5896 for i, v
in enumerate(vertex_normals):
5897 if not isinstance(v, vec3):
5898 raise ValueError(f
"vertex_normals[{i}] must be a vec3, got {type(v).__name__}")
5899 if vertex_uv
is not None:
5900 for i, v
in enumerate(vertex_uv):
5901 if not isinstance(v, vec2):
5902 raise ValueError(f
"vertex_uv[{i}] must be a vec2, got {type(v).__name__}")
5903 if len(face_UUIDs) != len(faces):
5905 f
"face_UUIDs must be parallel to faces: got {len(face_UUIDs)} UUIDs for {len(faces)} faces"
5908 context_wrapper.setPolymeshObjectTopologyWrapper(
5910 [(v.x, v.y, v.z)
for v
in vertices],
5911 [(f.x, f.y, f.z)
for f
in faces],
5913 [(v.x, v.y, v.z)
for v
in vertex_normals]
if vertex_normals
else [],
5914 [(v.x, v.y)
for v
in vertex_uv]
if vertex_uv
else [],
5920 Return True if a compound object can report analytic vertex normals.
5922 True for Sphere, Tube and Cone objects, which approximate a curved shape and can
5923 evaluate its true surface normal. False for object types built from genuinely
5924 flat faces, such as a tile or box.
5927 return context_wrapper.doesObjectHaveAnalyticVertexNormalsWrapper(self.
context, objID)
5930 uuid: Union[int, List[int]]) -> Union[List[vec3], List[List[vec3]]]:
5932 Return the analytic surface normals at each vertex of a member primitive.
5934 Normals are evaluated from the object's own shape definition rather than stored,
5935 so they account for taper and stay correct after the object is transformed or its
5936 nodes and radii change. Returns an empty list for an object with no analytic
5937 normals (see :meth:`doesObjectHaveAnalyticVertexNormals`).
5940 objID: Object ID of the compound object
5941 uuid: UUID of a member primitive, or a list of them
5944 A list of vec3 for a single UUID, or a list of such lists for a list of UUIDs
5947 if isinstance(uuid, (list, tuple)):
5948 batches = context_wrapper.getObjectPrimitiveVertexNormalsBatchWrapper(
5949 self.
context, objID, list(uuid)
5951 return [[
vec3(x, y, z)
for x, y, z
in b]
for b
in batches]
5952 triples = context_wrapper.getObjectPrimitiveVertexNormalsWrapper(self.
context, objID, uuid)
5953 return [
vec3(x, y, z)
for x, y, z
in triples]
5956 """Look up a material ID from its human-readable label."""
5958 return context_wrapper.getMaterialIDFromLabelWrapper(self.
context, material_label)
5961 """Return the material ID assigned to the given primitive."""
5963 return context_wrapper.getPrimitiveMaterialIDWrapper(self.
context, uuid)
5966 """Return the version counter for a global data entry. Increments on each update;
5967 useful for cache invalidation."""
5969 return context_wrapper.getGlobalDataVersionWrapper(self.
context, label)
5972 """Return the ID of the compound object the primitive belongs to.
5974 Returns 0 if the primitive is not part of any compound object (the documented
5975 "no parent" sentinel). Raises ``HeliosRuntimeError`` if ``uuid`` does not exist.
5978 return context_wrapper.getPrimitiveParentObjectIDWrapper(self.
context, uuid)
5983 """Return the filesystem path of the texture assigned to the object, or an
5984 empty string if no texture is assigned."""
5986 return context_wrapper.getObjectTextureFileWrapper(self.
context, objID)
5989 """Return the union of all primitive-data labels used across every primitive
5992 return context_wrapper.listAllPrimitiveDataLabelsWrapper(self.
context)
5995 """Return the list of XML file paths that have been loaded into this context."""
5997 return context_wrapper.getLoadedXMLFilesWrapper(self.
context)
6002 """Print summary info for the object to stdout (for debugging)."""
6004 context_wrapper.printObjectInfoWrapper(self.
context, objID)
6007 """Print summary info for the primitive to stdout (for debugging)."""
6009 context_wrapper.printPrimitiveInfoWrapper(self.
context, uuid)
6012 """Enable value caching for the given primitive-data label. Required before
6013 using getUniquePrimitiveDataValues for that label."""
6015 context_wrapper.enablePrimitiveDataValueCachingWrapper(self.
context, label)
6018 """Disable value caching for the given primitive-data label."""
6020 context_wrapper.disablePrimitiveDataValueCachingWrapper(self.
context, label)
6023 """Enable value caching for the given object-data label. Required before
6024 using getUniqueObjectDataValues for that label."""
6026 context_wrapper.enableObjectDataValueCachingWrapper(self.
context, label)
6029 """Disable value caching for the given object-data label."""
6031 context_wrapper.disableObjectDataValueCachingWrapper(self.
context, label)
6034 """Compute the mean of the given primitive-data label across the object's
6035 primitives and store it as object data on the object itself under the
6038 context_wrapper.setObjectDataFromPrimitiveDataMeanWrapper(self.
context, objID, label)
6040 def renameMaterial(self, old_label: str, new_label: str) ->
None:
6041 """Rename an existing material."""
6043 context_wrapper.renameMaterialWrapper(self.
context, old_label, new_label)
6046 """Rename a primitive-data label on a single primitive."""
6048 context_wrapper.renamePrimitiveDataWrapper(self.
context, uuid, old_label, new_label)
6051 """Clear the named data entry on the given material."""
6053 context_wrapper.clearMaterialDataWrapper(self.
context, material_label, data_label)
6062 """Return the list of UUIDs that have been deleted from the context.
6064 These UUIDs are tombstoned and will not appear in getAllUUIDs(), but their
6065 IDs are tracked so they can be excluded from external references.
6068 return context_wrapper.getDeletedUUIDsWrapper(self.
context)
6070 def getDirtyUUIDs(self, include_deleted: bool =
True) -> List[int]:
6071 """Return the list of UUIDs whose geometry has been modified since the last
6072 markGeometryClean call.
6075 include_deleted: If True (default), include UUIDs that were deleted while
6076 dirty. If False, only return UUIDs that still exist.
6079 return context_wrapper.getDirtyUUIDsWrapper(self.
context, include_deleted)
6082 include_zero: bool =
True) -> List[int]:
6083 """Return the unique set of compound-object IDs that the given primitives
6087 uuids: List of primitive UUIDs to inspect.
6088 include_zero: If True (default), include the sentinel object ID 0
6089 (i.e., primitives with no parent object). If False, only return
6090 IDs of real compound objects.
6093 if not isinstance(uuids, (list, tuple)):
6094 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6095 return context_wrapper.getUniquePrimitiveParentObjectIDsWrapper(
6096 self.
context, list(uuids), include_zero
6102 """Return the area-weighted average normal of all primitives in the object."""
6104 x, y, z = context_wrapper.getObjectAverageNormalWrapper(self.
context, objID)
6105 return vec3(x, y, z)
6108 """Rotate the object so its area-weighted average normal aligns with
6109 new_normal. The rotation is applied about the given origin point."""
6111 if not isinstance(origin, vec3):
6112 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6113 if not isinstance(new_normal, vec3):
6114 raise ValueError(f
"new_normal must be a vec3, got {type(new_normal).__name__}")
6115 context_wrapper.setObjectAverageNormalWrapper(
6116 self.
context, objID, origin.to_list(), new_normal.to_list()
6120 """Translate the object so its origin is moved to the given point."""
6122 if not isinstance(origin, vec3):
6123 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6124 context_wrapper.setObjectOriginWrapper(self.
context, objID, origin.to_list())
6129 """Rotate a single primitive about the given origin so its azimuth
6130 equals new_azimuth (radians)."""
6132 if not isinstance(origin, vec3):
6133 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6134 context_wrapper.setPrimitiveAzimuthWrapper(
6135 self.
context, uuid, origin.to_list(), float(new_azimuth)
6139 """Rotate a single primitive about the given origin so its elevation
6140 equals new_elevation (radians)."""
6142 if not isinstance(origin, vec3):
6143 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6144 context_wrapper.setPrimitiveElevationWrapper(
6145 self.
context, uuid, origin.to_list(), float(new_elevation)
6150 def setTriangleVertices(self, uuid: int, vertex0: vec3, vertex1: vec3, vertex2: vec3) ->
None:
6151 """Replace the three vertices of an existing triangle primitive."""
6153 for name, v
in ((
"vertex0", vertex0), (
"vertex1", vertex1), (
"vertex2", vertex2)):
6154 if not isinstance(v, vec3):
6155 raise ValueError(f
"{name} must be a vec3, got {type(v).__name__}")
6156 context_wrapper.setTriangleVerticesWrapper(
6157 self.
context, uuid, vertex0.to_list(), vertex1.to_list(), vertex2.to_list()
6161 """Rotate one or more primitives so their normals align with new_normal.
6163 Accepts either a single UUID (int) or a list/tuple of UUIDs.
6164 The rotation is applied about the given origin point.
6167 if not isinstance(origin, vec3):
6168 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6169 if not isinstance(new_normal, vec3):
6170 raise ValueError(f
"new_normal must be a vec3, got {type(new_normal).__name__}")
6171 if isinstance(uuids_or_uuid, (list, tuple)):
6172 context_wrapper.setPrimitiveNormalBatchWrapper(
6173 self.
context, list(uuids_or_uuid), origin.to_list(), new_normal.to_list()
6176 context_wrapper.setPrimitiveNormalWrapper(
6177 self.
context, uuids_or_uuid, origin.to_list(), new_normal.to_list()
6181 """Reassign one or more primitives to belong to the given compound object.
6183 Accepts either a single UUID (int) or a list/tuple of UUIDs. Pass objID=0
6184 to detach primitive(s) from any object.
6187 if isinstance(uuids_or_uuid, (list, tuple)):
6188 context_wrapper.setPrimitiveParentObjectIDBatchWrapper(
6189 self.
context, list(uuids_or_uuid), int(objID)
6192 context_wrapper.setPrimitiveParentObjectIDWrapper(
6193 self.
context, int(uuids_or_uuid), int(objID)
6203 def setMaterialDataInt(self, material_label: str, data_label: str, value: int) ->
None:
6204 """Set int data on a material. Affects all primitives that reference it."""
6206 context_wrapper.setMaterialDataIntWrapper(self.
context, material_label, data_label, int(value))
6209 """Set unsigned int data on a material."""
6211 context_wrapper.setMaterialDataUIntWrapper(self.
context, material_label, data_label, int(value))
6214 """Set float data on a material."""
6216 context_wrapper.setMaterialDataFloatWrapper(self.
context, material_label, data_label, float(value))
6219 """Set double-precision float data on a material."""
6221 context_wrapper.setMaterialDataDoubleWrapper(self.
context, material_label, data_label, float(value))
6224 """Set string data on a material."""
6226 context_wrapper.setMaterialDataStringWrapper(self.
context, material_label, data_label, str(value))
6229 """Set vec2 data on a material."""
6231 if not isinstance(value, vec2):
6232 raise ValueError(f
"value must be a vec2, got {type(value).__name__}")
6233 context_wrapper.setMaterialDataVec2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6236 """Set vec3 data on a material."""
6238 if not isinstance(value, vec3):
6239 raise ValueError(f
"value must be a vec3, got {type(value).__name__}")
6240 context_wrapper.setMaterialDataVec3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6243 """Set vec4 data on a material."""
6245 if not isinstance(value, vec4):
6246 raise ValueError(f
"value must be a vec4, got {type(value).__name__}")
6247 context_wrapper.setMaterialDataVec4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6250 """Set int2 data on a material."""
6252 if not isinstance(value, int2):
6253 raise ValueError(f
"value must be an int2, got {type(value).__name__}")
6254 context_wrapper.setMaterialDataInt2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6257 """Set int3 data on a material."""
6259 if not isinstance(value, int3):
6260 raise ValueError(f
"value must be an int3, got {type(value).__name__}")
6261 context_wrapper.setMaterialDataInt3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6264 """Set int4 data on a material."""
6266 if not isinstance(value, int4):
6267 raise ValueError(f
"value must be an int4, got {type(value).__name__}")
6268 context_wrapper.setMaterialDataInt4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6274 return context_wrapper.getMaterialDataIntWrapper(self.
context, material_label, data_label)
6278 return context_wrapper.getMaterialDataUIntWrapper(self.
context, material_label, data_label)
6282 return context_wrapper.getMaterialDataFloatWrapper(self.
context, material_label, data_label)
6286 return context_wrapper.getMaterialDataDoubleWrapper(self.
context, material_label, data_label)
6290 return context_wrapper.getMaterialDataStringWrapper(self.
context, material_label, data_label)
6294 x, y = context_wrapper.getMaterialDataVec2Wrapper(self.
context, material_label, data_label)
6299 x, y, z = context_wrapper.getMaterialDataVec3Wrapper(self.
context, material_label, data_label)
6300 return vec3(x, y, z)
6304 x, y, z, w = context_wrapper.getMaterialDataVec4Wrapper(self.
context, material_label, data_label)
6309 x, y = context_wrapper.getMaterialDataInt2Wrapper(self.
context, material_label, data_label)
6314 x, y, z = context_wrapper.getMaterialDataInt3Wrapper(self.
context, material_label, data_label)
6315 return int3(x, y, z)
6319 x, y, z, w = context_wrapper.getMaterialDataInt4Wrapper(self.
context, material_label, data_label)
6320 return int4(x, y, z, w)
6323 """Return the HeliosDataType enum value for the given material data entry.
6325 Encoding (from Helios core): 0=INT, 1=UINT, 2=FLOAT, 3=DOUBLE,
6326 4=VEC2, 5=VEC3, 6=VEC4, 7=INT2, 8=INT3, 9=INT4, 10=STRING.
6329 return context_wrapper.getMaterialDataTypeWrapper(self.
context, material_label, data_label)
6333 def setMaterialData(self, material_label: str, data_label: str, value) ->
None:
6334 """Set material data with type detection from the Python value.
6336 Dispatches to the correct typed setter based on ``isinstance`` of ``value``.
6337 For unambiguous numeric width control (e.g., uint vs int), call the
6338 per-type method directly (``setMaterialDataUInt``, etc.).
6341 if isinstance(value, bool):
6343 context_wrapper.setMaterialDataIntWrapper(self.
context, material_label, data_label, int(value))
6344 elif isinstance(value, int):
6345 context_wrapper.setMaterialDataIntWrapper(self.
context, material_label, data_label, int(value))
6346 elif isinstance(value, float):
6347 context_wrapper.setMaterialDataFloatWrapper(self.
context, material_label, data_label, float(value))
6348 elif isinstance(value, str):
6349 context_wrapper.setMaterialDataStringWrapper(self.
context, material_label, data_label, value)
6350 elif isinstance(value, vec2):
6351 context_wrapper.setMaterialDataVec2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6352 elif isinstance(value, vec3):
6353 context_wrapper.setMaterialDataVec3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6354 elif isinstance(value, vec4):
6355 context_wrapper.setMaterialDataVec4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6356 elif isinstance(value, int2):
6357 context_wrapper.setMaterialDataInt2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6358 elif isinstance(value, int3):
6359 context_wrapper.setMaterialDataInt3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6360 elif isinstance(value, int4):
6361 context_wrapper.setMaterialDataInt4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6364 f
"Unsupported value type for setMaterialData: {type(value).__name__}. "
6365 f
"Supported: int, float, str, vec2, vec3, vec4, int2, int3, int4. "
6366 f
"For uint/double, call setMaterialDataUInt/Double directly."
6369 def getMaterialData(self, material_label: str, data_label: str, data_type: type =
None):
6370 """Get material data, auto-detecting the type from Helios storage if not specified.
6373 material_label: Name of the material.
6374 data_label: Data entry label.
6375 data_type: Optional Python type (int, float, str, vec2, vec3, vec4, int2,
6376 int3, int4) or string ('uint', 'double'). If ``None``, the type is
6377 queried via getMaterialDataType and dispatched automatically.
6380 if data_type
is None:
6381 t = context_wrapper.getMaterialDataTypeWrapper(self.
context, material_label, data_label)
6384 return context_wrapper.getMaterialDataIntWrapper(self.
context, material_label, data_label)
6386 return context_wrapper.getMaterialDataUIntWrapper(self.
context, material_label, data_label)
6388 return context_wrapper.getMaterialDataFloatWrapper(self.
context, material_label, data_label)
6390 return context_wrapper.getMaterialDataDoubleWrapper(self.
context, material_label, data_label)
6392 x, y = context_wrapper.getMaterialDataVec2Wrapper(self.
context, material_label, data_label)
6395 x, y, z = context_wrapper.getMaterialDataVec3Wrapper(self.
context, material_label, data_label)
6396 return vec3(x, y, z)
6398 x, y, z, w = context_wrapper.getMaterialDataVec4Wrapper(self.
context, material_label, data_label)
6399 return vec4(x, y, z, w)
6401 x, y = context_wrapper.getMaterialDataInt2Wrapper(self.
context, material_label, data_label)
6404 x, y, z = context_wrapper.getMaterialDataInt3Wrapper(self.
context, material_label, data_label)
6405 return int3(x, y, z)
6407 x, y, z, w = context_wrapper.getMaterialDataInt4Wrapper(self.
context, material_label, data_label)
6408 return int4(x, y, z, w)
6410 return context_wrapper.getMaterialDataStringWrapper(self.
context, material_label, data_label)
6411 raise ValueError(f
"Unknown HeliosDataType code: {t}")
6414 if data_type == int:
6416 if data_type == float:
6418 if data_type == str:
6420 if data_type ==
"uint":
6422 if data_type ==
"double":
6424 if data_type == vec2:
6426 if data_type == vec3:
6428 if data_type == vec4:
6430 if data_type == int2:
6432 if data_type == int3:
6434 if data_type == int4:
6437 f
"Unsupported material data type: {data_type}. Supported: int, float, str, "
6438 f
"vec2, vec3, vec4, int2, int3, int4, 'uint', 'double'."
6444 """Return the unique values stored under ``label`` across all primitives.
6446 Requires value caching to be enabled for ``label`` first via
6447 ``enablePrimitiveDataValueCaching(label)``. Supported ``dtype`` values:
6448 ``int``, ``str``, or the string ``'uint'``.
6452 return context_wrapper.getUniquePrimitiveDataValuesIntWrapper(self.
context, label)
6454 return context_wrapper.getUniquePrimitiveDataValuesUIntWrapper(self.
context, label)
6456 return context_wrapper.getUniquePrimitiveDataValuesStringWrapper(self.
context, label)
6458 f
"Unsupported dtype for getUniquePrimitiveDataValues: {dtype}. "
6459 f
"Supported: int, str, 'uint'."
6463 """Return the unique values stored under ``label`` across all compound objects.
6465 Requires value caching to be enabled for ``label`` first via
6466 ``enableObjectDataValueCaching(label)``. Supported ``dtype`` values:
6467 ``int``, ``str``, or the string ``'uint'``.
6471 return context_wrapper.getUniqueObjectDataValuesIntWrapper(self.
context, label)
6473 return context_wrapper.getUniqueObjectDataValuesUIntWrapper(self.
context, label)
6475 return context_wrapper.getUniqueObjectDataValuesStringWrapper(self.
context, label)
6477 f
"Unsupported dtype for getUniqueObjectDataValues: {dtype}. "
6478 f
"Supported: int, str, 'uint'."
6487 """Coerce a 4x4 transformation matrix input into a flat list of 16 floats.
6489 Accepts: numpy.ndarray of shape (4,4) or (16,), list/tuple of 16 floats,
6490 or nested list/tuple of shape (4,4). Helios stores transformation matrices
6491 in **row-major** order: T[i*4 + j] = element (i, j). A numpy ndarray of
6492 shape (4,4) maps directly via .ravel() since numpy is row-major by default.
6495 if isinstance(value, np.ndarray):
6496 if value.shape == (4, 4):
6497 return [float(v)
for v
in value.ravel().tolist()]
6498 if value.shape == (16,):
6499 return [float(v)
for v
in value.tolist()]
6501 f
"Matrix ndarray must have shape (4,4) or (16,), got {value.shape}"
6504 if isinstance(value, (list, tuple))
and len(value) == 4
and \
6505 all(isinstance(row, (list, tuple))
and len(row) == 4
for row
in value):
6508 flat.extend(float(v)
for v
in row)
6511 if isinstance(value, (list, tuple))
and len(value) == 16:
6512 return [float(v)
for v
in value]
6514 f
"Matrix must be a (4,4) ndarray, (16,) ndarray, list of 16 floats, "
6515 f
"or nested 4x4 list. Got: {type(value).__name__}"
6520 """Convert a flat list of 16 floats (row-major) to a (4,4) numpy ndarray."""
6521 return np.array(flat, dtype=np.float32).reshape((4, 4))
6526 """Return the object's 4x4 transformation matrix as a (4,4) float32 ndarray.
6528 Helios stores matrices in row-major order, so element (i, j) is at
6529 position [i, j] of the returned ndarray. The translation column is at
6530 positions [0, 3], [1, 3], [2, 3].
6533 flat = context_wrapper.getObjectTransformationMatrixWrapper(self.
context, int(objID))
6537 """Set the 4x4 transformation matrix on one or more compound objects.
6540 objIDs_or_objID: A single object ID (int) or a list/tuple of object IDs.
6541 T: A 4x4 matrix as numpy.ndarray((4,4) | (16,) float), list of 16 floats,
6542 or a nested 4x4 list. Row-major; T[i, j] is element (i, j).
6546 if isinstance(objIDs_or_objID, (list, tuple)):
6547 context_wrapper.setObjectTransformationMatrixBatchWrapper(
6548 self.
context, list(objIDs_or_objID), flat
6551 context_wrapper.setObjectTransformationMatrixWrapper(
6556 """Return the primitive's 4x4 transformation matrix as a (4,4) float32 ndarray
6557 (row-major; see getObjectTransformationMatrix for layout details)."""
6559 flat = context_wrapper.getPrimitiveTransformationMatrixWrapper(self.
context, int(uuid))
6563 """Set the 4x4 transformation matrix on one or more primitives.
6566 uuids_or_uuid: A single UUID (int) or a list/tuple of UUIDs.
6567 T: A 4x4 matrix; see setObjectTransformationMatrix for accepted formats.
6571 if isinstance(uuids_or_uuid, (list, tuple)):
6572 context_wrapper.setPrimitiveTransformationMatrixBatchWrapper(
6573 self.
context, list(uuids_or_uuid), flat
6576 context_wrapper.setPrimitiveTransformationMatrixWrapper(
6577 self.
context, int(uuids_or_uuid), flat
6583 """Return the axis-aligned bounding box of the domain (or a UUID subset).
6586 uuids: Optional list of primitive UUIDs to restrict the computation to.
6587 If None (default), uses every primitive in the context.
6590 ``(xbounds, ybounds, zbounds)`` where each element is a ``vec2(min, max)``.
6594 xb, yb, zb = context_wrapper.getDomainBoundingBoxWrapper(self.
context)
6596 if not isinstance(uuids, (list, tuple)):
6597 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6598 xb, yb, zb = context_wrapper.getDomainBoundingBoxFilteredWrapper(self.
context, list(uuids))
6599 return (
vec2(xb[0], xb[1]),
vec2(yb[0], yb[1]),
vec2(zb[0], zb[1]))
6602 """Return the bounding sphere of the domain (or a UUID subset).
6605 ``(center, radius)`` where ``center`` is a ``vec3`` and ``radius`` is a float.
6609 center, radius = context_wrapper.getDomainBoundingSphereWrapper(self.
context)
6611 if not isinstance(uuids, (list, tuple)):
6612 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6613 center, radius = context_wrapper.getDomainBoundingSphereFilteredWrapper(self.
context, list(uuids))
6614 return (
vec3(center[0], center[1], center[2]), float(radius))
6622 def setTubeNodes(self, objID: int, nodes: List[vec3]) ->
None:
6623 """Replace the node positions of an existing tube object."""
6625 if not isinstance(nodes, (list, tuple)):
6626 raise ValueError(f
"nodes must be a list or tuple, got {type(nodes).__name__}")
6628 for i, n
in enumerate(nodes):
6629 if not isinstance(n, vec3):
6630 raise ValueError(f
"nodes[{i}] must be a vec3, got {type(n).__name__}")
6631 flat.extend([n.x, n.y, n.z])
6632 context_wrapper.setTubeNodesWrapper(self.
context, int(objID), flat)
6634 def setTubeRadii(self, objID: int, radii: List[float]) ->
None:
6635 """Replace the per-node radii of an existing tube object."""
6637 if not isinstance(radii, (list, tuple)):
6638 raise ValueError(f
"radii must be a list or tuple, got {type(radii).__name__}")
6639 context_wrapper.setTubeRadiiWrapper(self.
context, int(objID), [float(r)
for r
in radii])
6641 def scaleTubeGirth(self, objID: int, scale_factor: float) ->
None:
6642 """Scale the radii of an existing tube object by ``scale_factor``."""
6644 context_wrapper.scaleTubeGirthWrapper(self.
context, int(objID), float(scale_factor))
6647 """Scale the lengths between tube nodes by ``scale_factor``."""
6649 context_wrapper.scaleTubeLengthWrapper(self.
context, int(objID), float(scale_factor))
6652 """Remove all tube nodes from index ``node_index`` to the end."""
6654 context_wrapper.pruneTubeNodesWrapper(self.
context, int(objID), int(node_index))
6657 color: Optional[RGBcolor] =
None,
6658 texture_file: Optional[str] =
None,
6659 uv: Optional[vec2] =
None) ->
None:
6660 """Append a new segment to an existing tube object.
6662 Pass exactly one of ``color`` (an RGBcolor) or both ``texture_file`` and
6663 ``uv`` (a vec2 of texture u-fractions) to specify how the new segment
6667 if not isinstance(node_position, vec3):
6668 raise ValueError(f
"node_position must be a vec3, got {type(node_position).__name__}")
6669 has_color = color
is not None
6670 has_texture = texture_file
is not None or uv
is not None
6671 if has_color == has_texture:
6673 "appendTubeSegment requires exactly one of (color) or "
6674 "(texture_file and uv); cannot mix or omit both."
6677 if not isinstance(color, RGBcolor):
6678 raise ValueError(f
"color must be an RGBcolor, got {type(color).__name__}")
6679 context_wrapper.appendTubeSegmentColorWrapper(
6680 self.
context, int(objID), node_position.to_list(), float(radius),
6681 [color.r, color.g, color.b]
6684 if texture_file
is None or uv
is None:
6686 "appendTubeSegment with texture requires both texture_file and uv."
6688 if not isinstance(uv, vec2):
6689 raise ValueError(f
"uv must be a vec2, got {type(uv).__name__}")
6691 texture_file, [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp']
6693 context_wrapper.appendTubeSegmentTextureWrapper(
6694 self.
context, int(objID), node_position.to_list(), float(radius),
6695 tex_path, [uv.x, uv.y]
6701 """Group the given primitives into a new polymesh compound object and return its ID."""
6703 if not isinstance(uuids, (list, tuple)):
6704 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6706 raise ValueError(
"addPolymeshObject requires at least one UUID")
6707 return context_wrapper.addPolymeshObjectWrapper(self.
context, list(uuids))
6712 """Set the color of one or more compound objects.
6714 Accepts a single object ID or list/tuple of IDs. ``color`` must be an
6715 ``RGBcolor`` or ``RGBAcolor``.
6718 if isinstance(color, RGBAcolor):
6719 comps = [color.r, color.g, color.b, color.a]
6720 if isinstance(objIDs_or_objID, (list, tuple)):
6721 context_wrapper.setObjectColorRGBABatchWrapper(self.
context, list(objIDs_or_objID), comps)
6723 context_wrapper.setObjectColorRGBAWrapper(self.
context, int(objIDs_or_objID), comps)
6724 elif isinstance(color, RGBcolor):
6725 comps = [color.r, color.g, color.b]
6726 if isinstance(objIDs_or_objID, (list, tuple)):
6727 context_wrapper.setObjectColorRGBBatchWrapper(self.
context, list(objIDs_or_objID), comps)
6729 context_wrapper.setObjectColorRGBWrapper(self.
context, int(objIDs_or_objID), comps)
6732 f
"color must be an RGBcolor or RGBAcolor, got {type(color).__name__}"
6736 """Override the texture mapping with the object's vertex color."""
6738 if isinstance(objIDs_or_objID, (list, tuple)):
6739 context_wrapper.overrideObjectTextureColorBatchWrapper(self.
context, list(objIDs_or_objID))
6741 context_wrapper.overrideObjectTextureColorWrapper(self.
context, int(objIDs_or_objID))
6744 """Restore use of the texture color (undoes overrideObjectTextureColor)."""
6746 if isinstance(objIDs_or_objID, (list, tuple)):
6747 context_wrapper.useObjectTextureColorBatchWrapper(self.
context, list(objIDs_or_objID))
6749 context_wrapper.useObjectTextureColorWrapper(self.
context, int(objIDs_or_objID))
6754 """Mark one or more primitives as dirty (geometry has been modified)."""
6756 if isinstance(uuids_or_uuid, (list, tuple)):
6757 context_wrapper.markPrimitiveDirtyBatchWrapper(self.
context, list(uuids_or_uuid))
6759 context_wrapper.markPrimitiveDirtyWrapper(self.
context, int(uuids_or_uuid))
6762 """Mark one or more primitives as clean (cancels dirty state)."""
6764 if isinstance(uuids_or_uuid, (list, tuple)):
6765 context_wrapper.markPrimitiveCleanBatchWrapper(self.
context, list(uuids_or_uuid))
6767 context_wrapper.markPrimitiveCleanWrapper(self.
context, int(uuids_or_uuid))
6772 """Set the (Nx, Ny) subdivision count of one or more tile objects.
6774 The Helios C++ API is batch-only; a single objID is wrapped as a
6775 single-element list.
6778 if not isinstance(subdiv, int2):
6779 raise ValueError(f
"subdiv must be an int2, got {type(subdiv).__name__}")
6780 if isinstance(objIDs_or_objID, (list, tuple)):
6781 ids = list(objIDs_or_objID)
6783 ids = [int(objIDs_or_objID)]
6784 context_wrapper.setTileObjectSubdivisionCountWrapper(
6785 self.
context, ids, int(subdiv.x), int(subdiv.y)
6789 """Set tile object subdivision dynamically based on a target area ratio.
6791 ``area_ratio`` is the approximate ratio between the whole tile's area and an
6792 individual sub-patch's area, so each tile is subdivided into roughly
6793 ``area_ratio`` sub-patches. It must be >= 1 (a sub-patch cannot be larger than
6794 the tile). The tile's position, size, and orientation are preserved.
6799 f
"area_ratio must be >= 1 (it is the ratio of the whole tile area to an "
6800 f
"individual sub-patch area), got {area_ratio}"
6802 if isinstance(objIDs_or_objID, (list, tuple)):
6803 ids = list(objIDs_or_objID)
6805 ids = [int(objIDs_or_objID)]
6806 context_wrapper.setTileObjectSubdivisionByAreaRatioWrapper(
6807 self.
context, ids, float(area_ratio)
6817 """Return a new list with deleted UUIDs removed; the input list is not mutated.
6819 This mirrors the convention used by ``cropDomain``, which returns the
6820 survivors rather than mutating in place.
6823 if not isinstance(uuids, (list, tuple)):
6824 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6825 return context_wrapper.cleanDeletedUUIDsWrapper(self.
context, list(uuids))
6828 """Return a new list with deleted object IDs removed; input is not mutated."""
6830 if not isinstance(objIDs, (list, tuple)):
6831 raise ValueError(f
"objIDs must be a list or tuple, got {type(objIDs).__name__}")
6832 return context_wrapper.cleanDeletedObjectIDsWrapper(self.
context, list(objIDs))
6836 def writeXML(self, filename: str, uuids: Optional[List[int]] =
None, quiet: bool =
False) ->
None:
6837 """Write the context (or a UUID subset) to an XML file.
6840 filename: Output file path. Must end in .xml.
6841 uuids: Optional list of primitive UUIDs to restrict the export. If
6842 None (default), all primitives are written.
6843 quiet: Suppress informational console output.
6848 context_wrapper.writeXMLWrapper(self.
context, path, bool(quiet))
6850 if not isinstance(uuids, (list, tuple)):
6851 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6852 context_wrapper.writeXMLFilteredWrapper(self.
context, path, list(uuids), bool(quiet))
6854 def writeXML_byobject(self, filename: str, objIDs: List[int], quiet: bool =
False) ->
None:
6855 """Write a subset of compound objects to an XML file."""
6858 if not isinstance(objIDs, (list, tuple)):
6859 raise ValueError(f
"objIDs must be a list or tuple, got {type(objIDs).__name__}")
6860 context_wrapper.writeXMLByObjectWrapper(self.
context, path, list(objIDs), bool(quiet))
6864 def randu(self, low=None, high=None):
6865 """Draw a uniform random number using the Context's RNG.
6868 ``randu()`` -> float in [0, 1)
6869 ``randu(low: float, high: float)`` -> float in [low, high)
6870 ``randu(low: int, high: int)`` -> int in [low, high]
6872 Whether the integer or float overload is invoked is determined by
6873 ``isinstance(low, int)``; pass ``low/high`` as Python ints for the
6877 if low
is None and high
is None:
6878 return context_wrapper.randuBasicWrapper(self.
context)
6879 if low
is None or high
is None:
6880 raise ValueError(
"randu requires both low and high, or neither.")
6881 if isinstance(low, bool)
or isinstance(high, bool):
6882 raise ValueError(
"randu bounds cannot be bool.")
6885 if isinstance(low, int)
and isinstance(high, int):
6886 return context_wrapper.randuIntRangeWrapper(self.
context, low, high)
6887 return context_wrapper.randuRangeWrapper(self.
context, float(low), float(high))
6889 def randn(self, mean=None, stddev=None) -> float:
6890 """Draw a normal random number using the Context's RNG.
6893 ``randn()`` -> standard normal (mean 0, stddev 1)
6894 ``randn(mean: float, stddev: float)`` -> N(mean, stddev**2)
6897 if mean
is None and stddev
is None:
6898 return context_wrapper.randnBasicWrapper(self.
context)
6899 if mean
is None or stddev
is None:
6900 raise ValueError(
"randn requires both mean and stddev, or neither.")
6901 return context_wrapper.randnParamsWrapper(self.
context, float(mean), float(stddev))
6905 def setLocation(self, location_or_lat, longitude=None, utc_offset=None, altitude=0.0) -> None:
6906 """Set the geographic location used by solar/radiation calculations.
6909 ``setLocation(loc: Location)``
6910 ``setLocation(latitude_deg: float, longitude_deg: float, utc_offset: float, altitude=0.0)``
6912 ``altitude`` is the height of the local Cartesian origin in meters above
6913 sea level. It is only used in the (lat, lon, utc) float form; when passing
6914 a ``Location`` object, the location's own altitude is used.
6917 if isinstance(location_or_lat, Location):
6918 if longitude
is not None or utc_offset
is not None or altitude != 0.0:
6919 raise ValueError(
"When passing a Location, do not also pass longitude/utc_offset/altitude; "
6920 "set them on the Location object instead.")
6921 loc = location_or_lat
6923 if longitude
is None or utc_offset
is None:
6925 "setLocation requires either a Location object or "
6926 "(latitude_deg, longitude_deg, utc_offset) as 3 floats."
6928 loc =
Location(float(location_or_lat), float(longitude), float(utc_offset), float(altitude))
6929 context_wrapper.setLocationWrapper(self.
context, loc.latitude, loc.longitude, loc.utc_offset, loc.altitude)
6932 """Return the Context's currently-configured geographic location."""
6934 lat, lon, utc, alt = context_wrapper.getLocationWrapper(self.
context)
6935 return Location(lat, lon, utc, alt)
6942 """Generate a colormap with ``n_colors`` entries from a named colormap.
6945 name: Helios colormap name (e.g., "hot", "cool", "lava", "rainbow").
6946 n_colors: Number of colors in the returned ramp.
6949 A list of ``RGBcolor`` instances of length ``n_colors``.
6952 flat = context_wrapper.generateColormapNamedWrapper(self.
context, name, int(n_colors))
6953 return [
RGBcolor(flat[i*3 + 0], flat[i*3 + 1], flat[i*3 + 2])
for i
in range(int(n_colors))]
6956 """Generate one texture file per color in ``colormap`` derived from
6957 ``texture_file``. Returns the list of generated file paths.
6960 if not isinstance(colormap, (list, tuple)):
6961 raise ValueError(f
"colormap must be a list or tuple, got {type(colormap).__name__}")
6963 for i, c
in enumerate(colormap):
6964 if not isinstance(c, RGBcolor):
6965 raise ValueError(f
"colormap[{i}] must be an RGBcolor, got {type(c).__name__}")
6966 flat.extend([c.r, c.g, c.b])
6969 texture_file, [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp']
6971 return context_wrapper.generateTexturesFromColormapWrapper(
6972 self.
context, validated_path, flat
6976 """Return the primitive's texture transparency mask as a 2D bool ndarray.
6978 Returns None if the primitive has no associated transparency channel
6979 (e.g., it is untextured or its texture has no alpha). The returned
6980 ndarray has shape (height, width) and dtype ``bool``.
6983 result = context_wrapper.getPrimitiveTextureTransparencyDataWrapper(self.
context, int(uuid))
6986 width, height, flat = result
6987 return np.array(flat, dtype=bool).reshape((height, width))
6991 """Raise if `context`'s native Context has already been destroyed.
6993 Plugin models pass ``context.getNativePtr()`` to a C++ constructor that
6994 stores the raw pointer for the lifetime of the model. Destroying the
6995 Context (via ``__exit__``, ``__del__``, or garbage collection) frees that
6996 memory without invalidating the model's copy, so any later call
6997 dereferences freed memory and segfaults.
6999 Models must hold a Python reference to the owning Context (keeping it
7000 alive) and call this before every native call (turning an explicit close
7001 into an actionable error instead of a crash).
7004 context: The Context the model was constructed from.
7005 owner_name: Class name of the calling model, used in the message.
7008 RuntimeError: If the Context has been destroyed.
7010 if context
is None or getattr(context,
'context',
None)
is None:
7012 f
"{owner_name} is bound to a Context that has already been destroyed.\n"
7013 "The native Context was freed while this model still referenced it; "
7014 "continuing would dereference freed memory and crash the interpreter.\n"
7016 "This usually means the model outlived its Context's 'with' block:\n"
7017 " with Context() as context:\n"
7018 f
" model = {owner_name}(context)\n"
7019 " model.run() # <-- Context already destroyed here\n"
7021 f
"Fix: keep all {owner_name} usage inside the Context's 'with' block, "
7022 "or create the Context without a 'with' statement so it lives as long "
Central simulation environment for PyHelios that manages 3D primitives and their data.
getDomainBoundingSphere(self, Optional[List[int]] uuids=None)
Return the bounding sphere of the domain (or a UUID subset).
None setGlobalDataVec3(self, str label, x_or_vec, float y=None, float z=None)
Set global data as vec3.
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.
int getTubeObjectNodeCount(self, int objID)
vec3 getBoxObjectSize(self, int objID)
None duplicateObjectData(self, int objID, str old_label, str new_label)
Copy object data to a new label.
bool doesPolymeshObjectHaveVertexNormals(self, int objID)
Return True if a polymesh object carries per-vertex normals.
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.
getObjectData(self, int objID, str label, type data_type=None)
Get object data with optional type specification.
getAllPrimitiveVertices(self)
Get vertices for all primitives.
None setObjectOrigin(self, int objID, vec3 origin)
Translate the object so its origin is moved to the given point.
List[RGBcolor] getTubeObjectNodeColors(self, int objID)
'np.ndarray' _mat4_to_ndarray(List[float] flat)
Convert a flat list of 16 floats (row-major) to a (4,4) numpy ndarray.
Union[int, List[int]] copyObject(self, Union[int, List[int]] ObjID)
Copy one or more compound objects.
List[str] listObjectData(self, int objID)
List all data labels on a specific object.
None scaleTubeLength(self, int objID, float scale_factor)
Scale the lengths between tube nodes by scale_factor.
int getMaterialTwosidedFlag(self, str material_label)
Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided).
Optional[List[int]] cropDomain(self, *args)
Crop the context domain to the given XYZ bounds.
None setMaterialDataVec3(self, str material_label, str data_label, vec3 value)
Set vec3 data on a material.
None markPrimitiveDirty(self, uuids_or_uuid)
Mark one or more primitives as dirty (geometry has been modified).
None deletePrimitive(self, Union[int, List[int]] uuids_or_uuid)
Delete one or more primitives from the context.
None setMaterialDataInt4(self, str material_label, str data_label, int4 value)
Set int4 data on a material.
None clearAllPrimitiveData(self, str label)
Remove a named data field from every primitive in the Context.
None setMaterialDataInt3(self, str material_label, str data_label, int3 value)
Set int3 data on a material.
_validate_uuid(self, int uuid)
Validate that a UUID exists in this context.
int addAdaptiveTileObject(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), SphericalCoord rotation=SphericalCoord(1, 0, 0), Optional[AdaptiveTileRefinement] refinement=None, Optional[RGBcolor] color=None, Optional[str] texturefile=None, Optional[int2] texture_repeat=None)
Add a patch subdivided into sub-patches whose size adapts with distance from a target point.
getGlobalData(self, str label, type data_type=None)
Get global data with optional type specification.
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.
addTimeseriesData(self, str label, float value, 'Date' date, 'Time' time)
Add a data point to a timeseries variable.
int getPolymeshObjectVertexCount(self, int objID)
Return the number of shared vertices in a polymesh object.
None setObjectColor(self, objIDs_or_objID, color)
Set the color of one or more compound objects.
str _validate_output_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize output file path for security.
bool primitiveTextureHasTransparencyChannel(self, int uuid)
Check if primitive texture has a transparency channel.
getPrimitiveMaterialLabel(self, uuid)
Get the material label assigned to a primitive or multiple primitives.
None enablePrimitiveDataValueCaching(self, str label)
Enable value caching for the given primitive-data label.
int getPrimitiveTwosidedFlag(self, int uuid, int default_value=1)
Get two-sided rendering flag for a primitive.
None cropDomainX(self, vec2 xbounds)
List[str] getAllPrimitiveTextureFiles(self)
Get texture files for all primitives.
None setObjectDataVec4(self, objids_or_objid, str label, x_or_vec, float y=None, float z=None, float w=None)
Set object data as vec4.
getDomainBoundingBox(self, Optional[List[int]] uuids=None)
Return the axis-aligned bounding box of the domain (or a UUID subset).
bool isPrimitiveHidden(self, int uuid)
Check if a primitive is hidden.
np.ndarray getPrimitiveDataArray(self, List[int] uuids, str label)
Get primitive data values for multiple primitives as a NumPy array.
float calculateAreaIndex(self, List[int] leaf_uuids, Optional[List[int]] wood_uuids=None, Optional[float] ground_area=None)
Calculate the one-sided area index on a ground-area basis.
None setObjectAverageNormal(self, int objID, vec3 origin, vec3 new_normal)
Rotate the object so its area-weighted average normal aligns with new_normal.
int2 getTileObjectTextureRepeat(self, int objID)
Get the texture repeat count requested when the tile object was created.
List[int] getObjectPrimitiveUUIDs(self, objIDs)
Get flattened primitive UUIDs for one object, a list of objects, or a list-of-lists.
None writeXML_byobject(self, str filename, List[int] objIDs, bool quiet=False)
Write a subset of compound objects to an XML file.
bool is_plugin_available(self, str plugin_name)
Check if a specific plugin is available.
getPrimitiveColor(self, uuid)
Get the color of a primitive or multiple primitives.
getPrimitiveArea(self, uuid)
Get the area of a primitive or multiple primitives.
str getGlobalDataString(self, str label)
Get string global data.
vec3 getTileObjectNormal(self, int objID)
None setPolymeshObjectTopology(self, int objID, List[vec3] vertices, List[int3] faces, List[int] face_UUIDs, Optional[List[vec3]] vertex_normals=None, Optional[List[vec2]] vertex_uv=None, VertexNormalSource normal_source=VertexNormalSource.NONE)
Attach an indexed face set to a polymesh object built programmatically.
bool doesTimeseriesVariableExist(self, str label)
Check whether a timeseries variable exists.
None setTubeNodes(self, int objID, List[vec3] nodes)
Replace the node positions of an existing tube object.
List[int] getAllObjectIDs(self)
bool isPrimitiveDataValueCachingEnabled(self, str label)
Return True if value caching is enabled for the given primitive-data label.
None setObjectDataUInt(self, objids_or_objid, str label, int value)
Set object data as unsigned 32-bit integer.
deleteTimeseriesVariable(self, str label)
Delete a single timeseries variable and all of its data points.
List[str] listAllObjectDataLabels(self)
List all object data labels in context.
List[str] get_available_plugins(self)
Get list of available plugins for this PyHelios instance.
__exit__(self, exc_type, exc_value, traceback)
int getPrimitiveParentObjectID(self, int uuid)
Return the ID of the compound object the primitive belongs to.
None setPrimitiveNormal(self, uuids_or_uuid, vec3 origin, vec3 new_normal)
Rotate one or more primitives so their normals align with new_normal.
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.
None setTileObjectSubdivisionCount(self, objIDs_or_objID, int2 subdiv)
Set the (Nx, Ny) subdivision count of one or more tile objects.
None setPrimitiveParentObjectID(self, uuids_or_uuid, int objID)
Reassign one or more primitives to belong to the given compound object.
None hidePrimitive(self, uuids_or_uuid)
Hide one or more primitives.
None clearMaterialData(self, str material_label, str data_label)
Clear the named data entry on the given material.
int getTimeseriesLength(self, str label)
Get the number of data points in a timeseries variable.
None setTriangleVertices(self, int uuid, vec3 vertex0, vec3 vertex1, vec3 vertex2)
Replace the three vertices of an existing triangle primitive.
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.
vec2 getAdaptiveTileObjectSubpatchSizeRange(self, int objID)
Get the sub-patch edge lengths actually achieved, as opposed to those requested.
float getConeObjectVolume(self, int objID)
setCurrentTimeseriesPoint(self, str label, int index)
Set the Context date and time from a timeseries data point index.
calculatePrimitiveDataAreaWeightedMean(self, List[int] uuids, str label, type return_type=float)
Calculate area-weighted mean of primitive data.
bool doesMaterialDataExist(self, str material_label, str data_label)
Return True if the named material has data stored under data_label.
getTileObjectAreaRatio(self, objIDs)
Get tile-object area ratio for one or multiple tile objects.
bool doesPrimitiveDataExist(self, int uuid, str label)
Check if primitive data exists for a specific primitive and label.
None setMaterialDataVec2(self, str material_label, str data_label, vec2 value)
Set vec2 data on a material.
int getObjectDataSize(self, int objID, str label)
Get the size of object data array.
vec3 getAdaptiveTileObjectNormal(self, int objID)
Get a unit vector normal to an adaptive tile object surface.
float getTubeObjectSegmentVolume(self, int objID, int segment_index)
List[str] listMaterials(self)
Get list of all material labels in the context.
None enableObjectDataValueCaching(self, str label)
Enable value caching for the given object-data label.
None setMaterialDataInt2(self, str material_label, str data_label, int2 value)
Set int2 data on a material.
List[int] cleanDeletedUUIDs(self, List[int] uuids)
Return a new list with deleted UUIDs removed; the input list is not mutated.
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 getPrimitiveMaterialID(self, int uuid)
Return the material ID assigned to the given primitive.
'np.ndarray' getAllPrimitiveColors(self)
Get colors for all primitives.
int getPrimitiveCount(self)
vec3 getObjectAverageNormal(self, int objID)
Return the area-weighted average normal of all primitives in the object.
Optional[ 'np.ndarray'] getPrimitiveTextureTransparencyData(self, int uuid)
Return the primitive's texture transparency mask as a 2D bool ndarray.
None aggregatePrimitiveDataSum(self, List[int] uuids, List[str] labels, str result_label)
Sum multiple primitive data fields into a new field.
List[str] listGlobalData(self)
List all global data labels.
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.
int getAdaptiveTileObjectMaxRefinementLevel(self, int objID)
Get the maximum quadtree refinement level, i.e.
List[PrimitiveInfo] getAllPrimitiveInfo(self)
Get physical properties and geometry information for all primitives in the context.
float getMaterialDataDouble(self, str material_label, str data_label)
None incrementPrimitiveData(self, List[int] uuids, str label, increment, str data_type=None)
Increment primitive data for the given UUIDs.
None copyObjectData(self, int source_objID, int destination_objID)
Copy all object data from source to destination compound object.
'Time' queryTimeseriesTime(self, str label, int index)
Get the Time associated with a timeseries data point.
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 showPrimitive(self, uuids_or_uuid)
Show one or more previously hidden primitives.
int2 getAdaptiveTileObjectBaseSubdivisionCount(self, int objID)
Get the number of coarsest-level cells spanning an adaptive tile in x and y.
None writePLY(self, str filename, Optional[List[int]] UUIDs=None)
Write geometry to a PLY (Stanford Polygon) file.
List[int] filterPrimitivesByData(self, List[int] uuids, str label, value, str comparator="=")
Filter primitives by data value.
List[PrimitiveInfo] getPrimitivesInfoForObject(self, int object_id)
Get physical properties and geometry information for all primitives belonging to a specific object.
int2 getTileObjectEffectiveTextureRepeat(self, int objID)
Get the texture repeat count actually applied to the sub-patches of a tile object.
vec3 getSphereObjectCenter(self, int objID)
None setPrimitiveDataInt2(self, uuids_or_uuid, str label, x_or_vec, int y=None)
Set primitive data as int2 for one or multiple primitives.
int getGlobalDataType(self, str label)
Get the HeliosDataType enum for global data.
print_plugin_status(self)
Print detailed plugin status information.
vec3 getTriangleVertex(self, int uuid, int number)
float getSphereObjectVolume(self, int objID)
List getUniquePrimitiveDataValues(self, str label, type dtype)
Return the unique values stored under label across all primitives.
setDate(self, int year, int month, int day)
Set the simulation date.
List[int] getDirtyUUIDs(self, bool include_deleted=True)
Return the list of UUIDs whose geometry has been modified since the last markGeometryClean call.
bool objectHasTexture(self, int objID)
Return True if the compound object has a texture assigned.
vec3 getAdaptiveTileObjectCenter(self, int objID)
Get the Cartesian coordinates of the center of an adaptive tile object.
int2 getPrimitiveTextureSize(self, int uuid)
Get the texture size (width, height) of a primitive.
vec3 getSphereObjectRadius(self, int objID)
Get per-axis radii of a sphere object.
List[vec3] getTileObjectVertices(self, int objID)
int getMaterialIDFromLabel(self, str material_label)
Look up a material ID from its human-readable label.
getPrimitiveData(self, int uuid, str label, type data_type=None)
Get primitive data for a specific primitive.
None setGlobalDataUInt(self, str label, int value)
Set global data as unsigned 32-bit integer.
None setTileObjectSubdivisionByAreaRatio(self, objIDs_or_objID, float area_ratio)
Set tile object subdivision dynamically based on a target area ratio.
List[float] getConeObjectNodeRadii(self, int objID)
List[str] get_missing_plugins(self, List[str] requested_plugins)
Get list of requested plugins that are not available.
None setMaterialData(self, str material_label, str data_label, value)
Set material data with type detection from the Python value.
None showObject(self, objids_or_objid)
Show one or more previously hidden compound objects.
List[vec3] getConeObjectNodes(self, int objID)
None setObjectDataInt(self, objids_or_objid, str label, int value)
Set object data as signed 32-bit integer.
int2 getAdaptiveTileObjectTextureRepeat(self, int objID)
Get the texture repeat count of an adaptive tile object.
bool isGeometryDirty(self)
None printObjectInfo(self, int objID)
Print summary info for the object to stdout (for debugging).
clearTimeseriesData(self)
Clear all timeseries data from the Context.
vec3 getVoxelCenter(self, int uuid)
None copyPrimitiveData(self, int sourceUUID, int destinationUUID)
Copy all primitive data from source to destination primitive.
List[str] getAllPrimitiveMaterialLabels(self)
Get material labels for all primitives.
None duplicateGlobalData(self, str old_label, str new_label)
Duplicate global data to a new label.
List[vec2] getPolymeshObjectVertexUV(self, int objID)
Return the per-vertex texture coordinates of a polymesh object, or an empty list if it has none.
vec4 getMaterialDataVec4(self, str material_label, str data_label)
None setMaterialDataVec4(self, str material_label, str data_label, vec4 value)
Set vec4 data on a material.
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.
'np.ndarray' getPrimitiveTransformationMatrix(self, int uuid)
Return the primitive's 4x4 transformation matrix as a (4,4) float32 ndarray (row-major; see getObject...
bool doesObjectHaveAnalyticVertexNormals(self, int objID)
Return True if a compound object can report analytic vertex normals.
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.
vec3 getDiskObjectCenter(self, int objID)
None renamePrimitiveData(self, int uuid, str old_label, str new_label)
Rename a primitive-data label on a single primitive.
None incrementGlobalData(self, str label, increment)
Increment global data.
getObjectBoundingBox(self, objIDs)
Get axis-aligned bounding box for one object or a list of objects.
None setGlobalDataInt4(self, str label, x_or_vec, int y=None, int z=None, int w=None)
Set global data as int4.
None setObjectDataVec3(self, objids_or_objid, str label, x_or_vec, float y=None, float z=None)
Set object data as vec3.
_check_context_available(self)
Helper method to check if context is available with detailed error messages.
loadTabularTimeseriesData(self, str data_file, List[str] column_labels, str delimiter=",", str date_string_format="YYYYMMDD", int headerlines=0)
Load tabular timeseries data from a text file.
None markPrimitiveClean(self, uuids_or_uuid)
Mark one or more primitives as clean (cancels dirty state).
vec3 getConeObjectNode(self, int objID, int number)
int3 getMaterialDataInt3(self, str material_label, str data_label)
None clearGlobalData(self, str label)
Clear global data.
vec3 getConeObjectAxisUnitVector(self, int objID)
None scaleTubeGirth(self, int objID, float scale_factor)
Scale the radii of an existing tube object by scale_factor.
None setObjectDataVec2(self, objids_or_objid, str label, x_or_vec, float y=None)
Set object data as vec2.
int4 getMaterialDataInt4(self, str material_label, str data_label)
int addPatch(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), Optional[SphericalCoord] rotation=None, Optional[RGBcolor] color=None)
None setTubeRadii(self, int objID, List[float] radii)
Replace the per-node radii of an existing tube object.
addMaterial(self, str material_label)
Create a new material for sharing visual properties across primitives.
vec2 getTileObjectSize(self, int objID)
int getObjectType(self, int objID)
Return the integer-coded helios::ObjectType of a compound object.
List[int3] getPolymeshObjectFaces(self, int objID)
Return the vertex index triples defining each face of a polymesh object.
None disableObjectDataValueCaching(self, str label)
Disable value caching for the given object-data label.
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.
bool isPrimitiveDirty(self, int uuid)
Return True if the primitive's geometry has been modified since the last clean mark.
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 setObjectDataInt2(self, objids_or_objid, str label, x_or_vec, int y=None)
Set object data as int2.
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.
vec2 getDiskObjectSize(self, int objID)
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.
List[str] listAllPrimitiveDataLabels(self)
Return the union of all primitive-data labels used across every primitive in the context.
str _validate_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize file path for security.
int getJulianDate(self)
Get the current simulation date as Julian day (1-366).
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.
None setGlobalDataFloat(self, str label, float value)
Set global data as 32-bit float.
'np.ndarray' getAllPrimitiveTypes(self)
Get types for all primitives.
float queryTimeseriesData(self, str label, 'Date' date=None, 'Time' time=None, int index=None)
Query a timeseries data value.
int getConeObjectSubdivisionCount(self, int objID)
getPrimitiveVertices(self, uuid)
Get vertices of a primitive or multiple primitives.
int getPolymeshObjectPrimitiveUUIDForFace(self, int objID, int face_index)
Return the UUID of the primitive making up a given face of a polymesh object.
None appendTubeSegment(self, int objID, vec3 node_position, float radius, *, Optional[RGBcolor] color=None, Optional[str] texture_file=None, Optional[vec2] uv=None)
Append a new segment to an existing tube object.
None writeXML(self, str filename, Optional[List[int]] uuids=None, bool quiet=False)
Write the context (or a UUID subset) to an XML file.
None setPrimitiveElevation(self, int uuid, vec3 origin, float new_elevation)
Rotate a single primitive about the given origin so its elevation equals new_elevation (radians).
List[int2] getPolymeshObjectBoundaryEdges(self, int objID)
Return the boundary edges of a polymesh object as vertex index pairs.
assignMaterialToPrimitive(self, uuid, str material_label)
Assign a material to primitive(s).
randu(self, low=None, high=None)
Draw a uniform random number using the Context's RNG.
None scalePrimitiveData(self, uuids_or_label, label_or_factor, factor=None)
Scale primitive data by a factor.
List[int] getUniquePrimitiveParentObjectIDs(self, List[int] uuids, bool include_zero=True)
Return the unique set of compound-object IDs that the given primitives belong to.
bool doesObjectDataExist(self, int objID, str label)
Check if object data exists.
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.
getMaterialData(self, str material_label, str data_label, type data_type=None)
Get material data, auto-detecting the type from Helios storage if not specified.
bool isPrimitiveTextureColorOverridden(self, int uuid)
Check if primitive texture color is overridden.
List[int] getDeletedUUIDs(self)
Return the list of UUIDs that have been deleted from the context.
None clearPrimitiveData(self, uuids, str label)
Remove a named data field from one primitive or a list of primitives.
float getMaterialDataFloat(self, str material_label, str data_label)
float sumPrimitiveSurfaceArea(self, List[int] uuids)
Calculate total one-sided surface area for a set of primitives.
int predictAdaptiveTileObjectSubpatchCount(self, vec2 size, Optional[AdaptiveTileRefinement] refinement=None, Optional[int2] texture_repeat=None)
Determine how many sub-patches an adaptive tile object would contain, without building geometry.
int3 getBoxObjectSubdivisionCount(self, int objID)
bool doesObjectHaveSharedVertexTopology(self, int objID)
Return True if a compound object reports which member primitives meet at each vertex.
vec2 getPatchSize(self, int uuid)
None disablePrimitiveDataValueCaching(self, str label)
Disable value caching for the given primitive-data label.
bool doesObjectExist(self, int objID)
Return True if a compound object with the given ID exists.
vec2 getAdaptiveTileObjectSize(self, int objID)
Get the dimensions of an entire adaptive tile object.
bool isPolymeshObjectClosed(self, int objID)
Return True if a polymesh object is a closed surface, i.e.
int getPatchCount(self, bool include_hidden=True)
seedRandomGenerator(self, int seed)
Seed the random number generator for reproducible stochastic results.
None deleteObject(self, Union[int, List[int]] objIDs_or_objID)
Delete one or more compound objects from the context.
None translatePrimitive(self, Union[int, List[int]] UUID, vec3 shift)
Translate one or more primitives by a shift vector.
None setGlobalDataInt3(self, str label, x_or_vec, int y=None, int z=None)
Set global data as int3.
Union[int, List[int]] copyPrimitive(self, Union[int, List[int]] UUID)
Copy one or more primitives.
None setMaterialDataFloat(self, str material_label, str data_label, float value)
Set float data on a material.
None printPrimitiveInfo(self, int uuid)
Print summary info for the primitive to stdout (for debugging).
Union[List[vec3], List[List[vec3]]] getObjectPrimitiveVertexNormals(self, int objID, Union[int, List[int]] uuid)
Return the analytic surface normals at each vertex of a member primitive.
setMaterialTwosidedFlag(self, str material_label, int twosided_flag)
Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided).
None setObjectDataString(self, objids_or_objid, str label, str value)
Set object data as string.
int getPolymeshObjectFaceCount(self, int objID)
Return the number of faces in a polymesh object.
None setPrimitiveDataString(self, uuids_or_uuid, str label, str value)
Set primitive data as string for one or multiple primitives.
None setPrimitiveColor(self, uuids, color)
Set the RGB or RGBA color of one primitive or a list of primitives.
None setGlobalDataDouble(self, str label, float value)
Set global data as 64-bit double.
None renameMaterial(self, str old_label, str new_label)
Rename an existing material.
float getTubeObjectVolume(self, int objID)
None setMaterialDataInt(self, str material_label, str data_label, int value)
Set int data on a material.
vec2 getMaterialDataVec2(self, str material_label, str data_label)
List[vec3] getTubeObjectNodes(self, int objID)
None overrideObjectTextureColor(self, objIDs_or_objID)
Override the texture mapping with the object's vertex color.
vec3 getMaterialDataVec3(self, str material_label, str data_label)
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.
float getConeObjectLength(self, int objID)
None setMaterialDataDouble(self, str material_label, str data_label, float value)
Set double-precision float data on a material.
None rotatePrimitive(self, Union[int, List[int]] UUID, float angle, Union[str, vec3] axis, Optional[vec3] origin=None)
Rotate one or more primitives.
List[str] getLoadedXMLFiles(self)
Return the list of XML file paths that have been loaded into this context.
vec3 getBoxObjectCenter(self, int objID)
updateTimeseriesData(self, str label, 'Date' date, 'Time' time, float new_value)
Update the value of an existing timeseries data point.
calculatePrimitiveDataSum(self, List[int] uuids, str label, type return_type=float)
Calculate sum of primitive data across UUIDs.
List[vec3] getPolymeshObjectVertexNormals(self, int objID)
Return the per-vertex normals of a polymesh object.
getPrimitiveTextureUV(self, uuid)
Get the texture UV coordinates of a primitive or multiple primitives.
PrimitiveInfo getPrimitiveInfo(self, int uuid)
Get physical properties and geometry information for a single primitive.
int getMaterialDataType(self, str material_label, str data_label)
Return the HeliosDataType enum value for the given material data entry.
None setObjectDataInt3(self, objids_or_objid, str label, x_or_vec, int y=None, int z=None)
Set object data as int3.
_validate_uuids(self, uuids)
Validate that every UUID in uuids exists in this context.
int getPrimitiveDataSize(self, int uuid, str label)
Get the size/length of primitive data (for vector data).
int getGlobalDataSize(self, str label)
Get the size of global data array.
getPrimitiveBoundingBox(self, uuids)
Get axis-aligned bounding box for one primitive or a list of primitives.
float getPolymeshObjectSurfaceArea(self, int objID)
Return the total surface area of a polymesh object, summed over every face.
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.
List[vec3] getPolymeshObjectVertices(self, int objID)
Return the deduplicated shared vertex positions of a polymesh object.
int addPolymeshObject(self, List[int] uuids)
Group the given primitives into a new polymesh compound object and return its ID.
None setGlobalDataString(self, str label, str value)
Set global data as string.
None cropDomainY(self, vec2 ybounds)
vec3 getObjectCenter(self, int objID)
vec3 getPatchCenter(self, int uuid)
int2 getTileObjectSubdivisionCount(self, int objID)
None hideObject(self, objids_or_objid)
Hide one or more compound objects (and all their primitives).
None setObjectDataFloat(self, objids_or_objid, str label, float value)
Set object data as 32-bit float.
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.
getPrimitiveType(self, uuid)
Get the type of a primitive or multiple primitives.
int getPrimitiveDataType(self, int uuid, str label)
Get the Helios data type of primitive data.
calculatePrimitiveDataAreaWeightedSum(self, List[int] uuids, str label, type return_type=float)
Calculate area-weighted sum of primitive data.
bool doesObjectContainPrimitive(self, int objID, int uuid)
Return True if the given primitive UUID belongs to the given object.
str getObjectDataString(self, int objID, str label)
Get string object data.
None cropDomainZ(self, vec2 zbounds)
None setMaterialDataString(self, str material_label, str data_label, str value)
Set string data on a material.
List[List[int]] getObjectPrimitiveSharedVertexIndicesMulti(self, int objID, List[int] uuids, VertexWeldMode weld_mode=VertexWeldMode.WELD_FULL)
Return shared mesh vertex indices for many primitives of a compound object at once.
None setPrimitiveDataUInt(self, uuids_or_uuid, str label, int value)
Set primitive data as unsigned 32-bit integer for one or multiple primitives.
int getTubeObjectSubdivisionCount(self, int objID)
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.
float getObjectDataFloat(self, int objID, str label)
Get float object data.
calculatePrimitiveDataMean(self, List[int] uuids, str label, type return_type=float)
Calculate arithmetic mean of primitive data across UUIDs.
int getMaterialCount(self)
Return the total number of materials registered in the context.
List[List[int]] getPolymeshObjectConnectedComponents(self, int objID)
Return the connected components of a polymesh object.
None setPrimitiveAzimuth(self, int uuid, vec3 origin, float new_azimuth)
Rotate a single primitive about the given origin so its azimuth equals new_azimuth (radians).
Location getLocation(self)
Return the Context's currently-configured geographic location.
int getMaterialDataUInt(self, str material_label, str data_label)
int getObjectDataInt(self, int objID, str label)
Get int object data.
List[vec2] getTileObjectTextureUV(self, int objID)
None clearAllObjectData(self, str label)
Remove a named data field from every compound object in 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.
str getMaterialDataString(self, str material_label, str data_label)
None renameObjectData(self, int objID, str old_label, str new_label)
Rename an object data label.
None overridePrimitiveTextureColor(self, uuids_or_uuid)
Override texture color with the primitive's constant RGB color.
None translateObject(self, Union[int, List[int]] ObjID, vec3 shift)
Translate one or more compound objects by a shift vector.
int getObjectSharedVertexCount(self, int objID, VertexWeldMode weld_mode=VertexWeldMode.WELD_FULL)
Return the number of distinct shared vertices in a compound object's mesh.
np.ndarray getObjectDataArray(self, List[int] objids, str label)
Get object data values for multiple objects as a NumPy array.
float getPrimitiveDataFloat(self, int uuid, str label)
Convenience method to get float primitive data.
int2 getMaterialDataInt2(self, str material_label, str data_label)
int getGlobalDataVersion(self, str label)
Return the version counter for a global data entry.
None setGlobalDataVec2(self, str label, x_or_vec, float y=None)
Set global data as vec2.
deleteMaterial(self, str material_label)
Delete a material from the context.
List[PrimitiveInfo] _batchPrimitiveInfo(self, List[int] uuids)
Build PrimitiveInfo for many primitives with a fixed number of native calls.
None computePolymeshObjectVertexNormals(self, int objID, float crease_angle_degrees=30.0)
Compute per-vertex normals for a polymesh object by area-weighted averaging.
dict get_plugin_capabilities(self)
Get detailed information about available plugin capabilities.
'np.ndarray' getObjectTransformationMatrix(self, int objID)
Return the object's 4x4 transformation matrix as a (4,4) float32 ndarray.
_check_primitive_data_exists(self, List[int] uuids, str label)
Raise if any of uuids lacks primitive data label.
vec3 getVoxelSize(self, int uuid)
VertexNormalSource getPolymeshObjectVertexNormalSource(self, int objID)
Return where a polymesh object's vertex normals came from.
List[vec3] getAdaptiveTileObjectVertices(self, int objID)
Get the Cartesian coordinates of each of the four corners of an adaptive tile object.
getTime(self)
Get the current simulation time.
int getPolymeshObjectFaceIndexForPrimitive(self, int objID, int uuid)
Return the index into :meth:getPolymeshObjectFaces of the face made up by a member primitive.
getPrimitiveNormal(self, uuid)
Get the normal vector of a primitive or multiple primitives.
int getObjectPrimitiveCount(self, int objID)
Return the number of primitives currently belonging to the object.
int addPatchTextured(self, vec3 center, vec2 size, str texture_file, Optional[SphericalCoord] rotation=None, Optional[vec2] uv_center=None, Optional[vec2] uv_size=None)
Add a textured patch primitive to the context.
None scalePrimitive(self, Union[int, List[int]] UUID, vec3 scale, Optional[vec3] point=None)
Scale one or more primitives.
List[int] filterObjectsByData(self, List[int] objIDs, str label, value, str comparator="=")
Filter objects by data value.
List[int] cleanDeletedObjectIDs(self, List[int] objIDs)
Return a new list with deleted object IDs removed; input is not mutated.
List[str] generateTexturesFromColormap(self, str texture_file, List[RGBcolor] colormap)
Generate one texture file per color in colormap derived from texture_file.
List[int] getPrimitivesUsingMaterial(self, str material_label)
Get all primitive UUIDs that use a specific material.
float getPolymeshObjectVolume(self, int objID)
Return the enclosed volume of a polymesh object.
List[int] getPrimitiveSharedVertexIndices(self, int uuid, VertexWeldMode weld_mode=VertexWeldMode.WELD_FULL)
Return a primitive's shared mesh vertex indices without naming its parent object.
setDateJulian(self, int julian_day, int year)
Set the simulation date using Julian day number.
List[float] getTubeObjectNodeRadii(self, int objID)
None clearObjectData(self, objids_or_objid, str label)
Clear object data.
int getTriangleCount(self, bool include_hidden=True)
float getConeObjectNodeRadius(self, int objID, int number)
None pruneTubeNodes(self, int objID, int node_index)
Remove all tube nodes from index node_index to the end.
deleteTimeseriesDataPoint(self, 'Date' date, 'Time' time, Optional[str] label=None)
Delete a single timeseries data point at the given date and time.
int getObjectDataType(self, int objID, str label)
Get the HeliosDataType enum for object data.
None setObjectDataInt4(self, objids_or_objid, str label, x_or_vec, int y=None, int z=None, int w=None)
Set object data as int4.
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.
None useObjectTextureColor(self, objIDs_or_objID)
Restore use of the texture color (undoes overrideObjectTextureColor).
None setPrimitiveTransformationMatrix(self, uuids_or_uuid, T)
Set the 4x4 transformation matrix on one or more primitives.
'np.ndarray' getAllPrimitiveSolidFractions(self)
Get solid fractions for all primitives.
List getUniqueObjectDataValues(self, str label, type dtype)
Return the unique values stored under label across all compound objects.
getPrimitiveTextureFile(self, uuid)
Get the texture file path of a primitive or multiple primitives.
None setGlobalDataInt2(self, str label, x_or_vec, int y=None)
Set global data as int2.
AdaptiveTileRefinement getAdaptiveTileObjectRefinement(self, int objID)
Get the refinement parameters that were requested when the object was created.
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.
str getObjectTextureFile(self, int objID)
Return the filesystem path of the texture assigned to the object, or an empty string if no texture is...
None usePrimitiveTextureColor(self, uuids_or_uuid)
Use texture-map color instead of the constant RGB color.
float getBoxObjectVolume(self, int objID)
bool doesPrimitiveExist(self, uuid)
Check if a primitive exists for a given UUID or list of UUIDs.
float getGlobalDataFloat(self, str label)
Get float global data.
bool areObjectPrimitivesComplete(self, int objID)
Return True if all primitives originally belonging to this object still exist (i.e....
None setGlobalDataVec4(self, str label, x_or_vec, float y=None, float z=None, float w=None)
Set global data as vec4.
List[int] loadXML(self, str filename, bool quiet=False)
Load geometry from a Helios XML file.
float randn(self, mean=None, stddev=None)
Draw a normal random number using the Context's RNG.
None setGlobalDataInt(self, str label, int value)
Set global data as signed 32-bit integer.
setMaterialColor(self, str material_label, color)
Set the RGBA color of a material.
None setPrimitiveTextureFile(self, int uuid, str texture_file)
Set the texture file path of a primitive.
None aggregatePrimitiveDataProduct(self, List[int] uuids, List[str] labels, str result_label)
Multiply multiple primitive data fields into a new field.
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.
None setObjectDataDouble(self, objids_or_objid, str label, float value)
Set object data as 64-bit double.
List[int] getObjectPrimitiveSharedVertexIndices(self, int objID, int uuid, VertexWeldMode weld_mode=VertexWeldMode.WELD_FULL)
Return the shared mesh vertex each vertex of a primitive belongs to.
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.
int getDiskObjectSubdivisionCount(self, int objID)
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.
getPrimitiveSolidFraction(self, uuid)
Get the solid fraction of a primitive or multiple primitives.
packGPUBuffers(self, uuids)
Pack GPU-ready geometry buffers for a set of primitives in a single C++ pass.
None setMaterialDataUInt(self, str material_label, str data_label, int value)
Set unsigned int data on a material.
None setObjectTransformationMatrix(self, objIDs_or_objID, T)
Set the 4x4 transformation matrix on one or more compound objects.
float getObjectArea(self, int objID)
Return the total surface area (one-sided) of all primitives in the object.
assignMaterialToObject(self, objID, str material_label)
Assign a material to all primitives in compound object(s).
int getGlobalDataInt(self, str label)
Get int global data.
None setObjectDataFromPrimitiveDataMean(self, int objID, str label)
Compute the mean of the given primitive-data label across the object's primitives and store it as obj...
'Date' queryTimeseriesDate(self, str label, int index)
Get the Date associated with a timeseries data point.
'np.ndarray' getAllPrimitiveAreas(self)
Get areas for all primitives.
List[float] _marshal_mat4(value)
Coerce a 4x4 transformation matrix input into a flat list of 16 floats.
None setLocation(self, location_or_lat, longitude=None, utc_offset=None, altitude=0.0)
Set the geographic location used by solar/radiation calculations.
'np.ndarray' getAllPrimitiveNormals(self)
Get normals for all primitives.
bool isObjectHidden(self, int objID)
Check if a compound object is hidden.
resolveMaterialTextures(self, uuids, colors_np)
Resolve material texture suppression for export.
None renameGlobalData(self, str old_label, str new_label)
Rename a global data label.
None setPolymeshObjectVertices(self, int objID, List[vec3] vertices)
Move every shared vertex of a polymesh object, deforming the mesh.
bool isObjectDataValueCachingEnabled(self, str label)
Return True if value caching is enabled for the given object-data label.
List[str] listTimeseriesVariables(self)
List all existing timeseries variables.
List[str] listPrimitiveData(self, int uuid)
List all data labels attached to a primitive.
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.
List[RGBcolor] generateColormap(self, str name, int n_colors)
Generate a colormap with n_colors entries from a named colormap.
bool doesGlobalDataExist(self, str label)
Check if global data exists.
int getMaterialDataInt(self, str material_label, str data_label)
vec3 getTileObjectCenter(self, int objID)
setMaterialTexture(self, str material_label, str texture_file)
Set the texture file for a material.
int getSphereObjectSubdivisionCount(self, int objID)
Physical properties and geometry information for a primitive.
__post_init__(self)
Calculate centroid from vertices if not provided.
Parameters controlling the adaptive sub-patch refinement of an adaptive tile object.
Helios Date structure for representing date values.
Geographic location for solar position and radiation calculations.
Helios primitive type enumeration.
Helios Time structure for representing time values.
Provenance of a polymesh object's per-vertex normals (helios-core 1.3.83).
None check_context_alive('Context' context, str owner_name)
Raise if context's native Context has already been destroyed.