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