0.1.26
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, check_context_alive
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 _check_context_alive(self):
245 """Raise if a Context was loaded and has since been destroyed."""
246 if getattr(self, "_context", None) is not None:
247 check_context_alive(self._context, "Visualizer")
248
249 def __enter__(self):
250 """Context manager entry."""
251 return self
252
253 def __exit__(self, exc_type, exc_value, traceback):
254 """Context manager exit with proper cleanup."""
255 if self.visualizer is not None:
256 try:
258 visualizer_wrapper.destroy_visualizer(self.visualizer)
259 logger.debug("Visualizer destroyed successfully")
260 except Exception as e:
261 logger.warning(f"Error destroying Visualizer: {e}")
262 finally:
263 self.visualizer = None
264
265 @validate_build_geometry_params
266 def buildContextGeometry(self, context: Context, uuids: Optional[List[int]] = None) -> None:
267 """
268 Build Context geometry in the visualizer.
269
270 This method loads geometry from a Helios Context into the visualizer
271 for rendering. If no UUIDs are specified, all geometry is loaded.
272
273 Args:
274 context: Helios Context instance containing geometry
275 uuids: Optional list of primitive UUIDs to visualize (default: all)
276
277 Raises:
278 VisualizerError: If geometry building fails
279 ValueError: If parameters are invalid
280 """
281 if self.visualizer is None:
282 raise VisualizerError("Visualizer has been destroyed")
283 if not isinstance(context, Context):
284 raise ValueError("context must be a Context instance")
285
286 # Retain a reference to the Context. The native visualizer stores the raw
287 # Context* and only dereferences it later, at render time, so without this
288 # a temporary Context (e.g. buildContextGeometry(make_scene())) would be
289 # garbage collected before the first plot call and crash the interpreter.
290 self._context = context
291
292 try:
294 if uuids is None:
295 # Load all geometry
296 visualizer_wrapper.build_context_geometry(self.visualizer, context.getNativePtr())
297 logger.debug("Built all Context geometry in visualizer")
298 else:
299 # Load specific UUIDs
300 if not uuids:
301 raise ValueError("UUIDs list cannot be empty")
302 visualizer_wrapper.build_context_geometry_uuids(
303 self.visualizer, context.getNativePtr(), uuids
304 )
305 logger.debug(f"Built {len(uuids)} primitives in visualizer")
306
307 except Exception as e:
308 raise VisualizerError(f"Failed to build Context geometry: {e}")
309
310 def plotInteractive(self) -> None:
311 """
312 Open interactive visualization window.
313
314 This method opens a window with the current scene and allows user
315 interaction (camera rotation, zooming, etc.). The program will pause
316 until the window is closed by the user.
317
318 Interactive controls:
319 - Mouse scroll: Zoom in/out
320 - Left mouse + drag: Rotate camera
321 - Right mouse + drag: Pan camera
322 - Arrow keys: Camera movement
323 - +/- keys: Zoom in/out
324
325 Raises:
326 VisualizerError: If visualization fails
327 """
329 if self.visualizer is None:
330 raise VisualizerError("Visualizer has been destroyed")
331
332 try:
334 visualizer_wrapper.plot_interactive(self.visualizer)
335 logger.debug("Interactive visualization completed")
336 except Exception as e:
337 raise VisualizerError(f"Interactive visualization failed: {e}")
338
339 def plotUpdate(self) -> None:
340 """
341 Update visualization (non-interactive).
342
343 This method updates the visualization window without user interaction.
344 The program continues immediately after rendering. Useful for batch
345 processing or creating image sequences.
346
347 In headless mode, automatically hides the window to prevent graphics driver crashes on some platforms.
348
349 Raises:
350 VisualizerError: If visualization update fails
351 """
353 if self.visualizer is None:
354 raise VisualizerError("Visualizer has been destroyed")
355
356 try:
358 # In headless mode, hide the window to avoid OpenGL/Metal crashes on macOS
359 visualizer_wrapper.plot_update(self.visualizer, hide_window=self.headless)
360 logger.debug("Visualization updated")
361 except Exception as e:
362 raise VisualizerError(f"Visualization update failed: {e}")
364 @validate_print_window_params
365 def printWindow(self, filename: str, image_format: Optional[str] = None) -> None:
366 """
367 Save current visualization to image file.
368
369 This method exports the current visualization to an image file.
370 Starting from v1.3.53, supports both JPEG and PNG formats.
371
372 Args:
373 filename: Output filename for image
374 Can be absolute or relative to user's current working directory
375 Extension (.jpg, .png) is recommended but not required
376 image_format: Image format - "jpeg" or "png" (v1.3.53+).
377 If None, automatically detects from filename extension.
378 Defaults to "jpeg" if not detectable from extension.
379
380 Raises:
381 VisualizerError: If image saving fails
382 ValueError: If filename or format is invalid
383
384 Note:
385 PNG format is required to preserve transparent backgrounds when using
386 setBackgroundTransparent(). JPEG format will render transparent areas as black.
387
388 Example:
389 >>> visualizer.printWindow("output.jpg") # Auto-detects JPEG
390 >>> visualizer.printWindow("output.png") # Auto-detects PNG
391 >>> visualizer.printWindow("output.img", image_format="png") # Explicit PNG
392 """
394 if self.visualizer is None:
395 raise VisualizerError("Visualizer has been destroyed")
396 if not filename:
397 raise ValueError("Filename cannot be empty")
398
399 # Resolve filename relative to user's working directory before chdir
400 resolved_filename = _resolve_user_path(filename)
401
402 # Auto-detect format from extension if not specified
403 if image_format is None:
404 if resolved_filename.lower().endswith('.png'):
405 image_format = 'png'
406 elif resolved_filename.lower().endswith(('.jpg', '.jpeg')):
407 image_format = 'jpeg'
408 else:
409 # Default to jpeg for backward compatibility
410 image_format = 'jpeg'
411 logger.debug(f"No format specified and extension not recognized, defaulting to JPEG")
412
413 # Validate format
414 if image_format.lower() not in ['jpeg', 'png']:
415 raise ValueError(f"Image format must be 'jpeg' or 'png', got '{image_format}'")
416
417 try:
419 # Try using the new format-aware function (v1.3.53+)
420 try:
421 visualizer_wrapper.print_window_with_format(
422 self.visualizer,
423 resolved_filename,
424 image_format
425 )
426 logger.debug(f"Visualization saved to {resolved_filename} ({image_format.upper()} format)")
427 except (AttributeError, NotImplementedError):
428 # Fallback to old function for older Helios versions
429 if image_format.lower() != 'jpeg':
430 logger.warning(
431 "PNG format requested but not available in current Helios version. "
432 "Falling back to JPEG format. Update to Helios v1.3.53+ for PNG support."
433 )
434 visualizer_wrapper.print_window(self.visualizer, resolved_filename)
435 logger.debug(f"Visualization saved to {resolved_filename} (JPEG format - legacy mode)")
436 except Exception as e:
437 raise VisualizerError(f"Failed to save image: {e}")
438
439 def closeWindow(self) -> None:
440 """
441 Close visualization window.
442
443 This method closes any open visualization window. It's safe to call
444 even if no window is open.
445
446 Raises:
447 VisualizerError: If window closing fails
448 """
449 if self.visualizer is None:
450 raise VisualizerError("Visualizer has been destroyed")
451
452 try:
453 visualizer_wrapper.close_window(self.visualizer)
454 logger.debug("Visualization window closed")
455 except Exception as e:
456 raise VisualizerError(f"Failed to close window: {e}")
457
458 def setCameraPosition(self, position: vec3, lookAt: vec3) -> None:
459 """
460 Set camera position using Cartesian coordinates.
461
462 Args:
463 position: Camera position as vec3 in world coordinates
464 lookAt: Camera look-at point as vec3 in world coordinates
465
466 Raises:
467 VisualizerError: If camera positioning fails
468 ValueError: If parameters are invalid
469 """
470 if self.visualizer is None:
471 raise VisualizerError("Visualizer has been destroyed")
472
473 # Validate DataType parameters
474 if not isinstance(position, vec3):
475 raise ValueError(f"Position must be a vec3, got {type(position).__name__}")
476 if not isinstance(lookAt, vec3):
477 raise ValueError(f"LookAt must be a vec3, got {type(lookAt).__name__}")
478
479 try:
480 visualizer_wrapper.set_camera_position(self.visualizer, position, lookAt)
481 logger.debug(f"Camera position set to ({position.x}, {position.y}, {position.z}), looking at ({lookAt.x}, {lookAt.y}, {lookAt.z})")
482 except Exception as e:
483 raise VisualizerError(f"Failed to set camera position: {e}")
484
485 def setCameraPositionSpherical(self, angle: SphericalCoord, lookAt: vec3) -> None:
486 """
487 Set camera position using spherical coordinates.
488
489 Args:
490 angle: Camera position as SphericalCoord (radius, elevation, azimuth)
491 lookAt: Camera look-at point as vec3 in world coordinates
492
493 Raises:
494 VisualizerError: If camera positioning fails
495 ValueError: If parameters are invalid
496 """
497 if self.visualizer is None:
498 raise VisualizerError("Visualizer has been destroyed")
499
500 # Validate DataType parameters
501 if not isinstance(angle, SphericalCoord):
502 raise ValueError(f"Angle must be a SphericalCoord, got {type(angle).__name__}")
503 if not isinstance(lookAt, vec3):
504 raise ValueError(f"LookAt must be a vec3, got {type(lookAt).__name__}")
505
506 try:
507 visualizer_wrapper.set_camera_position_spherical(self.visualizer, angle, lookAt)
508 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})")
509 except Exception as e:
510 raise VisualizerError(f"Failed to set camera position (spherical): {e}")
511
512 def setBackgroundColor(self, color: RGBcolor) -> None:
513 """
514 Set background color.
515
516 Args:
517 color: Background color as RGBcolor with values in range [0, 1]
518
519 Raises:
520 VisualizerError: If color setting fails
521 ValueError: If color values are invalid
522 """
523 if self.visualizer is None:
524 raise VisualizerError("Visualizer has been destroyed")
525
526 # Validate DataType parameter
527 if not isinstance(color, RGBcolor):
528 raise ValueError(f"Color must be an RGBcolor, got {type(color).__name__}")
529
530 # Validate color range
531 if not (0 <= color.r <= 1 and 0 <= color.g <= 1 and 0 <= color.b <= 1):
532 raise ValueError(f"Color components ({color.r}, {color.g}, {color.b}) must be in range [0, 1]")
533
534 try:
535 visualizer_wrapper.set_background_color(self.visualizer, color)
536 logger.debug(f"Background color set to ({color.r}, {color.g}, {color.b})")
537 except Exception as e:
538 raise VisualizerError(f"Failed to set background color: {e}")
539
540 def setBackgroundTransparent(self) -> None:
541 """
542 Enable transparent background mode (v1.3.53+).
543
544 Sets the background to transparent with checkerboard pattern display.
545 Requires PNG output format to preserve transparency.
546
547 Note: When using transparent background, use printWindow() with PNG
548 format to save transparent images.
549
550 Raises:
551 VisualizerError: If transparent background setting fails
552 """
553 if self.visualizer is None:
554 raise VisualizerError("Visualizer has been destroyed")
555
556 try:
557 visualizer_wrapper.set_background_transparent(self.visualizer)
558 logger.debug("Background set to transparent mode")
559 except Exception as e:
560 raise VisualizerError(f"Failed to set transparent background: {e}")
561
562 def setBackgroundImage(self, texture_file: str) -> None:
563 """
564 Set custom background image texture (v1.3.53+).
565
566 Args:
567 texture_file: Path to background image file
568 Can be absolute or relative to working directory
569
570 Raises:
571 VisualizerError: If background image setting fails
572 ValueError: If texture file path is invalid
573 """
574 if self.visualizer is None:
575 raise VisualizerError("Visualizer has been destroyed")
576
577 if not texture_file or not isinstance(texture_file, str):
578 raise ValueError("Texture file path must be a non-empty string")
579
580 # Resolve texture file path relative to user's working directory
581 resolved_path = _resolve_user_path(texture_file)
582
583 try:
584 visualizer_wrapper.set_background_image(self.visualizer, resolved_path)
585 logger.debug(f"Background image set to {resolved_path}")
586 except Exception as e:
587 raise VisualizerError(f"Failed to set background image: {e}")
588
589 def setBackgroundSkyTexture(self, texture_file: Optional[str] = None, divisions: int = 50) -> None:
590 """
591 Set sky sphere texture background with automatic scaling (v1.3.53+).
592
593 Creates a sky sphere that automatically scales with the scene.
594 Replaces the deprecated addSkyDomeByCenter() method.
595
596 Args:
597 texture_file: Path to spherical/equirectangular texture image
598 If None, uses default gradient sky texture
599 divisions: Number of sphere tessellation divisions (default: 50)
600 Higher values create smoother sphere but use more GPU
601
602 Raises:
603 VisualizerError: If sky texture setting fails
604 ValueError: If parameters are invalid
605
606 Example:
607 >>> visualizer.setBackgroundSkyTexture() # Default gradient sky
608 >>> visualizer.setBackgroundSkyTexture("sky_hdri.jpg", divisions=100)
609 """
610 if self.visualizer is None:
611 raise VisualizerError("Visualizer has been destroyed")
612
613 if not isinstance(divisions, _INT_TYPE) or divisions <= 0:
614 raise ValueError("Divisions must be a positive integer")
615
616 # Resolve texture file path if provided
617 resolved_path = None
618 if texture_file:
619 if not isinstance(texture_file, str):
620 raise ValueError("Texture file must be a string")
621 resolved_path = _resolve_user_path(texture_file)
622
623 try:
624 visualizer_wrapper.set_background_sky_texture(
625 self.visualizer,
626 resolved_path,
627 divisions
628 )
629 if resolved_path:
630 logger.debug(f"Sky texture background set: {resolved_path}, divisions={divisions}")
631 else:
632 logger.debug(f"Default sky texture background set with divisions={divisions}")
633 except Exception as e:
634 raise VisualizerError(f"Failed to set sky texture background: {e}")
635
636 def setLightDirection(self, direction: vec3) -> None:
637 """
638 Set light direction.
639
640 Args:
641 direction: Light direction vector as vec3 (will be normalized)
642
643 Raises:
644 VisualizerError: If light direction setting fails
645 ValueError: If direction is invalid
646 """
647 if self.visualizer is None:
648 raise VisualizerError("Visualizer has been destroyed")
649
650 # Validate DataType parameter
651 if not isinstance(direction, vec3):
652 raise ValueError(f"Direction must be a vec3, got {type(direction).__name__}")
653
654 # Check for zero vector
655 if direction.x == 0 and direction.y == 0 and direction.z == 0:
656 raise ValueError("Light direction cannot be zero vector")
657
658 try:
659 visualizer_wrapper.set_light_direction(self.visualizer, direction)
660 logger.debug(f"Light direction set to ({direction.x}, {direction.y}, {direction.z})")
661 except Exception as e:
662 raise VisualizerError(f"Failed to set light direction: {e}")
663
664 def setLightingModel(self, lighting_model: Union[int, str]) -> None:
665 """
666 Set lighting model.
667
668 Args:
669 lighting_model: Lighting model, either:
670 - 0 or "none": No lighting
671 - 1 or "phong": Phong shading
672 - 2 or "phong_shadowed": Phong shading with shadows
673
674 Raises:
675 VisualizerError: If lighting model setting fails
676 ValueError: If lighting model is invalid
677 """
678 if self.visualizer is None:
679 raise VisualizerError("Visualizer has been destroyed")
680
681 # Convert string to integer if needed
682 if isinstance(lighting_model, str):
683 lighting_model_lower = lighting_model.lower()
684 if lighting_model_lower in ['none', 'no', 'off']:
685 lighting_model = self.LIGHTING_NONE
686 elif lighting_model_lower in ['phong', 'phong_lighting']:
687 lighting_model = self.LIGHTING_PHONG
688 elif lighting_model_lower in ['phong_shadowed', 'phong_shadows', 'shadowed']:
689 lighting_model = self.LIGHTING_PHONG_SHADOWED
690 else:
691 raise ValueError(f"Unknown lighting model string: {lighting_model}")
692
693 # Validate integer value
694 if lighting_model not in [self.LIGHTING_NONE, self.LIGHTING_PHONG, self.LIGHTING_PHONG_SHADOWED]:
695 raise ValueError(f"Lighting model must be 0 (NONE), 1 (PHONG), or 2 (PHONG_SHADOWED), got {lighting_model}")
696
697 try:
698 visualizer_wrapper.set_lighting_model(self.visualizer, lighting_model)
699 model_names = {0: "NONE", 1: "PHONG", 2: "PHONG_SHADOWED"}
700 logger.debug(f"Lighting model set to {model_names.get(lighting_model, lighting_model)}")
701 except Exception as e:
702 raise VisualizerError(f"Failed to set lighting model: {e}")
703
704 def colorContextPrimitivesByData(self, data_name: str, uuids: Optional[List[int]] = None) -> None:
705 """
706 Color context primitives based on primitive data values.
707
708 This method maps primitive data values to colors using the current colormap.
709 The visualization will be updated to show data variations across primitives.
710
711 The data must have been previously set on the primitives in the Context using
712 context.setPrimitiveDataFloat(UUID, data_name, value) before calling this method.
713
714 Args:
715 data_name: Name of the primitive data to use for coloring.
716 This should match the data label used with setPrimitiveDataFloat().
717 uuids: Optional list of specific primitive UUIDs to color.
718 If None, all primitives in context will be colored.
719
720 Raises:
721 VisualizerError: If visualizer is not initialized or operation fails
722 ValueError: If data_name is invalid or UUIDs are malformed
723
724 Example:
725 >>> # Set data on primitives in context
726 >>> context.setPrimitiveDataFloat(patch_uuid, "radiation_flux_SW", 450.2)
727 >>> context.setPrimitiveDataFloat(triangle_uuid, "radiation_flux_SW", 320.1)
728 >>>
729 >>> # Build geometry and color by data
730 >>> visualizer.buildContextGeometry(context)
731 >>> visualizer.colorContextPrimitivesByData("radiation_flux_SW")
732 >>> visualizer.plotInteractive()
733
734 >>> # Color only specific primitives
735 >>> visualizer.colorContextPrimitivesByData("temperature", [uuid1, uuid2, uuid3])
736 """
737 if not self.visualizer:
738 raise VisualizerError("Visualizer not initialized")
739
740 if not data_name or not isinstance(data_name, str):
741 raise ValueError("Data name must be a non-empty string")
742
743 try:
744 if uuids is None:
745 # Color all primitives
746 visualizer_wrapper.color_context_primitives_by_data(self.visualizer, data_name)
747 logger.debug(f"Colored all primitives by data: {data_name}")
748 else:
749 # Color specific primitives
750 if not isinstance(uuids, (list, tuple)) or not uuids:
751 raise ValueError("UUIDs must be a non-empty list or tuple")
752 if not all(isinstance(uuid, _INT_TYPE) and uuid >= 0 for uuid in uuids):
753 raise ValueError("All UUIDs must be non-negative integers")
754
755 visualizer_wrapper.color_context_primitives_by_data_uuids(self.visualizer, data_name, list(uuids))
756 logger.debug(f"Colored {len(uuids)} primitives by data: {data_name}")
757
758 except ValueError:
759 # Re-raise ValueError as is
760 raise
761 except Exception as e:
762 raise VisualizerError(f"Failed to color primitives by data '{data_name}': {e}")
763
764 # Camera Control Methods
765
766 def setCameraFieldOfView(self, angle_FOV: float) -> None:
767 """
768 Set camera field of view angle.
769
770 Args:
771 angle_FOV: Field of view angle in degrees
772
773 Raises:
774 ValueError: If angle is invalid
775 VisualizerError: If operation fails
776 """
777 if not self.visualizer:
778 raise VisualizerError("Visualizer not initialized")
779
780 try:
781 float(angle_FOV)
782 except (TypeError, ValueError):
783 raise ValueError("Field of view angle must be numeric")
784 if angle_FOV <= 0 or angle_FOV >= 180:
785 raise ValueError("Field of view angle must be between 0 and 180 degrees")
786
787 try:
788 helios_lib.setCameraFieldOfView(self.visualizer, ctypes.c_float(angle_FOV))
789 except Exception as e:
790 raise VisualizerError(f"Failed to set camera field of view: {e}")
791
792 def getCameraPosition(self) -> Tuple[vec3, vec3]:
793 """
794 Get current camera position and look-at point.
795
796 Returns:
797 Tuple of (camera_position, look_at_point) as vec3 objects
798
799 Raises:
800 VisualizerError: If operation fails
801 """
802 if not self.visualizer:
803 raise VisualizerError("Visualizer not initialized")
804
805 try:
806 # Prepare output arrays
807 camera_pos = (ctypes.c_float * 3)()
808 look_at = (ctypes.c_float * 3)()
809
810 helios_lib.getCameraPosition(self.visualizer, camera_pos, look_at)
811
812 return (vec3(camera_pos[0], camera_pos[1], camera_pos[2]),
813 vec3(look_at[0], look_at[1], look_at[2]))
814 except Exception as e:
815 raise VisualizerError(f"Failed to get camera position: {e}")
816
817 def getBackgroundColor(self) -> RGBcolor:
818 """
819 Get current background color.
820
821 Returns:
822 Background color as RGBcolor object
823
824 Raises:
825 VisualizerError: If operation fails
826 """
827 if not self.visualizer:
828 raise VisualizerError("Visualizer not initialized")
829
830 try:
831 # Prepare output array
832 color = (ctypes.c_float * 3)()
833
834 helios_lib.getBackgroundColor(self.visualizer, color)
835
836 return RGBcolor(color[0], color[1], color[2])
837 except Exception as e:
838 raise VisualizerError(f"Failed to get background color: {e}")
839
840 # Lighting Control Methods
841
842 def setLightIntensityFactor(self, intensity_factor: float) -> None:
843 """
844 Set light intensity scaling factor.
845
846 Args:
847 intensity_factor: Light intensity scaling factor (typically 0.1 to 10.0)
848
849 Raises:
850 ValueError: If intensity factor is invalid
851 VisualizerError: If operation fails
852 """
853 if not self.visualizer:
854 raise VisualizerError("Visualizer not initialized")
855
856 if not isinstance(intensity_factor, _NUMERIC_TYPES):
857 raise ValueError("Light intensity factor must be numeric")
858 if intensity_factor <= 0:
859 raise ValueError("Light intensity factor must be positive")
860
861 try:
862 helios_lib.setLightIntensityFactor(self.visualizer, ctypes.c_float(intensity_factor))
863 except Exception as e:
864 raise VisualizerError(f"Failed to set light intensity factor: {e}")
865
866 # Window and Display Methods
867
868 def getWindowSize(self) -> Tuple[int, int]:
869 """
870 Get window 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.getWindowSize(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 window size: {e}")
890
891 def getFramebufferSize(self) -> Tuple[int, int]:
892 """
893 Get framebuffer size in pixels.
894
895 Returns:
896 Tuple of (width, height) in pixels
897
898 Raises:
899 VisualizerError: If operation fails
900 """
901 if not self.visualizer:
902 raise VisualizerError("Visualizer not initialized")
903
904 try:
905 width = ctypes.c_uint()
906 height = ctypes.c_uint()
907
908 helios_lib.getFramebufferSize(self.visualizer, ctypes.byref(width), ctypes.byref(height))
909
910 return (width.value, height.value)
911 except Exception as e:
912 raise VisualizerError(f"Failed to get framebuffer size: {e}")
913
914 def printWindowDefault(self) -> None:
915 """
916 Print window with default filename.
917
918 Raises:
919 VisualizerError: If operation fails
920 """
922 if not self.visualizer:
923 raise VisualizerError("Visualizer not initialized")
924
925 try:
926 helios_lib.printWindowDefault(self.visualizer)
927 except Exception as e:
928 raise VisualizerError(f"Failed to print window: {e}")
929
930 def displayImageFromPixels(self, pixel_data: List[int], width: int, height: int) -> None:
931 """
932 Display image from RGBA pixel data.
933
934 Args:
935 pixel_data: RGBA pixel data as list of integers (0-255)
936 width: Image width in pixels
937 height: Image height in pixels
938
939 Raises:
940 ValueError: If parameters are invalid
941 VisualizerError: If operation fails
942 """
943 if not self.visualizer:
944 raise VisualizerError("Visualizer not initialized")
945
946 if not isinstance(pixel_data, (list, tuple)):
947 raise ValueError("Pixel data must be a list or tuple")
948 if not isinstance(width, _INT_TYPE) or width <= 0:
949 raise ValueError("Width must be a positive integer")
950 if not isinstance(height, _INT_TYPE) or height <= 0:
951 raise ValueError("Height must be a positive integer")
952
953 expected_size = width * height * 4 # RGBA format
954 if len(pixel_data) != expected_size:
955 raise ValueError(f"Pixel data size mismatch: expected {expected_size}, got {len(pixel_data)}")
956
957 try:
958 # Convert to ctypes array
959 pixel_array = (ctypes.c_ubyte * len(pixel_data))(*pixel_data)
960 helios_lib.displayImageFromPixels(self.visualizer, pixel_array, width, height)
961 except Exception as e:
962 raise VisualizerError(f"Failed to display image from pixels: {e}")
963
964 def displayImageFromFile(self, filename: str) -> None:
965 """
966 Display image from file.
967
968 Args:
969 filename: Path to image file
970
971 Raises:
972 ValueError: If filename is invalid
973 VisualizerError: If operation fails
974 """
975 if not self.visualizer:
976 raise VisualizerError("Visualizer not initialized")
977
978 if not isinstance(filename, str) or not filename.strip():
979 raise ValueError("Filename must be a non-empty string")
980
981 try:
982 helios_lib.displayImageFromFile(self.visualizer, filename.encode('utf-8'))
983 except Exception as e:
984 raise VisualizerError(f"Failed to display image from file '{filename}': {e}")
985
986 # Window Data Access Methods
987
988 def getWindowPixelsRGB(self, buffer: List[int]) -> None:
989 """
990 Get RGB pixel data from current window.
991
992 Args:
993 buffer: Pre-allocated buffer to store pixel data
994
995 Raises:
996 ValueError: If buffer is invalid
997 VisualizerError: If operation fails
998 """
1000 if not self.visualizer:
1001 raise VisualizerError("Visualizer not initialized")
1002
1003 if not isinstance(buffer, list):
1004 raise ValueError("Buffer must be a list")
1005
1006 try:
1007 # Convert buffer to ctypes array
1008 buffer_array = (ctypes.c_uint * len(buffer))(*buffer)
1009 helios_lib.getWindowPixelsRGB(self.visualizer, buffer_array)
1011 # Copy results back to Python list
1012 for i in range(len(buffer)):
1013 buffer[i] = buffer_array[i]
1014 except Exception as e:
1015 raise VisualizerError(f"Failed to get window pixels: {e}")
1016
1017 def getDepthMap(self) -> Tuple[List[float], int, int]:
1018 """
1019 Get depth map from current window.
1020
1021 Returns:
1022 Tuple of (depth_pixels, width, height)
1023
1024 Raises:
1025 VisualizerError: If operation fails
1026 """
1028 if not self.visualizer:
1029 raise VisualizerError("Visualizer not initialized")
1030
1031 try:
1032 depth_ptr = ctypes.POINTER(ctypes.c_float)()
1033 width = ctypes.c_uint()
1034 height = ctypes.c_uint()
1035 buffer_size = ctypes.c_uint()
1036
1037 helios_lib.getDepthMap(self.visualizer, ctypes.byref(depth_ptr),
1038 ctypes.byref(width), ctypes.byref(height),
1039 ctypes.byref(buffer_size))
1040
1041 # Convert to Python list
1042 if depth_ptr and buffer_size.value > 0:
1043 depth_data = [depth_ptr[i] for i in range(buffer_size.value)]
1044 return (depth_data, width.value, height.value)
1045 else:
1046 return ([], 0, 0)
1047 except Exception as e:
1048 raise VisualizerError(f"Failed to get depth map: {e}")
1049
1050 def plotDepthMap(self) -> None:
1051 """
1052 Plot depth map visualization.
1053
1054 Raises:
1055 VisualizerError: If operation fails
1056 """
1058 if not self.visualizer:
1059 raise VisualizerError("Visualizer not initialized")
1060
1061 try:
1062 helios_lib.plotDepthMap(self.visualizer)
1063 except Exception as e:
1064 raise VisualizerError(f"Failed to plot depth map: {e}")
1065
1066 # Geometry Management Methods
1067
1068 def clearGeometry(self) -> None:
1069 """
1070 Clear all geometry from visualizer.
1071
1072 Raises:
1073 VisualizerError: If operation fails
1074 """
1075 if not self.visualizer:
1076 raise VisualizerError("Visualizer not initialized")
1077
1078 try:
1079 helios_lib.clearGeometry(self.visualizer)
1080 except Exception as e:
1081 raise VisualizerError(f"Failed to clear geometry: {e}")
1082
1083 def clearContextGeometry(self) -> None:
1084 """
1085 Clear context geometry from visualizer.
1087 Raises:
1088 VisualizerError: If operation fails
1089 """
1090 if not self.visualizer:
1091 raise VisualizerError("Visualizer not initialized")
1092
1093 try:
1094 helios_lib.clearContextGeometry(self.visualizer)
1095 except Exception as e:
1096 raise VisualizerError(f"Failed to clear context geometry: {e}")
1097
1098 def deleteGeometry(self, geometry_id: int) -> None:
1099 """
1100 Delete specific geometry by ID.
1102 Args:
1103 geometry_id: ID of geometry to delete
1104
1105 Raises:
1106 ValueError: If geometry ID is invalid
1107 VisualizerError: If operation fails
1108 """
1109 if not self.visualizer:
1110 raise VisualizerError("Visualizer not initialized")
1111
1112 if not isinstance(geometry_id, _INT_TYPE) or geometry_id < 0:
1113 raise ValueError("Geometry ID must be a non-negative integer")
1114
1115 try:
1116 helios_lib.deleteGeometry(self.visualizer, geometry_id)
1117 except Exception as e:
1118 raise VisualizerError(f"Failed to delete geometry {geometry_id}: {e}")
1119
1121 """
1122 Update context primitive colors.
1123
1124 Raises:
1125 VisualizerError: If operation fails
1126 """
1127 if not self.visualizer:
1128 raise VisualizerError("Visualizer not initialized")
1129
1130 try:
1131 helios_lib.updateContextPrimitiveColors(self.visualizer)
1132 except Exception as e:
1133 raise VisualizerError(f"Failed to update context primitive colors: {e}")
1134
1135 # Geometry Vertex Manipulation Methods (v1.3.53+)
1136
1137 def getGeometryVertices(self, geometry_id: int) -> List[vec3]:
1138 """
1139 Get vertices of a geometry primitive.
1140
1141 Args:
1142 geometry_id: Unique identifier of the geometry primitive
1143
1144 Returns:
1145 List of vertices as vec3 objects
1146
1147 Raises:
1148 ValueError: If geometry ID is invalid
1149 VisualizerError: If operation fails
1150
1151 Example:
1152 >>> # Get vertices of a specific geometry
1153 >>> vertices = visualizer.getGeometryVertices(geometry_id)
1154 >>> for vertex in vertices:
1155 ... print(f"Vertex: ({vertex.x}, {vertex.y}, {vertex.z})")
1156 """
1157 if not self.visualizer:
1158 raise VisualizerError("Visualizer not initialized")
1159
1160 if not isinstance(geometry_id, _INT_TYPE) or geometry_id < 0:
1161 raise ValueError("Geometry ID must be a non-negative integer")
1162
1163 try:
1164 vertices_list = visualizer_wrapper.get_geometry_vertices(self.visualizer, geometry_id)
1165 # Convert [[x,y,z], ...] to [vec3(), ...]
1166 return [vec3(v[0], v[1], v[2]) for v in vertices_list]
1167 except Exception as e:
1168 raise VisualizerError(f"Failed to get geometry vertices: {e}")
1169
1170 def setGeometryVertices(self, geometry_id: int, vertices: List[vec3]) -> None:
1171 """
1172 Set vertices of a geometry primitive.
1173
1174 This allows dynamic modification of geometry shapes during visualization.
1175 Useful for animating geometry or adjusting shapes based on simulation results.
1176
1177 Args:
1178 geometry_id: Unique identifier of the geometry primitive
1179 vertices: List of new vertices as vec3 objects
1180
1181 Raises:
1182 ValueError: If parameters are invalid
1183 VisualizerError: If operation fails
1184
1185 Example:
1186 >>> # Modify vertices of an existing geometry
1187 >>> vertices = visualizer.getGeometryVertices(geometry_id)
1188 >>> # Scale all vertices by 2x
1189 >>> scaled_vertices = [vec3(v.x*2, v.y*2, v.z*2) for v in vertices]
1190 >>> visualizer.setGeometryVertices(geometry_id, scaled_vertices)
1191 """
1192 if not self.visualizer:
1193 raise VisualizerError("Visualizer not initialized")
1194
1195 if not isinstance(geometry_id, _INT_TYPE) or geometry_id < 0:
1196 raise ValueError("Geometry ID must be a non-negative integer")
1197
1198 if not vertices or not isinstance(vertices, (list, tuple)):
1199 raise ValueError("Vertices must be a non-empty list")
1200
1201 if not all(isinstance(v, vec3) for v in vertices):
1202 raise ValueError("All vertices must be vec3 objects")
1204 try:
1205 visualizer_wrapper.set_geometry_vertices(self.visualizer, geometry_id, vertices)
1206 logger.debug(f"Set {len(vertices)} vertices for geometry {geometry_id}")
1207 except Exception as e:
1208 raise VisualizerError(f"Failed to set geometry vertices: {e}")
1209
1210 # Coordinate Axes and Grid Methods
1211
1212 def addCoordinateAxes(self) -> None:
1213 """
1214 Add coordinate axes at origin with unit length.
1215
1216 Raises:
1217 VisualizerError: If operation fails
1218 """
1219 if not self.visualizer:
1220 raise VisualizerError("Visualizer not initialized")
1221
1222 try:
1223 helios_lib.addCoordinateAxes(self.visualizer)
1224 except Exception as e:
1225 raise VisualizerError(f"Failed to add coordinate axes: {e}")
1226
1227 def addCoordinateAxesCustom(self, origin: vec3, length: vec3, sign: str = "both") -> None:
1228 """
1229 Add coordinate axes with custom properties.
1231 Args:
1232 origin: Axes origin position
1233 length: Axes length in each direction
1234 sign: Axis direction ("both" or "positive")
1235
1236 Raises:
1237 ValueError: If parameters are invalid
1238 VisualizerError: If operation fails
1239 """
1240 if not self.visualizer:
1241 raise VisualizerError("Visualizer not initialized")
1242
1243 if not isinstance(origin, vec3):
1244 raise ValueError("Origin must be a vec3")
1245 if not isinstance(length, vec3):
1246 raise ValueError("Length must be a vec3")
1247 if not isinstance(sign, str) or sign not in ["both", "positive"]:
1248 raise ValueError("Sign must be 'both' or 'positive'")
1249
1250 try:
1251 origin_array = (ctypes.c_float * 3)(origin.x, origin.y, origin.z)
1252 length_array = (ctypes.c_float * 3)(length.x, length.y, length.z)
1253 helios_lib.addCoordinateAxesCustom(self.visualizer, origin_array, length_array, sign.encode('utf-8'))
1254 except Exception as e:
1255 raise VisualizerError(f"Failed to add custom coordinate axes: {e}")
1256
1257 def disableCoordinateAxes(self) -> None:
1258 """
1259 Remove coordinate axes.
1260
1261 Raises:
1262 VisualizerError: If operation fails
1263 """
1264 if not self.visualizer:
1265 raise VisualizerError("Visualizer not initialized")
1266
1267 try:
1268 helios_lib.disableCoordinateAxes(self.visualizer)
1269 except Exception as e:
1270 raise VisualizerError(f"Failed to disable coordinate axes: {e}")
1271
1272 def addGridWireFrame(self, center: vec3, size: vec3, subdivisions: List[int]) -> None:
1273 """
1274 Add grid wireframe.
1276 Args:
1277 center: Grid center position
1278 size: Grid size in each direction
1279 subdivisions: Grid subdivisions [x, y, z]
1280
1281 Raises:
1282 ValueError: If parameters are invalid
1283 VisualizerError: If operation fails
1284 """
1285 if not self.visualizer:
1286 raise VisualizerError("Visualizer not initialized")
1287
1288 if not isinstance(center, vec3):
1289 raise ValueError("Center must be a vec3")
1290 if not isinstance(size, vec3):
1291 raise ValueError("Size must be a vec3")
1292 if not isinstance(subdivisions, (list, tuple)) or len(subdivisions) != 3:
1293 raise ValueError("Subdivisions must be a list of 3 integers")
1294 if not all(isinstance(s, _INT_TYPE) and s > 0 for s in subdivisions):
1295 raise ValueError("All subdivisions must be positive integers")
1297 try:
1298 center_array = (ctypes.c_float * 3)(center.x, center.y, center.z)
1299 size_array = (ctypes.c_float * 3)(size.x, size.y, size.z)
1300 subdiv_array = (ctypes.c_int * 3)(*subdivisions)
1301 helios_lib.addGridWireFrame(self.visualizer, center_array, size_array, subdiv_array)
1302 except Exception as e:
1303 raise VisualizerError(f"Failed to add grid wireframe: {e}")
1304
1305 # Colorbar Control Methods
1306
1307 def enableColorbar(self) -> None:
1308 """
1309 Enable colorbar.
1310
1311 Raises:
1312 VisualizerError: If operation fails
1313 """
1314 if not self.visualizer:
1315 raise VisualizerError("Visualizer not initialized")
1316
1317 try:
1318 helios_lib.enableColorbar(self.visualizer)
1319 except Exception as e:
1320 raise VisualizerError(f"Failed to enable colorbar: {e}")
1321
1322 def disableColorbar(self) -> None:
1323 """
1324 Disable colorbar.
1326 Raises:
1327 VisualizerError: If operation fails
1328 """
1329 if not self.visualizer:
1330 raise VisualizerError("Visualizer not initialized")
1331
1332 try:
1333 helios_lib.disableColorbar(self.visualizer)
1334 except Exception as e:
1335 raise VisualizerError(f"Failed to disable colorbar: {e}")
1336
1337 def setColorbarPosition(self, position: vec3) -> None:
1338 """
1339 Set colorbar position.
1341 Args:
1342 position: Colorbar position
1343
1344 Raises:
1345 ValueError: If position is invalid
1346 VisualizerError: If operation fails
1347 """
1348 if not self.visualizer:
1349 raise VisualizerError("Visualizer not initialized")
1350
1351 if not isinstance(position, vec3):
1352 raise ValueError("Position must be a vec3")
1353
1354 try:
1355 pos_array = (ctypes.c_float * 3)(position.x, position.y, position.z)
1356 helios_lib.setColorbarPosition(self.visualizer, pos_array)
1357 except Exception as e:
1358 raise VisualizerError(f"Failed to set colorbar position: {e}")
1360 def setColorbarSize(self, width: float, height: float) -> None:
1361 """
1362 Set colorbar size.
1363
1364 Args:
1365 width: Colorbar width
1366 height: Colorbar height
1367
1368 Raises:
1369 ValueError: If size is invalid
1370 VisualizerError: If operation fails
1371 """
1372 if not self.visualizer:
1373 raise VisualizerError("Visualizer not initialized")
1374
1375 if not isinstance(width, _NUMERIC_TYPES) or width <= 0:
1376 raise ValueError("Width must be a positive number")
1377 if not isinstance(height, _NUMERIC_TYPES) or height <= 0:
1378 raise ValueError("Height must be a positive number")
1379
1380 try:
1381 size_array = (ctypes.c_float * 2)(float(width), float(height))
1382 helios_lib.setColorbarSize(self.visualizer, size_array)
1383 except Exception as e:
1384 raise VisualizerError(f"Failed to set colorbar size: {e}")
1385
1386 def setColorbarRange(self, min_val: float, max_val: float) -> None:
1387 """
1388 Set colorbar range.
1389
1390 Args:
1391 min_val: Minimum value
1392 max_val: Maximum value
1393
1394 Raises:
1395 ValueError: If range is invalid
1396 VisualizerError: If operation fails
1397 """
1398 if not self.visualizer:
1399 raise VisualizerError("Visualizer not initialized")
1400
1401 if not isinstance(min_val, _NUMERIC_TYPES):
1402 raise ValueError("Minimum value must be numeric")
1403 if not isinstance(max_val, _NUMERIC_TYPES):
1404 raise ValueError("Maximum value must be numeric")
1405 if min_val >= max_val:
1406 raise ValueError("Minimum value must be less than maximum value")
1407
1408 try:
1409 helios_lib.setColorbarRange(self.visualizer, float(min_val), float(max_val))
1410 except Exception as e:
1411 raise VisualizerError(f"Failed to set colorbar range: {e}")
1412
1413 def setColorbarTicks(self, ticks: List[float]) -> None:
1414 """
1415 Set colorbar tick marks.
1416
1417 Args:
1418 ticks: List of tick values
1419
1420 Raises:
1421 ValueError: If ticks are invalid
1422 VisualizerError: If operation fails
1423 """
1424 if not self.visualizer:
1425 raise VisualizerError("Visualizer not initialized")
1426
1427 if not isinstance(ticks, (list, tuple)):
1428 raise ValueError("Ticks must be a list or tuple")
1429 if not all(isinstance(t, _NUMERIC_TYPES) for t in ticks):
1430 raise ValueError("All tick values must be numeric")
1431
1432 try:
1433 if ticks:
1434 ticks_array = (ctypes.c_float * len(ticks))(*ticks)
1435 helios_lib.setColorbarTicks(self.visualizer, ticks_array, len(ticks))
1436 else:
1437 helios_lib.setColorbarTicks(self.visualizer, None, 0)
1438 except Exception as e:
1439 raise VisualizerError(f"Failed to set colorbar ticks: {e}")
1440
1441 def setColorbarTitle(self, title: str) -> None:
1442 """
1443 Set colorbar title.
1444
1445 Args:
1446 title: Colorbar title
1447
1448 Raises:
1449 ValueError: If title is invalid
1450 VisualizerError: If operation fails
1451 """
1452 if not self.visualizer:
1453 raise VisualizerError("Visualizer not initialized")
1454
1455 if not isinstance(title, str):
1456 raise ValueError("Title must be a string")
1457
1458 try:
1459 helios_lib.setColorbarTitle(self.visualizer, title.encode('utf-8'))
1460 except Exception as e:
1461 raise VisualizerError(f"Failed to set colorbar title: {e}")
1462
1463 def setColorbarFontColor(self, color: RGBcolor) -> None:
1464 """
1465 Set colorbar font color.
1466
1467 Args:
1468 color: Font color
1469
1470 Raises:
1471 ValueError: If color is invalid
1472 VisualizerError: If operation fails
1473 """
1474 if not self.visualizer:
1475 raise VisualizerError("Visualizer not initialized")
1476
1477 if not isinstance(color, RGBcolor):
1478 raise ValueError("Color must be an RGBcolor")
1479
1480 try:
1481 color_array = (ctypes.c_float * 3)(color.r, color.g, color.b)
1482 helios_lib.setColorbarFontColor(self.visualizer, color_array)
1483 except Exception as e:
1484 raise VisualizerError(f"Failed to set colorbar font color: {e}")
1486 def setColorbarFontSize(self, font_size: int) -> None:
1487 """
1488 Set colorbar font size.
1489
1490 Args:
1491 font_size: Font size
1492
1493 Raises:
1494 ValueError: If font size is invalid
1495 VisualizerError: If operation fails
1496 """
1497 if not self.visualizer:
1498 raise VisualizerError("Visualizer not initialized")
1499
1500 if not isinstance(font_size, _INT_TYPE) or font_size <= 0:
1501 raise ValueError("Font size must be a positive integer")
1502
1503 try:
1504 helios_lib.setColorbarFontSize(self.visualizer, font_size)
1505 except Exception as e:
1506 raise VisualizerError(f"Failed to set colorbar font size: {e}")
1507
1508 # Colormap Methods
1509
1510 def setColormap(self, colormap: Union[int, str]) -> None:
1511 """
1512 Set predefined colormap.
1513
1514 Args:
1515 colormap: Colormap ID (0-5) or name ("HOT", "COOL", "RAINBOW", "LAVA", "PARULA", "GRAY")
1516
1517 Raises:
1518 ValueError: If colormap is invalid
1519 VisualizerError: If operation fails
1520 """
1521 if not self.visualizer:
1522 raise VisualizerError("Visualizer not initialized")
1523
1524 colormap_map = {
1525 "HOT": 0, "COOL": 1, "RAINBOW": 2,
1526 "LAVA": 3, "PARULA": 4, "GRAY": 5
1527 }
1528
1529 if isinstance(colormap, str):
1530 if colormap.upper() not in colormap_map:
1531 raise ValueError(f"Unknown colormap name: {colormap}")
1532 colormap_id = colormap_map[colormap.upper()]
1533 elif isinstance(colormap, _INT_TYPE):
1534 if colormap < 0 or colormap > 5:
1535 raise ValueError("Colormap ID must be 0-5")
1536 colormap_id = colormap
1537 else:
1538 raise ValueError("Colormap must be integer ID or string name")
1539
1540 try:
1541 helios_lib.setColormap(self.visualizer, colormap_id)
1542 except Exception as e:
1543 raise VisualizerError(f"Failed to set colormap: {e}")
1544
1545 def setCustomColormap(self, colors: List[RGBcolor], divisions: List[float]) -> None:
1546 """
1547 Set custom colormap.
1548
1549 Args:
1550 colors: List of RGB colors
1551 divisions: List of division points (same length as colors)
1552
1553 Raises:
1554 ValueError: If parameters are invalid
1555 VisualizerError: If operation fails
1556 """
1557 if not self.visualizer:
1558 raise VisualizerError("Visualizer not initialized")
1559
1560 if not isinstance(colors, (list, tuple)) or not colors:
1561 raise ValueError("Colors must be a non-empty list")
1562 if not isinstance(divisions, (list, tuple)) or not divisions:
1563 raise ValueError("Divisions must be a non-empty list")
1564 if len(colors) != len(divisions):
1565 raise ValueError("Colors and divisions must have the same length")
1566
1567 if not all(isinstance(c, RGBcolor) for c in colors):
1568 raise ValueError("All colors must be RGBcolor objects")
1569 if not all(isinstance(d, _NUMERIC_TYPES) for d in divisions):
1570 raise ValueError("All divisions must be numeric")
1571
1572 try:
1573 # Flatten colors to RGB array
1574 color_array = (ctypes.c_float * (len(colors) * 3))()
1575 for i, color in enumerate(colors):
1576 color_array[i*3] = color.r
1577 color_array[i*3+1] = color.g
1578 color_array[i*3+2] = color.b
1579
1580 divisions_array = (ctypes.c_float * len(divisions))(*divisions)
1581
1582 helios_lib.setCustomColormap(self.visualizer, color_array, divisions_array, len(colors))
1583 except Exception as e:
1584 raise VisualizerError(f"Failed to set custom colormap: {e}")
1585
1586 # Advanced Coloring Methods
1587
1588 def colorContextPrimitivesByObjectData(self, data_name: str, obj_ids: Optional[List[int]] = None) -> None:
1589 """
1590 Color context primitives by object data.
1591
1592 Args:
1593 data_name: Name of object data to use for coloring
1594 obj_ids: Optional list of object IDs to color (None for all)
1595
1596 Raises:
1597 ValueError: If parameters are invalid
1598 VisualizerError: If operation fails
1599 """
1600 if not self.visualizer:
1601 raise VisualizerError("Visualizer not initialized")
1602
1603 if not isinstance(data_name, str) or not data_name.strip():
1604 raise ValueError("Data name must be a non-empty string")
1605
1606 try:
1607 if obj_ids is None:
1608 helios_lib.colorContextPrimitivesByObjectData(self.visualizer, data_name.encode('utf-8'))
1609 else:
1610 if not isinstance(obj_ids, (list, tuple)):
1611 raise ValueError("Object IDs must be a list or tuple")
1612 if not all(isinstance(oid, _INT_TYPE) and oid >= 0 for oid in obj_ids):
1613 raise ValueError("All object IDs must be non-negative integers")
1614
1615 if obj_ids:
1616 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
1617 helios_lib.colorContextPrimitivesByObjectDataIDs(self.visualizer, data_name.encode('utf-8'), obj_ids_array, len(obj_ids))
1618 else:
1619 helios_lib.colorContextPrimitivesByObjectDataIDs(self.visualizer, data_name.encode('utf-8'), None, 0)
1620 except Exception as e:
1621 raise VisualizerError(f"Failed to color primitives by object data '{data_name}': {e}")
1622
1623 def colorContextPrimitivesRandomly(self, uuids: Optional[List[int]] = None) -> None:
1624 """
1625 Color context primitives randomly.
1626
1627 Args:
1628 uuids: Optional list of primitive UUIDs to color (None for all)
1629
1630 Raises:
1631 ValueError: If UUIDs are invalid
1632 VisualizerError: If operation fails
1633 """
1634 if not self.visualizer:
1635 raise VisualizerError("Visualizer not initialized")
1636
1637 try:
1638 if uuids is None:
1639 helios_lib.colorContextPrimitivesRandomly(self.visualizer, None, 0)
1640 else:
1641 if not isinstance(uuids, (list, tuple)):
1642 raise ValueError("UUIDs must be a list or tuple")
1643 if not all(isinstance(uuid, _INT_TYPE) and uuid >= 0 for uuid in uuids):
1644 raise ValueError("All UUIDs must be non-negative integers")
1646 if uuids:
1647 uuid_array = (ctypes.c_uint * len(uuids))(*uuids)
1648 helios_lib.colorContextPrimitivesRandomly(self.visualizer, uuid_array, len(uuids))
1649 else:
1650 helios_lib.colorContextPrimitivesRandomly(self.visualizer, None, 0)
1651 except Exception as e:
1652 raise VisualizerError(f"Failed to color primitives randomly: {e}")
1653
1654 def colorContextObjectsRandomly(self, obj_ids: Optional[List[int]] = None) -> None:
1655 """
1656 Color context objects randomly.
1657
1658 Args:
1659 obj_ids: Optional list of object IDs to color (None for all)
1660
1661 Raises:
1662 ValueError: If object IDs are invalid
1663 VisualizerError: If operation fails
1664 """
1665 if not self.visualizer:
1666 raise VisualizerError("Visualizer not initialized")
1667
1668 try:
1669 if obj_ids is None:
1670 helios_lib.colorContextObjectsRandomly(self.visualizer, None, 0)
1671 else:
1672 if not isinstance(obj_ids, (list, tuple)):
1673 raise ValueError("Object IDs must be a list or tuple")
1674 if not all(isinstance(oid, _INT_TYPE) and oid >= 0 for oid in obj_ids):
1675 raise ValueError("All object IDs must be non-negative integers")
1677 if obj_ids:
1678 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
1679 helios_lib.colorContextObjectsRandomly(self.visualizer, obj_ids_array, len(obj_ids))
1680 else:
1681 helios_lib.colorContextObjectsRandomly(self.visualizer, None, 0)
1682 except Exception as e:
1683 raise VisualizerError(f"Failed to color objects randomly: {e}")
1684
1685 def clearColor(self) -> None:
1686 """
1687 Clear primitive colors from previous coloring operations.
1688
1689 Raises:
1690 VisualizerError: If operation fails
1691 """
1692 if not self.visualizer:
1693 raise VisualizerError("Visualizer not initialized")
1694
1695 try:
1696 helios_lib.clearColor(self.visualizer)
1697 except Exception as e:
1698 raise VisualizerError(f"Failed to clear colors: {e}")
1699
1700 # Watermark Control Methods
1701
1702 def hideWatermark(self) -> None:
1703 """
1704 Hide Helios logo watermark.
1705
1706 Raises:
1707 VisualizerError: If operation fails
1708 """
1709 if not self.visualizer:
1710 raise VisualizerError("Visualizer not initialized")
1711
1712 try:
1713 helios_lib.hideWatermark(self.visualizer)
1714 except Exception as e:
1715 raise VisualizerError(f"Failed to hide watermark: {e}")
1716
1717 def showWatermark(self) -> None:
1718 """
1719 Show Helios logo watermark.
1721 Raises:
1722 VisualizerError: If operation fails
1723 """
1724 if not self.visualizer:
1725 raise VisualizerError("Visualizer not initialized")
1726
1727 try:
1728 helios_lib.showWatermark(self.visualizer)
1729 except Exception as e:
1730 raise VisualizerError(f"Failed to show watermark: {e}")
1731
1732 def updateWatermark(self) -> None:
1733 """
1734 Update watermark geometry to match current window size.
1736 Raises:
1737 VisualizerError: If operation fails
1738 """
1739 if not self.visualizer:
1740 raise VisualizerError("Visualizer not initialized")
1741
1742 try:
1743 helios_lib.updateWatermark(self.visualizer)
1744 except Exception as e:
1745 raise VisualizerError(f"Failed to update watermark: {e}")
1746
1747 # Navigation Gizmo Methods (v1.3.53+)
1748
1749 def hideNavigationGizmo(self) -> None:
1750 """
1751 Hide navigation gizmo (coordinate axes indicator in corner).
1752
1753 The navigation gizmo shows XYZ axes orientation and can be clicked
1754 to snap the camera to standard views (top, front, side, etc.).
1755
1756 Raises:
1757 VisualizerError: If operation fails
1758 """
1759 if not self.visualizer:
1760 raise VisualizerError("Visualizer not initialized")
1761
1762 try:
1763 visualizer_wrapper.hide_navigation_gizmo(self.visualizer)
1764 logger.debug("Navigation gizmo hidden")
1765 except Exception as e:
1766 raise VisualizerError(f"Failed to hide navigation gizmo: {e}")
1767
1768 def showNavigationGizmo(self) -> None:
1769 """
1770 Show navigation gizmo (coordinate axes indicator in corner).
1771
1772 The navigation gizmo shows XYZ axes orientation and can be clicked
1773 to snap the camera to standard views (top, front, side, etc.).
1774
1775 Note: Navigation gizmo is shown by default in v1.3.53+.
1776
1777 Raises:
1778 VisualizerError: If operation fails
1779 """
1780 if not self.visualizer:
1781 raise VisualizerError("Visualizer not initialized")
1782
1783 try:
1784 visualizer_wrapper.show_navigation_gizmo(self.visualizer)
1785 logger.debug("Navigation gizmo shown")
1786 except Exception as e:
1787 raise VisualizerError(f"Failed to show navigation gizmo: {e}")
1788
1789 # Performance and Utility Methods
1790
1791 def enableMessages(self) -> None:
1792 """
1793 Enable standard output from visualizer plugin.
1794
1795 Raises:
1796 VisualizerError: If operation fails
1797 """
1798 if not self.visualizer:
1799 raise VisualizerError("Visualizer not initialized")
1800
1801 try:
1802 helios_lib.enableMessages(self.visualizer)
1803 except Exception as e:
1804 raise VisualizerError(f"Failed to enable messages: {e}")
1805
1806 def disableMessages(self) -> None:
1807 """
1808 Disable standard output from visualizer plugin.
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.disableMessages(self.visualizer)
1818 except Exception as e:
1819 raise VisualizerError(f"Failed to disable messages: {e}")
1820
1821 def plotOnce(self, get_keystrokes: bool = True) -> None:
1822 """
1823 Run one rendering loop.
1825 Args:
1826 get_keystrokes: Whether to process keystrokes
1827
1828 Raises:
1829 VisualizerError: If operation fails
1830 """
1831 if not self.visualizer:
1832 raise VisualizerError("Visualizer not initialized")
1833
1834 try:
1835 helios_lib.plotOnce(self.visualizer, get_keystrokes)
1836 except Exception as e:
1837 raise VisualizerError(f"Failed to run plot once: {e}")
1838
1839 def plotUpdateWithVisibility(self, hide_window: bool = False) -> None:
1840 """
1841 Update visualization with window visibility control.
1843 Args:
1844 hide_window: Whether to hide the window during update
1845
1846 Raises:
1847 VisualizerError: If operation fails
1848 """
1850 if not self.visualizer:
1851 raise VisualizerError("Visualizer not initialized")
1852
1853 try:
1855 helios_lib.plotUpdateWithVisibility(self.visualizer, hide_window)
1856 except Exception as e:
1857 raise VisualizerError(f"Failed to update plot with visibility control: {e}")
1858
1859 # Point Culling and LOD Methods (v1.3.54+)
1861 def setPointCullingEnabled(self, enabled: bool) -> None:
1862 """
1863 Enable or disable point cloud culling optimization.
1864
1865 Point culling improves rendering performance for large point clouds by
1866 selectively rendering only points that are visible based on distance
1867 and density criteria.
1868
1869 Args:
1870 enabled: True to enable culling, False to disable (default: True)
1871
1872 Raises:
1873 ValueError: If enabled is not a boolean
1874 VisualizerError: If operation fails
1875
1876 Example:
1877 >>> with Visualizer(800, 600) as vis:
1878 ... vis.setPointCullingEnabled(False) # Disable for highest quality
1879 ... vis.setPointCullingEnabled(True) # Enable for better performance
1880 """
1881 if not self.visualizer:
1882 raise VisualizerError("Visualizer not initialized")
1883 if not isinstance(enabled, bool):
1884 raise ValueError(f"Enabled must be a boolean, got {type(enabled).__name__}")
1885
1886 try:
1887 visualizer_wrapper.set_point_culling_enabled(self.visualizer, enabled)
1888 logger.debug(f"Point culling {'enabled' if enabled else 'disabled'}")
1889 except Exception as e:
1890 raise VisualizerError(f"Failed to set point culling enabled: {e}")
1891
1892 def setPointCullingThreshold(self, threshold: int) -> None:
1893 """
1894 Set the minimum number of points required to trigger culling.
1895
1896 Culling is only activated when the total point count exceeds this threshold.
1897 This prevents unnecessary culling overhead for small point clouds.
1898
1899 Args:
1900 threshold: Point count threshold (default: 10000). Set to 0 to always enable.
1901
1902 Raises:
1903 ValueError: If threshold is not a non-negative integer
1904 VisualizerError: If operation fails
1905
1906 Example:
1907 >>> vis.setPointCullingThreshold(50000) # Only cull for >50k points
1908 >>> vis.setPointCullingThreshold(0) # Always enable culling
1909 """
1910 if not self.visualizer:
1911 raise VisualizerError("Visualizer not initialized")
1912 if not isinstance(threshold, int):
1913 raise ValueError(f"Threshold must be an integer, got {type(threshold).__name__}")
1914 if threshold < 0:
1915 raise ValueError("Point culling threshold must be non-negative")
1916
1917 try:
1918 visualizer_wrapper.set_point_culling_threshold(self.visualizer, threshold)
1919 logger.debug(f"Point culling threshold set to {threshold}")
1920 except Exception as e:
1921 raise VisualizerError(f"Failed to set point culling threshold: {e}")
1922
1923 def setPointMaxRenderDistance(self, distance: float) -> None:
1924 """
1925 Set the maximum rendering distance for points.
1926
1927 Points beyond this distance from the camera are not rendered, improving
1928 performance for large scenes. The distance is measured in world units.
1929
1930 Args:
1931 distance: Maximum distance in world units. Use 0 for auto mode (scene_size * 5.0)
1932
1933 Raises:
1934 ValueError: If distance is negative
1935 VisualizerError: If operation fails
1936
1937 Example:
1938 >>> vis.setPointMaxRenderDistance(0.0) # Auto mode
1939 >>> vis.setPointMaxRenderDistance(100.0) # Fixed distance
1940
1941 Note:
1942 Setting distance to 0 enables automatic mode, which calculates the
1943 render distance based on the scene bounding box dimensions.
1944 """
1945 if not self.visualizer:
1946 raise VisualizerError("Visualizer not initialized")
1947 if not isinstance(distance, (int, float)):
1948 raise ValueError(f"Distance must be numeric, got {type(distance).__name__}")
1949 if distance < 0.0:
1950 raise ValueError("Point max render distance cannot be negative")
1951
1952 try:
1953 visualizer_wrapper.set_point_max_render_distance(self.visualizer, float(distance))
1954 if distance == 0.0:
1955 logger.debug("Point max render distance set to auto mode")
1956 else:
1957 logger.debug(f"Point max render distance set to {distance}")
1958 except Exception as e:
1959 raise VisualizerError(f"Failed to set point max render distance: {e}")
1960
1961 def setPointLODFactor(self, factor: float) -> None:
1962 """
1963 Set the level-of-detail factor for distance-based culling.
1964
1965 Controls how aggressively points are culled based on distance from camera.
1966 Higher values result in more aggressive culling (better performance, lower quality).
1967 Lower values preserve more points (higher quality, lower performance).
1968
1969 Args:
1970 factor: LOD factor (default: 10.0, typical range: 1.0-50.0). Must be positive.
1971
1972 Raises:
1973 ValueError: If factor is not positive
1974 VisualizerError: If operation fails
1975
1976 Example:
1977 >>> vis.setPointLODFactor(5.0) # Conservative culling
1978 >>> vis.setPointLODFactor(10.0) # Default culling
1979 >>> vis.setPointLODFactor(25.0) # Aggressive culling
1980
1981 Note:
1982 The LOD factor determines the rate at which point density decreases
1983 with distance. Higher factors mean points are culled more quickly
1984 as distance increases.
1985 """
1986 if not self.visualizer:
1987 raise VisualizerError("Visualizer not initialized")
1988 if not isinstance(factor, (int, float)):
1989 raise ValueError(f"LOD factor must be numeric, got {type(factor).__name__}")
1990 if factor <= 0.0:
1991 raise ValueError("Point LOD factor must be positive")
1992
1993 # Warn about extreme values
1994 if factor < 1.0:
1995 logger.warning(f"Point LOD factor {factor} is very low (< 1.0), may cause performance issues")
1996 elif factor > 100.0:
1997 logger.warning(f"Point LOD factor {factor} is very high (> 100.0), may over-cull points")
1998
1999 try:
2000 visualizer_wrapper.set_point_lod_factor(self.visualizer, float(factor))
2001 logger.debug(f"Point LOD factor set to {factor}")
2002 except Exception as e:
2003 raise VisualizerError(f"Failed to set point LOD factor: {e}")
2004
2005 def getPointRenderingMetrics(self) -> dict:
2006 """
2007 Get point cloud rendering performance metrics.
2008
2009 Provides detailed statistics about point cloud culling and rendering
2010 performance, useful for optimizing visualization settings.
2011
2012 Returns:
2013 Dictionary with keys:
2014 - 'total_points' (int): Total number of points in the scene
2015 - 'rendered_points' (int): Number of points actually rendered after culling
2016 - 'culling_time_ms' (float): Time spent on culling in milliseconds
2017
2018 Raises:
2019 VisualizerError: If operation fails
2020
2021 Example:
2022 >>> metrics = vis.getPointRenderingMetrics()
2023 >>> print(f"Total: {metrics['total_points']}")
2024 >>> print(f"Rendered: {metrics['rendered_points']}")
2025 >>> cull_rate = (1 - metrics['rendered_points']/metrics['total_points']) * 100
2026 >>> print(f"Culling rate: {cull_rate:.1f}%")
2027
2028 Note:
2029 Metrics are only meaningful after calling plotUpdate() or plotInteractive().
2030 The culling_time_ms represents CPU time spent on culling calculations,
2031 not total frame time.
2032 """
2033 if not self.visualizer:
2034 raise VisualizerError("Visualizer not initialized")
2035
2036 try:
2037 metrics = visualizer_wrapper.get_point_rendering_metrics(self.visualizer)
2038 logger.debug(
2039 f"Point rendering metrics: {metrics['total_points']} total, "
2040 f"{metrics['rendered_points']} rendered, "
2041 f"{metrics['culling_time_ms']:.2f} ms culling time"
2042 )
2043 return metrics
2044 except Exception as e:
2045 raise VisualizerError(f"Failed to get point rendering metrics: {e}")
2046
2047 def __del__(self):
2048 """Destructor to ensure proper cleanup."""
2049 if hasattr(self, 'visualizer') and self.visualizer is not None:
2050 try:
2052 visualizer_wrapper.destroy_visualizer(self.visualizer)
2053 except Exception:
2054 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.
None setPointCullingEnabled(self, bool enabled)
Enable or disable point cloud culling optimization.
__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.
None setPointMaxRenderDistance(self, float distance)
Set the maximum rendering distance for points.
__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 setPointLODFactor(self, float factor)
Set the level-of-detail factor for distance-based culling.
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 setPointCullingThreshold(self, int threshold)
Set the minimum number of points required to trigger culling.
None updateContextPrimitiveColors(self)
Update context primitive colors.
_check_context_alive(self)
Raise if a Context was loaded and has since been destroyed.
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.
dict getPointRenderingMetrics(self)
Get point cloud rendering performance metrics.
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