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