0.1.8
Loading...
Searching...
No Matches
Visualizer.py
Go to the documentation of this file.
1"""
2High-level Visualizer interface for PyHelios.
3
4This module provides a user-friendly interface to the 3D visualization
5capabilities with graceful plugin handling and informative error messages.
6"""
7
8import logging
9import os
10import ctypes
11from pathlib import Path
12from contextlib import contextmanager
13from typing import List, Optional, Union, Tuple
14
15from .plugins.registry import get_plugin_registry
16from .plugins import helios_lib
17from .wrappers import UVisualizerWrapper as visualizer_wrapper
18from .wrappers.DataTypes import vec3, RGBcolor, SphericalCoord
19from .Context import Context
20from .validation.plugin_decorators import validate_build_geometry_params, validate_print_window_params
21from .assets import get_asset_manager
22
23logger = logging.getLogger(__name__)
24
25
26def _resolve_user_path(path: str) -> str:
27 """
28 Resolve a user-provided path to an absolute path before working directory changes.
29
30 This ensures that user file paths are interpreted relative to their original
31 working directory, not the temporary working directory used for asset discovery.
32
33 Args:
34 path: User-provided file path (absolute or relative)
35
36 Returns:
37 Absolute path resolved from the user's original working directory
38 """
39 from pathlib import Path
41 path_obj = Path(path)
42 if path_obj.is_absolute():
43 return str(path_obj)
44 else:
45 # Resolve relative to the user's current working directory
46 return str(Path.cwd().resolve() / path_obj)
47
48@contextmanager
50 """
51 Context manager that temporarily changes working directory for visualizer operations.
52
53 The C++ visualizer code expects to find assets at 'plugins/visualizer/' relative
54 to the current working directory. This context manager ensures the working directory
55 is set correctly during visualizer initialization and operations.
56
57 Note: This is required because the Helios C++ core prioritizes current working
58 directory for asset resolution over environment variables.
59 """
60 # Find the build directory where assets are located
61 # Try asset manager first (works for both development and wheel installations)
62 asset_manager = get_asset_manager()
63 working_dir = asset_manager._get_helios_build_path()
64
65 if working_dir and working_dir.exists():
66 visualizer_assets = working_dir / 'plugins' / 'visualizer'
67 else:
68 # For wheel installations, check packaged assets
69 current_dir = Path(__file__).parent
70 packaged_build = current_dir / 'assets' / 'build'
71
72 if packaged_build.exists():
73 working_dir = packaged_build
74 visualizer_assets = working_dir / 'plugins' / 'visualizer'
75 else:
76 # Fallback to development paths
77 repo_root = current_dir.parent
78 build_lib_dir = repo_root / 'pyhelios_build' / 'build' / 'lib'
79 working_dir = build_lib_dir.parent
80 visualizer_assets = working_dir / 'plugins' / 'visualizer'
81
82 if not build_lib_dir.exists():
83 logger.warning(f"Build directory not found: {build_lib_dir}")
84 # Fallback to current directory - may not work but don't break
85 yield
86 return
87
88 if not (visualizer_assets / 'shaders').exists():
89 # Only warn in development environments, not wheel installations
90 asset_mgr = get_asset_manager()
91 if not asset_mgr._is_wheel_install():
92 logger.warning(f"Visualizer assets not found at: {visualizer_assets}")
93 # Continue anyway - may be using source assets or alternative setup
94
95 # Change working directory temporarily
96 original_cwd = Path.cwd()
97
98 try:
99 logger.debug(f"Changing working directory from {original_cwd} to {working_dir}")
100 os.chdir(working_dir)
101 yield
102 finally:
103 logger.debug(f"Restoring working directory to {original_cwd}")
104 os.chdir(original_cwd)
105
106
107class VisualizerError(Exception):
108 """Raised when Visualizer operations fail."""
109 pass
110
111
113 """
114 High-level interface for 3D visualization and rendering.
115
116 This class provides a user-friendly wrapper around the native Helios
117 visualizer plugin with automatic plugin availability checking and
118 graceful error handling.
119
120 The visualizer provides OpenGL-based 3D rendering with interactive controls,
121 image export, and comprehensive scene configuration options.
122 """
123
124 # Lighting model constants
125 LIGHTING_NONE = 0
126 LIGHTING_PHONG = 1
127 LIGHTING_PHONG_SHADOWED = 2
128
129 # Colormap constants (matching C++ enum values)
130 COLORMAP_HOT = 0
131 COLORMAP_COOL = 1
132 COLORMAP_RAINBOW = 2
133 COLORMAP_LAVA = 3
134 COLORMAP_PARULA = 4
135 COLORMAP_GRAY = 5
137 def __init__(self, width: int, height: int, antialiasing_samples: int = 1, headless: bool = False):
138 """
139 Initialize Visualizer with graceful plugin handling.
140
141 Args:
142 width: Window width in pixels
143 height: Window height in pixels
144 antialiasing_samples: Number of antialiasing samples (default: 1)
145 headless: Enable headless mode for offscreen rendering (default: False)
146
147 Raises:
148 VisualizerError: If visualizer plugin is not available
149 ValueError: If parameters are invalid
150 """
151 # Validate parameter types first
152 if not isinstance(width, int):
153 raise ValueError(f"Width must be an integer, got {type(width).__name__}")
154 if not isinstance(height, int):
155 raise ValueError(f"Height must be an integer, got {type(height).__name__}")
156 if not isinstance(antialiasing_samples, int):
157 raise ValueError(f"Antialiasing samples must be an integer, got {type(antialiasing_samples).__name__}")
158 if not isinstance(headless, bool):
159 raise ValueError(f"Headless must be a boolean, got {type(headless).__name__}")
160
161 # Validate parameter values
162 if width <= 0 or height <= 0:
163 raise ValueError("Width and height must be positive integers")
164 if antialiasing_samples < 1:
165 raise ValueError("Antialiasing samples must be at least 1")
166
167 self.width = width
168 self.height = height
169 self.antialiasing_samples = antialiasing_samples
170 self.headless = headless
171 self.visualizer = None
173 # Check plugin availability using registry
174 registry = get_plugin_registry()
176 if not registry.is_plugin_available('visualizer'):
177 # Get helpful information about the missing plugin
178 available_plugins = registry.get_available_plugins()
179
180 error_msg = (
181 "Visualizer requires the 'visualizer' plugin which is not available.\n\n"
182 "The visualizer plugin provides OpenGL-based 3D rendering and visualization.\n"
183 "System requirements:\n"
184 "- OpenGL 3.3 or higher\n"
185 "- GLFW library for window management\n"
186 "- FreeType library for text rendering\n"
187 "- Display/graphics drivers (X11 on Linux, native on Windows/macOS)\n\n"
188 "To enable visualization:\n"
189 "1. Build PyHelios with visualizer plugin:\n"
190 " build_scripts/build_helios --plugins visualizer\n"
191 f"\nCurrently available plugins: {available_plugins}"
192 )
193
194 # Add platform-specific installation hints
195 import platform
196 system = platform.system().lower()
197 if 'linux' in system:
198 error_msg += (
199 "\n\nLinux installation hints:\n"
200 "- Ubuntu/Debian: sudo apt-get install libx11-dev xorg-dev libgl1-mesa-dev libglu1-mesa-dev\n"
201 "- CentOS/RHEL: sudo yum install libX11-devel mesa-libGL-devel mesa-libGLU-devel"
202 )
203 elif 'darwin' in system:
204 error_msg += (
205 "\n\nmacOS installation hints:\n"
206 "- Install XQuartz: brew install --cask xquartz\n"
207 "- OpenGL should be available by default"
208 )
209 elif 'windows' in system:
210 error_msg += (
211 "\n\nWindows installation hints:\n"
212 "- OpenGL drivers should be provided by graphics card drivers\n"
213 "- Visual Studio runtime may be required"
214 )
215
216 raise VisualizerError(error_msg)
217
218 # Plugin is available - create visualizer with correct working directory
219 try:
221 if antialiasing_samples > 1:
222 self.visualizer = visualizer_wrapper.create_visualizer_with_antialiasing(
223 width, height, antialiasing_samples, headless
224 )
225 else:
226 self.visualizer = visualizer_wrapper.create_visualizer(
227 width, height, headless
228 )
229
230 if self.visualizer is None:
231 raise VisualizerError(
232 "Failed to create Visualizer instance. "
233 "This may indicate a problem with graphics drivers or OpenGL initialization."
234 )
235 logger.info(f"Visualizer created successfully ({width}x{height}, AA:{antialiasing_samples}, headless:{headless})")
236
237 except Exception as e:
238 raise VisualizerError(f"Failed to initialize Visualizer: {e}")
239
240 def __enter__(self):
241 """Context manager entry."""
242 return self
243
244 def __exit__(self, exc_type, exc_value, traceback):
245 """Context manager exit with proper cleanup."""
246 if self.visualizer is not None:
247 try:
249 visualizer_wrapper.destroy_visualizer(self.visualizer)
250 logger.debug("Visualizer destroyed successfully")
251 except Exception as e:
252 logger.warning(f"Error destroying Visualizer: {e}")
253 finally:
254 self.visualizer = None
255
256 @validate_build_geometry_params
257 def buildContextGeometry(self, context: Context, uuids: Optional[List[int]] = None) -> None:
258 """
259 Build Context geometry in the visualizer.
260
261 This method loads geometry from a Helios Context into the visualizer
262 for rendering. If no UUIDs are specified, all geometry is loaded.
263
264 Args:
265 context: Helios Context instance containing geometry
266 uuids: Optional list of primitive UUIDs to visualize (default: all)
267
268 Raises:
269 VisualizerError: If geometry building fails
270 ValueError: If parameters are invalid
271 """
272 if self.visualizer is None:
273 raise VisualizerError("Visualizer has been destroyed")
274 if not isinstance(context, Context):
275 raise ValueError("context must be a Context instance")
276
277 try:
279 if uuids is None:
280 # Load all geometry
281 visualizer_wrapper.build_context_geometry(self.visualizer, context.getNativePtr())
282 logger.debug("Built all Context geometry in visualizer")
283 else:
284 # Load specific UUIDs
285 if not uuids:
286 raise ValueError("UUIDs list cannot be empty")
287 visualizer_wrapper.build_context_geometry_uuids(
288 self.visualizer, context.getNativePtr(), uuids
289 )
290 logger.debug(f"Built {len(uuids)} primitives in visualizer")
291
292 except Exception as e:
293 raise VisualizerError(f"Failed to build Context geometry: {e}")
294
295 def plotInteractive(self) -> None:
296 """
297 Open interactive visualization window.
298
299 This method opens a window with the current scene and allows user
300 interaction (camera rotation, zooming, etc.). The program will pause
301 until the window is closed by the user.
302
303 Interactive controls:
304 - Mouse scroll: Zoom in/out
305 - Left mouse + drag: Rotate camera
306 - Right mouse + drag: Pan camera
307 - Arrow keys: Camera movement
308 - +/- keys: Zoom in/out
309
310 Raises:
311 VisualizerError: If visualization fails
312 """
313 if self.visualizer is None:
314 raise VisualizerError("Visualizer has been destroyed")
315
316 try:
318 visualizer_wrapper.plot_interactive(self.visualizer)
319 logger.debug("Interactive visualization completed")
320 except Exception as e:
321 raise VisualizerError(f"Interactive visualization failed: {e}")
322
323 def plotUpdate(self) -> None:
324 """
325 Update visualization (non-interactive).
326
327 This method updates the visualization window without user interaction.
328 The program continues immediately after rendering. Useful for batch
329 processing or creating image sequences.
330
331 Raises:
332 VisualizerError: If visualization update fails
333 """
334 if self.visualizer is None:
335 raise VisualizerError("Visualizer has been destroyed")
336
337 try:
339 visualizer_wrapper.plot_update(self.visualizer)
340 logger.debug("Visualization updated")
341 except Exception as e:
342 raise VisualizerError(f"Visualization update failed: {e}")
343
344 @validate_print_window_params
345 def printWindow(self, filename: str, image_format: Optional[str] = None) -> None:
346 """
347 Save current visualization to image file.
348
349 This method exports the current visualization to an image file.
350 Starting from v1.3.53, supports both JPEG and PNG formats.
351
352 Args:
353 filename: Output filename for image
354 Can be absolute or relative to user's current working directory
355 Extension (.jpg, .png) is recommended but not required
356 image_format: Image format - "jpeg" or "png" (v1.3.53+)
357 If None, automatically detects from filename extension
358 Default: "jpeg" if not detectable from extension
359
360 Raises:
361 VisualizerError: If image saving fails
362 ValueError: If filename or format is invalid
363
364 Note:
365 PNG format is required to preserve transparent backgrounds when using
366 setBackgroundTransparent(). JPEG format will render transparent areas as black.
367
368 Example:
369 >>> visualizer.printWindow("output.jpg") # Auto-detects JPEG
370 >>> visualizer.printWindow("output.png") # Auto-detects PNG
371 >>> visualizer.printWindow("output.img", image_format="png") # Explicit PNG
372 """
373 if self.visualizer is None:
374 raise VisualizerError("Visualizer has been destroyed")
375 if not filename:
376 raise ValueError("Filename cannot be empty")
377
378 # Resolve filename relative to user's working directory before chdir
379 resolved_filename = _resolve_user_path(filename)
380
381 # Auto-detect format from extension if not specified
382 if image_format is None:
383 if resolved_filename.lower().endswith('.png'):
384 image_format = 'png'
385 elif resolved_filename.lower().endswith(('.jpg', '.jpeg')):
386 image_format = 'jpeg'
387 else:
388 # Default to jpeg for backward compatibility
389 image_format = 'jpeg'
390 logger.debug(f"No format specified and extension not recognized, defaulting to JPEG")
391
392 # Validate format
393 if image_format.lower() not in ['jpeg', 'png']:
394 raise ValueError(f"Image format must be 'jpeg' or 'png', got '{image_format}'")
395
396 try:
398 # Try using the new format-aware function (v1.3.53+)
399 try:
400 visualizer_wrapper.print_window_with_format(
401 self.visualizer,
402 resolved_filename,
403 image_format
404 )
405 logger.debug(f"Visualization saved to {resolved_filename} ({image_format.upper()} format)")
406 except (AttributeError, NotImplementedError):
407 # Fallback to old function for older Helios versions
408 if image_format.lower() != 'jpeg':
409 logger.warning(
410 "PNG format requested but not available in current Helios version. "
411 "Falling back to JPEG format. Update to Helios v1.3.53+ for PNG support."
412 )
413 visualizer_wrapper.print_window(self.visualizer, resolved_filename)
414 logger.debug(f"Visualization saved to {resolved_filename} (JPEG format - legacy mode)")
415 except Exception as e:
416 raise VisualizerError(f"Failed to save image: {e}")
417
418 def closeWindow(self) -> None:
419 """
420 Close visualization window.
421
422 This method closes any open visualization window. It's safe to call
423 even if no window is open.
424
425 Raises:
426 VisualizerError: If window closing fails
427 """
428 if self.visualizer is None:
429 raise VisualizerError("Visualizer has been destroyed")
430
431 try:
432 visualizer_wrapper.close_window(self.visualizer)
433 logger.debug("Visualization window closed")
434 except Exception as e:
435 raise VisualizerError(f"Failed to close window: {e}")
436
437 def setCameraPosition(self, position: vec3, lookAt: vec3) -> None:
438 """
439 Set camera position using Cartesian coordinates.
440
441 Args:
442 position: Camera position as vec3 in world coordinates
443 lookAt: Camera look-at point as vec3 in world coordinates
444
445 Raises:
446 VisualizerError: If camera positioning fails
447 ValueError: If parameters are invalid
448 """
449 if self.visualizer is None:
450 raise VisualizerError("Visualizer has been destroyed")
451
452 # Validate DataType parameters
453 if not isinstance(position, vec3):
454 raise ValueError(f"Position must be a vec3, got {type(position).__name__}")
455 if not isinstance(lookAt, vec3):
456 raise ValueError(f"LookAt must be a vec3, got {type(lookAt).__name__}")
457
458 try:
459 visualizer_wrapper.set_camera_position(self.visualizer, position, lookAt)
460 logger.debug(f"Camera position set to ({position.x}, {position.y}, {position.z}), looking at ({lookAt.x}, {lookAt.y}, {lookAt.z})")
461 except Exception as e:
462 raise VisualizerError(f"Failed to set camera position: {e}")
463
464 def setCameraPositionSpherical(self, angle: SphericalCoord, lookAt: vec3) -> None:
465 """
466 Set camera position using spherical coordinates.
467
468 Args:
469 angle: Camera position as SphericalCoord (radius, elevation, azimuth)
470 lookAt: Camera look-at point as vec3 in world coordinates
471
472 Raises:
473 VisualizerError: If camera positioning fails
474 ValueError: If parameters are invalid
475 """
476 if self.visualizer is None:
477 raise VisualizerError("Visualizer has been destroyed")
478
479 # Validate DataType parameters
480 if not isinstance(angle, SphericalCoord):
481 raise ValueError(f"Angle must be a SphericalCoord, got {type(angle).__name__}")
482 if not isinstance(lookAt, vec3):
483 raise ValueError(f"LookAt must be a vec3, got {type(lookAt).__name__}")
484
485 try:
486 visualizer_wrapper.set_camera_position_spherical(self.visualizer, angle, lookAt)
487 logger.debug(f"Camera position set to spherical (r={angle.radius}, el={angle.elevation}, az={angle.azimuth}), looking at ({lookAt.x}, {lookAt.y}, {lookAt.z})")
488 except Exception as e:
489 raise VisualizerError(f"Failed to set camera position (spherical): {e}")
490
491 def setBackgroundColor(self, color: RGBcolor) -> None:
492 """
493 Set background color.
494
495 Args:
496 color: Background color as RGBcolor with values in range [0, 1]
497
498 Raises:
499 VisualizerError: If color setting fails
500 ValueError: If color values are invalid
501 """
502 if self.visualizer is None:
503 raise VisualizerError("Visualizer has been destroyed")
504
505 # Validate DataType parameter
506 if not isinstance(color, RGBcolor):
507 raise ValueError(f"Color must be an RGBcolor, got {type(color).__name__}")
508
509 # Validate color range
510 if not (0 <= color.r <= 1 and 0 <= color.g <= 1 and 0 <= color.b <= 1):
511 raise ValueError(f"Color components ({color.r}, {color.g}, {color.b}) must be in range [0, 1]")
512
513 try:
514 visualizer_wrapper.set_background_color(self.visualizer, color)
515 logger.debug(f"Background color set to ({color.r}, {color.g}, {color.b})")
516 except Exception as e:
517 raise VisualizerError(f"Failed to set background color: {e}")
518
519 def setBackgroundTransparent(self) -> None:
520 """
521 Enable transparent background mode (v1.3.53+).
522
523 Sets the background to transparent with checkerboard pattern display.
524 Requires PNG output format to preserve transparency.
525
526 Note: When using transparent background, use printWindow() with PNG
527 format to save transparent images.
528
529 Raises:
530 VisualizerError: If transparent background setting fails
531 """
532 if self.visualizer is None:
533 raise VisualizerError("Visualizer has been destroyed")
534
535 try:
536 visualizer_wrapper.set_background_transparent(self.visualizer)
537 logger.debug("Background set to transparent mode")
538 except Exception as e:
539 raise VisualizerError(f"Failed to set transparent background: {e}")
540
541 def setBackgroundImage(self, texture_file: str) -> None:
542 """
543 Set custom background image texture (v1.3.53+).
544
545 Args:
546 texture_file: Path to background image file
547 Can be absolute or relative to working directory
548
549 Raises:
550 VisualizerError: If background image setting fails
551 ValueError: If texture file path is invalid
552 """
553 if self.visualizer is None:
554 raise VisualizerError("Visualizer has been destroyed")
555
556 if not texture_file or not isinstance(texture_file, str):
557 raise ValueError("Texture file path must be a non-empty string")
559 # Resolve texture file path relative to user's working directory
560 resolved_path = _resolve_user_path(texture_file)
561
562 try:
563 visualizer_wrapper.set_background_image(self.visualizer, resolved_path)
564 logger.debug(f"Background image set to {resolved_path}")
565 except Exception as e:
566 raise VisualizerError(f"Failed to set background image: {e}")
567
568 def setBackgroundSkyTexture(self, texture_file: Optional[str] = None, divisions: int = 50) -> None:
569 """
570 Set sky sphere texture background with automatic scaling (v1.3.53+).
571
572 Creates a sky sphere that automatically scales with the scene.
573 Replaces the deprecated addSkyDomeByCenter() method.
574
575 Args:
576 texture_file: Path to spherical/equirectangular texture image
577 If None, uses default gradient sky texture
578 divisions: Number of sphere tessellation divisions (default: 50)
579 Higher values create smoother sphere but use more GPU
580
581 Raises:
582 VisualizerError: If sky texture setting fails
583 ValueError: If parameters are invalid
584
585 Example:
586 >>> visualizer.setBackgroundSkyTexture() # Default gradient sky
587 >>> visualizer.setBackgroundSkyTexture("sky_hdri.jpg", divisions=100)
588 """
589 if self.visualizer is None:
590 raise VisualizerError("Visualizer has been destroyed")
591
592 if not isinstance(divisions, int) or divisions <= 0:
593 raise ValueError("Divisions must be a positive integer")
594
595 # Resolve texture file path if provided
596 resolved_path = None
597 if texture_file:
598 if not isinstance(texture_file, str):
599 raise ValueError("Texture file must be a string")
600 resolved_path = _resolve_user_path(texture_file)
601
602 try:
603 visualizer_wrapper.set_background_sky_texture(
604 self.visualizer,
605 resolved_path,
606 divisions
607 )
608 if resolved_path:
609 logger.debug(f"Sky texture background set: {resolved_path}, divisions={divisions}")
610 else:
611 logger.debug(f"Default sky texture background set with divisions={divisions}")
612 except Exception as e:
613 raise VisualizerError(f"Failed to set sky texture background: {e}")
614
615 def setLightDirection(self, direction: vec3) -> None:
616 """
617 Set light direction.
618
619 Args:
620 direction: Light direction vector as vec3 (will be normalized)
621
622 Raises:
623 VisualizerError: If light direction setting fails
624 ValueError: If direction is invalid
625 """
626 if self.visualizer is None:
627 raise VisualizerError("Visualizer has been destroyed")
628
629 # Validate DataType parameter
630 if not isinstance(direction, vec3):
631 raise ValueError(f"Direction must be a vec3, got {type(direction).__name__}")
632
633 # Check for zero vector
634 if direction.x == 0 and direction.y == 0 and direction.z == 0:
635 raise ValueError("Light direction cannot be zero vector")
636
637 try:
638 visualizer_wrapper.set_light_direction(self.visualizer, direction)
639 logger.debug(f"Light direction set to ({direction.x}, {direction.y}, {direction.z})")
640 except Exception as e:
641 raise VisualizerError(f"Failed to set light direction: {e}")
642
643 def setLightingModel(self, lighting_model: Union[int, str]) -> None:
644 """
645 Set lighting model.
646
647 Args:
648 lighting_model: Lighting model, either:
649 - 0 or "none": No lighting
650 - 1 or "phong": Phong shading
651 - 2 or "phong_shadowed": Phong shading with shadows
652
653 Raises:
654 VisualizerError: If lighting model setting fails
655 ValueError: If lighting model is invalid
656 """
657 if self.visualizer is None:
658 raise VisualizerError("Visualizer has been destroyed")
659
660 # Convert string to integer if needed
661 if isinstance(lighting_model, str):
662 lighting_model_lower = lighting_model.lower()
663 if lighting_model_lower in ['none', 'no', 'off']:
664 lighting_model = self.LIGHTING_NONE
665 elif lighting_model_lower in ['phong', 'phong_lighting']:
666 lighting_model = self.LIGHTING_PHONG
667 elif lighting_model_lower in ['phong_shadowed', 'phong_shadows', 'shadowed']:
668 lighting_model = self.LIGHTING_PHONG_SHADOWED
669 else:
670 raise ValueError(f"Unknown lighting model string: {lighting_model}")
671
672 # Validate integer value
673 if lighting_model not in [self.LIGHTING_NONE, self.LIGHTING_PHONG, self.LIGHTING_PHONG_SHADOWED]:
674 raise ValueError(f"Lighting model must be 0 (NONE), 1 (PHONG), or 2 (PHONG_SHADOWED), got {lighting_model}")
675
676 try:
677 visualizer_wrapper.set_lighting_model(self.visualizer, lighting_model)
678 model_names = {0: "NONE", 1: "PHONG", 2: "PHONG_SHADOWED"}
679 logger.debug(f"Lighting model set to {model_names.get(lighting_model, lighting_model)}")
680 except Exception as e:
681 raise VisualizerError(f"Failed to set lighting model: {e}")
682
683 def colorContextPrimitivesByData(self, data_name: str, uuids: Optional[List[int]] = None) -> None:
684 """
685 Color context primitives based on primitive data values.
686
687 This method maps primitive data values to colors using the current colormap.
688 The visualization will be updated to show data variations across primitives.
689
690 The data must have been previously set on the primitives in the Context using
691 context.setPrimitiveDataFloat(UUID, data_name, value) before calling this method.
692
693 Args:
694 data_name: Name of the primitive data to use for coloring.
695 This should match the data label used with setPrimitiveDataFloat().
696 uuids: Optional list of specific primitive UUIDs to color.
697 If None, all primitives in context will be colored.
698
699 Raises:
700 VisualizerError: If visualizer is not initialized or operation fails
701 ValueError: If data_name is invalid or UUIDs are malformed
702
703 Example:
704 >>> # Set data on primitives in context
705 >>> context.setPrimitiveDataFloat(patch_uuid, "radiation_flux_SW", 450.2)
706 >>> context.setPrimitiveDataFloat(triangle_uuid, "radiation_flux_SW", 320.1)
707 >>>
708 >>> # Build geometry and color by data
709 >>> visualizer.buildContextGeometry(context)
710 >>> visualizer.colorContextPrimitivesByData("radiation_flux_SW")
711 >>> visualizer.plotInteractive()
712
713 >>> # Color only specific primitives
714 >>> visualizer.colorContextPrimitivesByData("temperature", [uuid1, uuid2, uuid3])
715 """
716 if not self.visualizer:
717 raise VisualizerError("Visualizer not initialized")
718
719 if not data_name or not isinstance(data_name, str):
720 raise ValueError("Data name must be a non-empty string")
721
722 try:
723 if uuids is None:
724 # Color all primitives
725 visualizer_wrapper.color_context_primitives_by_data(self.visualizer, data_name)
726 logger.debug(f"Colored all primitives by data: {data_name}")
727 else:
728 # Color specific primitives
729 if not isinstance(uuids, (list, tuple)) or not uuids:
730 raise ValueError("UUIDs must be a non-empty list or tuple")
731 if not all(isinstance(uuid, int) and uuid >= 0 for uuid in uuids):
732 raise ValueError("All UUIDs must be non-negative integers")
733
734 visualizer_wrapper.color_context_primitives_by_data_uuids(self.visualizer, data_name, list(uuids))
735 logger.debug(f"Colored {len(uuids)} primitives by data: {data_name}")
736
737 except ValueError:
738 # Re-raise ValueError as is
739 raise
740 except Exception as e:
741 raise VisualizerError(f"Failed to color primitives by data '{data_name}': {e}")
742
743 # Camera Control Methods
744
745 def setCameraFieldOfView(self, angle_FOV: float) -> None:
746 """
747 Set camera field of view angle.
748
749 Args:
750 angle_FOV: Field of view angle in degrees
751
752 Raises:
753 ValueError: If angle is invalid
754 VisualizerError: If operation fails
755 """
756 if not self.visualizer:
757 raise VisualizerError("Visualizer not initialized")
758
759 if not isinstance(angle_FOV, (int, float)):
760 raise ValueError("Field of view angle must be numeric")
761 if angle_FOV <= 0 or angle_FOV >= 180:
762 raise ValueError("Field of view angle must be between 0 and 180 degrees")
763
764 try:
765 helios_lib.setCameraFieldOfView(self.visualizer, ctypes.c_float(angle_FOV))
766 except Exception as e:
767 raise VisualizerError(f"Failed to set camera field of view: {e}")
768
769 def getCameraPosition(self) -> Tuple[vec3, vec3]:
770 """
771 Get current camera position and look-at point.
772
773 Returns:
774 Tuple of (camera_position, look_at_point) as vec3 objects
775
776 Raises:
777 VisualizerError: If operation fails
778 """
779 if not self.visualizer:
780 raise VisualizerError("Visualizer not initialized")
781
782 try:
783 # Prepare output arrays
784 camera_pos = (ctypes.c_float * 3)()
785 look_at = (ctypes.c_float * 3)()
786
787 helios_lib.getCameraPosition(self.visualizer, camera_pos, look_at)
789 return (vec3(camera_pos[0], camera_pos[1], camera_pos[2]),
790 vec3(look_at[0], look_at[1], look_at[2]))
791 except Exception as e:
792 raise VisualizerError(f"Failed to get camera position: {e}")
793
794 def getBackgroundColor(self) -> RGBcolor:
795 """
796 Get current background color.
797
798 Returns:
799 Background color as RGBcolor object
800
801 Raises:
802 VisualizerError: If operation fails
803 """
804 if not self.visualizer:
805 raise VisualizerError("Visualizer not initialized")
806
807 try:
808 # Prepare output array
809 color = (ctypes.c_float * 3)()
810
811 helios_lib.getBackgroundColor(self.visualizer, color)
812
813 return RGBcolor(color[0], color[1], color[2])
814 except Exception as e:
815 raise VisualizerError(f"Failed to get background color: {e}")
816
817 # Lighting Control Methods
818
819 def setLightIntensityFactor(self, intensity_factor: float) -> None:
820 """
821 Set light intensity scaling factor.
822
823 Args:
824 intensity_factor: Light intensity scaling factor (typically 0.1 to 10.0)
825
826 Raises:
827 ValueError: If intensity factor is invalid
828 VisualizerError: If operation fails
829 """
830 if not self.visualizer:
831 raise VisualizerError("Visualizer not initialized")
832
833 if not isinstance(intensity_factor, (int, float)):
834 raise ValueError("Light intensity factor must be numeric")
835 if intensity_factor <= 0:
836 raise ValueError("Light intensity factor must be positive")
837
838 try:
839 helios_lib.setLightIntensityFactor(self.visualizer, ctypes.c_float(intensity_factor))
840 except Exception as e:
841 raise VisualizerError(f"Failed to set light intensity factor: {e}")
842
843 # Window and Display Methods
844
845 def getWindowSize(self) -> Tuple[int, int]:
846 """
847 Get window size in pixels.
848
849 Returns:
850 Tuple of (width, height) in pixels
851
852 Raises:
853 VisualizerError: If operation fails
854 """
855 if not self.visualizer:
856 raise VisualizerError("Visualizer not initialized")
857
858 try:
859 width = ctypes.c_uint()
860 height = ctypes.c_uint()
861
862 helios_lib.getWindowSize(self.visualizer, ctypes.byref(width), ctypes.byref(height))
863
864 return (width.value, height.value)
865 except Exception as e:
866 raise VisualizerError(f"Failed to get window size: {e}")
867
868 def getFramebufferSize(self) -> Tuple[int, int]:
869 """
870 Get framebuffer size in pixels.
871
872 Returns:
873 Tuple of (width, height) in pixels
874
875 Raises:
876 VisualizerError: If operation fails
877 """
878 if not self.visualizer:
879 raise VisualizerError("Visualizer not initialized")
880
881 try:
882 width = ctypes.c_uint()
883 height = ctypes.c_uint()
884
885 helios_lib.getFramebufferSize(self.visualizer, ctypes.byref(width), ctypes.byref(height))
886
887 return (width.value, height.value)
888 except Exception as e:
889 raise VisualizerError(f"Failed to get framebuffer size: {e}")
890
891 def printWindowDefault(self) -> None:
892 """
893 Print window with default filename.
894
895 Raises:
896 VisualizerError: If operation fails
897 """
898 if not self.visualizer:
899 raise VisualizerError("Visualizer not initialized")
900
901 try:
902 helios_lib.printWindowDefault(self.visualizer)
903 except Exception as e:
904 raise VisualizerError(f"Failed to print window: {e}")
905
906 def displayImageFromPixels(self, pixel_data: List[int], width: int, height: int) -> None:
907 """
908 Display image from RGBA pixel data.
909
910 Args:
911 pixel_data: RGBA pixel data as list of integers (0-255)
912 width: Image width in pixels
913 height: Image height in pixels
914
915 Raises:
916 ValueError: If parameters are invalid
917 VisualizerError: If operation fails
918 """
919 if not self.visualizer:
920 raise VisualizerError("Visualizer not initialized")
921
922 if not isinstance(pixel_data, (list, tuple)):
923 raise ValueError("Pixel data must be a list or tuple")
924 if not isinstance(width, int) or width <= 0:
925 raise ValueError("Width must be a positive integer")
926 if not isinstance(height, int) or height <= 0:
927 raise ValueError("Height must be a positive integer")
929 expected_size = width * height * 4 # RGBA format
930 if len(pixel_data) != expected_size:
931 raise ValueError(f"Pixel data size mismatch: expected {expected_size}, got {len(pixel_data)}")
932
933 try:
934 # Convert to ctypes array
935 pixel_array = (ctypes.c_ubyte * len(pixel_data))(*pixel_data)
936 helios_lib.displayImageFromPixels(self.visualizer, pixel_array, width, height)
937 except Exception as e:
938 raise VisualizerError(f"Failed to display image from pixels: {e}")
939
940 def displayImageFromFile(self, filename: str) -> None:
941 """
942 Display image from file.
943
944 Args:
945 filename: Path to image file
946
947 Raises:
948 ValueError: If filename is invalid
949 VisualizerError: If operation fails
950 """
951 if not self.visualizer:
952 raise VisualizerError("Visualizer not initialized")
953
954 if not isinstance(filename, str) or not filename.strip():
955 raise ValueError("Filename must be a non-empty string")
956
957 try:
958 helios_lib.displayImageFromFile(self.visualizer, filename.encode('utf-8'))
959 except Exception as e:
960 raise VisualizerError(f"Failed to display image from file '{filename}': {e}")
961
962 # Window Data Access Methods
963
964 def getWindowPixelsRGB(self, buffer: List[int]) -> None:
965 """
966 Get RGB pixel data from current window.
967
968 Args:
969 buffer: Pre-allocated buffer to store pixel data
970
971 Raises:
972 ValueError: If buffer is invalid
973 VisualizerError: If operation fails
974 """
975 if not self.visualizer:
976 raise VisualizerError("Visualizer not initialized")
977
978 if not isinstance(buffer, list):
979 raise ValueError("Buffer must be a list")
980
981 try:
982 # Convert buffer to ctypes array
983 buffer_array = (ctypes.c_uint * len(buffer))(*buffer)
984 helios_lib.getWindowPixelsRGB(self.visualizer, buffer_array)
985
986 # Copy results back to Python list
987 for i in range(len(buffer)):
988 buffer[i] = buffer_array[i]
989 except Exception as e:
990 raise VisualizerError(f"Failed to get window pixels: {e}")
991
992 def getDepthMap(self) -> Tuple[List[float], int, int]:
993 """
994 Get depth map from current window.
995
996 Returns:
997 Tuple of (depth_pixels, width, height)
998
999 Raises:
1000 VisualizerError: If operation fails
1001 """
1002 if not self.visualizer:
1003 raise VisualizerError("Visualizer not initialized")
1004
1005 try:
1006 depth_ptr = ctypes.POINTER(ctypes.c_float)()
1007 width = ctypes.c_uint()
1008 height = ctypes.c_uint()
1009 buffer_size = ctypes.c_uint()
1010
1011 helios_lib.getDepthMap(self.visualizer, ctypes.byref(depth_ptr),
1012 ctypes.byref(width), ctypes.byref(height),
1013 ctypes.byref(buffer_size))
1014
1015 # Convert to Python list
1016 if depth_ptr and buffer_size.value > 0:
1017 depth_data = [depth_ptr[i] for i in range(buffer_size.value)]
1018 return (depth_data, width.value, height.value)
1019 else:
1020 return ([], 0, 0)
1021 except Exception as e:
1022 raise VisualizerError(f"Failed to get depth map: {e}")
1023
1024 def plotDepthMap(self) -> None:
1025 """
1026 Plot depth map visualization.
1027
1028 Raises:
1029 VisualizerError: If operation fails
1030 """
1031 if not self.visualizer:
1032 raise VisualizerError("Visualizer not initialized")
1033
1034 try:
1035 helios_lib.plotDepthMap(self.visualizer)
1036 except Exception as e:
1037 raise VisualizerError(f"Failed to plot depth map: {e}")
1038
1039 # Geometry Management Methods
1041 def clearGeometry(self) -> None:
1042 """
1043 Clear all geometry from visualizer.
1044
1045 Raises:
1046 VisualizerError: If operation fails
1047 """
1048 if not self.visualizer:
1049 raise VisualizerError("Visualizer not initialized")
1050
1051 try:
1052 helios_lib.clearGeometry(self.visualizer)
1053 except Exception as e:
1054 raise VisualizerError(f"Failed to clear geometry: {e}")
1055
1056 def clearContextGeometry(self) -> None:
1057 """
1058 Clear context geometry from visualizer.
1059
1060 Raises:
1061 VisualizerError: If operation fails
1062 """
1063 if not self.visualizer:
1064 raise VisualizerError("Visualizer not initialized")
1065
1066 try:
1067 helios_lib.clearContextGeometry(self.visualizer)
1068 except Exception as e:
1069 raise VisualizerError(f"Failed to clear context geometry: {e}")
1070
1071 def deleteGeometry(self, geometry_id: int) -> None:
1072 """
1073 Delete specific geometry by ID.
1074
1075 Args:
1076 geometry_id: ID of geometry to delete
1077
1078 Raises:
1079 ValueError: If geometry ID is invalid
1080 VisualizerError: If operation fails
1081 """
1082 if not self.visualizer:
1083 raise VisualizerError("Visualizer not initialized")
1084
1085 if not isinstance(geometry_id, int) or geometry_id < 0:
1086 raise ValueError("Geometry ID must be a non-negative integer")
1087
1088 try:
1089 helios_lib.deleteGeometry(self.visualizer, geometry_id)
1090 except Exception as e:
1091 raise VisualizerError(f"Failed to delete geometry {geometry_id}: {e}")
1092
1093 def updateContextPrimitiveColors(self) -> None:
1094 """
1095 Update context primitive colors.
1096
1097 Raises:
1098 VisualizerError: If operation fails
1099 """
1100 if not self.visualizer:
1101 raise VisualizerError("Visualizer not initialized")
1102
1103 try:
1104 helios_lib.updateContextPrimitiveColors(self.visualizer)
1105 except Exception as e:
1106 raise VisualizerError(f"Failed to update context primitive colors: {e}")
1107
1108 # Geometry Vertex Manipulation Methods (v1.3.53+)
1110 def getGeometryVertices(self, geometry_id: int) -> List[vec3]:
1111 """
1112 Get vertices of a geometry primitive.
1113
1114 Args:
1115 geometry_id: Unique identifier of the geometry primitive
1116
1117 Returns:
1118 List of vertices as vec3 objects
1119
1120 Raises:
1121 ValueError: If geometry ID is invalid
1122 VisualizerError: If operation fails
1123
1124 Example:
1125 >>> # Get vertices of a specific geometry
1126 >>> vertices = visualizer.getGeometryVertices(geometry_id)
1127 >>> for vertex in vertices:
1128 ... print(f"Vertex: ({vertex.x}, {vertex.y}, {vertex.z})")
1129 """
1130 if not self.visualizer:
1131 raise VisualizerError("Visualizer not initialized")
1132
1133 if not isinstance(geometry_id, int) or geometry_id < 0:
1134 raise ValueError("Geometry ID must be a non-negative integer")
1135
1136 try:
1137 vertices_list = visualizer_wrapper.get_geometry_vertices(self.visualizer, geometry_id)
1138 # Convert [[x,y,z], ...] to [vec3(), ...]
1139 return [vec3(v[0], v[1], v[2]) for v in vertices_list]
1140 except Exception as e:
1141 raise VisualizerError(f"Failed to get geometry vertices: {e}")
1142
1143 def setGeometryVertices(self, geometry_id: int, vertices: List[vec3]) -> None:
1144 """
1145 Set vertices of a geometry primitive.
1146
1147 This allows dynamic modification of geometry shapes during visualization.
1148 Useful for animating geometry or adjusting shapes based on simulation results.
1149
1150 Args:
1151 geometry_id: Unique identifier of the geometry primitive
1152 vertices: List of new vertices as vec3 objects
1153
1154 Raises:
1155 ValueError: If parameters are invalid
1156 VisualizerError: If operation fails
1157
1158 Example:
1159 >>> # Modify vertices of an existing geometry
1160 >>> vertices = visualizer.getGeometryVertices(geometry_id)
1161 >>> # Scale all vertices by 2x
1162 >>> scaled_vertices = [vec3(v.x*2, v.y*2, v.z*2) for v in vertices]
1163 >>> visualizer.setGeometryVertices(geometry_id, scaled_vertices)
1164 """
1165 if not self.visualizer:
1166 raise VisualizerError("Visualizer not initialized")
1167
1168 if not isinstance(geometry_id, int) or geometry_id < 0:
1169 raise ValueError("Geometry ID must be a non-negative integer")
1170
1171 if not vertices or not isinstance(vertices, (list, tuple)):
1172 raise ValueError("Vertices must be a non-empty list")
1173
1174 if not all(isinstance(v, vec3) for v in vertices):
1175 raise ValueError("All vertices must be vec3 objects")
1176
1177 try:
1178 visualizer_wrapper.set_geometry_vertices(self.visualizer, geometry_id, vertices)
1179 logger.debug(f"Set {len(vertices)} vertices for geometry {geometry_id}")
1180 except Exception as e:
1181 raise VisualizerError(f"Failed to set geometry vertices: {e}")
1182
1183 # Coordinate Axes and Grid Methods
1184
1185 def addCoordinateAxes(self) -> None:
1186 """
1187 Add coordinate axes at origin with unit length.
1188
1189 Raises:
1190 VisualizerError: If operation fails
1191 """
1192 if not self.visualizer:
1193 raise VisualizerError("Visualizer not initialized")
1194
1195 try:
1196 helios_lib.addCoordinateAxes(self.visualizer)
1197 except Exception as e:
1198 raise VisualizerError(f"Failed to add coordinate axes: {e}")
1199
1200 def addCoordinateAxesCustom(self, origin: vec3, length: vec3, sign: str = "both") -> None:
1201 """
1202 Add coordinate axes with custom properties.
1203
1204 Args:
1205 origin: Axes origin position
1206 length: Axes length in each direction
1207 sign: Axis direction ("both" or "positive")
1208
1209 Raises:
1210 ValueError: If parameters are invalid
1211 VisualizerError: If operation fails
1212 """
1213 if not self.visualizer:
1214 raise VisualizerError("Visualizer not initialized")
1215
1216 if not isinstance(origin, vec3):
1217 raise ValueError("Origin must be a vec3")
1218 if not isinstance(length, vec3):
1219 raise ValueError("Length must be a vec3")
1220 if not isinstance(sign, str) or sign not in ["both", "positive"]:
1221 raise ValueError("Sign must be 'both' or 'positive'")
1222
1223 try:
1224 origin_array = (ctypes.c_float * 3)(origin.x, origin.y, origin.z)
1225 length_array = (ctypes.c_float * 3)(length.x, length.y, length.z)
1226 helios_lib.addCoordinateAxesCustom(self.visualizer, origin_array, length_array, sign.encode('utf-8'))
1227 except Exception as e:
1228 raise VisualizerError(f"Failed to add custom coordinate axes: {e}")
1229
1230 def disableCoordinateAxes(self) -> None:
1231 """
1232 Remove coordinate axes.
1233
1234 Raises:
1235 VisualizerError: If operation fails
1236 """
1237 if not self.visualizer:
1238 raise VisualizerError("Visualizer not initialized")
1239
1240 try:
1241 helios_lib.disableCoordinateAxes(self.visualizer)
1242 except Exception as e:
1243 raise VisualizerError(f"Failed to disable coordinate axes: {e}")
1244
1245 def addGridWireFrame(self, center: vec3, size: vec3, subdivisions: List[int]) -> None:
1246 """
1247 Add grid wireframe.
1248
1249 Args:
1250 center: Grid center position
1251 size: Grid size in each direction
1252 subdivisions: Grid subdivisions [x, y, z]
1253
1254 Raises:
1255 ValueError: If parameters are invalid
1256 VisualizerError: If operation fails
1257 """
1258 if not self.visualizer:
1259 raise VisualizerError("Visualizer not initialized")
1260
1261 if not isinstance(center, vec3):
1262 raise ValueError("Center must be a vec3")
1263 if not isinstance(size, vec3):
1264 raise ValueError("Size must be a vec3")
1265 if not isinstance(subdivisions, (list, tuple)) or len(subdivisions) != 3:
1266 raise ValueError("Subdivisions must be a list of 3 integers")
1267 if not all(isinstance(s, int) and s > 0 for s in subdivisions):
1268 raise ValueError("All subdivisions must be positive integers")
1269
1270 try:
1271 center_array = (ctypes.c_float * 3)(center.x, center.y, center.z)
1272 size_array = (ctypes.c_float * 3)(size.x, size.y, size.z)
1273 subdiv_array = (ctypes.c_int * 3)(*subdivisions)
1274 helios_lib.addGridWireFrame(self.visualizer, center_array, size_array, subdiv_array)
1275 except Exception as e:
1276 raise VisualizerError(f"Failed to add grid wireframe: {e}")
1277
1278 # Colorbar Control Methods
1279
1280 def enableColorbar(self) -> None:
1281 """
1282 Enable colorbar.
1283
1284 Raises:
1285 VisualizerError: If operation fails
1286 """
1287 if not self.visualizer:
1288 raise VisualizerError("Visualizer not initialized")
1289
1290 try:
1291 helios_lib.enableColorbar(self.visualizer)
1292 except Exception as e:
1293 raise VisualizerError(f"Failed to enable colorbar: {e}")
1294
1295 def disableColorbar(self) -> None:
1296 """
1297 Disable colorbar.
1298
1299 Raises:
1300 VisualizerError: If operation fails
1301 """
1302 if not self.visualizer:
1303 raise VisualizerError("Visualizer not initialized")
1304
1305 try:
1306 helios_lib.disableColorbar(self.visualizer)
1307 except Exception as e:
1308 raise VisualizerError(f"Failed to disable colorbar: {e}")
1309
1310 def setColorbarPosition(self, position: vec3) -> None:
1311 """
1312 Set colorbar position.
1313
1314 Args:
1315 position: Colorbar position
1316
1317 Raises:
1318 ValueError: If position is invalid
1319 VisualizerError: If operation fails
1320 """
1321 if not self.visualizer:
1322 raise VisualizerError("Visualizer not initialized")
1323
1324 if not isinstance(position, vec3):
1325 raise ValueError("Position must be a vec3")
1326
1327 try:
1328 pos_array = (ctypes.c_float * 3)(position.x, position.y, position.z)
1329 helios_lib.setColorbarPosition(self.visualizer, pos_array)
1330 except Exception as e:
1331 raise VisualizerError(f"Failed to set colorbar position: {e}")
1332
1333 def setColorbarSize(self, width: float, height: float) -> None:
1334 """
1335 Set colorbar size.
1336
1337 Args:
1338 width: Colorbar width
1339 height: Colorbar height
1340
1341 Raises:
1342 ValueError: If size is invalid
1343 VisualizerError: If operation fails
1344 """
1345 if not self.visualizer:
1346 raise VisualizerError("Visualizer not initialized")
1347
1348 if not isinstance(width, (int, float)) or width <= 0:
1349 raise ValueError("Width must be a positive number")
1350 if not isinstance(height, (int, float)) or height <= 0:
1351 raise ValueError("Height must be a positive number")
1352
1353 try:
1354 size_array = (ctypes.c_float * 2)(float(width), float(height))
1355 helios_lib.setColorbarSize(self.visualizer, size_array)
1356 except Exception as e:
1357 raise VisualizerError(f"Failed to set colorbar size: {e}")
1359 def setColorbarRange(self, min_val: float, max_val: float) -> None:
1360 """
1361 Set colorbar range.
1362
1363 Args:
1364 min_val: Minimum value
1365 max_val: Maximum value
1366
1367 Raises:
1368 ValueError: If range is invalid
1369 VisualizerError: If operation fails
1370 """
1371 if not self.visualizer:
1372 raise VisualizerError("Visualizer not initialized")
1373
1374 if not isinstance(min_val, (int, float)):
1375 raise ValueError("Minimum value must be numeric")
1376 if not isinstance(max_val, (int, float)):
1377 raise ValueError("Maximum value must be numeric")
1378 if min_val >= max_val:
1379 raise ValueError("Minimum value must be less than maximum value")
1380
1381 try:
1382 helios_lib.setColorbarRange(self.visualizer, float(min_val), float(max_val))
1383 except Exception as e:
1384 raise VisualizerError(f"Failed to set colorbar range: {e}")
1385
1386 def setColorbarTicks(self, ticks: List[float]) -> None:
1387 """
1388 Set colorbar tick marks.
1389
1390 Args:
1391 ticks: List of tick values
1392
1393 Raises:
1394 ValueError: If ticks are invalid
1395 VisualizerError: If operation fails
1396 """
1397 if not self.visualizer:
1398 raise VisualizerError("Visualizer not initialized")
1399
1400 if not isinstance(ticks, (list, tuple)):
1401 raise ValueError("Ticks must be a list or tuple")
1402 if not all(isinstance(t, (int, float)) for t in ticks):
1403 raise ValueError("All tick values must be numeric")
1404
1405 try:
1406 if ticks:
1407 ticks_array = (ctypes.c_float * len(ticks))(*ticks)
1408 helios_lib.setColorbarTicks(self.visualizer, ticks_array, len(ticks))
1409 else:
1410 helios_lib.setColorbarTicks(self.visualizer, None, 0)
1411 except Exception as e:
1412 raise VisualizerError(f"Failed to set colorbar ticks: {e}")
1413
1414 def setColorbarTitle(self, title: str) -> None:
1415 """
1416 Set colorbar title.
1417
1418 Args:
1419 title: Colorbar title
1420
1421 Raises:
1422 ValueError: If title is invalid
1423 VisualizerError: If operation fails
1424 """
1425 if not self.visualizer:
1426 raise VisualizerError("Visualizer not initialized")
1427
1428 if not isinstance(title, str):
1429 raise ValueError("Title must be a string")
1430
1431 try:
1432 helios_lib.setColorbarTitle(self.visualizer, title.encode('utf-8'))
1433 except Exception as e:
1434 raise VisualizerError(f"Failed to set colorbar title: {e}")
1435
1436 def setColorbarFontColor(self, color: RGBcolor) -> None:
1437 """
1438 Set colorbar font color.
1439
1440 Args:
1441 color: Font color
1442
1443 Raises:
1444 ValueError: If color is invalid
1445 VisualizerError: If operation fails
1446 """
1447 if not self.visualizer:
1448 raise VisualizerError("Visualizer not initialized")
1449
1450 if not isinstance(color, RGBcolor):
1451 raise ValueError("Color must be an RGBcolor")
1452
1453 try:
1454 color_array = (ctypes.c_float * 3)(color.r, color.g, color.b)
1455 helios_lib.setColorbarFontColor(self.visualizer, color_array)
1456 except Exception as e:
1457 raise VisualizerError(f"Failed to set colorbar font color: {e}")
1458
1459 def setColorbarFontSize(self, font_size: int) -> None:
1460 """
1461 Set colorbar font size.
1462
1463 Args:
1464 font_size: Font size
1465
1466 Raises:
1467 ValueError: If font size is invalid
1468 VisualizerError: If operation fails
1469 """
1470 if not self.visualizer:
1471 raise VisualizerError("Visualizer not initialized")
1472
1473 if not isinstance(font_size, int) or font_size <= 0:
1474 raise ValueError("Font size must be a positive integer")
1475
1476 try:
1477 helios_lib.setColorbarFontSize(self.visualizer, font_size)
1478 except Exception as e:
1479 raise VisualizerError(f"Failed to set colorbar font size: {e}")
1480
1481 # Colormap Methods
1482
1483 def setColormap(self, colormap: Union[int, str]) -> None:
1484 """
1485 Set predefined colormap.
1486
1487 Args:
1488 colormap: Colormap ID (0-5) or name ("HOT", "COOL", "RAINBOW", "LAVA", "PARULA", "GRAY")
1489
1490 Raises:
1491 ValueError: If colormap is invalid
1492 VisualizerError: If operation fails
1493 """
1494 if not self.visualizer:
1495 raise VisualizerError("Visualizer not initialized")
1496
1497 colormap_map = {
1498 "HOT": 0, "COOL": 1, "RAINBOW": 2,
1499 "LAVA": 3, "PARULA": 4, "GRAY": 5
1500 }
1501
1502 if isinstance(colormap, str):
1503 if colormap.upper() not in colormap_map:
1504 raise ValueError(f"Unknown colormap name: {colormap}")
1505 colormap_id = colormap_map[colormap.upper()]
1506 elif isinstance(colormap, int):
1507 if colormap < 0 or colormap > 5:
1508 raise ValueError("Colormap ID must be 0-5")
1509 colormap_id = colormap
1510 else:
1511 raise ValueError("Colormap must be integer ID or string name")
1512
1513 try:
1514 helios_lib.setColormap(self.visualizer, colormap_id)
1515 except Exception as e:
1516 raise VisualizerError(f"Failed to set colormap: {e}")
1517
1518 def setCustomColormap(self, colors: List[RGBcolor], divisions: List[float]) -> None:
1519 """
1520 Set custom colormap.
1521
1522 Args:
1523 colors: List of RGB colors
1524 divisions: List of division points (same length as colors)
1525
1526 Raises:
1527 ValueError: If parameters are invalid
1528 VisualizerError: If operation fails
1529 """
1530 if not self.visualizer:
1531 raise VisualizerError("Visualizer not initialized")
1532
1533 if not isinstance(colors, (list, tuple)) or not colors:
1534 raise ValueError("Colors must be a non-empty list")
1535 if not isinstance(divisions, (list, tuple)) or not divisions:
1536 raise ValueError("Divisions must be a non-empty list")
1537 if len(colors) != len(divisions):
1538 raise ValueError("Colors and divisions must have the same length")
1539
1540 if not all(isinstance(c, RGBcolor) for c in colors):
1541 raise ValueError("All colors must be RGBcolor objects")
1542 if not all(isinstance(d, (int, float)) for d in divisions):
1543 raise ValueError("All divisions must be numeric")
1544
1545 try:
1546 # Flatten colors to RGB array
1547 color_array = (ctypes.c_float * (len(colors) * 3))()
1548 for i, color in enumerate(colors):
1549 color_array[i*3] = color.r
1550 color_array[i*3+1] = color.g
1551 color_array[i*3+2] = color.b
1552
1553 divisions_array = (ctypes.c_float * len(divisions))(*divisions)
1554
1555 helios_lib.setCustomColormap(self.visualizer, color_array, divisions_array, len(colors))
1556 except Exception as e:
1557 raise VisualizerError(f"Failed to set custom colormap: {e}")
1558
1559 # Advanced Coloring Methods
1560
1561 def colorContextPrimitivesByObjectData(self, data_name: str, obj_ids: Optional[List[int]] = None) -> None:
1562 """
1563 Color context primitives by object data.
1564
1565 Args:
1566 data_name: Name of object data to use for coloring
1567 obj_ids: Optional list of object IDs to color (None for all)
1568
1569 Raises:
1570 ValueError: If parameters are invalid
1571 VisualizerError: If operation fails
1572 """
1573 if not self.visualizer:
1574 raise VisualizerError("Visualizer not initialized")
1575
1576 if not isinstance(data_name, str) or not data_name.strip():
1577 raise ValueError("Data name must be a non-empty string")
1578
1579 try:
1580 if obj_ids is None:
1581 helios_lib.colorContextPrimitivesByObjectData(self.visualizer, data_name.encode('utf-8'))
1582 else:
1583 if not isinstance(obj_ids, (list, tuple)):
1584 raise ValueError("Object IDs must be a list or tuple")
1585 if not all(isinstance(oid, int) and oid >= 0 for oid in obj_ids):
1586 raise ValueError("All object IDs must be non-negative integers")
1587
1588 if obj_ids:
1589 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
1590 helios_lib.colorContextPrimitivesByObjectDataIDs(self.visualizer, data_name.encode('utf-8'), obj_ids_array, len(obj_ids))
1591 else:
1592 helios_lib.colorContextPrimitivesByObjectDataIDs(self.visualizer, data_name.encode('utf-8'), None, 0)
1593 except Exception as e:
1594 raise VisualizerError(f"Failed to color primitives by object data '{data_name}': {e}")
1595
1596 def colorContextPrimitivesRandomly(self, uuids: Optional[List[int]] = None) -> None:
1597 """
1598 Color context primitives randomly.
1599
1600 Args:
1601 uuids: Optional list of primitive UUIDs to color (None for all)
1602
1603 Raises:
1604 ValueError: If UUIDs are invalid
1605 VisualizerError: If operation fails
1606 """
1607 if not self.visualizer:
1608 raise VisualizerError("Visualizer not initialized")
1609
1610 try:
1611 if uuids is None:
1612 helios_lib.colorContextPrimitivesRandomly(self.visualizer, None, 0)
1613 else:
1614 if not isinstance(uuids, (list, tuple)):
1615 raise ValueError("UUIDs must be a list or tuple")
1616 if not all(isinstance(uuid, int) and uuid >= 0 for uuid in uuids):
1617 raise ValueError("All UUIDs must be non-negative integers")
1618
1619 if uuids:
1620 uuid_array = (ctypes.c_uint * len(uuids))(*uuids)
1621 helios_lib.colorContextPrimitivesRandomly(self.visualizer, uuid_array, len(uuids))
1622 else:
1623 helios_lib.colorContextPrimitivesRandomly(self.visualizer, None, 0)
1624 except Exception as e:
1625 raise VisualizerError(f"Failed to color primitives randomly: {e}")
1626
1627 def colorContextObjectsRandomly(self, obj_ids: Optional[List[int]] = None) -> None:
1628 """
1629 Color context objects randomly.
1630
1631 Args:
1632 obj_ids: Optional list of object IDs to color (None for all)
1633
1634 Raises:
1635 ValueError: If object IDs are invalid
1636 VisualizerError: If operation fails
1637 """
1638 if not self.visualizer:
1639 raise VisualizerError("Visualizer not initialized")
1640
1641 try:
1642 if obj_ids is None:
1643 helios_lib.colorContextObjectsRandomly(self.visualizer, None, 0)
1644 else:
1645 if not isinstance(obj_ids, (list, tuple)):
1646 raise ValueError("Object IDs must be a list or tuple")
1647 if not all(isinstance(oid, int) and oid >= 0 for oid in obj_ids):
1648 raise ValueError("All object IDs must be non-negative integers")
1649
1650 if obj_ids:
1651 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
1652 helios_lib.colorContextObjectsRandomly(self.visualizer, obj_ids_array, len(obj_ids))
1653 else:
1654 helios_lib.colorContextObjectsRandomly(self.visualizer, None, 0)
1655 except Exception as e:
1656 raise VisualizerError(f"Failed to color objects randomly: {e}")
1657
1658 def clearColor(self) -> None:
1659 """
1660 Clear primitive colors from previous coloring operations.
1661
1662 Raises:
1663 VisualizerError: If operation fails
1664 """
1665 if not self.visualizer:
1666 raise VisualizerError("Visualizer not initialized")
1667
1668 try:
1669 helios_lib.clearColor(self.visualizer)
1670 except Exception as e:
1671 raise VisualizerError(f"Failed to clear colors: {e}")
1672
1673 # Watermark Control Methods
1674
1675 def hideWatermark(self) -> None:
1676 """
1677 Hide Helios logo watermark.
1679 Raises:
1680 VisualizerError: If operation fails
1681 """
1682 if not self.visualizer:
1683 raise VisualizerError("Visualizer not initialized")
1684
1685 try:
1686 helios_lib.hideWatermark(self.visualizer)
1687 except Exception as e:
1688 raise VisualizerError(f"Failed to hide watermark: {e}")
1689
1690 def showWatermark(self) -> None:
1691 """
1692 Show Helios logo watermark.
1693
1694 Raises:
1695 VisualizerError: If operation fails
1696 """
1697 if not self.visualizer:
1698 raise VisualizerError("Visualizer not initialized")
1699
1700 try:
1701 helios_lib.showWatermark(self.visualizer)
1702 except Exception as e:
1703 raise VisualizerError(f"Failed to show watermark: {e}")
1704
1705 def updateWatermark(self) -> None:
1706 """
1707 Update watermark geometry to match current window size.
1708
1709 Raises:
1710 VisualizerError: If operation fails
1711 """
1712 if not self.visualizer:
1713 raise VisualizerError("Visualizer not initialized")
1714
1715 try:
1716 helios_lib.updateWatermark(self.visualizer)
1717 except Exception as e:
1718 raise VisualizerError(f"Failed to update watermark: {e}")
1719
1720 # Navigation Gizmo Methods (v1.3.53+)
1721
1722 def hideNavigationGizmo(self) -> None:
1723 """
1724 Hide navigation gizmo (coordinate axes indicator in corner).
1726 The navigation gizmo shows XYZ axes orientation and can be clicked
1727 to snap the camera to standard views (top, front, side, etc.).
1728
1729 Raises:
1730 VisualizerError: If operation fails
1731 """
1732 if not self.visualizer:
1733 raise VisualizerError("Visualizer not initialized")
1734
1735 try:
1736 visualizer_wrapper.hide_navigation_gizmo(self.visualizer)
1737 logger.debug("Navigation gizmo hidden")
1738 except Exception as e:
1739 raise VisualizerError(f"Failed to hide navigation gizmo: {e}")
1740
1741 def showNavigationGizmo(self) -> None:
1742 """
1743 Show navigation gizmo (coordinate axes indicator in corner).
1744
1745 The navigation gizmo shows XYZ axes orientation and can be clicked
1746 to snap the camera to standard views (top, front, side, etc.).
1747
1748 Note: Navigation gizmo is shown by default in v1.3.53+.
1749
1750 Raises:
1751 VisualizerError: If operation fails
1752 """
1753 if not self.visualizer:
1754 raise VisualizerError("Visualizer not initialized")
1755
1756 try:
1757 visualizer_wrapper.show_navigation_gizmo(self.visualizer)
1758 logger.debug("Navigation gizmo shown")
1759 except Exception as e:
1760 raise VisualizerError(f"Failed to show navigation gizmo: {e}")
1761
1762 # Performance and Utility Methods
1763
1764 def enableMessages(self) -> None:
1765 """
1766 Enable standard output from visualizer plugin.
1767
1768 Raises:
1769 VisualizerError: If operation fails
1770 """
1771 if not self.visualizer:
1772 raise VisualizerError("Visualizer not initialized")
1773
1774 try:
1775 helios_lib.enableMessages(self.visualizer)
1776 except Exception as e:
1777 raise VisualizerError(f"Failed to enable messages: {e}")
1778
1779 def disableMessages(self) -> None:
1780 """
1781 Disable standard output from visualizer plugin.
1782
1783 Raises:
1784 VisualizerError: If operation fails
1785 """
1786 if not self.visualizer:
1787 raise VisualizerError("Visualizer not initialized")
1788
1789 try:
1790 helios_lib.disableMessages(self.visualizer)
1791 except Exception as e:
1792 raise VisualizerError(f"Failed to disable messages: {e}")
1793
1794 def plotOnce(self, get_keystrokes: bool = True) -> None:
1795 """
1796 Run one rendering loop.
1797
1798 Args:
1799 get_keystrokes: Whether to process keystrokes
1800
1801 Raises:
1802 VisualizerError: If operation fails
1803 """
1804 if not self.visualizer:
1805 raise VisualizerError("Visualizer not initialized")
1806
1807 try:
1808 helios_lib.plotOnce(self.visualizer, get_keystrokes)
1809 except Exception as e:
1810 raise VisualizerError(f"Failed to run plot once: {e}")
1811
1812 def plotUpdateWithVisibility(self, hide_window: bool = False) -> None:
1813 """
1814 Update visualization with window visibility control.
1815
1816 Args:
1817 hide_window: Whether to hide the window during update
1818
1819 Raises:
1820 VisualizerError: If operation fails
1821 """
1822 if not self.visualizer:
1823 raise VisualizerError("Visualizer not initialized")
1824
1825 try:
1827 helios_lib.plotUpdateWithVisibility(self.visualizer, hide_window)
1828 except Exception as e:
1829 raise VisualizerError(f"Failed to update plot with visibility control: {e}")
1830
1831 def __del__(self):
1832 """Destructor to ensure proper cleanup."""
1833 if hasattr(self, 'visualizer') and self.visualizer is not None:
1834 try:
1836 visualizer_wrapper.destroy_visualizer(self.visualizer)
1837 except Exception:
1838 pass # Ignore errors during destruction
Raised when Visualizer operations fail.
High-level interface for 3D visualization and rendering.
None colorContextObjectsRandomly(self, Optional[List[int]] obj_ids=None)
Color context objects randomly.
None plotInteractive(self)
Open interactive visualization window.
None setColorbarFontColor(self, RGBcolor color)
Set colorbar font color.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
None hideNavigationGizmo(self)
Hide navigation gizmo (coordinate axes indicator in corner).
None setBackgroundImage(self, str texture_file)
Set custom background image texture (v1.3.53+).
None buildContextGeometry(self, Context context, Optional[List[int]] uuids=None)
Build Context geometry in the visualizer.
None disableMessages(self)
Disable standard output from visualizer plugin.
None hideWatermark(self)
Hide Helios logo watermark.
None setBackgroundTransparent(self)
Enable transparent background mode (v1.3.53+).
None colorContextPrimitivesByObjectData(self, str data_name, Optional[List[int]] obj_ids=None)
Color context primitives by object data.
__enter__(self)
Context manager entry.
__init__(self, int width, int height, int antialiasing_samples=1, bool headless=False)
Initialize Visualizer with graceful plugin handling.
None setGeometryVertices(self, int geometry_id, List[vec3] vertices)
Set vertices of a geometry primitive.
None clearColor(self)
Clear primitive colors from previous coloring operations.
None setColorbarPosition(self, vec3 position)
Set colorbar position.
None setCustomColormap(self, List[RGBcolor] colors, List[float] divisions)
Set custom colormap.
None deleteGeometry(self, int geometry_id)
Delete specific geometry by ID.
None getWindowPixelsRGB(self, List[int] buffer)
Get RGB pixel data from current window.
None setCameraPositionSpherical(self, SphericalCoord angle, vec3 lookAt)
Set camera position using spherical coordinates.
None clearGeometry(self)
Clear all geometry from visualizer.
None setCameraFieldOfView(self, float angle_FOV)
Set camera field of view angle.
None displayImageFromFile(self, str filename)
Display image from file.
None setLightingModel(self, Union[int, str] lighting_model)
Set lighting model.
None showNavigationGizmo(self)
Show navigation gizmo (coordinate axes indicator in corner).
None updateContextPrimitiveColors(self)
Update context primitive colors.
None setColorbarRange(self, float min_val, float max_val)
Set colorbar range.
None plotDepthMap(self)
Plot depth map visualization.
Tuple[int, int] getWindowSize(self)
Get window size in pixels.
List[vec3] getGeometryVertices(self, int geometry_id)
Get vertices of a geometry primitive.
None printWindowDefault(self)
Print window with default filename.
None disableColorbar(self)
Disable colorbar.
None plotUpdateWithVisibility(self, bool hide_window=False)
Update visualization with window visibility control.
Tuple[vec3, vec3] getCameraPosition(self)
Get current camera position and look-at point.
None setLightDirection(self, vec3 direction)
Set light direction.
None closeWindow(self)
Close visualization window.
None setColorbarTitle(self, str title)
Set colorbar title.
__del__(self)
Destructor to ensure proper cleanup.
None displayImageFromPixels(self, List[int] pixel_data, int width, int height)
Display image from RGBA pixel data.
None addCoordinateAxesCustom(self, vec3 origin, vec3 length, str sign="both")
Add coordinate axes with custom properties.
None setColormap(self, Union[int, str] colormap)
Set predefined colormap.
None setBackgroundColor(self, RGBcolor color)
Set background color.
None colorContextPrimitivesByData(self, str data_name, Optional[List[int]] uuids=None)
Color context primitives based on primitive data values.
None printWindow(self, str filename, Optional[str] image_format=None)
Save current visualization to image file.
None disableCoordinateAxes(self)
Remove coordinate axes.
None setColorbarSize(self, float width, float height)
Set colorbar size.
None clearContextGeometry(self)
Clear context geometry from visualizer.
Tuple[List[float], int, int] getDepthMap(self)
Get depth map from current window.
None setCameraPosition(self, vec3 position, vec3 lookAt)
Set camera position using Cartesian coordinates.
RGBcolor getBackgroundColor(self)
Get current background color.
None setLightIntensityFactor(self, float intensity_factor)
Set light intensity scaling factor.
None enableMessages(self)
Enable standard output from visualizer plugin.
None addCoordinateAxes(self)
Add coordinate axes at origin with unit length.
None addGridWireFrame(self, vec3 center, vec3 size, List[int] subdivisions)
Add grid wireframe.
None plotUpdate(self)
Update visualization (non-interactive).
None setColorbarFontSize(self, int font_size)
Set colorbar font size.
None setColorbarTicks(self, List[float] ticks)
Set colorbar tick marks.
None colorContextPrimitivesRandomly(self, Optional[List[int]] uuids=None)
Color context primitives randomly.
None showWatermark(self)
Show Helios logo watermark.
Tuple[int, int] getFramebufferSize(self)
Get framebuffer size in pixels.
None enableColorbar(self)
Enable colorbar.
None plotOnce(self, bool get_keystrokes=True)
Run one rendering loop.
None setBackgroundSkyTexture(self, Optional[str] texture_file=None, int divisions=50)
Set sky sphere texture background with automatic scaling (v1.3.53+).
None updateWatermark(self)
Update watermark geometry to match current window size.
str _resolve_user_path(str path)
Resolve a user-provided path to an absolute path before working directory changes.
Definition Visualizer.py:40
_visualizer_working_directory()
Context manager that temporarily changes working directory for visualizer operations.
Definition Visualizer.py:63