3from dataclasses
import dataclass
4from typing
import List, Optional, Tuple, 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 Get a counter that advances whenever any primitive is modified or deleted.
365 Any change to a primitive advances it, including setting its primitive data or
366 color. If two calls return the same value, no primitive was modified or deleted in
367 between, so code that tracks geometry changes can skip scanning for them. Unlike
368 the dirty flags, the counter is not reset by :meth:`markGeometryClean`.
370 Creating a primitive is not guaranteed to advance the counter; compare
371 :meth:`getPrimitiveCount` as well to detect new primitives.
374 Current geometry revision
377 NotImplementedError: If the native library predates helios-core v1.3.92
380 >>> rev = context.getGeometryRevision()
381 >>> context.setPrimitiveDataFloat(uuid, "temperature", 25.0)
382 >>> context.getGeometryRevision() != rev
386 return context_wrapper.getGeometryRevisionWrapper(self.
context)
390 Get a counter that advances whenever a primitive's shape changes or a primitive is deleted.
392 A shape change is any change to a primitive's vertices or transformation
393 (translation, rotation, scaling, setting its vertices or transformation matrix),
394 its texture file or (u,v) coordinates, or its solid fraction. Unlike
395 :meth:`getGeometryRevision`, setting primitive data or color does not advance this
396 counter, so a cache of geometry can tell that it is stale without being invalidated
397 by data writes. :meth:`markGeometryDirty` also advances it, and it is not reset by
398 :meth:`markGeometryClean`.
400 Creating a primitive is not guaranteed to advance the counter; compare
401 :meth:`getPrimitiveCount` as well to detect new primitives.
404 Current shape revision
407 NotImplementedError: If the native library predates helios-core v1.3.92
410 return context_wrapper.getShapeRevisionWrapper(self.
context)
414 Seed the random number generator for reproducible stochastic results.
417 seed: Integer seed value for random number generation
420 This is critical for reproducible results in stochastic simulations
421 (e.g., LiDAR scans with beam divergence, random perturbations).
424 context_wrapper.helios_lib.seedRandomGenerator(self.
context, seed)
426 @validate_patch_params
427 def addPatch(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1), rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None) -> int:
432 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
433 return context_wrapper.addPatchWithCenterSizeRotationAndColor(self.
context, center.to_list(), size.to_list(), rotation_list, color.to_list())
436 rotation: Optional[SphericalCoord] =
None,
437 uv_center: Optional[vec2] =
None,
438 uv_size: Optional[vec2] =
None) -> int:
439 """Add a textured patch primitive to the context.
441 Creates a rectangular patch with a texture image mapped to its surface.
444 center: 3D position of the patch center
445 size: Width and height of the patch
446 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
447 rotation: Optional spherical rotation (defaults to no rotation)
448 uv_center: Optional UV center of texture map (required if uv_size is provided)
449 uv_size: Optional UV size of texture map (required if uv_center is provided)
452 UUID of the created textured patch primitive
455 ValueError: If arguments have wrong types or UV params are partially specified
456 FileNotFoundError: If texture file doesn't exist
457 RuntimeError: If context is in mock mode
460 >>> context = Context()
461 >>> uuid = context.addPatchTextured(
462 ... center=vec3(0, 0, 0),
464 ... texture_file="texture.png"
469 if not isinstance(center, vec3):
470 raise ValueError(f
"center must be a vec3, got {type(center).__name__}")
471 if not isinstance(size, vec2):
472 raise ValueError(f
"size must be a vec2, got {type(size).__name__}")
473 if not isinstance(texture_file, str):
474 raise ValueError(f
"texture_file must be a str, got {type(texture_file).__name__}")
475 if rotation
is not None and not isinstance(rotation, SphericalCoord):
476 raise ValueError(f
"rotation must be a SphericalCoord, got {type(rotation).__name__}")
478 if (uv_center
is None) != (uv_size
is None):
479 raise ValueError(
"uv_center and uv_size must both be provided or both omitted")
480 if uv_center
is not None and not isinstance(uv_center, vec2):
481 raise ValueError(f
"uv_center must be a vec2, got {type(uv_center).__name__}")
482 if uv_size
is not None and not isinstance(uv_size, vec2):
483 raise ValueError(f
"uv_size must be a vec2, got {type(uv_size).__name__}")
486 [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp'])
489 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
491 if uv_center
is not None:
492 return context_wrapper.addPatchWithTextureAndUV(
493 self.
context, center.to_list(), size.to_list(), rotation_list,
494 validated_texture_file, uv_center.to_list(), uv_size.to_list()
497 return context_wrapper.addPatchWithTexture(
498 self.
context, center.to_list(), size.to_list(), rotation_list,
499 validated_texture_file
502 @validate_triangle_params
503 def addTriangle(self, vertex0: vec3, vertex1: vec3, vertex2: vec3, color: Optional[RGBcolor] =
None) -> int:
504 """Add a triangle primitive to the context
507 vertex0: First vertex of the triangle
508 vertex1: Second vertex of the triangle
509 vertex2: Third vertex of the triangle
510 color: Optional triangle color (defaults to white)
513 UUID of the created triangle primitive
517 return context_wrapper.addTriangle(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list())
519 return context_wrapper.addTriangleWithColor(self.
context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list(), color.to_list())
522 texture_file: str, uv0: vec2, uv1: vec2, uv2: vec2) -> int:
523 """Add a textured triangle primitive to the context
525 Creates a triangle with texture mapping. The texture image is mapped to the triangle
526 surface using UV coordinates, where (0,0) represents the top-left corner of the image
527 and (1,1) represents the bottom-right corner.
530 vertex0: First vertex of the triangle
531 vertex1: Second vertex of the triangle
532 vertex2: Third vertex of the triangle
533 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
534 uv0: UV texture coordinates for first vertex
535 uv1: UV texture coordinates for second vertex
536 uv2: UV texture coordinates for third vertex
539 UUID of the created textured triangle primitive
542 ValueError: If texture file path is invalid
543 FileNotFoundError: If texture file doesn't exist
544 RuntimeError: If context is in mock mode
547 >>> context = Context()
548 >>> # Create a textured triangle
549 >>> vertex0 = vec3(0, 0, 0)
550 >>> vertex1 = vec3(1, 0, 0)
551 >>> vertex2 = vec3(0.5, 1, 0)
552 >>> uv0 = vec2(0, 0) # Bottom-left of texture
553 >>> uv1 = vec2(1, 0) # Bottom-right of texture
554 >>> uv2 = vec2(0.5, 1) # Top-center of texture
555 >>> uuid = context.addTriangleTextured(vertex0, vertex1, vertex2,
556 ... "texture.png", uv0, uv1, uv2)
561 for name, val
in [(
"vertex0", vertex0), (
"vertex1", vertex1), (
"vertex2", vertex2)]:
562 if not isinstance(val, vec3):
563 raise ValueError(f
"{name} must be a vec3, got {type(val).__name__}")
564 for name, val
in [(
"uv0", uv0), (
"uv1", uv1), (
"uv2", uv2)]:
565 if not isinstance(val, vec2):
566 raise ValueError(f
"{name} must be a vec2, got {type(val).__name__}")
570 [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp'])
573 return context_wrapper.addTriangleWithTexture(
575 vertex0.to_list(), vertex1.to_list(), vertex2.to_list(),
576 validated_texture_file,
577 uv0.to_list(), uv1.to_list(), uv2.to_list()
581 """Get the type of a primitive or multiple primitives.
584 uuid: Single UUID (int) or list of UUIDs
587 PrimitiveType for single UUID, or np.ndarray of shape (N,) uint32 for list
590 if isinstance(uuid, (list, tuple)):
592 return np.empty((0,), dtype=np.uint32)
593 ptr, size = context_wrapper.getBatchPrimitiveTypes(self.
context, uuid)
594 if size == 0
or not ptr:
595 return np.empty((0,), dtype=np.uint32)
596 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
597 primitive_type = context_wrapper.getPrimitiveType(self.
context, uuid)
601 """Get the area of a primitive or multiple primitives.
604 uuid: Single UUID (int) or list of UUIDs
607 float for single UUID, or np.ndarray of shape (N,) for list
610 if isinstance(uuid, (list, tuple)):
612 return np.empty((0,), dtype=np.float32)
613 ptr, size = context_wrapper.getBatchPrimitiveAreas(self.
context, uuid)
614 if size == 0
or not ptr:
615 return np.empty((0,), dtype=np.float32)
616 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
617 return context_wrapper.getPrimitiveArea(self.
context, uuid)
620 """Get the normal vector of a primitive or multiple primitives.
623 uuid: Single UUID (int) or list of UUIDs
626 vec3 for single UUID, or np.ndarray of shape (N, 3) for list
629 if isinstance(uuid, (list, tuple)):
631 return np.empty((0, 3), dtype=np.float32)
632 ptr, size = context_wrapper.getBatchPrimitiveNormals(self.
context, uuid)
633 if size == 0
or not ptr:
634 return np.empty((0, 3), dtype=np.float32)
635 return np.ctypeslib.as_array(ptr, shape=(size,)).copy().reshape(-1, 3)
636 normal_ptr = context_wrapper.getPrimitiveNormal(self.
context, uuid)
637 return vec3(normal_ptr[0], normal_ptr[1], normal_ptr[2])
640 """Get vertices of a primitive or multiple primitives.
643 uuid: Single UUID (int) or list of UUIDs
646 List[vec3] for single UUID, or tuple of (flat_data, offsets) for list
647 where flat_data is a float32 ndarray and offsets is a uint32 ndarray
648 of length N+1. Vertices for primitive i are at
649 flat_data[offsets[i]:offsets[i+1]].
652 if isinstance(uuid, (list, tuple)):
654 return (np.empty((0,), dtype=np.float32), np.zeros((1,), dtype=np.uint32))
655 ptr, offsets, total = context_wrapper.getBatchPrimitiveVertices(self.
context, uuid)
656 offsets_arr = np.asarray(offsets, dtype=np.uint32)
657 if total == 0
or not ptr:
658 return (np.empty((0,), dtype=np.float32), offsets_arr)
659 data = np.ctypeslib.as_array(ptr, shape=(total,)).copy()
660 return (data, offsets_arr)
661 size = ctypes.c_uint()
662 vertices_ptr = context_wrapper.getPrimitiveVertices(self.
context, uuid, ctypes.byref(size))
664 vertices_list = ctypes.cast(vertices_ptr, ctypes.POINTER(ctypes.c_float * size.value)).contents
665 vertices = [
vec3(vertices_list[i], vertices_list[i+1], vertices_list[i+2])
for i
in range(0, size.value, 3)]
669 """Get the color of a primitive or multiple primitives.
672 uuid: Single UUID (int) or list of UUIDs
675 RGBcolor for single UUID, or np.ndarray of shape (N, 3) for list
678 if isinstance(uuid, (list, tuple)):
680 return np.empty((0, 3), dtype=np.float32)
681 ptr, size = context_wrapper.getBatchPrimitiveColors(self.
context, uuid)
682 if size == 0
or not ptr:
683 return np.empty((0, 3), dtype=np.float32)
684 return np.ctypeslib.as_array(ptr, shape=(size,)).copy().reshape(-1, 3)
685 color_ptr = context_wrapper.getPrimitiveColor(self.
context, uuid)
686 return RGBcolor(color_ptr[0], color_ptr[1], color_ptr[2])
690 return context_wrapper.getPrimitiveCount(self.
context)
693 """Check if a primitive exists for a given UUID or list of UUIDs.
696 uuid: A single UUID (int) or a list of UUIDs.
699 True if the primitive(s) exist, False otherwise.
700 For a list, returns True only if ALL primitives exist.
703 if isinstance(uuid, (list, tuple)):
704 arr = (ctypes.c_uint * len(uuid))(*uuid)
705 return context_wrapper.doesPrimitiveExistBatch(self.
context, arr, len(uuid))
706 return context_wrapper.doesPrimitiveExist(self.
context, uuid)
710 size = ctypes.c_uint()
711 uuids_ptr = context_wrapper.getAllUUIDs(self.
context, ctypes.byref(size))
712 return list(uuids_ptr[:size.value])
716 return context_wrapper.getObjectCount(self.
context)
720 size = ctypes.c_uint()
721 objectids_ptr = context_wrapper.getAllObjectIDs(self.
context, ctypes.byref(size))
722 return list(objectids_ptr[:size.value])
726 Get physical properties and geometry information for a single primitive.
729 uuid: UUID of the primitive
732 PrimitiveInfo object containing physical properties and geometry
747 solid_fraction =
None
752 except NotImplementedError:
758 except NotImplementedError:
762 except NotImplementedError:
763 solid_fraction =
None
767 primitive_type=primitive_type,
772 texture_file=texture_file,
773 texture_uv=texture_uv,
774 solid_fraction=solid_fraction,
778 """Build PrimitiveInfo for many primitives with a fixed number of native calls.
780 Field-for-field identical to calling :meth:`getPrimitiveInfo` on each UUID,
781 but each of the eight fields has a list-accepting getter backed by a native
782 ``getBatch*`` call. Doing it per primitive costs eight native round-trips
783 each — 77.5 ms for 5,000 primitives against 4.6 ms batched.
786 uuids: Primitives to describe, in the order to return them
789 List of PrimitiveInfo, one per UUID, in the order given
805 except NotImplementedError:
811 if uv_offsets
is None or len(uv_offsets) < len(uuids) + 1:
812 uv_data, uv_offsets =
None,
None
813 except NotImplementedError:
814 uv_data, uv_offsets =
None,
None
817 except NotImplementedError:
818 solid_fractions =
None
821 for i, uuid
in enumerate(uuids):
822 segment = vertex_data[vertex_offsets[i]:vertex_offsets[i + 1]]
823 vertices = [
vec3(float(segment[j]), float(segment[j + 1]), float(segment[j + 2]))
824 for j
in range(0, len(segment), 3)]
827 if uv_data
is not None:
828 uv_segment = uv_data[uv_offsets[i]:uv_offsets[i + 1]]
830 texture_uv = [
vec2(float(uv_segment[j]), float(uv_segment[j + 1]))
831 for j
in range(0, len(uv_segment), 2)]
834 if texture_files
is not None and texture_files[i]:
835 texture_file = texture_files[i]
840 area=float(areas[i]),
841 normal=
vec3(float(normals[i][0]), float(normals[i][1]), float(normals[i][2])),
843 color=
RGBcolor(float(colors[i][0]), float(colors[i][1]), float(colors[i][2])),
844 texture_file=texture_file,
845 texture_uv=texture_uv,
846 solid_fraction=(
None if solid_fractions
is None
847 else float(solid_fractions[i])),
853 Get physical properties and geometry information for all primitives in the context.
856 List of PrimitiveInfo objects for all primitives
862 Get physical properties and geometry information for all primitives belonging to a specific object.
865 object_id: ID of the object
868 List of PrimitiveInfo objects for primitives in the object
870 object_uuids = context_wrapper.getObjectPrimitiveUUIDs(self.
context, object_id)
874 def addTile(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
875 rotation: Optional[SphericalCoord] =
None, subdiv: int2 =
int2(1, 1),
876 color: Optional[RGBcolor] =
None) -> List[int]:
878 Add a subdivided patch (tile) to the context.
880 A tile is a patch subdivided into a regular grid of smaller patches,
881 useful for creating detailed surfaces or terrain.
884 center: 3D coordinates of tile center (default: origin)
885 size: Width and height of the tile (default: 1x1)
886 rotation: Orientation of the tile (default: no rotation)
887 subdiv: Number of subdivisions in x and y directions (default: 1x1)
888 color: Color of the tile (default: white)
891 List of UUIDs for all patches created in the tile
894 >>> context = Context()
895 >>> # Create a 2x2 meter tile subdivided into 4x4 patches
896 >>> tile_uuids = context.addTile(
897 ... center=vec3(0, 0, 1),
899 ... subdiv=int2(4, 4),
900 ... color=RGBcolor(0.5, 0.8, 0.2)
902 >>> print(f"Created {len(tile_uuids)} patches")
907 if not isinstance(center, vec3):
908 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
909 if not isinstance(size, vec2):
910 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
911 if rotation
is not None and not isinstance(rotation, SphericalCoord):
912 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
913 if not isinstance(subdiv, int2):
914 raise ValueError(f
"Subdiv must be an int2, got {type(subdiv).__name__}")
915 if color
is not None and not isinstance(color, RGBcolor):
916 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
919 if any(s <= 0
for s
in size.to_list()):
920 raise ValueError(
"All size dimensions must be positive")
921 if any(s <= 0
for s
in subdiv.to_list()):
922 raise ValueError(
"All subdivision counts must be positive")
928 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
930 if color
and not (color.r == 1.0
and color.g == 1.0
and color.b == 1.0):
931 return context_wrapper.addTileWithColor(
932 self.
context, center.to_list(), size.to_list(),
933 rotation_list, subdiv.to_list(), color.to_list()
936 return context_wrapper.addTile(
937 self.
context, center.to_list(), size.to_list(),
938 rotation_list, subdiv.to_list()
941 @validate_sphere_params
942 def addSphere(self, center: vec3 =
vec3(0, 0, 0), radius: float = 1.0,
943 ndivs: int = 10, color: Optional[RGBcolor] =
None) -> List[int]:
945 Add a sphere to the context.
947 The sphere is tessellated into triangular faces based on the specified
951 center: 3D coordinates of sphere center (default: origin)
952 radius: Radius of the sphere (default: 1.0)
953 ndivs: Number of divisions for tessellation (default: 10)
954 Higher values create smoother spheres but more triangles
955 color: Color of the sphere (default: white)
958 List of UUIDs for all triangles created in the sphere
961 >>> context = Context()
962 >>> # Create a red sphere at (1, 2, 3) with radius 0.5
963 >>> sphere_uuids = context.addSphere(
964 ... center=vec3(1, 2, 3),
967 ... color=RGBcolor(1, 0, 0)
969 >>> print(f"Created sphere with {len(sphere_uuids)} triangles")
974 if not isinstance(center, vec3):
975 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
976 if not isinstance(radius, (int, float)):
977 raise ValueError(f
"Radius must be a number, got {type(radius).__name__}")
978 if not isinstance(ndivs, int):
979 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
980 if color
is not None and not isinstance(color, RGBcolor):
981 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
985 raise ValueError(
"Sphere radius must be positive")
987 raise ValueError(
"Number of divisions must be at least 3")
990 return context_wrapper.addSphereWithColor(
991 self.
context, ndivs, center.to_list(), radius, color.to_list()
994 return context_wrapper.addSphere(
995 self.
context, ndivs, center.to_list(), radius
998 @validate_tube_params
999 def addTube(self, nodes: List[vec3], radii: Union[float, List[float]],
1000 ndivs: int = 6, colors: Optional[Union[RGBcolor, List[RGBcolor]]] =
None) -> List[int]:
1002 Add a tube (pipe/cylinder) to the context.
1004 The tube is defined by a series of nodes (path) with radius at each node.
1005 It's tessellated into triangular faces based on the number of radial divisions.
1008 nodes: List of 3D points defining the tube path (at least 2 nodes)
1009 radii: Radius at each node. Can be:
1010 - Single float: constant radius for all nodes
1011 - List of floats: radius for each node (must match nodes length)
1012 ndivs: Number of radial divisions (default: 6)
1013 Higher values create smoother tubes but more triangles
1014 colors: Colors at each node. Can be:
1016 - Single RGBcolor: constant color for all nodes
1017 - List of RGBcolor: color for each node (must match nodes length)
1020 List of UUIDs for all triangles created in the tube
1023 >>> context = Context()
1024 >>> # Create a curved tube with varying radius
1025 >>> nodes = [vec3(0, 0, 0), vec3(1, 0, 0), vec3(2, 1, 0)]
1026 >>> radii = [0.1, 0.2, 0.1]
1027 >>> colors = [RGBcolor(1, 0, 0), RGBcolor(0, 1, 0), RGBcolor(0, 0, 1)]
1028 >>> tube_uuids = context.addTube(nodes, radii, ndivs=8, colors=colors)
1029 >>> print(f"Created tube with {len(tube_uuids)} triangles")
1034 if not isinstance(nodes, (list, tuple)):
1035 raise ValueError(f
"Nodes must be a list or tuple, got {type(nodes).__name__}")
1036 if not isinstance(ndivs, int):
1037 raise ValueError(f
"Ndivs must be an integer, got {type(ndivs).__name__}")
1038 if colors
is not None and not isinstance(colors, (RGBcolor, list, tuple)):
1039 raise ValueError(f
"Colors must be RGBcolor, list, tuple, or None, got {type(colors).__name__}")
1043 raise ValueError(
"Tube requires at least 2 nodes")
1045 raise ValueError(
"Number of radial divisions must be at least 3")
1048 if isinstance(radii, (int, float)):
1049 radii_list = [float(radii)] * len(nodes)
1051 radii_list = [float(r)
for r
in radii]
1052 if len(radii_list) != len(nodes):
1053 raise ValueError(f
"Number of radii ({len(radii_list)}) must match number of nodes ({len(nodes)})")
1056 if any(r <= 0
for r
in radii_list):
1057 raise ValueError(
"All radii must be positive")
1062 nodes_flat.extend(node.to_list())
1066 return context_wrapper.addTube(self.
context, ndivs, nodes_flat, radii_list)
1067 elif isinstance(colors, RGBcolor):
1069 colors_flat = colors.to_list() * len(nodes)
1072 if len(colors) != len(nodes):
1073 raise ValueError(f
"Number of colors ({len(colors)}) must match number of nodes ({len(nodes)})")
1075 for color
in colors:
1076 colors_flat.extend(color.to_list())
1078 return context_wrapper.addTubeWithColor(self.
context, ndivs, nodes_flat, radii_list, colors_flat)
1080 @validate_box_params
1081 def addBox(self, center: vec3 =
vec3(0, 0, 0), size: vec3 =
vec3(1, 1, 1),
1082 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None) -> List[int]:
1084 Add a rectangular box to the context.
1086 The box is subdivided into patches on each face based on the specified
1090 center: 3D coordinates of box center (default: origin)
1091 size: Width, height, and depth of the box (default: 1x1x1)
1092 subdiv: Number of subdivisions in x, y, and z directions (default: 1x1x1)
1093 Higher values create more detailed surfaces
1094 color: Color of the box (default: white)
1097 List of UUIDs for all patches created on the box faces
1100 >>> context = Context()
1101 >>> # Create a blue box subdivided for detail
1102 >>> box_uuids = context.addBox(
1103 ... center=vec3(0, 0, 2),
1104 ... size=vec3(2, 1, 0.5),
1105 ... subdiv=int3(4, 2, 1),
1106 ... color=RGBcolor(0, 0, 1)
1108 >>> print(f"Created box with {len(box_uuids)} patches")
1113 if not isinstance(center, vec3):
1114 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1115 if not isinstance(size, vec3):
1116 raise ValueError(f
"Size must be a vec3, got {type(size).__name__}")
1117 if not isinstance(subdiv, int3):
1118 raise ValueError(f
"Subdiv must be an int3, got {type(subdiv).__name__}")
1119 if color
is not None and not isinstance(color, RGBcolor):
1120 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1123 if any(s <= 0
for s
in size.to_list()):
1124 raise ValueError(
"All box dimensions must be positive")
1125 if any(s < 1
for s
in subdiv.to_list()):
1126 raise ValueError(
"All subdivision counts must be at least 1")
1129 return context_wrapper.addBoxWithColor(
1130 self.
context, center.to_list(), size.to_list(),
1131 subdiv.to_list(), color.to_list()
1134 return context_wrapper.addBox(
1135 self.
context, center.to_list(), size.to_list(), subdiv.to_list()
1138 def addDisk(self, center: vec3 =
vec3(0, 0, 0), size: vec2 =
vec2(1, 1),
1139 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
1140 color: Optional[Union[RGBcolor, RGBAcolor]] =
None) -> List[int]:
1142 Add a disk (circular or elliptical surface) to the context.
1144 A disk is a flat circular or elliptical surface tessellated into
1145 triangular faces. Supports both uniform radial subdivisions and
1146 separate radial/azimuthal subdivisions for finer control.
1149 center: 3D coordinates of disk center (default: origin)
1150 size: Semi-major and semi-minor radii of the disk (default: 1x1 circle)
1151 ndivs: Number of radial divisions (int) or [radial, azimuthal] divisions (int2)
1152 (default: 20). Higher values create smoother circles but more triangles.
1153 rotation: Orientation of the disk (default: horizontal, normal = +z)
1154 color: Color of the disk (default: white). Can be RGBcolor or RGBAcolor for transparency.
1157 List of UUIDs for all triangles created in the disk
1160 >>> context = Context()
1161 >>> # Create a red disk at (0, 0, 1) with radius 0.5
1162 >>> disk_uuids = context.addDisk(
1163 ... center=vec3(0, 0, 1),
1164 ... size=vec2(0.5, 0.5),
1166 ... color=RGBcolor(1, 0, 0)
1168 >>> print(f"Created disk with {len(disk_uuids)} triangles")
1170 >>> # Create a semi-transparent blue elliptical disk
1171 >>> disk_uuids = context.addDisk(
1172 ... center=vec3(0, 0, 2),
1173 ... size=vec2(1.0, 0.5),
1175 ... rotation=SphericalCoord(1, 0.5, 0),
1176 ... color=RGBAcolor(0, 0, 1, 0.5)
1179 >>> # Create disk with polar/radial subdivisions for finer control
1180 >>> disk_uuids = context.addDisk(
1181 ... center=vec3(0, 0, 3),
1182 ... size=vec2(1, 1),
1183 ... ndivs=int2(10, 20), # 10 radial, 20 azimuthal divisions
1184 ... color=RGBcolor(0, 1, 0)
1190 if not isinstance(center, vec3):
1191 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1192 if not isinstance(size, vec2):
1193 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1194 if not isinstance(ndivs, (int, int2)):
1195 raise ValueError(f
"Ndivs must be an int or int2, got {type(ndivs).__name__}")
1196 if rotation
is not None and not isinstance(rotation, SphericalCoord):
1197 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
1198 if color
is not None and not isinstance(color, (RGBcolor, RGBAcolor)):
1199 raise ValueError(f
"Color must be an RGBcolor, RGBAcolor, or None, got {type(color).__name__}")
1202 if any(s <= 0
for s
in size.to_list()):
1203 raise ValueError(
"Disk size must be positive")
1206 if isinstance(ndivs, int):
1208 raise ValueError(
"Number of divisions must be at least 3")
1210 if any(n < 1
for n
in ndivs.to_list()):
1211 raise ValueError(
"Radial and angular divisions must be at least 1")
1214 if rotation
is None:
1219 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1222 if isinstance(ndivs, int2):
1225 if isinstance(color, RGBAcolor):
1226 return context_wrapper.addDiskPolarSubdivisionsRGBA(
1227 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
1228 rotation_list, color.to_list()
1232 return context_wrapper.addDiskPolarSubdivisions(
1233 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
1234 rotation_list, color.to_list()
1238 color_list = [1.0, 1.0, 1.0]
1239 return context_wrapper.addDiskPolarSubdivisions(
1240 self.
context, ndivs.to_list(), center.to_list(), size.to_list(),
1241 rotation_list, color_list
1246 if isinstance(color, RGBAcolor):
1248 return context_wrapper.addDiskWithRGBAColor(
1249 self.
context, ndivs, center.to_list(), size.to_list(),
1250 rotation_list, color.to_list()
1254 return context_wrapper.addDiskWithColor(
1255 self.
context, ndivs, center.to_list(), size.to_list(),
1256 rotation_list, color.to_list()
1260 return context_wrapper.addDiskWithRotation(
1261 self.
context, ndivs, center.to_list(), size.to_list(),
1265 def addCone(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1266 ndivs: int = 20, color: Optional[RGBcolor] =
None) -> List[int]:
1268 Add a cone (or cylinder/frustum) to the context.
1270 A cone is a 3D shape connecting two circular cross-sections with
1271 potentially different radii. When radii are equal, creates a cylinder.
1272 When one radius is zero, creates a true cone.
1275 node0: 3D coordinates of the base center
1276 node1: 3D coordinates of the apex center
1277 radius0: Radius at base (node0). Use 0 for pointed end.
1278 radius1: Radius at apex (node1). Use 0 for pointed end.
1279 ndivs: Number of radial divisions for tessellation (default: 20)
1280 color: Color of the cone (default: white)
1283 List of UUIDs for all triangles created in the cone
1286 >>> context = Context()
1287 >>> # Create a cylinder (equal radii)
1288 >>> cylinder_uuids = context.addCone(
1289 ... node0=vec3(0, 0, 0),
1290 ... node1=vec3(0, 0, 2),
1296 >>> # Create a true cone (one radius = 0)
1297 >>> cone_uuids = context.addCone(
1298 ... node0=vec3(1, 0, 0),
1299 ... node1=vec3(1, 0, 1.5),
1303 ... color=RGBcolor(1, 0, 0)
1306 >>> # Create a frustum (different radii)
1307 >>> frustum_uuids = context.addCone(
1308 ... node0=vec3(2, 0, 0),
1309 ... node1=vec3(2, 0, 1),
1318 if not isinstance(node0, vec3):
1319 raise ValueError(f
"node0 must be a vec3, got {type(node0).__name__}")
1320 if not isinstance(node1, vec3):
1321 raise ValueError(f
"node1 must be a vec3, got {type(node1).__name__}")
1322 if not isinstance(ndivs, int):
1323 raise ValueError(f
"ndivs must be an int, got {type(ndivs).__name__}")
1324 if color
is not None and not isinstance(color, RGBcolor):
1325 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1328 if radius0 < 0
or radius1 < 0:
1329 raise ValueError(
"Radii must be non-negative")
1331 raise ValueError(
"Number of radial divisions must be at least 3")
1335 return context_wrapper.addConeWithColor(
1336 self.
context, ndivs, node0.to_list(), node1.to_list(),
1337 radius0, radius1, color.to_list()
1340 return context_wrapper.addCone(
1341 self.
context, ndivs, node0.to_list(), node1.to_list(),
1346 radius: Union[float, vec3] = 1.0, ndivs: int = 20,
1347 color: Optional[RGBcolor] =
None,
1348 texturefile: Optional[str] =
None) -> int:
1350 Add a spherical or ellipsoidal compound object to the context.
1352 Creates a sphere or ellipsoid as a compound object with a trackable object ID.
1353 Primitives within the object are registered as children of the object.
1356 center: Center position of sphere/ellipsoid (default: origin)
1357 radius: Radius as float (sphere) or vec3 (ellipsoid) (default: 1.0)
1358 ndivs: Number of tessellation divisions (default: 20)
1359 color: Optional RGB color
1360 texturefile: Optional texture image file path
1363 Object ID of the created compound object
1366 ValueError: If parameters are invalid
1367 NotImplementedError: If object-returning functions unavailable
1370 >>> # Create a basic sphere at origin
1371 >>> obj_id = ctx.addSphereObject()
1373 >>> # Create a colored sphere
1374 >>> obj_id = ctx.addSphereObject(
1375 ... center=vec3(0, 0, 5),
1377 ... color=RGBcolor(1, 0, 0)
1380 >>> # Create an ellipsoid (stretched sphere)
1381 >>> obj_id = ctx.addSphereObject(
1382 ... center=vec3(10, 0, 0),
1383 ... radius=vec3(2, 1, 1), # Elongated in x-direction
1390 if not isinstance(center, vec3):
1391 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1392 if not isinstance(radius, (int, float, vec3)):
1393 raise ValueError(f
"Radius must be a number or vec3, got {type(radius).__name__}")
1394 if color
is not None and not isinstance(color, RGBcolor):
1395 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1399 raise ValueError(
"Number of divisions must be at least 3")
1402 is_ellipsoid = isinstance(radius, vec3)
1408 return context_wrapper.addSphereObject_ellipsoid_texture(
1409 self.
context, ndivs, center.to_list(), radius.to_list(), texturefile
1412 return context_wrapper.addSphereObject_ellipsoid_color(
1413 self.
context, ndivs, center.to_list(), radius.to_list(), color.to_list()
1416 return context_wrapper.addSphereObject_ellipsoid(
1417 self.
context, ndivs, center.to_list(), radius.to_list()
1422 return context_wrapper.addSphereObject_texture(
1423 self.
context, ndivs, center.to_list(), radius, texturefile
1426 return context_wrapper.addSphereObject_color(
1427 self.
context, ndivs, center.to_list(), radius, color.to_list()
1430 return context_wrapper.addSphereObject_basic(
1431 self.
context, ndivs, center.to_list(), radius
1436 subdiv: int2 =
int2(1, 1),
1437 color: Optional[RGBcolor] =
None,
1438 texturefile: Optional[str] =
None,
1439 texture_repeat: Optional[int2] =
None) -> int:
1441 Add a tiled patch (subdivided patch) as a compound object to the context.
1443 Creates a rectangular patch subdivided into a grid of smaller patches,
1444 registered as a compound object with a trackable object ID.
1447 center: Center position of tile (default: origin)
1448 size: Size in x and y directions (default: 1x1)
1449 rotation: Spherical rotation (default: no rotation)
1450 subdiv: Number of subdivisions in x and y (default: 1x1)
1451 color: Optional RGB color
1452 texturefile: Optional texture image file path
1453 texture_repeat: Optional texture repetitions in x and y
1456 Object ID of the created compound object
1459 ValueError: If parameters are invalid
1460 NotImplementedError: If object-returning functions unavailable
1463 >>> # Create a basic 2x2 tile
1464 >>> obj_id = ctx.addTileObject(
1465 ... center=vec3(0, 0, 0),
1466 ... size=vec2(10, 10),
1467 ... subdiv=int2(2, 2)
1470 >>> # Create a colored tile with rotation
1471 >>> obj_id = ctx.addTileObject(
1472 ... center=vec3(5, 0, 0),
1473 ... size=vec2(10, 5),
1474 ... rotation=SphericalCoord(1, 0, 45),
1475 ... subdiv=int2(4, 2),
1476 ... color=RGBcolor(0, 1, 0)
1482 if not isinstance(center, vec3):
1483 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1484 if not isinstance(size, vec2):
1485 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1486 if not isinstance(rotation, SphericalCoord):
1487 raise ValueError(f
"Rotation must be a SphericalCoord, got {type(rotation).__name__}")
1488 if not isinstance(subdiv, int2):
1489 raise ValueError(f
"Subdiv must be an int2, got {type(subdiv).__name__}")
1490 if color
is not None and not isinstance(color, RGBcolor):
1491 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1492 if texture_repeat
is not None and not isinstance(texture_repeat, int2):
1493 raise ValueError(f
"texture_repeat must be an int2 or None, got {type(texture_repeat).__name__}")
1496 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1499 if texture_repeat
is not None:
1500 if texturefile
is None:
1501 raise ValueError(
"texture_repeat requires texturefile")
1502 return context_wrapper.addTileObject_texture_repeat(
1503 self.
context, center.to_list(), size.to_list(), rotation_list,
1504 subdiv.to_list(), texturefile, texture_repeat.to_list()
1507 return context_wrapper.addTileObject_texture(
1508 self.
context, center.to_list(), size.to_list(), rotation_list,
1509 subdiv.to_list(), texturefile
1512 return context_wrapper.addTileObject_color(
1513 self.
context, center.to_list(), size.to_list(), rotation_list,
1514 subdiv.to_list(), color.to_list()
1517 return context_wrapper.addTileObject_basic(
1518 self.
context, center.to_list(), size.to_list(), rotation_list,
1524 refinement: Optional[AdaptiveTileRefinement] =
None,
1525 color: Optional[RGBcolor] =
None,
1526 texturefile: Optional[str] =
None,
1527 texture_repeat: Optional[int2] =
None) -> int:
1529 Add a patch subdivided into sub-patches whose size adapts with distance from a target point.
1531 Sub-patches are generated by recursive quadtree subdivision, so they are fine near
1532 ``refinement.target`` and progressively coarser away from it. This is intended for ground
1533 planes, where fine resolution is needed to resolve shadows cast near an object of interest
1534 but the remainder of the domain only needs to occlude the horizon. The sub-patches exactly
1535 partition the tile with no gaps and no overlaps, and the achieved sizes are typically
1536 within about 20% of those requested.
1539 center: Center position of tile (default: origin)
1540 size: Size in x and y directions (default: 1x1)
1541 rotation: Spherical rotation (default: no rotation)
1542 refinement: Parameters controlling the adaptive sub-patch resolution
1543 (default: ``AdaptiveTileRefinement()``)
1544 color: Optional RGB color
1545 texturefile: Optional texture image file path
1546 texture_repeat: Optional texture repetitions in x and y. Unlike
1547 :meth:`addTileObject`, the count is applied exactly -- the base grid is snapped to
1548 a multiple of it rather than the count being reduced.
1551 Object ID of the created compound object
1554 ValueError: If parameters are invalid
1555 RuntimeError: If the refinement is rejected by helios, e.g. because
1556 ``subpatch_size_max`` exceeds half the smaller tile dimension, or because the
1557 requested refinement would generate more than 2,000,000 sub-patches
1558 NotImplementedError: If object-returning functions unavailable
1561 Sub-patches are ordered by quadtree traversal, not row-major, so they cannot be
1562 indexed by grid position. Adaptive tiles have no subdivision count, so
1563 :meth:`getTileObjectSubdivisionCount` and :meth:`setTileObjectSubdivisionCount` do
1567 >>> # Ground plane resolved finely beneath a plant at the origin
1568 >>> refinement = AdaptiveTileRefinement(
1569 ... target=vec2(0, 0), subpatch_size_min=0.02, subpatch_size_max=2.0
1571 >>> obj_id = ctx.addAdaptiveTileObject(
1572 ... center=vec3(0, 0, 0), size=vec2(50, 50), refinement=refinement
1575 >>> # Check the cost before committing to it
1576 >>> ctx.predictAdaptiveTileObjectSubpatchCount(vec2(50, 50), refinement) # doctest: +SKIP
1581 if refinement
is None:
1585 if not isinstance(center, vec3):
1586 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1587 if not isinstance(size, vec2):
1588 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1589 if not isinstance(rotation, SphericalCoord):
1590 raise ValueError(f
"Rotation must be a SphericalCoord, got {type(rotation).__name__}")
1591 if not isinstance(refinement, AdaptiveTileRefinement):
1592 raise ValueError(f
"Refinement must be an AdaptiveTileRefinement or None, got {type(refinement).__name__}")
1593 if color
is not None and not isinstance(color, RGBcolor):
1594 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1595 if texture_repeat
is not None and not isinstance(texture_repeat, int2):
1596 raise ValueError(f
"texture_repeat must be an int2 or None, got {type(texture_repeat).__name__}")
1599 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1602 if texture_repeat
is not None:
1603 if texturefile
is None:
1604 raise ValueError(
"texture_repeat requires texturefile")
1605 return context_wrapper.addAdaptiveTileObject_texture_repeat(
1606 self.
context, center.to_list(), size.to_list(), rotation_list,
1607 refinement.to_list(), texturefile, texture_repeat.to_list()
1610 return context_wrapper.addAdaptiveTileObject_texture(
1611 self.
context, center.to_list(), size.to_list(), rotation_list,
1612 refinement.to_list(), texturefile
1615 return context_wrapper.addAdaptiveTileObject_color(
1616 self.
context, center.to_list(), size.to_list(), rotation_list,
1617 refinement.to_list(), color.to_list()
1620 return context_wrapper.addAdaptiveTileObject_basic(
1621 self.
context, center.to_list(), size.to_list(), rotation_list,
1622 refinement.to_list()
1626 refinement: Optional[AdaptiveTileRefinement] =
None,
1627 texture_repeat: Optional[int2] =
None) -> int:
1629 Determine how many sub-patches an adaptive tile object would contain, without building geometry.
1631 Runs the same quadtree traversal as :meth:`addAdaptiveTileObject` but counts cells instead
1632 of creating primitives. Useful for checking the cost of a set of refinement parameters
1633 before committing to it, since the sub-patch count is highly sensitive to
1634 ``refinement.transition_exponent``.
1637 size: Size of the tile in the x- and y-directions
1638 refinement: Parameters controlling the adaptive sub-patch resolution
1639 (default: ``AdaptiveTileRefinement()``)
1640 texture_repeat: Texture repetitions the tile would use (default: 1x1)
1643 Number of sub-patches that would be created
1646 ValueError: If parameters are invalid
1647 RuntimeError: Raised for a refinement that would generate too many sub-patches, rather
1648 than reporting a count, so that this and :meth:`addAdaptiveTileObject` answer the
1649 question the same way
1652 >>> refinement = AdaptiveTileRefinement(subpatch_size_min=0.02, subpatch_size_max=2.0)
1653 >>> ctx.predictAdaptiveTileObjectSubpatchCount(vec2(50, 50), refinement) # doctest: +SKIP
1658 if refinement
is None:
1660 if texture_repeat
is None:
1661 texture_repeat =
int2(1, 1)
1663 if not isinstance(size, vec2):
1664 raise ValueError(f
"Size must be a vec2, got {type(size).__name__}")
1665 if not isinstance(refinement, AdaptiveTileRefinement):
1666 raise ValueError(f
"Refinement must be an AdaptiveTileRefinement or None, got {type(refinement).__name__}")
1667 if not isinstance(texture_repeat, int2):
1668 raise ValueError(f
"texture_repeat must be an int2 or None, got {type(texture_repeat).__name__}")
1670 return context_wrapper.predictAdaptiveTileObjectSubpatchCount(
1671 self.
context, size.to_list(), refinement.to_list(), texture_repeat.to_list()
1675 subdiv: int3 =
int3(1, 1, 1), color: Optional[RGBcolor] =
None,
1676 texturefile: Optional[str] =
None, reverse_normals: bool =
False) -> int:
1678 Add a rectangular box (prism) as a compound object to the context.
1681 center: Center position (default: origin)
1682 size: Size in x, y, z directions (default: 1x1x1)
1683 subdiv: Subdivisions in x, y, z (default: 1x1x1)
1684 color: Optional RGB color
1685 texturefile: Optional texture file path
1686 reverse_normals: Reverse normal directions (default: False)
1689 Object ID of the created compound object
1694 if not isinstance(center, vec3):
1695 raise ValueError(f
"Center must be a vec3, got {type(center).__name__}")
1696 if not isinstance(size, vec3):
1697 raise ValueError(f
"Size must be a vec3, got {type(size).__name__}")
1698 if not isinstance(subdiv, int3):
1699 raise ValueError(f
"Subdiv must be an int3, got {type(subdiv).__name__}")
1700 if color
is not None and not isinstance(color, RGBcolor):
1701 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1705 return context_wrapper.addBoxObject_texture_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile, reverse_normals)
1707 return context_wrapper.addBoxObject_color_reverse(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list(), reverse_normals)
1709 raise ValueError(
"reverse_normals requires either color or texturefile")
1711 return context_wrapper.addBoxObject_texture(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile)
1713 return context_wrapper.addBoxObject_color(self.
context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list())
1715 return context_wrapper.addBoxObject_basic(self.
context, center.to_list(), size.to_list(), subdiv.to_list())
1717 def addConeObject(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1718 ndivs: int = 20, color: Optional[RGBcolor] =
None,
1719 texturefile: Optional[str] =
None) -> int:
1721 Add a cone/cylinder/frustum as a compound object to the context.
1724 node0: Base position
1726 radius0: Radius at base
1727 radius1: Radius at top
1728 ndivs: Number of radial divisions (default: 20)
1729 color: Optional RGB color
1730 texturefile: Optional texture file path
1733 Object ID of the created compound object
1738 if not isinstance(node0, vec3):
1739 raise ValueError(f
"node0 must be a vec3, got {type(node0).__name__}")
1740 if not isinstance(node1, vec3):
1741 raise ValueError(f
"node1 must be a vec3, got {type(node1).__name__}")
1742 if not isinstance(radius0, (int, float)):
1743 raise ValueError(f
"radius0 must be a number, got {type(radius0).__name__}")
1744 if not isinstance(radius1, (int, float)):
1745 raise ValueError(f
"radius1 must be a number, got {type(radius1).__name__}")
1746 if color
is not None and not isinstance(color, RGBcolor):
1747 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
1750 return context_wrapper.addConeObject_texture(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, texturefile)
1752 return context_wrapper.addConeObject_color(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, color.to_list())
1754 return context_wrapper.addConeObject_basic(self.
context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1)
1757 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] =
None,
1758 color: Optional[Union[RGBcolor, RGBAcolor]] =
None,
1759 texturefile: Optional[str] =
None) -> int:
1761 Add a disk as a compound object to the context.
1764 center: Center position (default: origin)
1765 size: Semi-major and semi-minor radii (default: 1x1)
1766 ndivs: int (uniform) or int2 (polar/radial subdivisions) (default: 20)
1767 rotation: Optional spherical rotation
1768 color: Optional RGB or RGBA color
1769 texturefile: Optional texture file path
1772 Object ID of the created compound object
1776 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
if rotation
else [1, 0, 0]
1777 is_polar = isinstance(ndivs, int2)
1781 return context_wrapper.addDiskObject_polar_texture(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, texturefile)
1783 if isinstance(color, RGBAcolor):
1784 return context_wrapper.addDiskObject_polar_rgba(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1786 return context_wrapper.addDiskObject_polar_color(self.
context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1788 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())
1791 return context_wrapper.addDiskObject_texture(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, texturefile)
1793 if isinstance(color, RGBAcolor):
1794 return context_wrapper.addDiskObject_rgba(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1796 return context_wrapper.addDiskObject_color(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1798 return context_wrapper.addDiskObject_rotation(self.
context, ndivs, center.to_list(), size.to_list(), rotation_list)
1800 return context_wrapper.addDiskObject_basic(self.
context, ndivs, center.to_list(), size.to_list())
1802 def addTubeObject(self, ndivs: int, nodes: List[vec3], radii: List[float],
1803 colors: Optional[List[RGBcolor]] =
None,
1804 texturefile: Optional[str] =
None,
1805 texture_uv: Optional[List[float]] =
None) -> int:
1807 Add a tube as a compound object to the context.
1810 ndivs: Number of radial subdivisions
1811 nodes: List of vec3 positions defining tube segments
1812 radii: List of radii at each node
1813 colors: Optional list of RGB colors for each segment
1814 texturefile: Optional texture file path
1815 texture_uv: Optional UV coordinates for texture mapping
1818 Object ID of the created compound object
1823 if not isinstance(nodes, (list, tuple)):
1824 raise ValueError(f
"Nodes must be a list, got {type(nodes).__name__}")
1825 for i, node
in enumerate(nodes):
1826 if not isinstance(node, vec3):
1827 raise ValueError(f
"nodes[{i}] must be a vec3, got {type(node).__name__}")
1828 if not isinstance(radii, (list, tuple)):
1829 raise ValueError(f
"Radii must be a list, got {type(radii).__name__}")
1830 if colors
is not None:
1831 if not isinstance(colors, (list, tuple)):
1832 raise ValueError(f
"Colors must be a list or None, got {type(colors).__name__}")
1833 for i, c
in enumerate(colors):
1834 if not isinstance(c, RGBcolor):
1835 raise ValueError(f
"colors[{i}] must be an RGBcolor, got {type(c).__name__}")
1838 raise ValueError(
"Tube requires at least 2 nodes")
1839 if len(radii) != len(nodes):
1840 raise ValueError(
"Number of radii must match number of nodes")
1842 nodes_flat = [coord
for node
in nodes
for coord
in node.to_list()]
1844 if texture_uv
is not None:
1845 if texturefile
is None:
1846 raise ValueError(
"texture_uv requires texturefile")
1847 return context_wrapper.addTubeObject_texture_uv(self.
context, ndivs, nodes_flat, radii, texturefile, texture_uv)
1849 return context_wrapper.addTubeObject_texture(self.
context, ndivs, nodes_flat, radii, texturefile)
1851 if len(colors) != len(nodes):
1852 raise ValueError(
"Number of colors must match number of nodes")
1853 colors_flat = [c
for color
in colors
for c
in color.to_list()]
1854 return context_wrapper.addTubeObject_color(self.
context, ndivs, nodes_flat, radii, colors_flat)
1856 return context_wrapper.addTubeObject_basic(self.
context, ndivs, nodes_flat, radii)
1858 def copyPrimitive(self, UUID: Union[int, List[int]]) -> Union[int, List[int]]:
1860 Copy one or more primitives.
1862 Creates a duplicate of the specified primitive(s) with all associated data.
1863 The copy is placed at the same location as the original.
1866 UUID: Single primitive UUID or list of UUIDs to copy
1869 Single UUID of copied primitive (if UUID is int) or
1870 List of UUIDs of copied primitives (if UUID is list)
1873 >>> context = Context()
1874 >>> original_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1875 >>> # Copy single primitive
1876 >>> copied_uuid = context.copyPrimitive(original_uuid)
1877 >>> # Copy multiple primitives
1878 >>> copied_uuids = context.copyPrimitive([uuid1, uuid2, uuid3])
1882 if isinstance(UUID, int):
1883 return context_wrapper.copyPrimitive(self.
context, UUID)
1884 elif isinstance(UUID, list):
1885 return context_wrapper.copyPrimitives(self.
context, UUID)
1887 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
1891 Copy all primitive data from source to destination primitive.
1893 Copies all associated data (primitive data fields) from the source
1894 primitive to the destination primitive. Both primitives must already exist.
1897 sourceUUID: UUID of the source primitive
1898 destinationUUID: UUID of the destination primitive
1901 >>> context = Context()
1902 >>> source_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1903 >>> dest_uuid = context.addPatch(center=vec3(1, 0, 0), size=vec2(1, 1))
1904 >>> context.setPrimitiveDataFloat(source_uuid, "temperature", 25.5)
1905 >>> context.copyPrimitiveData(source_uuid, dest_uuid)
1906 >>> # dest_uuid now has temperature data
1910 if not isinstance(sourceUUID, int):
1911 raise ValueError(f
"sourceUUID must be int, got {type(sourceUUID).__name__}")
1912 if not isinstance(destinationUUID, int):
1913 raise ValueError(f
"destinationUUID must be int, got {type(destinationUUID).__name__}")
1915 context_wrapper.copyPrimitiveData(self.
context, sourceUUID, destinationUUID)
1917 def copyObject(self, ObjID: Union[int, List[int]]) -> Union[int, List[int]]:
1919 Copy one or more compound objects.
1921 Creates a duplicate of the specified compound object(s) with all
1922 associated primitives and data. The copy is placed at the same location
1926 ObjID: Single object ID or list of object IDs to copy
1929 Single object ID of copied object (if ObjID is int) or
1930 List of object IDs of copied objects (if ObjID is list)
1933 >>> context = Context()
1934 >>> original_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1935 >>> # Copy single object
1936 >>> copied_obj = context.copyObject(original_obj)
1937 >>> # Copy multiple objects
1938 >>> copied_objs = context.copyObject([obj1, obj2, obj3])
1942 if isinstance(ObjID, int):
1943 return context_wrapper.copyObject(self.
context, ObjID)
1944 elif isinstance(ObjID, list):
1945 return context_wrapper.copyObjects(self.
context, ObjID)
1947 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
1949 def copyObjectData(self, source_objID: int, destination_objID: int) ->
None:
1951 Copy all object data from source to destination compound object.
1953 Copies all associated data (object data fields) from the source
1954 compound object to the destination object. Both objects must already exist.
1957 source_objID: Object ID of the source compound object
1958 destination_objID: Object ID of the destination compound object
1961 >>> context = Context()
1962 >>> source_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1963 >>> dest_obj = context.addTile(center=vec3(2, 0, 0), size=vec2(2, 2))
1964 >>> context.setObjectData(source_obj, "material", "wood")
1965 >>> context.copyObjectData(source_obj, dest_obj)
1966 >>> # dest_obj now has material data
1970 if not isinstance(source_objID, int):
1971 raise ValueError(f
"source_objID must be int, got {type(source_objID).__name__}")
1972 if not isinstance(destination_objID, int):
1973 raise ValueError(f
"destination_objID must be int, got {type(destination_objID).__name__}")
1975 context_wrapper.copyObjectData(self.
context, source_objID, destination_objID)
1979 Translate one or more primitives by a shift vector.
1981 Moves the specified primitive(s) by the given shift vector without
1982 changing their orientation or size.
1985 UUID: Single primitive UUID or list of UUIDs to translate
1986 shift: 3D vector representing the translation [x, y, z]
1989 >>> context = Context()
1990 >>> patch_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1991 >>> # Translate single primitive
1992 >>> context.translatePrimitive(patch_uuid, vec3(1, 0, 0)) # Move 1 unit in x
1993 >>> # Translate multiple primitives
1994 >>> context.translatePrimitive([uuid1, uuid2, uuid3], vec3(0, 0, 1)) # Move 1 unit in z
1999 if not isinstance(shift, vec3):
2000 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
2002 if isinstance(UUID, int):
2003 context_wrapper.translatePrimitive(self.
context, UUID, shift.to_list())
2004 elif isinstance(UUID, list):
2005 context_wrapper.translatePrimitives(self.
context, UUID, shift.to_list())
2007 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2009 def translateObject(self, ObjID: Union[int, List[int]], shift: vec3) ->
None:
2011 Translate one or more compound objects by a shift vector.
2013 Moves the specified compound object(s) and all their constituent
2014 primitives by the given shift vector without changing orientation or size.
2017 ObjID: Single object ID or list of object IDs to translate
2018 shift: 3D vector representing the translation [x, y, z]
2021 >>> context = Context()
2022 >>> tile_uuids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
2023 >>> obj_id = context.getPrimitiveParentObjectID(tile_uuids[0]) # Get object ID
2024 >>> # Translate single object
2025 >>> context.translateObject(obj_id, vec3(5, 0, 0)) # Move 5 units in x
2026 >>> # Translate multiple objects
2027 >>> context.translateObject([obj1, obj2, obj3], vec3(0, 2, 0)) # Move 2 units in y
2032 if not isinstance(shift, vec3):
2033 raise ValueError(f
"shift must be a vec3, got {type(shift).__name__}")
2035 if isinstance(ObjID, int):
2036 context_wrapper.translateObject(self.
context, ObjID, shift.to_list())
2037 elif isinstance(ObjID, list):
2038 context_wrapper.translateObjects(self.
context, ObjID, shift.to_list())
2040 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2043 axis: Union[str, vec3], origin: Optional[vec3] =
None) ->
None:
2045 Rotate one or more primitives.
2048 UUID: Single UUID or list of UUIDs to rotate
2049 angle: Rotation angle in radians
2050 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
2051 origin: Optional rotation origin point. If None, rotates about primitive center.
2052 If provided with string axis, raises ValueError.
2055 ValueError: If axis is invalid or if origin is provided with string axis
2060 if isinstance(axis, str):
2061 if axis
not in (
'x',
'y',
'z'):
2062 raise ValueError(
"axis must be 'x', 'y', or 'z'")
2063 if origin
is not None:
2064 raise ValueError(
"origin parameter cannot be used with string axis")
2067 if isinstance(UUID, int):
2068 context_wrapper.rotatePrimitive_axisString(self.
context, UUID, angle, axis)
2069 elif isinstance(UUID, list):
2070 context_wrapper.rotatePrimitives_axisString(self.
context, UUID, angle, axis)
2072 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2074 elif isinstance(axis, vec3):
2075 axis_list = axis.to_list()
2078 if all(abs(v) < 1e-10
for v
in axis_list):
2079 raise ValueError(
"axis vector cannot be zero")
2083 if isinstance(UUID, int):
2084 context_wrapper.rotatePrimitive_axisVector(self.
context, UUID, angle, axis_list)
2085 elif isinstance(UUID, list):
2086 context_wrapper.rotatePrimitives_axisVector(self.
context, UUID, angle, axis_list)
2088 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2091 if not isinstance(origin, vec3):
2092 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
2094 origin_list = origin.to_list()
2095 if isinstance(UUID, int):
2096 context_wrapper.rotatePrimitive_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
2097 elif isinstance(UUID, list):
2098 context_wrapper.rotatePrimitives_originAxisVector(self.
context, UUID, angle, origin_list, axis_list)
2100 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2102 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
2104 def rotateObject(self, ObjID: Union[int, List[int]], angle: float,
2105 axis: Union[str, vec3], origin: Optional[vec3] =
None,
2106 about_origin: bool =
False) ->
None:
2108 Rotate one or more objects.
2111 ObjID: Single object ID or list of object IDs to rotate
2112 angle: Rotation angle in radians
2113 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
2114 origin: Optional rotation origin point. If None, rotates about object center.
2115 If provided with string axis, raises ValueError.
2116 about_origin: If True, rotate about the object's own stored origin point
2117 (``object_origin``), which for most objects is its construction center —
2118 NOT the global origin (0,0,0). An object built away from the world origin
2119 therefore spins in place rather than orbiting the world origin. To orbit a
2120 specific point, pass that point as ``origin`` instead. Cannot be used with
2121 the origin parameter.
2124 ValueError: If axis is invalid or if origin and about_origin are both specified
2129 if origin
is not None and about_origin:
2130 raise ValueError(
"Cannot specify both origin and about_origin")
2133 if isinstance(axis, str):
2134 if axis
not in (
'x',
'y',
'z'):
2135 raise ValueError(
"axis must be 'x', 'y', or 'z'")
2136 if origin
is not None:
2137 raise ValueError(
"origin parameter cannot be used with string axis")
2139 raise ValueError(
"about_origin parameter cannot be used with string axis")
2142 if isinstance(ObjID, int):
2143 context_wrapper.rotateObject_axisString(self.
context, ObjID, angle, axis)
2144 elif isinstance(ObjID, list):
2145 context_wrapper.rotateObjects_axisString(self.
context, ObjID, angle, axis)
2147 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2149 elif isinstance(axis, vec3):
2150 axis_list = axis.to_list()
2153 if all(abs(v) < 1e-10
for v
in axis_list):
2154 raise ValueError(
"axis vector cannot be zero")
2158 if isinstance(ObjID, int):
2159 context_wrapper.rotateObjectAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
2160 elif isinstance(ObjID, list):
2161 context_wrapper.rotateObjectsAboutOrigin_axisVector(self.
context, ObjID, angle, axis_list)
2163 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2164 elif origin
is None:
2166 if isinstance(ObjID, int):
2167 context_wrapper.rotateObject_axisVector(self.
context, ObjID, angle, axis_list)
2168 elif isinstance(ObjID, list):
2169 context_wrapper.rotateObjects_axisVector(self.
context, ObjID, angle, axis_list)
2171 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2174 if not isinstance(origin, vec3):
2175 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
2177 origin_list = origin.to_list()
2178 if isinstance(ObjID, int):
2179 context_wrapper.rotateObject_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
2180 elif isinstance(ObjID, list):
2181 context_wrapper.rotateObjects_originAxisVector(self.
context, ObjID, angle, origin_list, axis_list)
2183 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2185 raise ValueError(f
"axis must be str or vec3, got {type(axis).__name__}")
2187 def scalePrimitive(self, UUID: Union[int, List[int]], scale: vec3, point: Optional[vec3] =
None) ->
None:
2189 Scale one or more primitives.
2192 UUID: Single UUID or list of UUIDs to scale
2193 scale: Scale factors as vec3(x, y, z)
2194 point: Optional point to scale about. If None, scales about primitive center.
2197 ValueError: If scale or point parameters are invalid
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(UUID, int):
2209 context_wrapper.scalePrimitive(self.
context, UUID, scale_list)
2210 elif isinstance(UUID, list):
2211 context_wrapper.scalePrimitives(self.
context, UUID, scale_list)
2213 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2216 if not isinstance(point, vec3):
2217 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
2219 point_list = point.to_list()
2220 if isinstance(UUID, int):
2221 context_wrapper.scalePrimitiveAboutPoint(self.
context, UUID, scale_list, point_list)
2222 elif isinstance(UUID, list):
2223 context_wrapper.scalePrimitivesAboutPoint(self.
context, UUID, scale_list, point_list)
2225 raise ValueError(f
"UUID must be int or List[int], got {type(UUID).__name__}")
2227 def scaleObject(self, ObjID: Union[int, List[int]], scale: vec3,
2228 point: Optional[vec3] =
None, about_center: bool =
False,
2229 about_origin: bool =
False) ->
None:
2231 Scale one or more objects.
2234 ObjID: Single object ID or list of object IDs to scale
2235 scale: Scale factors as vec3(x, y, z)
2236 point: Optional point to scale about
2237 about_center: If True, scale about object center (default behavior)
2238 about_origin: If True, scale about the object's own stored origin point
2239 (``object_origin``), not the global origin (0,0,0). Pass ``point`` to
2240 scale about a specific location instead.
2243 ValueError: If parameters are invalid or conflicting options specified
2248 options_count = sum([point
is not None, about_center, about_origin])
2249 if options_count > 1:
2250 raise ValueError(
"Cannot specify multiple scaling options (point, about_center, about_origin)")
2252 if not isinstance(scale, vec3):
2253 raise ValueError(f
"scale must be a vec3, got {type(scale).__name__}")
2255 scale_list = scale.to_list()
2259 if isinstance(ObjID, int):
2260 context_wrapper.scaleObjectAboutOrigin(self.
context, ObjID, scale_list)
2261 elif isinstance(ObjID, list):
2262 context_wrapper.scaleObjectsAboutOrigin(self.
context, ObjID, scale_list)
2264 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2267 if isinstance(ObjID, int):
2268 context_wrapper.scaleObjectAboutCenter(self.
context, ObjID, scale_list)
2269 elif isinstance(ObjID, list):
2270 context_wrapper.scaleObjectsAboutCenter(self.
context, ObjID, scale_list)
2272 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2273 elif point
is not None:
2275 if not isinstance(point, vec3):
2276 raise ValueError(f
"point must be a vec3, got {type(point).__name__}")
2278 point_list = point.to_list()
2279 if isinstance(ObjID, int):
2280 context_wrapper.scaleObjectAboutPoint(self.
context, ObjID, scale_list, point_list)
2281 elif isinstance(ObjID, list):
2282 context_wrapper.scaleObjectsAboutPoint(self.
context, ObjID, scale_list, point_list)
2284 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2287 if isinstance(ObjID, int):
2288 context_wrapper.scaleObject(self.
context, ObjID, scale_list)
2289 elif isinstance(ObjID, list):
2290 context_wrapper.scaleObjects(self.
context, ObjID, scale_list)
2292 raise ValueError(f
"ObjID must be int or List[int], got {type(ObjID).__name__}")
2296 Scale the length of a Cone object by scaling the distance between its two nodes.
2299 ObjID: Object ID of the Cone to scale
2300 scale_factor: Factor by which to scale the cone length (e.g., 2.0 doubles length)
2303 ValueError: If ObjID is not an integer or scale_factor is invalid
2304 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
2307 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
2308 >>> context.scaleConeObjectLength(cone_id, 1.5) # Make cone 50% longer
2310 if not isinstance(ObjID, int):
2311 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
2312 if not isinstance(scale_factor, (int, float)):
2313 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
2314 if scale_factor <= 0:
2315 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
2317 context_wrapper.scaleConeObjectLength(self.
context, ObjID, float(scale_factor))
2321 Scale the girth of a Cone object by scaling the radii at both nodes.
2324 ObjID: Object ID of the Cone to scale
2325 scale_factor: Factor by which to scale the cone girth (e.g., 2.0 doubles girth)
2328 ValueError: If ObjID is not an integer or scale_factor is invalid
2329 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
2332 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
2333 >>> context.scaleConeObjectGirth(cone_id, 2.0) # Double the cone girth
2335 if not isinstance(ObjID, int):
2336 raise ValueError(f
"ObjID must be an integer, got {type(ObjID).__name__}")
2337 if not isinstance(scale_factor, (int, float)):
2338 raise ValueError(f
"scale_factor must be numeric, got {type(scale_factor).__name__}")
2339 if scale_factor <= 0:
2340 raise ValueError(f
"scale_factor must be positive, got {scale_factor}")
2342 context_wrapper.scaleConeObjectGirth(self.
context, ObjID, float(scale_factor))
2344 def loadPLY(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
2345 rotation: Optional[SphericalCoord] =
None, color: Optional[RGBcolor] =
None,
2346 upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
2348 Load geometry from a PLY (Stanford Polygon) file.
2351 filename: Path to the PLY file to load
2352 origin: Origin point for positioning the geometry (optional)
2353 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)
2354 rotation: Rotation to apply to the geometry (optional)
2355 color: Default color for geometry without color data (optional)
2356 upaxis: Up axis orientation ("YUP" or "ZUP")
2357 silent: If True, suppress loading output messages
2360 List of UUIDs for the loaded primitives
2365 if origin
is not None and not isinstance(origin, vec3):
2366 raise ValueError(f
"Origin must be a vec3 or None, got {type(origin).__name__}")
2367 if rotation
is not None and not isinstance(rotation, SphericalCoord):
2368 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
2369 if color
is not None and not isinstance(color, RGBcolor):
2370 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
2375 if origin
is None and height
is None and rotation
is None and color
is None:
2377 return context_wrapper.loadPLY(self.
context, validated_filename, silent)
2379 elif origin
is not None and height
is not None and rotation
is None and color
is None:
2381 return context_wrapper.loadPLYWithOriginHeight(self.
context, validated_filename, origin.to_list(), height, upaxis, silent)
2383 elif origin
is not None and height
is not None and rotation
is not None and color
is None:
2385 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
2386 return context_wrapper.loadPLYWithOriginHeightRotation(self.
context, validated_filename, origin.to_list(), height, rotation_list, upaxis, silent)
2388 elif origin
is not None and height
is not None and rotation
is None and color
is not None:
2390 return context_wrapper.loadPLYWithOriginHeightColor(self.
context, validated_filename, origin.to_list(), height, color.to_list(), upaxis, silent)
2392 elif origin
is not None and height
is not None and rotation
is not None and color
is not None:
2394 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
2395 return context_wrapper.loadPLYWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation_list, color.to_list(), upaxis, silent)
2398 raise ValueError(
"Invalid parameter combination. When using transformations, both origin and height are required.")
2400 def loadOBJ(self, filename: str, origin: Optional[vec3] =
None, height: Optional[float] =
None,
2401 scale: Optional[vec3] =
None, rotation: Optional[SphericalCoord] =
None,
2402 color: Optional[RGBcolor] =
None, upaxis: str =
"YUP", silent: bool =
False) -> List[int]:
2404 Load geometry from an OBJ (Wavefront) file.
2407 filename: Path to the OBJ file to load
2408 origin: Origin point for positioning the geometry (optional)
2409 height: Absolute height in metres of the loaded geometry after scaling -- not a multiplier (optional, alternative to scale)
2410 scale: Scale factor for all dimensions (optional, alternative to height)
2411 rotation: Rotation to apply to the geometry (optional)
2412 color: Default color for geometry without color data (optional)
2413 upaxis: Up axis orientation ("YUP" or "ZUP")
2414 silent: If True, suppress loading output messages
2417 List of UUIDs for the loaded primitives
2422 if origin
is not None and not isinstance(origin, vec3):
2423 raise ValueError(f
"Origin must be a vec3 or None, got {type(origin).__name__}")
2424 if scale
is not None and not isinstance(scale, vec3):
2425 raise ValueError(f
"Scale must be a vec3 or None, got {type(scale).__name__}")
2426 if rotation
is not None and not isinstance(rotation, SphericalCoord):
2427 raise ValueError(f
"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
2428 if color
is not None and not isinstance(color, RGBcolor):
2429 raise ValueError(f
"Color must be an RGBcolor or None, got {type(color).__name__}")
2434 if origin
is None and height
is None and scale
is None and rotation
is None and color
is None:
2436 return context_wrapper.loadOBJ(self.
context, validated_filename, silent)
2438 elif origin
is not None and height
is not None and scale
is None and rotation
is not None and color
is not None:
2440 return context_wrapper.loadOBJWithOriginHeightRotationColor(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), silent)
2442 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":
2444 return context_wrapper.loadOBJWithOriginHeightRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), upaxis, silent)
2446 elif origin
is not None and scale
is not None and rotation
is not None and color
is not None:
2448 return context_wrapper.loadOBJWithOriginScaleRotationColorUpaxis(self.
context, validated_filename, origin.to_list(), scale.to_list(), rotation.to_list(), color.to_list(), upaxis, silent)
2451 raise ValueError(
"Invalid parameter combination. For OBJ loading, you must provide either: " +
2452 "1) No parameters (simple load), " +
2453 "2) origin + height + rotation + color, " +
2454 "3) origin + height + rotation + color + upaxis, or " +
2455 "4) origin + scale + rotation + color + upaxis")
2457 def loadXML(self, filename: str, quiet: bool =
False) -> List[int]:
2459 Load geometry from a Helios XML file.
2462 filename: Path to the XML file to load
2463 quiet: If True, suppress loading output messages
2466 List of UUIDs for the loaded primitives
2472 return context_wrapper.loadXML(self.
context, validated_filename, quiet)
2474 def writePLY(self, filename: str, UUIDs: Optional[List[int]] =
None) ->
None:
2476 Write geometry to a PLY (Stanford Polygon) file.
2479 filename: Path to the output PLY file
2480 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
2483 ValueError: If filename is invalid or UUIDs are invalid
2484 PermissionError: If output directory is not writable
2485 FileNotFoundError: If UUIDs do not exist in context
2486 RuntimeError: If Context is in mock mode
2489 >>> context.writePLY("output.ply") # Export all primitives
2490 >>> context.writePLY("subset.ply", [uuid1, uuid2]) # Export specific primitives
2499 context_wrapper.writePLY(self.
context, validated_filename)
2503 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
2509 context_wrapper.writePLYWithUUIDs(self.
context, validated_filename, UUIDs)
2511 def writeOBJ(self, filename: str, UUIDs: Optional[List[int]] =
None,
2512 primitive_data_fields: Optional[List[str]] =
None,
2513 write_normals: bool =
False, silent: bool =
False) ->
None:
2515 Write geometry to an OBJ (Wavefront) file.
2518 filename: Path to the output OBJ file
2519 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
2520 primitive_data_fields: Optional list of primitive data field names to export. For each
2521 label, an ASCII file ``<stem>_<label>.dat`` is written next to the OBJ file with one
2522 value per line: line i holds the value for face i of the OBJ file, counting faces in
2523 the order they appear. A patch is written as two triangular faces, so its value
2524 appears on two consecutive lines. Every primitive in ``UUIDs`` must carry each label
2525 with type int, uint, float, double or string; otherwise an error is raised before any
2527 write_normals: Whether to include vertex normals in the output
2528 silent: Whether to suppress output messages during export
2531 ValueError: If filename is invalid or UUIDs are invalid
2532 HeliosRuntimeError: If a primitive lacks a requested data field or the field is not
2534 PermissionError: If output directory is not writable
2535 FileNotFoundError: If UUIDs do not exist in context
2536 RuntimeError: If Context is in mock mode
2539 >>> context.writeOBJ("output.obj") # Export all primitives
2540 >>> context.writeOBJ("subset.obj", [uuid1, uuid2]) # Export specific primitives
2541 >>> context.writeOBJ("with_data.obj", [uuid1], ["temperature", "area"]) # Export with data
2550 context_wrapper.writeOBJ(self.
context, validated_filename, write_normals, silent)
2551 elif primitive_data_fields
is None:
2554 raise ValueError(
"UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
2559 context_wrapper.writeOBJWithUUIDs(self.
context, validated_filename, UUIDs, write_normals, silent)
2563 raise ValueError(
"UUIDs list cannot be empty when exporting primitive data")
2564 if not primitive_data_fields:
2565 raise ValueError(
"primitive_data_fields list cannot be empty")
2573 context_wrapper.writeOBJWithPrimitiveData(self.
context, validated_filename, UUIDs, primitive_data_fields, write_normals, silent)
2576 UUIDs: Optional[List[int]] =
None,
2577 print_header: bool =
False) ->
None:
2579 Write primitive data to an ASCII text file.
2581 Outputs a space-separated text file where each row corresponds to a primitive
2582 and each column corresponds to a primitive data label.
2585 filename: Path to the output file
2586 column_labels: List of primitive data labels to include as columns.
2587 Use "UUID" to include primitive UUIDs as a column.
2588 The order determines the column order in the output file.
2589 If a primitive does not have a label, or its data is not a scalar
2590 type (int, uint, float, double or string), 0 is written in its
2591 place and a single warning is printed for the call.
2592 UUIDs: Optional list of primitive UUIDs to include. If None, includes all primitives.
2593 print_header: If True, writes column labels as the first line of the file
2596 ValueError: If filename is invalid, column_labels is empty, or UUIDs list is empty when provided
2597 HeliosRuntimeError: If the output file cannot be opened for writing
2600 >>> # Write temperature and area for all primitives
2601 >>> context.writePrimitiveData("output.txt", ["UUID", "temperature", "area"])
2603 >>> # Write with header row
2604 >>> context.writePrimitiveData("output.txt", ["UUID", "radiation_flux"], print_header=True)
2606 >>> # Write only for selected primitives
2607 >>> context.writePrimitiveData("subset.txt", ["temperature"], UUIDs=[uuid1, uuid2])
2612 if not column_labels:
2613 raise ValueError(
"column_labels list cannot be empty")
2620 context_wrapper.writePrimitiveData(self.
context, validated_filename, column_labels, print_header)
2624 raise ValueError(
"UUIDs list cannot be empty when provided. Use UUIDs=None to include all primitives")
2629 context_wrapper.writePrimitiveDataWithUUIDs(self.
context, validated_filename, column_labels, UUIDs, print_header)
2632 colors: Optional[np.ndarray] =
None) -> List[int]:
2634 Add triangles from NumPy arrays (compatible with trimesh, Open3D format).
2637 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2638 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2639 colors: Optional NumPy array of shape (N, 3) or (M, 3) containing RGB colors as float32/float64
2640 If shape (N, 3): per-vertex colors
2641 If shape (M, 3): per-triangle colors
2644 List of UUIDs for the added triangles
2647 ValueError: If array dimensions are invalid
2650 if vertices.ndim != 2
or vertices.shape[1] != 3:
2651 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2652 if faces.ndim != 2
or faces.shape[1] != 3:
2653 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2656 max_vertex_index = np.max(faces)
2657 if max_vertex_index >= vertices.shape[0]:
2658 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2661 per_vertex_colors =
False
2662 per_triangle_colors =
False
2663 if colors
is not None:
2664 if colors.ndim != 2
or colors.shape[1] != 3:
2665 raise ValueError(f
"Colors array must have shape (N, 3) or (M, 3), got {colors.shape}")
2666 if colors.shape[0] == vertices.shape[0]:
2667 per_vertex_colors =
True
2668 elif colors.shape[0] == faces.shape[0]:
2669 per_triangle_colors =
True
2671 raise ValueError(f
"Colors array shape {colors.shape} doesn't match vertices ({vertices.shape[0]},) or faces ({faces.shape[0]},)")
2674 vertices_float = vertices.astype(np.float32)
2675 faces_int = faces.astype(np.int32)
2676 if colors
is not None:
2677 colors_float = colors.astype(np.float32)
2682 corner0 = vertices_float[faces_int[:, 0]].tolist()
2683 corner1 = vertices_float[faces_int[:, 1]].tolist()
2684 corner2 = vertices_float[faces_int[:, 2]].tolist()
2691 elif per_triangle_colors:
2692 face_colors = colors_float.tolist()
2694 averaged = (colors_float[faces_int[:, 0]]
2695 + colors_float[faces_int[:, 1]]
2696 + colors_float[faces_int[:, 2]]) / 3.0
2697 face_colors = averaged.tolist()
2701 if face_colors
is None:
2702 for vertex0, vertex1, vertex2
in zip(corner0, corner1, corner2):
2703 triangle_uuids.append(
2704 context_wrapper.addTriangle(self.
context, vertex0, vertex1, vertex2))
2706 for vertex0, vertex1, vertex2, color
in zip(corner0, corner1, corner2, face_colors):
2707 triangle_uuids.append(
2708 context_wrapper.addTriangleWithColor(
2709 self.
context, vertex0, vertex1, vertex2, color))
2711 return triangle_uuids
2714 uv_coords: np.ndarray, texture_files: Union[str, List[str]],
2715 material_ids: Optional[np.ndarray] =
None) -> List[int]:
2717 Add textured triangles from NumPy arrays with support for multiple textures.
2719 This method supports both single-texture and multi-texture workflows:
2720 - Single texture: Pass a single texture file string, all faces use the same texture
2721 - Multiple textures: Pass a list of texture files and material_ids array specifying which texture each face uses
2724 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2725 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2726 uv_coords: NumPy array of shape (N, 2) containing UV texture coordinates as float32/float64
2727 texture_files: Single texture file path (str) or list of texture file paths (List[str])
2728 material_ids: Optional NumPy array of shape (M,) containing material ID for each face.
2729 If None and texture_files is a list, all faces use texture 0.
2730 If None and texture_files is a string, this parameter is ignored.
2733 List of UUIDs for the added textured triangles
2736 ValueError: If array dimensions are invalid or material IDs are out of range
2739 # Single texture usage (backward compatible)
2740 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, "texture.png")
2742 # Multi-texture usage (Open3D style)
2743 >>> texture_files = ["wood.png", "metal.png", "glass.png"]
2744 >>> material_ids = np.array([0, 0, 1, 1, 2, 2]) # 6 faces using different textures
2745 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, texture_files, material_ids)
2750 if vertices.ndim != 2
or vertices.shape[1] != 3:
2751 raise ValueError(f
"Vertices array must have shape (N, 3), got {vertices.shape}")
2752 if faces.ndim != 2
or faces.shape[1] != 3:
2753 raise ValueError(f
"Faces array must have shape (M, 3), got {faces.shape}")
2754 if uv_coords.ndim != 2
or uv_coords.shape[1] != 2:
2755 raise ValueError(f
"UV coordinates array must have shape (N, 2), got {uv_coords.shape}")
2758 if uv_coords.shape[0] != vertices.shape[0]:
2759 raise ValueError(f
"UV coordinates count ({uv_coords.shape[0]}) must match vertices count ({vertices.shape[0]})")
2762 max_vertex_index = np.max(faces)
2763 if max_vertex_index >= vertices.shape[0]:
2764 raise ValueError(f
"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2767 if isinstance(texture_files, str):
2769 texture_file_list = [texture_files]
2770 if material_ids
is None:
2771 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2774 if not np.all(material_ids == 0):
2775 raise ValueError(
"When using single texture file, all material IDs must be 0")
2778 texture_file_list = list(texture_files)
2779 if len(texture_file_list) == 0:
2780 raise ValueError(
"Texture files list cannot be empty")
2782 if material_ids
is None:
2784 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2787 if material_ids.ndim != 1
or material_ids.shape[0] != faces.shape[0]:
2788 raise ValueError(f
"Material IDs must have shape ({faces.shape[0]},), got {material_ids.shape}")
2791 max_material_id = np.max(material_ids)
2792 if max_material_id >= len(texture_file_list):
2793 raise ValueError(f
"Material ID {max_material_id} exceeds texture count {len(texture_file_list)}")
2796 for i, texture_file
in enumerate(texture_file_list):
2799 except (FileNotFoundError, ValueError)
as e:
2800 raise ValueError(f
"Texture file {i} ({texture_file}): {e}")
2803 if 'addTrianglesFromArraysMultiTextured' in context_wrapper._AVAILABLE_TRIANGLE_FUNCTIONS:
2804 return context_wrapper.addTrianglesFromArraysMultiTextured(
2805 self.
context, vertices, faces, uv_coords, texture_file_list, material_ids
2809 from .wrappers.DataTypes
import vec3, vec2
2811 vertices_float = vertices.astype(np.float32)
2812 faces_int = faces.astype(np.int32)
2813 uv_coords_float = uv_coords.astype(np.float32)
2816 for i
in range(faces.shape[0]):
2818 v0_idx, v1_idx, v2_idx = faces_int[i]
2821 vertex0 =
vec3(vertices_float[v0_idx][0], vertices_float[v0_idx][1], vertices_float[v0_idx][2])
2822 vertex1 =
vec3(vertices_float[v1_idx][0], vertices_float[v1_idx][1], vertices_float[v1_idx][2])
2823 vertex2 =
vec3(vertices_float[v2_idx][0], vertices_float[v2_idx][1], vertices_float[v2_idx][2])
2826 uv0 =
vec2(uv_coords_float[v0_idx][0], uv_coords_float[v0_idx][1])
2827 uv1 =
vec2(uv_coords_float[v1_idx][0], uv_coords_float[v1_idx][1])
2828 uv2 =
vec2(uv_coords_float[v2_idx][0], uv_coords_float[v2_idx][1])
2831 material_id = material_ids[i]
2832 texture_file = texture_file_list[material_id]
2836 triangle_uuids.append(uuid)
2838 return triangle_uuids
2846 Set primitive data as signed 32-bit integer for one or multiple primitives.
2849 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2850 label: String key for the data
2851 value: Signed integer scalar (broadcast to all UUIDs), or a list of
2852 values (one per UUID) to set a distinct value on each primitive.
2854 if isinstance(uuids_or_uuid, (list, tuple)):
2855 if isinstance(value, (list, tuple, np.ndarray)):
2856 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int', value)
2858 context_wrapper.setBroadcastPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2860 context_wrapper.setPrimitiveDataInt(self.
context, uuids_or_uuid, label, value)
2864 Set primitive data as unsigned 32-bit integer for one or multiple primitives.
2866 Use this for built-in flags such as 'twosided_flag', which must be stored as unsigned integers.
2869 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2870 label: String key for the data
2871 value: Unsigned integer scalar (broadcast to all UUIDs), or a list of
2872 values (one per UUID) to set a distinct value on each primitive.
2874 if isinstance(uuids_or_uuid, (list, tuple)):
2875 if isinstance(value, (list, tuple, np.ndarray)):
2876 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'UInt', value)
2878 context_wrapper.setBroadcastPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2880 context_wrapper.setPrimitiveDataUInt(self.
context, uuids_or_uuid, label, value)
2884 Set primitive data as 32-bit float for one or multiple primitives.
2887 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2888 label: String key for the data
2889 value: Float scalar (broadcast to all UUIDs), or a list of values
2890 (one per UUID) to set a distinct value on each primitive.
2892 if isinstance(uuids_or_uuid, (list, tuple)):
2893 if isinstance(value, (list, tuple, np.ndarray)):
2894 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Float', value)
2896 context_wrapper.setBroadcastPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2898 context_wrapper.setPrimitiveDataFloat(self.
context, uuids_or_uuid, label, value)
2902 Set primitive data as 64-bit double for one or multiple primitives.
2905 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2906 label: String key for the data
2907 value: Double scalar (broadcast to all UUIDs), or a list of values
2908 (one per UUID) to set a distinct value on each primitive.
2910 if isinstance(uuids_or_uuid, (list, tuple)):
2911 if isinstance(value, (list, tuple, np.ndarray)):
2912 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Double', value)
2914 context_wrapper.setBroadcastPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2916 context_wrapper.setPrimitiveDataDouble(self.
context, uuids_or_uuid, label, value)
2920 Set primitive data as string for one or multiple primitives.
2923 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2924 label: String key for the data
2925 value: String scalar (broadcast to all UUIDs), or a list of strings
2926 (one per UUID) to set a distinct value on each primitive.
2928 if isinstance(uuids_or_uuid, (list, tuple)):
2929 if isinstance(value, (list, tuple, np.ndarray)):
2930 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'String', value)
2932 context_wrapper.setBroadcastPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2934 context_wrapper.setPrimitiveDataString(self.
context, uuids_or_uuid, label, value)
2938 Set primitive data as vec2 for one or multiple primitives.
2941 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2942 label: String key for the data
2943 x_or_vec: Either x component (float) or vec2 object
2944 y: Y component (if x_or_vec is float)
2946 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2947 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Vec2', x_or_vec)
2949 if hasattr(x_or_vec,
'x'):
2950 x, y = x_or_vec.x, x_or_vec.y
2953 if isinstance(uuids_or_uuid, (list, tuple)):
2954 context_wrapper.setBroadcastPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2956 context_wrapper.setPrimitiveDataVec2(self.
context, uuids_or_uuid, label, x, y)
2958 def setPrimitiveDataVec3(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
2960 Set primitive data as vec3 for one or multiple primitives.
2963 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2964 label: String key for the data
2965 x_or_vec: Either x component (float) or vec3 object
2966 y: Y component (if x_or_vec is float)
2967 z: Z component (if x_or_vec is float)
2969 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2970 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Vec3', x_or_vec)
2972 if hasattr(x_or_vec,
'x'):
2973 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2976 if isinstance(uuids_or_uuid, (list, tuple)):
2977 context_wrapper.setBroadcastPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2979 context_wrapper.setPrimitiveDataVec3(self.
context, uuids_or_uuid, label, x, y, z)
2981 def setPrimitiveDataVec4(self, uuids_or_uuid, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
2983 Set primitive data as vec4 for one or multiple primitives.
2986 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2987 label: String key for the data
2988 x_or_vec: Either x component (float) or vec4 object
2989 y: Y component (if x_or_vec is float)
2990 z: Z component (if x_or_vec is float)
2991 w: W component (if x_or_vec is float)
2993 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2994 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Vec4', x_or_vec)
2996 if hasattr(x_or_vec,
'x'):
2997 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
3000 if isinstance(uuids_or_uuid, (list, tuple)):
3001 context_wrapper.setBroadcastPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
3003 context_wrapper.setPrimitiveDataVec4(self.
context, uuids_or_uuid, label, x, y, z, w)
3007 Set primitive data as int2 for one or multiple primitives.
3010 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
3011 label: String key for the data
3012 x_or_vec: Either x component (int) or int2 object
3013 y: Y component (if x_or_vec is int)
3015 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
3016 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int2', x_or_vec)
3018 if hasattr(x_or_vec,
'x'):
3019 x, y = x_or_vec.x, x_or_vec.y
3022 if isinstance(uuids_or_uuid, (list, tuple)):
3023 context_wrapper.setBroadcastPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
3025 context_wrapper.setPrimitiveDataInt2(self.
context, uuids_or_uuid, label, x, y)
3027 def setPrimitiveDataInt3(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
3029 Set primitive data as int3 for one or multiple primitives.
3032 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
3033 label: String key for the data
3034 x_or_vec: Either x component (int) or int3 object
3035 y: Y component (if x_or_vec is int)
3036 z: Z component (if x_or_vec is int)
3038 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
3039 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int3', x_or_vec)
3041 if hasattr(x_or_vec,
'x'):
3042 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
3045 if isinstance(uuids_or_uuid, (list, tuple)):
3046 context_wrapper.setBroadcastPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
3048 context_wrapper.setPrimitiveDataInt3(self.
context, uuids_or_uuid, label, x, y, z)
3050 def setPrimitiveDataInt4(self, uuids_or_uuid, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
3052 Set primitive data as int4 for one or multiple primitives.
3055 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
3056 label: String key for the data
3057 x_or_vec: Either x component (int) or int4 object
3058 y: Y component (if x_or_vec is int)
3059 z: Z component (if x_or_vec is int)
3060 w: W component (if x_or_vec is int)
3062 if isinstance(uuids_or_uuid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
3063 context_wrapper.setPrimitiveDataArray(self.
context, uuids_or_uuid, label,
'Int4', x_or_vec)
3065 if hasattr(x_or_vec,
'x'):
3066 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
3069 if isinstance(uuids_or_uuid, (list, tuple)):
3070 context_wrapper.setBroadcastPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
3072 context_wrapper.setPrimitiveDataInt4(self.
context, uuids_or_uuid, label, x, y, z, w)
3076 Get primitive data for a specific primitive. If data_type is given, the value is read as that
3077 type; if None, the stored type is detected automatically.
3080 uuid: UUID of the primitive
3081 label: String key for the data
3082 data_type: Optional. Python type to retrieve (int, uint, float, double, bool, str, vec2, vec3, vec4, int2, int3, int4, etc.)
3083 If None, the stored type is detected automatically.
3086 The stored value of the specified or auto-detected type
3089 if data_type
is None:
3090 return context_wrapper.getPrimitiveDataAuto(self.
context, uuid, label)
3093 if data_type == int:
3094 return context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
3095 elif data_type == float:
3096 return context_wrapper.getPrimitiveDataFloat(self.
context, uuid, label)
3097 elif data_type == bool:
3099 int_value = context_wrapper.getPrimitiveDataInt(self.
context, uuid, label)
3100 return int_value != 0
3101 elif data_type == str:
3102 return context_wrapper.getPrimitiveDataString(self.
context, uuid, label)
3105 elif data_type == vec2:
3106 coords = context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
3107 return vec2(coords[0], coords[1])
3108 elif data_type == vec3:
3109 coords = context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
3110 return vec3(coords[0], coords[1], coords[2])
3111 elif data_type == vec4:
3112 coords = context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
3113 return vec4(coords[0], coords[1], coords[2], coords[3])
3114 elif data_type == int2:
3115 coords = context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
3116 return int2(coords[0], coords[1])
3117 elif data_type == int3:
3118 coords = context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
3119 return int3(coords[0], coords[1], coords[2])
3120 elif data_type == int4:
3121 coords = context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
3122 return int4(coords[0], coords[1], coords[2], coords[3])
3125 elif data_type ==
"uint":
3126 return context_wrapper.getPrimitiveDataUInt(self.
context, uuid, label)
3127 elif data_type ==
"double":
3128 return context_wrapper.getPrimitiveDataDouble(self.
context, uuid, label)
3131 elif data_type == list:
3133 return context_wrapper.getPrimitiveDataVec3(self.
context, uuid, label)
3134 elif data_type ==
"list_vec2":
3135 return context_wrapper.getPrimitiveDataVec2(self.
context, uuid, label)
3136 elif data_type ==
"list_vec4":
3137 return context_wrapper.getPrimitiveDataVec4(self.
context, uuid, label)
3138 elif data_type ==
"list_int2":
3139 return context_wrapper.getPrimitiveDataInt2(self.
context, uuid, label)
3140 elif data_type ==
"list_int3":
3141 return context_wrapper.getPrimitiveDataInt3(self.
context, uuid, label)
3142 elif data_type ==
"list_int4":
3143 return context_wrapper.getPrimitiveDataInt4(self.
context, uuid, label)
3146 raise ValueError(f
"Unsupported primitive data type: {data_type}. "
3147 f
"Supported types: int, float, bool, str, vec2, vec3, vec4, int2, int3, int4, "
3148 f
"'uint', 'double', list (for vec3), 'list_vec2', 'list_vec4', 'list_int2', 'list_int3', 'list_int4'")
3152 Check if primitive data exists for a specific primitive and label.
3155 uuid: UUID of the primitive
3156 label: String key for the data
3159 True if the data exists, False otherwise
3161 return context_wrapper.doesPrimitiveDataExistWrapper(self.
context, uuid, label)
3165 Convenience method to get float primitive data.
3168 uuid: UUID of the primitive
3169 label: String key for the data
3172 Float value stored for the primitive
3178 Get the Helios data type of primitive data.
3181 uuid: UUID of the primitive
3182 label: String key for the data
3185 HeliosDataType enum value as integer
3187 return context_wrapper.getPrimitiveDataTypeWrapper(self.
context, uuid, label)
3191 Get the size/length of primitive data (for vector data).
3194 uuid: UUID of the primitive
3195 label: String key for the data
3198 Size of data array, or 1 for scalar data
3200 return context_wrapper.getPrimitiveDataSizeWrapper(self.
context, uuid, label)
3203 """Raise if any of ``uuids`` lacks primitive data ``label``.
3205 Costs one native call per primitive, so call it only where the read that
3206 follows is itself per-primitive, or to diagnose a failure that has already
3210 ValueError: naming the first primitive that lacks the data
3214 raise ValueError(f
"Primitive data '{label}' does not exist for UUID {uuid}")
3218 Get primitive data values for multiple primitives as a NumPy array.
3220 This method retrieves primitive data for a list of UUIDs and returns the values
3221 as a NumPy array. The output array has the same length as the input UUID list,
3222 with each index corresponding to the primitive data value for that UUID.
3225 uuids: List of primitive UUIDs to get data for
3226 label: String key for the primitive data to retrieve
3229 NumPy array of primitive data values corresponding to each UUID.
3230 The array type depends on the data type:
3231 - int data: int32 array
3232 - uint data: uint32 array
3233 - float data: float32 array
3234 - double data: float64 array
3235 - vector data: float32 array with shape (N, vector_size)
3236 - string data: object array of strings
3239 ValueError: If UUID list is empty or UUIDs don't exist
3240 RuntimeError: If context is in mock mode or data doesn't exist for some UUIDs
3245 raise ValueError(
"UUID list cannot be empty")
3254 first_uuid = uuids[0]
3256 raise ValueError(f
"Primitive data '{label}' does not exist for UUID {first_uuid}")
3267 result = context_wrapper.getPrimitiveDataFloatArray(
3277 elif data_type
in _BULK_PRIMITIVE_DATA_TYPES:
3282 result = context_wrapper.getPrimitiveDataArrayBulk(
3283 self.
context, uuids, label, data_type)
3288 elif data_type == 10:
3292 result = context_wrapper.getPrimitiveDataStringArrayBulk(
3299 raise ValueError(f
"Unsupported primitive data type: {data_type}")
3305 colormap: str =
"hot", ncolors: int = 10,
3306 max_val: Optional[float] =
None, min_val: Optional[float] =
None):
3308 Color primitives based on primitive data values using pseudocolor mapping.
3310 This method applies a pseudocolor mapping to primitives based on the values
3311 of specified primitive data. The primitive colors are updated to reflect the
3312 data values using a color map.
3315 uuids: List of primitive UUIDs to color
3316 primitive_data: Name of primitive data to use for coloring (e.g., "radiation_flux_SW")
3317 colormap: Color map name: "hot", "cool", "lava", "rainbow", "parula", "gray",
3318 "green", "lines" or "algae" (see :meth:`getColormapNames`)
3319 ncolors: Number of discrete colors in color map (default: 10). Must be at least 2.
3320 max_val: Maximum value for color scale (auto-determined if None)
3321 min_val: Minimum value for color scale (auto-determined if None)
3323 If all primitives have the same data value (or ``min_val`` equals ``max_val``),
3324 they are all given the first color of the colormap.
3326 if max_val
is not None and min_val
is not None:
3327 context_wrapper.colorPrimitiveByDataPseudocolorWithRange(
3328 self.
context, uuids, primitive_data, colormap, ncolors, max_val, min_val)
3330 context_wrapper.colorPrimitiveByDataPseudocolor(
3331 self.
context, uuids, primitive_data, colormap, ncolors)
3334 def setTime(self, hour: int, minute: int = 0, second: int = 0):
3336 Set the simulation time.
3340 minute: Minute (0-59), defaults to 0
3341 second: Second (0-59), defaults to 0
3344 ValueError: If time values are out of range
3345 NotImplementedError: If time/date functions not available in current library build
3348 >>> context.setTime(14, 30) # Set to 2:30 PM
3349 >>> context.setTime(9, 15, 30) # Set to 9:15:30 AM
3351 context_wrapper.setTime(self.
context, hour, minute, second)
3353 def setDate(self, year: int, month: int, day: int):
3355 Set the simulation date.
3358 year: Year (1900-3000)
3363 ValueError: If date values are out of range
3364 NotImplementedError: If time/date functions not available in current library build
3367 >>> context.setDate(2023, 6, 21) # Set to June 21, 2023
3369 context_wrapper.setDate(self.
context, year, month, day)
3373 Set the simulation date using Julian day number.
3376 julian_day: Julian day (1-366)
3377 year: Year (1900-3000)
3380 ValueError: If values are out of range
3381 NotImplementedError: If time/date functions not available in current library build
3384 >>> context.setDateJulian(172, 2023) # Set to day 172 of 2023 (June 21)
3386 context_wrapper.setDateJulian(self.
context, julian_day, year)
3390 Get the current simulation time.
3393 Tuple of (hour, minute, second) as integers
3396 NotImplementedError: If time/date functions not available in current library build
3399 >>> hour, minute, second = context.getTime()
3400 >>> print(f"Current time: {hour:02d}:{minute:02d}:{second:02d}")
3406 Get the current simulation date.
3409 Tuple of (year, month, day) as integers
3412 NotImplementedError: If time/date functions not available in current library build
3415 >>> year, month, day = context.getDate()
3416 >>> print(f"Current date: {year}-{month:02d}-{day:02d}")
3424 def addTimeseriesData(self, label: str, value: float, date:
'Date', time:
'Time'):
3426 Add a data point to a timeseries variable.
3429 label: Name of the timeseries variable (e.g., "temperature")
3430 value: Value of the data point
3431 date (Date): Date of the data point
3432 time: Time of the data point
3435 ValueError: If label is empty, or date/time are wrong types
3436 NotImplementedError: If timeseries functions not available
3439 >>> from pyhelios.types import Date, Time
3440 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3443 if not isinstance(label, str)
or not label:
3444 raise ValueError(
"Label must be a non-empty string")
3445 if not isinstance(date, Date):
3446 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3447 if not isinstance(time, Time):
3448 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3450 context_wrapper.addTimeseriesData(
3451 self.
context, label, float(value),
3452 date.day, date.month, date.year,
3453 time.hour, time.minute, time.second
3458 Update the value of an existing timeseries data point.
3461 label: Name of the timeseries variable (must already exist)
3462 date (Date): Date of the existing point (must match exactly)
3463 time: Time of the existing point (must match exactly)
3464 new_value: Replacement value
3467 ValueError: If label is empty, or date/time are wrong types
3468 HeliosRuntimeError: If the variable does not exist or no point matches the (date, time)
3469 NotImplementedError: If timeseries functions not available
3472 >>> from pyhelios.types import Date, Time
3473 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3474 >>> context.updateTimeseriesData("temperature", Date(2024, 6, 15), Time(12, 0, 0), 26.5)
3477 if not isinstance(label, str)
or not label:
3478 raise ValueError(
"Label must be a non-empty string")
3479 if not isinstance(date, Date):
3480 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3481 if not isinstance(time, Time):
3482 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3484 context_wrapper.updateTimeseriesData(
3486 date.day, date.month, date.year,
3487 time.hour, time.minute, time.second,
3493 Set the Context date and time from a timeseries data point index.
3496 label: Name of the timeseries variable
3497 index: Index of the data point (0 = earliest, chronologically ordered)
3500 ValueError: If label is empty or index is negative
3501 NotImplementedError: If timeseries functions not available
3504 >>> context.setCurrentTimeseriesPoint("temperature", 0)
3507 if not isinstance(label, str)
or not label:
3508 raise ValueError(
"Label must be a non-empty string")
3509 if not isinstance(index, int)
or index < 0:
3510 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3512 context_wrapper.setCurrentTimeseriesPoint(self.
context, label, index)
3515 index: int =
None) -> float:
3517 Query a timeseries data value.
3519 Three modes of operation:
3520 - With date and time: returns interpolated value at the specified date/time
3521 - With index: returns value at the specified data point index
3522 - With neither: returns value at the current Context date/time
3525 label: Name of the timeseries variable
3526 date (Date): Date to query at (requires time as well)
3527 time: Time to query at (requires date as well)
3528 index: Index of the data point (0 = earliest)
3531 The timeseries value as a float
3534 ValueError: If both date/time and index are provided, or if date without time
3535 NotImplementedError: If timeseries functions not available
3538 >>> # Query at specific date/time
3539 >>> val = context.queryTimeseriesData("temperature", date=Date(2024, 6, 15), time=Time(12, 0, 0))
3540 >>> # Query by index
3541 >>> val = context.queryTimeseriesData("temperature", index=0)
3542 >>> # Query at current context time
3543 >>> val = context.queryTimeseriesData("temperature")
3546 if not isinstance(label, str)
or not label:
3547 raise ValueError(
"Label must be a non-empty string")
3549 has_datetime = date
is not None or time
is not None
3550 has_index = index
is not None
3552 if has_datetime
and has_index:
3553 raise ValueError(
"Cannot specify both date/time and index. Use one or the other.")
3556 if date
is None or time
is None:
3557 raise ValueError(
"Both date and time must be provided together")
3558 if not isinstance(date, Date):
3559 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3560 if not isinstance(time, Time):
3561 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3562 return context_wrapper.queryTimeseriesDataDateTime(
3564 date.day, date.month, date.year,
3565 time.hour, time.minute, time.second
3569 if not isinstance(index, int)
or index < 0:
3570 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3571 return context_wrapper.queryTimeseriesDataIndex(self.
context, label, index)
3573 return context_wrapper.queryTimeseriesDataCurrent(self.
context, label)
3577 Get the Time associated with a timeseries data point.
3580 label: Name of the timeseries variable
3581 index: Index of the data point (0 = earliest)
3584 Time object for the data point
3587 ValueError: If label is empty or index is negative
3588 NotImplementedError: If timeseries functions not available
3591 >>> t = context.queryTimeseriesTime("temperature", 0)
3592 >>> print(f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}")
3595 if not isinstance(label, str)
or not label:
3596 raise ValueError(
"Label must be a non-empty string")
3597 if not isinstance(index, int)
or index < 0:
3598 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3600 hour, minute, second = context_wrapper.queryTimeseriesTime(self.
context, label, index)
3601 return Time(hour=hour, minute=minute, second=second)
3605 Get the Date associated with a timeseries data point.
3608 label: Name of the timeseries variable
3609 index: Index of the data point (0 = earliest)
3612 Date object for the data point
3615 ValueError: If label is empty or index is negative
3616 NotImplementedError: If timeseries functions not available
3619 >>> d = context.queryTimeseriesDate("temperature", 0)
3620 >>> print(f"{d.year}-{d.month:02d}-{d.day:02d}")
3623 if not isinstance(label, str)
or not label:
3624 raise ValueError(
"Label must be a non-empty string")
3625 if not isinstance(index, int)
or index < 0:
3626 raise ValueError(f
"Index must be a non-negative integer, got {index}")
3628 year, month, day = context_wrapper.queryTimeseriesDate(self.
context, label, index)
3629 return Date(year=year, month=month, day=day)
3633 Get the number of data points in a timeseries variable.
3636 label: Name of the timeseries variable
3639 Number of data points
3642 ValueError: If label is empty
3643 NotImplementedError: If timeseries functions not available
3646 >>> n = context.getTimeseriesLength("temperature")
3647 >>> print(f"Timeseries has {n} data points")
3650 if not isinstance(label, str)
or not label:
3651 raise ValueError(
"Label must be a non-empty string")
3653 return context_wrapper.getTimeseriesLength(self.
context, label)
3657 Check whether a timeseries variable exists.
3660 label: Name of the timeseries variable
3663 True if the variable exists, False otherwise
3666 ValueError: If label is empty
3667 NotImplementedError: If timeseries functions not available
3670 >>> if context.doesTimeseriesVariableExist("temperature"):
3671 ... print("Temperature data loaded")
3674 if not isinstance(label, str)
or not label:
3675 raise ValueError(
"Label must be a non-empty string")
3677 return context_wrapper.doesTimeseriesVariableExist(self.
context, label)
3681 List all existing timeseries variables.
3684 List of timeseries variable names
3687 NotImplementedError: If timeseries functions not available
3690 >>> variables = context.listTimeseriesVariables()
3691 >>> for var in variables:
3692 ... print(f" {var}: {context.getTimeseriesLength(var)} points")
3696 return context_wrapper.listTimeseriesVariables(self.
context)
3699 """Clear all timeseries data from the Context.
3701 Removes all timeseries variables and their associated date/time values.
3704 NotImplementedError: If timeseries functions not available
3707 >>> context.clearTimeseriesData()
3708 >>> context.listTimeseriesVariables()
3712 context_wrapper.clearTimeseriesData(self.
context)
3715 """Delete a single timeseries variable and all of its data points.
3717 Complements :meth:`clearTimeseriesData` (which removes all variables) and
3718 :meth:`updateTimeseriesData` (which modifies a single point).
3721 label: Name of the timeseries variable to delete.
3724 ValueError: If ``label`` is empty.
3725 NotImplementedError: If running against helios-core older than v1.3.72.
3728 If the variable does not exist, the underlying Helios API issues a
3729 non-fatal warning to stderr and the call is otherwise a no-op.
3732 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3733 >>> context.deleteTimeseriesVariable("temperature")
3734 >>> context.doesTimeseriesVariableExist("temperature")
3738 if not isinstance(label, str)
or not label:
3739 raise ValueError(
"Label must be a non-empty string")
3740 context_wrapper.deleteTimeseriesVariable(self.
context, label)
3743 """Delete a single timeseries data point at the given date and time.
3745 If ``label`` is provided, only that variable's matching point is removed. If ``label``
3746 is omitted (None), the matching point is removed from every timeseries variable.
3749 date (Date): Date of the data point to delete.
3750 time: Time of the data point to delete.
3751 label: Optional name of the timeseries variable. None applies to all variables.
3754 ValueError: If date/time are wrong types, or label is an empty string.
3755 NotImplementedError: If running against helios-core older than v1.3.73.
3758 If no matching data point exists, the underlying Helios API issues a non-fatal
3759 warning to stderr and the call is otherwise a no-op. Matching uses the same
3760 (date, time) encoding as :meth:`addTimeseriesData`.
3763 >>> from pyhelios.types import Date, Time
3764 >>> context.deleteTimeseriesDataPoint(Date(2024, 6, 15), Time(12, 0, 0), "temperature")
3767 if not isinstance(date, Date):
3768 raise ValueError(f
"date must be a Date instance, got {type(date).__name__}")
3769 if not isinstance(time, Time):
3770 raise ValueError(f
"time must be a Time instance, got {type(time).__name__}")
3771 if label
is not None and (
not isinstance(label, str)
or not label):
3772 raise ValueError(
"label must be a non-empty string or None")
3775 context_wrapper.deleteTimeseriesDataPointAll(
3777 date.day, date.month, date.year,
3778 time.hour, time.minute, time.second
3781 context_wrapper.deleteTimeseriesDataPoint(
3783 date.day, date.month, date.year,
3784 time.hour, time.minute, time.second
3788 delimiter: str =
",", date_string_format: str =
"YYYYMMDD",
3789 headerlines: int = 0):
3791 Load tabular timeseries data from a text file.
3793 The file should contain columns of data with dates/times and measured values.
3794 Column labels specify how each column should be interpreted. Special labels
3795 include "year", "DOY", "date", "datetime", "hour", "minute", "second", "time".
3796 Other labels become timeseries variable names.
3799 data_file: Path to the text file containing tabular data
3800 column_labels: List of column label strings specifying what each column contains
3801 delimiter: Column delimiter string (default: ",")
3802 date_string_format: Format of date strings in the file. Supported formats:
3803 "YYYYMMDD", "YYYYMMDDHH", "YYYYMMDDHHMM", "DD/MM/YYYY",
3804 "MM/DD/YYYY", "DDMMYYYY", "YYYY-MM-DD", "DD/MM/YYYY HH:MM",
3805 "MM/DD/YYYY HH:MM", "ISO8601" (default: "YYYYMMDD")
3806 headerlines: Number of header lines to skip (default: 0)
3809 ValueError: If data_file is empty, column_labels is empty, or delimiter is empty
3810 RuntimeError: If the file cannot be read or parsed
3811 NotImplementedError: If timeseries functions not available
3814 >>> context.loadTabularTimeseriesData(
3815 ... "weather_data.csv",
3816 ... column_labels=["date", "hour", "temperature", "humidity"],
3820 >>> temp = context.queryTimeseriesData("temperature", index=0)
3823 if not isinstance(data_file, str)
or not data_file:
3824 raise ValueError(
"data_file must be a non-empty string")
3825 if not isinstance(column_labels, list)
or not column_labels:
3826 raise ValueError(
"column_labels must be a non-empty list of strings")
3827 for i, label
in enumerate(column_labels):
3828 if not isinstance(label, str):
3829 raise ValueError(f
"column_labels[{i}] must be a string, got {type(label).__name__}")
3830 if not isinstance(delimiter, str)
or not delimiter:
3831 raise ValueError(
"delimiter must be a non-empty string")
3833 context_wrapper.loadTabularTimeseriesData(
3834 self.
context, data_file, column_labels, delimiter,
3835 date_string_format, headerlines
3842 def deletePrimitive(self, uuids_or_uuid: Union[int, List[int]]) ->
None:
3844 Delete one or more primitives from the context.
3846 This removes the primitive(s) entirely from the context. If a primitive
3847 belongs to a compound object, it will be removed from that object. If the
3848 object becomes empty after removal, it is automatically deleted.
3851 uuids_or_uuid: Single UUID (int) or list of UUIDs to delete
3854 RuntimeError: If any UUID doesn't exist in the context
3855 ValueError: If UUID is invalid (negative)
3856 NotImplementedError: If delete functions not available in current library build
3859 >>> context = Context()
3860 >>> patch_id = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
3861 >>> context.deletePrimitive(patch_id) # Single deletion
3863 >>> # Multiple deletion
3864 >>> ids = [context.addPatch() for _ in range(5)]
3865 >>> context.deletePrimitive(ids) # Delete all at once
3869 if isinstance(uuids_or_uuid, (list, tuple)):
3870 for uuid
in uuids_or_uuid:
3872 raise ValueError(f
"UUID must be non-negative, got {uuid}")
3873 context_wrapper.deletePrimitives(self.
context, list(uuids_or_uuid))
3875 if uuids_or_uuid < 0:
3876 raise ValueError(f
"UUID must be non-negative, got {uuids_or_uuid}")
3877 context_wrapper.deletePrimitive(self.
context, uuids_or_uuid)
3879 def deleteObject(self, objIDs_or_objID: Union[int, List[int]]) ->
None:
3881 Delete one or more compound objects from the context.
3883 This removes the compound object(s) AND all their child primitives.
3884 Use this when you want to delete an entire object hierarchy at once.
3887 objIDs_or_objID: Single object ID (int) or list of object IDs to delete
3890 RuntimeError: If any object ID doesn't exist in the context
3891 ValueError: If object ID is invalid (negative)
3892 NotImplementedError: If delete functions not available in current library build
3895 >>> context = Context()
3896 >>> # Create a compound object (e.g., a tile with multiple patches)
3897 >>> patch_ids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2),
3898 ... tile_divisions=int2(2, 2))
3899 >>> obj_id = context.getPrimitiveParentObjectID(patch_ids[0])
3900 >>> context.deleteObject(obj_id) # Deletes tile and all its patches
3904 if isinstance(objIDs_or_objID, (list, tuple)):
3905 for objID
in objIDs_or_objID:
3907 raise ValueError(f
"Object ID must be non-negative, got {objID}")
3908 context_wrapper.deleteObjects(self.
context, list(objIDs_or_objID))
3910 if objIDs_or_objID < 0:
3911 raise ValueError(f
"Object ID must be non-negative, got {objIDs_or_objID}")
3912 context_wrapper.deleteObject(self.
context, objIDs_or_objID)
3917 Get list of available plugins for this PyHelios instance.
3920 List of available plugin names
3926 Check if a specific plugin is available.
3929 plugin_name: Name of the plugin to check
3932 True if plugin is available, False otherwise
3938 Get detailed information about available plugin capabilities.
3941 Dictionary mapping plugin names to capability information
3946 """Print detailed plugin status information."""
3951 Get list of requested plugins that are not available.
3954 requested_plugins: List of plugin names to check
3957 List of missing plugin names
3967 Create a new material for sharing visual properties across primitives.
3969 Materials enable efficient memory usage by allowing multiple primitives to
3970 share rendering properties. Changes to a material affect all primitives using it.
3973 material_label: Unique label for the material
3976 RuntimeError: If material label already exists
3979 >>> context.addMaterial("wood_oak")
3980 >>> context.setMaterialColor("wood_oak", (0.6, 0.4, 0.2, 1.0))
3981 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3983 context_wrapper.addMaterial(self.
context, material_label)
3986 """Check if a material with the given label exists."""
3987 return context_wrapper.doesMaterialExist(self.
context, material_label)
3990 """Get list of all material labels in the context."""
3991 return context_wrapper.listMaterials(self.
context)
3995 Delete a material from the context.
3997 Primitives using this material will be reassigned to the default material.
4000 material_label: Label of the material to delete
4003 RuntimeError: If material doesn't exist
4005 context_wrapper.deleteMaterial(self.
context, material_label)
4009 Get the RGBA color of a material.
4012 material_label: Label of the material
4018 RuntimeError: If material doesn't exist
4020 from .wrappers.DataTypes
import RGBAcolor
4021 color_list = context_wrapper.getMaterialColor(self.
context, material_label)
4022 return RGBAcolor(color_list[0], color_list[1], color_list[2], color_list[3])
4026 Set the RGBA color of a material.
4028 This affects all primitives that reference this material.
4031 material_label: Label of the material
4032 color: RGBAcolor object or tuple/list of (r, g, b, a) values
4035 RuntimeError: If material doesn't exist
4038 >>> from pyhelios.types import RGBAcolor
4039 >>> context.setMaterialColor("wood", RGBAcolor(0.6, 0.4, 0.2, 1.0))
4040 >>> context.setMaterialColor("wood", (0.6, 0.4, 0.2, 1.0))
4042 if isinstance(color, RGBAcolor):
4043 r, g, b, a = color.r, color.g, color.b, color.a
4044 elif isinstance(color, (list, tuple))
and len(color) == 4:
4045 r, g, b, a = color[0], color[1], color[2], color[3]
4047 raise ValueError(f
"Color must be an RGBAcolor or a 4-element list/tuple, got {type(color).__name__}")
4048 context_wrapper.setMaterialColor(self.
context, material_label, r, g, b, a)
4052 Get the texture file path for a material.
4055 material_label: Label of the material
4058 Texture file path, or empty string if no texture
4061 RuntimeError: If material doesn't exist
4063 return context_wrapper.getMaterialTexture(self.
context, material_label)
4067 Set the texture file for a material.
4069 This affects all primitives that reference this material.
4072 material_label: Label of the material
4073 texture_file: Path to texture image file
4076 RuntimeError: If material doesn't exist or texture file not found
4078 context_wrapper.setMaterialTexture(self.
context, material_label, texture_file)
4081 """Check if material texture color is overridden by material color."""
4082 return context_wrapper.isMaterialTextureColorOverridden(self.
context, material_label)
4085 """Set whether material color overrides texture color."""
4086 context_wrapper.setMaterialTextureColorOverride(self.
context, material_label, override)
4089 """Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
4090 return context_wrapper.getMaterialTwosidedFlag(self.
context, material_label)
4093 """Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
4094 context_wrapper.setMaterialTwosidedFlag(self.
context, material_label, twosided_flag)
4098 Assign a material to primitive(s).
4101 uuid: Single UUID (int) or list of UUIDs (List[int])
4102 material_label: Label of the material to assign
4105 RuntimeError: If primitive or material doesn't exist
4108 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
4109 >>> context.assignMaterialToPrimitive([uuid1, uuid2, uuid3], "wood_oak")
4111 if isinstance(uuid, (list, tuple)):
4112 context_wrapper.assignMaterialToPrimitives(self.
context, uuid, material_label)
4114 context_wrapper.assignMaterialToPrimitive(self.
context, uuid, material_label)
4118 Assign a material to all primitives in compound object(s).
4121 objID: Single object ID (int) or list of object IDs (List[int])
4122 material_label: Label of the material to assign
4125 RuntimeError: If object or material doesn't exist
4128 >>> tree_id = wpt.buildTree(WPTType.LEMON)
4129 >>> context.assignMaterialToObject(tree_id, "tree_bark")
4130 >>> context.assignMaterialToObject([id1, id2], "grass")
4132 if isinstance(objID, (list, tuple)):
4133 context_wrapper.assignMaterialToObjects(self.
context, objID, material_label)
4135 context_wrapper.assignMaterialToObject(self.
context, objID, material_label)
4138 """Get the material label assigned to 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 RuntimeError: If primitive doesn't exist
4149 if isinstance(uuid, (list, tuple)):
4153 ptr, offsets, total = context_wrapper.getBatchPrimitiveMaterialLabels(self.
context, uuid)
4154 if total == 0
or not ptr:
4155 return [
"" for _
in uuid]
4156 full_str = ptr.decode(
'utf-8')
if isinstance(ptr, bytes)
else ptr
4157 return [full_str[offsets[i]:offsets[i+1]]
for i
in range(len(uuid))]
4158 return context_wrapper.getPrimitiveMaterialLabel(self.
context, uuid)
4162 Get two-sided rendering flag for a primitive.
4164 Checks material first, then primitive data if no material assigned.
4167 uuid: UUID of the primitive
4168 default_value: Default value if no material/data (default 1 = two-sided)
4171 Two-sided flag (0 = one-sided, 1 = two-sided)
4173 return context_wrapper.getPrimitiveTwosidedFlag(self.
context, uuid, default_value)
4177 Get all primitive UUIDs that use a specific material.
4180 material_label: Label of the material
4183 List of primitive UUIDs using the material
4186 RuntimeError: If material doesn't exist
4188 return context_wrapper.getPrimitivesUsingMaterial(self.
context, material_label)
4195 """Get the texture file path of a primitive or multiple primitives.
4198 uuid: Single UUID (int) or list of UUIDs
4201 str for single UUID, or List[str] for list
4204 if isinstance(uuid, (list, tuple)):
4207 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureFiles(self.
context, uuid)
4208 if total == 0
or not ptr:
4209 return [
"" for _
in uuid]
4210 full_str = ptr.decode(
'utf-8')
if isinstance(ptr, bytes)
else ptr
4211 return [full_str[offsets[i]:offsets[i+1]]
for i
in range(len(uuid))]
4212 return context_wrapper.getPrimitiveTextureFile(self.
context, uuid)
4215 """Resolve material texture suppression for export.
4217 For each primitive, applies material-based texture suppression rules:
4218 1. If primitive has texture but material has no texture -> suppress texture, use material color
4219 2. If both have texture and textureColorOverride -> prefix "mask:", use material color
4220 3. Otherwise -> leave unchanged
4223 uuids: List of primitive UUIDs
4224 colors_np: numpy float32 array of shape (N, 3), modified IN-PLACE
4227 List[str] of resolved texture file paths
4232 return context_wrapper.resolveMaterialTextures(self.
context, uuids, colors_np)
4235 """Pack GPU-ready geometry buffers for a set of primitives in a single C++ pass.
4237 Produces a binary blob containing contiguous typed arrays (positions,
4238 colors, uvs, indices, faceToUuid) grouped by texture, ready for
4239 zero-copy loading into Three.js BufferGeometry attributes.
4242 uuids: List of primitive UUIDs
4245 bytes: Binary blob of the typed arrays described above
4250 return context_wrapper.packGPUBuffers(self.
context, uuids)
4253 """Set the texture file path of a primitive.
4256 uuid: UUID of the primitive
4257 texture_file: Path to the texture file
4260 context_wrapper.setPrimitiveTextureFile(self.
context, uuid, texture_file)
4263 """Get the texture size (width, height) of a primitive.
4266 uuid: UUID of the primitive
4269 int2 with width and height of the texture
4272 w, h = context_wrapper.getPrimitiveTextureSize(self.
context, uuid)
4276 """Get the texture UV coordinates of a primitive or multiple primitives.
4279 uuid: Single UUID (int) or list of UUIDs
4282 List[vec2] for single UUID, or tuple of (flat_data, offsets) for list
4285 if isinstance(uuid, (list, tuple)):
4287 return (np.empty((0,), dtype=np.float32), np.zeros((1,), dtype=np.uint32))
4288 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureUV(self.
context, uuid)
4289 offsets_arr = np.asarray(offsets, dtype=np.uint32)
4290 if total == 0
or not ptr:
4291 return (np.empty((0,), dtype=np.float32), offsets_arr)
4292 data = np.ctypeslib.as_array(ptr, shape=(total,)).copy()
4293 return (data, offsets_arr)
4294 uv_pairs = context_wrapper.getPrimitiveTextureUV(self.
context, uuid)
4295 return [
vec2(u, v)
for u, v
in uv_pairs]
4298 """Check if primitive texture has a transparency channel.
4301 uuid: UUID of the primitive
4304 True if texture has transparency channel
4307 return context_wrapper.primitiveTextureHasTransparencyChannel(self.
context, uuid)
4310 """Get the solid fraction of a primitive or multiple primitives.
4313 uuid: Single UUID (int) or list of UUIDs
4316 float for single UUID, or np.ndarray of shape (N,) for list
4319 if isinstance(uuid, (list, tuple)):
4321 return np.empty((0,), dtype=np.float32)
4322 ptr, size = context_wrapper.getBatchPrimitiveSolidFractions(self.
context, uuid)
4323 if size == 0
or not ptr:
4324 return np.empty((0,), dtype=np.float32)
4325 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
4326 return context_wrapper.getPrimitiveSolidFraction(self.
context, uuid)
4329 """Override texture color with the primitive's constant RGB color.
4332 uuids_or_uuid: A single UUID (int) or a list of UUIDs. When a list is
4333 given, the override is applied to all of them in a single bulk call.
4336 if isinstance(uuids_or_uuid, (list, tuple)):
4337 context_wrapper.overridePrimitiveTextureColorBatchWrapper(self.
context, list(uuids_or_uuid))
4339 context_wrapper.overridePrimitiveTextureColor(self.
context, uuids_or_uuid)
4342 """Use texture-map color instead of the constant RGB color.
4345 uuids_or_uuid: A single UUID (int) or a list of UUIDs. When a list is
4346 given, all of them are restored in a single bulk call.
4349 if isinstance(uuids_or_uuid, (list, tuple)):
4350 context_wrapper.usePrimitiveTextureColorBatchWrapper(self.
context, list(uuids_or_uuid))
4352 context_wrapper.usePrimitiveTextureColor(self.
context, uuids_or_uuid)
4355 """Check if primitive texture color is overridden.
4358 uuid: UUID of the primitive
4361 True if texture color is overridden with constant RGB
4364 return context_wrapper.isPrimitiveTextureColorOverridden(self.
context, uuid)
4371 """Get normals for all primitives. Returns ndarray of shape (N, 3)."""
4375 """Get colors for all primitives. Returns ndarray of shape (N, 3)."""
4379 """Get areas for all primitives. Returns ndarray of shape (N,)."""
4383 """Get types for all primitives. Returns ndarray of shape (N,) uint32."""
4387 """Get solid fractions for all primitives. Returns ndarray of shape (N,)."""
4391 """Get vertices for all primitives. Returns (flat_data, offsets) tuple."""
4395 """Get texture files for all primitives. Returns list of strings."""
4399 """Get material labels for all primitives. Returns list of strings."""
4405 """Hide one or more primitives. Hidden primitives are excluded from getAllUUIDs().
4408 uuids_or_uuid: Single UUID (int) or list of UUIDs to hide.
4410 if isinstance(uuids_or_uuid, (list, tuple)):
4411 context_wrapper.hidePrimitivesWrapper(self.
context, list(uuids_or_uuid))
4413 context_wrapper.hidePrimitiveWrapper(self.
context, uuids_or_uuid)
4416 """Show one or more previously hidden primitives.
4419 uuids_or_uuid: Single UUID (int) or list of UUIDs to show.
4421 if isinstance(uuids_or_uuid, (list, tuple)):
4422 context_wrapper.showPrimitivesWrapper(self.
context, list(uuids_or_uuid))
4424 context_wrapper.showPrimitiveWrapper(self.
context, uuids_or_uuid)
4427 """Check if a primitive is hidden.
4430 uuid: UUID of the primitive.
4433 True if the primitive is hidden.
4435 return context_wrapper.isPrimitiveHiddenWrapper(self.
context, uuid)
4438 """Hide one or more compound objects (and all their primitives).
4441 objids_or_objid: Single object ID (int) or list of object IDs to hide.
4443 if isinstance(objids_or_objid, (list, tuple)):
4444 context_wrapper.hideObjectsWrapper(self.
context, list(objids_or_objid))
4446 context_wrapper.hideObjectWrapper(self.
context, objids_or_objid)
4448 def showObject(self, objids_or_objid) -> None:
4449 """Show one or more previously hidden compound objects.
4452 objids_or_objid: Single object ID (int) or list of object IDs to show.
4454 if isinstance(objids_or_objid, (list, tuple)):
4455 context_wrapper.showObjectsWrapper(self.
context, list(objids_or_objid))
4457 context_wrapper.showObjectWrapper(self.
context, objids_or_objid)
4460 """Check if a compound object is hidden.
4466 True if the object is hidden.
4468 return context_wrapper.isObjectHiddenWrapper(self.
context, objID)
4472 def setObjectDataInt(self, objids_or_objid, label: str, value: int) ->
None:
4473 """Set object data as signed 32-bit integer. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4474 if isinstance(objids_or_objid, (list, tuple)):
4475 if isinstance(value, (list, tuple, np.ndarray)):
4476 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int', value)
4478 context_wrapper.setBroadcastObjectDataInt(self.
context, objids_or_objid, label, value)
4480 context_wrapper.setObjectDataInt(self.
context, objids_or_objid, label, value)
4483 """Set object data as unsigned 32-bit integer. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4484 if isinstance(objids_or_objid, (list, tuple)):
4485 if isinstance(value, (list, tuple, np.ndarray)):
4486 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'UInt', value)
4488 context_wrapper.setBroadcastObjectDataUInt(self.
context, objids_or_objid, label, value)
4490 context_wrapper.setObjectDataUInt(self.
context, objids_or_objid, label, value)
4493 """Set object data as 32-bit float. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4494 if isinstance(objids_or_objid, (list, tuple)):
4495 if isinstance(value, (list, tuple, np.ndarray)):
4496 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Float', value)
4498 context_wrapper.setBroadcastObjectDataFloat(self.
context, objids_or_objid, label, value)
4500 context_wrapper.setObjectDataFloat(self.
context, objids_or_objid, label, value)
4503 """Set object data as 64-bit double. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4504 if isinstance(objids_or_objid, (list, tuple)):
4505 if isinstance(value, (list, tuple, np.ndarray)):
4506 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Double', value)
4508 context_wrapper.setBroadcastObjectDataDouble(self.
context, objids_or_objid, label, value)
4510 context_wrapper.setObjectDataDouble(self.
context, objids_or_objid, label, value)
4513 """Set object data as string. Scalar broadcasts to all objIDs; a list of strings sets a distinct value per objID."""
4514 if isinstance(objids_or_objid, (list, tuple)):
4515 if isinstance(value, (list, tuple, np.ndarray)):
4516 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'String', value)
4518 context_wrapper.setBroadcastObjectDataString(self.
context, objids_or_objid, label, value)
4520 context_wrapper.setObjectDataString(self.
context, objids_or_objid, label, value)
4522 def setObjectDataVec2(self, objids_or_objid, label: str, x_or_vec, y: float =
None) ->
None:
4523 """Set object data as vec2. Accepts a vec2 / x,y components, or a list of vec2 (one per objID)."""
4524 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4525 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Vec2', x_or_vec)
4527 if hasattr(x_or_vec,
'x')
and y
is None:
4528 x, y = x_or_vec.x, x_or_vec.y
4531 if isinstance(objids_or_objid, (list, tuple)):
4532 context_wrapper.setBroadcastObjectDataVec2(self.
context, objids_or_objid, label, x, y)
4534 context_wrapper.setObjectDataVec2(self.
context, objids_or_objid, label, x, y)
4536 def setObjectDataVec3(self, objids_or_objid, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
4537 """Set object data as vec3. Accepts a vec3 / x,y,z components, or a list of vec3 (one per objID)."""
4538 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4539 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Vec3', x_or_vec)
4541 if hasattr(x_or_vec,
'x')
and y
is None:
4542 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4545 if isinstance(objids_or_objid, (list, tuple)):
4546 context_wrapper.setBroadcastObjectDataVec3(self.
context, objids_or_objid, label, x, y, z)
4548 context_wrapper.setObjectDataVec3(self.
context, objids_or_objid, label, x, y, z)
4550 def setObjectDataVec4(self, objids_or_objid, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
4551 """Set object data as vec4. Accepts a vec4 / x,y,z,w components, or a list of vec4 (one per objID)."""
4552 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4553 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Vec4', x_or_vec)
4555 if hasattr(x_or_vec,
'x')
and y
is None:
4556 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4559 if isinstance(objids_or_objid, (list, tuple)):
4560 context_wrapper.setBroadcastObjectDataVec4(self.
context, objids_or_objid, label, x, y, z, w)
4562 context_wrapper.setObjectDataVec4(self.
context, objids_or_objid, label, x, y, z, w)
4564 def setObjectDataInt2(self, objids_or_objid, label: str, x_or_vec, y: int =
None) ->
None:
4565 """Set object data as int2. Accepts an int2 / x,y components, or a list of int2 (one per objID)."""
4566 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4567 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int2', x_or_vec)
4569 if hasattr(x_or_vec,
'x')
and y
is None:
4570 x, y = x_or_vec.x, x_or_vec.y
4573 if isinstance(objids_or_objid, (list, tuple)):
4574 context_wrapper.setBroadcastObjectDataInt2(self.
context, objids_or_objid, label, x, y)
4576 context_wrapper.setObjectDataInt2(self.
context, objids_or_objid, label, x, y)
4578 def setObjectDataInt3(self, objids_or_objid, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
4579 """Set object data as int3. Accepts an int3 / x,y,z components, or a list of int3 (one per objID)."""
4580 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4581 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int3', x_or_vec)
4583 if hasattr(x_or_vec,
'x')
and y
is None:
4584 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4587 if isinstance(objids_or_objid, (list, tuple)):
4588 context_wrapper.setBroadcastObjectDataInt3(self.
context, objids_or_objid, label, x, y, z)
4590 context_wrapper.setObjectDataInt3(self.
context, objids_or_objid, label, x, y, z)
4592 def setObjectDataInt4(self, objids_or_objid, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
4593 """Set object data as int4. Accepts an int4 / x,y,z,w components, or a list of int4 (one per objID)."""
4594 if isinstance(objids_or_objid, (list, tuple))
and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4595 context_wrapper.setObjectDataArray(self.
context, objids_or_objid, label,
'Int4', x_or_vec)
4597 if hasattr(x_or_vec,
'x')
and y
is None:
4598 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4601 if isinstance(objids_or_objid, (list, tuple)):
4602 context_wrapper.setBroadcastObjectDataInt4(self.
context, objids_or_objid, label, x, y, z, w)
4604 context_wrapper.setObjectDataInt4(self.
context, objids_or_objid, label, x, y, z, w)
4606 def getObjectData(self, objID: int, label: str, data_type: type =
None):
4607 """Get object data with optional type specification. Auto-detects type if not specified."""
4608 if data_type
is None:
4609 return context_wrapper.getObjectDataAuto(self.
context, objID, label)
4610 if data_type == int:
4611 return context_wrapper.getObjectDataInt(self.
context, objID, label)
4612 elif data_type == float:
4613 return context_wrapper.getObjectDataFloat(self.
context, objID, label)
4614 elif data_type == str:
4615 return context_wrapper.getObjectDataString(self.
context, objID, label)
4616 elif data_type == vec3:
4617 coords = context_wrapper.getObjectDataVec3(self.
context, objID, label)
4618 return vec3(coords[0], coords[1], coords[2])
4619 elif data_type == vec2:
4620 coords = context_wrapper.getObjectDataVec2(self.
context, objID, label)
4621 return vec2(coords[0], coords[1])
4622 elif data_type == vec4:
4623 coords = context_wrapper.getObjectDataVec4(self.
context, objID, label)
4624 return vec4(coords[0], coords[1], coords[2], coords[3])
4625 elif data_type == int2:
4626 coords = context_wrapper.getObjectDataInt2(self.
context, objID, label)
4627 return int2(coords[0], coords[1])
4628 elif data_type == int3:
4629 coords = context_wrapper.getObjectDataInt3(self.
context, objID, label)
4630 return int3(coords[0], coords[1], coords[2])
4631 elif data_type == int4:
4632 coords = context_wrapper.getObjectDataInt4(self.
context, objID, label)
4633 return int4(coords[0], coords[1], coords[2], coords[3])
4634 elif data_type ==
"uint":
4635 return context_wrapper.getObjectDataUInt(self.
context, objID, label)
4636 elif data_type ==
"double":
4637 return context_wrapper.getObjectDataDouble(self.
context, objID, label)
4639 raise ValueError(f
"Unsupported object data type: {data_type}")
4642 """Get float object data."""
4643 return context_wrapper.getObjectDataFloat(self.
context, objID, label)
4646 """Get int object data."""
4647 return context_wrapper.getObjectDataInt(self.
context, objID, label)
4650 """Get string object data."""
4651 return context_wrapper.getObjectDataString(self.
context, objID, label)
4654 """Get the HeliosDataType enum for object data."""
4655 return context_wrapper.getObjectDataTypeWrapper(self.
context, objID, label)
4658 """Get the size of object data array."""
4659 return context_wrapper.getObjectDataSizeWrapper(self.
context, objID, label)
4662 """Check if object data exists."""
4663 return context_wrapper.doesObjectDataExistWrapper(self.
context, objID, label)
4666 """Get object data values for multiple objects as a NumPy array.
4668 Reads one label across every object in a single call, in the order the
4669 IDs were given; much faster than reading objects one at a time.
4672 objids: Object IDs to read, controlling the result order
4673 label: Object data label to retrieve
4676 NumPy array with one entry per object: int32, uint32, float32 or
4677 float64 for the scalar types, and shape (N, components) for the
4678 vec2/3/4 and int2/3/4 types.
4681 ValueError: If the ID list is empty or the label does not exist
4682 NotImplementedError: If the data type has no bulk getter
4685 if not isinstance(objids, (list, tuple)):
4687 f
"objids must be a list of object IDs, got {type(objids).__name__}")
4689 raise ValueError(
"Object ID list cannot be empty")
4694 f
"Object data '{label}' does not exist for object {first}")
4698 return context_wrapper.getObjectDataStringArrayBulk(
4700 return context_wrapper.getObjectDataArrayBulk(
4701 self.
context, objids, label, data_type)
4704 """Clear object data. Accepts single ID or list."""
4705 if isinstance(objids_or_objid, (list, tuple)):
4706 context_wrapper.clearObjectDataBatchWrapper(self.
context, objids_or_objid, label)
4708 context_wrapper.clearObjectDataWrapper(self.
context, objids_or_objid, label)
4711 """Remove a named data field from every compound object in the Context.
4713 Clears the data with the given label from all objects (including hidden ones) and
4714 releases the registered data type for the label, so it may subsequently be
4715 re-registered with a different type. Requires helios-core v1.3.73 or newer.
4718 context_wrapper.clearAllObjectDataByLabelWrapper(self.
context, label)
4721 """List all data labels on a specific object."""
4722 return context_wrapper.listObjectDataWrapper(self.
context, objID)
4725 """List all object data labels in context."""
4726 return context_wrapper.listAllObjectDataLabelsWrapper(self.
context)
4729 """Copy object data to a new label."""
4730 context_wrapper.duplicateObjectDataWrapper(self.
context, objID, old_label, new_label)
4732 def renameObjectData(self, objID: int, old_label: str, new_label: str) ->
None:
4733 """Rename an object data label."""
4734 context_wrapper.renameObjectDataWrapper(self.
context, objID, old_label, new_label)
4736 def filterObjectsByData(self, objIDs: List[int], label: str, value, comparator: str =
"=") -> List[int]:
4737 """Filter objects by data value. Auto-dispatches based on value type."""
4738 if isinstance(value, str):
4739 return context_wrapper.filterObjectsByDataStringWrapper(self.
context, objIDs, label, value)
4740 elif isinstance(value, float):
4741 return context_wrapper.filterObjectsByDataFloatWrapper(self.
context, objIDs, label, value, comparator)
4742 elif isinstance(value, int):
4743 return context_wrapper.filterObjectsByDataIntWrapper(self.
context, objIDs, label, value, comparator)
4745 raise ValueError(f
"Unsupported filter value type: {type(value).__name__}")
4750 """Set global data as signed 32-bit integer."""
4751 context_wrapper.setGlobalDataInt(self.
context, label, value)
4754 """Set global data as unsigned 32-bit integer."""
4755 context_wrapper.setGlobalDataUInt(self.
context, label, value)
4758 """Set global data as 32-bit float."""
4759 context_wrapper.setGlobalDataFloat(self.
context, label, value)
4762 """Set global data as 64-bit double."""
4763 context_wrapper.setGlobalDataDouble(self.
context, label, value)
4766 """Set global data as string."""
4767 context_wrapper.setGlobalDataString(self.
context, label, value)
4770 """Set global data as vec2."""
4771 if hasattr(x_or_vec,
'x')
and y
is None:
4772 x, y = x_or_vec.x, x_or_vec.y
4775 context_wrapper.setGlobalDataVec2(self.
context, label, x, y)
4777 def setGlobalDataVec3(self, label: str, x_or_vec, y: float =
None, z: float =
None) ->
None:
4778 """Set global data as vec3."""
4779 if hasattr(x_or_vec,
'x')
and y
is None:
4780 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4783 context_wrapper.setGlobalDataVec3(self.
context, label, x, y, z)
4785 def setGlobalDataVec4(self, label: str, x_or_vec, y: float =
None, z: float =
None, w: float =
None) ->
None:
4786 """Set global data as vec4."""
4787 if hasattr(x_or_vec,
'x')
and y
is None:
4788 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4791 context_wrapper.setGlobalDataVec4(self.
context, label, x, y, z, w)
4794 """Set global data as int2."""
4795 if hasattr(x_or_vec,
'x')
and y
is None:
4796 x, y = x_or_vec.x, x_or_vec.y
4799 context_wrapper.setGlobalDataInt2(self.
context, label, x, y)
4801 def setGlobalDataInt3(self, label: str, x_or_vec, y: int =
None, z: int =
None) ->
None:
4802 """Set global data as int3."""
4803 if hasattr(x_or_vec,
'x')
and y
is None:
4804 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4807 context_wrapper.setGlobalDataInt3(self.
context, label, x, y, z)
4809 def setGlobalDataInt4(self, label: str, x_or_vec, y: int =
None, z: int =
None, w: int =
None) ->
None:
4810 """Set global data as int4."""
4811 if hasattr(x_or_vec,
'x')
and y
is None:
4812 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4815 context_wrapper.setGlobalDataInt4(self.
context, label, x, y, z, w)
4817 def getGlobalData(self, label: str, data_type: type =
None):
4818 """Get global data with optional type specification. Auto-detects type if not specified."""
4819 if data_type
is None:
4820 return context_wrapper.getGlobalDataAuto(self.
context, label)
4821 if data_type == int:
4822 return context_wrapper.getGlobalDataInt(self.
context, label)
4823 elif data_type == float:
4824 return context_wrapper.getGlobalDataFloat(self.
context, label)
4825 elif data_type == str:
4826 return context_wrapper.getGlobalDataString(self.
context, label)
4827 elif data_type == vec3:
4828 coords = context_wrapper.getGlobalDataVec3(self.
context, label)
4829 return vec3(coords[0], coords[1], coords[2])
4830 elif data_type == vec2:
4831 coords = context_wrapper.getGlobalDataVec2(self.
context, label)
4832 return vec2(coords[0], coords[1])
4833 elif data_type == vec4:
4834 coords = context_wrapper.getGlobalDataVec4(self.
context, label)
4835 return vec4(coords[0], coords[1], coords[2], coords[3])
4836 elif data_type == int2:
4837 coords = context_wrapper.getGlobalDataInt2(self.
context, label)
4838 return int2(coords[0], coords[1])
4839 elif data_type == int3:
4840 coords = context_wrapper.getGlobalDataInt3(self.
context, label)
4841 return int3(coords[0], coords[1], coords[2])
4842 elif data_type == int4:
4843 coords = context_wrapper.getGlobalDataInt4(self.
context, label)
4844 return int4(coords[0], coords[1], coords[2], coords[3])
4845 elif data_type ==
"uint":
4846 return context_wrapper.getGlobalDataUInt(self.
context, label)
4847 elif data_type ==
"double":
4848 return context_wrapper.getGlobalDataDouble(self.
context, label)
4850 raise ValueError(f
"Unsupported global data type: {data_type}")
4853 """Get float global data."""
4854 return context_wrapper.getGlobalDataFloat(self.
context, label)
4857 """Get int global data."""
4858 return context_wrapper.getGlobalDataInt(self.
context, label)
4861 """Get string global data."""
4862 return context_wrapper.getGlobalDataString(self.
context, label)
4865 """Get the HeliosDataType enum for global data."""
4866 return context_wrapper.getGlobalDataTypeWrapper(self.
context, label)
4869 """Get the size of global data array."""
4870 return context_wrapper.getGlobalDataSizeWrapper(self.
context, label)
4873 """Check if global data exists."""
4874 return context_wrapper.doesGlobalDataExistWrapper(self.
context, label)
4877 """Clear global data."""
4878 context_wrapper.clearGlobalDataWrapper(self.
context, label)
4881 """Rename a global data label."""
4882 context_wrapper.renameGlobalDataWrapper(self.
context, old_label, new_label)
4885 """Duplicate global data to a new label."""
4886 context_wrapper.duplicateGlobalDataWrapper(self.
context, old_label, new_label)
4889 """List all global data labels."""
4890 return context_wrapper.listGlobalDataWrapper(self.
context)
4893 """Increment global data. Auto-dispatches based on increment type."""
4894 if isinstance(increment, float):
4895 context_wrapper.incrementGlobalDataFloatWrapper(self.
context, label, increment)
4896 elif isinstance(increment, int):
4897 context_wrapper.incrementGlobalDataIntWrapper(self.
context, label, increment)
4899 raise ValueError(f
"Unsupported increment type: {type(increment).__name__}")
4904 """Calculate arithmetic mean of primitive data across UUIDs.
4907 uuids: List of primitive UUIDs.
4909 return_type: float (default), "double", or vec3.
4911 if return_type == float:
4912 return context_wrapper.calculatePrimitiveDataMeanFloatWrapper(self.
context, uuids, label)
4913 elif return_type ==
"double":
4914 return context_wrapper.calculatePrimitiveDataMeanDoubleWrapper(self.
context, uuids, label)
4915 elif return_type == vec3:
4916 coords = context_wrapper.calculatePrimitiveDataMeanVec3Wrapper(self.
context, uuids, label)
4917 return vec3(coords[0], coords[1], coords[2])
4919 raise ValueError(f
"Unsupported return type: {return_type}")
4922 """Calculate area-weighted mean of primitive data."""
4923 if return_type == float:
4924 return context_wrapper.calculatePrimitiveDataAreaWeightedMeanFloatWrapper(self.
context, uuids, label)
4926 raise ValueError(f
"Unsupported return type: {return_type}")
4929 """Calculate sum of primitive data across UUIDs."""
4930 if return_type == float:
4931 return context_wrapper.calculatePrimitiveDataSumFloatWrapper(self.
context, uuids, label)
4932 elif return_type ==
"double":
4933 return context_wrapper.calculatePrimitiveDataSumDoubleWrapper(self.
context, uuids, label)
4935 raise ValueError(f
"Unsupported return type: {return_type}")
4938 """Calculate area-weighted sum of primitive data."""
4939 if return_type == float:
4940 return context_wrapper.calculatePrimitiveDataAreaWeightedSumFloatWrapper(self.
context, uuids, label)
4942 raise ValueError(f
"Unsupported return type: {return_type}")
4945 """Scale primitive data by a factor.
4948 scalePrimitiveData(uuids, label, factor) - scale for specific UUIDs
4949 scalePrimitiveData(label, factor) - scale for ALL primitives
4951 if isinstance(uuids_or_label, str):
4952 context_wrapper.scalePrimitiveDataAllWrapper(self.
context, uuids_or_label, label_or_factor)
4954 context_wrapper.scalePrimitiveDataWithUUIDsWrapper(self.
context, uuids_or_label, label_or_factor, factor)
4956 def incrementPrimitiveData(self, uuids: List[int], label: str, increment, data_type: str =
None) ->
None:
4957 """Increment primitive data for the given UUIDs.
4959 Each Helios increment overload only acts on fields whose stored type matches;
4960 fields of a different type are left unchanged. By default the overload is
4961 inferred from the Python type of ``increment`` (``int`` -> int, ``float`` ->
4962 float). To target an unsigned-int or double field, pass ``data_type``
4963 explicitly as one of ``'int'``, ``'uint'``, ``'float'``, ``'double'``.
4966 uuids: UUIDs whose data field to increment.
4967 label: Data field label.
4968 increment: Amount to add.
4969 data_type: Optional explicit field type to target.
4971 if data_type
is not None:
4972 dt = data_type.lower()
4974 context_wrapper.incrementPrimitiveDataIntWrapper(self.
context, uuids, label, int(increment))
4975 elif dt
in (
'uint',
'unsigned',
'unsigned int'):
4976 context_wrapper.incrementPrimitiveDataUIntWrapper(self.
context, uuids, label, int(increment))
4978 context_wrapper.incrementPrimitiveDataFloatWrapper(self.
context, uuids, label, float(increment))
4979 elif dt ==
'double':
4980 context_wrapper.incrementPrimitiveDataDoubleWrapper(self.
context, uuids, label, float(increment))
4982 raise ValueError(f
"Unsupported data_type: {data_type!r}. Expected one of 'int', 'uint', 'float', 'double'.")
4984 if isinstance(increment, float):
4985 context_wrapper.incrementPrimitiveDataFloatWrapper(self.
context, uuids, label, increment)
4986 elif isinstance(increment, int):
4987 context_wrapper.incrementPrimitiveDataIntWrapper(self.
context, uuids, label, increment)
4989 raise ValueError(f
"Unsupported increment type: {type(increment).__name__}")
4992 """Sum multiple primitive data fields into a new field."""
4993 context_wrapper.aggregatePrimitiveDataSumWrapper(self.
context, uuids, labels, result_label)
4996 """Multiply multiple primitive data fields into a new field."""
4997 context_wrapper.aggregatePrimitiveDataProductWrapper(self.
context, uuids, labels, result_label)
5000 """Calculate total one-sided surface area for a set of primitives."""
5001 return context_wrapper.sumPrimitiveSurfaceAreaWrapper(self.
context, uuids)
5003 def calculateAreaIndex(self, leaf_uuids: List[int], wood_uuids: Optional[List[int]] =
None,
5004 ground_area: Optional[float] =
None) -> float:
5005 """Calculate the one-sided area index on a ground-area basis.
5007 This is the quantity :math:`L` appearing in Beer's law,
5008 :math:`\\exp(-G L / \\cos\\theta)`: total one-sided area divided by the ground
5009 area over which it is distributed.
5012 leaf_uuids: UUIDs of leaf primitives.
5013 wood_uuids: Optional UUIDs of woody (branch, trunk, stem) primitives. When
5014 given, the result is a *plant* area index rather than a leaf area index.
5015 Woody area is counted as one half of its summed one-sided area, because a
5016 tube or cone encloses the branch and so sums to the full cylinder surface
5017 rather than the projected area Beer's law requires. If woody elements are
5018 instead represented by non-enclosing planar primitives that are already
5019 one-sided silhouettes, this halving underestimates them by a factor of two.
5020 ground_area: Optional ground area basis in m^2. When omitted, the basis is the
5021 horizontal (x-y) footprint of the bounding box of *all* primitives in the
5022 Context -- so a ground primitive extending beyond the canopy enlarges the
5023 basis and lowers the reported index. Supply this explicitly whenever the
5024 domain is not cropped tightly to the canopy.
5027 One-sided area index (m^2 area per m^2 ground area).
5030 ValueError: If ``leaf_uuids`` is empty or ``ground_area`` is not positive.
5031 RuntimeError: If any primitive is a voxel, whose area is its total enclosing
5032 surface area rather than a one-sided area.
5035 >>> lai = context.calculateAreaIndex(leaf_uuids)
5036 >>> pai = context.calculateAreaIndex(leaf_uuids, wood_uuids)
5037 >>> lai = context.calculateAreaIndex(leaf_uuids, ground_area=100.0)
5040 raise ValueError(
"leaf_uuids must contain at least one UUID")
5041 if ground_area
is not None and ground_area <= 0:
5042 raise ValueError(f
"Ground area must be positive, got {ground_area}")
5044 if wood_uuids
is not None and ground_area
is not None:
5045 return context_wrapper.calculateAreaIndexLeafWoodGroundAreaWrapper(
5046 self.
context, leaf_uuids, wood_uuids, ground_area)
5047 elif wood_uuids
is not None:
5048 return context_wrapper.calculateAreaIndexLeafWoodWrapper(self.
context, leaf_uuids, wood_uuids)
5049 elif ground_area
is not None:
5050 return context_wrapper.calculateAreaIndexLeafGroundAreaWrapper(self.
context, leaf_uuids, ground_area)
5052 return context_wrapper.calculateAreaIndexLeafWrapper(self.
context, leaf_uuids)
5055 """Filter primitives by data value. Auto-dispatches based on value type.
5058 uuids: UUIDs to filter.
5059 label: Data label to compare.
5060 value: Filter value (float, int, or str).
5061 comparator: Comparison operator ("=", "<", ">", "<=", ">="). Not used for strings.
5063 if isinstance(value, str):
5064 return context_wrapper.filterPrimitivesByDataStringWrapper(self.
context, uuids, label, value)
5065 elif isinstance(value, float):
5066 return context_wrapper.filterPrimitivesByDataFloatWrapper(self.
context, uuids, label, value, comparator)
5067 elif isinstance(value, int):
5068 return context_wrapper.filterPrimitivesByDataIntWrapper(self.
context, uuids, label, value, comparator)
5070 raise ValueError(f
"Unsupported filter value type: {type(value).__name__}")
5075 """Return the integer-coded `helios::ObjectType` of a compound object.
5077 Values follow the C++ `helios::ObjectType` enum
5078 (0=tile, 1=sphere, 2=tube, 3=box, 4=disk, 5=polymesh, 6=cone,
5082 return context_wrapper.getObjectTypeWrapper(self.
context, objID)
5086 x, y, z = context_wrapper.getObjectCenterWrapper(self.
context, objID)
5087 return vec3(x, y, z)
5090 """Get axis-aligned bounding box for one object or a list of objects.
5092 The box encloses every vertex of every primitive belonging to the given
5096 objIDs: Single object ID (int) or list of object IDs.
5099 Tuple of (min_corner: vec3, max_corner: vec3).
5102 HeliosRuntimeError: If an object ID does not exist, or if the given
5103 object(s) contain no primitives at all (a bounding box would be
5107 if isinstance(objIDs, (list, tuple)):
5108 mn, mx = context_wrapper.getObjectBoundingBoxBatchWrapper(self.
context, list(objIDs))
5110 mn, mx = context_wrapper.getObjectBoundingBoxWrapper(self.
context, objIDs)
5111 return (
vec3(mn[0], mn[1], mn[2]),
vec3(mx[0], mx[1], mx[2]))
5114 """Get flattened primitive UUIDs for one object, a list of objects, or a list-of-lists.
5117 objIDs: int, List[int], or List[List[int]].
5120 Flat list of primitive UUIDs (union across all objects).
5123 if isinstance(objIDs, (list, tuple))
and objIDs
and isinstance(objIDs[0], (list, tuple)):
5124 return context_wrapper.getObjectPrimitiveUUIDsNestedWrapper(self.
context, [list(x)
for x
in objIDs])
5125 if isinstance(objIDs, (list, tuple)):
5126 return context_wrapper.getObjectPrimitiveUUIDsBatchWrapper(self.
context, list(objIDs))
5127 return context_wrapper.getObjectPrimitiveUUIDs(self.
context, int(objIDs))
5131 """Get tile-object area ratio for one or multiple tile objects."""
5133 if isinstance(objIDs, (list, tuple)):
5134 return context_wrapper.getTileObjectAreaRatioBatchWrapper(self.
context, list(objIDs))
5135 return context_wrapper.getTileObjectAreaRatioWrapper(self.
context, objIDs)
5139 x, y, z = context_wrapper.getTileObjectCenterWrapper(self.
context, objID)
5140 return vec3(x, y, z)
5144 x, y = context_wrapper.getTileObjectSizeWrapper(self.
context, objID)
5149 x, y = context_wrapper.getTileObjectSubdivisionCountWrapper(self.
context, objID)
5154 x, y, z = context_wrapper.getTileObjectNormalWrapper(self.
context, objID)
5155 return vec3(x, y, z)
5159 pairs = context_wrapper.getTileObjectTextureUVWrapper(self.
context, objID)
5160 return [
vec2(u, v)
for u, v
in pairs]
5164 triples = context_wrapper.getTileObjectVerticesWrapper(self.
context, objID)
5165 return [
vec3(x, y, z)
for x, y, z
in triples]
5168 """Get the texture repeat count requested when the tile object was created.
5170 The repeat actually applied to the sub-patch texture coordinates is reduced whenever the
5171 requested count does not evenly divide the subdivision count; use
5172 :meth:`getTileObjectEffectiveTextureRepeat` to query that value.
5174 It is the requested rather than the reduced count that is retained on the object and
5175 re-applied when :meth:`setTileObjectSubdivisionCount` changes the subdivision count.
5178 x, y = context_wrapper.getTileObjectTextureRepeatWrapper(self.
context, objID)
5182 """Get the texture repeat count actually applied to the sub-patches of a tile object.
5184 This is the requested count (see :meth:`getTileObjectTextureRepeat`) reduced so that it
5185 evenly divides the subdivision count.
5188 x, y = context_wrapper.getTileObjectEffectiveTextureRepeatWrapper(self.
context, objID)
5193 """Get the Cartesian coordinates of the center of an adaptive tile object."""
5195 x, y, z = context_wrapper.getAdaptiveTileObjectCenterWrapper(self.
context, objID)
5196 return vec3(x, y, z)
5199 """Get the dimensions of an entire adaptive tile object."""
5201 x, y = context_wrapper.getAdaptiveTileObjectSizeWrapper(self.
context, objID)
5205 """Get a unit vector normal to an adaptive tile object surface."""
5207 x, y, z = context_wrapper.getAdaptiveTileObjectNormalWrapper(self.
context, objID)
5208 return vec3(x, y, z)
5211 """Get the Cartesian coordinates of each of the four corners of an adaptive tile object."""
5213 triples = context_wrapper.getAdaptiveTileObjectVerticesWrapper(self.
context, objID)
5214 return [
vec3(x, y, z)
for x, y, z
in triples]
5217 """Get the refinement parameters that were requested when the object was created."""
5219 tx, ty, smin, smax, exponent = context_wrapper.getAdaptiveTileObjectRefinementWrapper(self.
context, objID)
5221 subpatch_size_max=smax, transition_exponent=exponent)
5224 """Get the number of coarsest-level cells spanning an adaptive tile in x and y.
5226 The base grid is the uniform grid of unrefined cells that the quadtree refines within. It
5227 is derived from the requested sub-patch size range and the tile dimensions.
5230 x, y = context_wrapper.getAdaptiveTileObjectBaseSubdivisionCountWrapper(self.
context, objID)
5234 """Get the maximum quadtree refinement level, i.e. the number of times a base cell may be subdivided."""
5236 return context_wrapper.getAdaptiveTileObjectMaxRefinementLevelWrapper(self.
context, objID)
5239 """Get the sub-patch edge lengths actually achieved, as opposed to those requested.
5242 A vec2 holding the achieved minimum edge length in ``x`` and the achieved maximum edge
5246 x, y = context_wrapper.getAdaptiveTileObjectSubpatchSizeRangeWrapper(self.
context, objID)
5250 """Get the texture repeat count of an adaptive tile object.
5252 The base grid is snapped to a multiple of the requested repeat count when the object is
5253 created, so unlike :meth:`getTileObjectTextureRepeat` the requested count is always applied
5254 exactly and there is no effective-repeat variant.
5257 x, y = context_wrapper.getAdaptiveTileObjectTextureRepeatWrapper(self.
context, objID)
5263 x, y, z = context_wrapper.getSphereObjectCenterWrapper(self.
context, objID)
5264 return vec3(x, y, z)
5267 """Get per-axis radii of a sphere object.
5269 Note: Helios spheres are spheroids with three independent radii (rx, ry, rz).
5270 Returns a vec3 (not a scalar).
5273 x, y, z = context_wrapper.getSphereObjectRadiusWrapper(self.
context, objID)
5274 return vec3(x, y, z)
5278 return context_wrapper.getSphereObjectSubdivisionCountWrapper(self.
context, objID)
5282 return context_wrapper.getSphereObjectVolumeWrapper(self.
context, objID)
5287 x, y, z = context_wrapper.getBoxObjectCenterWrapper(self.
context, objID)
5292 x, y, z = context_wrapper.getBoxObjectSizeWrapper(self.
context, objID)
5297 x, y, z = context_wrapper.getBoxObjectSubdivisionCountWrapper(self.
context, objID)
5298 return int3(x, y, z)
5302 return context_wrapper.getBoxObjectVolumeWrapper(self.
context, objID)
5307 x, y, z = context_wrapper.getDiskObjectCenterWrapper(self.
context, objID)
5308 return vec3(x, y, z)
5312 x, y = context_wrapper.getDiskObjectSizeWrapper(self.
context, objID)
5317 return context_wrapper.getDiskObjectSubdivisionCountWrapper(self.
context, objID)
5322 return context_wrapper.getTubeObjectSubdivisionCountWrapper(self.
context, objID)
5326 return context_wrapper.getTubeObjectNodeCountWrapper(self.
context, objID)
5330 triples = context_wrapper.getTubeObjectNodesWrapper(self.
context, objID)
5331 return [
vec3(x, y, z)
for x, y, z
in triples]
5335 return context_wrapper.getTubeObjectNodeRadiiWrapper(self.
context, objID)
5339 triples = context_wrapper.getTubeObjectNodeColorsWrapper(self.
context, objID)
5340 return [
RGBcolor(r, g, b)
for r, g, b
in triples]
5344 return context_wrapper.getTubeObjectVolumeWrapper(self.
context, objID)
5348 return context_wrapper.getTubeObjectSegmentVolumeWrapper(self.
context, objID, segment_index)
5353 return context_wrapper.getConeObjectSubdivisionCountWrapper(self.
context, objID)
5357 triples = context_wrapper.getConeObjectNodesWrapper(self.
context, objID)
5358 return [
vec3(x, y, z)
for x, y, z
in triples]
5362 return context_wrapper.getConeObjectNodeRadiiWrapper(self.
context, objID)
5366 x, y, z = context_wrapper.getConeObjectNodeWrapper(self.
context, objID, number)
5367 return vec3(x, y, z)
5371 return context_wrapper.getConeObjectNodeRadiusWrapper(self.
context, objID, number)
5375 x, y, z = context_wrapper.getConeObjectAxisUnitVectorWrapper(self.
context, objID)
5376 return vec3(x, y, z)
5380 return context_wrapper.getConeObjectLengthWrapper(self.
context, objID)
5384 return context_wrapper.getConeObjectVolumeWrapper(self.
context, objID)
5390 x, y, z = context_wrapper.getPatchCenterWrapper(self.
context, uuid)
5391 return vec3(x, y, z)
5395 x, y = context_wrapper.getPatchSizeWrapper(self.
context, uuid)
5400 x, y, z = context_wrapper.getTriangleVertexWrapper(self.
context, uuid, number)
5401 return vec3(x, y, z)
5405 x, y, z = context_wrapper.getVoxelCenterWrapper(self.
context, uuid)
5406 return vec3(x, y, z)
5410 x, y, z = context_wrapper.getVoxelSizeWrapper(self.
context, uuid)
5411 return vec3(x, y, z)
5413 def getPatchCount(self, include_hidden: bool =
True) -> int:
5415 return context_wrapper.getPatchCountWrapper(self.
context, include_hidden)
5419 return context_wrapper.getTriangleCountWrapper(self.
context, include_hidden)
5422 """Get axis-aligned bounding box for one primitive or a list of primitives.
5425 uuids: Single UUID (int) or list of UUIDs.
5428 Tuple of (min_corner: vec3, max_corner: vec3).
5431 if isinstance(uuids, (list, tuple)):
5432 mn, mx = context_wrapper.getPrimitiveBoundingBoxBatchWrapper(self.
context, list(uuids))
5434 mn, mx = context_wrapper.getPrimitiveBoundingBoxWrapper(self.
context, uuids)
5435 return (
vec3(mn[0], mn[1], mn[2]),
vec3(mx[0], mx[1], mx[2]))
5440 """Set the RGB or RGBA color of one primitive or a list of primitives.
5443 uuids: Single UUID (int) or list of UUIDs.
5444 color: RGBcolor or RGBAcolor.
5447 if isinstance(color, RGBAcolor):
5448 rgba = [color.r, color.g, color.b, color.a]
5449 if isinstance(uuids, (list, tuple)):
5450 context_wrapper.setPrimitiveColorRGBABatchWrapper(self.
context, list(uuids), rgba)
5452 context_wrapper.setPrimitiveColorRGBAWrapper(self.
context, uuids, rgba)
5453 elif isinstance(color, RGBcolor):
5454 rgb = [color.r, color.g, color.b]
5455 if isinstance(uuids, (list, tuple)):
5456 context_wrapper.setPrimitiveColorBatchWrapper(self.
context, list(uuids), rgb)
5458 context_wrapper.setPrimitiveColorWrapper(self.
context, uuids, rgb)
5460 raise ValueError(f
"color must be RGBcolor or RGBAcolor, got {type(color).__name__}")
5465 """Remove a named data field from one primitive or a list of primitives."""
5467 if isinstance(uuids, (list, tuple)):
5468 context_wrapper.clearPrimitiveDataByLabelBatchWrapper(self.
context, list(uuids), label)
5470 context_wrapper.clearPrimitiveDataByLabelWrapper(self.
context, uuids, label)
5473 """Remove a named data field from every primitive in the Context.
5475 Clears the data with the given label from all primitives (including hidden ones)
5476 and releases the registered data type for the label, so it may subsequently be
5477 re-registered with a different type. Requires helios-core v1.3.73 or newer.
5480 context_wrapper.clearAllPrimitiveDataByLabelWrapper(self.
context, label)
5483 """List all data labels attached to a primitive."""
5485 return context_wrapper.listPrimitiveDataWrapper(self.
context, uuid)
5491 if not isinstance(xbounds, vec2):
5492 raise ValueError(f
"xbounds must be a vec2, got {type(xbounds).__name__}")
5493 context_wrapper.cropDomainXWrapper(self.
context, xbounds.to_list())
5497 if not isinstance(ybounds, vec2):
5498 raise ValueError(f
"ybounds must be a vec2, got {type(ybounds).__name__}")
5499 context_wrapper.cropDomainYWrapper(self.
context, ybounds.to_list())
5503 if not isinstance(zbounds, vec2):
5504 raise ValueError(f
"zbounds must be a vec2, got {type(zbounds).__name__}")
5505 context_wrapper.cropDomainZWrapper(self.
context, zbounds.to_list())
5507 def cropDomain(self, *args) -> Optional[List[int]]:
5508 """Crop the context domain to the given XYZ bounds.
5511 cropDomain(xbounds: vec2, ybounds: vec2, zbounds: vec2)
5512 -> crop ALL primitives; returns None.
5513 cropDomain(uuids: List[int], xbounds: vec2, ybounds: vec2, zbounds: vec2)
5514 -> crop only the given primitives; returns the list of primitives
5515 that survived (in-bounds UUIDs). The input list is NOT mutated.
5520 for name, b
in ((
"xbounds", xb), (
"ybounds", yb), (
"zbounds", zb)):
5521 if not isinstance(b, vec2):
5522 raise ValueError(f
"{name} must be a vec2, got {type(b).__name__}")
5523 context_wrapper.cropDomainXYZWrapper(self.
context, xb.to_list(), yb.to_list(), zb.to_list())
5526 uuids, xb, yb, zb = args
5527 if not isinstance(uuids, (list, tuple)):
5528 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
5529 for name, b
in ((
"xbounds", xb), (
"ybounds", yb), (
"zbounds", zb)):
5530 if not isinstance(b, vec2):
5531 raise ValueError(f
"{name} must be a vec2, got {type(b).__name__}")
5532 return context_wrapper.cropDomainByUUIDsWrapper(self.
context, list(uuids), xb.to_list(), yb.to_list(), zb.to_list())
5533 raise TypeError(f
"cropDomain() takes 3 or 4 positional arguments, got {len(args)}")
5542 """Return True if a compound object with the given ID exists."""
5544 return context_wrapper.doesObjectExistWrapper(self.
context, objID)
5547 """Return True if the given primitive UUID belongs to the given object."""
5549 return context_wrapper.doesObjectContainPrimitiveWrapper(self.
context, objID, uuid)
5552 """Return True if the named material has data stored under data_label."""
5554 return context_wrapper.doesMaterialDataExistWrapper(self.
context, material_label, data_label)
5557 """Return True if the compound object has a texture assigned."""
5559 return context_wrapper.objectHasTextureWrapper(self.
context, objID)
5562 """Return True if the primitive's geometry has been modified since the last clean mark."""
5564 return context_wrapper.isPrimitiveDirtyWrapper(self.
context, uuid)
5567 """Return True if value caching is enabled for the given object-data label."""
5569 return context_wrapper.isObjectDataValueCachingEnabledWrapper(self.
context, label)
5572 """Return True if value caching is enabled for the given primitive-data label."""
5574 return context_wrapper.isPrimitiveDataValueCachingEnabledWrapper(self.
context, label)
5577 """Return True if all primitives originally belonging to this object still exist
5578 (i.e., none have been deleted)."""
5580 return context_wrapper.areObjectPrimitivesCompleteWrapper(self.
context, objID)
5585 """Get the current simulation date as Julian day (1-366)."""
5587 return context_wrapper.getJulianDateWrapper(self.
context)
5590 """Return the total number of materials registered in the context."""
5592 return context_wrapper.getMaterialCountWrapper(self.
context)
5595 """Return the total surface area (one-sided) of all primitives in the object."""
5597 return context_wrapper.getObjectAreaWrapper(self.
context, objID)
5600 """Return the number of primitives currently belonging to the object."""
5602 return context_wrapper.getObjectPrimitiveCountWrapper(self.
context, objID)
5605 """Return the enclosed volume of a polymesh object.
5607 Since helios-core 1.3.84 a mesh carrying a face table is separated into its
5608 connected pieces and the volume of those that are closed is summed, so a solid
5609 shape modelled with an open stalk reports the shape's volume. An error is raised
5610 only when no piece is closed. A mesh carrying no face table has its closure
5611 checked by matching facets on coincident corners.
5614 return context_wrapper.getPolymeshObjectVolumeWrapper(self.
context, objID)
5617 """Return the total surface area of a polymesh object, summed over every face."""
5619 return context_wrapper.getPolymeshObjectSurfaceAreaWrapper(self.
context, objID)
5623 Return True if a polymesh object is a closed surface, i.e. has no boundary edges.
5625 Only a closed mesh has a well-defined enclosed volume. Since helios-core 1.3.84
5626 :meth:`getPolymeshObjectVolume` splits a mesh into its connected pieces and sums
5627 the volume of those that are closed, so it raises only when no piece is closed --
5628 a solid fruit modelled with an open stalk reports the fruit's volume.
5631 return context_wrapper.isPolymeshObjectClosedWrapper(self.
context, objID)
5635 Move every shared vertex of a polymesh object, deforming the mesh.
5637 Writes every shared vertex in one pass and pushes the new positions out to the
5638 member primitives, so faces that meet at a vertex stay welded. This is the
5639 supported way to deform a mesh: transforming the member primitives individually
5640 leaves each shared vertex wherever the last facet processed put it.
5642 The topology is unchanged, so ``vertices`` must be parallel to and the same length
5643 as the list returned by :meth:`getPolymeshObjectVertices`. Texture coordinates are
5644 not touched, and neither is the solid fraction of the member primitives -- so
5645 deforming a textured mesh does not re-rasterize its alpha mask.
5648 objID: Object ID of the polymesh object
5649 vertices: New vertex positions in global Cartesian coordinates
5652 RuntimeError: If the native library predates helios-core v1.3.84
5653 ValueError: If a vertex is not a vec3
5656 Vertex normals are NOT recomputed and no longer describe the deformed surface;
5657 call :meth:`computePolymeshObjectVertexNormals` again if exact normals matter.
5660 >>> verts = context.getPolymeshObjectVertices(objID)
5661 >>> stretched = [vec3(v.x, v.y, v.z * 2.0) for v in verts]
5662 >>> context.setPolymeshObjectVertices(objID, stretched)
5665 for i, v
in enumerate(vertices):
5666 if not isinstance(v, vec3):
5667 raise ValueError(f
"vertices[{i}] must be a vec3, got {type(v).__name__}")
5668 context_wrapper.setPolymeshObjectVerticesWrapper(
5669 self.
context, objID, [(v.x, v.y, v.z)
for v
in vertices]
5674 Return True if a compound object reports which member primitives meet at each vertex.
5676 True for Tile, AdaptiveTile, Sphere, Tube and Cone objects, and for a Polymesh that
5677 carries a face table. False for a Box, a Disk, and for a polymesh assembled from
5678 loose primitives by :meth:`addPolymeshObject`, which has no topology.
5681 RuntimeError: If the native library predates helios-core v1.3.84
5684 return context_wrapper.doesObjectHaveSharedVertexTopologyWrapper(self.
context, objID)
5687 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL) -> int:
5689 Return the number of distinct shared vertices in a compound object's mesh.
5691 This is one greater than the largest index
5692 :meth:`getObjectPrimitiveSharedVertexIndices` can return, and zero if the object
5693 exposes no topology.
5696 objID: Object ID of the compound object
5697 weld_mode: Granularity at which coincident vertices are treated as the same
5698 shared vertex. See :class:`VertexWeldMode`.
5701 RuntimeError: If the native library predates helios-core v1.3.84
5704 return context_wrapper.getObjectSharedVertexCountWrapper(
5705 self.
context, objID, int(weld_mode)
5709 self, objID: int, uuid: int,
5710 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL
5713 Return the shared mesh vertex each vertex of a primitive belongs to.
5715 Indices are in the same order as :meth:`getPrimitiveVertices`, and two primitives
5716 meeting at a corner report the same index there. This is what lets a per-face
5717 quantity be averaged onto the vertices neighbouring faces have in common.
5720 objID: Object ID of the compound object the primitive belongs to
5721 uuid: UUID of the primitive
5722 weld_mode: See :class:`VertexWeldMode`
5725 One index per vertex of the primitive; empty if the object exposes no topology.
5728 RuntimeError: If the native library predates helios-core v1.3.84
5731 When walking a whole object, prefer
5732 :meth:`getObjectPrimitiveSharedVertexIndicesMulti` -- this per-primitive form is
5733 O(n) per call on Sphere, Tube and Cone objects, so a full walk is O(n^2).
5736 return context_wrapper.getObjectPrimitiveSharedVertexIndicesWrapper(
5737 self.
context, objID, uuid, int(weld_mode)
5741 self, objID: int, uuids: List[int],
5742 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL
5743 ) -> List[List[int]]:
5745 Return shared mesh vertex indices for many primitives of a compound object at once.
5747 Equivalent to calling :meth:`getObjectPrimitiveSharedVertexIndices` for each UUID,
5748 except that any per-object quantity needed to locate the vertices is prepared once
5749 for the whole batch. Prefer this overload when walking an entire object.
5752 objID: Object ID of the compound object the primitives belong to
5753 uuids: UUIDs of the primitives
5754 weld_mode: See :class:`VertexWeldMode`
5757 A list parallel to ``uuids``, each entry holding one shared vertex index per
5758 vertex of the corresponding primitive.
5761 RuntimeError: If the native library predates helios-core v1.3.84
5764 return context_wrapper.getObjectPrimitiveSharedVertexIndicesMultiWrapper(
5765 self.
context, objID, uuids, int(weld_mode)
5770 weld_mode: VertexWeldMode = VertexWeldMode.WELD_FULL
5773 Return a primitive's shared mesh vertex indices without naming its parent object.
5775 Resolves the primitive's parent object and forwards to it.
5778 uuid: UUID of the primitive
5779 weld_mode: See :class:`VertexWeldMode`
5782 One index per vertex of the primitive. Empty if the primitive belongs to no
5783 object, or to one that exposes no topology.
5786 RuntimeError: If the native library predates helios-core v1.3.84
5789 return context_wrapper.getPrimitiveSharedVertexIndicesWrapper(
5790 self.
context, uuid, int(weld_mode)
5795 Return the deduplicated shared vertex positions of a polymesh object.
5797 Returns an empty list for a mesh with no retained topology, such as one built
5798 with :meth:`addPolymeshObject` and not given a face set.
5801 triples = context_wrapper.getPolymeshObjectVerticesWrapper(self.
context, objID)
5802 return [
vec3(x, y, z)
for x, y, z
in triples]
5805 """Return the vertex index triples defining each face of a polymesh object."""
5807 triples = context_wrapper.getPolymeshObjectFacesWrapper(self.
context, objID)
5808 return [
int3(a, b, c)
for a, b, c
in triples]
5812 Return the per-vertex normals of a polymesh object.
5814 Returns an empty list if the mesh carries none. A mesh loaded by :meth:`loadOBJ`
5815 or :meth:`loadPLY` always has them (helios-core v1.3.85+): normals authored in the
5816 file are kept, and a file that supplies none has them generated from the mesh
5817 connectivity. Only a mesh assembled programmatically through
5818 :meth:`setPolymeshObjectTopology` without normals carries none; call
5819 :meth:`computePolymeshObjectVertexNormals` to generate them.
5822 triples = context_wrapper.getPolymeshObjectVertexNormalsWrapper(self.
context, objID)
5823 return [
vec3(x, y, z)
for x, y, z
in triples]
5826 """Return the per-vertex texture coordinates of a polymesh object, or an empty list if it has none."""
5828 pairs = context_wrapper.getPolymeshObjectVertexUVWrapper(self.
context, objID)
5829 return [
vec2(u, v)
for u, v
in pairs]
5832 """Return True if a polymesh object carries per-vertex normals."""
5834 return context_wrapper.doesPolymeshObjectHaveVertexNormalsWrapper(self.
context, objID)
5838 Return where a polymesh object's vertex normals came from.
5840 ``AUTHORED`` means they were read from the source file (OBJ ``vn`` records, or
5841 PLY ``nx``/``ny``/``nz`` properties); ``COMPUTED`` means they were generated,
5842 either by the file loader because the file supplied none (helios-core v1.3.85+)
5843 or by :meth:`computePolymeshObjectVertexNormals`; ``NONE`` means the mesh has no
5844 vertex normals, which only arises for a mesh assembled programmatically through
5845 :meth:`setPolymeshObjectTopology`.
5849 context_wrapper.getPolymeshObjectVertexNormalSourceWrapper(self.
context, objID)
5853 """Return the number of shared vertices in a polymesh object."""
5855 return context_wrapper.getPolymeshObjectVertexCountWrapper(self.
context, objID)
5858 """Return the number of faces in a polymesh object."""
5860 return context_wrapper.getPolymeshObjectFaceCountWrapper(self.
context, objID)
5863 """Return the index into :meth:`getPolymeshObjectFaces` of the face made up by a member primitive."""
5865 return context_wrapper.getPolymeshObjectFaceIndexForPrimitiveWrapper(self.
context, objID, uuid)
5868 """Return the UUID of the primitive making up a given face of a polymesh object."""
5870 return context_wrapper.getPolymeshObjectPrimitiveUUIDForFaceWrapper(self.
context, objID, face_index)
5874 Compute per-vertex normals for a polymesh object by area-weighted averaging.
5876 Vertices are split across edges whose dihedral angle exceeds
5877 ``crease_angle_degrees``, so hard edges stay hard rather than being smoothed
5878 away. The resulting normals are reported as
5879 :attr:`VertexNormalSource.COMPUTED`.
5881 The file loaders call this automatically for a mesh whose source file supplied
5882 no normals (helios-core v1.3.85+), so it is only needed for a mesh assembled
5883 programmatically through :meth:`setPolymeshObjectTopology`, or to regenerate
5884 normals at a different crease angle or after the mesh has been deformed with
5885 :meth:`setPolymeshObjectVertices`.
5888 objID: Object ID of the polymesh object
5889 crease_angle_degrees: Dihedral angle above which an edge is kept hard
5892 context_wrapper.computePolymeshObjectVertexNormalsWrapper(
5893 self.
context, objID, float(crease_angle_degrees)
5898 Return the boundary edges of a polymesh object as vertex index pairs.
5900 A boundary edge is one referenced by exactly one face. An empty list means the
5904 pairs = context_wrapper.getPolymeshObjectBoundaryEdgesWrapper(self.
context, objID)
5905 return [
int2(a, b)
for a, b
in pairs]
5909 Return the connected components of a polymesh object.
5911 Each component is a list of face indices into :meth:`getPolymeshObjectFaces`.
5912 More than one component means the mesh is made of separate disjoint pieces.
5915 return context_wrapper.getPolymeshObjectConnectedComponentsWrapper(self.
context, objID)
5918 face_UUIDs: List[int],
5919 vertex_normals: Optional[List[vec3]] =
None,
5920 vertex_uv: Optional[List[vec2]] =
None,
5921 normal_source: VertexNormalSource = VertexNormalSource.NONE) ->
None:
5923 Attach an indexed face set to a polymesh object built programmatically.
5925 Meshes loaded from an OBJ or PLY file retain the connectivity of the source file
5926 automatically. This is for a mesh assembled with :meth:`addPolymeshObject`, which
5927 otherwise has no topology and behaves as a triangle soup: its face count is zero,
5928 it cannot report a volume, and it is written out as independent per-triangle
5932 objID: Object ID of the polymesh object
5933 vertices: Shared vertex positions in global Cartesian coordinates
5934 faces: Vertex index triples defining each face
5935 face_UUIDs: UUID of the primitive corresponding to each face, parallel to ``faces``
5936 vertex_normals: Per-vertex normals, or None if the mesh has none
5937 vertex_uv: Per-vertex texture coordinates, or None if the mesh has none
5938 normal_source: Provenance of the supplied vertex normals
5941 ValueError: If an argument has the wrong type or ``face_UUIDs`` is not
5942 parallel to ``faces``
5945 for i, v
in enumerate(vertices):
5946 if not isinstance(v, vec3):
5947 raise ValueError(f
"vertices[{i}] must be a vec3, got {type(v).__name__}")
5948 for i, f
in enumerate(faces):
5949 if not isinstance(f, int3):
5950 raise ValueError(f
"faces[{i}] must be an int3, got {type(f).__name__}")
5951 if vertex_normals
is not None:
5952 for i, v
in enumerate(vertex_normals):
5953 if not isinstance(v, vec3):
5954 raise ValueError(f
"vertex_normals[{i}] must be a vec3, got {type(v).__name__}")
5955 if vertex_uv
is not None:
5956 for i, v
in enumerate(vertex_uv):
5957 if not isinstance(v, vec2):
5958 raise ValueError(f
"vertex_uv[{i}] must be a vec2, got {type(v).__name__}")
5959 if len(face_UUIDs) != len(faces):
5961 f
"face_UUIDs must be parallel to faces: got {len(face_UUIDs)} UUIDs for {len(faces)} faces"
5964 context_wrapper.setPolymeshObjectTopologyWrapper(
5966 [(v.x, v.y, v.z)
for v
in vertices],
5967 [(f.x, f.y, f.z)
for f
in faces],
5969 [(v.x, v.y, v.z)
for v
in vertex_normals]
if vertex_normals
else [],
5970 [(v.x, v.y)
for v
in vertex_uv]
if vertex_uv
else [],
5976 Return True if a compound object can report analytic vertex normals.
5978 True for Sphere, Tube and Cone objects, which approximate a curved shape and can
5979 evaluate its true surface normal. False for object types built from genuinely
5980 flat faces, such as a tile or box.
5983 return context_wrapper.doesObjectHaveAnalyticVertexNormalsWrapper(self.
context, objID)
5986 uuid: Union[int, List[int]]) -> Union[List[vec3], List[List[vec3]]]:
5988 Return the analytic surface normals at each vertex of a member primitive.
5990 Normals are evaluated from the object's own shape definition rather than stored,
5991 so they account for taper and stay correct after the object is transformed or its
5992 nodes and radii change. Returns an empty list for an object with no analytic
5993 normals (see :meth:`doesObjectHaveAnalyticVertexNormals`).
5996 objID: Object ID of the compound object
5997 uuid: UUID of a member primitive, or a list of them
6000 A list of vec3 for a single UUID, or a list of such lists for a list of UUIDs
6003 if isinstance(uuid, (list, tuple)):
6004 batches = context_wrapper.getObjectPrimitiveVertexNormalsBatchWrapper(
6005 self.
context, objID, list(uuid)
6007 return [[
vec3(x, y, z)
for x, y, z
in b]
for b
in batches]
6008 triples = context_wrapper.getObjectPrimitiveVertexNormalsWrapper(self.
context, objID, uuid)
6009 return [
vec3(x, y, z)
for x, y, z
in triples]
6012 """Look up a material ID from its human-readable label."""
6014 return context_wrapper.getMaterialIDFromLabelWrapper(self.
context, material_label)
6017 """Return the material ID assigned to the given primitive."""
6019 return context_wrapper.getPrimitiveMaterialIDWrapper(self.
context, uuid)
6022 """Return the version counter for a global data entry. Increments on each update;
6023 useful for cache invalidation."""
6025 return context_wrapper.getGlobalDataVersionWrapper(self.
context, label)
6028 """Return the ID of the compound object the primitive belongs to.
6030 Returns 0 if the primitive is not part of any compound object (the documented
6031 "no parent" sentinel). Raises ``HeliosRuntimeError`` if ``uuid`` does not exist.
6034 return context_wrapper.getPrimitiveParentObjectIDWrapper(self.
context, uuid)
6039 """Return the filesystem path of the texture assigned to the object, or an
6040 empty string if no texture is assigned."""
6042 return context_wrapper.getObjectTextureFileWrapper(self.
context, objID)
6045 """Return the union of all primitive-data labels used across every primitive
6048 return context_wrapper.listAllPrimitiveDataLabelsWrapper(self.
context)
6051 """Return the list of XML file paths that have been loaded into this context."""
6053 return context_wrapper.getLoadedXMLFilesWrapper(self.
context)
6058 """Print summary info for the object to stdout (for debugging)."""
6060 context_wrapper.printObjectInfoWrapper(self.
context, objID)
6063 """Print summary info for the primitive to stdout (for debugging)."""
6065 context_wrapper.printPrimitiveInfoWrapper(self.
context, uuid)
6068 """Enable value caching for the given primitive-data label. Required before
6069 using getUniquePrimitiveDataValues for that label."""
6071 context_wrapper.enablePrimitiveDataValueCachingWrapper(self.
context, label)
6074 """Disable value caching for the given primitive-data label."""
6076 context_wrapper.disablePrimitiveDataValueCachingWrapper(self.
context, label)
6079 """Enable value caching for the given object-data label. Required before
6080 using getUniqueObjectDataValues for that label."""
6082 context_wrapper.enableObjectDataValueCachingWrapper(self.
context, label)
6085 """Disable value caching for the given object-data label."""
6087 context_wrapper.disableObjectDataValueCachingWrapper(self.
context, label)
6090 """Compute the mean of the given primitive-data label across the object's
6091 primitives and store it as object data on the object itself under the
6094 context_wrapper.setObjectDataFromPrimitiveDataMeanWrapper(self.
context, objID, label)
6096 def renameMaterial(self, old_label: str, new_label: str) ->
None:
6097 """Rename an existing material."""
6099 context_wrapper.renameMaterialWrapper(self.
context, old_label, new_label)
6102 """Rename a primitive-data label on a single primitive."""
6104 context_wrapper.renamePrimitiveDataWrapper(self.
context, uuid, old_label, new_label)
6107 """Clear the named data entry on the given material."""
6109 context_wrapper.clearMaterialDataWrapper(self.
context, material_label, data_label)
6118 """Return the list of UUIDs that have been deleted from the context.
6120 These UUIDs are tombstoned and will not appear in getAllUUIDs(), but their
6121 IDs are tracked so they can be excluded from external references.
6124 return context_wrapper.getDeletedUUIDsWrapper(self.
context)
6126 def getDirtyUUIDs(self, include_deleted: bool =
True) -> List[int]:
6127 """Return the list of UUIDs whose geometry has been modified since the last
6128 markGeometryClean call.
6131 include_deleted: If True (default), include UUIDs that were deleted while
6132 dirty. If False, only return UUIDs that still exist.
6135 return context_wrapper.getDirtyUUIDsWrapper(self.
context, include_deleted)
6138 include_zero: bool =
True) -> List[int]:
6139 """Return the unique set of compound-object IDs that the given primitives
6143 uuids: List of primitive UUIDs to inspect.
6144 include_zero: If True (default), include the sentinel object ID 0
6145 (i.e., primitives with no parent object). If False, only return
6146 IDs of real compound objects.
6149 if not isinstance(uuids, (list, tuple)):
6150 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6151 return context_wrapper.getUniquePrimitiveParentObjectIDsWrapper(
6152 self.
context, list(uuids), include_zero
6158 """Return the area-weighted average normal of all primitives in the object."""
6160 x, y, z = context_wrapper.getObjectAverageNormalWrapper(self.
context, objID)
6161 return vec3(x, y, z)
6164 """Rotate the object so its area-weighted average normal aligns with
6165 new_normal. 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 context_wrapper.setObjectAverageNormalWrapper(
6172 self.
context, objID, origin.to_list(), new_normal.to_list()
6176 """Translate the object so its origin is moved to the given point."""
6178 if not isinstance(origin, vec3):
6179 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6180 context_wrapper.setObjectOriginWrapper(self.
context, objID, origin.to_list())
6185 """Rotate a single primitive about the given origin so its azimuth
6186 equals new_azimuth (radians)."""
6188 if not isinstance(origin, vec3):
6189 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6190 context_wrapper.setPrimitiveAzimuthWrapper(
6191 self.
context, uuid, origin.to_list(), float(new_azimuth)
6195 """Rotate a single primitive about the given origin so its elevation
6196 equals new_elevation (radians)."""
6198 if not isinstance(origin, vec3):
6199 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6200 context_wrapper.setPrimitiveElevationWrapper(
6201 self.
context, uuid, origin.to_list(), float(new_elevation)
6206 def setTriangleVertices(self, uuid: int, vertex0: vec3, vertex1: vec3, vertex2: vec3) ->
None:
6207 """Replace the three vertices of an existing triangle primitive."""
6209 for name, v
in ((
"vertex0", vertex0), (
"vertex1", vertex1), (
"vertex2", vertex2)):
6210 if not isinstance(v, vec3):
6211 raise ValueError(f
"{name} must be a vec3, got {type(v).__name__}")
6212 context_wrapper.setTriangleVerticesWrapper(
6213 self.
context, uuid, vertex0.to_list(), vertex1.to_list(), vertex2.to_list()
6217 """Rotate one or more primitives so their normals align with new_normal.
6219 Accepts either a single UUID (int) or a list/tuple of UUIDs.
6220 The rotation is applied about the given origin point.
6223 if not isinstance(origin, vec3):
6224 raise ValueError(f
"origin must be a vec3, got {type(origin).__name__}")
6225 if not isinstance(new_normal, vec3):
6226 raise ValueError(f
"new_normal must be a vec3, got {type(new_normal).__name__}")
6227 if isinstance(uuids_or_uuid, (list, tuple)):
6228 context_wrapper.setPrimitiveNormalBatchWrapper(
6229 self.
context, list(uuids_or_uuid), origin.to_list(), new_normal.to_list()
6232 context_wrapper.setPrimitiveNormalWrapper(
6233 self.
context, uuids_or_uuid, origin.to_list(), new_normal.to_list()
6237 """Reassign one or more primitives to belong to the given compound object.
6239 Accepts either a single UUID (int) or a list/tuple of UUIDs. Pass objID=0
6240 to detach primitive(s) from any object.
6243 if isinstance(uuids_or_uuid, (list, tuple)):
6244 context_wrapper.setPrimitiveParentObjectIDBatchWrapper(
6245 self.
context, list(uuids_or_uuid), int(objID)
6248 context_wrapper.setPrimitiveParentObjectIDWrapper(
6249 self.
context, int(uuids_or_uuid), int(objID)
6259 def setMaterialDataInt(self, material_label: str, data_label: str, value: int) ->
None:
6260 """Set int data on a material. Affects all primitives that reference it."""
6262 context_wrapper.setMaterialDataIntWrapper(self.
context, material_label, data_label, int(value))
6265 """Set unsigned int data on a material."""
6267 context_wrapper.setMaterialDataUIntWrapper(self.
context, material_label, data_label, int(value))
6270 """Set float data on a material."""
6272 context_wrapper.setMaterialDataFloatWrapper(self.
context, material_label, data_label, float(value))
6275 """Set double-precision float data on a material."""
6277 context_wrapper.setMaterialDataDoubleWrapper(self.
context, material_label, data_label, float(value))
6280 """Set string data on a material."""
6282 context_wrapper.setMaterialDataStringWrapper(self.
context, material_label, data_label, str(value))
6285 """Set vec2 data on a material."""
6287 if not isinstance(value, vec2):
6288 raise ValueError(f
"value must be a vec2, got {type(value).__name__}")
6289 context_wrapper.setMaterialDataVec2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6292 """Set vec3 data on a material."""
6294 if not isinstance(value, vec3):
6295 raise ValueError(f
"value must be a vec3, got {type(value).__name__}")
6296 context_wrapper.setMaterialDataVec3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6299 """Set vec4 data on a material."""
6301 if not isinstance(value, vec4):
6302 raise ValueError(f
"value must be a vec4, got {type(value).__name__}")
6303 context_wrapper.setMaterialDataVec4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6306 """Set int2 data on a material."""
6308 if not isinstance(value, int2):
6309 raise ValueError(f
"value must be an int2, got {type(value).__name__}")
6310 context_wrapper.setMaterialDataInt2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6313 """Set int3 data on a material."""
6315 if not isinstance(value, int3):
6316 raise ValueError(f
"value must be an int3, got {type(value).__name__}")
6317 context_wrapper.setMaterialDataInt3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6320 """Set int4 data on a material."""
6322 if not isinstance(value, int4):
6323 raise ValueError(f
"value must be an int4, got {type(value).__name__}")
6324 context_wrapper.setMaterialDataInt4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6330 return context_wrapper.getMaterialDataIntWrapper(self.
context, material_label, data_label)
6334 return context_wrapper.getMaterialDataUIntWrapper(self.
context, material_label, data_label)
6338 return context_wrapper.getMaterialDataFloatWrapper(self.
context, material_label, data_label)
6342 return context_wrapper.getMaterialDataDoubleWrapper(self.
context, material_label, data_label)
6346 return context_wrapper.getMaterialDataStringWrapper(self.
context, material_label, data_label)
6350 x, y = context_wrapper.getMaterialDataVec2Wrapper(self.
context, material_label, data_label)
6355 x, y, z = context_wrapper.getMaterialDataVec3Wrapper(self.
context, material_label, data_label)
6356 return vec3(x, y, z)
6360 x, y, z, w = context_wrapper.getMaterialDataVec4Wrapper(self.
context, material_label, data_label)
6365 x, y = context_wrapper.getMaterialDataInt2Wrapper(self.
context, material_label, data_label)
6370 x, y, z = context_wrapper.getMaterialDataInt3Wrapper(self.
context, material_label, data_label)
6371 return int3(x, y, z)
6375 x, y, z, w = context_wrapper.getMaterialDataInt4Wrapper(self.
context, material_label, data_label)
6376 return int4(x, y, z, w)
6379 """Return the HeliosDataType enum value for the given material data entry.
6381 Encoding (from Helios core): 0=INT, 1=UINT, 2=FLOAT, 3=DOUBLE,
6382 4=VEC2, 5=VEC3, 6=VEC4, 7=INT2, 8=INT3, 9=INT4, 10=STRING.
6385 return context_wrapper.getMaterialDataTypeWrapper(self.
context, material_label, data_label)
6389 def setMaterialData(self, material_label: str, data_label: str, value) ->
None:
6390 """Set material data with type detection from the Python value.
6392 Dispatches to the correct typed setter based on ``isinstance`` of ``value``.
6393 For unambiguous numeric width control (e.g., uint vs int), call the
6394 per-type method directly (``setMaterialDataUInt``, etc.).
6397 if isinstance(value, bool):
6399 context_wrapper.setMaterialDataIntWrapper(self.
context, material_label, data_label, int(value))
6400 elif isinstance(value, int):
6401 context_wrapper.setMaterialDataIntWrapper(self.
context, material_label, data_label, int(value))
6402 elif isinstance(value, float):
6403 context_wrapper.setMaterialDataFloatWrapper(self.
context, material_label, data_label, float(value))
6404 elif isinstance(value, str):
6405 context_wrapper.setMaterialDataStringWrapper(self.
context, material_label, data_label, value)
6406 elif isinstance(value, vec2):
6407 context_wrapper.setMaterialDataVec2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6408 elif isinstance(value, vec3):
6409 context_wrapper.setMaterialDataVec3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6410 elif isinstance(value, vec4):
6411 context_wrapper.setMaterialDataVec4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6412 elif isinstance(value, int2):
6413 context_wrapper.setMaterialDataInt2Wrapper(self.
context, material_label, data_label, value.x, value.y)
6414 elif isinstance(value, int3):
6415 context_wrapper.setMaterialDataInt3Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z)
6416 elif isinstance(value, int4):
6417 context_wrapper.setMaterialDataInt4Wrapper(self.
context, material_label, data_label, value.x, value.y, value.z, value.w)
6420 f
"Unsupported value type for setMaterialData: {type(value).__name__}. "
6421 f
"Supported: int, float, str, vec2, vec3, vec4, int2, int3, int4. "
6422 f
"For uint/double, call setMaterialDataUInt/Double directly."
6425 def getMaterialData(self, material_label: str, data_label: str, data_type: type =
None):
6426 """Get material data, auto-detecting the type from Helios storage if not specified.
6429 material_label: Name of the material.
6430 data_label: Data entry label.
6431 data_type: Optional Python type (int, float, str, vec2, vec3, vec4, int2,
6432 int3, int4) or string ('uint', 'double'). If ``None``, the type is
6433 queried via getMaterialDataType and dispatched automatically.
6436 if data_type
is None:
6437 t = context_wrapper.getMaterialDataTypeWrapper(self.
context, material_label, data_label)
6440 return context_wrapper.getMaterialDataIntWrapper(self.
context, material_label, data_label)
6442 return context_wrapper.getMaterialDataUIntWrapper(self.
context, material_label, data_label)
6444 return context_wrapper.getMaterialDataFloatWrapper(self.
context, material_label, data_label)
6446 return context_wrapper.getMaterialDataDoubleWrapper(self.
context, material_label, data_label)
6448 x, y = context_wrapper.getMaterialDataVec2Wrapper(self.
context, material_label, data_label)
6451 x, y, z = context_wrapper.getMaterialDataVec3Wrapper(self.
context, material_label, data_label)
6452 return vec3(x, y, z)
6454 x, y, z, w = context_wrapper.getMaterialDataVec4Wrapper(self.
context, material_label, data_label)
6455 return vec4(x, y, z, w)
6457 x, y = context_wrapper.getMaterialDataInt2Wrapper(self.
context, material_label, data_label)
6460 x, y, z = context_wrapper.getMaterialDataInt3Wrapper(self.
context, material_label, data_label)
6461 return int3(x, y, z)
6463 x, y, z, w = context_wrapper.getMaterialDataInt4Wrapper(self.
context, material_label, data_label)
6464 return int4(x, y, z, w)
6466 return context_wrapper.getMaterialDataStringWrapper(self.
context, material_label, data_label)
6467 raise ValueError(f
"Unknown HeliosDataType code: {t}")
6470 if data_type == int:
6472 if data_type == float:
6474 if data_type == str:
6476 if data_type ==
"uint":
6478 if data_type ==
"double":
6480 if data_type == vec2:
6482 if data_type == vec3:
6484 if data_type == vec4:
6486 if data_type == int2:
6488 if data_type == int3:
6490 if data_type == int4:
6493 f
"Unsupported material data type: {data_type}. Supported: int, float, str, "
6494 f
"vec2, vec3, vec4, int2, int3, int4, 'uint', 'double'."
6500 """Return the unique values stored under ``label`` across all primitives.
6502 Requires value caching to be enabled for ``label`` first via
6503 ``enablePrimitiveDataValueCaching(label)``. Supported ``dtype`` values:
6504 ``int``, ``str``, or the string ``'uint'``.
6508 return context_wrapper.getUniquePrimitiveDataValuesIntWrapper(self.
context, label)
6510 return context_wrapper.getUniquePrimitiveDataValuesUIntWrapper(self.
context, label)
6512 return context_wrapper.getUniquePrimitiveDataValuesStringWrapper(self.
context, label)
6514 f
"Unsupported dtype for getUniquePrimitiveDataValues: {dtype}. "
6515 f
"Supported: int, str, 'uint'."
6519 """Return the unique values stored under ``label`` across all compound objects.
6521 Requires value caching to be enabled for ``label`` first via
6522 ``enableObjectDataValueCaching(label)``. Supported ``dtype`` values:
6523 ``int``, ``str``, or the string ``'uint'``.
6527 return context_wrapper.getUniqueObjectDataValuesIntWrapper(self.
context, label)
6529 return context_wrapper.getUniqueObjectDataValuesUIntWrapper(self.
context, label)
6531 return context_wrapper.getUniqueObjectDataValuesStringWrapper(self.
context, label)
6533 f
"Unsupported dtype for getUniqueObjectDataValues: {dtype}. "
6534 f
"Supported: int, str, 'uint'."
6543 """Coerce a 4x4 transformation matrix input into a flat list of 16 floats.
6545 Accepts: numpy.ndarray of shape (4,4) or (16,), list/tuple of 16 floats,
6546 or nested list/tuple of shape (4,4). Helios stores transformation matrices
6547 in **row-major** order: T[i*4 + j] = element (i, j). A numpy ndarray of
6548 shape (4,4) maps directly via .ravel() since numpy is row-major by default.
6551 if isinstance(value, np.ndarray):
6552 if value.shape == (4, 4):
6553 return [float(v)
for v
in value.ravel().tolist()]
6554 if value.shape == (16,):
6555 return [float(v)
for v
in value.tolist()]
6557 f
"Matrix ndarray must have shape (4,4) or (16,), got {value.shape}"
6560 if isinstance(value, (list, tuple))
and len(value) == 4
and \
6561 all(isinstance(row, (list, tuple))
and len(row) == 4
for row
in value):
6564 flat.extend(float(v)
for v
in row)
6567 if isinstance(value, (list, tuple))
and len(value) == 16:
6568 return [float(v)
for v
in value]
6570 f
"Matrix must be a (4,4) ndarray, (16,) ndarray, list of 16 floats, "
6571 f
"or nested 4x4 list. Got: {type(value).__name__}"
6576 """Convert a flat list of 16 floats (row-major) to a (4,4) numpy ndarray."""
6577 return np.array(flat, dtype=np.float32).reshape((4, 4))
6582 """Return the object's 4x4 transformation matrix as a (4,4) float32 ndarray.
6584 Helios stores matrices in row-major order, so element (i, j) is at
6585 position [i, j] of the returned ndarray. The translation column is at
6586 positions [0, 3], [1, 3], [2, 3].
6589 flat = context_wrapper.getObjectTransformationMatrixWrapper(self.
context, int(objID))
6593 """Set the 4x4 transformation matrix on one or more compound objects.
6596 objIDs_or_objID: A single object ID (int) or a list/tuple of object IDs.
6597 T: A 4x4 matrix as numpy.ndarray((4,4) | (16,) float), list of 16 floats,
6598 or a nested 4x4 list. Row-major; T[i, j] is element (i, j).
6602 if isinstance(objIDs_or_objID, (list, tuple)):
6603 context_wrapper.setObjectTransformationMatrixBatchWrapper(
6604 self.
context, list(objIDs_or_objID), flat
6607 context_wrapper.setObjectTransformationMatrixWrapper(
6612 """Return the primitive's 4x4 transformation matrix as a (4,4) float32 ndarray
6613 (row-major; see getObjectTransformationMatrix for layout details)."""
6615 flat = context_wrapper.getPrimitiveTransformationMatrixWrapper(self.
context, int(uuid))
6619 """Set the 4x4 transformation matrix on one or more primitives.
6622 uuids_or_uuid: A single UUID (int) or a list/tuple of UUIDs.
6623 T: A 4x4 matrix; see setObjectTransformationMatrix for accepted formats.
6627 if isinstance(uuids_or_uuid, (list, tuple)):
6628 context_wrapper.setPrimitiveTransformationMatrixBatchWrapper(
6629 self.
context, list(uuids_or_uuid), flat
6632 context_wrapper.setPrimitiveTransformationMatrixWrapper(
6633 self.
context, int(uuids_or_uuid), flat
6639 """Return the axis-aligned bounding box of the domain (or a UUID subset).
6642 uuids: Optional list of primitive UUIDs to restrict the computation to.
6643 If None (default), uses every primitive in the context.
6646 ``(xbounds, ybounds, zbounds)`` where each element is a ``vec2(min, max)``.
6650 xb, yb, zb = context_wrapper.getDomainBoundingBoxWrapper(self.
context)
6652 if not isinstance(uuids, (list, tuple)):
6653 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6654 xb, yb, zb = context_wrapper.getDomainBoundingBoxFilteredWrapper(self.
context, list(uuids))
6655 return (
vec2(xb[0], xb[1]),
vec2(yb[0], yb[1]),
vec2(zb[0], zb[1]))
6658 """Return the bounding sphere of the domain (or a UUID subset).
6661 ``(center, radius)`` where ``center`` is a ``vec3`` and ``radius`` is a float.
6665 center, radius = context_wrapper.getDomainBoundingSphereWrapper(self.
context)
6667 if not isinstance(uuids, (list, tuple)):
6668 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6669 center, radius = context_wrapper.getDomainBoundingSphereFilteredWrapper(self.
context, list(uuids))
6670 return (
vec3(center[0], center[1], center[2]), float(radius))
6678 def setTubeNodes(self, objID: int, nodes: List[vec3]) ->
None:
6679 """Replace the node positions of an existing tube object."""
6681 if not isinstance(nodes, (list, tuple)):
6682 raise ValueError(f
"nodes must be a list or tuple, got {type(nodes).__name__}")
6684 for i, n
in enumerate(nodes):
6685 if not isinstance(n, vec3):
6686 raise ValueError(f
"nodes[{i}] must be a vec3, got {type(n).__name__}")
6687 flat.extend([n.x, n.y, n.z])
6688 context_wrapper.setTubeNodesWrapper(self.
context, int(objID), flat)
6690 def setTubeRadii(self, objID: int, radii: List[float]) ->
None:
6691 """Replace the per-node radii of an existing tube object."""
6693 if not isinstance(radii, (list, tuple)):
6694 raise ValueError(f
"radii must be a list or tuple, got {type(radii).__name__}")
6695 context_wrapper.setTubeRadiiWrapper(self.
context, int(objID), [float(r)
for r
in radii])
6697 def scaleTubeGirth(self, objID: int, scale_factor: float) ->
None:
6698 """Scale the radii of an existing tube object by ``scale_factor``."""
6700 context_wrapper.scaleTubeGirthWrapper(self.
context, int(objID), float(scale_factor))
6703 """Scale the lengths between tube nodes by ``scale_factor``."""
6705 context_wrapper.scaleTubeLengthWrapper(self.
context, int(objID), float(scale_factor))
6708 """Remove all tube nodes from index ``node_index`` to the end."""
6710 context_wrapper.pruneTubeNodesWrapper(self.
context, int(objID), int(node_index))
6713 color: Optional[RGBcolor] =
None,
6714 texture_file: Optional[str] =
None,
6715 uv: Optional[vec2] =
None) ->
None:
6716 """Append a new segment to an existing tube object.
6718 Pass exactly one of ``color`` (an RGBcolor) or both ``texture_file`` and
6719 ``uv`` (a vec2 of texture u-fractions) to specify how the new segment
6723 if not isinstance(node_position, vec3):
6724 raise ValueError(f
"node_position must be a vec3, got {type(node_position).__name__}")
6725 has_color = color
is not None
6726 has_texture = texture_file
is not None or uv
is not None
6727 if has_color == has_texture:
6729 "appendTubeSegment requires exactly one of (color) or "
6730 "(texture_file and uv); cannot mix or omit both."
6733 if not isinstance(color, RGBcolor):
6734 raise ValueError(f
"color must be an RGBcolor, got {type(color).__name__}")
6735 context_wrapper.appendTubeSegmentColorWrapper(
6736 self.
context, int(objID), node_position.to_list(), float(radius),
6737 [color.r, color.g, color.b]
6740 if texture_file
is None or uv
is None:
6742 "appendTubeSegment with texture requires both texture_file and uv."
6744 if not isinstance(uv, vec2):
6745 raise ValueError(f
"uv must be a vec2, got {type(uv).__name__}")
6747 texture_file, [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp']
6749 context_wrapper.appendTubeSegmentTextureWrapper(
6750 self.
context, int(objID), node_position.to_list(), float(radius),
6751 tex_path, [uv.x, uv.y]
6757 """Group the given primitives into a new polymesh compound object and return its ID."""
6759 if not isinstance(uuids, (list, tuple)):
6760 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6762 raise ValueError(
"addPolymeshObject requires at least one UUID")
6763 return context_wrapper.addPolymeshObjectWrapper(self.
context, list(uuids))
6768 """Set the color of one or more compound objects.
6770 Accepts a single object ID or list/tuple of IDs. ``color`` must be an
6771 ``RGBcolor`` or ``RGBAcolor``.
6774 if isinstance(color, RGBAcolor):
6775 comps = [color.r, color.g, color.b, color.a]
6776 if isinstance(objIDs_or_objID, (list, tuple)):
6777 context_wrapper.setObjectColorRGBABatchWrapper(self.
context, list(objIDs_or_objID), comps)
6779 context_wrapper.setObjectColorRGBAWrapper(self.
context, int(objIDs_or_objID), comps)
6780 elif isinstance(color, RGBcolor):
6781 comps = [color.r, color.g, color.b]
6782 if isinstance(objIDs_or_objID, (list, tuple)):
6783 context_wrapper.setObjectColorRGBBatchWrapper(self.
context, list(objIDs_or_objID), comps)
6785 context_wrapper.setObjectColorRGBWrapper(self.
context, int(objIDs_or_objID), comps)
6788 f
"color must be an RGBcolor or RGBAcolor, got {type(color).__name__}"
6792 """Override the texture mapping with the object's vertex color."""
6794 if isinstance(objIDs_or_objID, (list, tuple)):
6795 context_wrapper.overrideObjectTextureColorBatchWrapper(self.
context, list(objIDs_or_objID))
6797 context_wrapper.overrideObjectTextureColorWrapper(self.
context, int(objIDs_or_objID))
6800 """Restore use of the texture color (undoes overrideObjectTextureColor)."""
6802 if isinstance(objIDs_or_objID, (list, tuple)):
6803 context_wrapper.useObjectTextureColorBatchWrapper(self.
context, list(objIDs_or_objID))
6805 context_wrapper.useObjectTextureColorWrapper(self.
context, int(objIDs_or_objID))
6810 """Mark one or more primitives as dirty (geometry has been modified)."""
6812 if isinstance(uuids_or_uuid, (list, tuple)):
6813 context_wrapper.markPrimitiveDirtyBatchWrapper(self.
context, list(uuids_or_uuid))
6815 context_wrapper.markPrimitiveDirtyWrapper(self.
context, int(uuids_or_uuid))
6818 """Mark one or more primitives as clean (cancels dirty state)."""
6820 if isinstance(uuids_or_uuid, (list, tuple)):
6821 context_wrapper.markPrimitiveCleanBatchWrapper(self.
context, list(uuids_or_uuid))
6823 context_wrapper.markPrimitiveCleanWrapper(self.
context, int(uuids_or_uuid))
6828 """Set the (Nx, Ny) subdivision count of one or more tile objects."""
6830 if not isinstance(subdiv, int2):
6831 raise ValueError(f
"subdiv must be an int2, got {type(subdiv).__name__}")
6832 if isinstance(objIDs_or_objID, (list, tuple)):
6833 ids = list(objIDs_or_objID)
6835 ids = [int(objIDs_or_objID)]
6836 context_wrapper.setTileObjectSubdivisionCountWrapper(
6837 self.
context, ids, int(subdiv.x), int(subdiv.y)
6841 """Set tile object subdivision dynamically based on a target area ratio.
6843 ``area_ratio`` is the approximate ratio between the whole tile's area and an
6844 individual sub-patch's area, so each tile is subdivided into roughly
6845 ``area_ratio`` sub-patches. It must be >= 1 (a sub-patch cannot be larger than
6846 the tile). The tile's position, size, and orientation are preserved.
6851 f
"area_ratio must be >= 1 (it is the ratio of the whole tile area to an "
6852 f
"individual sub-patch area), got {area_ratio}"
6854 if isinstance(objIDs_or_objID, (list, tuple)):
6855 ids = list(objIDs_or_objID)
6857 ids = [int(objIDs_or_objID)]
6858 context_wrapper.setTileObjectSubdivisionByAreaRatioWrapper(
6859 self.
context, ids, float(area_ratio)
6869 """Return a new list with deleted UUIDs removed; the input list is not mutated.
6871 This mirrors the convention used by ``cropDomain``, which returns the
6872 survivors rather than mutating in place.
6875 if not isinstance(uuids, (list, tuple)):
6876 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6877 return context_wrapper.cleanDeletedUUIDsWrapper(self.
context, list(uuids))
6880 """Return a new list with deleted object IDs removed; input is not mutated."""
6882 if not isinstance(objIDs, (list, tuple)):
6883 raise ValueError(f
"objIDs must be a list or tuple, got {type(objIDs).__name__}")
6884 return context_wrapper.cleanDeletedObjectIDsWrapper(self.
context, list(objIDs))
6888 def writeXML(self, filename: str, uuids: Optional[List[int]] =
None, quiet: bool =
False) ->
None:
6889 """Write the context (or a UUID subset) to an XML file.
6892 filename: Output file path. Must end in .xml.
6893 uuids: Optional list of primitive UUIDs to restrict the export. If
6894 None (default), all primitives are written.
6895 quiet: Suppress informational console output.
6900 context_wrapper.writeXMLWrapper(self.
context, path, bool(quiet))
6902 if not isinstance(uuids, (list, tuple)):
6903 raise ValueError(f
"uuids must be a list or tuple, got {type(uuids).__name__}")
6904 context_wrapper.writeXMLFilteredWrapper(self.
context, path, list(uuids), bool(quiet))
6906 def writeXML_byobject(self, filename: str, objIDs: List[int], quiet: bool =
False) ->
None:
6907 """Write a subset of compound objects to an XML file."""
6910 if not isinstance(objIDs, (list, tuple)):
6911 raise ValueError(f
"objIDs must be a list or tuple, got {type(objIDs).__name__}")
6912 context_wrapper.writeXMLByObjectWrapper(self.
context, path, list(objIDs), bool(quiet))
6916 def randu(self, low=None, high=None):
6917 """Draw a uniform random number using the Context's RNG.
6920 ``randu()`` -> float in [0, 1)
6921 ``randu(low: float, high: float)`` -> float in [low, high)
6922 ``randu(low: int, high: int)`` -> int in [low, high]
6924 Whether the integer or float overload is invoked is determined by
6925 ``isinstance(low, int)``; pass ``low/high`` as Python ints for the
6929 if low
is None and high
is None:
6930 return context_wrapper.randuBasicWrapper(self.
context)
6931 if low
is None or high
is None:
6932 raise ValueError(
"randu requires both low and high, or neither.")
6933 if isinstance(low, bool)
or isinstance(high, bool):
6934 raise ValueError(
"randu bounds cannot be bool.")
6937 if isinstance(low, int)
and isinstance(high, int):
6938 return context_wrapper.randuIntRangeWrapper(self.
context, low, high)
6939 return context_wrapper.randuRangeWrapper(self.
context, float(low), float(high))
6941 def randn(self, mean=None, stddev=None) -> float:
6942 """Draw a normal random number using the Context's RNG.
6945 ``randn()`` -> standard normal (mean 0, stddev 1)
6946 ``randn(mean: float, stddev: float)`` -> N(mean, stddev**2)
6949 if mean
is None and stddev
is None:
6950 return context_wrapper.randnBasicWrapper(self.
context)
6951 if mean
is None or stddev
is None:
6952 raise ValueError(
"randn requires both mean and stddev, or neither.")
6953 return context_wrapper.randnParamsWrapper(self.
context, float(mean), float(stddev))
6957 def setLocation(self, location_or_lat, longitude=None, utc_offset=None, altitude=0.0) -> None:
6958 """Set the geographic location used by solar/radiation calculations.
6961 ``setLocation(loc: Location)``
6962 ``setLocation(latitude_deg: float, longitude_deg: float, utc_offset: float, altitude=0.0)``
6964 ``altitude`` is the height of the local Cartesian origin in meters above
6965 sea level. It is only used in the (lat, lon, utc) float form; when passing
6966 a ``Location`` object, the location's own altitude is used.
6969 if isinstance(location_or_lat, Location):
6970 if longitude
is not None or utc_offset
is not None or altitude != 0.0:
6971 raise ValueError(
"When passing a Location, do not also pass longitude/utc_offset/altitude; "
6972 "set them on the Location object instead.")
6973 loc = location_or_lat
6975 if longitude
is None or utc_offset
is None:
6977 "setLocation requires either a Location object or "
6978 "(latitude_deg, longitude_deg, utc_offset) as 3 floats."
6980 loc =
Location(float(location_or_lat), float(longitude), float(utc_offset), float(altitude))
6981 context_wrapper.setLocationWrapper(self.
context, loc.latitude, loc.longitude, loc.utc_offset, loc.altitude)
6984 """Return the Context's currently-configured geographic location."""
6986 lat, lon, utc, alt = context_wrapper.getLocationWrapper(self.
context)
6987 return Location(lat, lon, utc, alt)
6994 """Generate a colormap with ``n_colors`` entries from a named colormap.
6997 name: Helios colormap name: "hot", "cool", "lava", "rainbow", "parula",
6998 "gray", "green", "lines" or "algae" (see :meth:`getColormapNames`).
6999 n_colors: Number of colors in the returned ramp. Must be at least 2.
7002 A list of ``RGBcolor`` instances of length ``n_colors``.
7005 HeliosRuntimeError: If ``name`` is not a predefined colormap (the error lists
7006 the valid names) or ``n_colors`` is less than 2.
7009 flat = context_wrapper.generateColormapNamedWrapper(self.
context, name, int(n_colors))
7010 return [
RGBcolor(flat[i*3 + 0], flat[i*3 + 1], flat[i*3 + 2])
for i
in range(len(flat) // 3)]
7014 """Get the names of all predefined Helios colormaps.
7016 These are the names accepted by :meth:`generateColormap`,
7017 :meth:`colorPrimitiveByDataPseudocolor` and :meth:`getColormapControlPoints`
7018 (as of helios-core 1.3.89: "hot", "cool", "lava", "rainbow", "parula", "gray",
7019 "green", "lines" and "algae"). No Context instance is needed.
7022 List of colormap names.
7024 return context_wrapper.getColormapNamesWrapper()
7028 """Get the control points that define a predefined Helios colormap.
7030 A colormap is a set of colors placed at normalized positions between 0 and 1,
7031 with colors linearly interpolated between them. These control points are the
7032 single definition of each named colormap, and are also what the Visualizer's
7033 predefined colormaps are built from. No Context instance is needed.
7036 name: Colormap name (see :meth:`getColormapNames`).
7039 Tuple ``(colors, positions)``: the ``RGBcolor`` of each control point, and its
7040 normalized position, in increasing order from 0 to 1.
7043 ValueError: If ``name`` is not a string.
7044 HeliosRuntimeError: If ``name`` is not a predefined colormap (the error lists
7047 if not isinstance(name, str):
7048 raise ValueError(f
"Colormap name must be a string, got {type(name).__name__}")
7049 colors, positions = context_wrapper.getColormapControlPointsWrapper(name)
7050 return [
RGBcolor(r, g, b)
for (r, g, b)
in colors], positions
7053 """Generate one texture file per color in ``colormap`` derived from
7054 ``texture_file``. Returns the list of generated file paths.
7057 if not isinstance(colormap, (list, tuple)):
7058 raise ValueError(f
"colormap must be a list or tuple, got {type(colormap).__name__}")
7060 for i, c
in enumerate(colormap):
7061 if not isinstance(c, RGBcolor):
7062 raise ValueError(f
"colormap[{i}] must be an RGBcolor, got {type(c).__name__}")
7063 flat.extend([c.r, c.g, c.b])
7066 texture_file, [
'.png',
'.jpg',
'.jpeg',
'.tga',
'.bmp']
7068 return context_wrapper.generateTexturesFromColormapWrapper(
7069 self.
context, validated_path, flat
7073 """Return the primitive's texture transparency mask as a 2D bool ndarray.
7075 Returns None if the primitive has no associated transparency channel
7076 (e.g., it is untextured or its texture has no alpha). The returned
7077 ndarray has shape (height, width) and dtype ``bool``.
7080 result = context_wrapper.getPrimitiveTextureTransparencyDataWrapper(self.
context, int(uuid))
7083 width, height, flat = result
7084 return np.array(flat, dtype=bool).reshape((height, width))
7088 """Raise if `context`'s native Context has already been destroyed.
7090 Plugin models pass ``context.getNativePtr()`` to a C++ constructor that
7091 stores the raw pointer for the lifetime of the model. Destroying the
7092 Context (via ``__exit__``, ``__del__``, or garbage collection) frees that
7093 memory without invalidating the model's copy, so any later call
7094 dereferences freed memory and segfaults.
7096 Models must hold a Python reference to the owning Context (keeping it
7097 alive) and call this before every native call (turning an explicit close
7098 into an actionable error instead of a crash).
7101 context: The Context the model was constructed from.
7102 owner_name: Class name of the calling model, used in the message.
7105 RuntimeError: If the Context has been destroyed.
7107 if context
is None or getattr(context,
'context',
None)
is None:
7109 f
"{owner_name} is bound to a Context that has already been destroyed.\n"
7110 "The native Context was freed while this model still referenced it; "
7111 "continuing would dereference freed memory and crash the interpreter.\n"
7113 "This usually means the model outlived its Context's 'with' block:\n"
7114 " with Context() as context:\n"
7115 f
" model = {owner_name}(context)\n"
7116 " model.run() # <-- Context already destroyed here\n"
7118 f
"Fix: keep all {owner_name} usage inside the Context's 'with' block, "
7119 "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.
Tuple[List[RGBcolor], List[float]] getColormapControlPoints(str name)
Get the control points that define a predefined Helios colormap.
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)
List[str] getColormapNames()
Get the names of all predefined Helios colormaps.
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.
int getGeometryRevision(self)
Get a counter that advances whenever any primitive is modified or deleted.
_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.
int getShapeRevision(self)
Get a counter that advances whenever a primitive's shape changes or a primitive is deleted.
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.