0.1.19
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
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 value (broadcast to all UUIDs if list provided)
2507 """
2508 if isinstance(uuids_or_uuid, (list, tuple)):
2509 context_wrapper.setBroadcastPrimitiveDataInt(self.context, uuids_or_uuid, label, value)
2510 else:
2511 context_wrapper.setPrimitiveDataInt(self.context, uuids_or_uuid, label, value)
2512
2513 def setPrimitiveDataUInt(self, uuids_or_uuid, label: str, value: int) -> None:
2514 """
2515 Set primitive data as unsigned 32-bit integer for one or multiple primitives.
2516
2517 Critical for properties like 'twosided_flag' which must be uint in C++.
2518
2519 Args:
2520 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2521 label: String key for the data
2522 value: Unsigned integer value (broadcast to all UUIDs if list provided)
2523 """
2524 if isinstance(uuids_or_uuid, (list, tuple)):
2525 context_wrapper.setBroadcastPrimitiveDataUInt(self.context, uuids_or_uuid, label, value)
2526 else:
2527 context_wrapper.setPrimitiveDataUInt(self.context, uuids_or_uuid, label, value)
2528
2529 def setPrimitiveDataFloat(self, uuids_or_uuid, label: str, value: float) -> None:
2530 """
2531 Set primitive data as 32-bit float for one or multiple primitives.
2532
2533 Args:
2534 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2535 label: String key for the data
2536 value: Float value (broadcast to all UUIDs if list provided)
2537 """
2538 if isinstance(uuids_or_uuid, (list, tuple)):
2539 context_wrapper.setBroadcastPrimitiveDataFloat(self.context, uuids_or_uuid, label, value)
2540 else:
2541 context_wrapper.setPrimitiveDataFloat(self.context, uuids_or_uuid, label, value)
2542
2543 def setPrimitiveDataDouble(self, uuids_or_uuid, label: str, value: float) -> None:
2544 """
2545 Set primitive data as 64-bit double for one or multiple primitives.
2546
2547 Args:
2548 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2549 label: String key for the data
2550 value: Double value (broadcast to all UUIDs if list provided)
2551 """
2552 if isinstance(uuids_or_uuid, (list, tuple)):
2553 context_wrapper.setBroadcastPrimitiveDataDouble(self.context, uuids_or_uuid, label, value)
2554 else:
2555 context_wrapper.setPrimitiveDataDouble(self.context, uuids_or_uuid, label, value)
2556
2557 def setPrimitiveDataString(self, uuids_or_uuid, label: str, value: str) -> None:
2558 """
2559 Set primitive data as string for one or multiple primitives.
2560
2561 Args:
2562 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2563 label: String key for the data
2564 value: String value (broadcast to all UUIDs if list provided)
2565 """
2566 if isinstance(uuids_or_uuid, (list, tuple)):
2567 context_wrapper.setBroadcastPrimitiveDataString(self.context, uuids_or_uuid, label, value)
2568 else:
2569 context_wrapper.setPrimitiveDataString(self.context, uuids_or_uuid, label, value)
2570
2571 def setPrimitiveDataVec2(self, uuids_or_uuid, label: str, x_or_vec, y: float = None) -> None:
2572 """
2573 Set primitive data as vec2 for one or multiple primitives.
2574
2575 Args:
2576 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2577 label: String key for the data
2578 x_or_vec: Either x component (float) or vec2 object
2579 y: Y component (if x_or_vec is float)
2580 """
2581 if hasattr(x_or_vec, 'x'):
2582 x, y = x_or_vec.x, x_or_vec.y
2583 else:
2584 x = x_or_vec
2585 if isinstance(uuids_or_uuid, (list, tuple)):
2586 context_wrapper.setBroadcastPrimitiveDataVec2(self.context, uuids_or_uuid, label, x, y)
2587 else:
2588 context_wrapper.setPrimitiveDataVec2(self.context, uuids_or_uuid, label, x, y)
2589
2590 def setPrimitiveDataVec3(self, uuids_or_uuid, label: str, x_or_vec, y: float = None, z: float = None) -> None:
2591 """
2592 Set primitive data as vec3 for one or multiple primitives.
2593
2594 Args:
2595 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2596 label: String key for the data
2597 x_or_vec: Either x component (float) or vec3 object
2598 y: Y component (if x_or_vec is float)
2599 z: Z component (if x_or_vec is float)
2600 """
2601 if hasattr(x_or_vec, 'x'):
2602 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2603 else:
2604 x = x_or_vec
2605 if isinstance(uuids_or_uuid, (list, tuple)):
2606 context_wrapper.setBroadcastPrimitiveDataVec3(self.context, uuids_or_uuid, label, x, y, z)
2607 else:
2608 context_wrapper.setPrimitiveDataVec3(self.context, uuids_or_uuid, label, x, y, z)
2609
2610 def setPrimitiveDataVec4(self, uuids_or_uuid, label: str, x_or_vec, y: float = None, z: float = None, w: float = None) -> None:
2611 """
2612 Set primitive data as vec4 for one or multiple primitives.
2613
2614 Args:
2615 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2616 label: String key for the data
2617 x_or_vec: Either x component (float) or vec4 object
2618 y: Y component (if x_or_vec is float)
2619 z: Z component (if x_or_vec is float)
2620 w: W component (if x_or_vec is float)
2621 """
2622 if hasattr(x_or_vec, 'x'):
2623 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2624 else:
2625 x = x_or_vec
2626 if isinstance(uuids_or_uuid, (list, tuple)):
2627 context_wrapper.setBroadcastPrimitiveDataVec4(self.context, uuids_or_uuid, label, x, y, z, w)
2628 else:
2629 context_wrapper.setPrimitiveDataVec4(self.context, uuids_or_uuid, label, x, y, z, w)
2630
2631 def setPrimitiveDataInt2(self, uuids_or_uuid, label: str, x_or_vec, y: int = None) -> None:
2632 """
2633 Set primitive data as int2 for one or multiple primitives.
2634
2635 Args:
2636 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2637 label: String key for the data
2638 x_or_vec: Either x component (int) or int2 object
2639 y: Y component (if x_or_vec is int)
2640 """
2641 if hasattr(x_or_vec, 'x'):
2642 x, y = x_or_vec.x, x_or_vec.y
2643 else:
2644 x = x_or_vec
2645 if isinstance(uuids_or_uuid, (list, tuple)):
2646 context_wrapper.setBroadcastPrimitiveDataInt2(self.context, uuids_or_uuid, label, x, y)
2647 else:
2648 context_wrapper.setPrimitiveDataInt2(self.context, uuids_or_uuid, label, x, y)
2649
2650 def setPrimitiveDataInt3(self, uuids_or_uuid, label: str, x_or_vec, y: int = None, z: int = None) -> None:
2651 """
2652 Set primitive data as int3 for one or multiple primitives.
2653
2654 Args:
2655 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2656 label: String key for the data
2657 x_or_vec: Either x component (int) or int3 object
2658 y: Y component (if x_or_vec is int)
2659 z: Z component (if x_or_vec is int)
2660 """
2661 if hasattr(x_or_vec, 'x'):
2662 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
2663 else:
2664 x = x_or_vec
2665 if isinstance(uuids_or_uuid, (list, tuple)):
2666 context_wrapper.setBroadcastPrimitiveDataInt3(self.context, uuids_or_uuid, label, x, y, z)
2667 else:
2668 context_wrapper.setPrimitiveDataInt3(self.context, uuids_or_uuid, label, x, y, z)
2669
2670 def setPrimitiveDataInt4(self, uuids_or_uuid, label: str, x_or_vec, y: int = None, z: int = None, w: int = None) -> None:
2671 """
2672 Set primitive data as int4 for one or multiple primitives.
2673
2674 Args:
2675 uuids_or_uuid: Single UUID (int) or list of UUIDs to set data for
2676 label: String key for the data
2677 x_or_vec: Either x component (int) or int4 object
2678 y: Y component (if x_or_vec is int)
2679 z: Z component (if x_or_vec is int)
2680 w: W component (if x_or_vec is int)
2681 """
2682 if hasattr(x_or_vec, 'x'):
2683 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
2684 else:
2685 x = x_or_vec
2686 if isinstance(uuids_or_uuid, (list, tuple)):
2687 context_wrapper.setBroadcastPrimitiveDataInt4(self.context, uuids_or_uuid, label, x, y, z, w)
2688 else:
2689 context_wrapper.setPrimitiveDataInt4(self.context, uuids_or_uuid, label, x, y, z, w)
2690
2691 def getPrimitiveData(self, uuid: int, label: str, data_type: type = None):
2692 """
2693 Get primitive data for a specific primitive. If data_type is provided, it works like before.
2694 If data_type is None, it automatically detects the type and returns the appropriate value.
2695
2696 Args:
2697 uuid: UUID of the primitive
2698 label: String key for the data
2699 data_type: Optional. Python type to retrieve (int, uint, float, double, bool, str, vec2, vec3, vec4, int2, int3, int4, etc.)
2700 If None, auto-detects the type using C++ getPrimitiveDataType().
2701
2702 Returns:
2703 The stored value of the specified or auto-detected type
2704 """
2705 # If no type specified, use auto-detection
2706 if data_type is None:
2707 return context_wrapper.getPrimitiveDataAuto(self.context, uuid, label)
2708
2709 # Handle basic types (original behavior when type is specified)
2710 if data_type == int:
2711 return context_wrapper.getPrimitiveDataInt(self.context, uuid, label)
2712 elif data_type == float:
2713 return context_wrapper.getPrimitiveDataFloat(self.context, uuid, label)
2714 elif data_type == bool:
2715 # Bool is not supported by Helios core - get as int and convert
2716 int_value = context_wrapper.getPrimitiveDataInt(self.context, uuid, label)
2717 return int_value != 0
2718 elif data_type == str:
2719 return context_wrapper.getPrimitiveDataString(self.context, uuid, label)
2720
2721 # Handle Helios vector types
2722 elif data_type == vec2:
2723 coords = context_wrapper.getPrimitiveDataVec2(self.context, uuid, label)
2724 return vec2(coords[0], coords[1])
2725 elif data_type == vec3:
2726 coords = context_wrapper.getPrimitiveDataVec3(self.context, uuid, label)
2727 return vec3(coords[0], coords[1], coords[2])
2728 elif data_type == vec4:
2729 coords = context_wrapper.getPrimitiveDataVec4(self.context, uuid, label)
2730 return vec4(coords[0], coords[1], coords[2], coords[3])
2731 elif data_type == int2:
2732 coords = context_wrapper.getPrimitiveDataInt2(self.context, uuid, label)
2733 return int2(coords[0], coords[1])
2734 elif data_type == int3:
2735 coords = context_wrapper.getPrimitiveDataInt3(self.context, uuid, label)
2736 return int3(coords[0], coords[1], coords[2])
2737 elif data_type == int4:
2738 coords = context_wrapper.getPrimitiveDataInt4(self.context, uuid, label)
2739 return int4(coords[0], coords[1], coords[2], coords[3])
2740
2741 # Handle extended numeric types (require explicit specification since Python doesn't have these as distinct types)
2742 elif data_type == "uint":
2743 return context_wrapper.getPrimitiveDataUInt(self.context, uuid, label)
2744 elif data_type == "double":
2745 return context_wrapper.getPrimitiveDataDouble(self.context, uuid, label)
2746
2747 # Handle list return types (for convenience)
2748 elif data_type == list:
2749 # Default to vec3 as list for backward compatibility
2750 return context_wrapper.getPrimitiveDataVec3(self.context, uuid, label)
2751 elif data_type == "list_vec2":
2752 return context_wrapper.getPrimitiveDataVec2(self.context, uuid, label)
2753 elif data_type == "list_vec4":
2754 return context_wrapper.getPrimitiveDataVec4(self.context, uuid, label)
2755 elif data_type == "list_int2":
2756 return context_wrapper.getPrimitiveDataInt2(self.context, uuid, label)
2757 elif data_type == "list_int3":
2758 return context_wrapper.getPrimitiveDataInt3(self.context, uuid, label)
2759 elif data_type == "list_int4":
2760 return context_wrapper.getPrimitiveDataInt4(self.context, uuid, label)
2761
2762 else:
2763 raise ValueError(f"Unsupported primitive data type: {data_type}. "
2764 f"Supported types: int, float, bool, str, vec2, vec3, vec4, int2, int3, int4, "
2765 f"'uint', 'double', list (for vec3), 'list_vec2', 'list_vec4', 'list_int2', 'list_int3', 'list_int4'")
2766
2767 def doesPrimitiveDataExist(self, uuid: int, label: str) -> bool:
2768 """
2769 Check if primitive data exists for a specific primitive and label.
2770
2771 Args:
2772 uuid: UUID of the primitive
2773 label: String key for the data
2774
2775 Returns:
2776 True if the data exists, False otherwise
2777 """
2778 return context_wrapper.doesPrimitiveDataExistWrapper(self.context, uuid, label)
2779
2780 def getPrimitiveDataFloat(self, uuid: int, label: str) -> float:
2781 """
2782 Convenience method to get float primitive data.
2783
2784 Args:
2785 uuid: UUID of the primitive
2786 label: String key for the data
2787
2788 Returns:
2789 Float value stored for the primitive
2790 """
2791 return self.getPrimitiveData(uuid, label, float)
2792
2793 def getPrimitiveDataType(self, uuid: int, label: str) -> int:
2794 """
2795 Get the Helios data type of primitive data.
2796
2797 Args:
2798 uuid: UUID of the primitive
2799 label: String key for the data
2800
2801 Returns:
2802 HeliosDataType enum value as integer
2803 """
2804 return context_wrapper.getPrimitiveDataTypeWrapper(self.context, uuid, label)
2805
2806 def getPrimitiveDataSize(self, uuid: int, label: str) -> int:
2807 """
2808 Get the size/length of primitive data (for vector data).
2809
2810 Args:
2811 uuid: UUID of the primitive
2812 label: String key for the data
2813
2814 Returns:
2815 Size of data array, or 1 for scalar data
2816 """
2817 return context_wrapper.getPrimitiveDataSizeWrapper(self.context, uuid, label)
2818
2819 def getPrimitiveDataArray(self, uuids: List[int], label: str) -> np.ndarray:
2820 """
2821 Get primitive data values for multiple primitives as a NumPy array.
2822
2823 This method retrieves primitive data for a list of UUIDs and returns the values
2824 as a NumPy array. The output array has the same length as the input UUID list,
2825 with each index corresponding to the primitive data value for that UUID.
2826
2827 Args:
2828 uuids: List of primitive UUIDs to get data for
2829 label: String key for the primitive data to retrieve
2830
2831 Returns:
2832 NumPy array of primitive data values corresponding to each UUID.
2833 The array type depends on the data type:
2834 - int data: int32 array
2835 - uint data: uint32 array
2836 - float data: float32 array
2837 - double data: float64 array
2838 - vector data: float32 array with shape (N, vector_size)
2839 - string data: object array of strings
2840
2841 Raises:
2842 ValueError: If UUID list is empty or UUIDs don't exist
2843 RuntimeError: If context is in mock mode or data doesn't exist for some UUIDs
2844 """
2846
2847 if not uuids:
2848 raise ValueError("UUID list cannot be empty")
2849
2850 # First validate that all UUIDs exist
2851 for uuid in uuids:
2853
2854 # Then check that all UUIDs have the specified data
2855 for uuid in uuids:
2856 if not self.doesPrimitiveDataExist(uuid, label):
2857 raise ValueError(f"Primitive data '{label}' does not exist for UUID {uuid}")
2858
2859 # Get data type from the first UUID to determine array type
2860 first_uuid = uuids[0]
2861 data_type = self.getPrimitiveDataType(first_uuid, label)
2862
2863 # Map Helios data types to NumPy array creation
2864 # Based on HeliosDataType enum from Helios core
2865 if data_type == 0: # HELIOS_TYPE_INT
2866 result = np.empty(len(uuids), dtype=np.int32)
2867 for i, uuid in enumerate(uuids):
2868 result[i] = self.getPrimitiveData(uuid, label, int)
2869
2870 elif data_type == 1: # HELIOS_TYPE_UINT
2871 result = np.empty(len(uuids), dtype=np.uint32)
2872 for i, uuid in enumerate(uuids):
2873 result[i] = self.getPrimitiveData(uuid, label, "uint")
2874
2875 elif data_type == 2: # HELIOS_TYPE_FLOAT
2876 result = np.empty(len(uuids), dtype=np.float32)
2877 for i, uuid in enumerate(uuids):
2878 result[i] = self.getPrimitiveData(uuid, label, float)
2879
2880 elif data_type == 3: # HELIOS_TYPE_DOUBLE
2881 result = np.empty(len(uuids), dtype=np.float64)
2882 for i, uuid in enumerate(uuids):
2883 result[i] = self.getPrimitiveData(uuid, label, "double")
2884
2885 elif data_type == 4: # HELIOS_TYPE_VEC2
2886 result = np.empty((len(uuids), 2), dtype=np.float32)
2887 for i, uuid in enumerate(uuids):
2888 vec_data = self.getPrimitiveData(uuid, label, vec2)
2889 result[i] = [vec_data.x, vec_data.y]
2890
2891 elif data_type == 5: # HELIOS_TYPE_VEC3
2892 result = np.empty((len(uuids), 3), dtype=np.float32)
2893 for i, uuid in enumerate(uuids):
2894 vec_data = self.getPrimitiveData(uuid, label, vec3)
2895 result[i] = [vec_data.x, vec_data.y, vec_data.z]
2896
2897 elif data_type == 6: # HELIOS_TYPE_VEC4
2898 result = np.empty((len(uuids), 4), dtype=np.float32)
2899 for i, uuid in enumerate(uuids):
2900 vec_data = self.getPrimitiveData(uuid, label, vec4)
2901 result[i] = [vec_data.x, vec_data.y, vec_data.z, vec_data.w]
2902
2903 elif data_type == 7: # HELIOS_TYPE_INT2
2904 result = np.empty((len(uuids), 2), dtype=np.int32)
2905 for i, uuid in enumerate(uuids):
2906 int_data = self.getPrimitiveData(uuid, label, int2)
2907 result[i] = [int_data.x, int_data.y]
2908
2909 elif data_type == 8: # HELIOS_TYPE_INT3
2910 result = np.empty((len(uuids), 3), dtype=np.int32)
2911 for i, uuid in enumerate(uuids):
2912 int_data = self.getPrimitiveData(uuid, label, int3)
2913 result[i] = [int_data.x, int_data.y, int_data.z]
2914
2915 elif data_type == 9: # HELIOS_TYPE_INT4
2916 result = np.empty((len(uuids), 4), dtype=np.int32)
2917 for i, uuid in enumerate(uuids):
2918 int_data = self.getPrimitiveData(uuid, label, int4)
2919 result[i] = [int_data.x, int_data.y, int_data.z, int_data.w]
2920
2921 elif data_type == 10: # HELIOS_TYPE_STRING
2922 result = np.empty(len(uuids), dtype=object)
2923 for i, uuid in enumerate(uuids):
2924 result[i] = self.getPrimitiveData(uuid, label, str)
2925
2926 else:
2927 raise ValueError(f"Unsupported primitive data type: {data_type}")
2928
2929 return result
2930
2931
2932 def colorPrimitiveByDataPseudocolor(self, uuids: List[int], primitive_data: str,
2933 colormap: str = "hot", ncolors: int = 10,
2934 max_val: Optional[float] = None, min_val: Optional[float] = None):
2935 """
2936 Color primitives based on primitive data values using pseudocolor mapping.
2937
2938 This method applies a pseudocolor mapping to primitives based on the values
2939 of specified primitive data. The primitive colors are updated to reflect the
2940 data values using a color map.
2941
2942 Args:
2943 uuids: List of primitive UUIDs to color
2944 primitive_data: Name of primitive data to use for coloring (e.g., "radiation_flux_SW")
2945 colormap: Color map name - options include "hot", "cool", "parula", "rainbow", "gray", "lava"
2946 ncolors: Number of discrete colors in color map (default: 10)
2947 max_val: Maximum value for color scale (auto-determined if None)
2948 min_val: Minimum value for color scale (auto-determined if None)
2949 """
2950 if max_val is not None and min_val is not None:
2951 context_wrapper.colorPrimitiveByDataPseudocolorWithRange(
2952 self.context, uuids, primitive_data, colormap, ncolors, max_val, min_val)
2953 else:
2954 context_wrapper.colorPrimitiveByDataPseudocolor(
2955 self.context, uuids, primitive_data, colormap, ncolors)
2956
2957 # Context time/date methods for solar position integration
2958 def setTime(self, hour: int, minute: int = 0, second: int = 0):
2959 """
2960 Set the simulation time.
2961
2962 Args:
2963 hour: Hour (0-23)
2964 minute: Minute (0-59), defaults to 0
2965 second: Second (0-59), defaults to 0
2966
2967 Raises:
2968 ValueError: If time values are out of range
2969 NotImplementedError: If time/date functions not available in current library build
2970
2971 Example:
2972 >>> context.setTime(14, 30) # Set to 2:30 PM
2973 >>> context.setTime(9, 15, 30) # Set to 9:15:30 AM
2974 """
2975 context_wrapper.setTime(self.context, hour, minute, second)
2976
2977 def setDate(self, year: int, month: int, day: int):
2978 """
2979 Set the simulation date.
2980
2981 Args:
2982 year: Year (1900-3000)
2983 month: Month (1-12)
2984 day: Day (1-31)
2985
2986 Raises:
2987 ValueError: If date values are out of range
2988 NotImplementedError: If time/date functions not available in current library build
2989
2990 Example:
2991 >>> context.setDate(2023, 6, 21) # Set to June 21, 2023
2992 """
2993 context_wrapper.setDate(self.context, year, month, day)
2994
2995 def setDateJulian(self, julian_day: int, year: int):
2996 """
2997 Set the simulation date using Julian day number.
2998
2999 Args:
3000 julian_day: Julian day (1-366)
3001 year: Year (1900-3000)
3002
3003 Raises:
3004 ValueError: If values are out of range
3005 NotImplementedError: If time/date functions not available in current library build
3006
3007 Example:
3008 >>> context.setDateJulian(172, 2023) # Set to day 172 of 2023 (June 21)
3009 """
3010 context_wrapper.setDateJulian(self.context, julian_day, year)
3011
3012 def getTime(self):
3013 """
3014 Get the current simulation time.
3015
3016 Returns:
3017 Tuple of (hour, minute, second) as integers
3018
3019 Raises:
3020 NotImplementedError: If time/date functions not available in current library build
3021
3022 Example:
3023 >>> hour, minute, second = context.getTime()
3024 >>> print(f"Current time: {hour:02d}:{minute:02d}:{second:02d}")
3025 """
3026 return context_wrapper.getTime(self.context)
3027
3028 def getDate(self):
3029 """
3030 Get the current simulation date.
3031
3032 Returns:
3033 Tuple of (year, month, day) as integers
3034
3035 Raises:
3036 NotImplementedError: If time/date functions not available in current library build
3037
3038 Example:
3039 >>> year, month, day = context.getDate()
3040 >>> print(f"Current date: {year}-{month:02d}-{day:02d}")
3041 """
3042 return context_wrapper.getDate(self.context)
3043
3044 # ==========================================================================
3045 # Timeseries Methods
3046 # ==========================================================================
3047
3048 def addTimeseriesData(self, label: str, value: float, date: 'Date', time: 'Time'):
3049 """
3050 Add a data point to a timeseries variable.
3051
3052 Args:
3053 label: Name of the timeseries variable (e.g., "temperature")
3054 value: Value of the data point
3055 date: Date of the data point
3056 time: Time of the data point
3057
3058 Raises:
3059 ValueError: If label is empty, or date/time are wrong types
3060 NotImplementedError: If timeseries functions not available
3061
3062 Example:
3063 >>> from pyhelios.types import Date, Time
3064 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3065 """
3067 if not isinstance(label, str) or not label:
3068 raise ValueError("Label must be a non-empty string")
3069 if not isinstance(date, Date):
3070 raise ValueError(f"date must be a Date instance, got {type(date).__name__}")
3071 if not isinstance(time, Time):
3072 raise ValueError(f"time must be a Time instance, got {type(time).__name__}")
3074 context_wrapper.addTimeseriesData(
3075 self.context, label, float(value),
3076 date.day, date.month, date.year,
3077 time.hour, time.minute, time.second
3078 )
3079
3080 def updateTimeseriesData(self, label: str, date: 'Date', time: 'Time', new_value: float):
3081 """
3082 Update the value of an existing timeseries data point.
3083
3084 Args:
3085 label: Name of the timeseries variable (must already exist)
3086 date: Date of the existing point (must match exactly)
3087 time: Time of the existing point (must match exactly)
3088 new_value: Replacement value
3089
3090 Raises:
3091 ValueError: If label is empty, or date/time are wrong types
3092 HeliosRuntimeError: If the variable does not exist or no point matches the (date, time)
3093 NotImplementedError: If timeseries functions not available
3094
3095 Example:
3096 >>> from pyhelios.types import Date, Time
3097 >>> context.addTimeseriesData("temperature", 25.3, Date(2024, 6, 15), Time(12, 0, 0))
3098 >>> context.updateTimeseriesData("temperature", Date(2024, 6, 15), Time(12, 0, 0), 26.5)
3099 """
3101 if not isinstance(label, str) or not label:
3102 raise ValueError("Label must be a non-empty string")
3103 if not isinstance(date, Date):
3104 raise ValueError(f"date must be a Date instance, got {type(date).__name__}")
3105 if not isinstance(time, Time):
3106 raise ValueError(f"time must be a Time instance, got {type(time).__name__}")
3108 context_wrapper.updateTimeseriesData(
3109 self.context, label,
3110 date.day, date.month, date.year,
3111 time.hour, time.minute, time.second,
3112 float(new_value)
3113 )
3114
3115 def setCurrentTimeseriesPoint(self, label: str, index: int):
3116 """
3117 Set the Context date and time from a timeseries data point index.
3118
3119 Args:
3120 label: Name of the timeseries variable
3121 index: Index of the data point (0 = earliest, chronologically ordered)
3122
3123 Raises:
3124 ValueError: If label is empty or index is negative
3125 NotImplementedError: If timeseries functions not available
3126
3127 Example:
3128 >>> context.setCurrentTimeseriesPoint("temperature", 0)
3129 """
3131 if not isinstance(label, str) or not label:
3132 raise ValueError("Label must be a non-empty string")
3133 if not isinstance(index, int) or index < 0:
3134 raise ValueError(f"Index must be a non-negative integer, got {index}")
3135
3136 context_wrapper.setCurrentTimeseriesPoint(self.context, label, index)
3138 def queryTimeseriesData(self, label: str, date: 'Date' = None, time: 'Time' = None,
3139 index: int = None) -> float:
3140 """
3141 Query a timeseries data value.
3142
3143 Three modes of operation:
3144 - With date and time: returns interpolated value at the specified date/time
3145 - With index: returns value at the specified data point index
3146 - With neither: returns value at the current Context date/time
3147
3148 Args:
3149 label: Name of the timeseries variable
3150 date: Date to query at (requires time as well)
3151 time: Time to query at (requires date as well)
3152 index: Index of the data point (0 = earliest)
3153
3154 Returns:
3155 The timeseries value as a float
3156
3157 Raises:
3158 ValueError: If both date/time and index are provided, or if date without time
3159 NotImplementedError: If timeseries functions not available
3160
3161 Example:
3162 >>> # Query at specific date/time
3163 >>> val = context.queryTimeseriesData("temperature", date=Date(2024, 6, 15), time=Time(12, 0, 0))
3164 >>> # Query by index
3165 >>> val = context.queryTimeseriesData("temperature", index=0)
3166 >>> # Query at current context time
3167 >>> val = context.queryTimeseriesData("temperature")
3168 """
3170 if not isinstance(label, str) or not label:
3171 raise ValueError("Label must be a non-empty string")
3172
3173 has_datetime = date is not None or time is not None
3174 has_index = index is not None
3176 if has_datetime and has_index:
3177 raise ValueError("Cannot specify both date/time and index. Use one or the other.")
3178
3179 if has_datetime:
3180 if date is None or time is None:
3181 raise ValueError("Both date and time must be provided together")
3182 if not isinstance(date, Date):
3183 raise ValueError(f"date must be a Date instance, got {type(date).__name__}")
3184 if not isinstance(time, Time):
3185 raise ValueError(f"time must be a Time instance, got {type(time).__name__}")
3186 return context_wrapper.queryTimeseriesDataDateTime(
3187 self.context, label,
3188 date.day, date.month, date.year,
3189 time.hour, time.minute, time.second
3190 )
3191
3192 if has_index:
3193 if not isinstance(index, int) or index < 0:
3194 raise ValueError(f"Index must be a non-negative integer, got {index}")
3195 return context_wrapper.queryTimeseriesDataIndex(self.context, label, index)
3196
3197 return context_wrapper.queryTimeseriesDataCurrent(self.context, label)
3198
3199 def queryTimeseriesTime(self, label: str, index: int) -> 'Time':
3200 """
3201 Get the Time associated with a timeseries data point.
3202
3203 Args:
3204 label: Name of the timeseries variable
3205 index: Index of the data point (0 = earliest)
3206
3207 Returns:
3208 Time object for the data point
3209
3210 Raises:
3211 ValueError: If label is empty or index is negative
3212 NotImplementedError: If timeseries functions not available
3213
3214 Example:
3215 >>> t = context.queryTimeseriesTime("temperature", 0)
3216 >>> print(f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}")
3217 """
3219 if not isinstance(label, str) or not label:
3220 raise ValueError("Label must be a non-empty string")
3221 if not isinstance(index, int) or index < 0:
3222 raise ValueError(f"Index must be a non-negative integer, got {index}")
3223
3224 hour, minute, second = context_wrapper.queryTimeseriesTime(self.context, label, index)
3225 return Time(hour=hour, minute=minute, second=second)
3226
3227 def queryTimeseriesDate(self, label: str, index: int) -> 'Date':
3228 """
3229 Get the Date associated with a timeseries data point.
3230
3231 Args:
3232 label: Name of the timeseries variable
3233 index: Index of the data point (0 = earliest)
3234
3235 Returns:
3236 Date object for the data point
3237
3238 Raises:
3239 ValueError: If label is empty or index is negative
3240 NotImplementedError: If timeseries functions not available
3241
3242 Example:
3243 >>> d = context.queryTimeseriesDate("temperature", 0)
3244 >>> print(f"{d.year}-{d.month:02d}-{d.day:02d}")
3245 """
3247 if not isinstance(label, str) or not label:
3248 raise ValueError("Label must be a non-empty string")
3249 if not isinstance(index, int) or index < 0:
3250 raise ValueError(f"Index must be a non-negative integer, got {index}")
3251
3252 year, month, day = context_wrapper.queryTimeseriesDate(self.context, label, index)
3253 return Date(year=year, month=month, day=day)
3254
3255 def getTimeseriesLength(self, label: str) -> int:
3256 """
3257 Get the number of data points in a timeseries variable.
3258
3259 Args:
3260 label: Name of the timeseries variable
3261
3262 Returns:
3263 Number of data points
3264
3265 Raises:
3266 ValueError: If label is empty
3267 NotImplementedError: If timeseries functions not available
3268
3269 Example:
3270 >>> n = context.getTimeseriesLength("temperature")
3271 >>> print(f"Timeseries has {n} data points")
3272 """
3274 if not isinstance(label, str) or not label:
3275 raise ValueError("Label must be a non-empty string")
3276
3277 return context_wrapper.getTimeseriesLength(self.context, label)
3278
3279 def doesTimeseriesVariableExist(self, label: str) -> bool:
3280 """
3281 Check whether a timeseries variable exists.
3282
3283 Args:
3284 label: Name of the timeseries variable
3285
3286 Returns:
3287 True if the variable exists, False otherwise
3288
3289 Raises:
3290 ValueError: If label is empty
3291 NotImplementedError: If timeseries functions not available
3292
3293 Example:
3294 >>> if context.doesTimeseriesVariableExist("temperature"):
3295 ... print("Temperature data loaded")
3296 """
3298 if not isinstance(label, str) or not label:
3299 raise ValueError("Label must be a non-empty string")
3300
3301 return context_wrapper.doesTimeseriesVariableExist(self.context, label)
3302
3303 def listTimeseriesVariables(self) -> List[str]:
3304 """
3305 List all existing timeseries variables.
3306
3307 Returns:
3308 List of timeseries variable names
3309
3310 Raises:
3311 NotImplementedError: If timeseries functions not available
3312
3313 Example:
3314 >>> variables = context.listTimeseriesVariables()
3315 >>> for var in variables:
3316 ... print(f" {var}: {context.getTimeseriesLength(var)} points")
3317 """
3319
3320 return context_wrapper.listTimeseriesVariables(self.context)
3321
3322 def clearTimeseriesData(self):
3323 """Clear all timeseries data from the Context.
3324
3325 Removes all timeseries variables and their associated date/time values.
3326
3327 Raises:
3328 NotImplementedError: If timeseries functions not available
3329
3330 Example:
3331 >>> context.clearTimeseriesData()
3332 >>> context.listTimeseriesVariables()
3333 []
3334 """
3336 context_wrapper.clearTimeseriesData(self.context)
3337
3338 def loadTabularTimeseriesData(self, data_file: str, column_labels: List[str],
3339 delimiter: str = ",", date_string_format: str = "YYYYMMDD",
3340 headerlines: int = 0):
3341 """
3342 Load tabular timeseries data from a text file.
3343
3344 The file should contain columns of data with dates/times and measured values.
3345 Column labels specify how each column should be interpreted. Special labels
3346 include "year", "DOY", "date", "datetime", "hour", "minute", "second", "time".
3347 Other labels become timeseries variable names.
3348
3349 Args:
3350 data_file: Path to the text file containing tabular data
3351 column_labels: List of column label strings specifying what each column contains
3352 delimiter: Column delimiter string (default: ",")
3353 date_string_format: Format of date strings in the file. Supported formats:
3354 "YYYYMMDD", "YYYYMMDDHH", "YYYYMMDDHHMM", "DD/MM/YYYY",
3355 "MM/DD/YYYY", "DDMMYYYY", "YYYY-MM-DD", "DD/MM/YYYY HH:MM",
3356 "MM/DD/YYYY HH:MM", "ISO8601" (default: "YYYYMMDD")
3357 headerlines: Number of header lines to skip (default: 0)
3358
3359 Raises:
3360 ValueError: If data_file is empty, column_labels is empty, or delimiter is empty
3361 RuntimeError: If the file cannot be read or parsed
3362 NotImplementedError: If timeseries functions not available
3363
3364 Example:
3365 >>> context.loadTabularTimeseriesData(
3366 ... "weather_data.csv",
3367 ... column_labels=["date", "hour", "temperature", "humidity"],
3368 ... delimiter=",",
3369 ... headerlines=1
3370 ... )
3371 >>> temp = context.queryTimeseriesData("temperature", index=0)
3372 """
3374 if not isinstance(data_file, str) or not data_file:
3375 raise ValueError("data_file must be a non-empty string")
3376 if not isinstance(column_labels, list) or not column_labels:
3377 raise ValueError("column_labels must be a non-empty list of strings")
3378 for i, label in enumerate(column_labels):
3379 if not isinstance(label, str):
3380 raise ValueError(f"column_labels[{i}] must be a string, got {type(label).__name__}")
3381 if not isinstance(delimiter, str) or not delimiter:
3382 raise ValueError("delimiter must be a non-empty string")
3383
3384 context_wrapper.loadTabularTimeseriesData(
3385 self.context, data_file, column_labels, delimiter,
3386 date_string_format, headerlines
3387 )
3388
3389 # ==========================================================================
3390 # Primitive and Object Deletion Methods
3391 # ==========================================================================
3392
3393 def deletePrimitive(self, uuids_or_uuid: Union[int, List[int]]) -> None:
3394 """
3395 Delete one or more primitives from the context.
3396
3397 This removes the primitive(s) entirely from the context. If a primitive
3398 belongs to a compound object, it will be removed from that object. If the
3399 object becomes empty after removal, it is automatically deleted.
3400
3401 Args:
3402 uuids_or_uuid: Single UUID (int) or list of UUIDs to delete
3403
3404 Raises:
3405 RuntimeError: If any UUID doesn't exist in the context
3406 ValueError: If UUID is invalid (negative)
3407 NotImplementedError: If delete functions not available in current library build
3408
3409 Example:
3410 >>> context = Context()
3411 >>> patch_id = context.addPatch(center=vec3(0, 0, 0), size=vec2(1, 1))
3412 >>> context.deletePrimitive(patch_id) # Single deletion
3413 >>>
3414 >>> # Multiple deletion
3415 >>> ids = [context.addPatch() for _ in range(5)]
3416 >>> context.deletePrimitive(ids) # Delete all at once
3417 """
3419
3420 if isinstance(uuids_or_uuid, (list, tuple)):
3421 for uuid in uuids_or_uuid:
3422 if uuid < 0:
3423 raise ValueError(f"UUID must be non-negative, got {uuid}")
3424 context_wrapper.deletePrimitives(self.context, list(uuids_or_uuid))
3425 else:
3426 if uuids_or_uuid < 0:
3427 raise ValueError(f"UUID must be non-negative, got {uuids_or_uuid}")
3428 context_wrapper.deletePrimitive(self.context, uuids_or_uuid)
3429
3430 def deleteObject(self, objIDs_or_objID: Union[int, List[int]]) -> None:
3431 """
3432 Delete one or more compound objects from the context.
3433
3434 This removes the compound object(s) AND all their child primitives.
3435 Use this when you want to delete an entire object hierarchy at once.
3436
3437 Args:
3438 objIDs_or_objID: Single object ID (int) or list of object IDs to delete
3439
3440 Raises:
3441 RuntimeError: If any object ID doesn't exist in the context
3442 ValueError: If object ID is invalid (negative)
3443 NotImplementedError: If delete functions not available in current library build
3444
3445 Example:
3446 >>> context = Context()
3447 >>> # Create a compound object (e.g., a tile with multiple patches)
3448 >>> patch_ids = context.addTile(center=vec3(0, 0, 0), size=vec2(2, 2),
3449 ... tile_divisions=int2(2, 2))
3450 >>> obj_id = context.getPrimitiveParentObjectID(patch_ids[0])
3451 >>> context.deleteObject(obj_id) # Deletes tile and all its patches
3452 """
3454
3455 if isinstance(objIDs_or_objID, (list, tuple)):
3456 for objID in objIDs_or_objID:
3457 if objID < 0:
3458 raise ValueError(f"Object ID must be non-negative, got {objID}")
3459 context_wrapper.deleteObjects(self.context, list(objIDs_or_objID))
3460 else:
3461 if objIDs_or_objID < 0:
3462 raise ValueError(f"Object ID must be non-negative, got {objIDs_or_objID}")
3463 context_wrapper.deleteObject(self.context, objIDs_or_objID)
3464
3465 # Plugin-related methods
3466 def get_available_plugins(self) -> List[str]:
3467 """
3468 Get list of available plugins for this PyHelios instance.
3469
3470 Returns:
3471 List of available plugin names
3472 """
3474
3475 def is_plugin_available(self, plugin_name: str) -> bool:
3476 """
3477 Check if a specific plugin is available.
3478
3479 Args:
3480 plugin_name: Name of the plugin to check
3481
3482 Returns:
3483 True if plugin is available, False otherwise
3484 """
3485 return self._plugin_registry.is_plugin_available(plugin_name)
3486
3487 def get_plugin_capabilities(self) -> dict:
3488 """
3489 Get detailed information about available plugin capabilities.
3490
3491 Returns:
3492 Dictionary mapping plugin names to capability information
3493 """
3495
3496 def print_plugin_status(self):
3497 """Print detailed plugin status information."""
3498 self._plugin_registry.print_status()
3499
3500 def get_missing_plugins(self, requested_plugins: List[str]) -> List[str]:
3501 """
3502 Get list of requested plugins that are not available.
3503
3504 Args:
3505 requested_plugins: List of plugin names to check
3506
3507 Returns:
3508 List of missing plugin names
3509 """
3510 return self._plugin_registry.get_missing_plugins(requested_plugins)
3511
3512 # =========================================================================
3513 # Materials System (v1.3.58+)
3514 # =========================================================================
3515
3516 def addMaterial(self, material_label: str):
3517 """
3518 Create a new material for sharing visual properties across primitives.
3519
3520 Materials enable efficient memory usage by allowing multiple primitives to
3521 share rendering properties. Changes to a material affect all primitives using it.
3522
3523 Args:
3524 material_label: Unique label for the material
3525
3526 Raises:
3527 RuntimeError: If material label already exists
3528
3529 Example:
3530 >>> context.addMaterial("wood_oak")
3531 >>> context.setMaterialColor("wood_oak", (0.6, 0.4, 0.2, 1.0))
3532 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3533 """
3534 context_wrapper.addMaterial(self.context, material_label)
3535
3536 def doesMaterialExist(self, material_label: str) -> bool:
3537 """Check if a material with the given label exists."""
3538 return context_wrapper.doesMaterialExist(self.context, material_label)
3539
3540 def listMaterials(self) -> List[str]:
3541 """Get list of all material labels in the context."""
3542 return context_wrapper.listMaterials(self.context)
3543
3544 def deleteMaterial(self, material_label: str):
3545 """
3546 Delete a material from the context.
3547
3548 Primitives using this material will be reassigned to the default material.
3550 Args:
3551 material_label: Label of the material to delete
3552
3553 Raises:
3554 RuntimeError: If material doesn't exist
3555 """
3556 context_wrapper.deleteMaterial(self.context, material_label)
3557
3558 def getMaterialColor(self, material_label: str):
3559 """
3560 Get the RGBA color of a material.
3561
3562 Args:
3563 material_label: Label of the material
3564
3565 Returns:
3566 RGBAcolor object
3567
3568 Raises:
3569 RuntimeError: If material doesn't exist
3570 """
3571 from .wrappers.DataTypes import RGBAcolor
3572 color_list = context_wrapper.getMaterialColor(self.context, material_label)
3573 return RGBAcolor(color_list[0], color_list[1], color_list[2], color_list[3])
3574
3575 def setMaterialColor(self, material_label: str, color):
3576 """
3577 Set the RGBA color of a material.
3579 This affects all primitives that reference this material.
3580
3581 Args:
3582 material_label: Label of the material
3583 color: RGBAcolor object or tuple/list of (r, g, b, a) values
3584
3585 Raises:
3586 RuntimeError: If material doesn't exist
3587
3588 Example:
3589 >>> from pyhelios.types import RGBAcolor
3590 >>> context.setMaterialColor("wood", RGBAcolor(0.6, 0.4, 0.2, 1.0))
3591 >>> context.setMaterialColor("wood", (0.6, 0.4, 0.2, 1.0))
3592 """
3593 if isinstance(color, RGBAcolor):
3594 r, g, b, a = color.r, color.g, color.b, color.a
3595 elif isinstance(color, (list, tuple)) and len(color) == 4:
3596 r, g, b, a = color[0], color[1], color[2], color[3]
3597 else:
3598 raise ValueError(f"Color must be an RGBAcolor or a 4-element list/tuple, got {type(color).__name__}")
3599 context_wrapper.setMaterialColor(self.context, material_label, r, g, b, a)
3601 def getMaterialTexture(self, material_label: str) -> str:
3602 """
3603 Get the texture file path for a material.
3604
3605 Args:
3606 material_label: Label of the material
3607
3608 Returns:
3609 Texture file path, or empty string if no texture
3610
3611 Raises:
3612 RuntimeError: If material doesn't exist
3613 """
3614 return context_wrapper.getMaterialTexture(self.context, material_label)
3615
3616 def setMaterialTexture(self, material_label: str, texture_file: str):
3617 """
3618 Set the texture file for a material.
3619
3620 This affects all primitives that reference this material.
3622 Args:
3623 material_label: Label of the material
3624 texture_file: Path to texture image file
3625
3626 Raises:
3627 RuntimeError: If material doesn't exist or texture file not found
3628 """
3629 context_wrapper.setMaterialTexture(self.context, material_label, texture_file)
3630
3631 def isMaterialTextureColorOverridden(self, material_label: str) -> bool:
3632 """Check if material texture color is overridden by material color."""
3633 return context_wrapper.isMaterialTextureColorOverridden(self.context, material_label)
3634
3635 def setMaterialTextureColorOverride(self, material_label: str, override: bool):
3636 """Set whether material color overrides texture color."""
3637 context_wrapper.setMaterialTextureColorOverride(self.context, material_label, override)
3638
3639 def getMaterialTwosidedFlag(self, material_label: str) -> int:
3640 """Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3641 return context_wrapper.getMaterialTwosidedFlag(self.context, material_label)
3642
3643 def setMaterialTwosidedFlag(self, material_label: str, twosided_flag: int):
3644 """Set the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided)."""
3645 context_wrapper.setMaterialTwosidedFlag(self.context, material_label, twosided_flag)
3646
3647 def assignMaterialToPrimitive(self, uuid, material_label: str):
3648 """
3649 Assign a material to primitive(s).
3650
3651 Args:
3652 uuid: Single UUID (int) or list of UUIDs (List[int])
3653 material_label: Label of the material to assign
3654
3655 Raises:
3656 RuntimeError: If primitive or material doesn't exist
3657
3658 Example:
3659 >>> context.assignMaterialToPrimitive(uuid, "wood_oak")
3660 >>> context.assignMaterialToPrimitive([uuid1, uuid2, uuid3], "wood_oak")
3661 """
3662 if isinstance(uuid, (list, tuple)):
3663 context_wrapper.assignMaterialToPrimitives(self.context, uuid, material_label)
3664 else:
3665 context_wrapper.assignMaterialToPrimitive(self.context, uuid, material_label)
3666
3667 def assignMaterialToObject(self, objID, material_label: str):
3668 """
3669 Assign a material to all primitives in compound object(s).
3670
3671 Args:
3672 objID: Single object ID (int) or list of object IDs (List[int])
3673 material_label: Label of the material to assign
3674
3675 Raises:
3676 RuntimeError: If object or material doesn't exist
3677
3678 Example:
3679 >>> tree_id = wpt.buildTree(WPTType.LEMON)
3680 >>> context.assignMaterialToObject(tree_id, "tree_bark")
3681 >>> context.assignMaterialToObject([id1, id2], "grass")
3682 """
3683 if isinstance(objID, (list, tuple)):
3684 context_wrapper.assignMaterialToObjects(self.context, objID, material_label)
3685 else:
3686 context_wrapper.assignMaterialToObject(self.context, objID, material_label)
3687
3688 def getPrimitiveMaterialLabel(self, uuid):
3689 """Get the material label assigned to a primitive or multiple primitives.
3691 Args:
3692 uuid: Single UUID (int) or list of UUIDs
3693
3694 Returns:
3695 str for single UUID, or List[str] for list
3696
3697 Raises:
3698 RuntimeError: If primitive doesn't exist
3699 """
3700 if isinstance(uuid, (list, tuple)):
3702 if not uuid:
3703 return []
3704 ptr, offsets, total = context_wrapper.getBatchPrimitiveMaterialLabels(self.context, uuid)
3705 if total == 0 or not ptr:
3706 return ["" for _ in uuid]
3707 full_str = ptr.decode('utf-8') if isinstance(ptr, bytes) else ptr
3708 return [full_str[offsets[i]:offsets[i+1]] for i in range(len(uuid))]
3709 return context_wrapper.getPrimitiveMaterialLabel(self.context, uuid)
3710
3711 def getPrimitiveTwosidedFlag(self, uuid: int, default_value: int = 1) -> int:
3712 """
3713 Get two-sided rendering flag for a primitive.
3714
3715 Checks material first, then primitive data if no material assigned.
3716
3717 Args:
3718 uuid: UUID of the primitive
3719 default_value: Default value if no material/data (default 1 = two-sided)
3720
3721 Returns:
3722 Two-sided flag (0 = one-sided, 1 = two-sided)
3723 """
3724 return context_wrapper.getPrimitiveTwosidedFlag(self.context, uuid, default_value)
3725
3726 def getPrimitivesUsingMaterial(self, material_label: str) -> List[int]:
3727 """
3728 Get all primitive UUIDs that use a specific material.
3729
3730 Args:
3731 material_label: Label of the material
3732
3733 Returns:
3734 List of primitive UUIDs using the material
3735
3736 Raises:
3737 RuntimeError: If material doesn't exist
3738 """
3739 return context_wrapper.getPrimitivesUsingMaterial(self.context, material_label)
3740
3741 # =========================================================================
3742 # Texture Methods
3743 # =========================================================================
3744
3745 def getPrimitiveTextureFile(self, uuid):
3746 """Get the texture file path of a primitive or multiple primitives.
3747
3748 Args:
3749 uuid: Single UUID (int) or list of UUIDs
3750
3751 Returns:
3752 str for single UUID, or List[str] for list
3753 """
3755 if isinstance(uuid, (list, tuple)):
3756 if not uuid:
3757 return []
3758 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureFiles(self.context, uuid)
3759 if total == 0 or not ptr:
3760 return ["" for _ in uuid]
3761 full_str = ptr.decode('utf-8') if isinstance(ptr, bytes) else ptr
3762 return [full_str[offsets[i]:offsets[i+1]] for i in range(len(uuid))]
3763 return context_wrapper.getPrimitiveTextureFile(self.context, uuid)
3764
3765 def resolveMaterialTextures(self, uuids, colors_np):
3766 """Resolve material texture suppression for export.
3767
3768 For each primitive, applies material-based texture suppression rules:
3769 1. If primitive has texture but material has no texture -> suppress texture, use material color
3770 2. If both have texture and textureColorOverride -> prefix "mask:", use material color
3771 3. Otherwise -> leave unchanged
3772
3773 Args:
3774 uuids: List of primitive UUIDs
3775 colors_np: numpy float32 array of shape (N, 3), modified IN-PLACE
3776
3777 Returns:
3778 List[str] of resolved texture file paths
3779 """
3781 if not uuids:
3782 return []
3783 return context_wrapper.resolveMaterialTextures(self.context, uuids, colors_np)
3784
3785 def packGPUBuffers(self, uuids):
3786 """Pack GPU-ready geometry buffers for a set of primitives in a single C++ pass.
3788 Produces a binary blob containing contiguous typed arrays (positions,
3789 colors, uvs, indices, faceToUuid) grouped by texture, ready for
3790 zero-copy loading into Three.js BufferGeometry attributes.
3791
3792 Args:
3793 uuids: List of primitive UUIDs
3794
3795 Returns:
3796 bytes: Raw binary blob (see wire format v2 spec)
3797 """
3799 if not uuids:
3800 return b''
3801 return context_wrapper.packGPUBuffers(self.context, uuids)
3802
3803 def setPrimitiveTextureFile(self, uuid: int, texture_file: str) -> None:
3804 """Set the texture file path of a primitive.
3806 Args:
3807 uuid: UUID of the primitive
3808 texture_file: Path to the texture file
3809 """
3811 context_wrapper.setPrimitiveTextureFile(self.context, uuid, texture_file)
3812
3813 def getPrimitiveTextureSize(self, uuid: int) -> int2:
3814 """Get the texture size (width, height) of a primitive.
3815
3816 Args:
3817 uuid: UUID of the primitive
3818
3819 Returns:
3820 int2 with width and height of the texture
3821 """
3823 w, h = context_wrapper.getPrimitiveTextureSize(self.context, uuid)
3824 return int2(w, h)
3825
3826 def getPrimitiveTextureUV(self, uuid):
3827 """Get the texture UV coordinates of a primitive or multiple primitives.
3828
3829 Args:
3830 uuid: Single UUID (int) or list of UUIDs
3831
3832 Returns:
3833 List[vec2] for single UUID, or tuple of (flat_data, offsets) for list
3834 """
3836 if isinstance(uuid, (list, tuple)):
3837 if not uuid:
3838 return (np.empty((0,), dtype=np.float32), np.zeros((1,), dtype=np.uint32))
3839 ptr, offsets, total = context_wrapper.getBatchPrimitiveTextureUV(self.context, uuid)
3840 offsets_arr = np.array(offsets, dtype=np.uint32)
3841 if total == 0 or not ptr:
3842 return (np.empty((0,), dtype=np.float32), offsets_arr)
3843 data = np.ctypeslib.as_array(ptr, shape=(total,)).copy()
3844 return (data, offsets_arr)
3845 uv_pairs = context_wrapper.getPrimitiveTextureUV(self.context, uuid)
3846 return [vec2(u, v) for u, v in uv_pairs]
3847
3848 def primitiveTextureHasTransparencyChannel(self, uuid: int) -> bool:
3849 """Check if primitive texture has a transparency channel.
3850
3851 Args:
3852 uuid: UUID of the primitive
3853
3854 Returns:
3855 True if texture has transparency channel
3856 """
3858 return context_wrapper.primitiveTextureHasTransparencyChannel(self.context, uuid)
3859
3860 def getPrimitiveSolidFraction(self, uuid):
3861 """Get the solid fraction of a primitive or multiple primitives.
3862
3863 Args:
3864 uuid: Single UUID (int) or list of UUIDs
3865
3866 Returns:
3867 float for single UUID, or np.ndarray of shape (N,) for list
3868 """
3870 if isinstance(uuid, (list, tuple)):
3871 if not uuid:
3872 return np.empty((0,), dtype=np.float32)
3873 ptr, size = context_wrapper.getBatchPrimitiveSolidFractions(self.context, uuid)
3874 if size == 0 or not ptr:
3875 return np.empty((0,), dtype=np.float32)
3876 return np.ctypeslib.as_array(ptr, shape=(size,)).copy()
3877 return context_wrapper.getPrimitiveSolidFraction(self.context, uuid)
3878
3879 def overridePrimitiveTextureColor(self, uuid: int) -> None:
3880 """Override texture color with constant RGB color for a primitive.
3881
3882 Args:
3883 uuid: UUID of the primitive
3884 """
3886 context_wrapper.overridePrimitiveTextureColor(self.context, uuid)
3887
3888 def usePrimitiveTextureColor(self, uuid: int) -> None:
3889 """Use texture map color instead of constant RGB for a primitive.
3890
3891 Args:
3892 uuid: UUID of the primitive
3893 """
3895 context_wrapper.usePrimitiveTextureColor(self.context, uuid)
3896
3897 def isPrimitiveTextureColorOverridden(self, uuid: int) -> bool:
3898 """Check if primitive texture color is overridden.
3899
3900 Args:
3901 uuid: UUID of the primitive
3902
3903 Returns:
3904 True if texture color is overridden with constant RGB
3905 """
3907 return context_wrapper.isPrimitiveTextureColorOverridden(self.context, uuid)
3908
3909 # =========================================================================
3910 # Convenience Methods (getAll*)
3911 # =========================================================================
3912
3913 def getAllPrimitiveNormals(self) -> 'np.ndarray':
3914 """Get normals for all primitives. Returns ndarray of shape (N, 3)."""
3915 return self.getPrimitiveNormal(self.getAllUUIDs())
3916
3917 def getAllPrimitiveColors(self) -> 'np.ndarray':
3918 """Get colors for all primitives. Returns ndarray of shape (N, 3)."""
3919 return self.getPrimitiveColor(self.getAllUUIDs())
3920
3921 def getAllPrimitiveAreas(self) -> 'np.ndarray':
3922 """Get areas for all primitives. Returns ndarray of shape (N,)."""
3923 return self.getPrimitiveArea(self.getAllUUIDs())
3924
3925 def getAllPrimitiveTypes(self) -> 'np.ndarray':
3926 """Get types for all primitives. Returns ndarray of shape (N,) uint32."""
3927 return self.getPrimitiveType(self.getAllUUIDs())
3928
3929 def getAllPrimitiveSolidFractions(self) -> 'np.ndarray':
3930 """Get solid fractions for all primitives. Returns ndarray of shape (N,)."""
3931 return self.getPrimitiveSolidFraction(self.getAllUUIDs())
3932
3933 def getAllPrimitiveVertices(self):
3934 """Get vertices for all primitives. Returns (flat_data, offsets) tuple."""
3935 return self.getPrimitiveVertices(self.getAllUUIDs())
3936
3937 def getAllPrimitiveTextureFiles(self) -> List[str]:
3938 """Get texture files for all primitives. Returns list of strings."""
3939 return self.getPrimitiveTextureFile(self.getAllUUIDs())
3940
3941 def getAllPrimitiveMaterialLabels(self) -> List[str]:
3942 """Get material labels for all primitives. Returns list of strings."""
3943 return self.getPrimitiveMaterialLabel(self.getAllUUIDs())
3944
3945 # ==================== Visibility Methods ====================
3947 def hidePrimitive(self, uuids_or_uuid) -> None:
3948 """Hide one or more primitives. Hidden primitives are excluded from getAllUUIDs().
3949
3950 Args:
3951 uuids_or_uuid: Single UUID (int) or list of UUIDs to hide.
3952 """
3953 if isinstance(uuids_or_uuid, (list, tuple)):
3954 context_wrapper.hidePrimitivesWrapper(self.context, list(uuids_or_uuid))
3955 else:
3956 context_wrapper.hidePrimitiveWrapper(self.context, uuids_or_uuid)
3957
3958 def showPrimitive(self, uuids_or_uuid) -> None:
3959 """Show one or more previously hidden primitives.
3961 Args:
3962 uuids_or_uuid: Single UUID (int) or list of UUIDs to show.
3963 """
3964 if isinstance(uuids_or_uuid, (list, tuple)):
3965 context_wrapper.showPrimitivesWrapper(self.context, list(uuids_or_uuid))
3966 else:
3967 context_wrapper.showPrimitiveWrapper(self.context, uuids_or_uuid)
3968
3969 def isPrimitiveHidden(self, uuid: int) -> bool:
3970 """Check if a primitive is hidden.
3972 Args:
3973 uuid: UUID of the primitive.
3974
3975 Returns:
3976 True if the primitive is hidden.
3977 """
3978 return context_wrapper.isPrimitiveHiddenWrapper(self.context, uuid)
3979
3980 def hideObject(self, objids_or_objid) -> None:
3981 """Hide one or more compound objects (and all their primitives).
3982
3983 Args:
3984 objids_or_objid: Single object ID (int) or list of object IDs to hide.
3985 """
3986 if isinstance(objids_or_objid, (list, tuple)):
3987 context_wrapper.hideObjectsWrapper(self.context, list(objids_or_objid))
3988 else:
3989 context_wrapper.hideObjectWrapper(self.context, objids_or_objid)
3990
3991 def showObject(self, objids_or_objid) -> None:
3992 """Show one or more previously hidden compound objects.
3994 Args:
3995 objids_or_objid: Single object ID (int) or list of object IDs to show.
3996 """
3997 if isinstance(objids_or_objid, (list, tuple)):
3998 context_wrapper.showObjectsWrapper(self.context, list(objids_or_objid))
3999 else:
4000 context_wrapper.showObjectWrapper(self.context, objids_or_objid)
4001
4002 def isObjectHidden(self, objID: int) -> bool:
4003 """Check if a compound object is hidden.
4005 Args:
4006 objID: Object ID.
4007
4008 Returns:
4009 True if the object is hidden.
4010 """
4011 return context_wrapper.isObjectHiddenWrapper(self.context, objID)
4012
4013 # ==================== Object Data Methods ====================
4014
4015 def setObjectDataInt(self, objids_or_objid, label: str, value: int) -> None:
4016 """Set object data as signed 32-bit integer for one or multiple objects."""
4017 if isinstance(objids_or_objid, (list, tuple)):
4018 context_wrapper.setBroadcastObjectDataInt(self.context, objids_or_objid, label, value)
4019 else:
4020 context_wrapper.setObjectDataInt(self.context, objids_or_objid, label, value)
4021
4022 def setObjectDataUInt(self, objids_or_objid, label: str, value: int) -> None:
4023 """Set object data as unsigned 32-bit integer for one or multiple objects."""
4024 if isinstance(objids_or_objid, (list, tuple)):
4025 context_wrapper.setBroadcastObjectDataUInt(self.context, objids_or_objid, label, value)
4026 else:
4027 context_wrapper.setObjectDataUInt(self.context, objids_or_objid, label, value)
4028
4029 def setObjectDataFloat(self, objids_or_objid, label: str, value: float) -> None:
4030 """Set object data as 32-bit float for one or multiple objects."""
4031 if isinstance(objids_or_objid, (list, tuple)):
4032 context_wrapper.setBroadcastObjectDataFloat(self.context, objids_or_objid, label, value)
4033 else:
4034 context_wrapper.setObjectDataFloat(self.context, objids_or_objid, label, value)
4035
4036 def setObjectDataDouble(self, objids_or_objid, label: str, value: float) -> None:
4037 """Set object data as 64-bit double for one or multiple objects."""
4038 if isinstance(objids_or_objid, (list, tuple)):
4039 context_wrapper.setBroadcastObjectDataDouble(self.context, objids_or_objid, label, value)
4040 else:
4041 context_wrapper.setObjectDataDouble(self.context, objids_or_objid, label, value)
4042
4043 def setObjectDataString(self, objids_or_objid, label: str, value: str) -> None:
4044 """Set object data as string for one or multiple objects."""
4045 if isinstance(objids_or_objid, (list, tuple)):
4046 context_wrapper.setBroadcastObjectDataString(self.context, objids_or_objid, label, value)
4047 else:
4048 context_wrapper.setObjectDataString(self.context, objids_or_objid, label, value)
4049
4050 def setObjectDataVec2(self, objids_or_objid, label: str, x_or_vec, y: float = None) -> None:
4051 """Set object data as vec2. Accepts vec2 object or x,y components."""
4052 if hasattr(x_or_vec, 'x') and y is None:
4053 x, y = x_or_vec.x, x_or_vec.y
4054 else:
4055 x = x_or_vec
4056 if isinstance(objids_or_objid, (list, tuple)):
4057 context_wrapper.setBroadcastObjectDataVec2(self.context, objids_or_objid, label, x, y)
4058 else:
4059 context_wrapper.setObjectDataVec2(self.context, objids_or_objid, label, x, y)
4060
4061 def setObjectDataVec3(self, objids_or_objid, label: str, x_or_vec, y: float = None, z: float = None) -> None:
4062 """Set object data as vec3. Accepts vec3 object or x,y,z components."""
4063 if hasattr(x_or_vec, 'x') and y is None:
4064 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4065 else:
4066 x = x_or_vec
4067 if isinstance(objids_or_objid, (list, tuple)):
4068 context_wrapper.setBroadcastObjectDataVec3(self.context, objids_or_objid, label, x, y, z)
4069 else:
4070 context_wrapper.setObjectDataVec3(self.context, objids_or_objid, label, x, y, z)
4071
4072 def setObjectDataVec4(self, objids_or_objid, label: str, x_or_vec, y: float = None, z: float = None, w: float = None) -> None:
4073 """Set object data as vec4. Accepts vec4 object or x,y,z,w components."""
4074 if hasattr(x_or_vec, 'x') and y is None:
4075 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4076 else:
4077 x = x_or_vec
4078 if isinstance(objids_or_objid, (list, tuple)):
4079 context_wrapper.setBroadcastObjectDataVec4(self.context, objids_or_objid, label, x, y, z, w)
4080 else:
4081 context_wrapper.setObjectDataVec4(self.context, objids_or_objid, label, x, y, z, w)
4082
4083 def setObjectDataInt2(self, objids_or_objid, label: str, x_or_vec, y: int = None) -> None:
4084 """Set object data as int2. Accepts int2 object or x,y components."""
4085 if hasattr(x_or_vec, 'x') and y is None:
4086 x, y = x_or_vec.x, x_or_vec.y
4087 else:
4088 x = x_or_vec
4089 if isinstance(objids_or_objid, (list, tuple)):
4090 context_wrapper.setBroadcastObjectDataInt2(self.context, objids_or_objid, label, x, y)
4091 else:
4092 context_wrapper.setObjectDataInt2(self.context, objids_or_objid, label, x, y)
4093
4094 def setObjectDataInt3(self, objids_or_objid, label: str, x_or_vec, y: int = None, z: int = None) -> None:
4095 """Set object data as int3. Accepts int3 object or x,y,z components."""
4096 if hasattr(x_or_vec, 'x') and y is None:
4097 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4098 else:
4099 x = x_or_vec
4100 if isinstance(objids_or_objid, (list, tuple)):
4101 context_wrapper.setBroadcastObjectDataInt3(self.context, objids_or_objid, label, x, y, z)
4102 else:
4103 context_wrapper.setObjectDataInt3(self.context, objids_or_objid, label, x, y, z)
4104
4105 def setObjectDataInt4(self, objids_or_objid, label: str, x_or_vec, y: int = None, z: int = None, w: int = None) -> None:
4106 """Set object data as int4. Accepts int4 object or x,y,z,w components."""
4107 if hasattr(x_or_vec, 'x') and y is None:
4108 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4109 else:
4110 x = x_or_vec
4111 if isinstance(objids_or_objid, (list, tuple)):
4112 context_wrapper.setBroadcastObjectDataInt4(self.context, objids_or_objid, label, x, y, z, w)
4113 else:
4114 context_wrapper.setObjectDataInt4(self.context, objids_or_objid, label, x, y, z, w)
4115
4116 def getObjectData(self, objID: int, label: str, data_type: type = None):
4117 """Get object data with optional type specification. Auto-detects type if not specified."""
4118 if data_type is None:
4119 return context_wrapper.getObjectDataAuto(self.context, objID, label)
4120 if data_type == int:
4121 return context_wrapper.getObjectDataInt(self.context, objID, label)
4122 elif data_type == float:
4123 return context_wrapper.getObjectDataFloat(self.context, objID, label)
4124 elif data_type == str:
4125 return context_wrapper.getObjectDataString(self.context, objID, label)
4126 elif data_type == vec3:
4127 coords = context_wrapper.getObjectDataVec3(self.context, objID, label)
4128 return vec3(coords[0], coords[1], coords[2])
4129 elif data_type == vec2:
4130 coords = context_wrapper.getObjectDataVec2(self.context, objID, label)
4131 return vec2(coords[0], coords[1])
4132 elif data_type == vec4:
4133 coords = context_wrapper.getObjectDataVec4(self.context, objID, label)
4134 return vec4(coords[0], coords[1], coords[2], coords[3])
4135 elif data_type == int2:
4136 coords = context_wrapper.getObjectDataInt2(self.context, objID, label)
4137 return int2(coords[0], coords[1])
4138 elif data_type == int3:
4139 coords = context_wrapper.getObjectDataInt3(self.context, objID, label)
4140 return int3(coords[0], coords[1], coords[2])
4141 elif data_type == int4:
4142 coords = context_wrapper.getObjectDataInt4(self.context, objID, label)
4143 return int4(coords[0], coords[1], coords[2], coords[3])
4144 elif data_type == "uint":
4145 return context_wrapper.getObjectDataUInt(self.context, objID, label)
4146 elif data_type == "double":
4147 return context_wrapper.getObjectDataDouble(self.context, objID, label)
4148 else:
4149 raise ValueError(f"Unsupported object data type: {data_type}")
4150
4151 def getObjectDataFloat(self, objID: int, label: str) -> float:
4152 """Get float object data."""
4153 return context_wrapper.getObjectDataFloat(self.context, objID, label)
4154
4155 def getObjectDataInt(self, objID: int, label: str) -> int:
4156 """Get int object data."""
4157 return context_wrapper.getObjectDataInt(self.context, objID, label)
4158
4159 def getObjectDataString(self, objID: int, label: str) -> str:
4160 """Get string object data."""
4161 return context_wrapper.getObjectDataString(self.context, objID, label)
4162
4163 def getObjectDataType(self, objID: int, label: str) -> int:
4164 """Get the HeliosDataType enum for object data."""
4165 return context_wrapper.getObjectDataTypeWrapper(self.context, objID, label)
4166
4167 def getObjectDataSize(self, objID: int, label: str) -> int:
4168 """Get the size of object data array."""
4169 return context_wrapper.getObjectDataSizeWrapper(self.context, objID, label)
4170
4171 def doesObjectDataExist(self, objID: int, label: str) -> bool:
4172 """Check if object data exists."""
4173 return context_wrapper.doesObjectDataExistWrapper(self.context, objID, label)
4174
4175 def clearObjectData(self, objids_or_objid, label: str) -> None:
4176 """Clear object data. Accepts single ID or list."""
4177 if isinstance(objids_or_objid, (list, tuple)):
4178 context_wrapper.clearObjectDataBatchWrapper(self.context, objids_or_objid, label)
4179 else:
4180 context_wrapper.clearObjectDataWrapper(self.context, objids_or_objid, label)
4181
4182 def listObjectData(self, objID: int) -> List[str]:
4183 """List all data labels on a specific object."""
4184 return context_wrapper.listObjectDataWrapper(self.context, objID)
4185
4186 def listAllObjectDataLabels(self) -> List[str]:
4187 """List all object data labels in context."""
4188 return context_wrapper.listAllObjectDataLabelsWrapper(self.context)
4189
4190 def duplicateObjectData(self, objID: int, old_label: str, new_label: str) -> None:
4191 """Copy object data to a new label."""
4192 context_wrapper.duplicateObjectDataWrapper(self.context, objID, old_label, new_label)
4193
4194 def renameObjectData(self, objID: int, old_label: str, new_label: str) -> None:
4195 """Rename an object data label."""
4196 context_wrapper.renameObjectDataWrapper(self.context, objID, old_label, new_label)
4197
4198 def filterObjectsByData(self, objIDs: List[int], label: str, value, comparator: str = "=") -> List[int]:
4199 """Filter objects by data value. Auto-dispatches based on value type."""
4200 if isinstance(value, str):
4201 return context_wrapper.filterObjectsByDataStringWrapper(self.context, objIDs, label, value)
4202 elif isinstance(value, float):
4203 return context_wrapper.filterObjectsByDataFloatWrapper(self.context, objIDs, label, value, comparator)
4204 elif isinstance(value, int):
4205 return context_wrapper.filterObjectsByDataIntWrapper(self.context, objIDs, label, value, comparator)
4206 else:
4207 raise ValueError(f"Unsupported filter value type: {type(value).__name__}")
4208
4209 # ==================== Global Data Methods ====================
4210
4211 def setGlobalDataInt(self, label: str, value: int) -> None:
4212 """Set global data as signed 32-bit integer."""
4213 context_wrapper.setGlobalDataInt(self.context, label, value)
4214
4215 def setGlobalDataUInt(self, label: str, value: int) -> None:
4216 """Set global data as unsigned 32-bit integer."""
4217 context_wrapper.setGlobalDataUInt(self.context, label, value)
4218
4219 def setGlobalDataFloat(self, label: str, value: float) -> None:
4220 """Set global data as 32-bit float."""
4221 context_wrapper.setGlobalDataFloat(self.context, label, value)
4222
4223 def setGlobalDataDouble(self, label: str, value: float) -> None:
4224 """Set global data as 64-bit double."""
4225 context_wrapper.setGlobalDataDouble(self.context, label, value)
4226
4227 def setGlobalDataString(self, label: str, value: str) -> None:
4228 """Set global data as string."""
4229 context_wrapper.setGlobalDataString(self.context, label, value)
4230
4231 def setGlobalDataVec2(self, label: str, x_or_vec, y: float = None) -> None:
4232 """Set global data as vec2."""
4233 if hasattr(x_or_vec, 'x') and y is None:
4234 x, y = x_or_vec.x, x_or_vec.y
4235 else:
4236 x = x_or_vec
4237 context_wrapper.setGlobalDataVec2(self.context, label, x, y)
4238
4239 def setGlobalDataVec3(self, label: str, x_or_vec, y: float = None, z: float = None) -> None:
4240 """Set global data as vec3."""
4241 if hasattr(x_or_vec, 'x') and y is None:
4242 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4243 else:
4244 x = x_or_vec
4245 context_wrapper.setGlobalDataVec3(self.context, label, x, y, z)
4246
4247 def setGlobalDataVec4(self, label: str, x_or_vec, y: float = None, z: float = None, w: float = None) -> None:
4248 """Set global data as vec4."""
4249 if hasattr(x_or_vec, 'x') and y is None:
4250 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4251 else:
4252 x = x_or_vec
4253 context_wrapper.setGlobalDataVec4(self.context, label, x, y, z, w)
4254
4255 def setGlobalDataInt2(self, label: str, x_or_vec, y: int = None) -> None:
4256 """Set global data as int2."""
4257 if hasattr(x_or_vec, 'x') and y is None:
4258 x, y = x_or_vec.x, x_or_vec.y
4259 else:
4260 x = x_or_vec
4261 context_wrapper.setGlobalDataInt2(self.context, label, x, y)
4262
4263 def setGlobalDataInt3(self, label: str, x_or_vec, y: int = None, z: int = None) -> None:
4264 """Set global data as int3."""
4265 if hasattr(x_or_vec, 'x') and y is None:
4266 x, y, z = x_or_vec.x, x_or_vec.y, x_or_vec.z
4267 else:
4268 x = x_or_vec
4269 context_wrapper.setGlobalDataInt3(self.context, label, x, y, z)
4270
4271 def setGlobalDataInt4(self, label: str, x_or_vec, y: int = None, z: int = None, w: int = None) -> None:
4272 """Set global data as int4."""
4273 if hasattr(x_or_vec, 'x') and y is None:
4274 x, y, z, w = x_or_vec.x, x_or_vec.y, x_or_vec.z, x_or_vec.w
4275 else:
4276 x = x_or_vec
4277 context_wrapper.setGlobalDataInt4(self.context, label, x, y, z, w)
4278
4279 def getGlobalData(self, label: str, data_type: type = None):
4280 """Get global data with optional type specification. Auto-detects type if not specified."""
4281 if data_type is None:
4282 return context_wrapper.getGlobalDataAuto(self.context, label)
4283 if data_type == int:
4284 return context_wrapper.getGlobalDataInt(self.context, label)
4285 elif data_type == float:
4286 return context_wrapper.getGlobalDataFloat(self.context, label)
4287 elif data_type == str:
4288 return context_wrapper.getGlobalDataString(self.context, label)
4289 elif data_type == vec3:
4290 coords = context_wrapper.getGlobalDataVec3(self.context, label)
4291 return vec3(coords[0], coords[1], coords[2])
4292 elif data_type == vec2:
4293 coords = context_wrapper.getGlobalDataVec2(self.context, label)
4294 return vec2(coords[0], coords[1])
4295 elif data_type == vec4:
4296 coords = context_wrapper.getGlobalDataVec4(self.context, label)
4297 return vec4(coords[0], coords[1], coords[2], coords[3])
4298 elif data_type == int2:
4299 coords = context_wrapper.getGlobalDataInt2(self.context, label)
4300 return int2(coords[0], coords[1])
4301 elif data_type == int3:
4302 coords = context_wrapper.getGlobalDataInt3(self.context, label)
4303 return int3(coords[0], coords[1], coords[2])
4304 elif data_type == int4:
4305 coords = context_wrapper.getGlobalDataInt4(self.context, label)
4306 return int4(coords[0], coords[1], coords[2], coords[3])
4307 elif data_type == "uint":
4308 return context_wrapper.getGlobalDataUInt(self.context, label)
4309 elif data_type == "double":
4310 return context_wrapper.getGlobalDataDouble(self.context, label)
4311 else:
4312 raise ValueError(f"Unsupported global data type: {data_type}")
4313
4314 def getGlobalDataFloat(self, label: str) -> float:
4315 """Get float global data."""
4316 return context_wrapper.getGlobalDataFloat(self.context, label)
4317
4318 def getGlobalDataInt(self, label: str) -> int:
4319 """Get int global data."""
4320 return context_wrapper.getGlobalDataInt(self.context, label)
4321
4322 def getGlobalDataString(self, label: str) -> str:
4323 """Get string global data."""
4324 return context_wrapper.getGlobalDataString(self.context, label)
4325
4326 def getGlobalDataType(self, label: str) -> int:
4327 """Get the HeliosDataType enum for global data."""
4328 return context_wrapper.getGlobalDataTypeWrapper(self.context, label)
4329
4330 def getGlobalDataSize(self, label: str) -> int:
4331 """Get the size of global data array."""
4332 return context_wrapper.getGlobalDataSizeWrapper(self.context, label)
4333
4334 def doesGlobalDataExist(self, label: str) -> bool:
4335 """Check if global data exists."""
4336 return context_wrapper.doesGlobalDataExistWrapper(self.context, label)
4337
4338 def clearGlobalData(self, label: str) -> None:
4339 """Clear global data."""
4340 context_wrapper.clearGlobalDataWrapper(self.context, label)
4341
4342 def renameGlobalData(self, old_label: str, new_label: str) -> None:
4343 """Rename a global data label."""
4344 context_wrapper.renameGlobalDataWrapper(self.context, old_label, new_label)
4345
4346 def duplicateGlobalData(self, old_label: str, new_label: str) -> None:
4347 """Duplicate global data to a new label."""
4348 context_wrapper.duplicateGlobalDataWrapper(self.context, old_label, new_label)
4349
4350 def listGlobalData(self) -> List[str]:
4351 """List all global data labels."""
4352 return context_wrapper.listGlobalDataWrapper(self.context)
4353
4354 def incrementGlobalData(self, label: str, increment) -> None:
4355 """Increment global data. Auto-dispatches based on increment type."""
4356 if isinstance(increment, float):
4357 context_wrapper.incrementGlobalDataFloatWrapper(self.context, label, increment)
4358 elif isinstance(increment, int):
4359 context_wrapper.incrementGlobalDataIntWrapper(self.context, label, increment)
4360 else:
4361 raise ValueError(f"Unsupported increment type: {type(increment).__name__}")
4362
4363 # ==================== Primitive Data Statistics & Filtering ====================
4364
4365 def calculatePrimitiveDataMean(self, uuids: List[int], label: str, return_type: type = float):
4366 """Calculate arithmetic mean of primitive data across UUIDs.
4367
4368 Args:
4369 uuids: List of primitive UUIDs.
4370 label: Data label.
4371 return_type: float (default), "double", or vec3.
4372 """
4373 if return_type == float:
4374 return context_wrapper.calculatePrimitiveDataMeanFloatWrapper(self.context, uuids, label)
4375 elif return_type == "double":
4376 return context_wrapper.calculatePrimitiveDataMeanDoubleWrapper(self.context, uuids, label)
4377 elif return_type == vec3:
4378 coords = context_wrapper.calculatePrimitiveDataMeanVec3Wrapper(self.context, uuids, label)
4379 return vec3(coords[0], coords[1], coords[2])
4380 else:
4381 raise ValueError(f"Unsupported return type: {return_type}")
4382
4383 def calculatePrimitiveDataAreaWeightedMean(self, uuids: List[int], label: str, return_type: type = float):
4384 """Calculate area-weighted mean of primitive data."""
4385 if return_type == float:
4386 return context_wrapper.calculatePrimitiveDataAreaWeightedMeanFloatWrapper(self.context, uuids, label)
4387 else:
4388 raise ValueError(f"Unsupported return type: {return_type}")
4389
4390 def calculatePrimitiveDataSum(self, uuids: List[int], label: str, return_type: type = float):
4391 """Calculate sum of primitive data across UUIDs."""
4392 if return_type == float:
4393 return context_wrapper.calculatePrimitiveDataSumFloatWrapper(self.context, uuids, label)
4394 elif return_type == "double":
4395 return context_wrapper.calculatePrimitiveDataSumDoubleWrapper(self.context, uuids, label)
4396 else:
4397 raise ValueError(f"Unsupported return type: {return_type}")
4398
4399 def calculatePrimitiveDataAreaWeightedSum(self, uuids: List[int], label: str, return_type: type = float):
4400 """Calculate area-weighted sum of primitive data."""
4401 if return_type == float:
4402 return context_wrapper.calculatePrimitiveDataAreaWeightedSumFloatWrapper(self.context, uuids, label)
4403 else:
4404 raise ValueError(f"Unsupported return type: {return_type}")
4405
4406 def scalePrimitiveData(self, uuids_or_label, label_or_factor, factor=None) -> None:
4407 """Scale primitive data by a factor.
4409 Overloads:
4410 scalePrimitiveData(uuids, label, factor) - scale for specific UUIDs
4411 scalePrimitiveData(label, factor) - scale for ALL primitives
4412 """
4413 if isinstance(uuids_or_label, str):
4414 context_wrapper.scalePrimitiveDataAllWrapper(self.context, uuids_or_label, label_or_factor)
4415 else:
4416 context_wrapper.scalePrimitiveDataWithUUIDsWrapper(self.context, uuids_or_label, label_or_factor, factor)
4417
4418 def incrementPrimitiveData(self, uuids: List[int], label: str, increment) -> None:
4419 """Increment primitive data. Auto-dispatches based on increment type."""
4420 if isinstance(increment, float):
4421 context_wrapper.incrementPrimitiveDataFloatWrapper(self.context, uuids, label, increment)
4422 elif isinstance(increment, int):
4423 context_wrapper.incrementPrimitiveDataIntWrapper(self.context, uuids, label, increment)
4424 else:
4425 raise ValueError(f"Unsupported increment type: {type(increment).__name__}")
4426
4427 def aggregatePrimitiveDataSum(self, uuids: List[int], labels: List[str], result_label: str) -> None:
4428 """Sum multiple primitive data fields into a new field."""
4429 context_wrapper.aggregatePrimitiveDataSumWrapper(self.context, uuids, labels, result_label)
4430
4431 def aggregatePrimitiveDataProduct(self, uuids: List[int], labels: List[str], result_label: str) -> None:
4432 """Multiply multiple primitive data fields into a new field."""
4433 context_wrapper.aggregatePrimitiveDataProductWrapper(self.context, uuids, labels, result_label)
4434
4435 def sumPrimitiveSurfaceArea(self, uuids: List[int]) -> float:
4436 """Calculate total one-sided surface area for a set of primitives."""
4437 return context_wrapper.sumPrimitiveSurfaceAreaWrapper(self.context, uuids)
4438
4439 def filterPrimitivesByData(self, uuids: List[int], label: str, value, comparator: str = "=") -> List[int]:
4440 """Filter primitives by data value. Auto-dispatches based on value type.
4441
4442 Args:
4443 uuids: UUIDs to filter.
4444 label: Data label to compare.
4445 value: Filter value (float, int, or str).
4446 comparator: Comparison operator ("=", "<", ">", "<=", ">="). Not used for strings.
4447 """
4448 if isinstance(value, str):
4449 return context_wrapper.filterPrimitivesByDataStringWrapper(self.context, uuids, label, value)
4450 elif isinstance(value, float):
4451 return context_wrapper.filterPrimitivesByDataFloatWrapper(self.context, uuids, label, value, comparator)
4452 elif isinstance(value, int):
4453 return context_wrapper.filterPrimitivesByDataIntWrapper(self.context, uuids, label, value, comparator)
4454 else:
4455 raise ValueError(f"Unsupported filter value type: {type(value).__name__}")
4456
4457 # ==================== Object Geometry Queries ====================
4458
4459 def getObjectType(self, objID: int) -> int:
4460 """Return the integer-coded `helios::ObjectType` of a compound object.
4461
4462 Values follow the C++ `helios::ObjectType` enum
4463 (0=tile, 1=sphere, 2=tube, 3=box, 4=disk, 5=polymesh, 6=cone).
4464 """
4466 return context_wrapper.getObjectTypeWrapper(self.context, objID)
4467
4468 def getObjectCenter(self, objID: int) -> vec3:
4470 x, y, z = context_wrapper.getObjectCenterWrapper(self.context, objID)
4471 return vec3(x, y, z)
4473 def getObjectBoundingBox(self, objIDs):
4474 """Get axis-aligned bounding box for one object or a list of objects.
4475
4476 Args:
4477 objIDs: Single object ID (int) or list of object IDs.
4478
4479 Returns:
4480 Tuple of (min_corner: vec3, max_corner: vec3).
4481 """
4483 if isinstance(objIDs, (list, tuple)):
4484 mn, mx = context_wrapper.getObjectBoundingBoxBatchWrapper(self.context, list(objIDs))
4485 else:
4486 mn, mx = context_wrapper.getObjectBoundingBoxWrapper(self.context, objIDs)
4487 return (vec3(mn[0], mn[1], mn[2]), vec3(mx[0], mx[1], mx[2]))
4488
4489 def getObjectPrimitiveUUIDs(self, objIDs) -> List[int]:
4490 """Get flattened primitive UUIDs for one object, a list of objects, or a list-of-lists.
4491
4492 Args:
4493 objIDs: int, List[int], or List[List[int]].
4494
4495 Returns:
4496 Flat list of primitive UUIDs (union across all objects).
4497 """
4499 if isinstance(objIDs, (list, tuple)) and objIDs and isinstance(objIDs[0], (list, tuple)):
4500 return context_wrapper.getObjectPrimitiveUUIDsNestedWrapper(self.context, [list(x) for x in objIDs])
4501 if isinstance(objIDs, (list, tuple)):
4502 return context_wrapper.getObjectPrimitiveUUIDsBatchWrapper(self.context, list(objIDs))
4503 return context_wrapper.getObjectPrimitiveUUIDs(self.context, int(objIDs))
4504
4505 # Tile
4506 def getTileObjectAreaRatio(self, objIDs):
4507 """Get tile-object area ratio for one or multiple tile objects."""
4509 if isinstance(objIDs, (list, tuple)):
4510 return context_wrapper.getTileObjectAreaRatioBatchWrapper(self.context, list(objIDs))
4511 return context_wrapper.getTileObjectAreaRatioWrapper(self.context, objIDs)
4512
4513 def getTileObjectCenter(self, objID: int) -> vec3:
4515 x, y, z = context_wrapper.getTileObjectCenterWrapper(self.context, objID)
4516 return vec3(x, y, z)
4517
4518 def getTileObjectSize(self, objID: int) -> vec2:
4520 x, y = context_wrapper.getTileObjectSizeWrapper(self.context, objID)
4521 return vec2(x, y)
4522
4523 def getTileObjectSubdivisionCount(self, objID: int) -> int2:
4525 x, y = context_wrapper.getTileObjectSubdivisionCountWrapper(self.context, objID)
4526 return int2(x, y)
4527
4528 def getTileObjectNormal(self, objID: int) -> vec3:
4530 x, y, z = context_wrapper.getTileObjectNormalWrapper(self.context, objID)
4531 return vec3(x, y, z)
4532
4533 def getTileObjectTextureUV(self, objID: int) -> List[vec2]:
4535 pairs = context_wrapper.getTileObjectTextureUVWrapper(self.context, objID)
4536 return [vec2(u, v) for u, v in pairs]
4537
4538 def getTileObjectVertices(self, objID: int) -> List[vec3]:
4540 triples = context_wrapper.getTileObjectVerticesWrapper(self.context, objID)
4541 return [vec3(x, y, z) for x, y, z in triples]
4542
4543 # Sphere
4544 def getSphereObjectCenter(self, objID: int) -> vec3:
4546 x, y, z = context_wrapper.getSphereObjectCenterWrapper(self.context, objID)
4547 return vec3(x, y, z)
4548
4549 def getSphereObjectRadius(self, objID: int) -> vec3:
4550 """Get per-axis radii of a sphere object.
4551
4552 Note: Helios spheres are spheroids with three independent radii (rx, ry, rz).
4553 Returns a vec3 (not a scalar).
4554 """
4556 x, y, z = context_wrapper.getSphereObjectRadiusWrapper(self.context, objID)
4557 return vec3(x, y, z)
4558
4559 def getSphereObjectSubdivisionCount(self, objID: int) -> int:
4561 return context_wrapper.getSphereObjectSubdivisionCountWrapper(self.context, objID)
4563 def getSphereObjectVolume(self, objID: int) -> float:
4565 return context_wrapper.getSphereObjectVolumeWrapper(self.context, objID)
4566
4567 # Box
4568 def getBoxObjectCenter(self, objID: int) -> vec3:
4570 x, y, z = context_wrapper.getBoxObjectCenterWrapper(self.context, objID)
4571 return vec3(x, y, z)
4572
4573 def getBoxObjectSize(self, objID: int) -> vec3:
4575 x, y, z = context_wrapper.getBoxObjectSizeWrapper(self.context, objID)
4576 return vec3(x, y, z)
4577
4578 def getBoxObjectSubdivisionCount(self, objID: int) -> int3:
4580 x, y, z = context_wrapper.getBoxObjectSubdivisionCountWrapper(self.context, objID)
4581 return int3(x, y, z)
4582
4583 def getBoxObjectVolume(self, objID: int) -> float:
4585 return context_wrapper.getBoxObjectVolumeWrapper(self.context, objID)
4587 # Disk
4588 def getDiskObjectCenter(self, objID: int) -> vec3:
4590 x, y, z = context_wrapper.getDiskObjectCenterWrapper(self.context, objID)
4591 return vec3(x, y, z)
4592
4593 def getDiskObjectSize(self, objID: int) -> vec2:
4595 x, y = context_wrapper.getDiskObjectSizeWrapper(self.context, objID)
4596 return vec2(x, y)
4597
4598 def getDiskObjectSubdivisionCount(self, objID: int) -> int:
4600 return context_wrapper.getDiskObjectSubdivisionCountWrapper(self.context, objID)
4602 # Tube
4603 def getTubeObjectSubdivisionCount(self, objID: int) -> int:
4605 return context_wrapper.getTubeObjectSubdivisionCountWrapper(self.context, objID)
4607 def getTubeObjectNodeCount(self, objID: int) -> int:
4609 return context_wrapper.getTubeObjectNodeCountWrapper(self.context, objID)
4610
4611 def getTubeObjectNodes(self, objID: int) -> List[vec3]:
4613 triples = context_wrapper.getTubeObjectNodesWrapper(self.context, objID)
4614 return [vec3(x, y, z) for x, y, z in triples]
4616 def getTubeObjectNodeRadii(self, objID: int) -> List[float]:
4618 return context_wrapper.getTubeObjectNodeRadiiWrapper(self.context, objID)
4620 def getTubeObjectNodeColors(self, objID: int) -> List[RGBcolor]:
4622 triples = context_wrapper.getTubeObjectNodeColorsWrapper(self.context, objID)
4623 return [RGBcolor(r, g, b) for r, g, b in triples]
4625 def getTubeObjectVolume(self, objID: int) -> float:
4627 return context_wrapper.getTubeObjectVolumeWrapper(self.context, objID)
4629 def getTubeObjectSegmentVolume(self, objID: int, segment_index: int) -> float:
4631 return context_wrapper.getTubeObjectSegmentVolumeWrapper(self.context, objID, segment_index)
4632
4633 # Cone
4634 def getConeObjectSubdivisionCount(self, objID: int) -> int:
4636 return context_wrapper.getConeObjectSubdivisionCountWrapper(self.context, objID)
4638 def getConeObjectNodes(self, objID: int) -> List[vec3]:
4640 triples = context_wrapper.getConeObjectNodesWrapper(self.context, objID)
4641 return [vec3(x, y, z) for x, y, z in triples]
4643 def getConeObjectNodeRadii(self, objID: int) -> List[float]:
4645 return context_wrapper.getConeObjectNodeRadiiWrapper(self.context, objID)
4647 def getConeObjectNode(self, objID: int, number: int) -> vec3:
4649 x, y, z = context_wrapper.getConeObjectNodeWrapper(self.context, objID, number)
4650 return vec3(x, y, z)
4652 def getConeObjectNodeRadius(self, objID: int, number: int) -> float:
4654 return context_wrapper.getConeObjectNodeRadiusWrapper(self.context, objID, number)
4656 def getConeObjectAxisUnitVector(self, objID: int) -> vec3:
4658 x, y, z = context_wrapper.getConeObjectAxisUnitVectorWrapper(self.context, objID)
4659 return vec3(x, y, z)
4661 def getConeObjectLength(self, objID: int) -> float:
4663 return context_wrapper.getConeObjectLengthWrapper(self.context, objID)
4665 def getConeObjectVolume(self, objID: int) -> float:
4667 return context_wrapper.getConeObjectVolumeWrapper(self.context, objID)
4668
4669 # ==================== Primitive Geometry Queries ====================
4670
4671 def getPatchCenter(self, uuid: int) -> vec3:
4673 x, y, z = context_wrapper.getPatchCenterWrapper(self.context, uuid)
4674 return vec3(x, y, z)
4675
4676 def getPatchSize(self, uuid: int) -> vec2:
4678 x, y = context_wrapper.getPatchSizeWrapper(self.context, uuid)
4679 return vec2(x, y)
4680
4681 def getTriangleVertex(self, uuid: int, number: int) -> vec3:
4683 x, y, z = context_wrapper.getTriangleVertexWrapper(self.context, uuid, number)
4684 return vec3(x, y, z)
4685
4686 def getVoxelCenter(self, uuid: int) -> vec3:
4688 x, y, z = context_wrapper.getVoxelCenterWrapper(self.context, uuid)
4689 return vec3(x, y, z)
4690
4691 def getVoxelSize(self, uuid: int) -> vec3:
4693 x, y, z = context_wrapper.getVoxelSizeWrapper(self.context, uuid)
4694 return vec3(x, y, z)
4695
4696 def getPatchCount(self, include_hidden: bool = True) -> int:
4698 return context_wrapper.getPatchCountWrapper(self.context, include_hidden)
4700 def getTriangleCount(self, include_hidden: bool = True) -> int:
4702 return context_wrapper.getTriangleCountWrapper(self.context, include_hidden)
4703
4704 def getPrimitiveBoundingBox(self, uuids):
4705 """Get axis-aligned bounding box for one primitive or a list of primitives.
4706
4707 Args:
4708 uuids: Single UUID (int) or list of UUIDs.
4709
4710 Returns:
4711 Tuple of (min_corner: vec3, max_corner: vec3).
4712 """
4714 if isinstance(uuids, (list, tuple)):
4715 mn, mx = context_wrapper.getPrimitiveBoundingBoxBatchWrapper(self.context, list(uuids))
4716 else:
4717 mn, mx = context_wrapper.getPrimitiveBoundingBoxWrapper(self.context, uuids)
4718 return (vec3(mn[0], mn[1], mn[2]), vec3(mx[0], mx[1], mx[2]))
4719
4720 # ==================== Primitive Color Mutation ====================
4721
4722 def setPrimitiveColor(self, uuids, color) -> None:
4723 """Set the RGB or RGBA color of one primitive or a list of primitives.
4724
4725 Args:
4726 uuids: Single UUID (int) or list of UUIDs.
4727 color: RGBcolor or RGBAcolor.
4728 """
4730 if isinstance(color, RGBAcolor):
4731 rgba = [color.r, color.g, color.b, color.a]
4732 if isinstance(uuids, (list, tuple)):
4733 context_wrapper.setPrimitiveColorRGBABatchWrapper(self.context, list(uuids), rgba)
4734 else:
4735 context_wrapper.setPrimitiveColorRGBAWrapper(self.context, uuids, rgba)
4736 elif isinstance(color, RGBcolor):
4737 rgb = [color.r, color.g, color.b]
4738 if isinstance(uuids, (list, tuple)):
4739 context_wrapper.setPrimitiveColorBatchWrapper(self.context, list(uuids), rgb)
4740 else:
4741 context_wrapper.setPrimitiveColorWrapper(self.context, uuids, rgb)
4742 else:
4743 raise ValueError(f"color must be RGBcolor or RGBAcolor, got {type(color).__name__}")
4744
4745 # ==================== Primitive Data Introspection / Cleanup ====================
4746
4747 def clearPrimitiveData(self, uuids, label: str) -> None:
4748 """Remove a named data field from one primitive or a list of primitives."""
4750 if isinstance(uuids, (list, tuple)):
4751 context_wrapper.clearPrimitiveDataByLabelBatchWrapper(self.context, list(uuids), label)
4752 else:
4753 context_wrapper.clearPrimitiveDataByLabelWrapper(self.context, uuids, label)
4754
4755 def listPrimitiveData(self, uuid: int) -> List[str]:
4756 """List all data labels attached to a primitive."""
4758 return context_wrapper.listPrimitiveDataWrapper(self.context, uuid)
4759
4760 # ==================== Domain Cropping ====================
4761
4762 def cropDomainX(self, xbounds: vec2) -> None:
4764 if not isinstance(xbounds, vec2):
4765 raise ValueError(f"xbounds must be a vec2, got {type(xbounds).__name__}")
4766 context_wrapper.cropDomainXWrapper(self.context, xbounds.to_list())
4767
4768 def cropDomainY(self, ybounds: vec2) -> None:
4770 if not isinstance(ybounds, vec2):
4771 raise ValueError(f"ybounds must be a vec2, got {type(ybounds).__name__}")
4772 context_wrapper.cropDomainYWrapper(self.context, ybounds.to_list())
4773
4774 def cropDomainZ(self, zbounds: vec2) -> None:
4776 if not isinstance(zbounds, vec2):
4777 raise ValueError(f"zbounds must be a vec2, got {type(zbounds).__name__}")
4778 context_wrapper.cropDomainZWrapper(self.context, zbounds.to_list())
4779
4780 def cropDomain(self, *args) -> Optional[List[int]]:
4781 """Crop the context domain to the given XYZ bounds.
4783 Two call forms:
4784 cropDomain(xbounds: vec2, ybounds: vec2, zbounds: vec2)
4785 -> crop ALL primitives; returns None.
4786 cropDomain(uuids: List[int], xbounds: vec2, ybounds: vec2, zbounds: vec2)
4787 -> crop only the given primitives; returns the list of primitives
4788 that survived (in-bounds UUIDs). The input list is NOT mutated.
4789 """
4791 if len(args) == 3:
4792 xb, yb, zb = args
4793 for name, b in (("xbounds", xb), ("ybounds", yb), ("zbounds", zb)):
4794 if not isinstance(b, vec2):
4795 raise ValueError(f"{name} must be a vec2, got {type(b).__name__}")
4796 context_wrapper.cropDomainXYZWrapper(self.context, xb.to_list(), yb.to_list(), zb.to_list())
4797 return None
4798 if len(args) == 4:
4799 uuids, xb, yb, zb = args
4800 if not isinstance(uuids, (list, tuple)):
4801 raise ValueError(f"uuids must be a list or tuple, got {type(uuids).__name__}")
4802 for name, b in (("xbounds", xb), ("ybounds", yb), ("zbounds", zb)):
4803 if not isinstance(b, vec2):
4804 raise ValueError(f"{name} must be a vec2, got {type(b).__name__}")
4805 return context_wrapper.cropDomainByUUIDsWrapper(self.context, list(uuids), xb.to_list(), yb.to_list(), zb.to_list())
4806 raise TypeError(f"cropDomain() takes 3 or 4 positional arguments, got {len(args)}")
4807
Central simulation environment for PyHelios that manages 3D primitives and their data.
Definition Context.py:78
None setGlobalDataVec3(self, str label, x_or_vec, float y=None, float z=None)
Set global data as vec3.
Definition Context.py:4248
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:4615
vec3 getBoxObjectSize(self, int objID)
Definition Context.py:4581
None duplicateObjectData(self, int objID, str old_label, str new_label)
Copy object data to a new label.
Definition Context.py:4199
str getMaterialTexture(self, str material_label)
Get the texture file path for a material.
Definition Context.py:3621
getMaterialColor(self, str material_label)
Get the RGBA color of a material.
Definition Context.py:3578
getObjectData(self, int objID, str label, type data_type=None)
Get object data with optional type specification.
Definition Context.py:4125
getAllPrimitiveVertices(self)
Get vertices for all primitives.
Definition Context.py:3942
List[RGBcolor] getTubeObjectNodeColors(self, int objID)
Definition Context.py:4628
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:4191
int getMaterialTwosidedFlag(self, str material_label)
Get the two-sided rendering flag for a material (0 = one-sided, 1 = two-sided).
Definition Context.py:3648
Optional[List[int]] cropDomain(self, *args)
Crop the context domain to the given XYZ bounds.
Definition Context.py:4797
None deletePrimitive(self, Union[int, List[int]] uuids_or_uuid)
Delete one or more primitives from the context.
Definition Context.py:3425
_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:4288
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:3073
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:3864
getPrimitiveMaterialLabel(self, uuid)
Get the material label assigned to a primitive or multiple primitives.
Definition Context.py:3707
int getPrimitiveTwosidedFlag(self, int uuid, int default_value=1)
Get two-sided rendering flag for a primitive.
Definition Context.py:3731
None cropDomainX(self, vec2 xbounds)
Definition Context.py:4770
List[str] getAllPrimitiveTextureFiles(self)
Get texture files for all primitives.
Definition Context.py:3946
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:4081
bool isPrimitiveHidden(self, int uuid)
Check if a primitive is hidden.
Definition Context.py:3985
np.ndarray getPrimitiveDataArray(self, List[int] uuids, str label)
Get primitive data values for multiple primitives as a NumPy array.
Definition Context.py:2852
List[int] getObjectPrimitiveUUIDs(self, objIDs)
Get flattened primitive UUIDs for one object, a list of objects, or a list-of-lists.
Definition Context.py:4505
bool is_plugin_available(self, str plugin_name)
Check if a specific plugin is available.
Definition Context.py:3492
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:4331
vec3 getTileObjectNormal(self, int objID)
Definition Context.py:4536
bool doesTimeseriesVariableExist(self, str label)
Check whether a timeseries variable exists.
Definition Context.py:3304
List[int] getAllObjectIDs(self)
Definition Context.py:628
None setObjectDataUInt(self, objids_or_objid, str label, int value)
Set object data as unsigned 32-bit integer for one or multiple objects.
Definition Context.py:4031
List[str] listAllObjectDataLabels(self)
List all object data labels in context.
Definition Context.py:4195
List[str] get_available_plugins(self)
Get list of available plugins for this PyHelios instance.
Definition Context.py:3480
__exit__(self, exc_type, exc_value, traceback)
Definition Context.py:288
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:2689
None hidePrimitive(self, uuids_or_uuid)
Hide one or more primitives.
Definition Context.py:3960
int getTimeseriesLength(self, str label)
Get the number of data points in a timeseries variable.
Definition Context.py:3280
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:4673
setCurrentTimeseriesPoint(self, str label, int index)
Set the Context date and time from a timeseries data point index.
Definition Context.py:3137
calculatePrimitiveDataAreaWeightedMean(self, List[int] uuids, str label, type return_type=float)
Calculate area-weighted mean of primitive data.
Definition Context.py:4392
getTileObjectAreaRatio(self, objIDs)
Get tile-object area ratio for one or multiple tile objects.
Definition Context.py:4515
bool doesPrimitiveDataExist(self, int uuid, str label)
Check if primitive data exists for a specific primitive and label.
Definition Context.py:2785
int getObjectDataSize(self, int objID, str label)
Get the size of object data array.
Definition Context.py:4176
float getTubeObjectSegmentVolume(self, int objID, int segment_index)
Definition Context.py:4637
List[str] listMaterials(self)
Get list of all material labels in the context.
Definition Context.py:3549
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:2515
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:2559
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:2588
'np.ndarray' getAllPrimitiveColors(self)
Get colors for all primitives.
Definition Context.py:3926
int getPrimitiveCount(self)
Definition Context.py:598
None aggregatePrimitiveDataSum(self, List[int] uuids, List[str] labels, str result_label)
Sum multiple primitive data fields into a new field.
Definition Context.py:4436
List[str] listGlobalData(self)
List all global data labels.
Definition Context.py:4359
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
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:3225
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:2668
None showPrimitive(self, uuids_or_uuid)
Show one or more previously hidden primitives.
Definition Context.py:3971
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:4455
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:4552
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:2648
int getGlobalDataType(self, str label)
Get the HeliosDataType enum for global data.
Definition Context.py:4335
print_plugin_status(self)
Print detailed plugin status information.
Definition Context.py:3505
vec3 getTriangleVertex(self, int uuid, int number)
Definition Context.py:4689
float getSphereObjectVolume(self, int objID)
Definition Context.py:4571
setDate(self, int year, int month, int day)
Set the simulation date.
Definition Context.py:3000
int2 getPrimitiveTextureSize(self, int uuid)
Get the texture size (width, height) of a primitive.
Definition Context.py:3829
vec3 getSphereObjectRadius(self, int objID)
Get per-axis radii of a sphere object.
Definition Context.py:4562
List[vec3] getTileObjectVertices(self, int objID)
Definition Context.py:4546
getPrimitiveData(self, int uuid, str label, type data_type=None)
Get primitive data for a specific primitive.
Definition Context.py:2712
None setGlobalDataUInt(self, str label, int value)
Set global data as unsigned 32-bit integer.
Definition Context.py:4224
List[float] getConeObjectNodeRadii(self, int objID)
Definition Context.py:4651
List[str] get_missing_plugins(self, List[str] requested_plugins)
Get list of requested plugins that are not available.
Definition Context.py:3517
None showObject(self, objids_or_objid)
Show one or more previously hidden compound objects.
Definition Context.py:4004
List[vec3] getConeObjectNodes(self, int objID)
Definition Context.py:4646
None setObjectDataInt(self, objids_or_objid, str label, int value)
Set object data as signed 32-bit integer for one or multiple objects.
Definition Context.py:4024
bool isGeometryDirty(self)
Definition Context.py:318
clearTimeseriesData(self)
Clear all timeseries data from the Context.
Definition Context.py:3342
vec3 getVoxelCenter(self, int uuid)
Definition Context.py:4694
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:3950
None duplicateGlobalData(self, str old_label, str new_label)
Duplicate global data to a new label.
Definition Context.py:4355
None usePrimitiveTextureColor(self, int uuid)
Use texture map color instead of constant RGB for a primitive.
Definition Context.py:3901
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:2957
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
Definition Context.py:295
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:4596
None incrementGlobalData(self, str label, increment)
Increment global data.
Definition Context.py:4363
getObjectBoundingBox(self, objIDs)
Get axis-aligned bounding box for one object or a list of objects.
Definition Context.py:4489
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:4280
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:4070
_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:3380
vec3 getConeObjectNode(self, int objID, int number)
Definition Context.py:4655
None clearGlobalData(self, str label)
Clear global data.
Definition Context.py:4347
vec3 getConeObjectAxisUnitVector(self, int objID)
Definition Context.py:4664
None setObjectDataVec2(self, objids_or_objid, str label, x_or_vec, float y=None)
Set object data as vec2.
Definition Context.py:4059
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
addMaterial(self, str material_label)
Create a new material for sharing visual properties across primitives.
Definition Context.py:3541
vec2 getTileObjectSize(self, int objID)
Definition Context.py:4526
int getObjectType(self, int objID)
Return the integer-coded helios::ObjectType of a compound object.
Definition Context.py:4472
bool isMaterialTextureColorOverridden(self, str material_label)
Check if material texture color is overridden by material color.
Definition Context.py:3640
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
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:2629
None setObjectDataInt2(self, objids_or_objid, str label, x_or_vec, int y=None)
Set object data as int2.
Definition Context.py:4092
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:2545
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:4601
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
str _validate_file_path(self, str filename, List[str] expected_extensions=None)
Validate and normalize file path for security.
Definition Context.py:212
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 overridePrimitiveTextureColor(self, int uuid)
Override texture color with constant RGB color for a primitive.
Definition Context.py:3892
None setGlobalDataFloat(self, str label, float value)
Set global data as 32-bit float.
Definition Context.py:4228
'np.ndarray' getAllPrimitiveTypes(self)
Get types for all primitives.
Definition Context.py:3934
float queryTimeseriesData(self, str label, 'Date' date=None, 'Time' time=None, int index=None)
Query a timeseries data value.
Definition Context.py:3176
int getConeObjectSubdivisionCount(self, int objID)
Definition Context.py:4642
getPrimitiveVertices(self, uuid)
Get vertices of a primitive or multiple primitives.
Definition Context.py:560
assignMaterialToPrimitive(self, uuid, str material_label)
Assign a material to primitive(s).
Definition Context.py:3669
None scalePrimitiveData(self, uuids_or_label, label_or_factor, factor=None)
Scale primitive data by a factor.
Definition Context.py:4420
bool doesObjectDataExist(self, int objID, str label)
Check if object data exists.
Definition Context.py:4180
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
bool isPrimitiveTextureColorOverridden(self, int uuid)
Check if primitive texture color is overridden.
Definition Context.py:3913
None clearPrimitiveData(self, uuids, str label)
Remove a named data field from one primitive or a list of primitives.
Definition Context.py:4756
float sumPrimitiveSurfaceArea(self, List[int] uuids)
Calculate total one-sided surface area for a set of primitives.
Definition Context.py:4444
int3 getBoxObjectSubdivisionCount(self, int objID)
Definition Context.py:4586
vec2 getPatchSize(self, int uuid)
Definition Context.py:4684
int getPatchCount(self, bool include_hidden=True)
Definition Context.py:4704
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:3460
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:4272
Union[int, List[int]] copyPrimitive(self, Union[int, List[int]] UUID)
Copy one or more primitives.
Definition Context.py:1553
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:3652
None setObjectDataString(self, objids_or_objid, str label, str value)
Set object data as string for one or multiple objects.
Definition Context.py:4052
None setPrimitiveDataString(self, uuids_or_uuid, str label, str value)
Set primitive data as string for one or multiple primitives.
Definition Context.py:2573
None setPrimitiveColor(self, uuids, color)
Set the RGB or RGBA color of one primitive or a list of primitives.
Definition Context.py:4736
None setGlobalDataDouble(self, str label, float value)
Set global data as 64-bit double.
Definition Context.py:4232
float getTubeObjectVolume(self, int objID)
Definition Context.py:4633
List[vec3] getTubeObjectNodes(self, int objID)
Definition Context.py:4619
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:4669
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
vec3 getBoxObjectCenter(self, int objID)
Definition Context.py:4576
updateTimeseriesData(self, str label, 'Date' date, 'Time' time, float new_value)
Update the value of an existing timeseries data point.
Definition Context.py:3107
calculatePrimitiveDataSum(self, List[int] uuids, str label, type return_type=float)
Calculate sum of primitive data across UUIDs.
Definition Context.py:4399
getPrimitiveTextureUV(self, uuid)
Get the texture UV coordinates of a primitive or multiple primitives.
Definition Context.py:3842
PrimitiveInfo getPrimitiveInfo(self, int uuid)
Get physical properties and geometry information for a single primitive.
Definition Context.py:643
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:4103
int getPrimitiveDataSize(self, int uuid, str label)
Get the size/length of primitive data (for vector data).
Definition Context.py:2824
int getGlobalDataSize(self, str label)
Get the size of global data array.
Definition Context.py:4339
getPrimitiveBoundingBox(self, uuids)
Get axis-aligned bounding box for one primitive or a list of primitives.
Definition Context.py:4720
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
None setGlobalDataString(self, str label, str value)
Set global data as string.
Definition Context.py:4236
None cropDomainY(self, vec2 ybounds)
Definition Context.py:4776
vec3 getObjectCenter(self, int objID)
Definition Context.py:4476
vec3 getPatchCenter(self, int uuid)
Definition Context.py:4679
int2 getTileObjectSubdivisionCount(self, int objID)
Definition Context.py:4531
None hideObject(self, objids_or_objid)
Hide one or more compound objects (and all their primitives).
Definition Context.py:3993
None setObjectDataFloat(self, objids_or_objid, str label, float value)
Set object data as 32-bit float for one or multiple objects.
Definition Context.py:4038
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:2811
calculatePrimitiveDataAreaWeightedSum(self, List[int] uuids, str label, type return_type=float)
Calculate area-weighted sum of primitive data.
Definition Context.py:4408
str getObjectDataString(self, int objID, str label)
Get string object data.
Definition Context.py:4168
None cropDomainZ(self, vec2 zbounds)
Definition Context.py:4782
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:2531
int getTubeObjectSubdivisionCount(self, int objID)
Definition Context.py:4611
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:4160
calculatePrimitiveDataMean(self, List[int] uuids, str label, type return_type=float)
Calculate arithmetic mean of primitive data across UUIDs.
Definition Context.py:4380
int getObjectDataInt(self, int objID, str label)
Get int object data.
Definition Context.py:4164
List[vec2] getTileObjectTextureUV(self, int objID)
Definition Context.py:4541
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
None renameObjectData(self, int objID, str old_label, str new_label)
Rename an object data label.
Definition Context.py:4203
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:2798
None setGlobalDataVec2(self, str label, x_or_vec, float y=None)
Set global data as vec2.
Definition Context.py:4240
deleteMaterial(self, str material_label)
Delete a material from the context.
Definition Context.py:3563
dict get_plugin_capabilities(self)
Get detailed information about available plugin capabilities.
Definition Context.py:3501
vec3 getVoxelSize(self, int uuid)
Definition Context.py:4699
getTime(self)
Get the current simulation time.
Definition Context.py:3033
getPrimitiveNormal(self, uuid)
Get the normal vector of a primitive or multiple primitives.
Definition Context.py:537
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:4207
List[int] getPrimitivesUsingMaterial(self, str material_label)
Get all primitive UUIDs that use a specific material.
Definition Context.py:3746
setDateJulian(self, int julian_day, int year)
Set the simulation date using Julian day number.
Definition Context.py:3017
List[float] getTubeObjectNodeRadii(self, int objID)
Definition Context.py:4624
None clearObjectData(self, objids_or_objid, str label)
Clear object data.
Definition Context.py:4184
int getTriangleCount(self, bool include_hidden=True)
Definition Context.py:4708
float getConeObjectNodeRadius(self, int objID, int number)
Definition Context.py:4660
int getObjectDataType(self, int objID, str label)
Get the HeliosDataType enum for object data.
Definition Context.py:4172
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:4114
setMaterialTextureColorOverride(self, str material_label, bool override)
Set whether material color overrides texture color.
Definition Context.py:3644
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:3049
'np.ndarray' getAllPrimitiveSolidFractions(self)
Get solid fractions for all primitives.
Definition Context.py:3938
getPrimitiveTextureFile(self, uuid)
Get the texture file path of a primitive or multiple primitives.
Definition Context.py:3761
None setGlobalDataInt2(self, str label, x_or_vec, int y=None)
Set global data as int2.
Definition Context.py:4264
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
float getBoxObjectVolume(self, int objID)
Definition Context.py:4591
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:4323
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:4256
List[int] loadXML(self, str filename, bool quiet=False)
Load geometry from a Helios XML file.
Definition Context.py:2142
None setGlobalDataInt(self, str label, int value)
Set global data as signed 32-bit integer.
Definition Context.py:4220
setMaterialColor(self, str material_label, color)
Set the RGBA color of a material.
Definition Context.py:3600
None setPrimitiveTextureFile(self, int uuid, str texture_file)
Set the texture file path of a primitive.
Definition Context.py:3817
None aggregatePrimitiveDataProduct(self, List[int] uuids, List[str] labels, str result_label)
Multiply multiple primitive data fields into a new field.
Definition Context.py:4440
bool doesMaterialExist(self, str material_label)
Check if a material with the given label exists.
Definition Context.py:3545
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 for one or multiple objects.
Definition Context.py:4045
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:4606
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:2982
getPrimitiveSolidFraction(self, uuid)
Get the solid fraction of a primitive or multiple primitives.
Definition Context.py:3876
packGPUBuffers(self, uuids)
Pack GPU-ready geometry buffers for a set of primitives in a single C++ pass.
Definition Context.py:3805
assignMaterialToObject(self, objID, str material_label)
Assign a material to all primitives in compound object(s).
Definition Context.py:3690
int getGlobalDataInt(self, str label)
Get int global data.
Definition Context.py:4327
'Date' queryTimeseriesDate(self, str label, int index)
Get the Date associated with a timeseries data point.
Definition Context.py:3253
None incrementPrimitiveData(self, List[int] uuids, str label, increment)
Increment primitive data.
Definition Context.py:4427
'np.ndarray' getAllPrimitiveAreas(self)
Get areas for all primitives.
Definition Context.py:3930
'np.ndarray' getAllPrimitiveNormals(self)
Get normals for all primitives.
Definition Context.py:3922
bool isObjectHidden(self, int objID)
Check if a compound object is hidden.
Definition Context.py:4018
resolveMaterialTextures(self, uuids, colors_np)
Resolve material texture suppression for export.
Definition Context.py:3787
None renameGlobalData(self, str old_label, str new_label)
Rename a global data label.
Definition Context.py:4351
List[str] listTimeseriesVariables(self)
List all existing timeseries variables.
Definition Context.py:3325
List[str] listPrimitiveData(self, int uuid)
List all data labels attached to a primitive.
Definition Context.py:4764
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:2608
bool doesGlobalDataExist(self, str label)
Check if global data exists.
Definition Context.py:4343
vec3 getTileObjectCenter(self, int objID)
Definition Context.py:4521
setMaterialTexture(self, str material_label, str texture_file)
Set the texture file for a material.
Definition Context.py:3636
int getSphereObjectSubdivisionCount(self, int objID)
Definition Context.py:4567
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
Helios primitive type enumeration.
Definition DataTypes.py:8
Helios Time structure for representing time values.
Definition DataTypes.py:672