141 def __init__(self, width: int, height: int, antialiasing_samples: int = 4, headless: bool =
False):
143 Initialize Visualizer with graceful plugin handling.
146 width: Window width in pixels
147 height: Window height in pixels
148 antialiasing_samples: Number of antialiasing samples (default: 4). Pass 0 to
149 disable antialiasing, which is required for exact color reproduction --
150 see :meth:`enableExactColorMode`.
151 headless: Enable headless mode for offscreen rendering (default: False)
154 VisualizerError: If visualizer plugin is not available
155 ValueError: If parameters are invalid
158 if not isinstance(width, _INT_TYPE):
159 raise ValueError(f
"Width must be an integer, got {type(width).__name__}")
160 if not isinstance(height, _INT_TYPE):
161 raise ValueError(f
"Height must be an integer, got {type(height).__name__}")
162 if not isinstance(antialiasing_samples, _INT_TYPE):
163 raise ValueError(f
"Antialiasing samples must be an integer, got {type(antialiasing_samples).__name__}")
164 if not isinstance(headless, bool):
165 raise ValueError(f
"Headless must be a boolean, got {type(headless).__name__}")
168 if width <= 0
or height <= 0:
169 raise ValueError(
"Width and height must be positive integers")
170 if antialiasing_samples < 0:
172 f
"Antialiasing samples must be non-negative, got {antialiasing_samples}. "
173 "Pass 0 to disable antialiasing.")
182 registry = get_plugin_registry()
184 if not registry.is_plugin_available(
'visualizer'):
186 available_plugins = registry.get_available_plugins()
189 "Visualizer requires the 'visualizer' plugin which is not available.\n\n"
190 "The visualizer plugin provides OpenGL-based 3D rendering and visualization.\n"
191 "System requirements:\n"
192 "- OpenGL 3.3 or higher\n"
193 "- GLFW library for window management\n"
194 "- FreeType library for text rendering\n"
195 "- Display/graphics drivers (X11 on Linux, native on Windows/macOS)\n\n"
196 "To enable visualization:\n"
197 "1. Build PyHelios with visualizer plugin:\n"
198 " build_scripts/build_helios --plugins visualizer\n"
199 f
"\nCurrently available plugins: {available_plugins}"
204 system = platform.system().lower()
205 if 'linux' in system:
207 "\n\nLinux installation hints:\n"
208 "- Ubuntu/Debian: sudo apt-get install libx11-dev xorg-dev libgl1-mesa-dev libglu1-mesa-dev\n"
209 "- CentOS/RHEL: sudo yum install libX11-devel mesa-libGL-devel mesa-libGLU-devel"
211 elif 'darwin' in system:
213 "\n\nmacOS installation hints:\n"
214 "- Install XQuartz: brew install --cask xquartz\n"
215 "- OpenGL should be available by default"
217 elif 'windows' in system:
219 "\n\nWindows installation hints:\n"
220 "- OpenGL drivers should be provided by graphics card drivers\n"
221 "- Visual Studio runtime may be required"
233 self.
visualizer = visualizer_wrapper.create_visualizer_with_antialiasing(
234 width, height, antialiasing_samples, headless
240 "Failed to create Visualizer instance. "
241 "This may indicate a problem with graphics drivers or OpenGL initialization."
243 logger.info(f
"Visualizer created successfully ({width}x{height}, AA:{antialiasing_samples}, headless:{headless})")
245 except Exception
as e:
249 """Raise if a Context was loaded and has since been destroyed."""
250 if getattr(self,
"_context",
None)
is not None:
251 check_context_alive(self.
_context,
"Visualizer")
254 """Context manager entry."""
257 def __exit__(self, exc_type, exc_value, traceback):
258 """Context manager exit with proper cleanup."""
262 visualizer_wrapper.destroy_visualizer(self.
visualizer)
263 logger.debug(
"Visualizer destroyed successfully")
264 except Exception
as e:
265 logger.warning(f
"Error destroying Visualizer: {e}")
269 @validate_build_geometry_params
272 Build Context geometry in the visualizer.
274 This method loads geometry from a Helios Context into the visualizer
275 for rendering. If no UUIDs are specified, all geometry is loaded.
278 context: Helios Context instance containing geometry
279 uuids: Optional list of primitive UUIDs to visualize (default: all)
282 VisualizerError: If geometry building fails
283 ValueError: If parameters are invalid
287 if not isinstance(context, Context):
288 raise ValueError(
"context must be a Context instance")
300 visualizer_wrapper.build_context_geometry(self.
visualizer, context.getNativePtr())
301 logger.debug(
"Built all Context geometry in visualizer")
305 raise ValueError(
"UUIDs list cannot be empty")
306 visualizer_wrapper.build_context_geometry_uuids(
307 self.
visualizer, context.getNativePtr(), uuids
309 logger.debug(f
"Built {len(uuids)} primitives in visualizer")
311 except Exception
as e:
316 Open interactive visualization window.
318 This method opens a window with the current scene and allows user
319 interaction (camera rotation, zooming, etc.). The program will pause
320 until the window is closed by the user.
322 Interactive controls:
323 - Mouse scroll: Zoom in/out
324 - Left mouse + drag: Rotate camera
325 - Right mouse + drag: Pan camera
326 - Arrow keys: Camera movement
327 - +/- keys: Zoom in/out
330 VisualizerError: If visualization fails, or if the Visualizer was
331 constructed with ``headless=True`` (helios-core 1.3.85+ raises rather
332 than driving a window that was never shown or does not exist)
340 visualizer_wrapper.plot_interactive(self.
visualizer)
341 logger.debug(
"Interactive visualization completed")
342 except Exception
as e:
347 Update visualization (non-interactive).
349 This method updates the visualization window without user interaction.
350 The program continues immediately after rendering. Useful for batch
351 processing or creating image sequences.
353 In headless mode, automatically hides the window to prevent graphics driver crashes on some platforms.
356 VisualizerError: If visualization update fails
366 logger.debug(
"Visualization updated")
367 except Exception
as e:
370 @validate_print_window_params
371 def printWindow(self, filename: str, image_format: Optional[str] =
None) ->
None:
373 Save current visualization to image file.
375 This method exports the current visualization to an image file.
376 Starting from v1.3.53, supports both JPEG and PNG formats.
378 The frame is rendered as part of the capture (helios-core 1.3.85+), so it is
379 not necessary to call :meth:`plotUpdate` beforehand: handing the Visualizer a
380 Context with :meth:`buildContextGeometry` and calling this straight away
381 produces a correct image. A render is performed whenever the frame currently
382 on the GPU is not the one that would be captured (geometry changed, the camera
383 moved, or the navigation gizmo had to be hidden) and skipped when it would
384 produce an identical frame, so calling :meth:`plotUpdate` first is harmless
385 but redundant. The navigation gizmo is always absent from the captured image.
388 filename: Output filename for image
389 Can be absolute or relative to user's current working directory
390 Extension (.jpg, .png) is recommended but not required
391 image_format: Image format - "jpeg" or "png" (v1.3.53+).
392 If None, automatically detects from filename extension.
393 Defaults to "jpeg" if not detectable from extension.
396 VisualizerError: If image saving fails
397 ValueError: If filename or format is invalid
400 PNG format is required to preserve transparent backgrounds when using
401 setBackgroundTransparent(). JPEG format will render transparent areas as black.
404 >>> visualizer.printWindow("output.jpg") # Auto-detects JPEG
405 >>> visualizer.printWindow("output.png") # Auto-detects PNG
406 >>> visualizer.printWindow("output.img", image_format="png") # Explicit PNG
412 raise ValueError(
"Filename cannot be empty")
418 if image_format
is None:
419 if resolved_filename.lower().endswith(
'.png'):
421 elif resolved_filename.lower().endswith((
'.jpg',
'.jpeg')):
422 image_format =
'jpeg'
425 image_format =
'jpeg'
426 logger.debug(f
"No format specified and extension not recognized, defaulting to JPEG")
429 if image_format.lower()
not in [
'jpeg',
'png']:
430 raise ValueError(f
"Image format must be 'jpeg' or 'png', got '{image_format}'")
436 visualizer_wrapper.print_window_with_format(
441 logger.debug(f
"Visualization saved to {resolved_filename} ({image_format.upper()} format)")
442 except (AttributeError, NotImplementedError):
444 if image_format.lower() !=
'jpeg':
446 "PNG format requested but not available in current Helios version. "
447 "Falling back to JPEG format. Update to Helios v1.3.53+ for PNG support."
449 visualizer_wrapper.print_window(self.
visualizer, resolved_filename)
450 logger.debug(f
"Visualization saved to {resolved_filename} (JPEG format - legacy mode)")
451 except Exception
as e:
456 Close visualization window.
458 This method closes any open visualization window. It's safe to call
459 even if no window is open.
462 VisualizerError: If window closing fails
468 visualizer_wrapper.close_window(self.
visualizer)
469 logger.debug(
"Visualization window closed")
470 except Exception
as e:
475 Set camera position using Cartesian coordinates.
478 position: Camera position as vec3 in world coordinates
479 lookAt: Camera look-at point as vec3 in world coordinates
482 VisualizerError: If camera positioning fails
483 ValueError: If parameters are invalid
489 if not isinstance(position, vec3):
490 raise ValueError(f
"Position must be a vec3, got {type(position).__name__}")
491 if not isinstance(lookAt, vec3):
492 raise ValueError(f
"LookAt must be a vec3, got {type(lookAt).__name__}")
495 visualizer_wrapper.set_camera_position(self.
visualizer, position, lookAt)
496 logger.debug(f
"Camera position set to ({position.x}, {position.y}, {position.z}), looking at ({lookAt.x}, {lookAt.y}, {lookAt.z})")
497 except Exception
as e:
502 Set camera position using spherical coordinates.
505 angle: Camera position as SphericalCoord (radius, elevation, azimuth)
506 lookAt: Camera look-at point as vec3 in world coordinates
509 VisualizerError: If camera positioning fails
510 ValueError: If parameters are invalid
516 if not isinstance(angle, SphericalCoord):
517 raise ValueError(f
"Angle must be a SphericalCoord, got {type(angle).__name__}")
518 if not isinstance(lookAt, vec3):
519 raise ValueError(f
"LookAt must be a vec3, got {type(lookAt).__name__}")
522 visualizer_wrapper.set_camera_position_spherical(self.
visualizer, angle, lookAt)
523 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})")
524 except Exception
as e:
525 raise VisualizerError(f
"Failed to set camera position (spherical): {e}")
529 Set background color.
532 color: Background color as RGBcolor with values in range [0, 1]
535 VisualizerError: If color setting fails
536 ValueError: If color values are invalid
542 if not isinstance(color, RGBcolor):
543 raise ValueError(f
"Color must be an RGBcolor, got {type(color).__name__}")
546 if not (0 <= color.r <= 1
and 0 <= color.g <= 1
and 0 <= color.b <= 1):
547 raise ValueError(f
"Color components ({color.r}, {color.g}, {color.b}) must be in range [0, 1]")
550 visualizer_wrapper.set_background_color(self.
visualizer, color)
551 logger.debug(f
"Background color set to ({color.r}, {color.g}, {color.b})")
552 except Exception
as e:
557 Enable transparent background mode (v1.3.53+).
559 Sets the background to transparent with checkerboard pattern display.
560 Requires PNG output format to preserve transparency.
562 Note: When using transparent background, use printWindow() with PNG
563 format to save transparent images.
566 VisualizerError: If transparent background setting fails
573 visualizer_wrapper.set_background_transparent(self.
visualizer)
574 logger.debug(
"Background set to transparent mode")
575 except Exception
as e:
580 Set custom background image texture (v1.3.53+).
583 texture_file: Path to background image file
584 Can be absolute or relative to working directory
587 VisualizerError: If background image setting fails
588 ValueError: If texture file path is invalid
593 if not texture_file
or not isinstance(texture_file, str):
594 raise ValueError(
"Texture file path must be a non-empty string")
600 visualizer_wrapper.set_background_image(self.
visualizer, resolved_path)
601 logger.debug(f
"Background image set to {resolved_path}")
602 except Exception
as e:
607 Set sky sphere texture background with automatic scaling (v1.3.53+).
609 Creates a sky sphere that automatically scales with the scene.
610 Replaces the deprecated addSkyDomeByCenter() method.
613 texture_file: Path to spherical/equirectangular texture image
614 If None, uses default gradient sky texture
615 divisions: Number of sphere tessellation divisions (default: 50)
616 Higher values create smoother sphere but use more GPU
619 VisualizerError: If sky texture setting fails
620 ValueError: If parameters are invalid
623 >>> visualizer.setBackgroundSkyTexture() # Default gradient sky
624 >>> visualizer.setBackgroundSkyTexture("sky_hdri.jpg", divisions=100)
629 if not isinstance(divisions, _INT_TYPE)
or divisions <= 0:
630 raise ValueError(
"Divisions must be a positive integer")
635 if not isinstance(texture_file, str):
636 raise ValueError(
"Texture file must be a string")
644 visualizer_wrapper.set_background_sky_texture(
650 logger.debug(f
"Sky texture background set: {resolved_path}, divisions={divisions}")
652 logger.debug(f
"Default sky texture background set with divisions={divisions}")
653 except Exception
as e:
661 direction: Light direction vector as vec3 (will be normalized)
664 VisualizerError: If light direction setting fails
665 ValueError: If direction is invalid
671 if not isinstance(direction, vec3):
672 raise ValueError(f
"Direction must be a vec3, got {type(direction).__name__}")
675 if direction.x == 0
and direction.y == 0
and direction.z == 0:
676 raise ValueError(
"Light direction cannot be zero vector")
679 visualizer_wrapper.set_light_direction(self.
visualizer, direction)
680 logger.debug(f
"Light direction set to ({direction.x}, {direction.y}, {direction.z})")
681 except Exception
as e:
689 lighting_model: Lighting model, either:
690 - 0 or "none": No lighting
691 - 1 or "phong": Phong shading
692 - 2 or "phong_shadowed": Phong shading with shadows
695 VisualizerError: If lighting model setting fails
696 ValueError: If lighting model is invalid
702 if isinstance(lighting_model, str):
703 lighting_model_lower = lighting_model.lower()
704 if lighting_model_lower
in [
'none',
'no',
'off']:
706 elif lighting_model_lower
in [
'phong',
'phong_lighting']:
708 elif lighting_model_lower
in [
'phong_shadowed',
'phong_shadows',
'shadowed']:
711 raise ValueError(f
"Unknown lighting model string: {lighting_model}")
715 raise ValueError(f
"Lighting model must be 0 (NONE), 1 (PHONG), or 2 (PHONG_SHADOWED), got {lighting_model}")
718 visualizer_wrapper.set_lighting_model(self.
visualizer, lighting_model)
719 model_names = {0:
"NONE", 1:
"PHONG", 2:
"PHONG_SHADOWED"}
720 logger.debug(f
"Lighting model set to {model_names.get(lighting_model, lighting_model)}")
721 except Exception
as e:
726 Color context primitives based on primitive data values.
728 This method maps primitive data values to colors using the current colormap.
729 The visualization will be updated to show data variations across primitives.
731 The data must have been previously set on the primitives in the Context using
732 context.setPrimitiveDataFloat(UUID, data_name, value) before calling this method.
735 data_name: Name of the primitive data to use for coloring.
736 This should match the data label used with setPrimitiveDataFloat().
737 uuids: Optional list of specific primitive UUIDs to color.
738 If None, all primitives in context will be colored.
741 VisualizerError: If visualizer is not initialized or operation fails
742 ValueError: If data_name is invalid or UUIDs are malformed
745 >>> # Set data on primitives in context
746 >>> context.setPrimitiveDataFloat(patch_uuid, "radiation_flux_SW", 450.2)
747 >>> context.setPrimitiveDataFloat(triangle_uuid, "radiation_flux_SW", 320.1)
749 >>> # Build geometry and color by data
750 >>> visualizer.buildContextGeometry(context)
751 >>> visualizer.colorContextPrimitivesByData("radiation_flux_SW")
752 >>> visualizer.plotInteractive()
754 >>> # Color only specific primitives
755 >>> visualizer.colorContextPrimitivesByData("temperature", [uuid1, uuid2, uuid3])
760 if not data_name
or not isinstance(data_name, str):
761 raise ValueError(
"Data name must be a non-empty string")
766 visualizer_wrapper.color_context_primitives_by_data(self.
visualizer, data_name)
767 logger.debug(f
"Colored all primitives by data: {data_name}")
770 if not isinstance(uuids, (list, tuple))
or not uuids:
771 raise ValueError(
"UUIDs must be a non-empty list or tuple")
772 if not all(isinstance(uuid, _INT_TYPE)
and uuid >= 0
for uuid
in uuids):
773 raise ValueError(
"All UUIDs must be non-negative integers")
775 visualizer_wrapper.color_context_primitives_by_data_uuids(self.
visualizer, data_name, list(uuids))
776 logger.debug(f
"Colored {len(uuids)} primitives by data: {data_name}")
781 except Exception
as e:
782 raise VisualizerError(f
"Failed to color primitives by data '{data_name}': {e}")
788 Set camera field of view angle.
791 angle_FOV: Field of view angle in degrees
794 ValueError: If angle is invalid
795 VisualizerError: If operation fails
802 except (TypeError, ValueError):
803 raise ValueError(
"Field of view angle must be numeric")
804 if angle_FOV <= 0
or angle_FOV >= 180:
805 raise ValueError(
"Field of view angle must be between 0 and 180 degrees")
808 helios_lib.setCameraFieldOfView(self.
visualizer, ctypes.c_float(angle_FOV))
809 except Exception
as e:
814 Get current camera position and look-at point.
817 Tuple of (camera_position, look_at_point) as vec3 objects
820 VisualizerError: If operation fails
827 camera_pos = (ctypes.c_float * 3)()
828 look_at = (ctypes.c_float * 3)()
830 helios_lib.getCameraPosition(self.
visualizer, camera_pos, look_at)
832 return (
vec3(camera_pos[0], camera_pos[1], camera_pos[2]),
833 vec3(look_at[0], look_at[1], look_at[2]))
834 except Exception
as e:
839 Get current background color.
842 Background color as RGBcolor object
845 VisualizerError: If operation fails
852 color = (ctypes.c_float * 3)()
854 helios_lib.getBackgroundColor(self.
visualizer, color)
856 return RGBcolor(color[0], color[1], color[2])
857 except Exception
as e:
864 Set light intensity scaling factor.
867 intensity_factor: Light intensity scaling factor (typically 0.1 to 10.0)
870 ValueError: If intensity factor is invalid
871 VisualizerError: If operation fails
876 if not isinstance(intensity_factor, _NUMERIC_TYPES):
877 raise ValueError(
"Light intensity factor must be numeric")
878 if intensity_factor <= 0:
879 raise ValueError(
"Light intensity factor must be positive")
882 helios_lib.setLightIntensityFactor(self.
visualizer, ctypes.c_float(intensity_factor))
883 except Exception
as e:
890 Get window size in pixels.
893 Tuple of (width, height) in pixels
896 VisualizerError: If operation fails
902 width = ctypes.c_uint()
903 height = ctypes.c_uint()
905 helios_lib.getWindowSize(self.
visualizer, ctypes.byref(width), ctypes.byref(height))
907 return (width.value, height.value)
908 except Exception
as e:
913 Get framebuffer size in pixels.
916 Tuple of (width, height) in pixels
919 VisualizerError: If operation fails
925 width = ctypes.c_uint()
926 height = ctypes.c_uint()
928 helios_lib.getFramebufferSize(self.
visualizer, ctypes.byref(width), ctypes.byref(height))
930 return (width.value, height.value)
931 except Exception
as e:
936 Print window with default filename.
939 VisualizerError: If operation fails
946 helios_lib.printWindowDefault(self.
visualizer)
947 except Exception
as e:
952 Display image from RGBA pixel data.
955 pixel_data: RGBA pixel data as list of integers (0-255)
956 width: Image width in pixels
957 height: Image height in pixels
960 ValueError: If parameters are invalid
961 VisualizerError: If operation fails
966 if not isinstance(pixel_data, (list, tuple)):
967 raise ValueError(
"Pixel data must be a list or tuple")
968 if not isinstance(width, _INT_TYPE)
or width <= 0:
969 raise ValueError(
"Width must be a positive integer")
970 if not isinstance(height, _INT_TYPE)
or height <= 0:
971 raise ValueError(
"Height must be a positive integer")
973 expected_size = width * height * 4
974 if len(pixel_data) != expected_size:
975 raise ValueError(f
"Pixel data size mismatch: expected {expected_size}, got {len(pixel_data)}")
979 pixel_array = (ctypes.c_ubyte * len(pixel_data))(*pixel_data)
980 helios_lib.displayImageFromPixels(self.
visualizer, pixel_array, width, height)
981 except Exception
as e:
986 Display image from file.
989 filename: Path to image file
992 ValueError: If filename is invalid
993 VisualizerError: If operation fails
998 if not isinstance(filename, str)
or not filename.strip():
999 raise ValueError(
"Filename must be a non-empty string")
1002 helios_lib.displayImageFromFile(self.
visualizer, filename.encode(
'utf-8'))
1003 except Exception
as e:
1004 raise VisualizerError(f
"Failed to display image from file '{filename}': {e}")
1007 classes_file: str =
"", line_width: float = 2.0,
1008 fontsize: int = 12) ->
None:
1010 Display an image with YOLO bounding boxes overlaid.
1012 Each box is drawn as a colored outline with its class name on a filled chip in
1013 the box's top-left corner. Boxes are colored by class ID from a fixed palette of
1014 seven colors, so classes whose IDs differ by a multiple of seven share a color.
1016 This reads the annotation format written by
1017 :meth:`pyhelios.RadiationModel.writeImageBoundingBoxes`.
1020 image_file: Path to the image file (JPEG or PNG)
1021 bbox_file: Path to the YOLO-format bounding box annotation file
1022 classes_file: Path to the class name file. If empty (the default), a file
1023 named "classes.txt" beside ``bbox_file`` is used when one exists;
1024 otherwise boxes are labeled with their numeric class ID.
1025 line_width: Width of the box outlines in screen pixels
1026 fontsize: Size of the class label font in points
1029 ValueError: If a path is not a non-empty string, or a numeric argument is out of range
1030 VisualizerError: If the operation fails
1033 Like :meth:`displayImageFromFile`, this clears any existing geometry and does
1034 not return until the window is closed.
1037 >>> vis.displayImageWithBoundingBoxes("scene.jpeg", "scene.txt")
1042 if not isinstance(image_file, str)
or not image_file.strip():
1043 raise ValueError(
"Image file must be a non-empty string")
1044 if not isinstance(bbox_file, str)
or not bbox_file.strip():
1045 raise ValueError(
"Bounding box file must be a non-empty string")
1046 if classes_file
and not isinstance(classes_file, str):
1048 f
"Classes file must be a string, got {type(classes_file).__name__}")
1050 raise ValueError(f
"Line width must be positive, got {line_width}")
1051 if not isinstance(fontsize, _INT_TYPE)
or fontsize <= 0:
1052 raise ValueError(f
"Font size must be a positive integer, got {fontsize}")
1055 visualizer_wrapper.display_image_with_bounding_boxes(
1060 float(line_width), int(fontsize))
1061 except Exception
as e:
1063 f
"Failed to display image '{image_file}' with bounding boxes: {e}")
1066 fill_opacity: float = 0.4, line_width: float = 2.0,
1067 fontsize: int = 12, show_labels: bool =
True) ->
None:
1069 Display an image with COCO segmentation masks overlaid.
1071 Each mask is drawn as a translucent filled polygon with a solid outline and its
1072 class name on a filled chip. Masks are colored by their position in the file
1073 rather than by class, so two touching objects of the same class stay
1076 This reads the annotation format written by
1077 :meth:`pyhelios.RadiationModel.writeImageSegmentationMasks`.
1080 image_file: Path to the image file (JPEG or PNG)
1081 mask_file: Path to the COCO JSON segmentation mask file
1082 fill_opacity: Opacity of the translucent fill, between 0 and 1. A value of 0
1083 draws the outline without a fill.
1084 line_width: Width of the mask outlines in screen pixels
1085 fontsize: Size of the class label font in points
1086 show_labels: Whether to draw the class name chip on each mask. Pass False to
1087 see the masks alone, which helps when many overlapping chips would cover
1091 ValueError: If a path is not a non-empty string, or a numeric argument is out of range
1092 VisualizerError: If the operation fails
1095 Like :meth:`displayImageFromFile`, this clears any existing geometry and does
1096 not return until the window is closed.
1099 >>> vis.displayImageWithSegmentationMasks("scene.jpeg", "annotations.json")
1104 if not isinstance(image_file, str)
or not image_file.strip():
1105 raise ValueError(
"Image file must be a non-empty string")
1106 if not isinstance(mask_file, str)
or not mask_file.strip():
1107 raise ValueError(
"Mask file must be a non-empty string")
1108 if not 0.0 <= fill_opacity <= 1.0:
1109 raise ValueError(f
"Fill opacity must be between 0 and 1, got {fill_opacity}")
1111 raise ValueError(f
"Line width must be positive, got {line_width}")
1112 if not isinstance(fontsize, _INT_TYPE)
or fontsize <= 0:
1113 raise ValueError(f
"Font size must be a positive integer, got {fontsize}")
1114 if not isinstance(show_labels, bool):
1116 f
"Show labels must be a boolean, got {type(show_labels).__name__}")
1119 visualizer_wrapper.display_image_with_segmentation_masks(
1123 float(fill_opacity), float(line_width), int(fontsize), show_labels)
1124 except Exception
as e:
1126 f
"Failed to display image '{image_file}' with segmentation masks: {e}")
1130 Render primitive colors exactly as they are set in the Context.
1132 By default the fragment shader multiplies primitive colors by 1.5, which
1133 brightens ordinary renders but means a color read back out of the framebuffer is
1134 not the color that was set. This mode disables that multiplier, which is required
1135 when the framebuffer carries data rather than an image -- for example when object
1136 IDs are encoded as RGB values and decoded from the rendered pixels.
1138 Exact reproduction additionally requires that no lighting be applied (see
1139 :meth:`setLightingModel` with ``LIGHTING_NONE``, the default) and that the
1140 Visualizer was constructed with ``antialiasing_samples=0``, since antialiasing
1141 blends colors at primitive edges and produces pixels that decode to meaningless
1144 This also disables the linear-light pipeline (see :meth:`disableLinearPipeline`),
1145 since tone mapping would likewise alter the values read back.
1148 VisualizerError: If the operation fails
1151 >>> vis = Visualizer(800, 600, antialiasing_samples=0)
1152 >>> vis.enableExactColorMode()
1157 visualizer_wrapper.enable_exact_color_mode(self.
visualizer)
1158 except Exception
as e:
1163 Restore the default brightening of primitive colors.
1165 This also restores the linear-light pipeline (see :meth:`enableLinearPipeline`).
1168 VisualizerError: If the operation fails
1171 >>> vis.disableExactColorMode()
1176 visualizer_wrapper.disable_exact_color_mode(self.
visualizer)
1177 except Exception
as e:
1182 Enable the physically-based linear-light rendering pipeline.
1184 This is the default. Albedo is decoded from sRGB to linear light before shading,
1185 and the shaded radiance is tone-mapped through an ACES filmic curve and
1186 re-encoded to sRGB. Compared with shading directly on non-linear sRGB values,
1187 mid-tones are brighter, shadow terminators are smoother, and bright surfaces roll
1188 off rather than clipping flat against the 8-bit framebuffer.
1191 VisualizerError: If the operation fails
1194 >>> vis.enableLinearPipeline()
1199 visualizer_wrapper.enable_linear_pipeline(self.
visualizer)
1200 except Exception
as e:
1205 Disable the linear-light pipeline, shading directly in sRGB space.
1207 This reproduces the rendering behavior of helios-core versions before 1.3.83.
1210 VisualizerError: If the operation fails
1213 >>> vis.disableLinearPipeline()
1218 visualizer_wrapper.disable_linear_pipeline(self.
visualizer)
1219 except Exception
as e:
1224 Check whether the linear-light rendering pipeline is enabled.
1227 True if the linear pipeline is enabled
1230 VisualizerError: If the operation fails
1233 >>> vis.isLinearPipelineEnabled()
1239 return visualizer_wrapper.is_linear_pipeline_enabled(self.
visualizer)
1240 except Exception
as e:
1245 Set the linear exposure multiplier applied before tone mapping.
1247 Only has an effect while the linear pipeline is enabled.
1250 exposure: Exposure multiplier; must be positive
1253 ValueError: If exposure is not positive
1254 VisualizerError: If the operation fails
1257 >>> vis.setExposure(1.5)
1261 if not isinstance(exposure, (int, float))
or isinstance(exposure, bool):
1262 raise ValueError(f
"Exposure must be a number, got {type(exposure).__name__}")
1264 raise ValueError(f
"Exposure must be positive, got {exposure}")
1266 visualizer_wrapper.set_exposure(self.
visualizer, float(exposure))
1267 except Exception
as e:
1272 Get the linear exposure multiplier applied before tone mapping.
1275 Current exposure multiplier
1278 VisualizerError: If the operation fails
1281 >>> vis.getExposure()
1287 return visualizer_wrapper.get_exposure(self.
visualizer)
1288 except Exception
as e:
1291 def setPhongMaterial(self, ambient: float, diffuse: float, specular: float,
1292 shininess: float) ->
None:
1294 Set the Phong material parameters used to shade Context primitives.
1296 Surfaces are shaded as ``ambient*A + diffuse*max(0, N.L) +
1297 specular*max(0, N.H)^shininess``, where ``A`` is the hemispheric ambient term set
1298 by :meth:`setAmbientColors`. Setting ``specular`` to zero removes the highlight,
1299 recovering the appearance of helios-core versions before 1.3.83. Has no effect
1300 under ``LIGHTING_NONE``.
1302 Individual materials can override any of these parameters by attaching
1303 ``phong_ambient``, ``phong_diffuse``, ``phong_specular`` or ``phong_shininess``
1304 material data with :meth:`Context.setMaterialDataFloat`; each parameter falls
1305 back individually to the value set here.
1308 ambient: Ambient reflectance weight
1309 diffuse: Diffuse reflectance weight
1310 specular: Specular highlight strength
1311 shininess: Specular exponent controlling highlight tightness
1314 VisualizerError: If the operation fails
1317 >>> vis.setPhongMaterial(1.0, 0.8, 0.0, 32.0) # no specular highlight
1322 visualizer_wrapper.set_phong_material(self.
visualizer, float(ambient),
1323 float(diffuse), float(specular),
1325 except Exception
as e:
1330 Get the Phong material parameters used to shade Context primitives.
1333 Tuple of (ambient, diffuse, specular, shininess)
1336 VisualizerError: If the operation fails
1339 >>> ambient, diffuse, specular, shininess = vis.getPhongMaterial()
1344 return visualizer_wrapper.get_phong_material(self.
visualizer)
1345 except Exception
as e:
1348 def setAmbientColors(self, sky_color: RGBcolor, ground_color: RGBcolor) ->
None:
1350 Set the hemispheric ambient sky and ground-bounce colors.
1352 Ambient light is blended between the two according to surface orientation:
1353 upward-facing surfaces pick up ``sky_color``, downward-facing surfaces
1354 ``ground_color``. Setting both to the same value recovers the single constant
1355 ambient term used before helios-core 1.3.83.
1358 sky_color: Color of light arriving from above
1359 ground_color: Color of light bouncing from below
1362 ValueError: If either argument is not an RGBcolor
1363 VisualizerError: If the operation fails
1366 >>> vis.setAmbientColors(RGBcolor(0.5, 0.6, 0.75), RGBcolor(0.35, 0.3, 0.22))
1370 if not isinstance(sky_color, RGBcolor):
1371 raise ValueError(f
"sky_color must be an RGBcolor, got {type(sky_color).__name__}")
1372 if not isinstance(ground_color, RGBcolor):
1373 raise ValueError(f
"ground_color must be an RGBcolor, got {type(ground_color).__name__}")
1375 visualizer_wrapper.set_ambient_colors(
1377 (sky_color.r, sky_color.g, sky_color.b),
1378 (ground_color.r, ground_color.g, ground_color.b),
1380 except Exception
as e:
1385 Get the hemispheric ambient sky color.
1388 Color of ambient light arriving from above
1391 VisualizerError: If the operation fails
1394 >>> vis.getAmbientSkyColor()
1399 r, g, b = visualizer_wrapper.get_ambient_sky_color(self.
visualizer)
1401 except Exception
as e:
1406 Get the hemispheric ambient ground-bounce color.
1409 Color of ambient light bouncing from below
1412 VisualizerError: If the operation fails
1415 >>> vis.getAmbientGroundColor()
1420 r, g, b = visualizer_wrapper.get_ambient_ground_color(self.
visualizer)
1422 except Exception
as e:
1427 Enable smooth per-vertex-normal shading.
1429 This is the default. Only geometry that supplies distinct vertex normals is
1430 affected: Sphere, Tube and Cone objects, and Polymesh objects loaded from an OBJ
1431 or PLY file that carries them. Patches, triangles and voxels added directly to
1432 the Context carry the face normal replicated across their vertices and render
1433 identically either way.
1436 VisualizerError: If the operation fails
1439 >>> vis.enableSmoothShading()
1444 visualizer_wrapper.enable_smooth_shading(self.
visualizer)
1445 except Exception
as e:
1450 Select flat (per-face) shading.
1452 Useful for alpha-masked cutout geometry such as leaf textures, where interpolated
1453 normals can look worse than a single flat normal per face.
1456 VisualizerError: If the operation fails
1459 >>> vis.disableSmoothShading()
1464 visualizer_wrapper.disable_smooth_shading(self.
visualizer)
1465 except Exception
as e:
1470 Check whether smooth per-vertex-normal shading is enabled.
1473 True if smooth shading is enabled
1476 VisualizerError: If the operation fails
1479 >>> vis.isSmoothShadingEnabled()
1485 return visualizer_wrapper.is_smooth_shading_enabled(self.
visualizer)
1486 except Exception
as e:
1491 Check whether headless rendering obtained multisampled framebuffer attachments.
1493 Headless rendering draws into a multisampled framebuffer using the sample count
1494 given to the constructor and resolves it before readback, so saved images are
1495 anti-aliased. The requested count is clamped to the driver maximum, and a driver
1496 that refuses the attachments falls back silently -- this query is how to detect
1500 True if multisampled attachments were obtained
1503 macOS drives OpenGL through a translation layer that accepts the attachments
1504 but does not rasterize into them, so this can report True while the saved
1505 image is not actually anti-aliased.
1508 VisualizerError: If the operation fails
1511 >>> vis.isHeadlessMultisamplingActive()
1517 return visualizer_wrapper.is_headless_multisampling_active(self.
visualizer)
1518 except Exception
as e:
1519 raise VisualizerError(f
"Failed to query headless multisampling state: {e}")
1521 def getTextboxSize(self, textstring: str, fontsize: int, fontname: str) -> vec2:
1523 Measure the rendered size of a text string without adding it to the visualizer.
1525 Returns the extent that :meth:`addTextboxByCenter` would occupy for the same
1526 string, font and font size. The width is the sum of the glyph advances, so it
1527 includes the side bearings; the height is that of the tallest glyph in the
1528 string, so it depends on which characters the string contains. The ``_`` and
1529 ``^`` subscript and superscript markers are handled as
1530 :meth:`addTextboxByCenter` handles them: they occupy no width themselves and
1531 halve the size of the character that follows.
1534 textstring: Text to be measured
1535 fontsize: Size of the text font in points
1536 fontname: Name of a font in the visualizer fonts directory, e.g.
1540 Width and height of the text in window-normalized units.
1543 ValueError: If an argument is invalid
1544 VisualizerError: If the operation fails
1547 The result depends on the current framebuffer dimensions and DPI scale, and
1548 therefore changes when the window is resized.
1551 >>> size = vis.getTextboxSize("Leaf area", 14, "OpenSans-Regular")
1552 >>> print(f"{size.x:.3f} x {size.y:.3f}")
1557 if not isinstance(textstring, str):
1559 f
"Text string must be a string, got {type(textstring).__name__}")
1560 if not isinstance(fontsize, _INT_TYPE)
or fontsize <= 0:
1561 raise ValueError(f
"Font size must be a positive integer, got {fontsize}")
1562 if not isinstance(fontname, str)
or not fontname.strip():
1563 raise ValueError(
"Font name must be a non-empty string")
1570 width, height = visualizer_wrapper.get_textbox_size(
1571 self.
visualizer, textstring, int(fontsize), fontname)
1572 return vec2(width, height)
1573 except Exception
as e:
1580 Get RGB pixel data from the current window.
1582 Data is stored as r-g-b * column * row, so indices (0,1,2) are the RGB values for row 0
1583 column 0, indices (3,4,5) are row 0 column 1, and so on.
1585 Call without arguments to have the buffer allocated for you at the correct size -- this is
1586 the recommended form and cannot be undersized::
1588 pixels, width, height = visualizer.getWindowPixelsRGB()
1591 buffer: Optional pre-allocated list to fill in place. It must hold exactly
1592 ``3 * width * height`` elements, where width and height come from
1593 :meth:`getFramebufferSize` -- **not** :meth:`getWindowSize` and not the dimensions
1594 passed to the constructor. On a high-DPI (Retina) display the framebuffer is larger
1595 than the window, typically by a factor of two per axis, so a buffer sized from the
1596 window dimensions is four times too small.
1599 If ``buffer`` is None, a tuple of ``(pixel_data, width, height)``. Otherwise ``None``;
1600 ``buffer`` is filled in place.
1603 ValueError: If ``buffer`` is not a list, or is not sized for the current framebuffer
1604 VisualizerError: If operation fails
1612 width = ctypes.c_uint()
1613 height = ctypes.c_uint()
1614 size = ctypes.c_uint()
1615 ptr = helios_lib.getWindowPixelsRGB_sized(
1616 self.
visualizer, ctypes.byref(width), ctypes.byref(height), ctypes.byref(size)
1618 visualizer_wrapper._check_for_helios_error()
1619 if not ptr
or size.value == 0:
1621 "getWindowPixelsRGB() returned no pixel data; the framebuffer reported "
1622 f
"{width.value}x{height.value}"
1624 arr = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_uint * size.value)).contents
1625 return ([int(v)
for v
in arr], width.value, height.value)
1626 except VisualizerError:
1628 except Exception
as e:
1631 if not isinstance(buffer, list):
1632 raise ValueError(
"Buffer must be a list")
1636 required = 3 * fb_width * fb_height
1637 if len(buffer) != required:
1639 f
"Buffer must hold exactly {required} elements for the current framebuffer of "
1640 f
"{fb_width}x{fb_height} (3*width*height), got {len(buffer)}. Note the framebuffer "
1641 f
"may be larger than the window on a high-DPI display; use getFramebufferSize(), "
1642 f
"not getWindowSize(). Call getWindowPixelsRGB() with no argument to have the "
1643 f
"buffer allocated for you."
1648 buffer_array = (ctypes.c_uint * len(buffer))(*buffer)
1649 helios_lib.getWindowPixelsRGB(self.
visualizer, buffer_array)
1650 visualizer_wrapper._check_for_helios_error()
1653 for i
in range(len(buffer)):
1654 buffer[i] = buffer_array[i]
1655 except VisualizerError:
1657 except Exception
as e:
1660 def getDepthMap(self) -> Tuple[List[float], int, int]:
1662 Get depth map from current window.
1665 Tuple of (depth_pixels, width, height)
1668 VisualizerError: If operation fails
1675 depth_ptr = ctypes.POINTER(ctypes.c_float)()
1676 width = ctypes.c_uint()
1677 height = ctypes.c_uint()
1678 buffer_size = ctypes.c_uint()
1680 helios_lib.getDepthMap(self.
visualizer, ctypes.byref(depth_ptr),
1681 ctypes.byref(width), ctypes.byref(height),
1682 ctypes.byref(buffer_size))
1685 if depth_ptr
and buffer_size.value > 0:
1686 depth_data = [depth_ptr[i]
for i
in range(buffer_size.value)]
1687 return (depth_data, width.value, height.value)
1690 except Exception
as e:
1695 Plot depth map visualization.
1698 VisualizerError: If operation fails
1706 except Exception
as e:
1713 Clear all geometry from visualizer.
1716 Do NOT use this to refresh a scene between animation frames. The
1717 visualizer syncs from the Context incrementally using the Context's
1718 dirty flags. Clearing discards the visualizer's geometry while the
1719 Context primitives remain marked clean, so the next rebuild pulls in
1720 nothing and subsequent frames render empty -- printWindow() then
1721 silently writes no file.
1723 To animate a changing Context, just call buildContextGeometry()
1724 and plotUpdate() each frame without clearing; additions, deletions
1725 and modifications are all picked up automatically.
1728 VisualizerError: If operation fails
1735 except Exception
as e:
1740 Clear context geometry from visualizer.
1743 VisualizerError: If operation fails
1749 helios_lib.clearContextGeometry(self.
visualizer)
1750 except Exception
as e:
1755 Delete specific geometry by ID.
1758 geometry_id: ID of geometry to delete
1761 ValueError: If geometry ID is invalid
1762 VisualizerError: If operation fails
1767 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
1768 raise ValueError(
"Geometry ID must be a non-negative integer")
1771 helios_lib.deleteGeometry(self.
visualizer, geometry_id)
1772 except Exception
as e:
1773 raise VisualizerError(f
"Failed to delete geometry {geometry_id}: {e}")
1777 Update context primitive colors.
1780 VisualizerError: If operation fails
1786 helios_lib.updateContextPrimitiveColors(self.
visualizer)
1787 except Exception
as e:
1788 raise VisualizerError(f
"Failed to update context primitive colors: {e}")
1794 Get vertices of a geometry primitive.
1797 geometry_id: Unique identifier of the geometry primitive
1800 List of vertices as vec3 objects
1803 ValueError: If geometry ID is invalid
1804 VisualizerError: If operation fails
1807 >>> # Get vertices of a specific geometry
1808 >>> vertices = visualizer.getGeometryVertices(geometry_id)
1809 >>> for vertex in vertices:
1810 ... print(f"Vertex: ({vertex.x}, {vertex.y}, {vertex.z})")
1815 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
1816 raise ValueError(
"Geometry ID must be a non-negative integer")
1819 vertices_list = visualizer_wrapper.get_geometry_vertices(self.
visualizer, geometry_id)
1821 return [
vec3(v[0], v[1], v[2])
for v
in vertices_list]
1822 except Exception
as e:
1827 Set vertices of a geometry primitive.
1829 This allows dynamic modification of geometry shapes during visualization.
1830 Useful for animating geometry or adjusting shapes based on simulation results.
1833 geometry_id: Unique identifier of the geometry primitive
1834 vertices: List of new vertices as vec3 objects
1837 ValueError: If parameters are invalid
1838 VisualizerError: If operation fails
1841 >>> # Modify vertices of an existing geometry
1842 >>> vertices = visualizer.getGeometryVertices(geometry_id)
1843 >>> # Scale all vertices by 2x
1844 >>> scaled_vertices = [vec3(v.x*2, v.y*2, v.z*2) for v in vertices]
1845 >>> visualizer.setGeometryVertices(geometry_id, scaled_vertices)
1850 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
1851 raise ValueError(
"Geometry ID must be a non-negative integer")
1853 if not vertices
or not isinstance(vertices, (list, tuple)):
1854 raise ValueError(
"Vertices must be a non-empty list")
1856 if not all(isinstance(v, vec3)
for v
in vertices):
1857 raise ValueError(
"All vertices must be vec3 objects")
1860 visualizer_wrapper.set_geometry_vertices(self.
visualizer, geometry_id, vertices)
1861 logger.debug(f
"Set {len(vertices)} vertices for geometry {geometry_id}")
1862 except Exception
as e:
1869 Add coordinate axes at origin with unit length.
1872 VisualizerError: If operation fails
1878 helios_lib.addCoordinateAxes(self.
visualizer)
1879 except Exception
as e:
1884 Add coordinate axes with custom properties.
1887 origin: Axes origin position
1888 length: Axes length in each direction
1889 sign: Axis direction ("both" or "positive")
1892 ValueError: If parameters are invalid
1893 VisualizerError: If operation fails
1898 if not isinstance(origin, vec3):
1899 raise ValueError(
"Origin must be a vec3")
1900 if not isinstance(length, vec3):
1901 raise ValueError(
"Length must be a vec3")
1902 if not isinstance(sign, str)
or sign
not in [
"both",
"positive"]:
1903 raise ValueError(
"Sign must be 'both' or 'positive'")
1906 origin_array = (ctypes.c_float * 3)(origin.x, origin.y, origin.z)
1907 length_array = (ctypes.c_float * 3)(length.x, length.y, length.z)
1908 helios_lib.addCoordinateAxesCustom(self.
visualizer, origin_array, length_array, sign.encode(
'utf-8'))
1909 except Exception
as e:
1914 Remove coordinate axes.
1917 VisualizerError: If operation fails
1923 helios_lib.disableCoordinateAxes(self.
visualizer)
1924 except Exception
as e:
1927 def addGridWireFrame(self, center: vec3, size: vec3, subdivisions: List[int]) ->
None:
1932 center: Grid center position
1933 size: Grid size in each direction
1934 subdivisions: Grid subdivisions [x, y, z]
1937 ValueError: If parameters are invalid
1938 VisualizerError: If operation fails
1943 if not isinstance(center, vec3):
1944 raise ValueError(
"Center must be a vec3")
1945 if not isinstance(size, vec3):
1946 raise ValueError(
"Size must be a vec3")
1947 if not isinstance(subdivisions, (list, tuple))
or len(subdivisions) != 3:
1948 raise ValueError(
"Subdivisions must be a list of 3 integers")
1949 if not all(isinstance(s, _INT_TYPE)
and s > 0
for s
in subdivisions):
1950 raise ValueError(
"All subdivisions must be positive integers")
1953 center_array = (ctypes.c_float * 3)(center.x, center.y, center.z)
1954 size_array = (ctypes.c_float * 3)(size.x, size.y, size.z)
1955 subdiv_array = (ctypes.c_int * 3)(*subdivisions)
1956 helios_lib.addGridWireFrame(self.
visualizer, center_array, size_array, subdiv_array)
1957 except Exception
as e:
1967 VisualizerError: If operation fails
1974 except Exception
as e:
1982 VisualizerError: If operation fails
1989 except Exception
as e:
1994 Set colorbar position.
1997 position: Colorbar position
2000 ValueError: If position is invalid
2001 VisualizerError: If operation fails
2006 if not isinstance(position, vec3):
2007 raise ValueError(
"Position must be a vec3")
2010 pos_array = (ctypes.c_float * 3)(position.x, position.y, position.z)
2011 helios_lib.setColorbarPosition(self.
visualizer, pos_array)
2012 except Exception
as e:
2020 width: Colorbar width
2021 height: Colorbar height
2024 ValueError: If size is invalid
2025 VisualizerError: If operation fails
2030 if not isinstance(width, _NUMERIC_TYPES)
or width <= 0:
2031 raise ValueError(
"Width must be a positive number")
2032 if not isinstance(height, _NUMERIC_TYPES)
or height <= 0:
2033 raise ValueError(
"Height must be a positive number")
2036 size_array = (ctypes.c_float * 2)(float(width), float(height))
2037 helios_lib.setColorbarSize(self.
visualizer, size_array)
2038 except Exception
as e:
2045 Setting a range explicitly stops the colorbar from auto-ranging over the data, including
2046 for the degenerate range ``setColorbarRange(0, 0)``.
2049 min_val: Minimum value
2050 max_val: Maximum value. Must be greater than or equal to ``min_val``; helios ignores an
2054 ValueError: If range is invalid
2055 VisualizerError: If operation fails
2060 if not isinstance(min_val, _NUMERIC_TYPES):
2061 raise ValueError(
"Minimum value must be numeric")
2062 if not isinstance(max_val, _NUMERIC_TYPES):
2063 raise ValueError(
"Maximum value must be numeric")
2064 if min_val > max_val:
2065 raise ValueError(
"Minimum value must not be greater than maximum value")
2068 helios_lib.setColorbarRange(self.
visualizer, float(min_val), float(max_val))
2069 except Exception
as e:
2074 Set colorbar tick marks.
2077 ticks: List of tick values
2080 If a tick value falls outside the colorbar range, the range is automatically expanded
2081 to fit it. Because the colormap limits follow the colorbar range, this changes the
2082 colors shown as well as the labels. To keep an explicit range authoritative, call
2083 :meth:`setColorbarRange` after :meth:`setColorbarTicks`.
2086 ValueError: If ticks are invalid
2087 VisualizerError: If operation fails
2092 if not isinstance(ticks, (list, tuple)):
2093 raise ValueError(
"Ticks must be a list or tuple")
2094 if not all(isinstance(t, _NUMERIC_TYPES)
for t
in ticks):
2095 raise ValueError(
"All tick values must be numeric")
2099 ticks_array = (ctypes.c_float * len(ticks))(*ticks)
2100 helios_lib.setColorbarTicks(self.
visualizer, ticks_array, len(ticks))
2102 helios_lib.setColorbarTicks(self.
visualizer,
None, 0)
2103 except Exception
as e:
2111 title: Colorbar title
2114 ValueError: If title is invalid
2115 VisualizerError: If operation fails
2120 if not isinstance(title, str):
2121 raise ValueError(
"Title must be a string")
2124 helios_lib.setColorbarTitle(self.
visualizer, title.encode(
'utf-8'))
2125 except Exception
as e:
2130 Set colorbar font color.
2136 ValueError: If color is invalid
2137 VisualizerError: If operation fails
2142 if not isinstance(color, RGBcolor):
2143 raise ValueError(
"Color must be an RGBcolor")
2146 color_array = (ctypes.c_float * 3)(color.r, color.g, color.b)
2147 helios_lib.setColorbarFontColor(self.
visualizer, color_array)
2148 except Exception
as e:
2153 Set colorbar font size.
2156 font_size: Font size
2159 ValueError: If font size is invalid
2160 VisualizerError: If operation fails
2165 if not isinstance(font_size, _INT_TYPE)
or font_size <= 0:
2166 raise ValueError(
"Font size must be a positive integer")
2169 helios_lib.setColorbarFontSize(self.
visualizer, font_size)
2170 except Exception
as e:
2175 def setColormap(self, colormap: Union[int, str]) ->
None:
2177 Set predefined colormap.
2180 colormap: Colormap ID (0-5) or name ("HOT", "COOL", "RAINBOW", "LAVA", "PARULA", "GRAY")
2183 ValueError: If colormap is invalid
2184 VisualizerError: If operation fails
2190 "HOT": 0,
"COOL": 1,
"RAINBOW": 2,
2191 "LAVA": 3,
"PARULA": 4,
"GRAY": 5
2194 if isinstance(colormap, str):
2195 if colormap.upper()
not in colormap_map:
2196 raise ValueError(f
"Unknown colormap name: {colormap}")
2197 colormap_id = colormap_map[colormap.upper()]
2198 elif isinstance(colormap, _INT_TYPE):
2199 if colormap < 0
or colormap > 5:
2200 raise ValueError(
"Colormap ID must be 0-5")
2201 colormap_id = colormap
2203 raise ValueError(
"Colormap must be integer ID or string name")
2206 helios_lib.setColormap(self.
visualizer, colormap_id)
2207 except Exception
as e:
2210 def setCustomColormap(self, colors: List[RGBcolor], divisions: List[float]) ->
None:
2212 Set custom colormap.
2215 colors: List of RGB colors
2216 divisions: List of division points (same length as colors)
2219 ValueError: If parameters are invalid
2220 VisualizerError: If operation fails
2225 if not isinstance(colors, (list, tuple))
or not colors:
2226 raise ValueError(
"Colors must be a non-empty list")
2227 if not isinstance(divisions, (list, tuple))
or not divisions:
2228 raise ValueError(
"Divisions must be a non-empty list")
2229 if len(colors) != len(divisions):
2230 raise ValueError(
"Colors and divisions must have the same length")
2232 if not all(isinstance(c, RGBcolor)
for c
in colors):
2233 raise ValueError(
"All colors must be RGBcolor objects")
2234 if not all(isinstance(d, _NUMERIC_TYPES)
for d
in divisions):
2235 raise ValueError(
"All divisions must be numeric")
2239 color_array = (ctypes.c_float * (len(colors) * 3))()
2240 for i, color
in enumerate(colors):
2241 color_array[i*3] = color.r
2242 color_array[i*3+1] = color.g
2243 color_array[i*3+2] = color.b
2245 divisions_array = (ctypes.c_float * len(divisions))(*divisions)
2247 helios_lib.setCustomColormap(self.
visualizer, color_array, divisions_array, len(colors))
2248 except Exception
as e:
2255 Color context primitives by object data.
2258 data_name: Name of object data to use for coloring
2259 obj_ids: Optional list of object IDs to color (None for all)
2262 ValueError: If parameters are invalid
2263 VisualizerError: If operation fails
2268 if not isinstance(data_name, str)
or not data_name.strip():
2269 raise ValueError(
"Data name must be a non-empty string")
2273 helios_lib.colorContextPrimitivesByObjectData(self.
visualizer, data_name.encode(
'utf-8'))
2275 if not isinstance(obj_ids, (list, tuple)):
2276 raise ValueError(
"Object IDs must be a list or tuple")
2277 if not all(isinstance(oid, _INT_TYPE)
and oid >= 0
for oid
in obj_ids):
2278 raise ValueError(
"All object IDs must be non-negative integers")
2281 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
2282 helios_lib.colorContextPrimitivesByObjectDataIDs(self.
visualizer, data_name.encode(
'utf-8'), obj_ids_array, len(obj_ids))
2284 helios_lib.colorContextPrimitivesByObjectDataIDs(self.
visualizer, data_name.encode(
'utf-8'),
None, 0)
2285 except Exception
as e:
2286 raise VisualizerError(f
"Failed to color primitives by object data '{data_name}': {e}")
2290 Color context primitives randomly.
2293 uuids: Optional list of primitive UUIDs to color (None for all)
2296 ValueError: If UUIDs are invalid
2297 VisualizerError: If operation fails
2304 helios_lib.colorContextPrimitivesRandomly(self.
visualizer,
None, 0)
2306 if not isinstance(uuids, (list, tuple)):
2307 raise ValueError(
"UUIDs must be a list or tuple")
2308 if not all(isinstance(uuid, _INT_TYPE)
and uuid >= 0
for uuid
in uuids):
2309 raise ValueError(
"All UUIDs must be non-negative integers")
2312 uuid_array = (ctypes.c_uint * len(uuids))(*uuids)
2313 helios_lib.colorContextPrimitivesRandomly(self.
visualizer, uuid_array, len(uuids))
2315 helios_lib.colorContextPrimitivesRandomly(self.
visualizer,
None, 0)
2316 except Exception
as e:
2321 Color context objects randomly.
2324 obj_ids: Optional list of object IDs to color (None for all)
2327 ValueError: If object IDs are invalid
2328 VisualizerError: If operation fails
2335 helios_lib.colorContextObjectsRandomly(self.
visualizer,
None, 0)
2337 if not isinstance(obj_ids, (list, tuple)):
2338 raise ValueError(
"Object IDs must be a list or tuple")
2339 if not all(isinstance(oid, _INT_TYPE)
and oid >= 0
for oid
in obj_ids):
2340 raise ValueError(
"All object IDs must be non-negative integers")
2343 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
2344 helios_lib.colorContextObjectsRandomly(self.
visualizer, obj_ids_array, len(obj_ids))
2346 helios_lib.colorContextObjectsRandomly(self.
visualizer,
None, 0)
2347 except Exception
as e:
2352 Clear primitive colors from previous coloring operations.
2355 VisualizerError: If operation fails
2362 except Exception
as e:
2369 Hide Helios logo watermark.
2372 VisualizerError: If operation fails
2379 except Exception
as e:
2384 Show Helios logo watermark.
2387 VisualizerError: If operation fails
2394 except Exception
as e:
2399 Update watermark geometry to match current window size.
2402 VisualizerError: If operation fails
2409 except Exception
as e:
2416 Hide navigation gizmo (coordinate axes indicator in corner).
2418 The navigation gizmo shows XYZ axes orientation and can be clicked
2419 to snap the camera to standard views (top, front, side, etc.).
2422 VisualizerError: If operation fails
2429 visualizer_wrapper.hide_navigation_gizmo(self.
visualizer)
2430 logger.debug(
"Navigation gizmo hidden")
2431 except Exception
as e:
2436 Show navigation gizmo (coordinate axes indicator in corner).
2438 The navigation gizmo shows XYZ axes orientation and can be clicked
2439 to snap the camera to standard views (top, front, side, etc.).
2441 Note: Navigation gizmo is shown by default in v1.3.53+.
2444 VisualizerError: If operation fails
2451 visualizer_wrapper.show_navigation_gizmo(self.
visualizer)
2452 logger.debug(
"Navigation gizmo shown")
2453 except Exception
as e:
2460 Enable standard output from visualizer plugin.
2463 VisualizerError: If operation fails
2470 except Exception
as e:
2475 Disable standard output from visualizer plugin.
2478 VisualizerError: If operation fails
2485 except Exception
as e:
2488 def plotOnce(self, get_keystrokes: bool =
True) ->
None:
2490 Run one rendering loop.
2492 Any geometry pending upload is transferred to the GPU before rendering, but unlike
2493 :meth:`plotUpdate` the Context geometry is not rebuilt. Call :meth:`buildContextGeometry`
2494 or :meth:`plotUpdate` if primitives have been added to or changed in the Context since the
2498 get_keystrokes: Whether to process keystrokes
2501 VisualizerError: If operation fails
2507 helios_lib.plotOnce(self.
visualizer, get_keystrokes)
2508 except Exception
as e:
2513 Update visualization with window visibility control.
2516 hide_window: Whether to hide the window during update
2519 VisualizerError: If operation fails
2527 helios_lib.plotUpdateWithVisibility(self.
visualizer, hide_window)
2528 except Exception
as e:
2529 raise VisualizerError(f
"Failed to update plot with visibility control: {e}")
2535 Enable or disable point cloud culling optimization.
2537 Point culling improves rendering performance for large point clouds by
2538 selectively rendering only points that are visible based on distance
2539 and density criteria.
2542 enabled: True to enable culling, False to disable (default: True)
2545 ValueError: If enabled is not a boolean
2546 VisualizerError: If operation fails
2549 >>> with Visualizer(800, 600) as vis:
2550 ... vis.setPointCullingEnabled(False) # Disable for highest quality
2551 ... vis.setPointCullingEnabled(True) # Enable for better performance
2555 if not isinstance(enabled, bool):
2556 raise ValueError(f
"Enabled must be a boolean, got {type(enabled).__name__}")
2559 visualizer_wrapper.set_point_culling_enabled(self.
visualizer, enabled)
2560 logger.debug(f
"Point culling {'enabled' if enabled else 'disabled'}")
2561 except Exception
as e:
2566 Set the minimum number of points required to trigger culling.
2568 Culling is only activated when the total point count exceeds this threshold.
2569 This prevents unnecessary culling overhead for small point clouds.
2572 threshold: Point count threshold (default: 10000). Set to 0 to always enable.
2575 ValueError: If threshold is not a non-negative integer
2576 VisualizerError: If operation fails
2579 >>> vis.setPointCullingThreshold(50000) # Only cull for >50k points
2580 >>> vis.setPointCullingThreshold(0) # Always enable culling
2584 if not isinstance(threshold, int):
2585 raise ValueError(f
"Threshold must be an integer, got {type(threshold).__name__}")
2587 raise ValueError(
"Point culling threshold must be non-negative")
2590 visualizer_wrapper.set_point_culling_threshold(self.
visualizer, threshold)
2591 logger.debug(f
"Point culling threshold set to {threshold}")
2592 except Exception
as e:
2597 Set the maximum rendering distance for points.
2599 Points beyond this distance from the camera are not rendered, improving
2600 performance for large scenes. The distance is measured in world units.
2603 distance: Maximum distance in world units. Use 0 for auto mode (scene_size * 5.0)
2606 ValueError: If distance is negative
2607 VisualizerError: If operation fails
2610 >>> vis.setPointMaxRenderDistance(0.0) # Auto mode
2611 >>> vis.setPointMaxRenderDistance(100.0) # Fixed distance
2614 Setting distance to 0 enables automatic mode, which calculates the
2615 render distance based on the scene bounding box dimensions.
2619 if not isinstance(distance, (int, float)):
2620 raise ValueError(f
"Distance must be numeric, got {type(distance).__name__}")
2622 raise ValueError(
"Point max render distance cannot be negative")
2625 visualizer_wrapper.set_point_max_render_distance(self.
visualizer, float(distance))
2627 logger.debug(
"Point max render distance set to auto mode")
2629 logger.debug(f
"Point max render distance set to {distance}")
2630 except Exception
as e:
2631 raise VisualizerError(f
"Failed to set point max render distance: {e}")
2635 Set the level-of-detail factor for distance-based culling.
2637 Controls how aggressively points are culled based on distance from camera.
2638 Higher values result in more aggressive culling (better performance, lower quality).
2639 Lower values preserve more points (higher quality, lower performance).
2642 factor: LOD factor (default: 10.0, typical range: 1.0-50.0). Must be positive.
2645 ValueError: If factor is not positive
2646 VisualizerError: If operation fails
2649 >>> vis.setPointLODFactor(5.0) # Conservative culling
2650 >>> vis.setPointLODFactor(10.0) # Default culling
2651 >>> vis.setPointLODFactor(25.0) # Aggressive culling
2654 The LOD factor determines the rate at which point density decreases
2655 with distance. Higher factors mean points are culled more quickly
2656 as distance increases.
2660 if not isinstance(factor, (int, float)):
2661 raise ValueError(f
"LOD factor must be numeric, got {type(factor).__name__}")
2663 raise ValueError(
"Point LOD factor must be positive")
2667 logger.warning(f
"Point LOD factor {factor} is very low (< 1.0), may cause performance issues")
2668 elif factor > 100.0:
2669 logger.warning(f
"Point LOD factor {factor} is very high (> 100.0), may over-cull points")
2672 visualizer_wrapper.set_point_lod_factor(self.
visualizer, float(factor))
2673 logger.debug(f
"Point LOD factor set to {factor}")
2674 except Exception
as e:
2679 Get point cloud rendering performance metrics.
2681 Provides detailed statistics about point cloud culling and rendering
2682 performance, useful for optimizing visualization settings.
2685 Dictionary with keys:
2686 - 'total_points' (int): Total number of points in the scene
2687 - 'rendered_points' (int): Number of points actually rendered after culling
2688 - 'culling_time_ms' (float): Time spent on culling in milliseconds
2691 VisualizerError: If operation fails
2694 >>> metrics = vis.getPointRenderingMetrics()
2695 >>> print(f"Total: {metrics['total_points']}")
2696 >>> print(f"Rendered: {metrics['rendered_points']}")
2697 >>> cull_rate = (1 - metrics['rendered_points']/metrics['total_points']) * 100
2698 >>> print(f"Culling rate: {cull_rate:.1f}%")
2701 Metrics are only meaningful after calling plotUpdate() or plotInteractive().
2702 The culling_time_ms represents CPU time spent on culling calculations,
2703 not total frame time.
2709 metrics = visualizer_wrapper.get_point_rendering_metrics(self.
visualizer)
2711 f
"Point rendering metrics: {metrics['total_points']} total, "
2712 f
"{metrics['rendered_points']} rendered, "
2713 f
"{metrics['culling_time_ms']:.2f} ms culling time"
2716 except Exception
as e:
2720 """Destructor to ensure proper cleanup."""
2721 if hasattr(self,
'visualizer')
and self.
visualizer is not None:
2724 visualizer_wrapper.destroy_visualizer(self.
visualizer)