0.1.26
Loading...
Searching...
No Matches
Context.py
Go to the documentation of this file.
1import ctypes
2import warnings
3from dataclasses import dataclass
4from typing import List, Optional, Union
5from enum import Enum
6
7import numpy as np
8
9from .wrappers import UContextWrapper as context_wrapper
10from .wrappers.DataTypes import vec2, vec3, vec4, int2, int3, int4, SphericalCoord, RGBcolor, RGBAcolor, PrimitiveType, Date, Time, Location
11from .plugins.loader import LibraryLoadError, validate_library, get_library_info
12from .plugins.registry import get_plugin_registry
13from .validation.geometry import (
14 validate_patch_params, validate_triangle_params, validate_sphere_params,
15 validate_tube_params, validate_box_params
16)
17
18
19@dataclass
20class PrimitiveInfo:
21 """
22 Physical properties and geometry information for a primitive.
23 This is separate from primitive data (user-defined key-value pairs).
24 """
25 uuid: int
26 primitive_type: PrimitiveType
27 area: float
28 normal: vec3
29 vertices: List[vec3]
30 color: RGBcolor
31 centroid: Optional[vec3] = None
32 texture_file: Optional[str] = None
33 texture_uv: Optional[List[vec2]] = None
34 solid_fraction: Optional[float] = None
35
36 def __post_init__(self):
37 """Calculate centroid from vertices if not provided."""
38 if self.centroid is None and self.vertices:
39 # Calculate centroid as average of vertices
40 total_x = sum(v.x for v in self.vertices)
41 total_y = sum(v.y for v in self.vertices)
42 total_z = sum(v.z for v in self.vertices)
43 count = len(self.vertices)
44 self.centroid = vec3(total_x / count, total_y / count, total_z / count)
45
46
47class Context:
48 """
49 Central simulation environment for PyHelios that manages 3D primitives and their data.
50
51 The Context class provides methods for:
52 - Creating geometric primitives (patches, triangles)
53 - Creating compound geometry (tiles, spheres, tubes, boxes)
54 - Loading 3D models from files (PLY, OBJ, XML)
55 - Managing primitive data (flexible key-value storage)
56 - Querying primitive properties and collections
57 - Batch operations on multiple primitives
58
59 Key features:
60 - UUID-based primitive tracking
61 - Comprehensive primitive data system with auto-type detection
62 - Efficient array-based data retrieval via getPrimitiveDataArray()
63 - Cross-platform compatibility with mock mode support
64 - Context manager protocol for resource cleanup
65
66 Example:
67 >>> with Context() as context:
68 ... # Create primitives
69 ... patch_uuid = context.addPatch(center=vec3(0, 0, 0))
70 ... triangle_uuid = context.addTriangle(vec3(0,0,0), vec3(1,0,0), vec3(0.5,1,0))
71 ...
72 ... # Set primitive data
73 ... context.setPrimitiveDataFloat(patch_uuid, "temperature", 25.5)
74 ... context.setPrimitiveDataFloat(triangle_uuid, "temperature", 30.2)
75 ...
76 ... # Get data efficiently as NumPy array
77 ... temps = context.getPrimitiveDataArray([patch_uuid, triangle_uuid], "temperature")
78 ... print(temps) # [25.5 30.2]
79 """
80
81 def __init__(self):
82 # Initialize plugin registry for availability checking
83 self._plugin_registry = get_plugin_registry()
84
85 # Track Context lifecycle state for better error messages
86 self._lifecycle_state = 'initializing'
87
88 # Check if we're in mock/development mode
89 library_info = get_library_info()
90 if library_info.get('is_mock', False):
91 # In mock mode, don't validate but warn that functionality is limited
92 print("Warning: PyHelios running in development mock mode - functionality is limited")
93 print("Available plugins: None (mock mode)")
94 self.context = None # Mock context
95 self._lifecycle_state = 'mock_mode'
96 return
97
98 # Validate native library is properly loaded before creating context
99 try:
100 if not validate_library():
101 raise LibraryLoadError(
102 "Native Helios library validation failed. Some required functions are missing. "
103 "Try rebuilding the native library: build_scripts/build_helios"
104 )
105 except LibraryLoadError:
106 raise
107 except Exception as e:
108 raise LibraryLoadError(
109 f"Failed to validate native Helios library: {e}. "
110 f"To enable development mode without native libraries, set PYHELIOS_DEV_MODE=1"
111 )
112
113 # Create the context - this will fail if library isn't properly loaded
114 try:
115 self.context = context_wrapper.createContext()
116 if self.context is None:
117 self._lifecycle_state = 'creation_failed'
118 raise LibraryLoadError(
119 "Failed to create Helios context. Native library may not be functioning correctly."
120 )
121
122 self._lifecycle_state = 'active'
123
124 except Exception as e:
125 self._lifecycle_state = 'creation_failed'
126 raise LibraryLoadError(
127 f"Failed to create Helios context: {e}. "
128 f"Ensure native libraries are built and accessible."
129 )
130
131 def _check_context_available(self):
132 """Helper method to check if context is available with detailed error messages."""
133 if self.context is None:
134 # Provide specific error message based on lifecycle state
135 if self._lifecycle_state == 'mock_mode':
136 raise RuntimeError(
137 "Context is in mock mode - native functionality not available.\n"
138 "Build native libraries with 'python build_scripts/build_helios.py' or set PYHELIOS_DEV_MODE=1 for development."
139 )
140 elif self._lifecycle_state == 'cleaned_up':
141 raise RuntimeError(
142 "Context has been cleaned up and is no longer usable.\n"
143 "This usually means you're trying to use a Context outside its 'with' statement scope.\n"
144 "\n"
145 "Fix: Ensure all Context usage is inside the 'with Context() as context:' block:\n"
146 " with Context() as context:\n"
147 " # All context operations must be here\n"
148 " with SomePlugin(context) as plugin:\n"
149 " plugin.do_something()\n"
150 " with Visualizer() as vis:\n"
151 " vis.buildContextGeometry(context) # Still inside Context scope\n"
152 " # Context is cleaned up here - cannot use context after this point"
153 )
154 elif self._lifecycle_state == 'creation_failed':
155 raise RuntimeError(
156 "Context creation failed - native functionality not available.\n"
157 "Build native libraries with 'python build_scripts/build_helios.py'"
158 )
159 else:
160 # Fallback for unknown states
161 raise RuntimeError(
162 f"Context is not available (state: {self._lifecycle_state}).\n"
163 "Build native libraries with 'python build_scripts/build_helios.py' or set PYHELIOS_DEV_MODE=1 for development."
164 )
165
166 def _validate_uuid(self, uuid: int):
167 """Validate that a UUID exists in this context.
168
169 Args:
170 uuid: The UUID to validate
171
172 Raises:
173 RuntimeError: If UUID is invalid or doesn't exist in context
174 """
175 # First check if it's a reasonable UUID value
176 if not isinstance(uuid, int) or uuid < 0:
177 raise RuntimeError(f"Invalid UUID: {uuid}. UUIDs must be non-negative integers.")
179 # Check if UUID exists in context by getting all valid UUIDs
180 try:
181 valid_uuids = self.getAllUUIDs()
182 if uuid not in valid_uuids:
183 raise RuntimeError(f"UUID {uuid} does not exist in context. Valid UUIDs: {valid_uuids[:10]}{'...' if len(valid_uuids) > 10 else ''}")
184 except RuntimeError:
185 # Re-raise RuntimeError (validation failed)
186 raise
187 except Exception:
188 # If we can't get valid UUIDs due to other issues (e.g., mock mode), skip validation
189 # The _check_context_available() call will have already caught mock mode
190 pass
191
192
193 def _validate_file_path(self, filename: str, expected_extensions: List[str] = None) -> str:
194 """Validate and normalize file path for security.
195
196 Args:
197 filename: File path to validate
198 expected_extensions: List of allowed file extensions (e.g., ['.ply', '.obj'])
199
200 Returns:
201 Normalized absolute path
202
203
204 Raises:
205 ValueError: If path is invalid or potentially dangerous
206 FileNotFoundError: If file does not exist
207 """
208 import os.path
209
210 # Convert to absolute path and normalize
211 abs_path = os.path.abspath(filename)
212
213 # Check for path traversal attempts by verifying the resolved path is safe
214 # Allow relative paths with .. as long as they resolve to valid absolute paths
215 normalized_path = os.path.normpath(abs_path)
216 if abs_path != normalized_path:
217 raise ValueError(f"Invalid file path (potential path traversal): {filename}")
218
219 # Check file extension first (before checking existence) - better UX
220 if expected_extensions:
221 file_ext = os.path.splitext(abs_path)[1].lower()
222 if file_ext not in [ext.lower() for ext in expected_extensions]:
223 raise ValueError(f"Invalid file extension '{file_ext}'. Expected one of: {expected_extensions}")
224
225 # Check if file exists
226 if not os.path.exists(abs_path):
227 raise FileNotFoundError(f"File not found: {abs_path}")
228
229 # Check if it's actually a file (not a directory)
230 if not os.path.isfile(abs_path):
231 raise ValueError(f"Path is not a file: {abs_path}")
232
233 return abs_path
234
235 def _validate_output_file_path(self, filename: str, expected_extensions: List[str] = None) -> str:
236 """Validate and normalize output file path for security.
237
238 Args:
239 filename: Output file path to validate
240 expected_extensions: List of allowed file extensions (e.g., ['.ply', '.obj'])
241
242 Returns:
243 Normalized absolute path
244
245 Raises:
246 ValueError: If path is invalid or potentially dangerous
247 PermissionError: If output directory is not writable
248 """
249 import os.path
250
251 # Check for empty filename
252 if not filename or not filename.strip():
253 raise ValueError("Filename cannot be empty")
254
255 # Convert to absolute path and normalize
256 abs_path = os.path.abspath(filename)
257
258 # Check for path traversal attempts
259 normalized_path = os.path.normpath(abs_path)
260 if abs_path != normalized_path:
261 raise ValueError(f"Invalid file path (potential path traversal): {filename}")
262
263 # Check file extension
264 if expected_extensions:
265 file_ext = os.path.splitext(abs_path)[1].lower()
266 if file_ext not in [ext.lower() for ext in expected_extensions]:
267 raise ValueError(f"Invalid file extension '{file_ext}'. Expected one of: {expected_extensions}")
268
269 # Check if output directory exists and is writable
270 output_dir = os.path.dirname(abs_path)
271 if not os.path.exists(output_dir):
272 raise ValueError(f"Output directory does not exist: {output_dir}")
273 if not os.access(output_dir, os.W_OK):
274 raise PermissionError(f"Output directory is not writable: {output_dir}")
275
276 return abs_path
277
278 def __enter__(self):
279 return self
280
281 def __exit__(self, exc_type, exc_value, traceback):
282 if self.context is not None:
283 context_wrapper.destroyContext(self.context)
284 self.context = None # Prevent double deletion
285 self._lifecycle_state = 'cleaned_up'
287 def __del__(self):
288 """Destructor to ensure C++ resources freed even without 'with' statement."""
289 if hasattr(self, 'context') and self.context is not None:
290 try:
291 context_wrapper.destroyContext(self.context)
292 self.context = None
293 self._lifecycle_state = 'cleaned_up'
294 except Exception as e:
295 # __del__ may run during interpreter shutdown, when module
296 # globals and the import machinery are already torn down. Both
297 # the warn and any fallback must therefore be able to fail
298 # without escaping: an exception here cannot propagate to the
299 # caller, it only produces an "Exception ignored in" traceback
300 # that hides the original error.
301 try:
302 warnings.warn(f"Error in Context.__del__: {e}")
303 except BaseException:
304 pass
305
306 def getNativePtr(self):
308 return self.context
309
310 def markGeometryClean(self):
312 context_wrapper.markGeometryClean(self.context)
313
316 context_wrapper.markGeometryDirty(self.context)
317
319 def isGeometryDirty(self) -> bool:
321 return context_wrapper.isGeometryDirty(self.context)
323 def seedRandomGenerator(self, seed: int):
324 """
325 Seed the random number generator for reproducible stochastic results.
326
327 Args:
328 seed: Integer seed value for random number generation
329
330 Note:
331 This is critical for reproducible results in stochastic simulations
332 (e.g., LiDAR scans with beam divergence, random perturbations).
333 """
335 context_wrapper.helios_lib.seedRandomGenerator(self.context, seed)
336
337 @validate_patch_params
338 def addPatch(self, center: vec3 = vec3(0, 0, 0), size: vec2 = vec2(1, 1), rotation: Optional[SphericalCoord] = None, color: Optional[RGBcolor] = None) -> int:
340 rotation = rotation or SphericalCoord(1, 0, 0) # radius=1, elevation=0, azimuth=0 (no effective rotation)
341 color = color or RGBcolor(1, 1, 1)
342 # C++ interface expects [radius, elevation, azimuth] (3 values), not [radius, elevation, zenith, azimuth] (4 values)
343 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
344 return context_wrapper.addPatchWithCenterSizeRotationAndColor(self.context, center.to_list(), size.to_list(), rotation_list, color.to_list())
345
346 def addPatchTextured(self, center: vec3, size: vec2, texture_file: str,
347 rotation: Optional[SphericalCoord] = None,
348 uv_center: Optional[vec2] = None,
349 uv_size: Optional[vec2] = None) -> int:
350 """Add a textured patch primitive to the context.
351
352 Creates a rectangular patch with a texture image mapped to its surface.
353
354 Args:
355 center: 3D position of the patch center
356 size: Width and height of the patch
357 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
358 rotation: Optional spherical rotation (defaults to no rotation)
359 uv_center: Optional UV center of texture map (required if uv_size is provided)
360 uv_size: Optional UV size of texture map (required if uv_center is provided)
361
362 Returns:
363 UUID of the created textured patch primitive
364
365 Raises:
366 ValueError: If arguments have wrong types or UV params are partially specified
367 FileNotFoundError: If texture file doesn't exist
368 RuntimeError: If context is in mock mode
369
370 Example:
371 >>> context = Context()
372 >>> uuid = context.addPatchTextured(
373 ... center=vec3(0, 0, 0),
374 ... size=vec2(2, 2),
375 ... texture_file="texture.png"
376 ... )
377 """
379
380 if not isinstance(center, vec3):
381 raise ValueError(f"center must be a vec3, got {type(center).__name__}")
382 if not isinstance(size, vec2):
383 raise ValueError(f"size must be a vec2, got {type(size).__name__}")
384 if not isinstance(texture_file, str):
385 raise ValueError(f"texture_file must be a str, got {type(texture_file).__name__}")
386 if rotation is not None and not isinstance(rotation, SphericalCoord):
387 raise ValueError(f"rotation must be a SphericalCoord, got {type(rotation).__name__}")
388
389 if (uv_center is None) != (uv_size is None):
390 raise ValueError("uv_center and uv_size must both be provided or both omitted")
391 if uv_center is not None and not isinstance(uv_center, vec2):
392 raise ValueError(f"uv_center must be a vec2, got {type(uv_center).__name__}")
393 if uv_size is not None and not isinstance(uv_size, vec2):
394 raise ValueError(f"uv_size must be a vec2, got {type(uv_size).__name__}")
395
396 validated_texture_file = self._validate_file_path(texture_file,
397 ['.png', '.jpg', '.jpeg', '.tga', '.bmp'])
398
399 rotation = rotation or SphericalCoord(1, 0, 0)
400 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
401
402 if uv_center is not None:
403 return context_wrapper.addPatchWithTextureAndUV(
404 self.context, center.to_list(), size.to_list(), rotation_list,
405 validated_texture_file, uv_center.to_list(), uv_size.to_list()
406 )
407 else:
408 return context_wrapper.addPatchWithTexture(
409 self.context, center.to_list(), size.to_list(), rotation_list,
410 validated_texture_file
411 )
412
413 @validate_triangle_params
414 def addTriangle(self, vertex0: vec3, vertex1: vec3, vertex2: vec3, color: Optional[RGBcolor] = None) -> int:
415 """Add a triangle primitive to the context
416
417 Args:
418 vertex0: First vertex of the triangle
419 vertex1: Second vertex of the triangle
420 vertex2: Third vertex of the triangle
421 color: Optional triangle color (defaults to white)
422
423 Returns:
424 UUID of the created triangle primitive
425 """
427 if color is None:
428 return context_wrapper.addTriangle(self.context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list())
429 else:
430 return context_wrapper.addTriangleWithColor(self.context, vertex0.to_list(), vertex1.to_list(), vertex2.to_list(), color.to_list())
431
432 def addTriangleTextured(self, vertex0: vec3, vertex1: vec3, vertex2: vec3,
433 texture_file: str, uv0: vec2, uv1: vec2, uv2: vec2) -> int:
434 """Add a textured triangle primitive to the context
435
436 Creates a triangle with texture mapping. The texture image is mapped to the triangle
437 surface using UV coordinates, where (0,0) represents the top-left corner of the image
438 and (1,1) represents the bottom-right corner.
439
440 Args:
441 vertex0: First vertex of the triangle
442 vertex1: Second vertex of the triangle
443 vertex2: Third vertex of the triangle
444 texture_file: Path to texture image file (supports PNG, JPG, JPEG, TGA, BMP)
445 uv0: UV texture coordinates for first vertex
446 uv1: UV texture coordinates for second vertex
447 uv2: UV texture coordinates for third vertex
448
449 Returns:
450 UUID of the created textured triangle primitive
451
452 Raises:
453 ValueError: If texture file path is invalid
454 FileNotFoundError: If texture file doesn't exist
455 RuntimeError: If context is in mock mode
456
457 Example:
458 >>> context = Context()
459 >>> # Create a textured triangle
460 >>> vertex0 = vec3(0, 0, 0)
461 >>> vertex1 = vec3(1, 0, 0)
462 >>> vertex2 = vec3(0.5, 1, 0)
463 >>> uv0 = vec2(0, 0) # Bottom-left of texture
464 >>> uv1 = vec2(1, 0) # Bottom-right of texture
465 >>> uv2 = vec2(0.5, 1) # Top-center of texture
466 >>> uuid = context.addTriangleTextured(vertex0, vertex1, vertex2,
467 ... "texture.png", uv0, uv1, uv2)
468 """
470
471 # Parameter type validation
472 for name, val in [("vertex0", vertex0), ("vertex1", vertex1), ("vertex2", vertex2)]:
473 if not isinstance(val, vec3):
474 raise ValueError(f"{name} must be a vec3, got {type(val).__name__}")
475 for name, val in [("uv0", uv0), ("uv1", uv1), ("uv2", uv2)]:
476 if not isinstance(val, vec2):
477 raise ValueError(f"{name} must be a vec2, got {type(val).__name__}")
478
479 # Validate texture file path
480 validated_texture_file = self._validate_file_path(texture_file,
481 ['.png', '.jpg', '.jpeg', '.tga', '.bmp'])
482
483 # Call the wrapper function
484 return context_wrapper.addTriangleWithTexture(
485 self.context,
486 vertex0.to_list(), vertex1.to_list(), vertex2.to_list(),
487 validated_texture_file,
488 uv0.to_list(), uv1.to_list(), uv2.to_list()
489 )
490
491 def getPrimitiveType(self, uuid):
492 """Get the type of a primitive or multiple primitives.
493
494 Args:
495 uuid: Single UUID (int) or list of UUIDs
496
497 Returns:
498 PrimitiveType for single UUID, or np.ndarray of shape (N,) uint32 for list
499 """
501 if isinstance(uuid, (list, tuple)):
502 if not uuid:
503 return np.empty((0,), dtype=np.uint32)
504 ptr, size = context_wrapper.getBatchPrimitiveTypes(self.context, uuid)
505 if size == 0 or not ptr:
506 return np.empty((0,), dtype=np.uint32)
507 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
508 primitive_type = context_wrapper.getPrimitiveType(self.context, uuid)
509 return PrimitiveType(primitive_type)
510
511 def getPrimitiveArea(self, uuid):
512 """Get the area of a primitive or multiple primitives.
513
514 Args:
515 uuid: Single UUID (int) or list of UUIDs
516
517 Returns:
518 float for single UUID, or np.ndarray of shape (N,) for list
519 """
521 if isinstance(uuid, (list, tuple)):
522 if not uuid:
523 return np.empty((0,), dtype=np.float32)
524 ptr, size = context_wrapper.getBatchPrimitiveAreas(self.context, uuid)
525 if size == 0 or not ptr:
526 return np.empty((0,), dtype=np.float32)
527 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
528 return context_wrapper.getPrimitiveArea(self.context, uuid)
529
530 def getPrimitiveNormal(self, uuid):
531 """Get the normal vector of a primitive or multiple primitives.
532
533 Args:
534 uuid: Single UUID (int) or list of UUIDs
535
536 Returns:
537 vec3 for single UUID, or np.ndarray of shape (N, 3) for list
538 """
540 if isinstance(uuid, (list, tuple)):
541 if not uuid:
542 return np.empty((0, 3), dtype=np.float32)
543 ptr, size = context_wrapper.getBatchPrimitiveNormals(self.context, uuid)
544 if size == 0 or not ptr:
545 return np.empty((0, 3), dtype=np.float32)
546 return np.ctypeslib.as_array(ptr, shape=(size,)).copy().reshape(-1, 3)
547 normal_ptr = context_wrapper.getPrimitiveNormal(self.context, uuid)
548 return vec3(normal_ptr[0], normal_ptr[1], normal_ptr[2])
549
550 def getPrimitiveVertices(self, uuid):
551 """Get vertices of a primitive or multiple primitives.
552
553 Args:
554 uuid: Single UUID (int) or list of UUIDs
555
556 Returns:
557 List[vec3] for single UUID, or tuple of (flat_data, offsets) for list
558 where flat_data is a float32 ndarray and offsets is a uint32 ndarray
559 of length N+1. Vertices for primitive i are at
560 flat_data[offsets[i]:offsets[i+1]].
561 """
563 if isinstance(uuid, (list, tuple)):
564 if not uuid:
565 return (np.empty((0,), dtype=np.float32), np.zeros((1,), dtype=np.uint32))
566 ptr, offsets, total = context_wrapper.getBatchPrimitiveVertices(self.context, uuid)
567 offsets_arr = np.array(offsets, dtype=np.uint32)
568 if total == 0 or not ptr:
569 return (np.empty((0,), dtype=np.float32), offsets_arr)
570 data = np.ctypeslib.as_array(ptr, shape=(total,)).copy()
571 return (data, offsets_arr)
572 size = ctypes.c_uint()
573 vertices_ptr = context_wrapper.getPrimitiveVertices(self.context, uuid, ctypes.byref(size))
574 # size.value is the total number of floats (3 per vertex), not the number of vertices
575 vertices_list = ctypes.cast(vertices_ptr, ctypes.POINTER(ctypes.c_float * size.value)).contents
576 vertices = [vec3(vertices_list[i], vertices_list[i+1], vertices_list[i+2]) for i in range(0, size.value, 3)]
577 return vertices
578
579 def getPrimitiveColor(self, uuid):
580 """Get the color of a primitive or multiple primitives.
581
582 Args:
583 uuid: Single UUID (int) or list of UUIDs
584
585 Returns:
586 RGBcolor for single UUID, or np.ndarray of shape (N, 3) for list
587 """
589 if isinstance(uuid, (list, tuple)):
590 if not uuid:
591 return np.empty((0, 3), dtype=np.float32)
592 ptr, size = context_wrapper.getBatchPrimitiveColors(self.context, uuid)
593 if size == 0 or not ptr:
594 return np.empty((0, 3), dtype=np.float32)
595 return np.ctypeslib.as_array(ptr, shape=(size,)).copy().reshape(-1, 3)
596 color_ptr = context_wrapper.getPrimitiveColor(self.context, uuid)
597 return RGBcolor(color_ptr[0], color_ptr[1], color_ptr[2])
598
599 def getPrimitiveCount(self) -> int:
601 return context_wrapper.getPrimitiveCount(self.context)
602
603 def doesPrimitiveExist(self, uuid) -> bool:
604 """Check if a primitive exists for a given UUID or list of UUIDs.
605
606 Args:
607 uuid: A single UUID (int) or a list of UUIDs.
608
609 Returns:
610 True if the primitive(s) exist, False otherwise.
611 For a list, returns True only if ALL primitives exist.
612 """
614 if isinstance(uuid, (list, tuple)):
615 arr = (ctypes.c_uint * len(uuid))(*uuid)
616 return context_wrapper.doesPrimitiveExistBatch(self.context, arr, len(uuid))
617 return context_wrapper.doesPrimitiveExist(self.context, uuid)
618
619 def getAllUUIDs(self) -> List[int]:
621 size = ctypes.c_uint()
622 uuids_ptr = context_wrapper.getAllUUIDs(self.context, ctypes.byref(size))
623 return list(uuids_ptr[:size.value])
624
625 def getObjectCount(self) -> int:
627 return context_wrapper.getObjectCount(self.context)
628
629 def getAllObjectIDs(self) -> List[int]:
631 size = ctypes.c_uint()
632 objectids_ptr = context_wrapper.getAllObjectIDs(self.context, ctypes.byref(size))
633 return list(objectids_ptr[:size.value])
634
635 def getPrimitiveInfo(self, uuid: int) -> PrimitiveInfo:
636 """
637 Get physical properties and geometry information for a single primitive.
638
639 Args:
640 uuid: UUID of the primitive
641
642 Returns:
643 PrimitiveInfo object containing physical properties and geometry
644 """
645 primitive_type = self.getPrimitiveType(uuid)
646 area = self.getPrimitiveArea(uuid)
647 normal = self.getPrimitiveNormal(uuid)
648 vertices = self.getPrimitiveVertices(uuid)
649 color = self.getPrimitiveColor(uuid)
650
651 # Texture/solid-fraction getters are absent from older library builds, in
652 # which case the wrappers raise NotImplementedError and these fields stay
653 # None. Only that specific case is tolerated - a genuine native failure
654 # must propagate rather than be reported as missing data, and each getter
655 # is attempted independently so one failure cannot suppress the others.
656 texture_file = None
657 texture_uv = None
658 solid_fraction = None
659 try:
660 tf = self.getPrimitiveTextureFile(uuid)
661 if tf:
662 texture_file = tf
663 except NotImplementedError:
664 pass
665 try:
666 texture_uv = self.getPrimitiveTextureUV(uuid)
667 if not texture_uv:
668 texture_uv = None
669 except NotImplementedError:
670 texture_uv = None
671 try:
672 solid_fraction = self.getPrimitiveSolidFraction(uuid)
673 except NotImplementedError:
674 solid_fraction = None
675
676 return PrimitiveInfo(
677 uuid=uuid,
678 primitive_type=primitive_type,
679 area=area,
680 normal=normal,
681 vertices=vertices,
682 color=color,
683 texture_file=texture_file,
684 texture_uv=texture_uv,
685 solid_fraction=solid_fraction,
686 )
687
688 def getAllPrimitiveInfo(self) -> List[PrimitiveInfo]:
689 """
690 Get physical properties and geometry information for all primitives in the context.
691
692 Returns:
693 List of PrimitiveInfo objects for all primitives
694 """
695 all_uuids = self.getAllUUIDs()
696 return [self.getPrimitiveInfo(uuid) for uuid in all_uuids]
697
698 def getPrimitivesInfoForObject(self, object_id: int) -> List[PrimitiveInfo]:
699 """
700 Get physical properties and geometry information for all primitives belonging to a specific object.
701
702 Args:
703 object_id: ID of the object
704
705 Returns:
706 List of PrimitiveInfo objects for primitives in the object
707 """
708 object_uuids = context_wrapper.getObjectPrimitiveUUIDs(self.context, object_id)
709 return [self.getPrimitiveInfo(uuid) for uuid in object_uuids]
710
711 # Compound geometry methods
712 def addTile(self, center: vec3 = vec3(0, 0, 0), size: vec2 = vec2(1, 1),
713 rotation: Optional[SphericalCoord] = None, subdiv: int2 = int2(1, 1),
714 color: Optional[RGBcolor] = None) -> List[int]:
715 """
716 Add a subdivided patch (tile) to the context.
717
718 A tile is a patch subdivided into a regular grid of smaller patches,
719 useful for creating detailed surfaces or terrain.
720
721 Args:
722 center: 3D coordinates of tile center (default: origin)
723 size: Width and height of the tile (default: 1x1)
724 rotation: Orientation of the tile (default: no rotation)
725 subdiv: Number of subdivisions in x and y directions (default: 1x1)
726 color: Color of the tile (default: white)
727
728 Returns:
729 List of UUIDs for all patches created in the tile
730
731 Example:
732 >>> context = Context()
733 >>> # Create a 2x2 meter tile subdivided into 4x4 patches
734 >>> tile_uuids = context.addTile(
735 ... center=vec3(0, 0, 1),
736 ... size=vec2(2, 2),
737 ... subdiv=int2(4, 4),
738 ... color=RGBcolor(0.5, 0.8, 0.2)
739 ... )
740 >>> print(f"Created {len(tile_uuids)} patches")
741 """
743
744 # Parameter type validation
745 if not isinstance(center, vec3):
746 raise ValueError(f"Center must be a vec3, got {type(center).__name__}")
747 if not isinstance(size, vec2):
748 raise ValueError(f"Size must be a vec2, got {type(size).__name__}")
749 if rotation is not None and not isinstance(rotation, SphericalCoord):
750 raise ValueError(f"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
751 if not isinstance(subdiv, int2):
752 raise ValueError(f"Subdiv must be an int2, got {type(subdiv).__name__}")
753 if color is not None and not isinstance(color, RGBcolor):
754 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
755
756 # Parameter value validation
757 if any(s <= 0 for s in size.to_list()):
758 raise ValueError("All size dimensions must be positive")
759 if any(s <= 0 for s in subdiv.to_list()):
760 raise ValueError("All subdivision counts must be positive")
761
762 rotation = rotation or SphericalCoord(1, 0, 0)
763 color = color or RGBcolor(1, 1, 1)
764
765 # Extract only radius, elevation, azimuth for C++ interface
766 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
767
768 if color and not (color.r == 1.0 and color.g == 1.0 and color.b == 1.0):
769 return context_wrapper.addTileWithColor(
770 self.context, center.to_list(), size.to_list(),
771 rotation_list, subdiv.to_list(), color.to_list()
772 )
773 else:
774 return context_wrapper.addTile(
775 self.context, center.to_list(), size.to_list(),
776 rotation_list, subdiv.to_list()
777 )
778
779 @validate_sphere_params
780 def addSphere(self, center: vec3 = vec3(0, 0, 0), radius: float = 1.0,
781 ndivs: int = 10, color: Optional[RGBcolor] = None) -> List[int]:
782 """
783 Add a sphere to the context.
784
785 The sphere is tessellated into triangular faces based on the specified
786 number of divisions.
787
788 Args:
789 center: 3D coordinates of sphere center (default: origin)
790 radius: Radius of the sphere (default: 1.0)
791 ndivs: Number of divisions for tessellation (default: 10)
792 Higher values create smoother spheres but more triangles
793 color: Color of the sphere (default: white)
794
795 Returns:
796 List of UUIDs for all triangles created in the sphere
797
798 Example:
799 >>> context = Context()
800 >>> # Create a red sphere at (1, 2, 3) with radius 0.5
801 >>> sphere_uuids = context.addSphere(
802 ... center=vec3(1, 2, 3),
803 ... radius=0.5,
804 ... ndivs=20,
805 ... color=RGBcolor(1, 0, 0)
806 ... )
807 >>> print(f"Created sphere with {len(sphere_uuids)} triangles")
808 """
810
811 # Parameter type validation
812 if not isinstance(center, vec3):
813 raise ValueError(f"Center must be a vec3, got {type(center).__name__}")
814 if not isinstance(radius, (int, float)):
815 raise ValueError(f"Radius must be a number, got {type(radius).__name__}")
816 if not isinstance(ndivs, int):
817 raise ValueError(f"Ndivs must be an integer, got {type(ndivs).__name__}")
818 if color is not None and not isinstance(color, RGBcolor):
819 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
820
821 # Parameter value validation
822 if radius <= 0:
823 raise ValueError("Sphere radius must be positive")
824 if ndivs < 3:
825 raise ValueError("Number of divisions must be at least 3")
826
827 if color:
828 return context_wrapper.addSphereWithColor(
829 self.context, ndivs, center.to_list(), radius, color.to_list()
830 )
831 else:
832 return context_wrapper.addSphere(
833 self.context, ndivs, center.to_list(), radius
834 )
835
836 @validate_tube_params
837 def addTube(self, nodes: List[vec3], radii: Union[float, List[float]],
838 ndivs: int = 6, colors: Optional[Union[RGBcolor, List[RGBcolor]]] = None) -> List[int]:
839 """
840 Add a tube (pipe/cylinder) to the context.
841
842 The tube is defined by a series of nodes (path) with radius at each node.
843 It's tessellated into triangular faces based on the number of radial divisions.
844
845 Args:
846 nodes: List of 3D points defining the tube path (at least 2 nodes)
847 radii: Radius at each node. Can be:
848 - Single float: constant radius for all nodes
849 - List of floats: radius for each node (must match nodes length)
850 ndivs: Number of radial divisions (default: 6)
851 Higher values create smoother tubes but more triangles
852 colors: Colors at each node. Can be:
853 - None: white tube
854 - Single RGBcolor: constant color for all nodes
855 - List of RGBcolor: color for each node (must match nodes length)
856
857 Returns:
858 List of UUIDs for all triangles created in the tube
859
860 Example:
861 >>> context = Context()
862 >>> # Create a curved tube with varying radius
863 >>> nodes = [vec3(0, 0, 0), vec3(1, 0, 0), vec3(2, 1, 0)]
864 >>> radii = [0.1, 0.2, 0.1]
865 >>> colors = [RGBcolor(1, 0, 0), RGBcolor(0, 1, 0), RGBcolor(0, 0, 1)]
866 >>> tube_uuids = context.addTube(nodes, radii, ndivs=8, colors=colors)
867 >>> print(f"Created tube with {len(tube_uuids)} triangles")
868 """
870
871 # Parameter type validation
872 if not isinstance(nodes, (list, tuple)):
873 raise ValueError(f"Nodes must be a list or tuple, got {type(nodes).__name__}")
874 if not isinstance(ndivs, int):
875 raise ValueError(f"Ndivs must be an integer, got {type(ndivs).__name__}")
876 if colors is not None and not isinstance(colors, (RGBcolor, list, tuple)):
877 raise ValueError(f"Colors must be RGBcolor, list, tuple, or None, got {type(colors).__name__}")
878
879 # Parameter value validation
880 if len(nodes) < 2:
881 raise ValueError("Tube requires at least 2 nodes")
882 if ndivs < 3:
883 raise ValueError("Number of radial divisions must be at least 3")
884
885 # Handle radius parameter
886 if isinstance(radii, (int, float)):
887 radii_list = [float(radii)] * len(nodes)
888 else:
889 radii_list = [float(r) for r in radii]
890 if len(radii_list) != len(nodes):
891 raise ValueError(f"Number of radii ({len(radii_list)}) must match number of nodes ({len(nodes)})")
892
893 # Validate radii
894 if any(r <= 0 for r in radii_list):
895 raise ValueError("All radii must be positive")
896
897 # Convert nodes to flat list
898 nodes_flat = []
899 for node in nodes:
900 nodes_flat.extend(node.to_list())
901
902 # Handle colors parameter
903 if colors is None:
904 return context_wrapper.addTube(self.context, ndivs, nodes_flat, radii_list)
905 elif isinstance(colors, RGBcolor):
906 # Single color for all nodes
907 colors_flat = colors.to_list() * len(nodes)
908 else:
909 # List of colors
910 if len(colors) != len(nodes):
911 raise ValueError(f"Number of colors ({len(colors)}) must match number of nodes ({len(nodes)})")
912 colors_flat = []
913 for color in colors:
914 colors_flat.extend(color.to_list())
915
916 return context_wrapper.addTubeWithColor(self.context, ndivs, nodes_flat, radii_list, colors_flat)
917
918 @validate_box_params
919 def addBox(self, center: vec3 = vec3(0, 0, 0), size: vec3 = vec3(1, 1, 1),
920 subdiv: int3 = int3(1, 1, 1), color: Optional[RGBcolor] = None) -> List[int]:
921 """
922 Add a rectangular box to the context.
923
924 The box is subdivided into patches on each face based on the specified
925 subdivisions.
926
927 Args:
928 center: 3D coordinates of box center (default: origin)
929 size: Width, height, and depth of the box (default: 1x1x1)
930 subdiv: Number of subdivisions in x, y, and z directions (default: 1x1x1)
931 Higher values create more detailed surfaces
932 color: Color of the box (default: white)
933
934 Returns:
935 List of UUIDs for all patches created on the box faces
936
937 Example:
938 >>> context = Context()
939 >>> # Create a blue box subdivided for detail
940 >>> box_uuids = context.addBox(
941 ... center=vec3(0, 0, 2),
942 ... size=vec3(2, 1, 0.5),
943 ... subdiv=int3(4, 2, 1),
944 ... color=RGBcolor(0, 0, 1)
945 ... )
946 >>> print(f"Created box with {len(box_uuids)} patches")
947 """
949
950 # Parameter type validation
951 if not isinstance(center, vec3):
952 raise ValueError(f"Center must be a vec3, got {type(center).__name__}")
953 if not isinstance(size, vec3):
954 raise ValueError(f"Size must be a vec3, got {type(size).__name__}")
955 if not isinstance(subdiv, int3):
956 raise ValueError(f"Subdiv must be an int3, got {type(subdiv).__name__}")
957 if color is not None and not isinstance(color, RGBcolor):
958 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
959
960 # Parameter value validation
961 if any(s <= 0 for s in size.to_list()):
962 raise ValueError("All box dimensions must be positive")
963 if any(s < 1 for s in subdiv.to_list()):
964 raise ValueError("All subdivision counts must be at least 1")
965
966 if color:
967 return context_wrapper.addBoxWithColor(
968 self.context, center.to_list(), size.to_list(),
969 subdiv.to_list(), color.to_list()
970 )
971 else:
972 return context_wrapper.addBox(
973 self.context, center.to_list(), size.to_list(), subdiv.to_list()
974 )
975
976 def addDisk(self, center: vec3 = vec3(0, 0, 0), size: vec2 = vec2(1, 1),
977 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] = None,
978 color: Optional[Union[RGBcolor, RGBAcolor]] = None) -> List[int]:
979 """
980 Add a disk (circular or elliptical surface) to the context.
981
982 A disk is a flat circular or elliptical surface tessellated into
983 triangular faces. Supports both uniform radial subdivisions and
984 separate radial/azimuthal subdivisions for finer control.
985
986 Args:
987 center: 3D coordinates of disk center (default: origin)
988 size: Semi-major and semi-minor radii of the disk (default: 1x1 circle)
989 ndivs: Number of radial divisions (int) or [radial, azimuthal] divisions (int2)
990 (default: 20). Higher values create smoother circles but more triangles.
991 rotation: Orientation of the disk (default: horizontal, normal = +z)
992 color: Color of the disk (default: white). Can be RGBcolor or RGBAcolor for transparency.
993
994 Returns:
995 List of UUIDs for all triangles created in the disk
996
997 Example:
998 >>> context = Context()
999 >>> # Create a red disk at (0, 0, 1) with radius 0.5
1000 >>> disk_uuids = context.addDisk(
1001 ... center=vec3(0, 0, 1),
1002 ... size=vec2(0.5, 0.5),
1003 ... ndivs=30,
1004 ... color=RGBcolor(1, 0, 0)
1005 ... )
1006 >>> print(f"Created disk with {len(disk_uuids)} triangles")
1007 >>>
1008 >>> # Create a semi-transparent blue elliptical disk
1009 >>> disk_uuids = context.addDisk(
1010 ... center=vec3(0, 0, 2),
1011 ... size=vec2(1.0, 0.5),
1012 ... ndivs=40,
1013 ... rotation=SphericalCoord(1, 0.5, 0),
1014 ... color=RGBAcolor(0, 0, 1, 0.5)
1015 ... )
1016 >>>
1017 >>> # Create disk with polar/radial subdivisions for finer control
1018 >>> disk_uuids = context.addDisk(
1019 ... center=vec3(0, 0, 3),
1020 ... size=vec2(1, 1),
1021 ... ndivs=int2(10, 20), # 10 radial, 20 azimuthal divisions
1022 ... color=RGBcolor(0, 1, 0)
1023 ... )
1024 """
1026
1027 # Parameter type validation
1028 if not isinstance(center, vec3):
1029 raise ValueError(f"Center must be a vec3, got {type(center).__name__}")
1030 if not isinstance(size, vec2):
1031 raise ValueError(f"Size must be a vec2, got {type(size).__name__}")
1032 if not isinstance(ndivs, (int, int2)):
1033 raise ValueError(f"Ndivs must be an int or int2, got {type(ndivs).__name__}")
1034 if rotation is not None and not isinstance(rotation, SphericalCoord):
1035 raise ValueError(f"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
1036 if color is not None and not isinstance(color, (RGBcolor, RGBAcolor)):
1037 raise ValueError(f"Color must be an RGBcolor, RGBAcolor, or None, got {type(color).__name__}")
1038
1039 # Parameter value validation
1040 if any(s <= 0 for s in size.to_list()):
1041 raise ValueError("Disk size must be positive")
1042
1043 # Validate subdivisions based on type
1044 if isinstance(ndivs, int):
1045 if ndivs < 3:
1046 raise ValueError("Number of divisions must be at least 3")
1047 else: # int2
1048 if any(n < 1 for n in ndivs.to_list()):
1049 raise ValueError("Radial and angular divisions must be at least 1")
1050
1051 # Default rotation (horizontal disk, normal pointing +z)
1052 if rotation is None:
1053 rotation = SphericalCoord(1, 0, 0)
1054
1055 # CRITICAL: Extract only radius, elevation, azimuth for C++ interface
1056 # (rotation.to_list() returns 4 values, but C++ expects 3)
1057 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1058
1059 # Dispatch based on ndivs and color types
1060 if isinstance(ndivs, int2):
1061 # Polar subdivisions variant (supports RGB and RGBA color)
1062 if color:
1063 if isinstance(color, RGBAcolor):
1064 return context_wrapper.addDiskPolarSubdivisionsRGBA(
1065 self.context, ndivs.to_list(), center.to_list(), size.to_list(),
1066 rotation_list, color.to_list()
1067 )
1068 else:
1069 # RGB color
1070 return context_wrapper.addDiskPolarSubdivisions(
1071 self.context, ndivs.to_list(), center.to_list(), size.to_list(),
1072 rotation_list, color.to_list()
1073 )
1074 else:
1075 # No color - use default white
1076 color_list = [1.0, 1.0, 1.0]
1077 return context_wrapper.addDiskPolarSubdivisions(
1078 self.context, ndivs.to_list(), center.to_list(), size.to_list(),
1079 rotation_list, color_list
1080 )
1081 else:
1082 # Uniform radial subdivisions
1083 if color:
1084 if isinstance(color, RGBAcolor):
1085 # RGBA color variant
1086 return context_wrapper.addDiskWithRGBAColor(
1087 self.context, ndivs, center.to_list(), size.to_list(),
1088 rotation_list, color.to_list()
1089 )
1090 else:
1091 # RGB color variant
1092 return context_wrapper.addDiskWithColor(
1093 self.context, ndivs, center.to_list(), size.to_list(),
1094 rotation_list, color.to_list()
1095 )
1096 else:
1097 # No color - use rotation variant
1098 return context_wrapper.addDiskWithRotation(
1099 self.context, ndivs, center.to_list(), size.to_list(),
1100 rotation_list
1101 )
1102
1103 def addCone(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1104 ndivs: int = 20, color: Optional[RGBcolor] = None) -> List[int]:
1105 """
1106 Add a cone (or cylinder/frustum) to the context.
1107
1108 A cone is a 3D shape connecting two circular cross-sections with
1109 potentially different radii. When radii are equal, creates a cylinder.
1110 When one radius is zero, creates a true cone.
1111
1112 Args:
1113 node0: 3D coordinates of the base center
1114 node1: 3D coordinates of the apex center
1115 radius0: Radius at base (node0). Use 0 for pointed end.
1116 radius1: Radius at apex (node1). Use 0 for pointed end.
1117 ndivs: Number of radial divisions for tessellation (default: 20)
1118 color: Color of the cone (default: white)
1119
1120 Returns:
1121 List of UUIDs for all triangles created in the cone
1122
1123 Example:
1124 >>> context = Context()
1125 >>> # Create a cylinder (equal radii)
1126 >>> cylinder_uuids = context.addCone(
1127 ... node0=vec3(0, 0, 0),
1128 ... node1=vec3(0, 0, 2),
1129 ... radius0=0.5,
1130 ... radius1=0.5,
1131 ... ndivs=20
1132 ... )
1133 >>>
1134 >>> # Create a true cone (one radius = 0)
1135 >>> cone_uuids = context.addCone(
1136 ... node0=vec3(1, 0, 0),
1137 ... node1=vec3(1, 0, 1.5),
1138 ... radius0=0.5,
1139 ... radius1=0.0,
1140 ... ndivs=24,
1141 ... color=RGBcolor(1, 0, 0)
1142 ... )
1143 >>>
1144 >>> # Create a frustum (different radii)
1145 >>> frustum_uuids = context.addCone(
1146 ... node0=vec3(2, 0, 0),
1147 ... node1=vec3(2, 0, 1),
1148 ... radius0=0.8,
1149 ... radius1=0.4,
1150 ... ndivs=16
1151 ... )
1152 """
1154
1155 # Parameter type validation
1156 if not isinstance(node0, vec3):
1157 raise ValueError(f"node0 must be a vec3, got {type(node0).__name__}")
1158 if not isinstance(node1, vec3):
1159 raise ValueError(f"node1 must be a vec3, got {type(node1).__name__}")
1160 if not isinstance(ndivs, int):
1161 raise ValueError(f"ndivs must be an int, got {type(ndivs).__name__}")
1162 if color is not None and not isinstance(color, RGBcolor):
1163 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
1164
1165 # Parameter value validation
1166 if radius0 < 0 or radius1 < 0:
1167 raise ValueError("Radii must be non-negative")
1168 if ndivs < 3:
1169 raise ValueError("Number of radial divisions must be at least 3")
1170
1171 # Dispatch based on color
1172 if color:
1173 return context_wrapper.addConeWithColor(
1174 self.context, ndivs, node0.to_list(), node1.to_list(),
1175 radius0, radius1, color.to_list()
1176 )
1177 else:
1178 return context_wrapper.addCone(
1179 self.context, ndivs, node0.to_list(), node1.to_list(),
1180 radius0, radius1
1181 )
1182
1183 def addSphereObject(self, center: vec3 = vec3(0, 0, 0),
1184 radius: Union[float, vec3] = 1.0, ndivs: int = 20,
1185 color: Optional[RGBcolor] = None,
1186 texturefile: Optional[str] = None) -> int:
1187 """
1188 Add a spherical or ellipsoidal compound object to the context.
1189
1190 Creates a sphere or ellipsoid as a compound object with a trackable object ID.
1191 Primitives within the object are registered as children of the object.
1192
1193 Args:
1194 center: Center position of sphere/ellipsoid (default: origin)
1195 radius: Radius as float (sphere) or vec3 (ellipsoid) (default: 1.0)
1196 ndivs: Number of tessellation divisions (default: 20)
1197 color: Optional RGB color
1198 texturefile: Optional texture image file path
1199
1200 Returns:
1201 Object ID of the created compound object
1202
1203 Raises:
1204 ValueError: If parameters are invalid
1205 NotImplementedError: If object-returning functions unavailable
1206
1207 Examples:
1208 >>> # Create a basic sphere at origin
1209 >>> obj_id = ctx.addSphereObject()
1210
1211 >>> # Create a colored sphere
1212 >>> obj_id = ctx.addSphereObject(
1213 ... center=vec3(0, 0, 5),
1214 ... radius=2.0,
1215 ... color=RGBcolor(1, 0, 0)
1216 ... )
1217
1218 >>> # Create an ellipsoid (stretched sphere)
1219 >>> obj_id = ctx.addSphereObject(
1220 ... center=vec3(10, 0, 0),
1221 ... radius=vec3(2, 1, 1), # Elongated in x-direction
1222 ... ndivs=30
1223 ... )
1224 """
1226
1227 # Parameter type validation
1228 if not isinstance(center, vec3):
1229 raise ValueError(f"Center must be a vec3, got {type(center).__name__}")
1230 if not isinstance(radius, (int, float, vec3)):
1231 raise ValueError(f"Radius must be a number or vec3, got {type(radius).__name__}")
1232 if color is not None and not isinstance(color, RGBcolor):
1233 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
1234
1235 # Validate parameters
1236 if ndivs < 3:
1237 raise ValueError("Number of divisions must be at least 3")
1238
1239 # Check if radius is scalar (sphere) or vector (ellipsoid)
1240 is_ellipsoid = isinstance(radius, vec3)
1241
1242 # Dispatch based on parameters
1243 if is_ellipsoid:
1244 # Ellipsoid variants
1245 if texturefile:
1246 return context_wrapper.addSphereObject_ellipsoid_texture(
1247 self.context, ndivs, center.to_list(), radius.to_list(), texturefile
1248 )
1249 elif color:
1250 return context_wrapper.addSphereObject_ellipsoid_color(
1251 self.context, ndivs, center.to_list(), radius.to_list(), color.to_list()
1252 )
1253 else:
1254 return context_wrapper.addSphereObject_ellipsoid(
1255 self.context, ndivs, center.to_list(), radius.to_list()
1256 )
1257 else:
1258 # Sphere variants (radius is float)
1259 if texturefile:
1260 return context_wrapper.addSphereObject_texture(
1261 self.context, ndivs, center.to_list(), radius, texturefile
1262 )
1263 elif color:
1264 return context_wrapper.addSphereObject_color(
1265 self.context, ndivs, center.to_list(), radius, color.to_list()
1266 )
1267 else:
1268 return context_wrapper.addSphereObject_basic(
1269 self.context, ndivs, center.to_list(), radius
1270 )
1271
1272 def addTileObject(self, center: vec3 = vec3(0, 0, 0), size: vec2 = vec2(1, 1),
1273 rotation: SphericalCoord = SphericalCoord(1, 0, 0),
1274 subdiv: int2 = int2(1, 1),
1275 color: Optional[RGBcolor] = None,
1276 texturefile: Optional[str] = None,
1277 texture_repeat: Optional[int2] = None) -> int:
1278 """
1279 Add a tiled patch (subdivided patch) as a compound object to the context.
1280
1281 Creates a rectangular patch subdivided into a grid of smaller patches,
1282 registered as a compound object with a trackable object ID.
1283
1284 Args:
1285 center: Center position of tile (default: origin)
1286 size: Size in x and y directions (default: 1x1)
1287 rotation: Spherical rotation (default: no rotation)
1288 subdiv: Number of subdivisions in x and y (default: 1x1)
1289 color: Optional RGB color
1290 texturefile: Optional texture image file path
1291 texture_repeat: Optional texture repetitions in x and y
1292
1293 Returns:
1294 Object ID of the created compound object
1295
1296 Raises:
1297 ValueError: If parameters are invalid
1298 NotImplementedError: If object-returning functions unavailable
1299
1300 Examples:
1301 >>> # Create a basic 2x2 tile
1302 >>> obj_id = ctx.addTileObject(
1303 ... center=vec3(0, 0, 0),
1304 ... size=vec2(10, 10),
1305 ... subdiv=int2(2, 2)
1306 ... )
1307
1308 >>> # Create a colored tile with rotation
1309 >>> obj_id = ctx.addTileObject(
1310 ... center=vec3(5, 0, 0),
1311 ... size=vec2(10, 5),
1312 ... rotation=SphericalCoord(1, 0, 45),
1313 ... subdiv=int2(4, 2),
1314 ... color=RGBcolor(0, 1, 0)
1315 ... )
1316 """
1318
1319 # Parameter type validation
1320 if not isinstance(center, vec3):
1321 raise ValueError(f"Center must be a vec3, got {type(center).__name__}")
1322 if not isinstance(size, vec2):
1323 raise ValueError(f"Size must be a vec2, got {type(size).__name__}")
1324 if not isinstance(rotation, SphericalCoord):
1325 raise ValueError(f"Rotation must be a SphericalCoord, got {type(rotation).__name__}")
1326 if not isinstance(subdiv, int2):
1327 raise ValueError(f"Subdiv must be an int2, got {type(subdiv).__name__}")
1328 if color is not None and not isinstance(color, RGBcolor):
1329 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
1330 if texture_repeat is not None and not isinstance(texture_repeat, int2):
1331 raise ValueError(f"texture_repeat must be an int2 or None, got {type(texture_repeat).__name__}")
1332
1333 # Extract rotation as 3 values (radius, elevation, azimuth)
1334 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
1335
1336 # Dispatch based on parameters
1337 if texture_repeat is not None:
1338 if texturefile is None:
1339 raise ValueError("texture_repeat requires texturefile")
1340 return context_wrapper.addTileObject_texture_repeat(
1341 self.context, center.to_list(), size.to_list(), rotation_list,
1342 subdiv.to_list(), texturefile, texture_repeat.to_list()
1343 )
1344 elif texturefile:
1345 return context_wrapper.addTileObject_texture(
1346 self.context, center.to_list(), size.to_list(), rotation_list,
1347 subdiv.to_list(), texturefile
1348 )
1349 elif color:
1350 return context_wrapper.addTileObject_color(
1351 self.context, center.to_list(), size.to_list(), rotation_list,
1352 subdiv.to_list(), color.to_list()
1353 )
1354 else:
1355 return context_wrapper.addTileObject_basic(
1356 self.context, center.to_list(), size.to_list(), rotation_list,
1357 subdiv.to_list()
1358 )
1359
1360 def addBoxObject(self, center: vec3 = vec3(0, 0, 0), size: vec3 = vec3(1, 1, 1),
1361 subdiv: int3 = int3(1, 1, 1), color: Optional[RGBcolor] = None,
1362 texturefile: Optional[str] = None, reverse_normals: bool = False) -> int:
1363 """
1364 Add a rectangular box (prism) as a compound object to the context.
1365
1366 Args:
1367 center: Center position (default: origin)
1368 size: Size in x, y, z directions (default: 1x1x1)
1369 subdiv: Subdivisions in x, y, z (default: 1x1x1)
1370 color: Optional RGB color
1371 texturefile: Optional texture file path
1372 reverse_normals: Reverse normal directions (default: False)
1373
1374 Returns:
1375 Object ID of the created compound object
1376 """
1378
1379 # Parameter type validation
1380 if not isinstance(center, vec3):
1381 raise ValueError(f"Center must be a vec3, got {type(center).__name__}")
1382 if not isinstance(size, vec3):
1383 raise ValueError(f"Size must be a vec3, got {type(size).__name__}")
1384 if not isinstance(subdiv, int3):
1385 raise ValueError(f"Subdiv must be an int3, got {type(subdiv).__name__}")
1386 if color is not None and not isinstance(color, RGBcolor):
1387 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
1388
1389 if reverse_normals:
1390 if texturefile:
1391 return context_wrapper.addBoxObject_texture_reverse(self.context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile, reverse_normals)
1392 elif color:
1393 return context_wrapper.addBoxObject_color_reverse(self.context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list(), reverse_normals)
1394 else:
1395 raise ValueError("reverse_normals requires either color or texturefile")
1396 elif texturefile:
1397 return context_wrapper.addBoxObject_texture(self.context, center.to_list(), size.to_list(), subdiv.to_list(), texturefile)
1398 elif color:
1399 return context_wrapper.addBoxObject_color(self.context, center.to_list(), size.to_list(), subdiv.to_list(), color.to_list())
1400 else:
1401 return context_wrapper.addBoxObject_basic(self.context, center.to_list(), size.to_list(), subdiv.to_list())
1402
1403 def addConeObject(self, node0: vec3, node1: vec3, radius0: float, radius1: float,
1404 ndivs: int = 20, color: Optional[RGBcolor] = None,
1405 texturefile: Optional[str] = None) -> int:
1406 """
1407 Add a cone/cylinder/frustum as a compound object to the context.
1408
1409 Args:
1410 node0: Base position
1411 node1: Top position
1412 radius0: Radius at base
1413 radius1: Radius at top
1414 ndivs: Number of radial divisions (default: 20)
1415 color: Optional RGB color
1416 texturefile: Optional texture file path
1417
1418 Returns:
1419 Object ID of the created compound object
1420 """
1422
1423 # Parameter type validation
1424 if not isinstance(node0, vec3):
1425 raise ValueError(f"node0 must be a vec3, got {type(node0).__name__}")
1426 if not isinstance(node1, vec3):
1427 raise ValueError(f"node1 must be a vec3, got {type(node1).__name__}")
1428 if not isinstance(radius0, (int, float)):
1429 raise ValueError(f"radius0 must be a number, got {type(radius0).__name__}")
1430 if not isinstance(radius1, (int, float)):
1431 raise ValueError(f"radius1 must be a number, got {type(radius1).__name__}")
1432 if color is not None and not isinstance(color, RGBcolor):
1433 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
1434
1435 if texturefile:
1436 return context_wrapper.addConeObject_texture(self.context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, texturefile)
1437 elif color:
1438 return context_wrapper.addConeObject_color(self.context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1, color.to_list())
1439 else:
1440 return context_wrapper.addConeObject_basic(self.context, ndivs, node0.to_list(), node1.to_list(), radius0, radius1)
1441
1442 def addDiskObject(self, center: vec3 = vec3(0, 0, 0), size: vec2 = vec2(1, 1),
1443 ndivs: Union[int, int2] = 20, rotation: Optional[SphericalCoord] = None,
1444 color: Optional[Union[RGBcolor, RGBAcolor]] = None,
1445 texturefile: Optional[str] = None) -> int:
1446 """
1447 Add a disk as a compound object to the context.
1448
1449 Args:
1450 center: Center position (default: origin)
1451 size: Semi-major and semi-minor radii (default: 1x1)
1452 ndivs: int (uniform) or int2 (polar/radial subdivisions) (default: 20)
1453 rotation: Optional spherical rotation
1454 color: Optional RGB or RGBA color
1455 texturefile: Optional texture file path
1456
1457 Returns:
1458 Object ID of the created compound object
1459 """
1461
1462 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth] if rotation else [1, 0, 0]
1463 is_polar = isinstance(ndivs, int2)
1465 if is_polar:
1466 if texturefile:
1467 return context_wrapper.addDiskObject_polar_texture(self.context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, texturefile)
1468 elif color:
1469 if isinstance(color, RGBAcolor):
1470 return context_wrapper.addDiskObject_polar_rgba(self.context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1471 else:
1472 return context_wrapper.addDiskObject_polar_color(self.context, ndivs.to_list(), center.to_list(), size.to_list(), rotation_list, color.to_list())
1473 else:
1474 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())
1475 else:
1476 if texturefile:
1477 return context_wrapper.addDiskObject_texture(self.context, ndivs, center.to_list(), size.to_list(), rotation_list, texturefile)
1478 elif color:
1479 if isinstance(color, RGBAcolor):
1480 return context_wrapper.addDiskObject_rgba(self.context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1481 else:
1482 return context_wrapper.addDiskObject_color(self.context, ndivs, center.to_list(), size.to_list(), rotation_list, color.to_list())
1483 elif rotation:
1484 return context_wrapper.addDiskObject_rotation(self.context, ndivs, center.to_list(), size.to_list(), rotation_list)
1485 else:
1486 return context_wrapper.addDiskObject_basic(self.context, ndivs, center.to_list(), size.to_list())
1487
1488 def addTubeObject(self, ndivs: int, nodes: List[vec3], radii: List[float],
1489 colors: Optional[List[RGBcolor]] = None,
1490 texturefile: Optional[str] = None,
1491 texture_uv: Optional[List[float]] = None) -> int:
1492 """
1493 Add a tube as a compound object to the context.
1494
1495 Args:
1496 ndivs: Number of radial subdivisions
1497 nodes: List of vec3 positions defining tube segments
1498 radii: List of radii at each node
1499 colors: Optional list of RGB colors for each segment
1500 texturefile: Optional texture file path
1501 texture_uv: Optional UV coordinates for texture mapping
1502
1503 Returns:
1504 Object ID of the created compound object
1505 """
1507
1508 # Parameter type validation
1509 if not isinstance(nodes, (list, tuple)):
1510 raise ValueError(f"Nodes must be a list, got {type(nodes).__name__}")
1511 for i, node in enumerate(nodes):
1512 if not isinstance(node, vec3):
1513 raise ValueError(f"nodes[{i}] must be a vec3, got {type(node).__name__}")
1514 if not isinstance(radii, (list, tuple)):
1515 raise ValueError(f"Radii must be a list, got {type(radii).__name__}")
1516 if colors is not None:
1517 if not isinstance(colors, (list, tuple)):
1518 raise ValueError(f"Colors must be a list or None, got {type(colors).__name__}")
1519 for i, c in enumerate(colors):
1520 if not isinstance(c, RGBcolor):
1521 raise ValueError(f"colors[{i}] must be an RGBcolor, got {type(c).__name__}")
1522
1523 if len(nodes) < 2:
1524 raise ValueError("Tube requires at least 2 nodes")
1525 if len(radii) != len(nodes):
1526 raise ValueError("Number of radii must match number of nodes")
1527
1528 nodes_flat = [coord for node in nodes for coord in node.to_list()]
1529
1530 if texture_uv is not None:
1531 if texturefile is None:
1532 raise ValueError("texture_uv requires texturefile")
1533 return context_wrapper.addTubeObject_texture_uv(self.context, ndivs, nodes_flat, radii, texturefile, texture_uv)
1534 elif texturefile:
1535 return context_wrapper.addTubeObject_texture(self.context, ndivs, nodes_flat, radii, texturefile)
1536 elif colors:
1537 if len(colors) != len(nodes):
1538 raise ValueError("Number of colors must match number of nodes")
1539 colors_flat = [c for color in colors for c in color.to_list()]
1540 return context_wrapper.addTubeObject_color(self.context, ndivs, nodes_flat, radii, colors_flat)
1541 else:
1542 return context_wrapper.addTubeObject_basic(self.context, ndivs, nodes_flat, radii)
1543
1544 def copyPrimitive(self, UUID: Union[int, List[int]]) -> Union[int, List[int]]:
1545 """
1546 Copy one or more primitives.
1547
1548 Creates a duplicate of the specified primitive(s) with all associated data.
1549 The copy is placed at the same location as the original.
1550
1551 Args:
1552 UUID: Single primitive UUID or list of UUIDs to copy
1553
1554 Returns:
1555 Single UUID of copied primitive (if UUID is int) or
1556 List of UUIDs of copied primitives (if UUID is list)
1557
1558 Example:
1559 >>> context = Context()
1560 >>> original_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1561 >>> # Copy single primitive
1562 >>> copied_uuid = context.copyPrimitive(original_uuid)
1563 >>> # Copy multiple primitives
1564 >>> copied_uuids = context.copyPrimitive([uuid1, uuid2, uuid3])
1565 """
1567
1568 if isinstance(UUID, int):
1569 return context_wrapper.copyPrimitive(self.context, UUID)
1570 elif isinstance(UUID, list):
1571 return context_wrapper.copyPrimitives(self.context, UUID)
1572 else:
1573 raise ValueError(f"UUID must be int or List[int], got {type(UUID).__name__}")
1574
1575 def copyPrimitiveData(self, sourceUUID: int, destinationUUID: int) -> None:
1576 """
1577 Copy all primitive data from source to destination primitive.
1578
1579 Copies all associated data (primitive data fields) from the source
1580 primitive to the destination primitive. Both primitives must already exist.
1581
1582 Args:
1583 sourceUUID: UUID of the source primitive
1584 destinationUUID: UUID of the destination primitive
1585
1586 Example:
1587 >>> context = Context()
1588 >>> source_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1589 >>> dest_uuid = context.addPatch(center=vec3(1, 0, 0), size=vec2(1, 1))
1590 >>> context.setPrimitiveData(source_uuid, "temperature", 25.5)
1591 >>> context.copyPrimitiveData(source_uuid, dest_uuid)
1592 >>> # dest_uuid now has temperature data
1593 """
1595
1596 if not isinstance(sourceUUID, int):
1597 raise ValueError(f"sourceUUID must be int, got {type(sourceUUID).__name__}")
1598 if not isinstance(destinationUUID, int):
1599 raise ValueError(f"destinationUUID must be int, got {type(destinationUUID).__name__}")
1600
1601 context_wrapper.copyPrimitiveData(self.context, sourceUUID, destinationUUID)
1602
1603 def copyObject(self, ObjID: Union[int, List[int]]) -> Union[int, List[int]]:
1604 """
1605 Copy one or more compound objects.
1606
1607 Creates a duplicate of the specified compound object(s) with all
1608 associated primitives and data. The copy is placed at the same location
1609 as the original.
1610
1611 Args:
1612 ObjID: Single object ID or list of object IDs to copy
1613
1614 Returns:
1615 Single object ID of copied object (if ObjID is int) or
1616 List of object IDs of copied objects (if ObjID is list)
1617
1618 Example:
1619 >>> context = Context()
1620 >>> original_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1621 >>> # Copy single object
1622 >>> copied_obj = context.copyObject(original_obj)
1623 >>> # Copy multiple objects
1624 >>> copied_objs = context.copyObject([obj1, obj2, obj3])
1625 """
1627
1628 if isinstance(ObjID, int):
1629 return context_wrapper.copyObject(self.context, ObjID)
1630 elif isinstance(ObjID, list):
1631 return context_wrapper.copyObjects(self.context, ObjID)
1632 else:
1633 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1634
1635 def copyObjectData(self, source_objID: int, destination_objID: int) -> None:
1636 """
1637 Copy all object data from source to destination compound object.
1638
1639 Copies all associated data (object data fields) from the source
1640 compound object to the destination object. Both objects must already exist.
1641
1642 Args:
1643 source_objID: Object ID of the source compound object
1644 destination_objID: Object ID of the destination compound object
1645
1646 Example:
1647 >>> context = Context()
1648 >>> source_obj = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1649 >>> dest_obj = context.addTile(center=vec3(2, 0, 0), size=vec2(2, 2))
1650 >>> context.setObjectData(source_obj, "material", "wood")
1651 >>> context.copyObjectData(source_obj, dest_obj)
1652 >>> # dest_obj now has material data
1653 """
1655
1656 if not isinstance(source_objID, int):
1657 raise ValueError(f"source_objID must be int, got {type(source_objID).__name__}")
1658 if not isinstance(destination_objID, int):
1659 raise ValueError(f"destination_objID must be int, got {type(destination_objID).__name__}")
1660
1661 context_wrapper.copyObjectData(self.context, source_objID, destination_objID)
1662
1663 def translatePrimitive(self, UUID: Union[int, List[int]], shift: vec3) -> None:
1664 """
1665 Translate one or more primitives by a shift vector.
1666
1667 Moves the specified primitive(s) by the given shift vector without
1668 changing their orientation or size.
1669
1670 Args:
1671 UUID: Single primitive UUID or list of UUIDs to translate
1672 shift: 3D vector representing the translation [x, y, z]
1673
1674 Example:
1675 >>> context = Context()
1676 >>> patch_uuid = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
1677 >>> # Translate single primitive
1678 >>> context.translatePrimitive(patch_uuid, vec3(1, 0, 0)) # Move 1 unit in x
1679 >>> # Translate multiple primitives
1680 >>> context.translatePrimitive([uuid1, uuid2, uuid3], vec3(0, 0, 1)) # Move 1 unit in z
1681 """
1683
1684 # Type validation
1685 if not isinstance(shift, vec3):
1686 raise ValueError(f"shift must be a vec3, got {type(shift).__name__}")
1687
1688 if isinstance(UUID, int):
1689 context_wrapper.translatePrimitive(self.context, UUID, shift.to_list())
1690 elif isinstance(UUID, list):
1691 context_wrapper.translatePrimitives(self.context, UUID, shift.to_list())
1692 else:
1693 raise ValueError(f"UUID must be int or List[int], got {type(UUID).__name__}")
1694
1695 def translateObject(self, ObjID: Union[int, List[int]], shift: vec3) -> None:
1696 """
1697 Translate one or more compound objects by a shift vector.
1698
1699 Moves the specified compound object(s) and all their constituent
1700 primitives by the given shift vector without changing orientation or size.
1701
1702 Args:
1703 ObjID: Single object ID or list of object IDs to translate
1704 shift: 3D vector representing the translation [x, y, z]
1705
1706 Example:
1707 >>> context = Context()
1708 >>> tile_uuids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2))
1709 >>> obj_id = context.getPrimitiveParentObjectID(tile_uuids[0]) # Get object ID
1710 >>> # Translate single object
1711 >>> context.translateObject(obj_id, vec3(5, 0, 0)) # Move 5 units in x
1712 >>> # Translate multiple objects
1713 >>> context.translateObject([obj1, obj2, obj3], vec3(0, 2, 0)) # Move 2 units in y
1714 """
1716
1717 # Type validation
1718 if not isinstance(shift, vec3):
1719 raise ValueError(f"shift must be a vec3, got {type(shift).__name__}")
1720
1721 if isinstance(ObjID, int):
1722 context_wrapper.translateObject(self.context, ObjID, shift.to_list())
1723 elif isinstance(ObjID, list):
1724 context_wrapper.translateObjects(self.context, ObjID, shift.to_list())
1725 else:
1726 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1727
1728 def rotatePrimitive(self, UUID: Union[int, List[int]], angle: float,
1729 axis: Union[str, vec3], origin: Optional[vec3] = None) -> None:
1730 """
1731 Rotate one or more primitives.
1732
1733 Args:
1734 UUID: Single UUID or list of UUIDs to rotate
1735 angle: Rotation angle in radians
1736 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
1737 origin: Optional rotation origin point. If None, rotates about primitive center.
1738 If provided with string axis, raises ValueError.
1739
1740 Raises:
1741 ValueError: If axis is invalid or if origin is provided with string axis
1742 """
1744
1745 # Validate axis parameter
1746 if isinstance(axis, str):
1747 if axis not in ('x', 'y', 'z'):
1748 raise ValueError("axis must be 'x', 'y', or 'z'")
1749 if origin is not None:
1750 raise ValueError("origin parameter cannot be used with string axis")
1751
1752 # Use string axis variant
1753 if isinstance(UUID, int):
1754 context_wrapper.rotatePrimitive_axisString(self.context, UUID, angle, axis)
1755 elif isinstance(UUID, list):
1756 context_wrapper.rotatePrimitives_axisString(self.context, UUID, angle, axis)
1757 else:
1758 raise ValueError(f"UUID must be int or List[int], got {type(UUID).__name__}")
1759
1760 elif isinstance(axis, vec3):
1761 axis_list = axis.to_list()
1762
1763 # Check for zero-length axis
1764 if all(abs(v) < 1e-10 for v in axis_list):
1765 raise ValueError("axis vector cannot be zero")
1766
1767 if origin is None:
1768 # Rotate about primitive center (axis vector variant)
1769 if isinstance(UUID, int):
1770 context_wrapper.rotatePrimitive_axisVector(self.context, UUID, angle, axis_list)
1771 elif isinstance(UUID, list):
1772 context_wrapper.rotatePrimitives_axisVector(self.context, UUID, angle, axis_list)
1773 else:
1774 raise ValueError(f"UUID must be int or List[int], got {type(UUID).__name__}")
1775 else:
1776 # Rotate about specified origin point
1777 if not isinstance(origin, vec3):
1778 raise ValueError(f"origin must be a vec3, got {type(origin).__name__}")
1779
1780 origin_list = origin.to_list()
1781 if isinstance(UUID, int):
1782 context_wrapper.rotatePrimitive_originAxisVector(self.context, UUID, angle, origin_list, axis_list)
1783 elif isinstance(UUID, list):
1784 context_wrapper.rotatePrimitives_originAxisVector(self.context, UUID, angle, origin_list, axis_list)
1785 else:
1786 raise ValueError(f"UUID must be int or List[int], got {type(UUID).__name__}")
1787 else:
1788 raise ValueError(f"axis must be str or vec3, got {type(axis).__name__}")
1789
1790 def rotateObject(self, ObjID: Union[int, List[int]], angle: float,
1791 axis: Union[str, vec3], origin: Optional[vec3] = None,
1792 about_origin: bool = False) -> None:
1793 """
1794 Rotate one or more objects.
1795
1796 Args:
1797 ObjID: Single object ID or list of object IDs to rotate
1798 angle: Rotation angle in radians
1799 axis: Rotation axis - either 'x', 'y', 'z' or a vec3 direction vector
1800 origin: Optional rotation origin point. If None, rotates about object center.
1801 If provided with string axis, raises ValueError.
1802 about_origin: If True, rotate about the object's own stored origin point
1803 (``object_origin``), which for most objects is its construction center —
1804 NOT the global origin (0,0,0). An object built away from the world origin
1805 therefore spins in place rather than orbiting the world origin. To orbit a
1806 specific point, pass that point as ``origin`` instead. Cannot be used with
1807 the origin parameter.
1808
1809 Raises:
1810 ValueError: If axis is invalid or if origin and about_origin are both specified
1811 """
1813
1814 # Validate parameter combinations
1815 if origin is not None and about_origin:
1816 raise ValueError("Cannot specify both origin and about_origin")
1818 # Validate axis parameter
1819 if isinstance(axis, str):
1820 if axis not in ('x', 'y', 'z'):
1821 raise ValueError("axis must be 'x', 'y', or 'z'")
1822 if origin is not None:
1823 raise ValueError("origin parameter cannot be used with string axis")
1824 if about_origin:
1825 raise ValueError("about_origin parameter cannot be used with string axis")
1826
1827 # Use string axis variant
1828 if isinstance(ObjID, int):
1829 context_wrapper.rotateObject_axisString(self.context, ObjID, angle, axis)
1830 elif isinstance(ObjID, list):
1831 context_wrapper.rotateObjects_axisString(self.context, ObjID, angle, axis)
1832 else:
1833 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1834
1835 elif isinstance(axis, vec3):
1836 axis_list = axis.to_list()
1837
1838 # Check for zero-length axis
1839 if all(abs(v) < 1e-10 for v in axis_list):
1840 raise ValueError("axis vector cannot be zero")
1841
1842 if about_origin:
1843 # Rotate about global origin
1844 if isinstance(ObjID, int):
1845 context_wrapper.rotateObjectAboutOrigin_axisVector(self.context, ObjID, angle, axis_list)
1846 elif isinstance(ObjID, list):
1847 context_wrapper.rotateObjectsAboutOrigin_axisVector(self.context, ObjID, angle, axis_list)
1848 else:
1849 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1850 elif origin is None:
1851 # Rotate about object center
1852 if isinstance(ObjID, int):
1853 context_wrapper.rotateObject_axisVector(self.context, ObjID, angle, axis_list)
1854 elif isinstance(ObjID, list):
1855 context_wrapper.rotateObjects_axisVector(self.context, ObjID, angle, axis_list)
1856 else:
1857 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1858 else:
1859 # Rotate about specified origin point
1860 if not isinstance(origin, vec3):
1861 raise ValueError(f"origin must be a vec3, got {type(origin).__name__}")
1862
1863 origin_list = origin.to_list()
1864 if isinstance(ObjID, int):
1865 context_wrapper.rotateObject_originAxisVector(self.context, ObjID, angle, origin_list, axis_list)
1866 elif isinstance(ObjID, list):
1867 context_wrapper.rotateObjects_originAxisVector(self.context, ObjID, angle, origin_list, axis_list)
1868 else:
1869 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1870 else:
1871 raise ValueError(f"axis must be str or vec3, got {type(axis).__name__}")
1872
1873 def scalePrimitive(self, UUID: Union[int, List[int]], scale: vec3, point: Optional[vec3] = None) -> None:
1874 """
1875 Scale one or more primitives.
1876
1877 Args:
1878 UUID: Single UUID or list of UUIDs to scale
1879 scale: Scale factors as vec3(x, y, z)
1880 point: Optional point to scale about. If None, scales about primitive center.
1881
1882 Raises:
1883 ValueError: If scale or point parameters are invalid
1884 """
1886
1887 if not isinstance(scale, vec3):
1888 raise ValueError(f"scale must be a vec3, got {type(scale).__name__}")
1889
1890 scale_list = scale.to_list()
1891
1892 if point is None:
1893 # Scale about primitive center
1894 if isinstance(UUID, int):
1895 context_wrapper.scalePrimitive(self.context, UUID, scale_list)
1896 elif isinstance(UUID, list):
1897 context_wrapper.scalePrimitives(self.context, UUID, scale_list)
1898 else:
1899 raise ValueError(f"UUID must be int or List[int], got {type(UUID).__name__}")
1900 else:
1901 # Scale about specified point
1902 if not isinstance(point, vec3):
1903 raise ValueError(f"point must be a vec3, got {type(point).__name__}")
1904
1905 point_list = point.to_list()
1906 if isinstance(UUID, int):
1907 context_wrapper.scalePrimitiveAboutPoint(self.context, UUID, scale_list, point_list)
1908 elif isinstance(UUID, list):
1909 context_wrapper.scalePrimitivesAboutPoint(self.context, UUID, scale_list, point_list)
1910 else:
1911 raise ValueError(f"UUID must be int or List[int], got {type(UUID).__name__}")
1912
1913 def scaleObject(self, ObjID: Union[int, List[int]], scale: vec3,
1914 point: Optional[vec3] = None, about_center: bool = False,
1915 about_origin: bool = False) -> None:
1916 """
1917 Scale one or more objects.
1918
1919 Args:
1920 ObjID: Single object ID or list of object IDs to scale
1921 scale: Scale factors as vec3(x, y, z)
1922 point: Optional point to scale about
1923 about_center: If True, scale about object center (default behavior)
1924 about_origin: If True, scale about the object's own stored origin point
1925 (``object_origin``), not the global origin (0,0,0). Pass ``point`` to
1926 scale about a specific location instead.
1927
1928 Raises:
1929 ValueError: If parameters are invalid or conflicting options specified
1930 """
1932
1933 # Validate parameter combinations
1934 options_count = sum([point is not None, about_center, about_origin])
1935 if options_count > 1:
1936 raise ValueError("Cannot specify multiple scaling options (point, about_center, about_origin)")
1937
1938 if not isinstance(scale, vec3):
1939 raise ValueError(f"scale must be a vec3, got {type(scale).__name__}")
1940
1941 scale_list = scale.to_list()
1942
1943 if about_origin:
1944 # Scale about global origin
1945 if isinstance(ObjID, int):
1946 context_wrapper.scaleObjectAboutOrigin(self.context, ObjID, scale_list)
1947 elif isinstance(ObjID, list):
1948 context_wrapper.scaleObjectsAboutOrigin(self.context, ObjID, scale_list)
1949 else:
1950 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1951 elif about_center:
1952 # Scale about object center
1953 if isinstance(ObjID, int):
1954 context_wrapper.scaleObjectAboutCenter(self.context, ObjID, scale_list)
1955 elif isinstance(ObjID, list):
1956 context_wrapper.scaleObjectsAboutCenter(self.context, ObjID, scale_list)
1957 else:
1958 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1959 elif point is not None:
1960 # Scale about specified point
1961 if not isinstance(point, vec3):
1962 raise ValueError(f"point must be a vec3, got {type(point).__name__}")
1963
1964 point_list = point.to_list()
1965 if isinstance(ObjID, int):
1966 context_wrapper.scaleObjectAboutPoint(self.context, ObjID, scale_list, point_list)
1967 elif isinstance(ObjID, list):
1968 context_wrapper.scaleObjectsAboutPoint(self.context, ObjID, scale_list, point_list)
1969 else:
1970 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1971 else:
1972 # Default: scale object (standard behavior)
1973 if isinstance(ObjID, int):
1974 context_wrapper.scaleObject(self.context, ObjID, scale_list)
1975 elif isinstance(ObjID, list):
1976 context_wrapper.scaleObjects(self.context, ObjID, scale_list)
1977 else:
1978 raise ValueError(f"ObjID must be int or List[int], got {type(ObjID).__name__}")
1979
1980 def scaleConeObjectLength(self, ObjID: int, scale_factor: float) -> None:
1981 """
1982 Scale the length of a Cone object by scaling the distance between its two nodes.
1983
1984 Args:
1985 ObjID: Object ID of the Cone to scale
1986 scale_factor: Factor by which to scale the cone length (e.g., 2.0 doubles length)
1987
1988 Raises:
1989 ValueError: If ObjID is not an integer or scale_factor is invalid
1990 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
1991
1992 Note:
1993 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
1994 method, enforcing better encapsulation.
1995
1996 Example:
1997 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
1998 >>> context.scaleConeObjectLength(cone_id, 1.5) # Make cone 50% longer
1999 """
2000 if not isinstance(ObjID, int):
2001 raise ValueError(f"ObjID must be an integer, got {type(ObjID).__name__}")
2002 if not isinstance(scale_factor, (int, float)):
2003 raise ValueError(f"scale_factor must be numeric, got {type(scale_factor).__name__}")
2004 if scale_factor <= 0:
2005 raise ValueError(f"scale_factor must be positive, got {scale_factor}")
2006
2007 context_wrapper.scaleConeObjectLength(self.context, ObjID, float(scale_factor))
2008
2009 def scaleConeObjectGirth(self, ObjID: int, scale_factor: float) -> None:
2010 """
2011 Scale the girth of a Cone object by scaling the radii at both nodes.
2012
2013 Args:
2014 ObjID: Object ID of the Cone to scale
2015 scale_factor: Factor by which to scale the cone girth (e.g., 2.0 doubles girth)
2016
2017 Raises:
2018 ValueError: If ObjID is not an integer or scale_factor is invalid
2019 HeliosRuntimeError: If operation fails (e.g., ObjID is not a Cone object)
2020
2021 Note:
2022 Added in helios-core v1.3.59 as a replacement for the removed getConeObjectPointer()
2023 method, enforcing better encapsulation.
2024
2025 Example:
2026 >>> cone_id = context.addConeObject(10, [0,0,0], [0,0,1], 0.1, 0.05)
2027 >>> context.scaleConeObjectGirth(cone_id, 2.0) # Double the cone girth
2028 """
2029 if not isinstance(ObjID, int):
2030 raise ValueError(f"ObjID must be an integer, got {type(ObjID).__name__}")
2031 if not isinstance(scale_factor, (int, float)):
2032 raise ValueError(f"scale_factor must be numeric, got {type(scale_factor).__name__}")
2033 if scale_factor <= 0:
2034 raise ValueError(f"scale_factor must be positive, got {scale_factor}")
2035
2036 context_wrapper.scaleConeObjectGirth(self.context, ObjID, float(scale_factor))
2037
2038 def loadPLY(self, filename: str, origin: Optional[vec3] = None, height: Optional[float] = None,
2039 rotation: Optional[SphericalCoord] = None, color: Optional[RGBcolor] = None,
2040 upaxis: str = "YUP", silent: bool = False) -> List[int]:
2041 """
2042 Load geometry from a PLY (Stanford Polygon) file.
2043
2044 Args:
2045 filename: Path to the PLY file to load
2046 origin: Origin point for positioning the geometry (optional)
2047 height: Height scaling factor (optional)
2048 rotation: Rotation to apply to the geometry (optional)
2049 color: Default color for geometry without color data (optional)
2050 upaxis: Up axis orientation ("YUP" or "ZUP")
2051 silent: If True, suppress loading output messages
2052
2053 Returns:
2054 List of UUIDs for the loaded primitives
2055 """
2057
2058 # Parameter type validation
2059 if origin is not None and not isinstance(origin, vec3):
2060 raise ValueError(f"Origin must be a vec3 or None, got {type(origin).__name__}")
2061 if rotation is not None and not isinstance(rotation, SphericalCoord):
2062 raise ValueError(f"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
2063 if color is not None and not isinstance(color, RGBcolor):
2064 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
2065
2066 # Validate file path for security
2067 validated_filename = self._validate_file_path(filename, ['.ply'])
2068
2069 if origin is None and height is None and rotation is None and color is None:
2070 # Simple load with no transformations
2071 return context_wrapper.loadPLY(self.context, validated_filename, silent)
2072
2073 elif origin is not None and height is not None and rotation is None and color is None:
2074 # Load with origin and height
2075 return context_wrapper.loadPLYWithOriginHeight(self.context, validated_filename, origin.to_list(), height, upaxis, silent)
2076
2077 elif origin is not None and height is not None and rotation is not None and color is None:
2078 # Load with origin, height, and rotation
2079 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
2080 return context_wrapper.loadPLYWithOriginHeightRotation(self.context, validated_filename, origin.to_list(), height, rotation_list, upaxis, silent)
2081
2082 elif origin is not None and height is not None and rotation is None and color is not None:
2083 # Load with origin, height, and color
2084 return context_wrapper.loadPLYWithOriginHeightColor(self.context, validated_filename, origin.to_list(), height, color.to_list(), upaxis, silent)
2085
2086 elif origin is not None and height is not None and rotation is not None and color is not None:
2087 # Load with all parameters
2088 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
2089 return context_wrapper.loadPLYWithOriginHeightRotationColor(self.context, validated_filename, origin.to_list(), height, rotation_list, color.to_list(), upaxis, silent)
2090
2091 else:
2092 raise ValueError("Invalid parameter combination. When using transformations, both origin and height are required.")
2093
2094 def loadOBJ(self, filename: str, origin: Optional[vec3] = None, height: Optional[float] = None,
2095 scale: Optional[vec3] = None, rotation: Optional[SphericalCoord] = None,
2096 color: Optional[RGBcolor] = None, upaxis: str = "YUP", silent: bool = False) -> List[int]:
2097 """
2098 Load geometry from an OBJ (Wavefront) file.
2099
2100 Args:
2101 filename: Path to the OBJ file to load
2102 origin: Origin point for positioning the geometry (optional)
2103 height: Height scaling factor (optional, alternative to scale)
2104 scale: Scale factor for all dimensions (optional, alternative to height)
2105 rotation: Rotation to apply to the geometry (optional)
2106 color: Default color for geometry without color data (optional)
2107 upaxis: Up axis orientation ("YUP" or "ZUP")
2108 silent: If True, suppress loading output messages
2109
2110 Returns:
2111 List of UUIDs for the loaded primitives
2112 """
2114
2115 # Parameter type validation
2116 if origin is not None and not isinstance(origin, vec3):
2117 raise ValueError(f"Origin must be a vec3 or None, got {type(origin).__name__}")
2118 if scale is not None and not isinstance(scale, vec3):
2119 raise ValueError(f"Scale must be a vec3 or None, got {type(scale).__name__}")
2120 if rotation is not None and not isinstance(rotation, SphericalCoord):
2121 raise ValueError(f"Rotation must be a SphericalCoord or None, got {type(rotation).__name__}")
2122 if color is not None and not isinstance(color, RGBcolor):
2123 raise ValueError(f"Color must be an RGBcolor or None, got {type(color).__name__}")
2124
2125 # Validate file path for security
2126 validated_filename = self._validate_file_path(filename, ['.obj'])
2127
2128 if origin is None and height is None and scale is None and rotation is None and color is None:
2129 # Simple load with no transformations
2130 return context_wrapper.loadOBJ(self.context, validated_filename, silent)
2131
2132 elif origin is not None and height is not None and scale is None and rotation is not None and color is not None:
2133 # Load with origin, height, rotation, and color (no upaxis)
2134 return context_wrapper.loadOBJWithOriginHeightRotationColor(self.context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), silent)
2135
2136 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":
2137 # Load with origin, height, rotation, color, and upaxis
2138 return context_wrapper.loadOBJWithOriginHeightRotationColorUpaxis(self.context, validated_filename, origin.to_list(), height, rotation.to_list(), color.to_list(), upaxis, silent)
2139
2140 elif origin is not None and scale is not None and rotation is not None and color is not None:
2141 # Load with origin, scale, rotation, color, and upaxis
2142 return context_wrapper.loadOBJWithOriginScaleRotationColorUpaxis(self.context, validated_filename, origin.to_list(), scale.to_list(), rotation.to_list(), color.to_list(), upaxis, silent)
2143
2144 else:
2145 raise ValueError("Invalid parameter combination. For OBJ loading, you must provide either: " +
2146 "1) No parameters (simple load), " +
2147 "2) origin + height + rotation + color, " +
2148 "3) origin + height + rotation + color + upaxis, or " +
2149 "4) origin + scale + rotation + color + upaxis")
2150
2151 def loadXML(self, filename: str, quiet: bool = False) -> List[int]:
2152 """
2153 Load geometry from a Helios XML file.
2154
2155 Args:
2156 filename: Path to the XML file to load
2157 quiet: If True, suppress loading output messages
2158
2159 Returns:
2160 List of UUIDs for the loaded primitives
2161 """
2163 # Validate file path for security
2164 validated_filename = self._validate_file_path(filename, ['.xml'])
2165
2166 return context_wrapper.loadXML(self.context, validated_filename, quiet)
2167
2168 def writePLY(self, filename: str, UUIDs: Optional[List[int]] = None) -> None:
2169 """
2170 Write geometry to a PLY (Stanford Polygon) file.
2171
2172 Args:
2173 filename: Path to the output PLY file
2174 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
2175
2176 Raises:
2177 ValueError: If filename is invalid or UUIDs are invalid
2178 PermissionError: If output directory is not writable
2179 FileNotFoundError: If UUIDs do not exist in context
2180 RuntimeError: If Context is in mock mode
2181
2182 Example:
2183 >>> context.writePLY("output.ply") # Export all primitives
2184 >>> context.writePLY("subset.ply", [uuid1, uuid2]) # Export specific primitives
2185 """
2187
2188 # Validate output file path for security
2189 validated_filename = self._validate_output_file_path(filename, ['.ply'])
2190
2191 if UUIDs is None:
2192 # Export all primitives
2193 context_wrapper.writePLY(self.context, validated_filename)
2194 else:
2195 # Validate UUIDs exist in context
2196 if not UUIDs:
2197 raise ValueError("UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
2198
2199 # Validate each UUID exists
2200 for uuid in UUIDs:
2201 self._validate_uuid(uuid)
2202
2203 # Export specified UUIDs
2204 context_wrapper.writePLYWithUUIDs(self.context, validated_filename, UUIDs)
2205
2206 def writeOBJ(self, filename: str, UUIDs: Optional[List[int]] = None,
2207 primitive_data_fields: Optional[List[str]] = None,
2208 write_normals: bool = False, silent: bool = False) -> None:
2209 """
2210 Write geometry to an OBJ (Wavefront) file.
2211
2212 Args:
2213 filename: Path to the output OBJ file
2214 UUIDs: Optional list of primitive UUIDs to export. If None, exports all primitives
2215 primitive_data_fields: Optional list of primitive data field names to export
2216 write_normals: Whether to include vertex normals in the output
2217 silent: Whether to suppress output messages during export
2218
2219 Raises:
2220 ValueError: If filename is invalid, UUIDs are invalid, or data fields don't exist
2221 PermissionError: If output directory is not writable
2222 FileNotFoundError: If UUIDs do not exist in context
2223 RuntimeError: If Context is in mock mode
2224
2225 Example:
2226 >>> context.writeOBJ("output.obj") # Export all primitives
2227 >>> context.writeOBJ("subset.obj", [uuid1, uuid2]) # Export specific primitives
2228 >>> context.writeOBJ("with_data.obj", [uuid1], ["temperature", "area"]) # Export with data
2229 """
2231
2232 # Validate output file path for security
2233 validated_filename = self._validate_output_file_path(filename, ['.obj'])
2234
2235 if UUIDs is None:
2236 # Export all primitives
2237 context_wrapper.writeOBJ(self.context, validated_filename, write_normals, silent)
2238 elif primitive_data_fields is None:
2239 # Export specified UUIDs without data fields
2240 if not UUIDs:
2241 raise ValueError("UUIDs list cannot be empty. Use UUIDs=None to export all primitives")
2242
2243 # Validate each UUID exists
2244 for uuid in UUIDs:
2245 self._validate_uuid(uuid)
2246
2247 context_wrapper.writeOBJWithUUIDs(self.context, validated_filename, UUIDs, write_normals, silent)
2248 else:
2249 # Export specified UUIDs with primitive data fields
2250 if not UUIDs:
2251 raise ValueError("UUIDs list cannot be empty when exporting primitive data")
2252 if not primitive_data_fields:
2253 raise ValueError("primitive_data_fields list cannot be empty")
2254
2255 # Validate each UUID exists
2256 for uuid in UUIDs:
2257 self._validate_uuid(uuid)
2258
2259 # Note: Primitive data field validation is handled by the native library
2260 # which will raise appropriate errors if fields don't exist for the specified primitives
2261
2262 context_wrapper.writeOBJWithPrimitiveData(self.context, validated_filename, UUIDs, primitive_data_fields, write_normals, silent)
2263
2264 def writePrimitiveData(self, filename: str, column_labels: List[str],
2265 UUIDs: Optional[List[int]] = None,
2266 print_header: bool = False) -> None:
2267 """
2268 Write primitive data to an ASCII text file.
2269
2270 Outputs a space-separated text file where each row corresponds to a primitive
2271 and each column corresponds to a primitive data label.
2272
2273 Args:
2274 filename: Path to the output file
2275 column_labels: List of primitive data labels to include as columns.
2276 Use "UUID" to include primitive UUIDs as a column.
2277 The order determines the column order in the output file.
2278 UUIDs: Optional list of primitive UUIDs to include. If None, includes all primitives.
2279 print_header: If True, writes column labels as the first line of the file
2280
2281 Raises:
2282 ValueError: If filename is invalid, column_labels is empty, or UUIDs list is empty when provided
2283 HeliosFileIOError: If file cannot be written
2284 HeliosRuntimeError: If a column label doesn't exist for any primitive
2285
2286 Example:
2287 >>> # Write temperature and area for all primitives
2288 >>> context.writePrimitiveData("output.txt", ["UUID", "temperature", "area"])
2289
2290 >>> # Write with header row
2291 >>> context.writePrimitiveData("output.txt", ["UUID", "radiation_flux"], print_header=True)
2292
2293 >>> # Write only for selected primitives
2294 >>> context.writePrimitiveData("subset.txt", ["temperature"], UUIDs=[uuid1, uuid2])
2295 """
2297
2298 # Validate column_labels
2299 if not column_labels:
2300 raise ValueError("column_labels list cannot be empty")
2302 # Validate output file path (allow any extension for text files)
2303 validated_filename = self._validate_output_file_path(filename)
2304
2305 if UUIDs is None:
2306 # Export all primitives
2307 context_wrapper.writePrimitiveData(self.context, validated_filename, column_labels, print_header)
2308 else:
2309 # Export specified UUIDs
2310 if not UUIDs:
2311 raise ValueError("UUIDs list cannot be empty when provided. Use UUIDs=None to include all primitives")
2312
2313 # Validate each UUID exists
2314 for uuid in UUIDs:
2315 self._validate_uuid(uuid)
2316
2317 context_wrapper.writePrimitiveDataWithUUIDs(self.context, validated_filename, column_labels, UUIDs, print_header)
2318
2319 def addTrianglesFromArrays(self, vertices: np.ndarray, faces: np.ndarray,
2320 colors: Optional[np.ndarray] = None) -> List[int]:
2321 """
2322 Add triangles from NumPy arrays (compatible with trimesh, Open3D format).
2323
2324 Args:
2325 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2326 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2327 colors: Optional NumPy array of shape (N, 3) or (M, 3) containing RGB colors as float32/float64
2328 If shape (N, 3): per-vertex colors
2329 If shape (M, 3): per-triangle colors
2330
2331 Returns:
2332 List of UUIDs for the added triangles
2333
2334 Raises:
2335 ValueError: If array dimensions are invalid
2336 """
2337 # Validate input arrays
2338 if vertices.ndim != 2 or vertices.shape[1] != 3:
2339 raise ValueError(f"Vertices array must have shape (N, 3), got {vertices.shape}")
2340 if faces.ndim != 2 or faces.shape[1] != 3:
2341 raise ValueError(f"Faces array must have shape (M, 3), got {faces.shape}")
2342
2343 # Check vertex indices are valid
2344 max_vertex_index = np.max(faces)
2345 if max_vertex_index >= vertices.shape[0]:
2346 raise ValueError(f"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2347
2348 # Validate colors array if provided
2349 per_vertex_colors = False
2350 per_triangle_colors = False
2351 if colors is not None:
2352 if colors.ndim != 2 or colors.shape[1] != 3:
2353 raise ValueError(f"Colors array must have shape (N, 3) or (M, 3), got {colors.shape}")
2354 if colors.shape[0] == vertices.shape[0]:
2355 per_vertex_colors = True
2356 elif colors.shape[0] == faces.shape[0]:
2357 per_triangle_colors = True
2358 else:
2359 raise ValueError(f"Colors array shape {colors.shape} doesn't match vertices ({vertices.shape[0]},) or faces ({faces.shape[0]},)")
2360
2361 # Convert arrays to appropriate data types
2362 vertices_float = vertices.astype(np.float32)
2363 faces_int = faces.astype(np.int32)
2364 if colors is not None:
2365 colors_float = colors.astype(np.float32)
2366
2367 # Add triangles
2368 triangle_uuids = []
2369 for i in range(faces.shape[0]):
2370 # Get vertex indices for this triangle
2371 v0_idx, v1_idx, v2_idx = faces_int[i]
2372
2373 # Get vertex coordinates
2374 vertex0 = vertices_float[v0_idx].tolist()
2375 vertex1 = vertices_float[v1_idx].tolist()
2376 vertex2 = vertices_float[v2_idx].tolist()
2377
2378 # Add triangle with or without color
2379 if colors is None:
2380 # No color specified
2381 uuid = context_wrapper.addTriangle(self.context, vertex0, vertex1, vertex2)
2382 elif per_triangle_colors:
2383 # Use per-triangle color
2384 color = colors_float[i].tolist()
2385 uuid = context_wrapper.addTriangleWithColor(self.context, vertex0, vertex1, vertex2, color)
2386 elif per_vertex_colors:
2387 # Average the per-vertex colors for the triangle
2388 color = np.mean([colors_float[v0_idx], colors_float[v1_idx], colors_float[v2_idx]], axis=0).tolist()
2389 uuid = context_wrapper.addTriangleWithColor(self.context, vertex0, vertex1, vertex2, color)
2390
2391 triangle_uuids.append(uuid)
2392
2393 return triangle_uuids
2394
2395 def addTrianglesFromArraysTextured(self, vertices: np.ndarray, faces: np.ndarray,
2396 uv_coords: np.ndarray, texture_files: Union[str, List[str]],
2397 material_ids: Optional[np.ndarray] = None) -> List[int]:
2398 """
2399 Add textured triangles from NumPy arrays with support for multiple textures.
2400
2401 This method supports both single-texture and multi-texture workflows:
2402 - Single texture: Pass a single texture file string, all faces use the same texture
2403 - Multiple textures: Pass a list of texture files and material_ids array specifying which texture each face uses
2404
2405 Args:
2406 vertices: NumPy array of shape (N, 3) containing vertex coordinates as float32/float64
2407 faces: NumPy array of shape (M, 3) containing triangle vertex indices as int32/int64
2408 uv_coords: NumPy array of shape (N, 2) containing UV texture coordinates as float32/float64
2409 texture_files: Single texture file path (str) or list of texture file paths (List[str])
2410 material_ids: Optional NumPy array of shape (M,) containing material ID for each face.
2411 If None and texture_files is a list, all faces use texture 0.
2412 If None and texture_files is a string, this parameter is ignored.
2413
2414 Returns:
2415 List of UUIDs for the added textured triangles
2416
2417 Raises:
2418 ValueError: If array dimensions are invalid or material IDs are out of range
2419
2420 Example:
2421 # Single texture usage (backward compatible)
2422 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, "texture.png")
2423
2424 # Multi-texture usage (Open3D style)
2425 >>> texture_files = ["wood.png", "metal.png", "glass.png"]
2426 >>> material_ids = np.array([0, 0, 1, 1, 2, 2]) # 6 faces using different textures
2427 >>> uuids = context.addTrianglesFromArraysTextured(vertices, faces, uvs, texture_files, material_ids)
2428 """
2430
2431 # Validate input arrays
2432 if vertices.ndim != 2 or vertices.shape[1] != 3:
2433 raise ValueError(f"Vertices array must have shape (N, 3), got {vertices.shape}")
2434 if faces.ndim != 2 or faces.shape[1] != 3:
2435 raise ValueError(f"Faces array must have shape (M, 3), got {faces.shape}")
2436 if uv_coords.ndim != 2 or uv_coords.shape[1] != 2:
2437 raise ValueError(f"UV coordinates array must have shape (N, 2), got {uv_coords.shape}")
2438
2439 # Check array consistency
2440 if uv_coords.shape[0] != vertices.shape[0]:
2441 raise ValueError(f"UV coordinates count ({uv_coords.shape[0]}) must match vertices count ({vertices.shape[0]})")
2442
2443 # Check vertex indices are valid
2444 max_vertex_index = np.max(faces)
2445 if max_vertex_index >= vertices.shape[0]:
2446 raise ValueError(f"Face indices reference vertex {max_vertex_index}, but only {vertices.shape[0]} vertices provided")
2447
2448 # Handle texture files parameter (single string or list)
2449 if isinstance(texture_files, str):
2450 # Single texture case - use original implementation for efficiency
2451 texture_file_list = [texture_files]
2452 if material_ids is None:
2453 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2454 else:
2455 # Validate that all material IDs are 0 for single texture
2456 if not np.all(material_ids == 0):
2457 raise ValueError("When using single texture file, all material IDs must be 0")
2458 else:
2459 # Multiple textures case
2460 texture_file_list = list(texture_files)
2461 if len(texture_file_list) == 0:
2462 raise ValueError("Texture files list cannot be empty")
2463
2464 if material_ids is None:
2465 # Default: all faces use first texture
2466 material_ids = np.zeros(faces.shape[0], dtype=np.uint32)
2467 else:
2468 # Validate material IDs array
2469 if material_ids.ndim != 1 or material_ids.shape[0] != faces.shape[0]:
2470 raise ValueError(f"Material IDs must have shape ({faces.shape[0]},), got {material_ids.shape}")
2471
2472 # Check material ID range
2473 max_material_id = np.max(material_ids)
2474 if max_material_id >= len(texture_file_list):
2475 raise ValueError(f"Material ID {max_material_id} exceeds texture count {len(texture_file_list)}")
2476
2477 # Validate all texture files exist
2478 for i, texture_file in enumerate(texture_file_list):
2479 try:
2480 self._validate_file_path(texture_file)
2481 except (FileNotFoundError, ValueError) as e:
2482 raise ValueError(f"Texture file {i} ({texture_file}): {e}")
2483
2484 # Use efficient multi-texture C++ implementation if available, otherwise triangle-by-triangle
2485 if 'addTrianglesFromArraysMultiTextured' in context_wrapper._AVAILABLE_TRIANGLE_FUNCTIONS:
2486 return context_wrapper.addTrianglesFromArraysMultiTextured(
2487 self.context, vertices, faces, uv_coords, texture_file_list, material_ids
2488 )
2489 else:
2490 # Use triangle-by-triangle approach with addTriangleTextured
2491 from .wrappers.DataTypes import vec3, vec2
2492
2493 vertices_float = vertices.astype(np.float32)
2494 faces_int = faces.astype(np.int32)
2495 uv_coords_float = uv_coords.astype(np.float32)
2496
2497 triangle_uuids = []
2498 for i in range(faces.shape[0]):
2499 # Get vertex indices for this triangle
2500 v0_idx, v1_idx, v2_idx = faces_int[i]
2501
2502 # Get vertex coordinates as vec3 objects
2503 vertex0 = vec3(vertices_float[v0_idx][0], vertices_float[v0_idx][1], vertices_float[v0_idx][2])
2504 vertex1 = vec3(vertices_float[v1_idx][0], vertices_float[v1_idx][1], vertices_float[v1_idx][2])
2505 vertex2 = vec3(vertices_float[v2_idx][0], vertices_float[v2_idx][1], vertices_float[v2_idx][2])
2506
2507 # Get UV coordinates as vec2 objects
2508 uv0 = vec2(uv_coords_float[v0_idx][0], uv_coords_float[v0_idx][1])
2509 uv1 = vec2(uv_coords_float[v1_idx][0], uv_coords_float[v1_idx][1])
2510 uv2 = vec2(uv_coords_float[v2_idx][0], uv_coords_float[v2_idx][1])
2511
2512 # Use texture file based on material ID for this triangle
2513 material_id = material_ids[i]
2514 texture_file = texture_file_list[material_id]
2515
2516 # Add textured triangle using the new addTriangleTextured method
2517 uuid = self.addTriangleTextured(vertex0, vertex1, vertex2, texture_file, uv0, uv1, uv2)
2518 triangle_uuids.append(uuid)
2519
2520 return triangle_uuids
2521
2522 # ==================== PRIMITIVE DATA METHODS ====================
2523 # Primitive data is a flexible key-value store where users can associate
2524 # arbitrary data with primitives using string keys
2525
2526 def setPrimitiveDataInt(self, uuids_or_uuid, label: str, value: int) -> None:
2527 """
2528 Set primitive data as signed 32-bit integer for one or multiple primitives.
2529
2530 Args:
2531 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2532 label: String key for the data
2533 value: Signed integer scalar (broadcast to all UUIDs), or a list of
2534 values (one per UUID) to set a distinct value on each primitive.
2535 """
2536 if isinstance(uuids_or_uuid, (list, tuple)):
2537 if isinstance(value, (list, tuple, np.ndarray)):
2538 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Int', value)
2539 else:
2540 context_wrapper.setBroadcastPrimitiveDataInt(self.context, uuids_or_uuid, label, value)
2541 else:
2542 context_wrapper.setPrimitiveDataInt(self.context, uuids_or_uuid, label, value)
2544 def setPrimitiveDataUInt(self, uuids_or_uuid, label: str, value: int) -> None:
2545 """
2546 Set primitive data as unsigned 32-bit integer for one or multiple primitives.
2547
2548 Critical for properties like 'twosided_flag' which must be uint in C++.
2549
2550 Args:
2551 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2552 label: String key for the data
2553 value: Unsigned integer scalar (broadcast to all UUIDs), or a list of
2554 values (one per UUID) to set a distinct value on each primitive.
2555 """
2556 if isinstance(uuids_or_uuid, (list, tuple)):
2557 if isinstance(value, (list, tuple, np.ndarray)):
2558 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'UInt', value)
2559 else:
2560 context_wrapper.setBroadcastPrimitiveDataUInt(self.context, uuids_or_uuid, label, value)
2561 else:
2562 context_wrapper.setPrimitiveDataUInt(self.context, uuids_or_uuid, label, value)
2564 def setPrimitiveDataFloat(self, uuids_or_uuid, label: str, value: float) -> None:
2565 """
2566 Set primitive data as 32-bit float for one or multiple primitives.
2567
2568 Args:
2569 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2570 label: String key for the data
2571 value: Float scalar (broadcast to all UUIDs), or a list of values
2572 (one per UUID) to set a distinct value on each primitive.
2573 """
2574 if isinstance(uuids_or_uuid, (list, tuple)):
2575 if isinstance(value, (list, tuple, np.ndarray)):
2576 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Float', value)
2577 else:
2578 context_wrapper.setBroadcastPrimitiveDataFloat(self.context, uuids_or_uuid, label, value)
2579 else:
2580 context_wrapper.setPrimitiveDataFloat(self.context, uuids_or_uuid, label, value)
2582 def setPrimitiveDataDouble(self, uuids_or_uuid, label: str, value: float) -> None:
2583 """
2584 Set primitive data as 64-bit double for one or multiple primitives.
2585
2586 Args:
2587 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2588 label: String key for the data
2589 value: Double scalar (broadcast to all UUIDs), or a list of values
2590 (one per UUID) to set a distinct value on each primitive.
2591 """
2592 if isinstance(uuids_or_uuid, (list, tuple)):
2593 if isinstance(value, (list, tuple, np.ndarray)):
2594 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Double', value)
2595 else:
2596 context_wrapper.setBroadcastPrimitiveDataDouble(self.context, uuids_or_uuid, label, value)
2597 else:
2598 context_wrapper.setPrimitiveDataDouble(self.context, uuids_or_uuid, label, value)
2600 def setPrimitiveDataString(self, uuids_or_uuid, label: str, value: str) -> None:
2601 """
2602 Set primitive data as string for one or multiple primitives.
2603
2604 Args:
2605 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2606 label: String key for the data
2607 value: String scalar (broadcast to all UUIDs), or a list of strings
2608 (one per UUID) to set a distinct value on each primitive.
2609 """
2610 if isinstance(uuids_or_uuid, (list, tuple)):
2611 if isinstance(value, (list, tuple, np.ndarray)):
2612 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'String', value)
2613 else:
2614 context_wrapper.setBroadcastPrimitiveDataString(self.context, uuids_or_uuid, label, value)
2615 else:
2616 context_wrapper.setPrimitiveDataString(self.context, uuids_or_uuid, label, value)
2618 def setPrimitiveDataVec2(self, uuids_or_uuid, label: str, x_or_vec, y: float = None) -> None:
2619 """
2620 Set primitive data as vec2 for one or multiple primitives.
2621
2622 Args:
2623 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2624 label: String key for the data
2625 x_or_vec: Either x component (float) or vec2 object
2626 y: Y component (if x_or_vec is float)
2627 """
2628 if isinstance(uuids_or_uuid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2629 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Vec2', x_or_vec)
2630 return
2631 if hasattr(x_or_vec, 'x'):
2632 x, y = x_or_vec.x, x_or_vec.y
2633 else:
2634 x = x_or_vec
2635 if isinstance(uuids_or_uuid, (list, tuple)):
2636 context_wrapper.setBroadcastPrimitiveDataVec2(self.context, uuids_or_uuid, label, x, y)
2637 else:
2638 context_wrapper.setPrimitiveDataVec2(self.context, uuids_or_uuid, label, x, y)
2639
2640 def setPrimitiveDataVec3(self, uuids_or_uuid, label: str, x_or_vec, y: float = None, z: float = None) -> None:
2641 """
2642 Set primitive data as vec3 for one or multiple primitives.
2643
2644 Args:
2645 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2646 label: String key for the data
2647 x_or_vec: Either x component (float) or vec3 object
2648 y: Y component (if x_or_vec is float)
2649 z: Z component (if x_or_vec is float)
2650 """
2651 if isinstance(uuids_or_uuid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2652 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Vec3', x_or_vec)
2653 return
2654 if hasattr(x_or_vec, 'x'):
2655 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2656 else:
2657 x = x_or_vec
2658 if isinstance(uuids_or_uuid, (list, tuple)):
2659 context_wrapper.setBroadcastPrimitiveDataVec3(self.context, uuids_or_uuid, label, x, y, z)
2660 else:
2661 context_wrapper.setPrimitiveDataVec3(self.context, uuids_or_uuid, label, x, y, z)
2662
2663 def setPrimitiveDataVec4(self, uuids_or_uuid, label: str, x_or_vec, y: float = None, z: float = None, w: float = None) -> None:
2664 """
2665 Set primitive data as vec4 for one or multiple primitives.
2666
2667 Args:
2668 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2669 label: String key for the data
2670 x_or_vec: Either x component (float) or vec4 object
2671 y: Y component (if x_or_vec is float)
2672 z: Z component (if x_or_vec is float)
2673 w: W component (if x_or_vec is float)
2674 """
2675 if isinstance(uuids_or_uuid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2676 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Vec4', x_or_vec)
2677 return
2678 if hasattr(x_or_vec, 'x'):
2679 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2680 else:
2681 x = x_or_vec
2682 if isinstance(uuids_or_uuid, (list, tuple)):
2683 context_wrapper.setBroadcastPrimitiveDataVec4(self.context, uuids_or_uuid, label, x, y, z, w)
2684 else:
2685 context_wrapper.setPrimitiveDataVec4(self.context, uuids_or_uuid, label, x, y, z, w)
2686
2687 def setPrimitiveDataInt2(self, uuids_or_uuid, label: str, x_or_vec, y: int = None) -> None:
2688 """
2689 Set primitive data as int2 for one or multiple primitives.
2690
2691 Args:
2692 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2693 label: String key for the data
2694 x_or_vec: Either x component (int) or int2 object
2695 y: Y component (if x_or_vec is int)
2696 """
2697 if isinstance(uuids_or_uuid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2698 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Int2', x_or_vec)
2699 return
2700 if hasattr(x_or_vec, 'x'):
2701 x, y = x_or_vec.x, x_or_vec.y
2702 else:
2703 x = x_or_vec
2704 if isinstance(uuids_or_uuid, (list, tuple)):
2705 context_wrapper.setBroadcastPrimitiveDataInt2(self.context, uuids_or_uuid, label, x, y)
2706 else:
2707 context_wrapper.setPrimitiveDataInt2(self.context, uuids_or_uuid, label, x, y)
2708
2709 def setPrimitiveDataInt3(self, uuids_or_uuid, label: str, x_or_vec, y: int = None, z: int = None) -> None:
2710 """
2711 Set primitive data as int3 for one or multiple primitives.
2712
2713 Args:
2714 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2715 label: String key for the data
2716 x_or_vec: Either x component (int) or int3 object
2717 y: Y component (if x_or_vec is int)
2718 z: Z component (if x_or_vec is int)
2719 """
2720 if isinstance(uuids_or_uuid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2721 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Int3', x_or_vec)
2722 return
2723 if hasattr(x_or_vec, 'x'):
2724 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2725 else:
2726 x = x_or_vec
2727 if isinstance(uuids_or_uuid, (list, tuple)):
2728 context_wrapper.setBroadcastPrimitiveDataInt3(self.context, uuids_or_uuid, label, x, y, z)
2729 else:
2730 context_wrapper.setPrimitiveDataInt3(self.context, uuids_or_uuid, label, x, y, z)
2731
2732 def setPrimitiveDataInt4(self, uuids_or_uuid, label: str, x_or_vec, y: int = None, z: int = None, w: int = None) -> None:
2733 """
2734 Set primitive data as int4 for one or multiple primitives.
2735
2736 Args:
2737 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2738 label: String key for the data
2739 x_or_vec: Either x component (int) or int4 object
2740 y: Y component (if x_or_vec is int)
2741 z: Z component (if x_or_vec is int)
2742 w: W component (if x_or_vec is int)
2743 """
2744 if isinstance(uuids_or_uuid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
2745 context_wrapper.setPrimitiveDataArray(self.context, uuids_or_uuid, label, 'Int4', x_or_vec)
2746 return
2747 if hasattr(x_or_vec, 'x'):
2748 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2749 else:
2750 x = x_or_vec
2751 if isinstance(uuids_or_uuid, (list, tuple)):
2752 context_wrapper.setBroadcastPrimitiveDataInt4(self.context, uuids_or_uuid, label, x, y, z, w)
2753 else:
2754 context_wrapper.setPrimitiveDataInt4(self.context, uuids_or_uuid, label, x, y, z, w)
2755
2756 def getPrimitiveData(self, uuid: int, label: str, data_type: type = None):
2757 """
2758 Get primitive data for a specific primitive. If data_type is provided, it works like before.
2759 If data_type is None, it automatically detects the type and returns the appropriate value.
2760
2761 Args:
2762 uuid: UUID of the primitive
2763 label: String key for the data
2764 data_type: Optional. Python type to retrieve (int, uint, float, double, bool, str, vec2, vec3, vec4, int2, int3, int4, etc.)
2765 If None, auto-detects the type using C++ getPrimitiveDataType().
2766
2767 Returns:
2768 The stored value of the specified or auto-detected type
2769 """
2770 # If no type specified, use auto-detection
2771 if data_type is None:
2772 return context_wrapper.getPrimitiveDataAuto(self.context, uuid, label)
2773
2774 # Handle basic types (original behavior when type is specified)
2775 if data_type == int:
2776 return context_wrapper.getPrimitiveDataInt(self.context, uuid, label)
2777 elif data_type == float:
2778 return context_wrapper.getPrimitiveDataFloat(self.context, uuid, label)
2779 elif data_type == bool:
2780 # Bool is not supported by Helios core - get as int and convert
2781 int_value = context_wrapper.getPrimitiveDataInt(self.context, uuid, label)
2782 return int_value != 0
2783 elif data_type == str:
2784 return context_wrapper.getPrimitiveDataString(self.context, uuid, label)
2785
2786 # Handle Helios vector types
2787 elif data_type == vec2:
2788 coords = context_wrapper.getPrimitiveDataVec2(self.context, uuid, label)
2789 return vec2(coords[0], coords[1])
2790 elif data_type == vec3:
2791 coords = context_wrapper.getPrimitiveDataVec3(self.context, uuid, label)
2792 return vec3(coords[0], coords[1], coords[2])
2793 elif data_type == vec4:
2794 coords = context_wrapper.getPrimitiveDataVec4(self.context, uuid, label)
2795 return vec4(coords[0], coords[1], coords[2], coords[3])
2796 elif data_type == int2:
2797 coords = context_wrapper.getPrimitiveDataInt2(self.context, uuid, label)
2798 return int2(coords[0], coords[1])
2799 elif data_type == int3:
2800 coords = context_wrapper.getPrimitiveDataInt3(self.context, uuid, label)
2801 return int3(coords[0], coords[1], coords[2])
2802 elif data_type == int4:
2803 coords = context_wrapper.getPrimitiveDataInt4(self.context, uuid, label)
2804 return int4(coords[0], coords[1], coords[2], coords[3])
2805
2806 # Handle extended numeric types (require explicit specification since Python doesn't have these as distinct types)
2807 elif data_type == "uint":
2808 return context_wrapper.getPrimitiveDataUInt(self.context, uuid, label)
2809 elif data_type == "double":
2810 return context_wrapper.getPrimitiveDataDouble(self.context, uuid, label)
2811
2812 # Handle list return types (for convenience)
2813 elif data_type == list:
2814 # Default to vec3 as list for backward compatibility
2815 return context_wrapper.getPrimitiveDataVec3(self.context, uuid, label)
2816 elif data_type == "list_vec2":
2817 return context_wrapper.getPrimitiveDataVec2(self.context, uuid, label)
2818 elif data_type == "list_vec4":
2819 return context_wrapper.getPrimitiveDataVec4(self.context, uuid, label)
2820 elif data_type == "list_int2":
2821 return context_wrapper.getPrimitiveDataInt2(self.context, uuid, label)
2822 elif data_type == "list_int3":
2823 return context_wrapper.getPrimitiveDataInt3(self.context, uuid, label)
2824 elif data_type == "list_int4":
2825 return context_wrapper.getPrimitiveDataInt4(self.context, uuid, label)
2826
2827 else:
2828 raise ValueError(f"Unsupported primitive data type: {data_type}. "
2829 f"Supported types: int, float, bool, str, vec2, vec3, vec4, int2, int3, int4, "
2830 f"'uint', 'double', list (for vec3), 'list_vec2', 'list_vec4', 'list_int2', 'list_int3', 'list_int4'")
2831
2832 def doesPrimitiveDataExist(self, uuid: int, label: str) -> bool:
2833 """
2834 Check if primitive data exists for a specific primitive and label.
2835
2836 Args:
2837 uuid: UUID of the primitive
2838 label: String key for the data
2839
2840 Returns:
2841 True if the data exists, False otherwise
2842 """
2843 return context_wrapper.doesPrimitiveDataExistWrapper(self.context, uuid, label)
2844
2845 def getPrimitiveDataFloat(self, uuid: int, label: str) -> float:
2846 """
2847 Convenience method to get float primitive data.
2848
2849 Args:
2850 uuid: UUID of the primitive
2851 label: String key for the data
2852
2853 Returns:
2854 Float value stored for the primitive
2855 """
2856 return self.getPrimitiveData(uuid, label, float)
2857
2858 def getPrimitiveDataType(self, uuid: int, label: str) -> int:
2859 """
2860 Get the Helios data type of primitive data.
2861
2862 Args:
2863 uuid: UUID of the primitive
2864 label: String key for the data
2865
2866 Returns:
2867 HeliosDataType enum value as integer
2868 """
2869 return context_wrapper.getPrimitiveDataTypeWrapper(self.context, uuid, label)
2870
2871 def getPrimitiveDataSize(self, uuid: int, label: str) -> int:
2872 """
2873 Get the size/length of primitive data (for vector data).
2874
2875 Args:
2876 uuid: UUID of the primitive
2877 label: String key for the data
2878
2879 Returns:
2880 Size of data array, or 1 for scalar data
2881 """
2882 return context_wrapper.getPrimitiveDataSizeWrapper(self.context, uuid, label)
2883
2884 def getPrimitiveDataArray(self, uuids: List[int], label: str) -> np.ndarray:
2885 """
2886 Get primitive data values for multiple primitives as a NumPy array.
2887
2888 This method retrieves primitive data for a list of UUIDs and returns the values
2889 as a NumPy array. The output array has the same length as the input UUID list,
2890 with each index corresponding to the primitive data value for that UUID.
2891
2892 Args:
2893 uuids: List of primitive UUIDs to get data for
2894 label: String key for the primitive data to retrieve
2895
2896 Returns:
2897 NumPy array of primitive data values corresponding to each UUID.
2898 The array type depends on the data type:
2899 - int data: int32 array
2900 - uint data: uint32 array
2901 - float data: float32 array
2902 - double data: float64 array
2903 - vector data: float32 array with shape (N, vector_size)
2904 - string data: object array of strings
2905
2906 Raises:
2907 ValueError: If UUID list is empty or UUIDs don't exist
2908 RuntimeError: If context is in mock mode or data doesn't exist for some UUIDs
2909 """
2911
2912 if not uuids:
2913 raise ValueError("UUID list cannot be empty")
2914
2915 # First validate that all UUIDs exist
2916 for uuid in uuids:
2918
2919 # Then check that all UUIDs have the specified data
2920 for uuid in uuids:
2921 if not self.doesPrimitiveDataExist(uuid, label):
2922 raise ValueError(f"Primitive data '{label}' does not exist for UUID {uuid}")
2923
2924 # Get data type from the first UUID to determine array type
2925 first_uuid = uuids[0]
2926 data_type = self.getPrimitiveDataType(first_uuid, label)
2927
2928 # Map Helios data types to NumPy array creation
2929 # Based on HeliosDataType enum from Helios core
2930 if data_type == 0: # HELIOS_TYPE_INT
2931 result = np.empty(len(uuids), dtype=np.int32)
2932 for i, uuid in enumerate(uuids):
2933 result[i] = self.getPrimitiveData(uuid, label, int)
2934
2935 elif data_type == 1: # HELIOS_TYPE_UINT
2936 result = np.empty(len(uuids), dtype=np.uint32)
2937 for i, uuid in enumerate(uuids):
2938 result[i] = self.getPrimitiveData(uuid, label, "uint")
2939
2940 elif data_type == 2: # HELIOS_TYPE_FLOAT
2941 result = np.empty(len(uuids), dtype=np.float32)
2942 for i, uuid in enumerate(uuids):
2943 result[i] = self.getPrimitiveData(uuid, label, float)
2944
2945 elif data_type == 3: # HELIOS_TYPE_DOUBLE
2946 result = np.empty(len(uuids), dtype=np.float64)
2947 for i, uuid in enumerate(uuids):
2948 result[i] = self.getPrimitiveData(uuid, label, "double")
2949
2950 elif data_type == 4: # HELIOS_TYPE_VEC2
2951 result = np.empty((len(uuids), 2), dtype=np.float32)
2952 for i, uuid in enumerate(uuids):
2953 vec_data = self.getPrimitiveData(uuid, label, vec2)
2954 result[i] = [vec_data.x, vec_data.y]
2955
2956 elif data_type == 5: # HELIOS_TYPE_VEC3
2957 result = np.empty((len(uuids), 3), dtype=np.float32)
2958 for i, uuid in enumerate(uuids):
2959 vec_data = self.getPrimitiveData(uuid, label, vec3)
2960 result[i] = [vec_data.x, vec_data.y, vec_data.z]
2961
2962 elif data_type == 6: # HELIOS_TYPE_VEC4
2963 result = np.empty((len(uuids), 4), dtype=np.float32)
2964 for i, uuid in enumerate(uuids):
2965 vec_data = self.getPrimitiveData(uuid, label, vec4)
2966 result[i] = [vec_data.x, vec_data.y, vec_data.z, vec_data.w]
2967
2968 elif data_type == 7: # HELIOS_TYPE_INT2
2969 result = np.empty((len(uuids), 2), dtype=np.int32)
2970 for i, uuid in enumerate(uuids):
2971 int_data = self.getPrimitiveData(uuid, label, int2)
2972 result[i] = [int_data.x, int_data.y]
2973
2974 elif data_type == 8: # HELIOS_TYPE_INT3
2975 result = np.empty((len(uuids), 3), dtype=np.int32)
2976 for i, uuid in enumerate(uuids):
2977 int_data = self.getPrimitiveData(uuid, label, int3)
2978 result[i] = [int_data.x, int_data.y, int_data.z]
2979
2980 elif data_type == 9: # HELIOS_TYPE_INT4
2981 result = np.empty((len(uuids), 4), dtype=np.int32)
2982 for i, uuid in enumerate(uuids):
2983 int_data = self.getPrimitiveData(uuid, label, int4)
2984 result[i] = [int_data.x, int_data.y, int_data.z, int_data.w]
2985
2986 elif data_type == 10: # HELIOS_TYPE_STRING
2987 result = np.empty(len(uuids), dtype=object)
2988 for i, uuid in enumerate(uuids):
2989 result[i] = self.getPrimitiveData(uuid, label, str)
2990
2991 else:
2992 raise ValueError(f"Unsupported primitive data type: {data_type}")
2993
2994 return result
2995
2996
2997 def colorPrimitiveByDataPseudocolor(self, uuids: List[int], primitive_data: str,
2998 colormap: str = "hot", ncolors: int = 10,
2999 max_val: Optional[float] = None, min_val: Optional[float] = None):
3000 """
3001 Color primitives based on primitive data values using pseudocolor mapping.
3002
3003 This method applies a pseudocolor mapping to primitives based on the values
3004 of specified primitive data. The primitive colors are updated to reflect the
3005 data values using a color map.
3006
3007 Args:
3008 uuids: List of primitive UUIDs to color
3009 primitive_data: Name of primitive data to use for coloring (e.g., "radiation_flux_SW")
3010 colormap: Color map name - options include "hot", "cool", "parula", "rainbow", "gray", "lava"
3011 ncolors: Number of discrete colors in color map (default: 10)
3012 max_val: Maximum value for color scale (auto-determined if None)
3013 min_val: Minimum value for color scale (auto-determined if None)
3014 """
3015 if max_val is not None and min_val is not None:
3016 context_wrapper.colorPrimitiveByDataPseudocolorWithRange(
3017 self.context, uuids, primitive_data, colormap, ncolors, max_val, min_val)
3018 else:
3019 context_wrapper.colorPrimitiveByDataPseudocolor(
3020 self.context, uuids, primitive_data, colormap, ncolors)
3021
3022 # Context time/date methods for solar position integration
3023 def setTime(self, hour: int, minute: int = 0, second: int = 0):
3024 """
3025 Set the simulation time.
3026
3027 Args:
3028 hour: Hour (0-23)
3029 minute: Minute (0-59), defaults to 0
3030 second: Second (0-59), defaults to 0
3031
3032 Raises:
3033 ValueError: If time values are out of range
3034 NotImplementedError: If time/date functions not available in current library build
3035
3036 Example:
3037 >>> context.setTime(14, 30) # Set to 2:30 PM
3038 >>> context.setTime(9, 15, 30) # Set to 9:15:30 AM
3039 """
3040 context_wrapper.setTime(self.context, hour, minute, second)
3041
3042 def setDate(self, year: int, month: int, day: int):
3043 """
3044 Set the simulation date.
3045
3046 Args:
3047 year: Year (1900-3000)
3048 month: Month (1-12)
3049 day: Day (1-31)
3050
3051 Raises:
3052 ValueError: If date values are out of range
3053 NotImplementedError: If time/date functions not available in current library build
3054
3055 Example:
3056 >>> context.setDate(2023, 6, 21) # Set to June 21, 2023
3057 """
3058 context_wrapper.setDate(self.context, year, month, day)
3059
3060 def setDateJulian(self, julian_day: int, year: int):
3061 """
3062 Set the simulation date using Julian day number.
3063
3064 Args:
3065 julian_day: Julian day (1-366)
3066 year: Year (1900-3000)
3067
3068 Raises:
3069 ValueError: If values are out of range
3070 NotImplementedError: If time/date functions not available in current library build
3071
3072 Example:
3073 >>> context.setDateJulian(172, 2023) # Set to day 172 of 2023 (June 21)
3074 """
3075 context_wrapper.setDateJulian(self.context, julian_day, year)
3076
3077 def getTime(self):
3078 """
3079 Get the current simulation time.
3080
3081 Returns:
3082 Tuple of (hour, minute, second) as integers
3083
3084 Raises:
3085 NotImplementedError: If time/date functions not available in current library build
3086
3087 Example:
3088 >>> hour, minute, second = context.getTime()
3089 >>> print(f"Current time: {hour:02d}:{minute:02d}:{second:02d}")
3090 """
3091 return context_wrapper.getTime(self.context)
3092
3093 def getDate(self):
3094 """
3095 Get the current simulation date.
3096
3097 Returns:
3098 Tuple of (year, month, day) as integers
3099
3100 Raises:
3101 NotImplementedError: If time/date functions not available in current library build
3102
3103 Example:
3104 >>> year, month, day = context.getDate()
3105 >>> print(f"Current date: {year}-{month:02d}-{day:02d}")
3106 """
3107 return context_wrapper.getDate(self.context)
3108
3109 # ==========================================================================
3110 # Timeseries Methods
3111 # ==========================================================================
3112
3113 def addTimeseriesData(self, label: str, value: float, date: 'Date', time: 'Time'):
3114 """
3115 Add a data point to a timeseries variable.
3116
3117 Args:
3118 label: Name of the timeseries variable (e.g., "temperature")
3119 value: Value of the data point
3120 date: Date of the data point
3121 time: Time of the data point
3122
3123 Raises:
3124 ValueError: If label is empty, or date/time are wrong types
3125 NotImplementedError: If timeseries functions not available
3126
3127 Example:
3128 >>> from pyhelios.types import Date, Time
3129 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3130 """
3132 if not isinstance(label, str) or not label:
3133 raise ValueError("Label must be a non-empty string")
3134 if not isinstance(date, Date):
3135 raise ValueError(f"date must be a Date instance, got {type(date).__name__}")
3136 if not isinstance(time, Time):
3137 raise ValueError(f"time must be a Time instance, got {type(time).__name__}")
3139 context_wrapper.addTimeseriesData(
3140 self.context, label, float(value),
3141 date.day, date.month, date.year,
3142 time.hour, time.minute, time.second
3143 )
3144
3145 def updateTimeseriesData(self, label: str, date: 'Date', time: 'Time', new_value: float):
3146 """
3147 Update the value of an existing timeseries data point.
3148
3149 Args:
3150 label: Name of the timeseries variable (must already exist)
3151 date: Date of the existing point (must match exactly)
3152 time: Time of the existing point (must match exactly)
3153 new_value: Replacement value
3154
3155 Raises:
3156 ValueError: If label is empty, or date/time are wrong types
3157 HeliosRuntimeError: If the variable does not exist or no point matches the (date, time)
3158 NotImplementedError: If timeseries functions not available
3159
3160 Example:
3161 >>> from pyhelios.types import Date, Time
3162 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3163 >>> context.updateTimeseriesData("temperature", Date(2024, 6, 15), Time(12, 0, 0), 26.5)
3164 """
3166 if not isinstance(label, str) or not label:
3167 raise ValueError("Label must be a non-empty string")
3168 if not isinstance(date, Date):
3169 raise ValueError(f"date must be a Date instance, got {type(date).__name__}")
3170 if not isinstance(time, Time):
3171 raise ValueError(f"time must be a Time instance, got {type(time).__name__}")
3173 context_wrapper.updateTimeseriesData(
3174 self.context, label,
3175 date.day, date.month, date.year,
3176 time.hour, time.minute, time.second,
3177 float(new_value)
3178 )
3179
3180 def setCurrentTimeseriesPoint(self, label: str, index: int):
3181 """
3182 Set the Context date and time from a timeseries data point index.
3183
3184 Args:
3185 label: Name of the timeseries variable
3186 index: Index of the data point (0 = earliest, chronologically ordered)
3187
3188 Raises:
3189 ValueError: If label is empty or index is negative
3190 NotImplementedError: If timeseries functions not available
3191
3192 Example:
3193 >>> context.setCurrentTimeseriesPoint("temperature", 0)
3194 """
3196 if not isinstance(label, str) or not label:
3197 raise ValueError("Label must be a non-empty string")
3198 if not isinstance(index, int) or index < 0:
3199 raise ValueError(f"Index must be a non-negative integer, got {index}")
3200
3201 context_wrapper.setCurrentTimeseriesPoint(self.context, label, index)
3203 def queryTimeseriesData(self, label: str, date: 'Date' = None, time: 'Time' = None,
3204 index: int = None) -> float:
3205 """
3206 Query a timeseries data value.
3207
3208 Three modes of operation:
3209 - With date and time: returns interpolated value at the specified date/time
3210 - With index: returns value at the specified data point index
3211 - With neither: returns value at the current Context date/time
3212
3213 Args:
3214 label: Name of the timeseries variable
3215 date: Date to query at (requires time as well)
3216 time: Time to query at (requires date as well)
3217 index: Index of the data point (0 = earliest)
3218
3219 Returns:
3220 The timeseries value as a float
3221
3222 Raises:
3223 ValueError: If both date/time and index are provided, or if date without time
3224 NotImplementedError: If timeseries functions not available
3225
3226 Example:
3227 >>> # Query at specific date/time
3228 >>> val = context.queryTimeseriesData("temperature", date=Date(2024, 6, 15), time=Time(12, 0, 0))
3229 >>> # Query by index
3230 >>> val = context.queryTimeseriesData("temperature", index=0)
3231 >>> # Query at current context time
3232 >>> val = context.queryTimeseriesData("temperature")
3233 """
3235 if not isinstance(label, str) or not label:
3236 raise ValueError("Label must be a non-empty string")
3237
3238 has_datetime = date is not None or time is not None
3239 has_index = index is not None
3241 if has_datetime and has_index:
3242 raise ValueError("Cannot specify both date/time and index. Use one or the other.")
3243
3244 if has_datetime:
3245 if date is None or time is None:
3246 raise ValueError("Both date and time must be provided together")
3247 if not isinstance(date, Date):
3248 raise ValueError(f"date must be a Date instance, got {type(date).__name__}")
3249 if not isinstance(time, Time):
3250 raise ValueError(f"time must be a Time instance, got {type(time).__name__}")
3251 return context_wrapper.queryTimeseriesDataDateTime(
3252 self.context, label,
3253 date.day, date.month, date.year,
3254 time.hour, time.minute, time.second
3255 )
3256
3257 if has_index:
3258 if not isinstance(index, int) or index < 0:
3259 raise ValueError(f"Index must be a non-negative integer, got {index}")
3260 return context_wrapper.queryTimeseriesDataIndex(self.context, label, index)
3261
3262 return context_wrapper.queryTimeseriesDataCurrent(self.context, label)
3263
3264 def queryTimeseriesTime(self, label: str, index: int) -> 'Time':
3265 """
3266 Get the Time associated with a timeseries data point.
3267
3268 Args:
3269 label: Name of the timeseries variable
3270 index: Index of the data point (0 = earliest)
3271
3272 Returns:
3273 Time object for the data point
3274
3275 Raises:
3276 ValueError: If label is empty or index is negative
3277 NotImplementedError: If timeseries functions not available
3278
3279 Example:
3280 >>> t = context.queryTimeseriesTime("temperature", 0)
3281 >>> print(f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}")
3282 """
3284 if not isinstance(label, str) or not label:
3285 raise ValueError("Label must be a non-empty string")
3286 if not isinstance(index, int) or index < 0:
3287 raise ValueError(f"Index must be a non-negative integer, got {index}")
3288
3289 hour, minute, second = context_wrapper.queryTimeseriesTime(self.context, label, index)
3290 return Time(hour=hour, minute=minute, second=second)
3291
3292 def queryTimeseriesDate(self, label: str, index: int) -> 'Date':
3293 """
3294 Get the Date associated with a timeseries data point.
3295
3296 Args:
3297 label: Name of the timeseries variable
3298 index: Index of the data point (0 = earliest)
3299
3300 Returns:
3301 Date object for the data point
3302
3303 Raises:
3304 ValueError: If label is empty or index is negative
3305 NotImplementedError: If timeseries functions not available
3306
3307 Example:
3308 >>> d = context.queryTimeseriesDate("temperature", 0)
3309 >>> print(f"{d.year}-{d.month:02d}-{d.day:02d}")
3310 """
3312 if not isinstance(label, str) or not label:
3313 raise ValueError("Label must be a non-empty string")
3314 if not isinstance(index, int) or index < 0:
3315 raise ValueError(f"Index must be a non-negative integer, got {index}")
3316
3317 year, month, day = context_wrapper.queryTimeseriesDate(self.context, label, index)
3318 return Date(year=year, month=month, day=day)
3319
3320 def getTimeseriesLength(self, label: str) -> int:
3321 """
3322 Get the number of data points in a timeseries variable.
3323
3324 Args:
3325 label: Name of the timeseries variable
3326
3327 Returns:
3328 Number of data points
3329
3330 Raises:
3331 ValueError: If label is empty
3332 NotImplementedError: If timeseries functions not available
3333
3334 Example:
3335 >>> n = context.getTimeseriesLength("temperature")
3336 >>> print(f"Timeseries has {n} data points")
3337 """
3339 if not isinstance(label, str) or not label:
3340 raise ValueError("Label must be a non-empty string")
3341
3342 return context_wrapper.getTimeseriesLength(self.context, label)
3343
3344 def doesTimeseriesVariableExist(self, label: str) -> bool:
3345 """
3346 Check whether a timeseries variable exists.
3347
3348 Args:
3349 label: Name of the timeseries variable
3350
3351 Returns:
3352 True if the variable exists, False otherwise
3353
3354 Raises:
3355 ValueError: If label is empty
3356 NotImplementedError: If timeseries functions not available
3357
3358 Example:
3359 >>> if context.doesTimeseriesVariableExist("temperature"):
3360 ... print("Temperature data loaded")
3361 """
3363 if not isinstance(label, str) or not label:
3364 raise ValueError("Label must be a non-empty string")
3365
3366 return context_wrapper.doesTimeseriesVariableExist(self.context, label)
3367
3368 def listTimeseriesVariables(self) -> List[str]:
3369 """
3370 List all existing timeseries variables.
3371
3372 Returns:
3373 List of timeseries variable names
3374
3375 Raises:
3376 NotImplementedError: If timeseries functions not available
3377
3378 Example:
3379 >>> variables = context.listTimeseriesVariables()
3380 >>> for var in variables:
3381 ... print(f" {var}: {context.getTimeseriesLength(var)} points")
3382 """
3384
3385 return context_wrapper.listTimeseriesVariables(self.context)
3386
3387 def clearTimeseriesData(self):
3388 """Clear all timeseries data from the Context.
3389
3390 Removes all timeseries variables and their associated date/time values.
3391
3392 Raises:
3393 NotImplementedError: If timeseries functions not available
3394
3395 Example:
3396 >>> context.clearTimeseriesData()
3397 >>> context.listTimeseriesVariables()
3398 []
3399 """
3401 context_wrapper.clearTimeseriesData(self.context)
3402
3403 def deleteTimeseriesVariable(self, label: str):
3404 """Delete a single timeseries variable and all of its data points.
3405
3406 Complements :meth:`clearTimeseriesData` (which removes all variables) and
3407 :meth:`updateTimeseriesData` (which modifies a single point).
3408
3409 Args:
3410 label: Name of the timeseries variable to delete.
3411
3412 Raises:
3413 ValueError: If ``label`` is empty.
3414 NotImplementedError: If running against helios-core older than v1.3.72.
3415
3416 Note:
3417 If the variable does not exist, the underlying Helios API issues a
3418 non-fatal warning to stderr and the call is otherwise a no-op.
3419
3420 Example:
3421 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3422 >>> context.deleteTimeseriesVariable("temperature")
3423 >>> context.doesTimeseriesVariableExist("temperature")
3424 False
3425 """
3427 if not isinstance(label, str) or not label:
3428 raise ValueError("Label must be a non-empty string")
3429 context_wrapper.deleteTimeseriesVariable(self.context, label)
3430
3431 def deleteTimeseriesDataPoint(self, date: 'Date', time: 'Time', label: Optional[str] = None):
3432 """Delete a single timeseries data point at the given date and time.
3434 If ``label`` is provided, only that variable's matching point is removed. If ``label``
3435 is omitted (None), the matching point is removed from every timeseries variable.
3436
3437 Args:
3438 date: Date of the data point to delete.
3439 time: Time of the data point to delete.
3440 label: Optional name of the timeseries variable. None applies to all variables.
3441
3442 Raises:
3443 ValueError: If date/time are wrong types, or label is an empty string.
3444 NotImplementedError: If running against helios-core older than v1.3.73.
3445
3446 Note:
3447 If no matching data point exists, the underlying Helios API issues a non-fatal
3448 warning to stderr and the call is otherwise a no-op. Matching uses the same
3449 (date, time) encoding as :meth:`addTimeseriesData`.
3450
3451 Example:
3452 >>> from pyhelios.types import Date, Time
3453 >>> context.deleteTimeseriesDataPoint(Date(2024, 6, 15), Time(12, 0, 0), "temperature")
3454 """
3456 if not isinstance(date, Date):
3457 raise ValueError(f"date must be a Date instance, got {type(date).__name__}")
3458 if not isinstance(time, Time):
3459 raise ValueError(f"time must be a Time instance, got {type(time).__name__}")
3460 if label is not None and (not isinstance(label, str) or not label):
3461 raise ValueError("label must be a non-empty string or None")
3463 if label is None:
3464 context_wrapper.deleteTimeseriesDataPointAll(
3465 self.context,
3466 date.day, date.month, date.year,
3467 time.hour, time.minute, time.second
3468 )
3469 else:
3470 context_wrapper.deleteTimeseriesDataPoint(
3471 self.context, label,
3472 date.day, date.month, date.year,
3473 time.hour, time.minute, time.second
3474 )
3475
3476 def loadTabularTimeseriesData(self, data_file: str, column_labels: List[str],
3477 delimiter: str = ",", date_string_format: str = "YYYYMMDD",
3478 headerlines: int = 0):
3479 """
3480 Load tabular timeseries data from a text file.
3481
3482 The file should contain columns of data with dates/times and measured values.
3483 Column labels specify how each column should be interpreted. Special labels
3484 include "year", "DOY", "date", "datetime", "hour", "minute", "second", "time".
3485 Other labels become timeseries variable names.
3486
3487 Args:
3488 data_file: Path to the text file containing tabular data
3489 column_labels: List of column label strings specifying what each column contains
3490 delimiter: Column delimiter string (default: ",")
3491 date_string_format: Format of date strings in the file. Supported formats:
3492 "YYYYMMDD", "YYYYMMDDHH", "YYYYMMDDHHMM", "DD/MM/YYYY",
3493 "MM/DD/YYYY", "DDMMYYYY", "YYYY-MM-DD", "DD/MM/YYYY HH:MM",
3494 "MM/DD/YYYY HH:MM", "ISO8601" (default: "YYYYMMDD")
3495 headerlines: Number of header lines to skip (default: 0)
3496
3497 Raises:
3498 ValueError: If data_file is empty, column_labels is empty, or delimiter is empty
3499 RuntimeError: If the file cannot be read or parsed
3500 NotImplementedError: If timeseries functions not available
3501
3502 Example:
3503 >>> context.loadTabularTimeseriesData(
3504 ... "weather_data.csv",
3505 ... column_labels=["date", "hour", "temperature", "humidity"],
3506 ... delimiter=",",
3507 ... headerlines=1
3508 ... )
3509 >>> temp = context.queryTimeseriesData("temperature", index=0)
3510 """
3512 if not isinstance(data_file, str) or not data_file:
3513 raise ValueError("data_file must be a non-empty string")
3514 if not isinstance(column_labels, list) or not column_labels:
3515 raise ValueError("column_labels must be a non-empty list of strings")
3516 for i, label in enumerate(column_labels):
3517 if not isinstance(label, str):
3518 raise ValueError(f"column_labels[{i}] must be a string, got {type(label).__name__}")
3519 if not isinstance(delimiter, str) or not delimiter:
3520 raise ValueError("delimiter must be a non-empty string")
3521
3522 context_wrapper.loadTabularTimeseriesData(
3523 self.context, data_file, column_labels, delimiter,
3524 date_string_format, headerlines
3525 )
3526
3527 # ==========================================================================
3528 # Primitive and Object Deletion Methods
3529 # ==========================================================================
3530
3531 def deletePrimitive(self, uuids_or_uuid: Union[int, List[int]]) -> None:
3532 """
3533 Delete one or more primitives from the context.
3534
3535 This removes the primitive(s) entirely from the context. If a primitive
3536 belongs to a compound object, it will be removed from that object. If the
3537 object becomes empty after removal, it is automatically deleted.
3538
3539 Args:
3540 uuids_or_uuid: Single UUID (int) or list of UUIDs to delete
3541
3542 Raises:
3543 RuntimeError: If any UUID doesn't exist in the context
3544 ValueError: If UUID is invalid (negative)
3545 NotImplementedError: If delete functions not available in current library build
3546
3547 Example:
3548 >>> context = Context()
3549 >>> patch_id = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
3550 >>> context.deletePrimitive(patch_id) # Single deletion
3551 >>>
3552 >>> # Multiple deletion
3553 >>> ids = [context.addPatch() for _ in range(5)]
3554 >>> context.deletePrimitive(ids) # Delete all at once
3555 """
3557
3558 if isinstance(uuids_or_uuid, (list, tuple)):
3559 for uuid in uuids_or_uuid:
3560 if uuid < 0:
3561 raise ValueError(f"UUID must be non-negative, got {uuid}")
3562 context_wrapper.deletePrimitives(self.context, list(uuids_or_uuid))
3563 else:
3564 if uuids_or_uuid < 0:
3565 raise ValueError(f"UUID must be non-negative, got {uuids_or_uuid}")
3566 context_wrapper.deletePrimitive(self.context, uuids_or_uuid)
3567
3568 def deleteObject(self, objIDs_or_objID: Union[int, List[int]]) -> None:
3569 """
3570 Delete one or more compound objects from the context.
3571
3572 This removes the compound object(s) AND all their child primitives.
3573 Use this when you want to delete an entire object hierarchy at once.
3574
3575 Args:
3576 objIDs_or_objID: Single object ID (int) or list of object IDs to delete
3577
3578 Raises:
3579 RuntimeError: If any object ID doesn't exist in the context
3580 ValueError: If object ID is invalid (negative)
3581 NotImplementedError: If delete functions not available in current library build
3582
3583 Example:
3584 >>> context = Context()
3585 >>> # Create a compound object (e.g., a tile with multiple patches)
3586 >>> patch_ids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2),
3587 ... tile_divisions=int2(2, 2))
3588 >>> obj_id = context.getPrimitiveParentObjectID(patch_ids[0])
3589 >>> context.deleteObject(obj_id) # Deletes tile and all its patches
3590 """
3592
3593 if isinstance(objIDs_or_objID, (list, tuple)):
3594 for objID in objIDs_or_objID:
3595 if objID < 0:
3596 raise ValueError(f"Object ID must be non-negative, got {objID}")
3597 context_wrapper.deleteObjects(self.context, list(objIDs_or_objID))
3598 else:
3599 if objIDs_or_objID < 0:
3600 raise ValueError(f"Object ID must be non-negative, got {objIDs_or_objID}")
3601 context_wrapper.deleteObject(self.context, objIDs_or_objID)
3602
3603 # Plugin-related methods
3604 def get_available_plugins(self) -> List[str]:
3605 """
3606 Get list of available plugins for this PyHelios instance.
3607
3608 Returns:
3609 List of available plugin names
3610 """
3612
3613 def is_plugin_available(self, plugin_name: str) -> bool:
3614 """
3615 Check if a specific plugin is available.
3616
3617 Args:
3618 plugin_name: Name of the plugin to check
3619
3620 Returns:
3621 True if plugin is available, False otherwise
3622 """
3623 return self._plugin_registry.is_plugin_available(plugin_name)
3624
3625 def get_plugin_capabilities(self) -> dict:
3626 """
3627 Get detailed information about available plugin capabilities.
3628
3629 Returns:
3630 Dictionary mapping plugin names to capability information
3631 """
3633
3634 def print_plugin_status(self):
3635 """Print detailed plugin status information."""
3636 self._plugin_registry.print_status()
3637
3638 def get_missing_plugins(self, requested_plugins: List[str]) -> List[str]:
3639 """
3640 Get list of requested plugins that are not available.
3641
3642 Args:
3643 requested_plugins: List of plugin names to check
3644
3645 Returns:
3646 List of missing plugin names
3647 """
3648 return self._plugin_registry.get_missing_plugins(requested_plugins)
3649
3650 # =========================================================================
3651 # Materials System (v1.3.58+)
3652 # =========================================================================
3653
3654 def addMaterial(self, material_label: str):
3655 """
3656 Create a new material for sharing visual properties across primitives.
3657
3658 Materials enable efficient memory usage by allowing multiple primitives to
3659 share rendering properties. Changes to a material affect all primitives using it.
3660
3661 Args:
3662 material_label: Unique label for the material
3663
3664 Raises:
3665 RuntimeError: If material label already exists
3666
3667 Example:
3668 >>> context.addMaterial("wood_oak")
3669 >>> context.setMaterialColor("wood_oak", (0.6, 0.4, 0.2, 1.0))
3670 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3671 """
3672 context_wrapper.addMaterial(self.context, material_label)
3673
3674 def doesMaterialExist(self, material_label: str) -> bool:
3675 """Check if a material with the given label exists."""
3676 return context_wrapper.doesMaterialExist(self.context, material_label)
3677
3678 def listMaterials(self) -> List[str]:
3679 """Get list of all material labels in the context."""
3680 return context_wrapper.listMaterials(self.context)
3681
3682 def deleteMaterial(self, material_label: str):
3683 """
3684 Delete a material from the context.
3685
3686 Primitives using this material will be reassigned to the default material.
3688 Args:
3689 material_label: Label of the material to delete
3690
3691 Raises:
3692 RuntimeError: If material doesn't exist
3693 """
3694 context_wrapper.deleteMaterial(self.context, material_label)
3695
3696 def getMaterialColor(self, material_label: str):
3697 """
3698 Get the RGBA color of a material.
3699
3700 Args:
3701 material_label: Label of the material
3702
3703 Returns:
3704 RGBAcolor object
3705
3706 Raises:
3707 RuntimeError: If material doesn't exist
3708 """
3709 from .wrappers.DataTypes import RGBAcolor
3710 color_list = context_wrapper.getMaterialColor(self.context, material_label)
3711 return RGBAcolor(color_list[0], color_list[1], color_list[2], color_list[3])
3712
3713 def setMaterialColor(self, material_label: str, color):
3714 """
3715 Set the RGBA color of a material.
3717 This affects all primitives that reference this material.
3718
3719 Args:
3720 material_label: Label of the material
3721 color: RGBAcolor object or tuple/list of (r, g, b, a) values
3722
3723 Raises:
3724 RuntimeError: If material doesn't exist
3725
3726 Example:
3727 >>> from pyhelios.types import RGBAcolor
3728 >>> context.setMaterialColor("wood", RGBAcolor(0.6, 0.4, 0.2, 1.0))
3729 >>> context.setMaterialColor("wood", (0.6, 0.4, 0.2, 1.0))
3730 """
3731 if isinstance(color, RGBAcolor):
3732 r, g, b, a = color.r, color.g, color.b, color.a
3733 elif isinstance(color, (list, tuple)) and len(color) == 4:
3734 r, g, b, a = color[0], color[1], color[2], color[3]
3735 else:
3736 raise ValueError(f"Color must be an RGBAcolor or a 4-element list/tuple, got {type(color).__name__}")
3737 context_wrapper.setMaterialColor(self.context, material_label, r, g, b, a)
3739 def getMaterialTexture(self, material_label: str) -> str:
3740 """
3741 Get the texture file path for a material.
3742
3743 Args:
3744 material_label: Label of the material
3745
3746 Returns:
3747 Texture file path, or empty string if no texture
3748
3749 Raises:
3750 RuntimeError: If material doesn't exist
3751 """
3752 return context_wrapper.getMaterialTexture(self.context, material_label)
3753
3754 def setMaterialTexture(self, material_label: str, texture_file: str):
3755 """
3756 Set the texture file for a material.
3757
3758 This affects all primitives that reference this material.
3760 Args:
3761 material_label: Label of the material
3762 texture_file: Path to texture image file
3763
3764 Raises:
3765 RuntimeError: If material doesn't exist or texture file not found
3766 """
3767 context_wrapper.setMaterialTexture(self.context, material_label, texture_file)
3768
3769 def isMaterialTextureColorOverridden(self, material_label: str) -> bool:
3770 """Check if material texture color is overridden by material color."""
3771 return context_wrapper.isMaterialTextureColorOverridden(self.context, material_label)
3772
3773 def setMaterialTextureColorOverride(self, material_label: str, override: bool):
3774 """Set whether material color overrides texture color."""
3775 context_wrapper.setMaterialTextureColorOverride(self.context, material_label, override)
3776
3777 def getMaterialTwosidedFlag(self, material_label: str) -> int:
3778 """Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3779 return context_wrapper.getMaterialTwosidedFlag(self.context, material_label)
3780
3781 def setMaterialTwosidedFlag(self, material_label: str, twosided_flag: int):
3782 """Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3783 context_wrapper.setMaterialTwosidedFlag(self.context, material_label, twosided_flag)
3784
3785 def assignMaterialToPrimitive(self, uuid, material_label: str):
3786 """
3787 Assign a material to primitive(s).
3788
3789 Args:
3790 uuid: Single UUID (int) or list of UUIDs (List[int])
3791 material_label: Label of the material to assign
3792
3793 Raises:
3794 RuntimeError: If primitive or material doesn't exist
3795
3796 Example:
3797 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3798 >>> context.assignMaterialToPrimitive([uuid1, uuid2, uuid3], "wood_oak")
3799 """
3800 if isinstance(uuid, (list, tuple)):
3801 context_wrapper.assignMaterialToPrimitives(self.context, uuid, material_label)
3802 else:
3803 context_wrapper.assignMaterialToPrimitive(self.context, uuid, material_label)
3804
3805 def assignMaterialToObject(self, objID, material_label: str):
3806 """
3807 Assign a material to all primitives in compound object(s).
3808
3809 Args:
3810 objID: Single object ID (int) or list of object IDs (List[int])
3811 material_label: Label of the material to assign
3812
3813 Raises:
3814 RuntimeError: If object or material doesn't exist
3815
3816 Example:
3817 >>> tree_id = wpt.buildTree(WPTType.LEMON)
3818 >>> context.assignMaterialToObject(tree_id, "tree_bark")
3819 >>> context.assignMaterialToObject([id1, id2], "grass")
3820 """
3821 if isinstance(objID, (list, tuple)):
3822 context_wrapper.assignMaterialToObjects(self.context, objID, material_label)
3823 else:
3824 context_wrapper.assignMaterialToObject(self.context, objID, material_label)
3825
3826 def getPrimitiveMaterialLabel(self, uuid):
3827 """Get the material label assigned to a primitive or multiple primitives.
3829 Args:
3830 uuid: Single UUID (int) or list of UUIDs
3831
3832 Returns:
3833 str for single UUID, or List[str] for list
3834
3835 Raises:
3836 RuntimeError: If primitive doesn't exist
3837 """
3838 if isinstance(uuid, (list, tuple)):
3840 if not uuid:
3841 return []
3842 ptr, offsets, total = context_wrapper.getBatchPrimitiveMaterialLabels(self.context, uuid)
3843 if total == 0 or not ptr:
3844 return ["" for _ in uuid]
3845 full_str = ptr.decode('utf-8') if isinstance(ptr, bytes) else ptr
3846 return [full_str[offsets[i]:offsets[i+1]] for i in range(len(uuid))]
3847 return context_wrapper.getPrimitiveMaterialLabel(self.context, uuid)
3848
3849 def getPrimitiveTwosidedFlag(self, uuid: int, default_value: int = 1) -> int:
3850 """
3851 Get two-sided rendering flag for a primitive.
3852
3853 Checks material first, then primitive data if no material assigned.
3854
3855 Args:
3856 uuid: UUID of the primitive
3857 default_value: Default value if no material/data (default 1 = two-sided)
3858
3859 Returns:
3860 Two-sided flag (0 = one-sided, 1 = two-sided)
3861 """
3862 return context_wrapper.getPrimitiveTwosidedFlag(self.context, uuid, default_value)
3863
3864 def getPrimitivesUsingMaterial(self, material_label: str) -> List[int]:
3865 """
3866 Get all primitive UUIDs that use a specific material.
3867
3868 Args:
3869 material_label: Label of the material
3870
3871 Returns:
3872 List of primitive UUIDs using the material
3873
3874 Raises:
3875 RuntimeError: If material doesn't exist
3876 """
3877 return context_wrapper.getPrimitivesUsingMaterial(self.context, material_label)
3878
3879 # =========================================================================
3880 # Texture Methods
3881 # =========================================================================
3882
3883 def getPrimitiveTextureFile(self, uuid):
3884 """Get the texture file path of a primitive or multiple primitives.
3885
3886 Args:
3887 uuid: Single UUID (int) or list of UUIDs
3888
3889 Returns:
3890 str for single UUID, or List[str] for list
3891 """
3893 if isinstance(uuid, (list, tuple)):
3894 if not uuid:
3895 return []
3896 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureFiles(self.context, uuid)
3897 if total == 0 or not ptr:
3898 return ["" for _ in uuid]
3899 full_str = ptr.decode('utf-8') if isinstance(ptr, bytes) else ptr
3900 return [full_str[offsets[i]:offsets[i+1]] for i in range(len(uuid))]
3901 return context_wrapper.getPrimitiveTextureFile(self.context, uuid)
3902
3903 def resolveMaterialTextures(self, uuids, colors_np):
3904 """Resolve material texture suppression for export.
3905
3906 For each primitive, applies material-based texture suppression rules:
3907 1. If primitive has texture but material has no texture -> suppress texture, use material color
3908 2. If both have texture and textureColorOverride -> prefix "mask:", use material color
3909 3. Otherwise -> leave unchanged
3910
3911 Args:
3912 uuids: List of primitive UUIDs
3913 colors_np: numpy float32 array of shape (N, 3), modified IN-PLACE
3914
3915 Returns:
3916 List[str] of resolved texture file paths
3917 """
3919 if not uuids:
3920 return []
3921 return context_wrapper.resolveMaterialTextures(self.context, uuids, colors_np)
3922
3923 def packGPUBuffers(self, uuids):
3924 """Pack GPU-ready geometry buffers for a set of primitives in a single C++ pass.
3926 Produces a binary blob containing contiguous typed arrays (positions,
3927 colors, uvs, indices, faceToUuid) grouped by texture, ready for
3928 zero-copy loading into Three.js BufferGeometry attributes.
3929
3930 Args:
3931 uuids: List of primitive UUIDs
3932
3933 Returns:
3934 bytes: Raw binary blob (see wire format v2 spec)
3935 """
3937 if not uuids:
3938 return b''
3939 return context_wrapper.packGPUBuffers(self.context, uuids)
3940
3941 def setPrimitiveTextureFile(self, uuid: int, texture_file: str) -> None:
3942 """Set the texture file path of a primitive.
3944 Args:
3945 uuid: UUID of the primitive
3946 texture_file: Path to the texture file
3947 """
3949 context_wrapper.setPrimitiveTextureFile(self.context, uuid, texture_file)
3950
3951 def getPrimitiveTextureSize(self, uuid: int) -> int2:
3952 """Get the texture size (width, height) of a primitive.
3953
3954 Args:
3955 uuid: UUID of the primitive
3956
3957 Returns:
3958 int2 with width and height of the texture
3959 """
3961 w, h = context_wrapper.getPrimitiveTextureSize(self.context, uuid)
3962 return int2(w, h)
3963
3964 def getPrimitiveTextureUV(self, uuid):
3965 """Get the texture UV coordinates of a primitive or multiple primitives.
3966
3967 Args:
3968 uuid: Single UUID (int) or list of UUIDs
3969
3970 Returns:
3971 List[vec2] for single UUID, or tuple of (flat_data, offsets) for list
3972 """
3974 if isinstance(uuid, (list, tuple)):
3975 if not uuid:
3976 return (np.empty((0,), dtype=np.float32), np.zeros((1,), dtype=np.uint32))
3977 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureUV(self.context, uuid)
3978 offsets_arr = np.array(offsets, dtype=np.uint32)
3979 if total == 0 or not ptr:
3980 return (np.empty((0,), dtype=np.float32), offsets_arr)
3981 data = np.ctypeslib.as_array(ptr, shape=(total,)).copy()
3982 return (data, offsets_arr)
3983 uv_pairs = context_wrapper.getPrimitiveTextureUV(self.context, uuid)
3984 return [vec2(u, v) for u, v in uv_pairs]
3985
3986 def primitiveTextureHasTransparencyChannel(self, uuid: int) -> bool:
3987 """Check if primitive texture has a transparency channel.
3988
3989 Args:
3990 uuid: UUID of the primitive
3991
3992 Returns:
3993 True if texture has transparency channel
3994 """
3996 return context_wrapper.primitiveTextureHasTransparencyChannel(self.context, uuid)
3997
3998 def getPrimitiveSolidFraction(self, uuid):
3999 """Get the solid fraction of a primitive or multiple primitives.
4000
4001 Args:
4002 uuid: Single UUID (int) or list of UUIDs
4003
4004 Returns:
4005 float for single UUID, or np.ndarray of shape (N,) for list
4006 """
4008 if isinstance(uuid, (list, tuple)):
4009 if not uuid:
4010 return np.empty((0,), dtype=np.float32)
4011 ptr, size = context_wrapper.getBatchPrimitiveSolidFractions(self.context, uuid)
4012 if size == 0 or not ptr:
4013 return np.empty((0,), dtype=np.float32)
4014 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
4015 return context_wrapper.getPrimitiveSolidFraction(self.context, uuid)
4016
4017 def overridePrimitiveTextureColor(self, uuids_or_uuid) -> None:
4018 """Override texture color with the primitive's constant RGB color.
4019
4020 Args:
4021 uuids_or_uuid: A single UUID (int) or a list of UUIDs. When a list is
4022 given, the override is applied to all of them in a single bulk call.
4023 """
4025 if isinstance(uuids_or_uuid, (list, tuple)):
4026 context_wrapper.overridePrimitiveTextureColorBatchWrapper(self.context, list(uuids_or_uuid))
4027 else:
4028 context_wrapper.overridePrimitiveTextureColor(self.context, uuids_or_uuid)
4029
4030 def usePrimitiveTextureColor(self, uuids_or_uuid) -> None:
4031 """Use texture-map color instead of the constant RGB color.
4032
4033 Args:
4034 uuids_or_uuid: A single UUID (int) or a list of UUIDs. When a list is
4035 given, all of them are restored in a single bulk call.
4036 """
4038 if isinstance(uuids_or_uuid, (list, tuple)):
4039 context_wrapper.usePrimitiveTextureColorBatchWrapper(self.context, list(uuids_or_uuid))
4040 else:
4041 context_wrapper.usePrimitiveTextureColor(self.context, uuids_or_uuid)
4042
4043 def isPrimitiveTextureColorOverridden(self, uuid: int) -> bool:
4044 """Check if primitive texture color is overridden.
4045
4046 Args:
4047 uuid: UUID of the primitive
4048
4049 Returns:
4050 True if texture color is overridden with constant RGB
4051 """
4053 return context_wrapper.isPrimitiveTextureColorOverridden(self.context, uuid)
4054
4055 # =========================================================================
4056 # Convenience Methods (getAll*)
4057 # =========================================================================
4058
4059 def getAllPrimitiveNormals(self) -> 'np.ndarray':
4060 """Get normals for all primitives. Returns ndarray of shape (N, 3)."""
4061 return self.getPrimitiveNormal(self.getAllUUIDs())
4062
4063 def getAllPrimitiveColors(self) -> 'np.ndarray':
4064 """Get colors for all primitives. Returns ndarray of shape (N, 3)."""
4065 return self.getPrimitiveColor(self.getAllUUIDs())
4066
4067 def getAllPrimitiveAreas(self) -> 'np.ndarray':
4068 """Get areas for all primitives. Returns ndarray of shape (N,)."""
4069 return self.getPrimitiveArea(self.getAllUUIDs())
4070
4071 def getAllPrimitiveTypes(self) -> 'np.ndarray':
4072 """Get types for all primitives. Returns ndarray of shape (N,) uint32."""
4073 return self.getPrimitiveType(self.getAllUUIDs())
4074
4075 def getAllPrimitiveSolidFractions(self) -> 'np.ndarray':
4076 """Get solid fractions for all primitives. Returns ndarray of shape (N,)."""
4077 return self.getPrimitiveSolidFraction(self.getAllUUIDs())
4078
4079 def getAllPrimitiveVertices(self):
4080 """Get vertices for all primitives. Returns (flat_data, offsets) tuple."""
4081 return self.getPrimitiveVertices(self.getAllUUIDs())
4082
4083 def getAllPrimitiveTextureFiles(self) -> List[str]:
4084 """Get texture files for all primitives. Returns list of strings."""
4085 return self.getPrimitiveTextureFile(self.getAllUUIDs())
4086
4087 def getAllPrimitiveMaterialLabels(self) -> List[str]:
4088 """Get material labels for all primitives. Returns list of strings."""
4089 return self.getPrimitiveMaterialLabel(self.getAllUUIDs())
4090
4091 # ==================== Visibility Methods ====================
4093 def hidePrimitive(self, uuids_or_uuid) -> None:
4094 """Hide one or more primitives. Hidden primitives are excluded from getAllUUIDs().
4095
4096 Args:
4097 uuids_or_uuid: Single UUID (int) or list of UUIDs to hide.
4098 """
4099 if isinstance(uuids_or_uuid, (list, tuple)):
4100 context_wrapper.hidePrimitivesWrapper(self.context, list(uuids_or_uuid))
4101 else:
4102 context_wrapper.hidePrimitiveWrapper(self.context, uuids_or_uuid)
4103
4104 def showPrimitive(self, uuids_or_uuid) -> None:
4105 """Show one or more previously hidden primitives.
4107 Args:
4108 uuids_or_uuid: Single UUID (int) or list of UUIDs to show.
4109 """
4110 if isinstance(uuids_or_uuid, (list, tuple)):
4111 context_wrapper.showPrimitivesWrapper(self.context, list(uuids_or_uuid))
4112 else:
4113 context_wrapper.showPrimitiveWrapper(self.context, uuids_or_uuid)
4114
4115 def isPrimitiveHidden(self, uuid: int) -> bool:
4116 """Check if a primitive is hidden.
4118 Args:
4119 uuid: UUID of the primitive.
4120
4121 Returns:
4122 True if the primitive is hidden.
4123 """
4124 return context_wrapper.isPrimitiveHiddenWrapper(self.context, uuid)
4125
4126 def hideObject(self, objids_or_objid) -> None:
4127 """Hide one or more compound objects (and all their primitives).
4128
4129 Args:
4130 objids_or_objid: Single object ID (int) or list of object IDs to hide.
4131 """
4132 if isinstance(objids_or_objid, (list, tuple)):
4133 context_wrapper.hideObjectsWrapper(self.context, list(objids_or_objid))
4134 else:
4135 context_wrapper.hideObjectWrapper(self.context, objids_or_objid)
4136
4137 def showObject(self, objids_or_objid) -> None:
4138 """Show one or more previously hidden compound objects.
4140 Args:
4141 objids_or_objid: Single object ID (int) or list of object IDs to show.
4142 """
4143 if isinstance(objids_or_objid, (list, tuple)):
4144 context_wrapper.showObjectsWrapper(self.context, list(objids_or_objid))
4145 else:
4146 context_wrapper.showObjectWrapper(self.context, objids_or_objid)
4147
4148 def isObjectHidden(self, objID: int) -> bool:
4149 """Check if a compound object is hidden.
4151 Args:
4152 objID: Object ID.
4153
4154 Returns:
4155 True if the object is hidden.
4156 """
4157 return context_wrapper.isObjectHiddenWrapper(self.context, objID)
4158
4159 # ==================== Object Data Methods ====================
4160
4161 def setObjectDataInt(self, objids_or_objid, label: str, value: int) -> None:
4162 """Set object data as signed 32-bit integer. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4163 if isinstance(objids_or_objid, (list, tuple)):
4164 if isinstance(value, (list, tuple, np.ndarray)):
4165 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Int', value)
4166 else:
4167 context_wrapper.setBroadcastObjectDataInt(self.context, objids_or_objid, label, value)
4168 else:
4169 context_wrapper.setObjectDataInt(self.context, objids_or_objid, label, value)
4171 def setObjectDataUInt(self, objids_or_objid, label: str, value: int) -> None:
4172 """Set object data as unsigned 32-bit integer. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4173 if isinstance(objids_or_objid, (list, tuple)):
4174 if isinstance(value, (list, tuple, np.ndarray)):
4175 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'UInt', value)
4176 else:
4177 context_wrapper.setBroadcastObjectDataUInt(self.context, objids_or_objid, label, value)
4178 else:
4179 context_wrapper.setObjectDataUInt(self.context, objids_or_objid, label, value)
4181 def setObjectDataFloat(self, objids_or_objid, label: str, value: float) -> None:
4182 """Set object data as 32-bit float. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4183 if isinstance(objids_or_objid, (list, tuple)):
4184 if isinstance(value, (list, tuple, np.ndarray)):
4185 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Float', value)
4186 else:
4187 context_wrapper.setBroadcastObjectDataFloat(self.context, objids_or_objid, label, value)
4188 else:
4189 context_wrapper.setObjectDataFloat(self.context, objids_or_objid, label, value)
4191 def setObjectDataDouble(self, objids_or_objid, label: str, value: float) -> None:
4192 """Set object data as 64-bit double. Scalar broadcasts to all objIDs; a list of values sets a distinct value per objID."""
4193 if isinstance(objids_or_objid, (list, tuple)):
4194 if isinstance(value, (list, tuple, np.ndarray)):
4195 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Double', value)
4196 else:
4197 context_wrapper.setBroadcastObjectDataDouble(self.context, objids_or_objid, label, value)
4198 else:
4199 context_wrapper.setObjectDataDouble(self.context, objids_or_objid, label, value)
4201 def setObjectDataString(self, objids_or_objid, label: str, value: str) -> None:
4202 """Set object data as string. Scalar broadcasts to all objIDs; a list of strings sets a distinct value per objID."""
4203 if isinstance(objids_or_objid, (list, tuple)):
4204 if isinstance(value, (list, tuple, np.ndarray)):
4205 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'String', value)
4206 else:
4207 context_wrapper.setBroadcastObjectDataString(self.context, objids_or_objid, label, value)
4208 else:
4209 context_wrapper.setObjectDataString(self.context, objids_or_objid, label, value)
4211 def setObjectDataVec2(self, objids_or_objid, label: str, x_or_vec, y: float = None) -> None:
4212 """Set object data as vec2. Accepts a vec2 / x,y components, or a list of vec2 (one per objID)."""
4213 if isinstance(objids_or_objid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4214 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Vec2', x_or_vec)
4215 return
4216 if hasattr(x_or_vec, 'x') and y is None:
4217 x, y = x_or_vec.x, x_or_vec.y
4218 else:
4219 x = x_or_vec
4220 if isinstance(objids_or_objid, (list, tuple)):
4221 context_wrapper.setBroadcastObjectDataVec2(self.context, objids_or_objid, label, x, y)
4222 else:
4223 context_wrapper.setObjectDataVec2(self.context, objids_or_objid, label, x, y)
4224
4225 def setObjectDataVec3(self, objids_or_objid, label: str, x_or_vec, y: float = None, z: float = None) -> None:
4226 """Set object data as vec3. Accepts a vec3 / x,y,z components, or a list of vec3 (one per objID)."""
4227 if isinstance(objids_or_objid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4228 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Vec3', x_or_vec)
4229 return
4230 if hasattr(x_or_vec, 'x') and y is None:
4231 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4232 else:
4233 x = x_or_vec
4234 if isinstance(objids_or_objid, (list, tuple)):
4235 context_wrapper.setBroadcastObjectDataVec3(self.context, objids_or_objid, label, x, y, z)
4236 else:
4237 context_wrapper.setObjectDataVec3(self.context, objids_or_objid, label, x, y, z)
4238
4239 def setObjectDataVec4(self, objids_or_objid, label: str, x_or_vec, y: float = None, z: float = None, w: float = None) -> None:
4240 """Set object data as vec4. Accepts a vec4 / x,y,z,w components, or a list of vec4 (one per objID)."""
4241 if isinstance(objids_or_objid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4242 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Vec4', x_or_vec)
4243 return
4244 if hasattr(x_or_vec, 'x') and y is None:
4245 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4246 else:
4247 x = x_or_vec
4248 if isinstance(objids_or_objid, (list, tuple)):
4249 context_wrapper.setBroadcastObjectDataVec4(self.context, objids_or_objid, label, x, y, z, w)
4250 else:
4251 context_wrapper.setObjectDataVec4(self.context, objids_or_objid, label, x, y, z, w)
4252
4253 def setObjectDataInt2(self, objids_or_objid, label: str, x_or_vec, y: int = None) -> None:
4254 """Set object data as int2. Accepts an int2 / x,y components, or a list of int2 (one per objID)."""
4255 if isinstance(objids_or_objid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4256 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Int2', x_or_vec)
4257 return
4258 if hasattr(x_or_vec, 'x') and y is None:
4259 x, y = x_or_vec.x, x_or_vec.y
4260 else:
4261 x = x_or_vec
4262 if isinstance(objids_or_objid, (list, tuple)):
4263 context_wrapper.setBroadcastObjectDataInt2(self.context, objids_or_objid, label, x, y)
4264 else:
4265 context_wrapper.setObjectDataInt2(self.context, objids_or_objid, label, x, y)
4266
4267 def setObjectDataInt3(self, objids_or_objid, label: str, x_or_vec, y: int = None, z: int = None) -> None:
4268 """Set object data as int3. Accepts an int3 / x,y,z components, or a list of int3 (one per objID)."""
4269 if isinstance(objids_or_objid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4270 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Int3', x_or_vec)
4271 return
4272 if hasattr(x_or_vec, 'x') and y is None:
4273 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4274 else:
4275 x = x_or_vec
4276 if isinstance(objids_or_objid, (list, tuple)):
4277 context_wrapper.setBroadcastObjectDataInt3(self.context, objids_or_objid, label, x, y, z)
4278 else:
4279 context_wrapper.setObjectDataInt3(self.context, objids_or_objid, label, x, y, z)
4280
4281 def setObjectDataInt4(self, objids_or_objid, label: str, x_or_vec, y: int = None, z: int = None, w: int = None) -> None:
4282 """Set object data as int4. Accepts an int4 / x,y,z,w components, or a list of int4 (one per objID)."""
4283 if isinstance(objids_or_objid, (list, tuple)) and isinstance(x_or_vec, (list, tuple, np.ndarray)):
4284 context_wrapper.setObjectDataArray(self.context, objids_or_objid, label, 'Int4', x_or_vec)
4285 return
4286 if hasattr(x_or_vec, 'x') and y is None:
4287 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4288 else:
4289 x = x_or_vec
4290 if isinstance(objids_or_objid, (list, tuple)):
4291 context_wrapper.setBroadcastObjectDataInt4(self.context, objids_or_objid, label, x, y, z, w)
4292 else:
4293 context_wrapper.setObjectDataInt4(self.context, objids_or_objid, label, x, y, z, w)
4294
4295 def getObjectData(self, objID: int, label: str, data_type: type = None):
4296 """Get object data with optional type specification. Auto-detects type if not specified."""
4297 if data_type is None:
4298 return context_wrapper.getObjectDataAuto(self.context, objID, label)
4299 if data_type == int:
4300 return context_wrapper.getObjectDataInt(self.context, objID, label)
4301 elif data_type == float:
4302 return context_wrapper.getObjectDataFloat(self.context, objID, label)
4303 elif data_type == str:
4304 return context_wrapper.getObjectDataString(self.context, objID, label)
4305 elif data_type == vec3:
4306 coords = context_wrapper.getObjectDataVec3(self.context, objID, label)
4307 return vec3(coords[0], coords[1], coords[2])
4308 elif data_type == vec2:
4309 coords = context_wrapper.getObjectDataVec2(self.context, objID, label)
4310 return vec2(coords[0], coords[1])
4311 elif data_type == vec4:
4312 coords = context_wrapper.getObjectDataVec4(self.context, objID, label)
4313 return vec4(coords[0], coords[1], coords[2], coords[3])
4314 elif data_type == int2:
4315 coords = context_wrapper.getObjectDataInt2(self.context, objID, label)
4316 return int2(coords[0], coords[1])
4317 elif data_type == int3:
4318 coords = context_wrapper.getObjectDataInt3(self.context, objID, label)
4319 return int3(coords[0], coords[1], coords[2])
4320 elif data_type == int4:
4321 coords = context_wrapper.getObjectDataInt4(self.context, objID, label)
4322 return int4(coords[0], coords[1], coords[2], coords[3])
4323 elif data_type == "uint":
4324 return context_wrapper.getObjectDataUInt(self.context, objID, label)
4325 elif data_type == "double":
4326 return context_wrapper.getObjectDataDouble(self.context, objID, label)
4327 else:
4328 raise ValueError(f"Unsupported object data type: {data_type}")
4329
4330 def getObjectDataFloat(self, objID: int, label: str) -> float:
4331 """Get float object data."""
4332 return context_wrapper.getObjectDataFloat(self.context, objID, label)
4333
4334 def getObjectDataInt(self, objID: int, label: str) -> int:
4335 """Get int object data."""
4336 return context_wrapper.getObjectDataInt(self.context, objID, label)
4337
4338 def getObjectDataString(self, objID: int, label: str) -> str:
4339 """Get string object data."""
4340 return context_wrapper.getObjectDataString(self.context, objID, label)
4341
4342 def getObjectDataType(self, objID: int, label: str) -> int:
4343 """Get the HeliosDataType enum for object data."""
4344 return context_wrapper.getObjectDataTypeWrapper(self.context, objID, label)
4345
4346 def getObjectDataSize(self, objID: int, label: str) -> int:
4347 """Get the size of object data array."""
4348 return context_wrapper.getObjectDataSizeWrapper(self.context, objID, label)
4349
4350 def doesObjectDataExist(self, objID: int, label: str) -> bool:
4351 """Check if object data exists."""
4352 return context_wrapper.doesObjectDataExistWrapper(self.context, objID, label)
4353
4354 def clearObjectData(self, objids_or_objid, label: str) -> None:
4355 """Clear object data. Accepts single ID or list."""
4356 if isinstance(objids_or_objid, (list, tuple)):
4357 context_wrapper.clearObjectDataBatchWrapper(self.context, objids_or_objid, label)
4358 else:
4359 context_wrapper.clearObjectDataWrapper(self.context, objids_or_objid, label)
4360
4361 def clearAllObjectData(self, label: str) -> None:
4362 """Remove a named data field from every compound object in the Context.
4364 Clears the data with the given label from all objects (including hidden ones) and
4365 releases the registered data type for the label, so it may subsequently be
4366 re-registered with a different type. Requires helios-core v1.3.73 or newer.
4367 """
4369 context_wrapper.clearAllObjectDataByLabelWrapper(self.context, label)
4370
4371 def listObjectData(self, objID: int) -> List[str]:
4372 """List all data labels on a specific object."""
4373 return context_wrapper.listObjectDataWrapper(self.context, objID)
4374
4375 def listAllObjectDataLabels(self) -> List[str]:
4376 """List all object data labels in context."""
4377 return context_wrapper.listAllObjectDataLabelsWrapper(self.context)
4378
4379 def duplicateObjectData(self, objID: int, old_label: str, new_label: str) -> None:
4380 """Copy object data to a new label."""
4381 context_wrapper.duplicateObjectDataWrapper(self.context, objID, old_label, new_label)
4382
4383 def renameObjectData(self, objID: int, old_label: str, new_label: str) -> None:
4384 """Rename an object data label."""
4385 context_wrapper.renameObjectDataWrapper(self.context, objID, old_label, new_label)
4386
4387 def filterObjectsByData(self, objIDs: List[int], label: str, value, comparator: str = "=") -> List[int]:
4388 """Filter objects by data value. Auto-dispatches based on value type."""
4389 if isinstance(value, str):
4390 return context_wrapper.filterObjectsByDataStringWrapper(self.context, objIDs, label, value)
4391 elif isinstance(value, float):
4392 return context_wrapper.filterObjectsByDataFloatWrapper(self.context, objIDs, label, value, comparator)
4393 elif isinstance(value, int):
4394 return context_wrapper.filterObjectsByDataIntWrapper(self.context, objIDs, label, value, comparator)
4395 else:
4396 raise ValueError(f"Unsupported filter value type: {type(value).__name__}")
4397
4398 # ==================== Global Data Methods ====================
4399
4400 def setGlobalDataInt(self, label: str, value: int) -> None:
4401 """Set global data as signed 32-bit integer."""
4402 context_wrapper.setGlobalDataInt(self.context, label, value)
4403
4404 def setGlobalDataUInt(self, label: str, value: int) -> None:
4405 """Set global data as unsigned 32-bit integer."""
4406 context_wrapper.setGlobalDataUInt(self.context, label, value)
4407
4408 def setGlobalDataFloat(self, label: str, value: float) -> None:
4409 """Set global data as 32-bit float."""
4410 context_wrapper.setGlobalDataFloat(self.context, label, value)
4411
4412 def setGlobalDataDouble(self, label: str, value: float) -> None:
4413 """Set global data as 64-bit double."""
4414 context_wrapper.setGlobalDataDouble(self.context, label, value)
4415
4416 def setGlobalDataString(self, label: str, value: str) -> None:
4417 """Set global data as string."""
4418 context_wrapper.setGlobalDataString(self.context, label, value)
4419
4420 def setGlobalDataVec2(self, label: str, x_or_vec, y: float = None) -> None:
4421 """Set global data as vec2."""
4422 if hasattr(x_or_vec, 'x') and y is None:
4423 x, y = x_or_vec.x, x_or_vec.y
4424 else:
4425 x = x_or_vec
4426 context_wrapper.setGlobalDataVec2(self.context, label, x, y)
4427
4428 def setGlobalDataVec3(self, label: str, x_or_vec, y: float = None, z: float = None) -> None:
4429 """Set global data as vec3."""
4430 if hasattr(x_or_vec, 'x') and y is None:
4431 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4432 else:
4433 x = x_or_vec
4434 context_wrapper.setGlobalDataVec3(self.context, label, x, y, z)
4435
4436 def setGlobalDataVec4(self, label: str, x_or_vec, y: float = None, z: float = None, w: float = None) -> None:
4437 """Set global data as vec4."""
4438 if hasattr(x_or_vec, 'x') and y is None:
4439 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4440 else:
4441 x = x_or_vec
4442 context_wrapper.setGlobalDataVec4(self.context, label, x, y, z, w)
4443
4444 def setGlobalDataInt2(self, label: str, x_or_vec, y: int = None) -> None:
4445 """Set global data as int2."""
4446 if hasattr(x_or_vec, 'x') and y is None:
4447 x, y = x_or_vec.x, x_or_vec.y
4448 else:
4449 x = x_or_vec
4450 context_wrapper.setGlobalDataInt2(self.context, label, x, y)
4451
4452 def setGlobalDataInt3(self, label: str, x_or_vec, y: int = None, z: int = None) -> None:
4453 """Set global data as int3."""
4454 if hasattr(x_or_vec, 'x') and y is None:
4455 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4456 else:
4457 x = x_or_vec
4458 context_wrapper.setGlobalDataInt3(self.context, label, x, y, z)
4459
4460 def setGlobalDataInt4(self, label: str, x_or_vec, y: int = None, z: int = None, w: int = None) -> None:
4461 """Set global data as int4."""
4462 if hasattr(x_or_vec, 'x') and y is None:
4463 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4464 else:
4465 x = x_or_vec
4466 context_wrapper.setGlobalDataInt4(self.context, label, x, y, z, w)
4467
4468 def getGlobalData(self, label: str, data_type: type = None):
4469 """Get global data with optional type specification. Auto-detects type if not specified."""
4470 if data_type is None:
4471 return context_wrapper.getGlobalDataAuto(self.context, label)
4472 if data_type == int:
4473 return context_wrapper.getGlobalDataInt(self.context, label)
4474 elif data_type == float:
4475 return context_wrapper.getGlobalDataFloat(self.context, label)
4476 elif data_type == str:
4477 return context_wrapper.getGlobalDataString(self.context, label)
4478 elif data_type == vec3:
4479 coords = context_wrapper.getGlobalDataVec3(self.context, label)
4480 return vec3(coords[0], coords[1], coords[2])
4481 elif data_type == vec2:
4482 coords = context_wrapper.getGlobalDataVec2(self.context, label)
4483 return vec2(coords[0], coords[1])
4484 elif data_type == vec4:
4485 coords = context_wrapper.getGlobalDataVec4(self.context, label)
4486 return vec4(coords[0], coords[1], coords[2], coords[3])
4487 elif data_type == int2:
4488 coords = context_wrapper.getGlobalDataInt2(self.context, label)
4489 return int2(coords[0], coords[1])
4490 elif data_type == int3:
4491 coords = context_wrapper.getGlobalDataInt3(self.context, label)
4492 return int3(coords[0], coords[1], coords[2])
4493 elif data_type == int4:
4494 coords = context_wrapper.getGlobalDataInt4(self.context, label)
4495 return int4(coords[0], coords[1], coords[2], coords[3])
4496 elif data_type == "uint":
4497 return context_wrapper.getGlobalDataUInt(self.context, label)
4498 elif data_type == "double":
4499 return context_wrapper.getGlobalDataDouble(self.context, label)
4500 else:
4501 raise ValueError(f"Unsupported global data type: {data_type}")
4502
4503 def getGlobalDataFloat(self, label: str) -> float:
4504 """Get float global data."""
4505 return context_wrapper.getGlobalDataFloat(self.context, label)
4506
4507 def getGlobalDataInt(self, label: str) -> int:
4508 """Get int global data."""
4509 return context_wrapper.getGlobalDataInt(self.context, label)
4510
4511 def getGlobalDataString(self, label: str) -> str:
4512 """Get string global data."""
4513 return context_wrapper.getGlobalDataString(self.context, label)
4514
4515 def getGlobalDataType(self, label: str) -> int:
4516 """Get the HeliosDataType enum for global data."""
4517 return context_wrapper.getGlobalDataTypeWrapper(self.context, label)
4518
4519 def getGlobalDataSize(self, label: str) -> int:
4520 """Get the size of global data array."""
4521 return context_wrapper.getGlobalDataSizeWrapper(self.context, label)
4522
4523 def doesGlobalDataExist(self, label: str) -> bool:
4524 """Check if global data exists."""
4525 return context_wrapper.doesGlobalDataExistWrapper(self.context, label)
4526
4527 def clearGlobalData(self, label: str) -> None:
4528 """Clear global data."""
4529 context_wrapper.clearGlobalDataWrapper(self.context, label)
4530
4531 def renameGlobalData(self, old_label: str, new_label: str) -> None:
4532 """Rename a global data label."""
4533 context_wrapper.renameGlobalDataWrapper(self.context, old_label, new_label)
4534
4535 def duplicateGlobalData(self, old_label: str, new_label: str) -> None:
4536 """Duplicate global data to a new label."""
4537 context_wrapper.duplicateGlobalDataWrapper(self.context, old_label, new_label)
4538
4539 def listGlobalData(self) -> List[str]:
4540 """List all global data labels."""
4541 return context_wrapper.listGlobalDataWrapper(self.context)
4542
4543 def incrementGlobalData(self, label: str, increment) -> None:
4544 """Increment global data. Auto-dispatches based on increment type."""
4545 if isinstance(increment, float):
4546 context_wrapper.incrementGlobalDataFloatWrapper(self.context, label, increment)
4547 elif isinstance(increment, int):
4548 context_wrapper.incrementGlobalDataIntWrapper(self.context, label, increment)
4549 else:
4550 raise ValueError(f"Unsupported increment type: {type(increment).__name__}")
4551
4552 # ==================== Primitive Data Statistics & Filtering ====================
4553
4554 def calculatePrimitiveDataMean(self, uuids: List[int], label: str, return_type: type = float):
4555 """Calculate arithmetic mean of primitive data across UUIDs.
4556
4557 Args:
4558 uuids: List of primitive UUIDs.
4559 label: Data label.
4560 return_type: float (default), "double", or vec3.
4561 """
4562 if return_type == float:
4563 return context_wrapper.calculatePrimitiveDataMeanFloatWrapper(self.context, uuids, label)
4564 elif return_type == "double":
4565 return context_wrapper.calculatePrimitiveDataMeanDoubleWrapper(self.context, uuids, label)
4566 elif return_type == vec3:
4567 coords = context_wrapper.calculatePrimitiveDataMeanVec3Wrapper(self.context, uuids, label)
4568 return vec3(coords[0], coords[1], coords[2])
4569 else:
4570 raise ValueError(f"Unsupported return type: {return_type}")
4571
4572 def calculatePrimitiveDataAreaWeightedMean(self, uuids: List[int], label: str, return_type: type = float):
4573 """Calculate area-weighted mean of primitive data."""
4574 if return_type == float:
4575 return context_wrapper.calculatePrimitiveDataAreaWeightedMeanFloatWrapper(self.context, uuids, label)
4576 else:
4577 raise ValueError(f"Unsupported return type: {return_type}")
4578
4579 def calculatePrimitiveDataSum(self, uuids: List[int], label: str, return_type: type = float):
4580 """Calculate sum of primitive data across UUIDs."""
4581 if return_type == float:
4582 return context_wrapper.calculatePrimitiveDataSumFloatWrapper(self.context, uuids, label)
4583 elif return_type == "double":
4584 return context_wrapper.calculatePrimitiveDataSumDoubleWrapper(self.context, uuids, label)
4585 else:
4586 raise ValueError(f"Unsupported return type: {return_type}")
4587
4588 def calculatePrimitiveDataAreaWeightedSum(self, uuids: List[int], label: str, return_type: type = float):
4589 """Calculate area-weighted sum of primitive data."""
4590 if return_type == float:
4591 return context_wrapper.calculatePrimitiveDataAreaWeightedSumFloatWrapper(self.context, uuids, label)
4592 else:
4593 raise ValueError(f"Unsupported return type: {return_type}")
4594
4595 def scalePrimitiveData(self, uuids_or_label, label_or_factor, factor=None) -> None:
4596 """Scale primitive data by a factor.
4598 Overloads:
4599 scalePrimitiveData(uuids, label, factor) - scale for specific UUIDs
4600 scalePrimitiveData(label, factor) - scale for ALL primitives
4601 """
4602 if isinstance(uuids_or_label, str):
4603 context_wrapper.scalePrimitiveDataAllWrapper(self.context, uuids_or_label, label_or_factor)
4604 else:
4605 context_wrapper.scalePrimitiveDataWithUUIDsWrapper(self.context, uuids_or_label, label_or_factor, factor)
4606
4607 def incrementPrimitiveData(self, uuids: List[int], label: str, increment, data_type: str = None) -> None:
4608 """Increment primitive data for the given UUIDs.
4610 Each Helios increment overload only acts on fields whose stored type matches;
4611 fields of a different type are left unchanged. By default the overload is
4612 inferred from the Python type of ``increment`` (``int`` -> int, ``float`` ->
4613 float). To target an unsigned-int or double field, pass ``data_type``
4614 explicitly as one of ``'int'``, ``'uint'``, ``'float'``, ``'double'``.
4615
4616 Args:
4617 uuids: UUIDs whose data field to increment.
4618 label: Data field label.
4619 increment: Amount to add.
4620 data_type: Optional explicit field type to target.
4621 """
4622 if data_type is not None:
4623 dt = data_type.lower()
4624 if dt == 'int':
4625 context_wrapper.incrementPrimitiveDataIntWrapper(self.context, uuids, label, int(increment))
4626 elif dt in ('uint', 'unsigned', 'unsigned int'):
4627 context_wrapper.incrementPrimitiveDataUIntWrapper(self.context, uuids, label, int(increment))
4628 elif dt == 'float':
4629 context_wrapper.incrementPrimitiveDataFloatWrapper(self.context, uuids, label, float(increment))
4630 elif dt == 'double':
4631 context_wrapper.incrementPrimitiveDataDoubleWrapper(self.context, uuids, label, float(increment))
4632 else:
4633 raise ValueError(f"Unsupported data_type: {data_type!r}. Expected one of 'int', 'uint', 'float', 'double'.")
4634 return
4635 if isinstance(increment, float):
4636 context_wrapper.incrementPrimitiveDataFloatWrapper(self.context, uuids, label, increment)
4637 elif isinstance(increment, int):
4638 context_wrapper.incrementPrimitiveDataIntWrapper(self.context, uuids, label, increment)
4639 else:
4640 raise ValueError(f"Unsupported increment type: {type(increment).__name__}")
4641
4642 def aggregatePrimitiveDataSum(self, uuids: List[int], labels: List[str], result_label: str) -> None:
4643 """Sum multiple primitive data fields into a new field."""
4644 context_wrapper.aggregatePrimitiveDataSumWrapper(self.context, uuids, labels, result_label)
4645
4646 def aggregatePrimitiveDataProduct(self, uuids: List[int], labels: List[str], result_label: str) -> None:
4647 """Multiply multiple primitive data fields into a new field."""
4648 context_wrapper.aggregatePrimitiveDataProductWrapper(self.context, uuids, labels, result_label)
4649
4650 def sumPrimitiveSurfaceArea(self, uuids: List[int]) -> float:
4651 """Calculate total one-sided surface area for a set of primitives."""
4652 return context_wrapper.sumPrimitiveSurfaceAreaWrapper(self.context, uuids)
4653
4654 def filterPrimitivesByData(self, uuids: List[int], label: str, value, comparator: str = "=") -> List[int]:
4655 """Filter primitives by data value. Auto-dispatches based on value type.
4656
4657 Args:
4658 uuids: UUIDs to filter.
4659 label: Data label to compare.
4660 value: Filter value (float, int, or str).
4661 comparator: Comparison operator ("=", "<", ">", "<=", ">="). Not used for strings.
4662 """
4663 if isinstance(value, str):
4664 return context_wrapper.filterPrimitivesByDataStringWrapper(self.context, uuids, label, value)
4665 elif isinstance(value, float):
4666 return context_wrapper.filterPrimitivesByDataFloatWrapper(self.context, uuids, label, value, comparator)
4667 elif isinstance(value, int):
4668 return context_wrapper.filterPrimitivesByDataIntWrapper(self.context, uuids, label, value, comparator)
4669 else:
4670 raise ValueError(f"Unsupported filter value type: {type(value).__name__}")
4671
4672 # ==================== Object Geometry Queries ====================
4673
4674 def getObjectType(self, objID: int) -> int:
4675 """Return the integer-coded `helios::ObjectType` of a compound object.
4676
4677 Values follow the C++ `helios::ObjectType` enum
4678 (0=tile, 1=sphere, 2=tube, 3=box, 4=disk, 5=polymesh, 6=cone).
4679 """
4681 return context_wrapper.getObjectTypeWrapper(self.context, objID)
4682
4683 def getObjectCenter(self, objID: int) -> vec3:
4685 x, y, z = context_wrapper.getObjectCenterWrapper(self.context, objID)
4686 return vec3(x, y, z)
4688 def getObjectBoundingBox(self, objIDs):
4689 """Get axis-aligned bounding box for one object or a list of objects.
4690
4691 The box encloses every vertex of every primitive belonging to the given
4692 object(s).
4693
4694 Args:
4695 objIDs: Single object ID (int) or list of object IDs.
4696
4697 Returns:
4698 Tuple of (min_corner: vec3, max_corner: vec3).
4699
4700 Raises:
4701 HeliosRuntimeError: If an object ID does not exist, or if the given
4702 object(s) contain no primitives at all (a bounding box would be
4703 undefined; this previously returned a misleading box at the origin).
4704 """
4706 if isinstance(objIDs, (list, tuple)):
4707 mn, mx = context_wrapper.getObjectBoundingBoxBatchWrapper(self.context, list(objIDs))
4708 else:
4709 mn, mx = context_wrapper.getObjectBoundingBoxWrapper(self.context, objIDs)
4710 return (vec3(mn[0], mn[1], mn[2]), vec3(mx[0], mx[1], mx[2]))
4711
4712 def getObjectPrimitiveUUIDs(self, objIDs) -> List[int]:
4713 """Get flattened primitive UUIDs for one object, a list of objects, or a list-of-lists.
4714
4715 Args:
4716 objIDs: int, List[int], or List[List[int]].
4717
4718 Returns:
4719 Flat list of primitive UUIDs (union across all objects).
4720 """
4722 if isinstance(objIDs, (list, tuple)) and objIDs and isinstance(objIDs[0], (list, tuple)):
4723 return context_wrapper.getObjectPrimitiveUUIDsNestedWrapper(self.context, [list(x) for x in objIDs])
4724 if isinstance(objIDs, (list, tuple)):
4725 return context_wrapper.getObjectPrimitiveUUIDsBatchWrapper(self.context, list(objIDs))
4726 return context_wrapper.getObjectPrimitiveUUIDs(self.context, int(objIDs))
4727
4728 # Tile
4729 def getTileObjectAreaRatio(self, objIDs):
4730 """Get tile-object area ratio for one or multiple tile objects."""
4732 if isinstance(objIDs, (list, tuple)):
4733 return context_wrapper.getTileObjectAreaRatioBatchWrapper(self.context, list(objIDs))
4734 return context_wrapper.getTileObjectAreaRatioWrapper(self.context, objIDs)
4735
4736 def getTileObjectCenter(self, objID: int) -> vec3:
4738 x, y, z = context_wrapper.getTileObjectCenterWrapper(self.context, objID)
4739 return vec3(x, y, z)
4740
4741 def getTileObjectSize(self, objID: int) -> vec2:
4743 x, y = context_wrapper.getTileObjectSizeWrapper(self.context, objID)
4744 return vec2(x, y)
4745
4746 def getTileObjectSubdivisionCount(self, objID: int) -> int2:
4748 x, y = context_wrapper.getTileObjectSubdivisionCountWrapper(self.context, objID)
4749 return int2(x, y)
4750
4751 def getTileObjectNormal(self, objID: int) -> vec3:
4753 x, y, z = context_wrapper.getTileObjectNormalWrapper(self.context, objID)
4754 return vec3(x, y, z)
4755
4756 def getTileObjectTextureUV(self, objID: int) -> List[vec2]:
4758 pairs = context_wrapper.getTileObjectTextureUVWrapper(self.context, objID)
4759 return [vec2(u, v) for u, v in pairs]
4760
4761 def getTileObjectVertices(self, objID: int) -> List[vec3]:
4763 triples = context_wrapper.getTileObjectVerticesWrapper(self.context, objID)
4764 return [vec3(x, y, z) for x, y, z in triples]
4765
4766 # Sphere
4767 def getSphereObjectCenter(self, objID: int) -> vec3:
4769 x, y, z = context_wrapper.getSphereObjectCenterWrapper(self.context, objID)
4770 return vec3(x, y, z)
4771
4772 def getSphereObjectRadius(self, objID: int) -> vec3:
4773 """Get per-axis radii of a sphere object.
4774
4775 Note: Helios spheres are spheroids with three independent radii (rx, ry, rz).
4776 Returns a vec3 (not a scalar).
4777 """
4779 x, y, z = context_wrapper.getSphereObjectRadiusWrapper(self.context, objID)
4780 return vec3(x, y, z)
4781
4782 def getSphereObjectSubdivisionCount(self, objID: int) -> int:
4784 return context_wrapper.getSphereObjectSubdivisionCountWrapper(self.context, objID)
4786 def getSphereObjectVolume(self, objID: int) -> float:
4788 return context_wrapper.getSphereObjectVolumeWrapper(self.context, objID)
4789
4790 # Box
4791 def getBoxObjectCenter(self, objID: int) -> vec3:
4793 x, y, z = context_wrapper.getBoxObjectCenterWrapper(self.context, objID)
4794 return vec3(x, y, z)
4795
4796 def getBoxObjectSize(self, objID: int) -> vec3:
4798 x, y, z = context_wrapper.getBoxObjectSizeWrapper(self.context, objID)
4799 return vec3(x, y, z)
4800
4801 def getBoxObjectSubdivisionCount(self, objID: int) -> int3:
4803 x, y, z = context_wrapper.getBoxObjectSubdivisionCountWrapper(self.context, objID)
4804 return int3(x, y, z)
4805
4806 def getBoxObjectVolume(self, objID: int) -> float:
4808 return context_wrapper.getBoxObjectVolumeWrapper(self.context, objID)
4810 # Disk
4811 def getDiskObjectCenter(self, objID: int) -> vec3:
4813 x, y, z = context_wrapper.getDiskObjectCenterWrapper(self.context, objID)
4814 return vec3(x, y, z)
4815
4816 def getDiskObjectSize(self, objID: int) -> vec2:
4818 x, y = context_wrapper.getDiskObjectSizeWrapper(self.context, objID)
4819 return vec2(x, y)
4820
4821 def getDiskObjectSubdivisionCount(self, objID: int) -> int:
4823 return context_wrapper.getDiskObjectSubdivisionCountWrapper(self.context, objID)
4825 # Tube
4826 def getTubeObjectSubdivisionCount(self, objID: int) -> int:
4828 return context_wrapper.getTubeObjectSubdivisionCountWrapper(self.context, objID)
4830 def getTubeObjectNodeCount(self, objID: int) -> int:
4832 return context_wrapper.getTubeObjectNodeCountWrapper(self.context, objID)
4833
4834 def getTubeObjectNodes(self, objID: int) -> List[vec3]:
4836 triples = context_wrapper.getTubeObjectNodesWrapper(self.context, objID)
4837 return [vec3(x, y, z) for x, y, z in triples]
4839 def getTubeObjectNodeRadii(self, objID: int) -> List[float]:
4841 return context_wrapper.getTubeObjectNodeRadiiWrapper(self.context, objID)
4843 def getTubeObjectNodeColors(self, objID: int) -> List[RGBcolor]:
4845 triples = context_wrapper.getTubeObjectNodeColorsWrapper(self.context, objID)
4846 return [RGBcolor(r, g, b) for r, g, b in triples]
4848 def getTubeObjectVolume(self, objID: int) -> float:
4850 return context_wrapper.getTubeObjectVolumeWrapper(self.context, objID)
4852 def getTubeObjectSegmentVolume(self, objID: int, segment_index: int) -> float:
4854 return context_wrapper.getTubeObjectSegmentVolumeWrapper(self.context, objID, segment_index)
4855
4856 # Cone
4857 def getConeObjectSubdivisionCount(self, objID: int) -> int:
4859 return context_wrapper.getConeObjectSubdivisionCountWrapper(self.context, objID)
4861 def getConeObjectNodes(self, objID: int) -> List[vec3]:
4863 triples = context_wrapper.getConeObjectNodesWrapper(self.context, objID)
4864 return [vec3(x, y, z) for x, y, z in triples]
4866 def getConeObjectNodeRadii(self, objID: int) -> List[float]:
4868 return context_wrapper.getConeObjectNodeRadiiWrapper(self.context, objID)
4870 def getConeObjectNode(self, objID: int, number: int) -> vec3:
4872 x, y, z = context_wrapper.getConeObjectNodeWrapper(self.context, objID, number)
4873 return vec3(x, y, z)
4875 def getConeObjectNodeRadius(self, objID: int, number: int) -> float:
4877 return context_wrapper.getConeObjectNodeRadiusWrapper(self.context, objID, number)
4879 def getConeObjectAxisUnitVector(self, objID: int) -> vec3:
4881 x, y, z = context_wrapper.getConeObjectAxisUnitVectorWrapper(self.context, objID)
4882 return vec3(x, y, z)
4884 def getConeObjectLength(self, objID: int) -> float:
4886 return context_wrapper.getConeObjectLengthWrapper(self.context, objID)
4888 def getConeObjectVolume(self, objID: int) -> float:
4890 return context_wrapper.getConeObjectVolumeWrapper(self.context, objID)
4891
4892 # ==================== Primitive Geometry Queries ====================
4893
4894 def getPatchCenter(self, uuid: int) -> vec3:
4896 x, y, z = context_wrapper.getPatchCenterWrapper(self.context, uuid)
4897 return vec3(x, y, z)
4898
4899 def getPatchSize(self, uuid: int) -> vec2:
4901 x, y = context_wrapper.getPatchSizeWrapper(self.context, uuid)
4902 return vec2(x, y)
4903
4904 def getTriangleVertex(self, uuid: int, number: int) -> vec3:
4906 x, y, z = context_wrapper.getTriangleVertexWrapper(self.context, uuid, number)
4907 return vec3(x, y, z)
4908
4909 def getVoxelCenter(self, uuid: int) -> vec3:
4911 x, y, z = context_wrapper.getVoxelCenterWrapper(self.context, uuid)
4912 return vec3(x, y, z)
4913
4914 def getVoxelSize(self, uuid: int) -> vec3:
4916 x, y, z = context_wrapper.getVoxelSizeWrapper(self.context, uuid)
4917 return vec3(x, y, z)
4918
4919 def getPatchCount(self, include_hidden: bool = True) -> int:
4921 return context_wrapper.getPatchCountWrapper(self.context, include_hidden)
4923 def getTriangleCount(self, include_hidden: bool = True) -> int:
4925 return context_wrapper.getTriangleCountWrapper(self.context, include_hidden)
4926
4927 def getPrimitiveBoundingBox(self, uuids):
4928 """Get axis-aligned bounding box for one primitive or a list of primitives.
4929
4930 Args:
4931 uuids: Single UUID (int) or list of UUIDs.
4932
4933 Returns:
4934 Tuple of (min_corner: vec3, max_corner: vec3).
4935 """
4937 if isinstance(uuids, (list, tuple)):
4938 mn, mx = context_wrapper.getPrimitiveBoundingBoxBatchWrapper(self.context, list(uuids))
4939 else:
4940 mn, mx = context_wrapper.getPrimitiveBoundingBoxWrapper(self.context, uuids)
4941 return (vec3(mn[0], mn[1], mn[2]), vec3(mx[0], mx[1], mx[2]))
4942
4943 # ==================== Primitive Color Mutation ====================
4944
4945 def setPrimitiveColor(self, uuids, color) -> None:
4946 """Set the RGB or RGBA color of one primitive or a list of primitives.
4947
4948 Args:
4949 uuids: Single UUID (int) or list of UUIDs.
4950 color: RGBcolor or RGBAcolor.
4951 """
4953 if isinstance(color, RGBAcolor):
4954 rgba = [color.r, color.g, color.b, color.a]
4955 if isinstance(uuids, (list, tuple)):
4956 context_wrapper.setPrimitiveColorRGBABatchWrapper(self.context, list(uuids), rgba)
4957 else:
4958 context_wrapper.setPrimitiveColorRGBAWrapper(self.context, uuids, rgba)
4959 elif isinstance(color, RGBcolor):
4960 rgb = [color.r, color.g, color.b]
4961 if isinstance(uuids, (list, tuple)):
4962 context_wrapper.setPrimitiveColorBatchWrapper(self.context, list(uuids), rgb)
4963 else:
4964 context_wrapper.setPrimitiveColorWrapper(self.context, uuids, rgb)
4965 else:
4966 raise ValueError(f"color must be RGBcolor or RGBAcolor, got {type(color).__name__}")
4967
4968 # ==================== Primitive Data Introspection / Cleanup ====================
4969
4970 def clearPrimitiveData(self, uuids, label: str) -> None:
4971 """Remove a named data field from one primitive or a list of primitives."""
4973 if isinstance(uuids, (list, tuple)):
4974 context_wrapper.clearPrimitiveDataByLabelBatchWrapper(self.context, list(uuids), label)
4975 else:
4976 context_wrapper.clearPrimitiveDataByLabelWrapper(self.context, uuids, label)
4977
4978 def clearAllPrimitiveData(self, label: str) -> None:
4979 """Remove a named data field from every primitive in the Context.
4980
4981 Clears the data with the given label from all primitives (including hidden ones)
4982 and releases the registered data type for the label, so it may subsequently be
4983 re-registered with a different type. Requires helios-core v1.3.73 or newer.
4984 """
4986 context_wrapper.clearAllPrimitiveDataByLabelWrapper(self.context, label)
4987
4988 def listPrimitiveData(self, uuid: int) -> List[str]:
4989 """List all data labels attached to a primitive."""
4991 return context_wrapper.listPrimitiveDataWrapper(self.context, uuid)
4993 # ==================== Domain Cropping ====================
4994
4995 def cropDomainX(self, xbounds: vec2) -> None:
4997 if not isinstance(xbounds, vec2):
4998 raise ValueError(f"xbounds must be a vec2, got {type(xbounds).__name__}")
4999 context_wrapper.cropDomainXWrapper(self.context, xbounds.to_list())
5000
5001 def cropDomainY(self, ybounds: vec2) -> None:
5003 if not isinstance(ybounds, vec2):
5004 raise ValueError(f"ybounds must be a vec2, got {type(ybounds).__name__}")
5005 context_wrapper.cropDomainYWrapper(self.context, ybounds.to_list())
5006
5007 def cropDomainZ(self, zbounds: vec2) -> None:
5009 if not isinstance(zbounds, vec2):
5010 raise ValueError(f"zbounds must be a vec2, got {type(zbounds).__name__}")
5011 context_wrapper.cropDomainZWrapper(self.context, zbounds.to_list())
5012
5013 def cropDomain(self, *args) -> Optional[List[int]]:
5014 """Crop the context domain to the given XYZ bounds.
5016 Two call forms:
5017 cropDomain(xbounds: vec2, ybounds: vec2, zbounds: vec2)
5018 -> crop ALL primitives; returns None.
5019 cropDomain(uuids: List[int], xbounds: vec2, ybounds: vec2, zbounds: vec2)
5020 -> crop only the given primitives; returns the list of primitives
5021 that survived (in-bounds UUIDs). The input list is NOT mutated.
5022 """
5024 if len(args) == 3:
5025 xb, yb, zb = args
5026 for name, b in (("xbounds", xb), ("ybounds", yb), ("zbounds", zb)):
5027 if not isinstance(b, vec2):
5028 raise ValueError(f"{name} must be a vec2, got {type(b).__name__}")
5029 context_wrapper.cropDomainXYZWrapper(self.context, xb.to_list(), yb.to_list(), zb.to_list())
5030 return None
5031 if len(args) == 4:
5032 uuids, xb, yb, zb = args
5033 if not isinstance(uuids, (list, tuple)):
5034 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
5035 for name, b in (("xbounds", xb), ("ybounds", yb), ("zbounds", zb)):
5036 if not isinstance(b, vec2):
5037 raise ValueError(f"{name} must be a vec2, got {type(b).__name__}")
5038 return context_wrapper.cropDomainByUUIDsWrapper(self.context, list(uuids), xb.to_list(), yb.to_list(), zb.to_list())
5039 raise TypeError(f"cropDomain() takes 3 or 4 positional arguments, got {len(args)}")
5040
5041 # =========================================================================
5042 # Scalar Getters / Setters & List-of-String Getters
5043 # =========================================================================
5044
5045 # ---- Existence / state queries ----
5046
5047 def doesObjectExist(self, objID: int) -> bool:
5048 """Return True if a compound object with the given ID exists."""
5050 return context_wrapper.doesObjectExistWrapper(self.context, objID)
5051
5052 def doesObjectContainPrimitive(self, objID: int, uuid: int) -> bool:
5053 """Return True if the given primitive UUID belongs to the given object."""
5055 return context_wrapper.doesObjectContainPrimitiveWrapper(self.context, objID, uuid)
5057 def doesMaterialDataExist(self, material_label: str, data_label: str) -> bool:
5058 """Return True if the named material has data stored under data_label."""
5060 return context_wrapper.doesMaterialDataExistWrapper(self.context, material_label, data_label)
5062 def objectHasTexture(self, objID: int) -> bool:
5063 """Return True if the compound object has a texture assigned."""
5065 return context_wrapper.objectHasTextureWrapper(self.context, objID)
5067 def isPrimitiveDirty(self, uuid: int) -> bool:
5068 """Return True if the primitive's geometry has been modified since the last clean mark."""
5070 return context_wrapper.isPrimitiveDirtyWrapper(self.context, uuid)
5072 def isObjectDataValueCachingEnabled(self, label: str) -> bool:
5073 """Return True if value caching is enabled for the given object-data label."""
5075 return context_wrapper.isObjectDataValueCachingEnabledWrapper(self.context, label)
5077 def isPrimitiveDataValueCachingEnabled(self, label: str) -> bool:
5078 """Return True if value caching is enabled for the given primitive-data label."""
5080 return context_wrapper.isPrimitiveDataValueCachingEnabledWrapper(self.context, label)
5082 def areObjectPrimitivesComplete(self, objID: int) -> bool:
5083 """Return True if all primitives originally belonging to this object still exist
5084 (i.e., none have been deleted)."""
5086 return context_wrapper.areObjectPrimitivesCompleteWrapper(self.context, objID)
5087
5088 # ---- Numeric scalar getters ----
5089
5090 def getJulianDate(self) -> int:
5091 """Get the current simulation date as Julian day (1-366)."""
5093 return context_wrapper.getJulianDateWrapper(self.context)
5094
5095 def getMaterialCount(self) -> int:
5096 """Return the total number of materials registered in the context."""
5098 return context_wrapper.getMaterialCountWrapper(self.context)
5100 def getObjectArea(self, objID: int) -> float:
5101 """Return the total surface area (one-sided) of all primitives in the object."""
5103 return context_wrapper.getObjectAreaWrapper(self.context, objID)
5105 def getObjectPrimitiveCount(self, objID: int) -> int:
5106 """Return the number of primitives currently belonging to the object."""
5108 return context_wrapper.getObjectPrimitiveCountWrapper(self.context, objID)
5110 def getPolymeshObjectVolume(self, objID: int) -> float:
5111 """Return the enclosed volume of a polymesh object."""
5113 return context_wrapper.getPolymeshObjectVolumeWrapper(self.context, objID)
5115 def getMaterialIDFromLabel(self, material_label: str) -> int:
5116 """Look up a material ID from its human-readable label."""
5118 return context_wrapper.getMaterialIDFromLabelWrapper(self.context, material_label)
5120 def getPrimitiveMaterialID(self, uuid: int) -> int:
5121 """Return the material ID assigned to the given primitive."""
5123 return context_wrapper.getPrimitiveMaterialIDWrapper(self.context, uuid)
5125 def getGlobalDataVersion(self, label: str) -> int:
5126 """Return the version counter for a global data entry. Increments on each update;
5127 useful for cache invalidation."""
5129 return context_wrapper.getGlobalDataVersionWrapper(self.context, label)
5130
5131 def getPrimitiveParentObjectID(self, uuid: int) -> int:
5132 """Return the ID of the compound object the primitive belongs to.
5133
5134 Returns 0 if the primitive is not part of any compound object (the documented
5135 "no parent" sentinel). Raises ``HeliosRuntimeError`` if ``uuid`` does not exist.
5136 """
5138 return context_wrapper.getPrimitiveParentObjectIDWrapper(self.context, uuid)
5139
5140 # ---- String / list-of-string getters ----
5141
5142 def getObjectTextureFile(self, objID: int) -> str:
5143 """Return the filesystem path of the texture assigned to the object, or an
5144 empty string if no texture is assigned."""
5146 return context_wrapper.getObjectTextureFileWrapper(self.context, objID)
5147
5148 def listAllPrimitiveDataLabels(self) -> List[str]:
5149 """Return the union of all primitive-data labels used across every primitive
5150 in the context."""
5152 return context_wrapper.listAllPrimitiveDataLabelsWrapper(self.context)
5153
5154 def getLoadedXMLFiles(self) -> List[str]:
5155 """Return the list of XML file paths that have been loaded into this context."""
5157 return context_wrapper.getLoadedXMLFilesWrapper(self.context)
5159 # ---- Simple actions ----
5160
5161 def printObjectInfo(self, objID: int) -> None:
5162 """Print summary info for the object to stdout (for debugging)."""
5164 context_wrapper.printObjectInfoWrapper(self.context, objID)
5165
5166 def printPrimitiveInfo(self, uuid: int) -> None:
5167 """Print summary info for the primitive to stdout (for debugging)."""
5169 context_wrapper.printPrimitiveInfoWrapper(self.context, uuid)
5171 def enablePrimitiveDataValueCaching(self, label: str) -> None:
5172 """Enable value caching for the given primitive-data label. Required before
5173 using getUniquePrimitiveDataValues for that label."""
5175 context_wrapper.enablePrimitiveDataValueCachingWrapper(self.context, label)
5176
5177 def disablePrimitiveDataValueCaching(self, label: str) -> None:
5178 """Disable value caching for the given primitive-data label."""
5180 context_wrapper.disablePrimitiveDataValueCachingWrapper(self.context, label)
5182 def enableObjectDataValueCaching(self, label: str) -> None:
5183 """Enable value caching for the given object-data label. Required before
5184 using getUniqueObjectDataValues for that label."""
5186 context_wrapper.enableObjectDataValueCachingWrapper(self.context, label)
5187
5188 def disableObjectDataValueCaching(self, label: str) -> None:
5189 """Disable value caching for the given object-data label."""
5191 context_wrapper.disableObjectDataValueCachingWrapper(self.context, label)
5193 def setObjectDataFromPrimitiveDataMean(self, objID: int, label: str) -> None:
5194 """Compute the mean of the given primitive-data label across the object's
5195 primitives and store it as object data on the object itself under the
5196 same label."""
5198 context_wrapper.setObjectDataFromPrimitiveDataMeanWrapper(self.context, objID, label)
5199
5200 def renameMaterial(self, old_label: str, new_label: str) -> None:
5201 """Rename an existing material."""
5203 context_wrapper.renameMaterialWrapper(self.context, old_label, new_label)
5205 def renamePrimitiveData(self, uuid: int, old_label: str, new_label: str) -> None:
5206 """Rename a primitive-data label on a single primitive."""
5208 context_wrapper.renamePrimitiveDataWrapper(self.context, uuid, old_label, new_label)
5210 def clearMaterialData(self, material_label: str, data_label: str) -> None:
5211 """Clear the named data entry on the given material."""
5213 context_wrapper.clearMaterialDataWrapper(self.context, material_label, data_label)
5215 # =========================================================================
5216 # Vector-return getters & geometry mutators
5217 # =========================================================================
5218
5219 # ---- Vector<uint> queries ----
5220
5221 def getDeletedUUIDs(self) -> List[int]:
5222 """Return the list of UUIDs that have been deleted from the context.
5223
5224 These UUIDs are tombstoned and will not appear in getAllUUIDs(), but their
5225 IDs are tracked so they can be excluded from external references.
5226 """
5228 return context_wrapper.getDeletedUUIDsWrapper(self.context)
5229
5230 def getDirtyUUIDs(self, include_deleted: bool = True) -> List[int]:
5231 """Return the list of UUIDs whose geometry has been modified since the last
5232 markGeometryClean call.
5233
5234 Args:
5235 include_deleted: If True (default), include UUIDs that were deleted while
5236 dirty. If False, only return UUIDs that still exist.
5237 """
5239 return context_wrapper.getDirtyUUIDsWrapper(self.context, include_deleted)
5240
5241 def getUniquePrimitiveParentObjectIDs(self, uuids: List[int],
5242 include_zero: bool = True) -> List[int]:
5243 """Return the unique set of compound-object IDs that the given primitives
5244 belong to.
5246 Args:
5247 uuids: List of primitive UUIDs to inspect.
5248 include_zero: If True (default), include the sentinel object ID 0
5249 (i.e., primitives with no parent object). If False, only return
5250 IDs of real compound objects.
5251 """
5253 if not isinstance(uuids, (list, tuple)):
5254 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
5255 return context_wrapper.getUniquePrimitiveParentObjectIDsWrapper(
5256 self.context, list(uuids), include_zero
5257 )
5259 # ---- Object normal / origin ----
5260
5261 def getObjectAverageNormal(self, objID: int) -> vec3:
5262 """Return the area-weighted average normal of all primitives in the object."""
5264 x, y, z = context_wrapper.getObjectAverageNormalWrapper(self.context, objID)
5265 return vec3(x, y, z)
5266
5267 def setObjectAverageNormal(self, objID: int, origin: vec3, new_normal: vec3) -> None:
5268 """Rotate the object so its area-weighted average normal aligns with
5269 new_normal. The rotation is applied about the given origin point."""
5271 if not isinstance(origin, vec3):
5272 raise ValueError(f"origin must be a vec3, got {type(origin).__name__}")
5273 if not isinstance(new_normal, vec3):
5274 raise ValueError(f"new_normal must be a vec3, got {type(new_normal).__name__}")
5275 context_wrapper.setObjectAverageNormalWrapper(
5276 self.context, objID, origin.to_list(), new_normal.to_list()
5278
5279 def setObjectOrigin(self, objID: int, origin: vec3) -> None:
5280 """Translate the object so its origin is moved to the given point."""
5282 if not isinstance(origin, vec3):
5283 raise ValueError(f"origin must be a vec3, got {type(origin).__name__}")
5284 context_wrapper.setObjectOriginWrapper(self.context, objID, origin.to_list())
5285
5286 # ---- Primitive azimuth / elevation ----
5287
5288 def setPrimitiveAzimuth(self, uuid: int, origin: vec3, new_azimuth: float) -> None:
5289 """Rotate a single primitive about the given origin so its azimuth
5290 equals new_azimuth (radians)."""
5292 if not isinstance(origin, vec3):
5293 raise ValueError(f"origin must be a vec3, got {type(origin).__name__}")
5294 context_wrapper.setPrimitiveAzimuthWrapper(
5295 self.context, uuid, origin.to_list(), float(new_azimuth)
5296 )
5297
5298 def setPrimitiveElevation(self, uuid: int, origin: vec3, new_elevation: float) -> None:
5299 """Rotate a single primitive about the given origin so its elevation
5300 equals new_elevation (radians)."""
5302 if not isinstance(origin, vec3):
5303 raise ValueError(f"origin must be a vec3, got {type(origin).__name__}")
5304 context_wrapper.setPrimitiveElevationWrapper(
5305 self.context, uuid, origin.to_list(), float(new_elevation)
5306 )
5307
5308 # ---- Geometry mutators ----
5309
5310 def setTriangleVertices(self, uuid: int, vertex0: vec3, vertex1: vec3, vertex2: vec3) -> None:
5311 """Replace the three vertices of an existing triangle primitive."""
5313 for name, v in (("vertex0", vertex0), ("vertex1", vertex1), ("vertex2", vertex2)):
5314 if not isinstance(v, vec3):
5315 raise ValueError(f"{name} must be a vec3, got {type(v).__name__}")
5316 context_wrapper.setTriangleVerticesWrapper(
5317 self.context, uuid, vertex0.to_list(), vertex1.to_list(), vertex2.to_list()
5318 )
5320 def setPrimitiveNormal(self, uuids_or_uuid, origin: vec3, new_normal: vec3) -> None:
5321 """Rotate one or more primitives so their normals align with new_normal.
5322
5323 Accepts either a single UUID (int) or a list/tuple of UUIDs.
5324 The rotation is applied about the given origin point.
5325 """
5327 if not isinstance(origin, vec3):
5328 raise ValueError(f"origin must be a vec3, got {type(origin).__name__}")
5329 if not isinstance(new_normal, vec3):
5330 raise ValueError(f"new_normal must be a vec3, got {type(new_normal).__name__}")
5331 if isinstance(uuids_or_uuid, (list, tuple)):
5332 context_wrapper.setPrimitiveNormalBatchWrapper(
5333 self.context, list(uuids_or_uuid), origin.to_list(), new_normal.to_list()
5334 )
5335 else:
5336 context_wrapper.setPrimitiveNormalWrapper(
5337 self.context, uuids_or_uuid, origin.to_list(), new_normal.to_list()
5338 )
5339
5340 def setPrimitiveParentObjectID(self, uuids_or_uuid, objID: int) -> None:
5341 """Reassign one or more primitives to belong to the given compound object.
5342
5343 Accepts either a single UUID (int) or a list/tuple of UUIDs. Pass objID=0
5344 to detach primitive(s) from any object.
5345 """
5347 if isinstance(uuids_or_uuid, (list, tuple)):
5348 context_wrapper.setPrimitiveParentObjectIDBatchWrapper(
5349 self.context, list(uuids_or_uuid), int(objID)
5350 )
5351 else:
5352 context_wrapper.setPrimitiveParentObjectIDWrapper(
5353 self.context, int(uuids_or_uuid), int(objID)
5354 )
5355
5356 # =========================================================================
5357 # Material data API + unique data values
5358 # =========================================================================
5359
5360 # ---- Per-type explicit setMaterialData* methods ----
5361 # These mirror the existing setPrimitiveData<Type> family for parity.
5362
5363 def setMaterialDataInt(self, material_label: str, data_label: str, value: int) -> None:
5364 """Set int data on a material. Affects all primitives that reference it."""
5366 context_wrapper.setMaterialDataIntWrapper(self.context, material_label, data_label, int(value))
5367
5368 def setMaterialDataUInt(self, material_label: str, data_label: str, value: int) -> None:
5369 """Set unsigned int data on a material."""
5371 context_wrapper.setMaterialDataUIntWrapper(self.context, material_label, data_label, int(value))
5373 def setMaterialDataFloat(self, material_label: str, data_label: str, value: float) -> None:
5374 """Set float data on a material."""
5376 context_wrapper.setMaterialDataFloatWrapper(self.context, material_label, data_label, float(value))
5378 def setMaterialDataDouble(self, material_label: str, data_label: str, value: float) -> None:
5379 """Set double-precision float data on a material."""
5381 context_wrapper.setMaterialDataDoubleWrapper(self.context, material_label, data_label, float(value))
5383 def setMaterialDataString(self, material_label: str, data_label: str, value: str) -> None:
5384 """Set string data on a material."""
5386 context_wrapper.setMaterialDataStringWrapper(self.context, material_label, data_label, str(value))
5388 def setMaterialDataVec2(self, material_label: str, data_label: str, value: vec2) -> None:
5389 """Set vec2 data on a material."""
5391 if not isinstance(value, vec2):
5392 raise ValueError(f"value must be a vec2, got {type(value).__name__}")
5393 context_wrapper.setMaterialDataVec2Wrapper(self.context, material_label, data_label, value.x, value.y)
5394
5395 def setMaterialDataVec3(self, material_label: str, data_label: str, value: vec3) -> None:
5396 """Set vec3 data on a material."""
5398 if not isinstance(value, vec3):
5399 raise ValueError(f"value must be a vec3, got {type(value).__name__}")
5400 context_wrapper.setMaterialDataVec3Wrapper(self.context, material_label, data_label, value.x, value.y, value.z)
5401
5402 def setMaterialDataVec4(self, material_label: str, data_label: str, value: vec4) -> None:
5403 """Set vec4 data on a material."""
5405 if not isinstance(value, vec4):
5406 raise ValueError(f"value must be a vec4, got {type(value).__name__}")
5407 context_wrapper.setMaterialDataVec4Wrapper(self.context, material_label, data_label, value.x, value.y, value.z, value.w)
5408
5409 def setMaterialDataInt2(self, material_label: str, data_label: str, value: int2) -> None:
5410 """Set int2 data on a material."""
5412 if not isinstance(value, int2):
5413 raise ValueError(f"value must be an int2, got {type(value).__name__}")
5414 context_wrapper.setMaterialDataInt2Wrapper(self.context, material_label, data_label, value.x, value.y)
5415
5416 def setMaterialDataInt3(self, material_label: str, data_label: str, value: int3) -> None:
5417 """Set int3 data on a material."""
5419 if not isinstance(value, int3):
5420 raise ValueError(f"value must be an int3, got {type(value).__name__}")
5421 context_wrapper.setMaterialDataInt3Wrapper(self.context, material_label, data_label, value.x, value.y, value.z)
5422
5423 def setMaterialDataInt4(self, material_label: str, data_label: str, value: int4) -> None:
5424 """Set int4 data on a material."""
5426 if not isinstance(value, int4):
5427 raise ValueError(f"value must be an int4, got {type(value).__name__}")
5428 context_wrapper.setMaterialDataInt4Wrapper(self.context, material_label, data_label, value.x, value.y, value.z, value.w)
5429
5430 # ---- Per-type explicit getMaterialData* methods ----
5431
5432 def getMaterialDataInt(self, material_label: str, data_label: str) -> int:
5434 return context_wrapper.getMaterialDataIntWrapper(self.context, material_label, data_label)
5435
5436 def getMaterialDataUInt(self, material_label: str, data_label: str) -> int:
5438 return context_wrapper.getMaterialDataUIntWrapper(self.context, material_label, data_label)
5439
5440 def getMaterialDataFloat(self, material_label: str, data_label: str) -> float:
5442 return context_wrapper.getMaterialDataFloatWrapper(self.context, material_label, data_label)
5443
5444 def getMaterialDataDouble(self, material_label: str, data_label: str) -> float:
5446 return context_wrapper.getMaterialDataDoubleWrapper(self.context, material_label, data_label)
5447
5448 def getMaterialDataString(self, material_label: str, data_label: str) -> str:
5450 return context_wrapper.getMaterialDataStringWrapper(self.context, material_label, data_label)
5451
5452 def getMaterialDataVec2(self, material_label: str, data_label: str) -> vec2:
5454 x, y = context_wrapper.getMaterialDataVec2Wrapper(self.context, material_label, data_label)
5455 return vec2(x, y)
5457 def getMaterialDataVec3(self, material_label: str, data_label: str) -> vec3:
5459 x, y, z = context_wrapper.getMaterialDataVec3Wrapper(self.context, material_label, data_label)
5460 return vec3(x, y, z)
5461
5462 def getMaterialDataVec4(self, material_label: str, data_label: str) -> vec4:
5464 x, y, z, w = context_wrapper.getMaterialDataVec4Wrapper(self.context, material_label, data_label)
5465 return vec4(x, y, z, w)
5466
5467 def getMaterialDataInt2(self, material_label: str, data_label: str) -> int2:
5469 x, y = context_wrapper.getMaterialDataInt2Wrapper(self.context, material_label, data_label)
5470 return int2(x, y)
5471
5472 def getMaterialDataInt3(self, material_label: str, data_label: str) -> int3:
5474 x, y, z = context_wrapper.getMaterialDataInt3Wrapper(self.context, material_label, data_label)
5475 return int3(x, y, z)
5476
5477 def getMaterialDataInt4(self, material_label: str, data_label: str) -> int4:
5479 x, y, z, w = context_wrapper.getMaterialDataInt4Wrapper(self.context, material_label, data_label)
5480 return int4(x, y, z, w)
5481
5482 def getMaterialDataType(self, material_label: str, data_label: str) -> int:
5483 """Return the HeliosDataType enum value for the given material data entry.
5484
5485 Encoding (from Helios core): 0=INT, 1=UINT, 2=FLOAT, 3=DOUBLE,
5486 4=VEC2, 5=VEC3, 6=VEC4, 7=INT2, 8=INT3, 9=INT4, 10=STRING.
5487 """
5489 return context_wrapper.getMaterialDataTypeWrapper(self.context, material_label, data_label)
5490
5491 # ---- Unified dispatch setMaterialData / getMaterialData ----
5492
5493 def setMaterialData(self, material_label: str, data_label: str, value) -> None:
5494 """Set material data with type detection from the Python value.
5496 Dispatches to the correct typed setter based on ``isinstance`` of ``value``.
5497 For unambiguous numeric width control (e.g., uint vs int), call the
5498 per-type method directly (``setMaterialDataUInt``, etc.).
5499 """
5501 if isinstance(value, bool):
5502 # bool is a subclass of int in Python; route to int explicitly.
5503 context_wrapper.setMaterialDataIntWrapper(self.context, material_label, data_label, int(value))
5504 elif isinstance(value, int):
5505 context_wrapper.setMaterialDataIntWrapper(self.context, material_label, data_label, int(value))
5506 elif isinstance(value, float):
5507 context_wrapper.setMaterialDataFloatWrapper(self.context, material_label, data_label, float(value))
5508 elif isinstance(value, str):
5509 context_wrapper.setMaterialDataStringWrapper(self.context, material_label, data_label, value)
5510 elif isinstance(value, vec2):
5511 context_wrapper.setMaterialDataVec2Wrapper(self.context, material_label, data_label, value.x, value.y)
5512 elif isinstance(value, vec3):
5513 context_wrapper.setMaterialDataVec3Wrapper(self.context, material_label, data_label, value.x, value.y, value.z)
5514 elif isinstance(value, vec4):
5515 context_wrapper.setMaterialDataVec4Wrapper(self.context, material_label, data_label, value.x, value.y, value.z, value.w)
5516 elif isinstance(value, int2):
5517 context_wrapper.setMaterialDataInt2Wrapper(self.context, material_label, data_label, value.x, value.y)
5518 elif isinstance(value, int3):
5519 context_wrapper.setMaterialDataInt3Wrapper(self.context, material_label, data_label, value.x, value.y, value.z)
5520 elif isinstance(value, int4):
5521 context_wrapper.setMaterialDataInt4Wrapper(self.context, material_label, data_label, value.x, value.y, value.z, value.w)
5522 else:
5523 raise ValueError(
5524 f"Unsupported value type for setMaterialData: {type(value).__name__}. "
5525 f"Supported: int, float, str, vec2, vec3, vec4, int2, int3, int4. "
5526 f"For uint/double, call setMaterialDataUInt/Double directly."
5527 )
5528
5529 def getMaterialData(self, material_label: str, data_label: str, data_type: type = None):
5530 """Get material data, auto-detecting the type from Helios storage if not specified.
5531
5532 Args:
5533 material_label: Name of the material.
5534 data_label: Data entry label.
5535 data_type: Optional Python type (int, float, str, vec2, vec3, vec4, int2,
5536 int3, int4) or string ('uint', 'double'). If ``None``, the type is
5537 queried via getMaterialDataType and dispatched automatically.
5538 """
5540 if data_type is None:
5541 t = context_wrapper.getMaterialDataTypeWrapper(self.context, material_label, data_label)
5542 # Map HeliosDataType enum → typed call
5543 if t == 0:
5544 return context_wrapper.getMaterialDataIntWrapper(self.context, material_label, data_label)
5545 if t == 1:
5546 return context_wrapper.getMaterialDataUIntWrapper(self.context, material_label, data_label)
5547 if t == 2:
5548 return context_wrapper.getMaterialDataFloatWrapper(self.context, material_label, data_label)
5549 if t == 3:
5550 return context_wrapper.getMaterialDataDoubleWrapper(self.context, material_label, data_label)
5551 if t == 4:
5552 x, y = context_wrapper.getMaterialDataVec2Wrapper(self.context, material_label, data_label)
5553 return vec2(x, y)
5554 if t == 5:
5555 x, y, z = context_wrapper.getMaterialDataVec3Wrapper(self.context, material_label, data_label)
5556 return vec3(x, y, z)
5557 if t == 6:
5558 x, y, z, w = context_wrapper.getMaterialDataVec4Wrapper(self.context, material_label, data_label)
5559 return vec4(x, y, z, w)
5560 if t == 7:
5561 x, y = context_wrapper.getMaterialDataInt2Wrapper(self.context, material_label, data_label)
5562 return int2(x, y)
5563 if t == 8:
5564 x, y, z = context_wrapper.getMaterialDataInt3Wrapper(self.context, material_label, data_label)
5565 return int3(x, y, z)
5566 if t == 9:
5567 x, y, z, w = context_wrapper.getMaterialDataInt4Wrapper(self.context, material_label, data_label)
5568 return int4(x, y, z, w)
5569 if t == 10:
5570 return context_wrapper.getMaterialDataStringWrapper(self.context, material_label, data_label)
5571 raise ValueError(f"Unknown HeliosDataType code: {t}")
5572
5573 # Explicit type dispatch
5574 if data_type == int:
5575 return self.getMaterialDataInt(material_label, data_label)
5576 if data_type == float:
5577 return self.getMaterialDataFloat(material_label, data_label)
5578 if data_type == str:
5579 return self.getMaterialDataString(material_label, data_label)
5580 if data_type == "uint":
5581 return self.getMaterialDataUInt(material_label, data_label)
5582 if data_type == "double":
5583 return self.getMaterialDataDouble(material_label, data_label)
5584 if data_type == vec2:
5585 return self.getMaterialDataVec2(material_label, data_label)
5586 if data_type == vec3:
5587 return self.getMaterialDataVec3(material_label, data_label)
5588 if data_type == vec4:
5589 return self.getMaterialDataVec4(material_label, data_label)
5590 if data_type == int2:
5591 return self.getMaterialDataInt2(material_label, data_label)
5592 if data_type == int3:
5593 return self.getMaterialDataInt3(material_label, data_label)
5594 if data_type == int4:
5595 return self.getMaterialDataInt4(material_label, data_label)
5596 raise ValueError(
5597 f"Unsupported material data type: {data_type}. Supported: int, float, str, "
5598 f"vec2, vec3, vec4, int2, int3, int4, 'uint', 'double'."
5599 )
5600
5601 # ---- Unique data values ----
5602
5603 def getUniquePrimitiveDataValues(self, label: str, dtype: type) -> List:
5604 """Return the unique values stored under ``label`` across all primitives.
5605
5606 Requires value caching to be enabled for ``label`` first via
5607 ``enablePrimitiveDataValueCaching(label)``. Supported ``dtype`` values:
5608 ``int``, ``str``, or the string ``'uint'``.
5609 """
5611 if dtype == int:
5612 return context_wrapper.getUniquePrimitiveDataValuesIntWrapper(self.context, label)
5613 if dtype == "uint":
5614 return context_wrapper.getUniquePrimitiveDataValuesUIntWrapper(self.context, label)
5615 if dtype == str:
5616 return context_wrapper.getUniquePrimitiveDataValuesStringWrapper(self.context, label)
5617 raise ValueError(
5618 f"Unsupported dtype for getUniquePrimitiveDataValues: {dtype}. "
5619 f"Supported: int, str, 'uint'."
5620 )
5621
5622 def getUniqueObjectDataValues(self, label: str, dtype: type) -> List:
5623 """Return the unique values stored under ``label`` across all compound objects.
5624
5625 Requires value caching to be enabled for ``label`` first via
5626 ``enableObjectDataValueCaching(label)``. Supported ``dtype`` values:
5627 ``int``, ``str``, or the string ``'uint'``.
5628 """
5630 if dtype == int:
5631 return context_wrapper.getUniqueObjectDataValuesIntWrapper(self.context, label)
5632 if dtype == "uint":
5633 return context_wrapper.getUniqueObjectDataValuesUIntWrapper(self.context, label)
5634 if dtype == str:
5635 return context_wrapper.getUniqueObjectDataValuesStringWrapper(self.context, label)
5636 raise ValueError(
5637 f"Unsupported dtype for getUniqueObjectDataValues: {dtype}. "
5638 f"Supported: int, str, 'uint'."
5639 )
5640
5641 # =========================================================================
5642 # 4x4 transformation matrices + domain bounds
5643 # =========================================================================
5644
5645 @staticmethod
5646 def _marshal_mat4(value) -> List[float]:
5647 """Coerce a 4x4 transformation matrix input into a flat list of 16 floats.
5648
5649 Accepts: numpy.ndarray of shape (4,4) or (16,), list/tuple of 16 floats,
5650 or nested list/tuple of shape (4,4). Helios stores transformation matrices
5651 in **row-major** order: T[i*4 + j] = element (i, j). A numpy ndarray of
5652 shape (4,4) maps directly via .ravel() since numpy is row-major by default.
5653 """
5654 # numpy ndarray fast path
5655 if isinstance(value, np.ndarray):
5656 if value.shape == (4, 4):
5657 return [float(v) for v in value.ravel().tolist()]
5658 if value.shape == (16,):
5659 return [float(v) for v in value.tolist()]
5660 raise ValueError(
5661 f"Matrix ndarray must have shape (4,4) or (16,), got {value.shape}"
5662 )
5663 # Nested list/tuple of shape (4,4)
5664 if isinstance(value, (list, tuple)) and len(value) == 4 and \
5665 all(isinstance(row, (list, tuple)) and len(row) == 4 for row in value):
5666 flat = []
5667 for row in value:
5668 flat.extend(float(v) for v in row)
5669 return flat
5670 # Flat list/tuple of 16 floats
5671 if isinstance(value, (list, tuple)) and len(value) == 16:
5672 return [float(v) for v in value]
5673 raise ValueError(
5674 f"Matrix must be a (4,4) ndarray, (16,) ndarray, list of 16 floats, "
5675 f"or nested 4x4 list. Got: {type(value).__name__}"
5676 )
5677
5678 @staticmethod
5679 def _mat4_to_ndarray(flat: List[float]) -> 'np.ndarray':
5680 """Convert a flat list of 16 floats (row-major) to a (4,4) numpy ndarray."""
5681 return np.array(flat, dtype=np.float32).reshape((4, 4))
5682
5683 # ---- Transformation matrices ----
5684
5685 def getObjectTransformationMatrix(self, objID: int) -> 'np.ndarray':
5686 """Return the object's 4x4 transformation matrix as a (4,4) float32 ndarray.
5687
5688 Helios stores matrices in row-major order, so element (i, j) is at
5689 position [i, j] of the returned ndarray. The translation column is at
5690 positions [0, 3], [1, 3], [2, 3].
5691 """
5693 flat = context_wrapper.getObjectTransformationMatrixWrapper(self.context, int(objID))
5694 return self._mat4_to_ndarray(flat)
5695
5696 def setObjectTransformationMatrix(self, objIDs_or_objID, T) -> None:
5697 """Set the 4x4 transformation matrix on one or more compound objects.
5698
5699 Args:
5700 objIDs_or_objID: A single object ID (int) or a list/tuple of object IDs.
5701 T: A 4x4 matrix as numpy.ndarray((4,4) | (16,) float), list of 16 floats,
5702 or a nested 4x4 list. Row-major; T[i, j] is element (i, j).
5703 """
5705 flat = self._marshal_mat4(T)
5706 if isinstance(objIDs_or_objID, (list, tuple)):
5707 context_wrapper.setObjectTransformationMatrixBatchWrapper(
5708 self.context, list(objIDs_or_objID), flat
5709 )
5710 else:
5711 context_wrapper.setObjectTransformationMatrixWrapper(
5712 self.context, int(objIDs_or_objID), flat
5713 )
5714
5715 def getPrimitiveTransformationMatrix(self, uuid: int) -> 'np.ndarray':
5716 """Return the primitive's 4x4 transformation matrix as a (4,4) float32 ndarray
5717 (row-major; see getObjectTransformationMatrix for layout details)."""
5719 flat = context_wrapper.getPrimitiveTransformationMatrixWrapper(self.context, int(uuid))
5720 return self._mat4_to_ndarray(flat)
5721
5722 def setPrimitiveTransformationMatrix(self, uuids_or_uuid, T) -> None:
5723 """Set the 4x4 transformation matrix on one or more primitives.
5724
5725 Args:
5726 uuids_or_uuid: A single UUID (int) or a list/tuple of UUIDs.
5727 T: A 4x4 matrix; see setObjectTransformationMatrix for accepted formats.
5728 """
5730 flat = self._marshal_mat4(T)
5731 if isinstance(uuids_or_uuid, (list, tuple)):
5732 context_wrapper.setPrimitiveTransformationMatrixBatchWrapper(
5733 self.context, list(uuids_or_uuid), flat
5734 )
5735 else:
5736 context_wrapper.setPrimitiveTransformationMatrixWrapper(
5737 self.context, int(uuids_or_uuid), flat
5738 )
5739
5740 # ---- Domain bounds ----
5741
5742 def getDomainBoundingBox(self, uuids: Optional[List[int]] = None):
5743 """Return the axis-aligned bounding box of the domain (or a UUID subset).
5744
5745 Args:
5746 uuids: Optional list of primitive UUIDs to restrict the computation to.
5747 If None (default), uses every primitive in the context.
5748
5749 Returns:
5750 ``(xbounds, ybounds, zbounds)`` where each element is a ``vec2(min, max)``.
5751 """
5753 if uuids is None:
5754 xb, yb, zb = context_wrapper.getDomainBoundingBoxWrapper(self.context)
5755 else:
5756 if not isinstance(uuids, (list, tuple)):
5757 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
5758 xb, yb, zb = context_wrapper.getDomainBoundingBoxFilteredWrapper(self.context, list(uuids))
5759 return (vec2(xb[0], xb[1]), vec2(yb[0], yb[1]), vec2(zb[0], zb[1]))
5760
5761 def getDomainBoundingSphere(self, uuids: Optional[List[int]] = None):
5762 """Return the bounding sphere of the domain (or a UUID subset).
5764 Returns:
5765 ``(center, radius)`` where ``center`` is a ``vec3`` and ``radius`` is a float.
5766 """
5768 if uuids is None:
5769 center, radius = context_wrapper.getDomainBoundingSphereWrapper(self.context)
5770 else:
5771 if not isinstance(uuids, (list, tuple)):
5772 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
5773 center, radius = context_wrapper.getDomainBoundingSphereFilteredWrapper(self.context, list(uuids))
5774 return (vec3(center[0], center[1], center[2]), float(radius))
5775
5776 # =========================================================================
5777 # Tube/polymesh + object color/dirty/tile mutators
5778 # =========================================================================
5779
5780 # ---- Tube object mutators ----
5781
5782 def setTubeNodes(self, objID: int, nodes: List[vec3]) -> None:
5783 """Replace the node positions of an existing tube object."""
5785 if not isinstance(nodes, (list, tuple)):
5786 raise ValueError(f"nodes must be a list or tuple, got {type(nodes).__name__}")
5787 flat = []
5788 for i, n in enumerate(nodes):
5789 if not isinstance(n, vec3):
5790 raise ValueError(f"nodes[{i}] must be a vec3, got {type(n).__name__}")
5791 flat.extend([n.x, n.y, n.z])
5792 context_wrapper.setTubeNodesWrapper(self.context, int(objID), flat)
5793
5794 def setTubeRadii(self, objID: int, radii: List[float]) -> None:
5795 """Replace the per-node radii of an existing tube object."""
5797 if not isinstance(radii, (list, tuple)):
5798 raise ValueError(f"radii must be a list or tuple, got {type(radii).__name__}")
5799 context_wrapper.setTubeRadiiWrapper(self.context, int(objID), [float(r) for r in radii])
5800
5801 def scaleTubeGirth(self, objID: int, scale_factor: float) -> None:
5802 """Scale the radii of an existing tube object by ``scale_factor``."""
5804 context_wrapper.scaleTubeGirthWrapper(self.context, int(objID), float(scale_factor))
5805
5806 def scaleTubeLength(self, objID: int, scale_factor: float) -> None:
5807 """Scale the lengths between tube nodes by ``scale_factor``."""
5809 context_wrapper.scaleTubeLengthWrapper(self.context, int(objID), float(scale_factor))
5810
5811 def pruneTubeNodes(self, objID: int, node_index: int) -> None:
5812 """Remove all tube nodes from index ``node_index`` to the end."""
5814 context_wrapper.pruneTubeNodesWrapper(self.context, int(objID), int(node_index))
5815
5816 def appendTubeSegment(self, objID: int, node_position: vec3, radius: float, *,
5817 color: Optional[RGBcolor] = None,
5818 texture_file: Optional[str] = None,
5819 uv: Optional[vec2] = None) -> None:
5820 """Append a new segment to an existing tube object.
5821
5822 Pass exactly one of ``color`` (an RGBcolor) or both ``texture_file`` and
5823 ``uv`` (a vec2 of texture u-fractions) to specify how the new segment
5824 should be shaded.
5825 """
5827 if not isinstance(node_position, vec3):
5828 raise ValueError(f"node_position must be a vec3, got {type(node_position).__name__}")
5829 has_color = color is not None
5830 has_texture = texture_file is not None or uv is not None
5831 if has_color == has_texture:
5832 raise ValueError(
5833 "appendTubeSegment requires exactly one of (color) or "
5834 "(texture_file and uv); cannot mix or omit both."
5835 )
5836 if has_color:
5837 if not isinstance(color, RGBcolor):
5838 raise ValueError(f"color must be an RGBcolor, got {type(color).__name__}")
5839 context_wrapper.appendTubeSegmentColorWrapper(
5840 self.context, int(objID), node_position.to_list(), float(radius),
5841 [color.r, color.g, color.b]
5842 )
5843 else:
5844 if texture_file is None or uv is None:
5845 raise ValueError(
5846 "appendTubeSegment with texture requires both texture_file and uv."
5847 )
5848 if not isinstance(uv, vec2):
5849 raise ValueError(f"uv must be a vec2, got {type(uv).__name__}")
5850 tex_path = self._validate_file_path(
5851 texture_file, ['.png', '.jpg', '.jpeg', '.tga', '.bmp']
5852 )
5853 context_wrapper.appendTubeSegmentTextureWrapper(
5854 self.context, int(objID), node_position.to_list(), float(radius),
5855 tex_path, [uv.x, uv.y]
5856 )
5857
5858 # ---- Polymesh object ----
5859
5860 def addPolymeshObject(self, uuids: List[int]) -> int:
5861 """Group the given primitives into a new polymesh compound object and return its ID."""
5863 if not isinstance(uuids, (list, tuple)):
5864 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
5865 if len(uuids) == 0:
5866 raise ValueError("addPolymeshObject requires at least one UUID")
5867 return context_wrapper.addPolymeshObjectWrapper(self.context, list(uuids))
5868
5869 # ---- Object color ----
5870
5871 def setObjectColor(self, objIDs_or_objID, color) -> None:
5872 """Set the color of one or more compound objects.
5874 Accepts a single object ID or list/tuple of IDs. ``color`` must be an
5875 ``RGBcolor`` or ``RGBAcolor``.
5876 """
5878 if isinstance(color, RGBAcolor):
5879 comps = [color.r, color.g, color.b, color.a]
5880 if isinstance(objIDs_or_objID, (list, tuple)):
5881 context_wrapper.setObjectColorRGBABatchWrapper(self.context, list(objIDs_or_objID), comps)
5882 else:
5883 context_wrapper.setObjectColorRGBAWrapper(self.context, int(objIDs_or_objID), comps)
5884 elif isinstance(color, RGBcolor):
5885 comps = [color.r, color.g, color.b]
5886 if isinstance(objIDs_or_objID, (list, tuple)):
5887 context_wrapper.setObjectColorRGBBatchWrapper(self.context, list(objIDs_or_objID), comps)
5888 else:
5889 context_wrapper.setObjectColorRGBWrapper(self.context, int(objIDs_or_objID), comps)
5890 else:
5891 raise ValueError(
5892 f"color must be an RGBcolor or RGBAcolor, got {type(color).__name__}"
5893 )
5894
5895 def overrideObjectTextureColor(self, objIDs_or_objID) -> None:
5896 """Override the texture mapping with the object's vertex color."""
5898 if isinstance(objIDs_or_objID, (list, tuple)):
5899 context_wrapper.overrideObjectTextureColorBatchWrapper(self.context, list(objIDs_or_objID))
5900 else:
5901 context_wrapper.overrideObjectTextureColorWrapper(self.context, int(objIDs_or_objID))
5902
5903 def useObjectTextureColor(self, objIDs_or_objID) -> None:
5904 """Restore use of the texture color (undoes overrideObjectTextureColor)."""
5906 if isinstance(objIDs_or_objID, (list, tuple)):
5907 context_wrapper.useObjectTextureColorBatchWrapper(self.context, list(objIDs_or_objID))
5908 else:
5909 context_wrapper.useObjectTextureColorWrapper(self.context, int(objIDs_or_objID))
5910
5911 # ---- Mark dirty/clean ----
5912
5913 def markPrimitiveDirty(self, uuids_or_uuid) -> None:
5914 """Mark one or more primitives as dirty (geometry has been modified)."""
5916 if isinstance(uuids_or_uuid, (list, tuple)):
5917 context_wrapper.markPrimitiveDirtyBatchWrapper(self.context, list(uuids_or_uuid))
5918 else:
5919 context_wrapper.markPrimitiveDirtyWrapper(self.context, int(uuids_or_uuid))
5920
5921 def markPrimitiveClean(self, uuids_or_uuid) -> None:
5922 """Mark one or more primitives as clean (cancels dirty state)."""
5924 if isinstance(uuids_or_uuid, (list, tuple)):
5925 context_wrapper.markPrimitiveCleanBatchWrapper(self.context, list(uuids_or_uuid))
5926 else:
5927 context_wrapper.markPrimitiveCleanWrapper(self.context, int(uuids_or_uuid))
5928
5929 # ---- Tile subdivision ----
5930
5931 def setTileObjectSubdivisionCount(self, objIDs_or_objID, subdiv: int2) -> None:
5932 """Set the (Nx, Ny) subdivision count of one or more tile objects.
5933
5934 The Helios C++ API is batch-only; a single objID is wrapped as a
5935 single-element list.
5936 """
5938 if not isinstance(subdiv, int2):
5939 raise ValueError(f"subdiv must be an int2, got {type(subdiv).__name__}")
5940 if isinstance(objIDs_or_objID, (list, tuple)):
5941 ids = list(objIDs_or_objID)
5942 else:
5943 ids = [int(objIDs_or_objID)]
5944 context_wrapper.setTileObjectSubdivisionCountWrapper(
5945 self.context, ids, int(subdiv.x), int(subdiv.y)
5946 )
5947
5948 def setTileObjectSubdivisionByAreaRatio(self, objIDs_or_objID, area_ratio: float) -> None:
5949 """Set tile object subdivision dynamically based on a target area ratio.
5950
5951 ``area_ratio`` is the approximate ratio between the whole tile's area and an
5952 individual sub-patch's area, so each tile is subdivided into roughly
5953 ``area_ratio`` sub-patches. It must be >= 1 (a sub-patch cannot be larger than
5954 the tile). The tile's position, size, and orientation are preserved.
5955 """
5957 if area_ratio < 1:
5958 raise ValueError(
5959 f"area_ratio must be >= 1 (it is the ratio of the whole tile area to an "
5960 f"individual sub-patch area), got {area_ratio}"
5961 )
5962 if isinstance(objIDs_or_objID, (list, tuple)):
5963 ids = list(objIDs_or_objID)
5964 else:
5965 ids = [int(objIDs_or_objID)]
5966 context_wrapper.setTileObjectSubdivisionByAreaRatioWrapper(
5967 self.context, ids, float(area_ratio)
5968 )
5969
5970 # =========================================================================
5971 # Cleanup, XML write, RNG, Location
5972 # =========================================================================
5973
5974 # ---- Cleanup ----
5975
5976 def cleanDeletedUUIDs(self, uuids: List[int]) -> List[int]:
5977 """Return a new list with deleted UUIDs removed; the input list is not mutated.
5978
5979 This mirrors the convention used by ``cropDomain``, which returns the
5980 survivors rather than mutating in place.
5981 """
5983 if not isinstance(uuids, (list, tuple)):
5984 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
5985 return context_wrapper.cleanDeletedUUIDsWrapper(self.context, list(uuids))
5986
5987 def cleanDeletedObjectIDs(self, objIDs: List[int]) -> List[int]:
5988 """Return a new list with deleted object IDs removed; input is not mutated."""
5990 if not isinstance(objIDs, (list, tuple)):
5991 raise ValueError(f"objIDs must be a list or tuple, got {type(objIDs).__name__}")
5992 return context_wrapper.cleanDeletedObjectIDsWrapper(self.context, list(objIDs))
5994 # ---- XML write ----
5995
5996 def writeXML(self, filename: str, uuids: Optional[List[int]] = None, quiet: bool = False) -> None:
5997 """Write the context (or a UUID subset) to an XML file.
5998
5999 Args:
6000 filename: Output file path. Must end in .xml.
6001 uuids: Optional list of primitive UUIDs to restrict the export. If
6002 None (default), all primitives are written.
6003 quiet: Suppress informational console output.
6004 """
6006 path = self._validate_output_file_path(filename, ['.xml'])
6007 if uuids is None:
6008 context_wrapper.writeXMLWrapper(self.context, path, bool(quiet))
6009 else:
6010 if not isinstance(uuids, (list, tuple)):
6011 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
6012 context_wrapper.writeXMLFilteredWrapper(self.context, path, list(uuids), bool(quiet))
6013
6014 def writeXML_byobject(self, filename: str, objIDs: List[int], quiet: bool = False) -> None:
6015 """Write a subset of compound objects to an XML file."""
6017 path = self._validate_output_file_path(filename, ['.xml'])
6018 if not isinstance(objIDs, (list, tuple)):
6019 raise ValueError(f"objIDs must be a list or tuple, got {type(objIDs).__name__}")
6020 context_wrapper.writeXMLByObjectWrapper(self.context, path, list(objIDs), bool(quiet))
6021
6022 # ---- RNG ----
6023
6024 def randu(self, low=None, high=None):
6025 """Draw a uniform random number using the Context's RNG.
6026
6027 Three forms:
6028 ``randu()`` -> float in [0, 1)
6029 ``randu(low: float, high: float)`` -> float in [low, high)
6030 ``randu(low: int, high: int)`` -> int in [low, high]
6031
6032 Whether the integer or float overload is invoked is determined by
6033 ``isinstance(low, int)``; pass ``low/high`` as Python ints for the
6034 integer range form.
6035 """
6037 if low is None and high is None:
6038 return context_wrapper.randuBasicWrapper(self.context)
6039 if low is None or high is None:
6040 raise ValueError("randu requires both low and high, or neither.")
6041 if isinstance(low, bool) or isinstance(high, bool):
6042 raise ValueError("randu bounds cannot be bool.")
6043 # Treat the call as integer-range only when BOTH bounds are Python ints
6044 # (and not bools, handled above). Otherwise use the float form.
6045 if isinstance(low, int) and isinstance(high, int):
6046 return context_wrapper.randuIntRangeWrapper(self.context, low, high)
6047 return context_wrapper.randuRangeWrapper(self.context, float(low), float(high))
6048
6049 def randn(self, mean=None, stddev=None) -> float:
6050 """Draw a normal random number using the Context's RNG.
6051
6052 Two forms:
6053 ``randn()`` -> standard normal (mean 0, stddev 1)
6054 ``randn(mean: float, stddev: float)`` -> N(mean, stddev**2)
6055 """
6057 if mean is None and stddev is None:
6058 return context_wrapper.randnBasicWrapper(self.context)
6059 if mean is None or stddev is None:
6060 raise ValueError("randn requires both mean and stddev, or neither.")
6061 return context_wrapper.randnParamsWrapper(self.context, float(mean), float(stddev))
6062
6063 # ---- Location ----
6064
6065 def setLocation(self, location_or_lat, longitude=None, utc_offset=None, altitude=0.0) -> None:
6066 """Set the geographic location used by solar/radiation calculations.
6068 Two call forms:
6069 ``setLocation(loc: Location)``
6070 ``setLocation(latitude_deg: float, longitude_deg: float, utc_offset: float, altitude=0.0)``
6071
6072 ``altitude`` is the height of the local Cartesian origin in meters above
6073 sea level. It is only used in the (lat, lon, utc) float form; when passing
6074 a ``Location`` object, the location's own altitude is used.
6075 """
6077 if isinstance(location_or_lat, Location):
6078 if longitude is not None or utc_offset is not None or altitude != 0.0:
6079 raise ValueError("When passing a Location, do not also pass longitude/utc_offset/altitude; "
6080 "set them on the Location object instead.")
6081 loc = location_or_lat
6082 else:
6083 if longitude is None or utc_offset is None:
6084 raise ValueError(
6085 "setLocation requires either a Location object or "
6086 "(latitude_deg, longitude_deg, utc_offset) as 3 floats."
6088 loc = Location(float(location_or_lat), float(longitude), float(utc_offset), float(altitude))
6089 context_wrapper.setLocationWrapper(self.context, loc.latitude, loc.longitude, loc.utc_offset, loc.altitude)
6090
6091 def getLocation(self) -> Location:
6092 """Return the Context's currently-configured geographic location."""
6094 lat, lon, utc, alt = context_wrapper.getLocationWrapper(self.context)
6095 return Location(lat, lon, utc, alt)
6096
6097 # =========================================================================
6098 # Colormap helpers + texture transparency
6099 # =========================================================================
6100
6101 def generateColormap(self, name: str, n_colors: int) -> List[RGBcolor]:
6102 """Generate a colormap with ``n_colors`` entries from a named colormap.
6103
6104 Args:
6105 name: Helios colormap name (e.g., "hot", "cool", "lava", "rainbow").
6106 n_colors: Number of colors in the returned ramp.
6107
6108 Returns:
6109 A list of ``RGBcolor`` instances of length ``n_colors``.
6110 """
6112 flat = context_wrapper.generateColormapNamedWrapper(self.context, name, int(n_colors))
6113 return [RGBcolor(flat[i*3 + 0], flat[i*3 + 1], flat[i*3 + 2]) for i in range(int(n_colors))]
6114
6115 def generateTexturesFromColormap(self, texture_file: str, colormap: List[RGBcolor]) -> List[str]:
6116 """Generate one texture file per color in ``colormap`` derived from
6117 ``texture_file``. Returns the list of generated file paths.
6118 """
6120 if not isinstance(colormap, (list, tuple)):
6121 raise ValueError(f"colormap must be a list or tuple, got {type(colormap).__name__}")
6122 flat = []
6123 for i, c in enumerate(colormap):
6124 if not isinstance(c, RGBcolor):
6125 raise ValueError(f"colormap[{i}] must be an RGBcolor, got {type(c).__name__}")
6126 flat.extend([c.r, c.g, c.b])
6127 # Validate the input texture exists and looks like an image.
6128 validated_path = self._validate_file_path(
6129 texture_file, ['.png', '.jpg', '.jpeg', '.tga', '.bmp']
6131 return context_wrapper.generateTexturesFromColormapWrapper(
6132 self.context, validated_path, flat
6133 )
6134
6135 def getPrimitiveTextureTransparencyData(self, uuid: int) -> Optional['np.ndarray']:
6136 """Return the primitive's texture transparency mask as a 2D bool ndarray.
6137
6138 Returns None if the primitive has no associated transparency channel
6139 (e.g., it is untextured or its texture has no alpha). The returned
6140 ndarray has shape (height, width) and dtype ``bool``.
6141 """
6143 result = context_wrapper.getPrimitiveTextureTransparencyDataWrapper(self.context, int(uuid))
6144 if result is None:
6145 return None
6146 width, height, flat = result
6147 return np.array(flat, dtype=bool).reshape((height, width))
6148
6149
6150def check_context_alive(context: 'Context', owner_name: str) -> None:
6151 """Raise if `context`'s native Context has already been destroyed.
6152
6153 Plugin models pass ``context.getNativePtr()`` to a C++ constructor that
6154 stores the raw pointer for the lifetime of the model. Destroying the
6155 Context (via ``__exit__``, ``__del__``, or garbage collection) frees that
6156 memory without invalidating the model's copy, so any later call
6157 dereferences freed memory and segfaults.
6158
6159 Models must hold a Python reference to the owning Context (keeping it
6160 alive) and call this before every native call (turning an explicit close
6161 into an actionable error instead of a crash).
6162
6163 Args:
6164 context: The Context the model was constructed from.
6165 owner_name: Class name of the calling model, used in the message.
6166
6167 Raises:
6168 RuntimeError: If the Context has been destroyed.
6169 """
6170 if context is None or getattr(context, 'context', None) is None:
6171 raise RuntimeError(
6172 f"{owner_name} is bound to a Context that has already been destroyed.\n"
6173 "The native Context was freed while this model still referenced it; "
6174 "continuing would dereference freed memory and crash the interpreter.\n"
6175 "\n"
6176 "This usually means the model outlived its Context's 'with' block:\n"
6177 " with Context() as context:\n"
6178 f" model = {owner_name}(context)\n"
6179 " model.run() # <-- Context already destroyed here\n"
6180 "\n"
6181 f"Fix: keep all {owner_name} usage inside the Context's 'with' block, "
6182 "or create the Context without a 'with' statement so it lives as long "
6183 "as the model."
6184 )
6185
6186
Central simulation environment for PyHelios that manages 3D primitives and their data.
Definition Context.py:79
getDomainBoundingSphere(self, Optional[List[int]] uuids=None)
Return the bounding sphere of the domain (or a UUID subset).
Definition Context.py:5778
None setGlobalDataVec3(self, str label, x_or_vec, float y=None, float z=None)
Set global data as vec3.
Definition Context.py:4437
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.
Definition Context.py:1324
None scaleConeObjectGirth(self, int ObjID, float scale_factor)
Scale the girth of a Cone object by scaling the radii at both nodes.
Definition Context.py:2036
int getTubeObjectNodeCount(self, int objID)
Definition Context.py:4838
vec3 getBoxObjectSize(self, int objID)
Definition Context.py:4804
None duplicateObjectData(self, int objID, str old_label, str new_label)
Copy object data to a new label.
Definition Context.py:4388
str getMaterialTexture(self, str material_label)
Get the texture file path for a material.
Definition Context.py:3759
getMaterialColor(self, str material_label)
Get the RGBA color of a material.
Definition Context.py:3716
getObjectData(self, int objID, str label, type data_type=None)
Get object data with optional type specification.
Definition Context.py:4304
getAllPrimitiveVertices(self)
Get vertices for all primitives.
Definition Context.py:4088
None setObjectOrigin(self, int objID, vec3 origin)
Translate the object so its origin is moved to the given point.
Definition Context.py:5288
List[RGBcolor] getTubeObjectNodeColors(self, int objID)
Definition Context.py:4851
'np.ndarray' _mat4_to_ndarray(List[float] flat)
Convert a flat list of 16 floats (row-major) to a (4,4) numpy ndarray.
Definition Context.py:5692
Union[int, List[int]] copyObject(self, Union[int, List[int]] ObjID)
Copy one or more compound objects.
Definition Context.py:1633
List[str] listObjectData(self, int objID)
List all data labels on a specific object.
Definition Context.py:4380
None scaleTubeLength(self, int objID, float scale_factor)
Scale the lengths between tube nodes by scale_factor.
Definition Context.py:5819
int getMaterialTwosidedFlag(self, str material_label)
Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided).
Definition Context.py:3786
Optional[List[int]] cropDomain(self, *args)
Crop the context domain to the given XYZ bounds.
Definition Context.py:5030
None setMaterialDataVec3(self, str material_label, str data_label, vec3 value)
Set vec3 data on a material.
Definition Context.py:5404
None markPrimitiveDirty(self, uuids_or_uuid)
Mark one or more primitives as dirty (geometry has been modified).
Definition Context.py:5926
None deletePrimitive(self, Union[int, List[int]] uuids_or_uuid)
Delete one or more primitives from the context.
Definition Context.py:3563
None setMaterialDataInt4(self, str material_label, str data_label, int4 value)
Set int4 data on a material.
Definition Context.py:5432
None clearAllPrimitiveData(self, str label)
Remove a named data field from every primitive in the Context.
Definition Context.py:4992
None setMaterialDataInt3(self, str material_label, str data_label, int3 value)
Set int3 data on a material.
Definition Context.py:5425
_validate_uuid(self, int uuid)
Validate that a UUID exists in this context.
Definition Context.py:178
getGlobalData(self, str label, type data_type=None)
Get global data with optional type specification.
Definition Context.py:4477
List[int] getAllUUIDs(self)
Definition Context.py:627
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.
Definition Context.py:955
addTimeseriesData(self, str label, float value, 'Date' date, 'Time' time)
Add a data point to a timeseries variable.
Definition Context.py:3138
None setObjectColor(self, objIDs_or_objID, color)
Set the color of one or more compound objects.
Definition Context.py:5888
str _validate_output_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize output file path for security.
Definition Context.py:256
bool primitiveTextureHasTransparencyChannel(self, int uuid)
Check if primitive texture has a transparency channel.
Definition Context.py:4002
getPrimitiveMaterialLabel(self, uuid)
Get the material label assigned to a primitive or multiple primitives.
Definition Context.py:3845
None enablePrimitiveDataValueCaching(self, str label)
Enable value caching for the given primitive-data label.
Definition Context.py:5181
int getPrimitiveTwosidedFlag(self, int uuid, int default_value=1)
Get two-sided rendering flag for a primitive.
Definition Context.py:3869
None cropDomainX(self, vec2 xbounds)
Definition Context.py:5003
List[str] getAllPrimitiveTextureFiles(self)
Get texture files for all primitives.
Definition Context.py:4092
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.
Definition Context.py:4248
getDomainBoundingBox(self, Optional[List[int]] uuids=None)
Return the axis-aligned bounding box of the domain (or a UUID subset).
Definition Context.py:5763
bool isPrimitiveHidden(self, int uuid)
Check if a primitive is hidden.
Definition Context.py:4131
np.ndarray getPrimitiveDataArray(self, List[int] uuids, str label)
Get primitive data values for multiple primitives as a NumPy array.
Definition Context.py:2917
None setObjectAverageNormal(self, int objID, vec3 origin, vec3 new_normal)
Rotate the object so its area-weighted average normal aligns with new_normal.
Definition Context.py:5277
List[int] getObjectPrimitiveUUIDs(self, objIDs)
Get flattened primitive UUIDs for one object, a list of objects, or a list-of-lists.
Definition Context.py:4728
None writeXML_byobject(self, str filename, List[int] objIDs, bool quiet=False)
Write a subset of compound objects to an XML file.
Definition Context.py:6027
bool is_plugin_available(self, str plugin_name)
Check if a specific plugin is available.
Definition Context.py:3630
getPrimitiveColor(self, uuid)
Get the color of a primitive or multiple primitives.
Definition Context.py:595
getPrimitiveArea(self, uuid)
Get the area of a primitive or multiple primitives.
Definition Context.py:527
str getGlobalDataString(self, str label)
Get string global data.
Definition Context.py:4520
vec3 getTileObjectNormal(self, int objID)
Definition Context.py:4759
bool doesTimeseriesVariableExist(self, str label)
Check whether a timeseries variable exists.
Definition Context.py:3369
None setTubeNodes(self, int objID, List[vec3] nodes)
Replace the node positions of an existing tube object.
Definition Context.py:5795
List[int] getAllObjectIDs(self)
Definition Context.py:637
bool isPrimitiveDataValueCachingEnabled(self, str label)
Return True if value caching is enabled for the given primitive-data label.
Definition Context.py:5086
None setObjectDataUInt(self, objids_or_objid, str label, int value)
Set object data as unsigned 32-bit integer.
Definition Context.py:4180
deleteTimeseriesVariable(self, str label)
Delete a single timeseries variable and all of its data points.
Definition Context.py:3433
List[str] listAllObjectDataLabels(self)
List all object data labels in context.
Definition Context.py:4384
List[str] get_available_plugins(self)
Get list of available plugins for this PyHelios instance.
Definition Context.py:3618
__exit__(self, exc_type, exc_value, traceback)
Definition Context.py:289
int getPrimitiveParentObjectID(self, int uuid)
Return the ID of the compound object the primitive belongs to.
Definition Context.py:5144
None setPrimitiveNormal(self, uuids_or_uuid, vec3 origin, vec3 new_normal)
Rotate one or more primitives so their normals align with new_normal.
Definition Context.py:5333
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.
Definition Context.py:2751
None setTileObjectSubdivisionCount(self, objIDs_or_objID, int2 subdiv)
Set the (Nx, Ny) subdivision count of one or more tile objects.
Definition Context.py:5948
None setPrimitiveParentObjectID(self, uuids_or_uuid, int objID)
Reassign one or more primitives to belong to the given compound object.
Definition Context.py:5353
None hidePrimitive(self, uuids_or_uuid)
Hide one or more primitives.
Definition Context.py:4106
None clearMaterialData(self, str material_label, str data_label)
Clear the named data entry on the given material.
Definition Context.py:5219
int getTimeseriesLength(self, str label)
Get the number of data points in a timeseries variable.
Definition Context.py:3345
None setTriangleVertices(self, int uuid, vec3 vertex0, vec3 vertex1, vec3 vertex2)
Replace the three vertices of an existing triangle primitive.
Definition Context.py:5319
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.
Definition Context.py:1232
float getConeObjectVolume(self, int objID)
Definition Context.py:4896
setCurrentTimeseriesPoint(self, str label, int index)
Set the Context date and time from a timeseries data point index.
Definition Context.py:3202
calculatePrimitiveDataAreaWeightedMean(self, List[int] uuids, str label, type return_type=float)
Calculate area-weighted mean of primitive data.
Definition Context.py:4581
bool doesMaterialDataExist(self, str material_label, str data_label)
Return True if the named material has data stored under data_label.
Definition Context.py:5066
getTileObjectAreaRatio(self, objIDs)
Get tile-object area ratio for one or multiple tile objects.
Definition Context.py:4738
bool doesPrimitiveDataExist(self, int uuid, str label)
Check if primitive data exists for a specific primitive and label.
Definition Context.py:2850
None setMaterialDataVec2(self, str material_label, str data_label, vec2 value)
Set vec2 data on a material.
Definition Context.py:5397
int getObjectDataSize(self, int objID, str label)
Get the size of object data array.
Definition Context.py:4355
float getTubeObjectSegmentVolume(self, int objID, int segment_index)
Definition Context.py:4860
List[str] listMaterials(self)
Get list of all material labels in the context.
Definition Context.py:3687
None enableObjectDataValueCaching(self, str label)
Enable value caching for the given object-data label.
Definition Context.py:5192
None setMaterialDataInt2(self, str material_label, str data_label, int2 value)
Set int2 data on a material.
Definition Context.py:5418
List[int] cleanDeletedUUIDs(self, List[int] uuids)
Return a new list with deleted UUIDs removed; the input list is not mutated.
Definition Context.py:5993
None setPrimitiveDataInt(self, uuids_or_uuid, str label, int value)
Set primitive data as signed 32-bit integer for one or multiple primitives.
Definition Context.py:2543
None setPrimitiveDataDouble(self, uuids_or_uuid, str label, float value)
Set primitive data as 64-bit double for one or multiple primitives.
Definition Context.py:2599
None setPrimitiveDataVec2(self, uuids_or_uuid, str label, x_or_vec, float y=None)
Set primitive data as vec2 for one or multiple primitives.
Definition Context.py:2635
int getPrimitiveMaterialID(self, int uuid)
Return the material ID assigned to the given primitive.
Definition Context.py:5129
'np.ndarray' getAllPrimitiveColors(self)
Get colors for all primitives.
Definition Context.py:4072
int getPrimitiveCount(self)
Definition Context.py:607
vec3 getObjectAverageNormal(self, int objID)
Return the area-weighted average normal of all primitives in the object.
Definition Context.py:5270
Optional[ 'np.ndarray'] getPrimitiveTextureTransparencyData(self, int uuid)
Return the primitive's texture transparency mask as a 2D bool ndarray.
Definition Context.py:6153
None aggregatePrimitiveDataSum(self, List[int] uuids, List[str] labels, str result_label)
Sum multiple primitive data fields into a new field.
Definition Context.py:4651
List[str] listGlobalData(self)
List all global data labels.
Definition Context.py:4548
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.
Definition Context.py:1428
List[PrimitiveInfo] getAllPrimitiveInfo(self)
Get physical properties and geometry information for all primitives in the context.
Definition Context.py:702
float getMaterialDataDouble(self, str material_label, str data_label)
Definition Context.py:5452
None incrementPrimitiveData(self, List[int] uuids, str label, increment, str data_type=None)
Increment primitive data for the given UUIDs.
Definition Context.py:4629
None copyObjectData(self, int source_objID, int destination_objID)
Copy all object data from source to destination compound object.
Definition Context.py:1661
'Time' queryTimeseriesTime(self, str label, int index)
Get the Time associated with a timeseries data point.
Definition Context.py:3290
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.
Definition Context.py:2727
None showPrimitive(self, uuids_or_uuid)
Show one or more previously hidden primitives.
Definition Context.py:4117
None writePLY(self, str filename, Optional[List[int]] UUIDs=None)
Write geometry to a PLY (Stanford Polygon) file.
Definition Context.py:2193
List[int] filterPrimitivesByData(self, List[int] uuids, str label, value, str comparator="=")
Filter primitives by data value.
Definition Context.py:4670
List[PrimitiveInfo] getPrimitivesInfoForObject(self, int object_id)
Get physical properties and geometry information for all primitives belonging to a specific object.
Definition Context.py:715
vec3 getSphereObjectCenter(self, int objID)
Definition Context.py:4775
None setPrimitiveDataInt2(self, uuids_or_uuid, str label, x_or_vec, int y=None)
Set primitive data as int2 for one or multiple primitives.
Definition Context.py:2704
int getGlobalDataType(self, str label)
Get the HeliosDataType enum for global data.
Definition Context.py:4524
print_plugin_status(self)
Print detailed plugin status information.
Definition Context.py:3643
vec3 getTriangleVertex(self, int uuid, int number)
Definition Context.py:4912
float getSphereObjectVolume(self, int objID)
Definition Context.py:4794
List getUniquePrimitiveDataValues(self, str label, type dtype)
Return the unique values stored under label across all primitives.
Definition Context.py:5617
setDate(self, int year, int month, int day)
Set the simulation date.
Definition Context.py:3065
List[int] getDirtyUUIDs(self, bool include_deleted=True)
Return the list of UUIDs whose geometry has been modified since the last markGeometryClean call.
Definition Context.py:5245
bool objectHasTexture(self, int objID)
Return True if the compound object has a texture assigned.
Definition Context.py:5071
int2 getPrimitiveTextureSize(self, int uuid)
Get the texture size (width, height) of a primitive.
Definition Context.py:3967
vec3 getSphereObjectRadius(self, int objID)
Get per-axis radii of a sphere object.
Definition Context.py:4785
List[vec3] getTileObjectVertices(self, int objID)
Definition Context.py:4769
int getMaterialIDFromLabel(self, str material_label)
Look up a material ID from its human-readable label.
Definition Context.py:5124
getPrimitiveData(self, int uuid, str label, type data_type=None)
Get primitive data for a specific primitive.
Definition Context.py:2777
None setGlobalDataUInt(self, str label, int value)
Set global data as unsigned 32-bit integer.
Definition Context.py:4413
None setTileObjectSubdivisionByAreaRatio(self, objIDs_or_objID, float area_ratio)
Set tile object subdivision dynamically based on a target area ratio.
Definition Context.py:5967
List[float] getConeObjectNodeRadii(self, int objID)
Definition Context.py:4874
List[str] get_missing_plugins(self, List[str] requested_plugins)
Get list of requested plugins that are not available.
Definition Context.py:3655
None setMaterialData(self, str material_label, str data_label, value)
Set material data with type detection from the Python value.
Definition Context.py:5507
None showObject(self, objids_or_objid)
Show one or more previously hidden compound objects.
Definition Context.py:4150
List[vec3] getConeObjectNodes(self, int objID)
Definition Context.py:4869
None setObjectDataInt(self, objids_or_objid, str label, int value)
Set object data as signed 32-bit integer.
Definition Context.py:4170
bool isGeometryDirty(self)
Definition Context.py:327
None printObjectInfo(self, int objID)
Print summary info for the object to stdout (for debugging).
Definition Context.py:5170
clearTimeseriesData(self)
Clear all timeseries data from the Context.
Definition Context.py:3407
vec3 getVoxelCenter(self, int uuid)
Definition Context.py:4917
None copyPrimitiveData(self, int sourceUUID, int destinationUUID)
Copy all primitive data from source to destination primitive.
Definition Context.py:1601
List[str] getAllPrimitiveMaterialLabels(self)
Get material labels for all primitives.
Definition Context.py:4096
None duplicateGlobalData(self, str old_label, str new_label)
Duplicate global data to a new label.
Definition Context.py:4544
vec4 getMaterialDataVec4(self, str material_label, str data_label)
Definition Context.py:5470
None setMaterialDataVec4(self, str material_label, str data_label, vec4 value)
Set vec4 data on a material.
Definition Context.py:5411
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.
Definition Context.py:3022
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
Definition Context.py:296
'np.ndarray' getPrimitiveTransformationMatrix(self, int uuid)
Return the primitive's 4x4 transformation matrix as a (4,4) float32 ndarray (row-major; see getObject...
Definition Context.py:5729
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.
Definition Context.py:749
vec3 getDiskObjectCenter(self, int objID)
Definition Context.py:4819
None renamePrimitiveData(self, int uuid, str old_label, str new_label)
Rename a primitive-data label on a single primitive.
Definition Context.py:5214
None incrementGlobalData(self, str label, increment)
Increment global data.
Definition Context.py:4552
getObjectBoundingBox(self, objIDs)
Get axis-aligned bounding box for one object or a list of objects.
Definition Context.py:4712
None setGlobalDataInt4(self, str label, x_or_vec, int y=None, int z=None, int w=None)
Set global data as int4.
Definition Context.py:4469
None setObjectDataVec3(self, objids_or_objid, str label, x_or_vec, float y=None, float z=None)
Set object data as vec3.
Definition Context.py:4234
_check_context_available(self)
Helper method to check if context is available with detailed error messages.
Definition Context.py:134
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.
Definition Context.py:3518
None markPrimitiveClean(self, uuids_or_uuid)
Mark one or more primitives as clean (cancels dirty state).
Definition Context.py:5934
vec3 getConeObjectNode(self, int objID, int number)
Definition Context.py:4878
int3 getMaterialDataInt3(self, str material_label, str data_label)
Definition Context.py:5480
None clearGlobalData(self, str label)
Clear global data.
Definition Context.py:4536
vec3 getConeObjectAxisUnitVector(self, int objID)
Definition Context.py:4887
None scaleTubeGirth(self, int objID, float scale_factor)
Scale the radii of an existing tube object by scale_factor.
Definition Context.py:5814
None setObjectDataVec2(self, objids_or_objid, str label, x_or_vec, float y=None)
Set object data as vec2.
Definition Context.py:4220
int4 getMaterialDataInt4(self, str material_label, str data_label)
Definition Context.py:5485
int addPatch(self, vec3 center=vec3(0, 0, 0), vec2 size=vec2(1, 1), Optional[SphericalCoord] rotation=None, Optional[RGBcolor] color=None)
Definition Context.py:346
None setTubeRadii(self, int objID, List[float] radii)
Replace the per-node radii of an existing tube object.
Definition Context.py:5807
addMaterial(self, str material_label)
Create a new material for sharing visual properties across primitives.
Definition Context.py:3679
vec2 getTileObjectSize(self, int objID)
Definition Context.py:4749
int getObjectType(self, int objID)
Return the integer-coded helios::ObjectType of a compound object.
Definition Context.py:4687
None disableObjectDataValueCaching(self, str label)
Disable value caching for the given object-data label.
Definition Context.py:5197
bool isMaterialTextureColorOverridden(self, str material_label)
Check if material texture color is overridden by material color.
Definition Context.py:3778
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.
Definition Context.py:2120
bool isPrimitiveDirty(self, int uuid)
Return True if the primitive's geometry has been modified since the last clean mark.
Definition Context.py:5076
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.
Definition Context.py:2682
None setObjectDataInt2(self, objids_or_objid, str label, x_or_vec, int y=None)
Set object data as int2.
Definition Context.py:4262
None setPrimitiveDataFloat(self, uuids_or_uuid, str label, float value)
Set primitive data as 32-bit float for one or multiple primitives.
Definition Context.py:2581
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.
Definition Context.py:1467
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.
Definition Context.py:876
vec2 getDiskObjectSize(self, int objID)
Definition Context.py:4824
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.
Definition Context.py:1160
List[str] listAllPrimitiveDataLabels(self)
Return the union of all primitive-data labels used across every primitive in the context.
Definition Context.py:5158
str _validate_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize file path for security.
Definition Context.py:213
int getJulianDate(self)
Get the current simulation date as Julian day (1-366).
Definition Context.py:5099
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.
Definition Context.py:1032
None setGlobalDataFloat(self, str label, float value)
Set global data as 32-bit float.
Definition Context.py:4417
'np.ndarray' getAllPrimitiveTypes(self)
Get types for all primitives.
Definition Context.py:4080
float queryTimeseriesData(self, str label, 'Date' date=None, 'Time' time=None, int index=None)
Query a timeseries data value.
Definition Context.py:3241
int getConeObjectSubdivisionCount(self, int objID)
Definition Context.py:4865
getPrimitiveVertices(self, uuid)
Get vertices of a primitive or multiple primitives.
Definition Context.py:569
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.
Definition Context.py:5837
None writeXML(self, str filename, Optional[List[int]] uuids=None, bool quiet=False)
Write the context (or a UUID subset) to an XML file.
Definition Context.py:6016
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).
Definition Context.py:5308
assignMaterialToPrimitive(self, uuid, str material_label)
Assign a material to primitive(s).
Definition Context.py:3807
randu(self, low=None, high=None)
Draw a uniform random number using the Context's RNG.
Definition Context.py:6047
None scalePrimitiveData(self, uuids_or_label, label_or_factor, factor=None)
Scale primitive data by a factor.
Definition Context.py:4609
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.
Definition Context.py:5259
bool doesObjectDataExist(self, int objID, str label)
Check if object data exists.
Definition Context.py:4359
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.
Definition Context.py:1938
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.
Definition Context.py:5546
bool isPrimitiveTextureColorOverridden(self, int uuid)
Check if primitive texture color is overridden.
Definition Context.py:4059
List[int] getDeletedUUIDs(self)
Return the list of UUIDs that have been deleted from the context.
Definition Context.py:5234
None clearPrimitiveData(self, uuids, str label)
Remove a named data field from one primitive or a list of primitives.
Definition Context.py:4979
float getMaterialDataFloat(self, str material_label, str data_label)
Definition Context.py:5448
float sumPrimitiveSurfaceArea(self, List[int] uuids)
Calculate total one-sided surface area for a set of primitives.
Definition Context.py:4659
int3 getBoxObjectSubdivisionCount(self, int objID)
Definition Context.py:4809
vec2 getPatchSize(self, int uuid)
Definition Context.py:4907
None disablePrimitiveDataValueCaching(self, str label)
Disable value caching for the given primitive-data label.
Definition Context.py:5186
bool doesObjectExist(self, int objID)
Return True if a compound object with the given ID exists.
Definition Context.py:5056
int getPatchCount(self, bool include_hidden=True)
Definition Context.py:4927
seedRandomGenerator(self, int seed)
Seed the random number generator for reproducible stochastic results.
Definition Context.py:341
None deleteObject(self, Union[int, List[int]] objIDs_or_objID)
Delete one or more compound objects from the context.
Definition Context.py:3598
None translatePrimitive(self, Union[int, List[int]] UUID, vec3 shift)
Translate one or more primitives by a shift vector.
Definition Context.py:1689
None setGlobalDataInt3(self, str label, x_or_vec, int y=None, int z=None)
Set global data as int3.
Definition Context.py:4461
Union[int, List[int]] copyPrimitive(self, Union[int, List[int]] UUID)
Copy one or more primitives.
Definition Context.py:1573
None setMaterialDataFloat(self, str material_label, str data_label, float value)
Set float data on a material.
Definition Context.py:5382
None printPrimitiveInfo(self, int uuid)
Print summary info for the primitive to stdout (for debugging).
Definition Context.py:5175
setMaterialTwosidedFlag(self, str material_label, int twosided_flag)
Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided).
Definition Context.py:3790
None setObjectDataString(self, objids_or_objid, str label, str value)
Set object data as string.
Definition Context.py:4210
None setPrimitiveDataString(self, uuids_or_uuid, str label, str value)
Set primitive data as string for one or multiple primitives.
Definition Context.py:2617
None setPrimitiveColor(self, uuids, color)
Set the RGB or RGBA color of one primitive or a list of primitives.
Definition Context.py:4959
None setGlobalDataDouble(self, str label, float value)
Set global data as 64-bit double.
Definition Context.py:4421
None renameMaterial(self, str old_label, str new_label)
Rename an existing material.
Definition Context.py:5209
float getTubeObjectVolume(self, int objID)
Definition Context.py:4856
None setMaterialDataInt(self, str material_label, str data_label, int value)
Set int data on a material.
Definition Context.py:5372
vec2 getMaterialDataVec2(self, str material_label, str data_label)
Definition Context.py:5460
List[vec3] getTubeObjectNodes(self, int objID)
Definition Context.py:4842
None overrideObjectTextureColor(self, objIDs_or_objID)
Override the texture mapping with the object's vertex color.
Definition Context.py:5908
vec3 getMaterialDataVec3(self, str material_label, str data_label)
Definition Context.py:5465
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.
Definition Context.py:1819
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.
Definition Context.py:2303
float getConeObjectLength(self, int objID)
Definition Context.py:4892
None setMaterialDataDouble(self, str material_label, str data_label, float value)
Set double-precision float data on a material.
Definition Context.py:5387
None rotatePrimitive(self, Union[int, List[int]] UUID, float angle, Union[str, vec3] axis, Optional[vec3] origin=None)
Rotate one or more primitives.
Definition Context.py:1750
List[str] getLoadedXMLFiles(self)
Return the list of XML file paths that have been loaded into this context.
Definition Context.py:5163
vec3 getBoxObjectCenter(self, int objID)
Definition Context.py:4799
updateTimeseriesData(self, str label, 'Date' date, 'Time' time, float new_value)
Update the value of an existing timeseries data point.
Definition Context.py:3172
calculatePrimitiveDataSum(self, List[int] uuids, str label, type return_type=float)
Calculate sum of primitive data across UUIDs.
Definition Context.py:4588
getPrimitiveTextureUV(self, uuid)
Get the texture UV coordinates of a primitive or multiple primitives.
Definition Context.py:3980
PrimitiveInfo getPrimitiveInfo(self, int uuid)
Get physical properties and geometry information for a single primitive.
Definition Context.py:652
int getMaterialDataType(self, str material_label, str data_label)
Return the HeliosDataType enum value for the given material data entry.
Definition Context.py:5495
None setObjectDataInt3(self, objids_or_objid, str label, x_or_vec, int y=None, int z=None)
Set object data as int3.
Definition Context.py:4276
int getPrimitiveDataSize(self, int uuid, str label)
Get the size/length of primitive data (for vector data).
Definition Context.py:2889
int getGlobalDataSize(self, str label)
Get the size of global data array.
Definition Context.py:4528
getPrimitiveBoundingBox(self, uuids)
Get axis-aligned bounding box for one primitive or a list of primitives.
Definition Context.py:4943
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.
Definition Context.py:1384
int addPolymeshObject(self, List[int] uuids)
Group the given primitives into a new polymesh compound object and return its ID.
Definition Context.py:5873
None setGlobalDataString(self, str label, str value)
Set global data as string.
Definition Context.py:4425
None cropDomainY(self, vec2 ybounds)
Definition Context.py:5009
vec3 getObjectCenter(self, int objID)
Definition Context.py:4691
vec3 getPatchCenter(self, int uuid)
Definition Context.py:4902
int2 getTileObjectSubdivisionCount(self, int objID)
Definition Context.py:4754
None hideObject(self, objids_or_objid)
Hide one or more compound objects (and all their primitives).
Definition Context.py:4139
None setObjectDataFloat(self, objids_or_objid, str label, float value)
Set object data as 32-bit float.
Definition Context.py:4190
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.
Definition Context.py:476
getPrimitiveType(self, uuid)
Get the type of a primitive or multiple primitives.
Definition Context.py:507
int getPrimitiveDataType(self, int uuid, str label)
Get the Helios data type of primitive data.
Definition Context.py:2876
calculatePrimitiveDataAreaWeightedSum(self, List[int] uuids, str label, type return_type=float)
Calculate area-weighted sum of primitive data.
Definition Context.py:4597
bool doesObjectContainPrimitive(self, int objID, int uuid)
Return True if the given primitive UUID belongs to the given object.
Definition Context.py:5061
str getObjectDataString(self, int objID, str label)
Get string object data.
Definition Context.py:4347
None cropDomainZ(self, vec2 zbounds)
Definition Context.py:5015
None setMaterialDataString(self, str material_label, str data_label, str value)
Set string data on a material.
Definition Context.py:5392
None setPrimitiveDataUInt(self, uuids_or_uuid, str label, int value)
Set primitive data as unsigned 32-bit integer for one or multiple primitives.
Definition Context.py:2563
int getTubeObjectSubdivisionCount(self, int objID)
Definition Context.py:4834
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.
Definition Context.py:1513
float getObjectDataFloat(self, int objID, str label)
Get float object data.
Definition Context.py:4339
calculatePrimitiveDataMean(self, List[int] uuids, str label, type return_type=float)
Calculate arithmetic mean of primitive data across UUIDs.
Definition Context.py:4569
int getMaterialCount(self)
Return the total number of materials registered in the context.
Definition Context.py:5104
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).
Definition Context.py:5298
Location getLocation(self)
Return the Context's currently-configured geographic location.
Definition Context.py:6104
int getMaterialDataUInt(self, str material_label, str data_label)
Definition Context.py:5444
int getObjectDataInt(self, int objID, str label)
Get int object data.
Definition Context.py:4343
List[vec2] getTileObjectTextureUV(self, int objID)
Definition Context.py:4764
None clearAllObjectData(self, str label)
Remove a named data field from every compound object in the Context.
Definition Context.py:4375
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.
Definition Context.py:2063
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.
Definition Context.py:2237
str getMaterialDataString(self, str material_label, str data_label)
Definition Context.py:5456
None renameObjectData(self, int objID, str old_label, str new_label)
Rename an object data label.
Definition Context.py:4392
None overridePrimitiveTextureColor(self, uuids_or_uuid)
Override texture color with the primitive's constant RGB color.
Definition Context.py:4031
None translateObject(self, Union[int, List[int]] ObjID, vec3 shift)
Translate one or more compound objects by a shift vector.
Definition Context.py:1722
float getPrimitiveDataFloat(self, int uuid, str label)
Convenience method to get float primitive data.
Definition Context.py:2863
int2 getMaterialDataInt2(self, str material_label, str data_label)
Definition Context.py:5475
int getGlobalDataVersion(self, str label)
Return the version counter for a global data entry.
Definition Context.py:5135
None setGlobalDataVec2(self, str label, x_or_vec, float y=None)
Set global data as vec2.
Definition Context.py:4429
deleteMaterial(self, str material_label)
Delete a material from the context.
Definition Context.py:3701
dict get_plugin_capabilities(self)
Get detailed information about available plugin capabilities.
Definition Context.py:3639
'np.ndarray' getObjectTransformationMatrix(self, int objID)
Return the object's 4x4 transformation matrix as a (4,4) float32 ndarray.
Definition Context.py:5703
vec3 getVoxelSize(self, int uuid)
Definition Context.py:4922
getTime(self)
Get the current simulation time.
Definition Context.py:3098
getPrimitiveNormal(self, uuid)
Get the normal vector of a primitive or multiple primitives.
Definition Context.py:546
int getObjectPrimitiveCount(self, int objID)
Return the number of primitives currently belonging to the object.
Definition Context.py:5114
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.
Definition Context.py:385
None scalePrimitive(self, Union[int, List[int]] UUID, vec3 scale, Optional[vec3] point=None)
Scale one or more primitives.
Definition Context.py:1892
List[int] filterObjectsByData(self, List[int] objIDs, str label, value, str comparator="=")
Filter objects by data value.
Definition Context.py:4396
List[int] cleanDeletedObjectIDs(self, List[int] objIDs)
Return a new list with deleted object IDs removed; input is not mutated.
Definition Context.py:6000
List[str] generateTexturesFromColormap(self, str texture_file, List[RGBcolor] colormap)
Generate one texture file per color in colormap derived from texture_file.
Definition Context.py:6130
List[int] getPrimitivesUsingMaterial(self, str material_label)
Get all primitive UUIDs that use a specific material.
Definition Context.py:3884
float getPolymeshObjectVolume(self, int objID)
Return the enclosed volume of a polymesh object.
Definition Context.py:5119
setDateJulian(self, int julian_day, int year)
Set the simulation date using Julian day number.
Definition Context.py:3082
List[float] getTubeObjectNodeRadii(self, int objID)
Definition Context.py:4847
None clearObjectData(self, objids_or_objid, str label)
Clear object data.
Definition Context.py:4363
int getTriangleCount(self, bool include_hidden=True)
Definition Context.py:4931
float getConeObjectNodeRadius(self, int objID, int number)
Definition Context.py:4883
None pruneTubeNodes(self, int objID, int node_index)
Remove all tube nodes from index node_index to the end.
Definition Context.py:5824
deleteTimeseriesDataPoint(self, 'Date' date, 'Time' time, Optional[str] label=None)
Delete a single timeseries data point at the given date and time.
Definition Context.py:3462
int getObjectDataType(self, int objID, str label)
Get the HeliosDataType enum for object data.
Definition Context.py:4351
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.
Definition Context.py:4290
setMaterialTextureColorOverride(self, str material_label, bool override)
Set whether material color overrides texture color.
Definition Context.py:3782
int addTriangle(self, vec3 vertex0, vec3 vertex1, vec3 vertex2, Optional[RGBcolor] color=None)
Add a triangle primitive to the context.
Definition Context.py:433
getDate(self)
Get the current simulation date.
Definition Context.py:3114
None useObjectTextureColor(self, objIDs_or_objID)
Restore use of the texture color (undoes overrideObjectTextureColor).
Definition Context.py:5916
None setPrimitiveTransformationMatrix(self, uuids_or_uuid, T)
Set the 4x4 transformation matrix on one or more primitives.
Definition Context.py:5740
'np.ndarray' getAllPrimitiveSolidFractions(self)
Get solid fractions for all primitives.
Definition Context.py:4084
List getUniqueObjectDataValues(self, str label, type dtype)
Return the unique values stored under label across all compound objects.
Definition Context.py:5636
getPrimitiveTextureFile(self, uuid)
Get the texture file path of a primitive or multiple primitives.
Definition Context.py:3899
None setGlobalDataInt2(self, str label, x_or_vec, int y=None)
Set global data as int2.
Definition Context.py:4453
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.
Definition Context.py:2436
str getObjectTextureFile(self, int objID)
Return the filesystem path of the texture assigned to the object, or an empty string if no texture is...
Definition Context.py:5152
None usePrimitiveTextureColor(self, uuids_or_uuid)
Use texture-map color instead of the constant RGB color.
Definition Context.py:4044
float getBoxObjectVolume(self, int objID)
Definition Context.py:4814
bool doesPrimitiveExist(self, uuid)
Check if a primitive exists for a given UUID or list of UUIDs.
Definition Context.py:620
float getGlobalDataFloat(self, str label)
Get float global data.
Definition Context.py:4512
bool areObjectPrimitivesComplete(self, int objID)
Return True if all primitives originally belonging to this object still exist (i.e....
Definition Context.py:5092
None setGlobalDataVec4(self, str label, x_or_vec, float y=None, float z=None, float w=None)
Set global data as vec4.
Definition Context.py:4445
List[int] loadXML(self, str filename, bool quiet=False)
Load geometry from a Helios XML file.
Definition Context.py:2169
float randn(self, mean=None, stddev=None)
Draw a normal random number using the Context's RNG.
Definition Context.py:6067
None setGlobalDataInt(self, str label, int value)
Set global data as signed 32-bit integer.
Definition Context.py:4409
setMaterialColor(self, str material_label, color)
Set the RGBA color of a material.
Definition Context.py:3738
None setPrimitiveTextureFile(self, int uuid, str texture_file)
Set the texture file path of a primitive.
Definition Context.py:3955
None aggregatePrimitiveDataProduct(self, List[int] uuids, List[str] labels, str result_label)
Multiply multiple primitive data fields into a new field.
Definition Context.py:4655
bool doesMaterialExist(self, str material_label)
Check if a material with the given label exists.
Definition Context.py:3683
None scaleConeObjectLength(self, int ObjID, float scale_factor)
Scale the length of a Cone object by scaling the distance between its two nodes.
Definition Context.py:2007
None setObjectDataDouble(self, objids_or_objid, str label, float value)
Set object data as 64-bit double.
Definition Context.py:4200
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.
Definition Context.py:816
int getDiskObjectSubdivisionCount(self, int objID)
Definition Context.py:4829
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).
Definition Context.py:2344
setTime(self, int hour, int minute=0, int second=0)
Set the simulation time.
Definition Context.py:3047
getPrimitiveSolidFraction(self, uuid)
Get the solid fraction of a primitive or multiple primitives.
Definition Context.py:4014
packGPUBuffers(self, uuids)
Pack GPU-ready geometry buffers for a set of primitives in a single C++ pass.
Definition Context.py:3943
None setMaterialDataUInt(self, str material_label, str data_label, int value)
Set unsigned int data on a material.
Definition Context.py:5377
None setObjectTransformationMatrix(self, objIDs_or_objID, T)
Set the 4x4 transformation matrix on one or more compound objects.
Definition Context.py:5715
float getObjectArea(self, int objID)
Return the total surface area (one-sided) of all primitives in the object.
Definition Context.py:5109
assignMaterialToObject(self, objID, str material_label)
Assign a material to all primitives in compound object(s).
Definition Context.py:3828
int getGlobalDataInt(self, str label)
Get int global data.
Definition Context.py:4516
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...
Definition Context.py:5204
'Date' queryTimeseriesDate(self, str label, int index)
Get the Date associated with a timeseries data point.
Definition Context.py:3318
'np.ndarray' getAllPrimitiveAreas(self)
Get areas for all primitives.
Definition Context.py:4076
List[float] _marshal_mat4(value)
Coerce a 4x4 transformation matrix input into a flat list of 16 floats.
Definition Context.py:5663
None setLocation(self, location_or_lat, longitude=None, utc_offset=None, altitude=0.0)
Set the geographic location used by solar/radiation calculations.
Definition Context.py:6087
'np.ndarray' getAllPrimitiveNormals(self)
Get normals for all primitives.
Definition Context.py:4068
bool isObjectHidden(self, int objID)
Check if a compound object is hidden.
Definition Context.py:4164
resolveMaterialTextures(self, uuids, colors_np)
Resolve material texture suppression for export.
Definition Context.py:3925
None renameGlobalData(self, str old_label, str new_label)
Rename a global data label.
Definition Context.py:4540
bool isObjectDataValueCachingEnabled(self, str label)
Return True if value caching is enabled for the given object-data label.
Definition Context.py:5081
List[str] listTimeseriesVariables(self)
List all existing timeseries variables.
Definition Context.py:3390
List[str] listPrimitiveData(self, int uuid)
List all data labels attached to a primitive.
Definition Context.py:4997
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.
Definition Context.py:2658
List[RGBcolor] generateColormap(self, str name, int n_colors)
Generate a colormap with n_colors entries from a named colormap.
Definition Context.py:6122
bool doesGlobalDataExist(self, str label)
Check if global data exists.
Definition Context.py:4532
int getMaterialDataInt(self, str material_label, str data_label)
Definition Context.py:5440
vec3 getTileObjectCenter(self, int objID)
Definition Context.py:4744
setMaterialTexture(self, str material_label, str texture_file)
Set the texture file for a material.
Definition Context.py:3774
int getSphereObjectSubdivisionCount(self, int objID)
Definition Context.py:4790
Physical properties and geometry information for a primitive.
Definition Context.py:24
__post_init__(self)
Calculate centroid from vertices if not provided.
Definition Context.py:37
Helios Date structure for representing date values.
Definition DataTypes.py:744
Geographic location for solar position and radiation calculations.
Definition DataTypes.py:859
Helios primitive type enumeration.
Definition DataTypes.py:8
Helios Time structure for representing time values.
Definition DataTypes.py:672
None check_context_alive('Context' context, str owner_name)
Raise if context's native Context has already been destroyed.
Definition Context.py:6181