141 def __init__(self, width: int, height: int, antialiasing_samples: int = 1, 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: 1)
149 headless: Enable headless mode for offscreen rendering (default: False)
152 VisualizerError: If visualizer plugin is not available
153 ValueError: If parameters are invalid
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__}")
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")
178 registry = get_plugin_registry()
180 if not registry.is_plugin_available(
'visualizer'):
182 available_plugins = registry.get_available_plugins()
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}"
200 system = platform.system().lower()
201 if 'linux' in system:
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"
207 elif 'darwin' in system:
209 "\n\nmacOS installation hints:\n"
210 "- Install XQuartz: brew install --cask xquartz\n"
211 "- OpenGL should be available by default"
213 elif 'windows' in system:
215 "\n\nWindows installation hints:\n"
216 "- OpenGL drivers should be provided by graphics card drivers\n"
217 "- Visual Studio runtime may be required"
225 if antialiasing_samples > 1:
226 self.
visualizer = visualizer_wrapper.create_visualizer_with_antialiasing(
227 width, height, antialiasing_samples, headless
230 self.
visualizer = visualizer_wrapper.create_visualizer(
231 width, height, headless
236 "Failed to create Visualizer instance. "
237 "This may indicate a problem with graphics drivers or OpenGL initialization."
239 logger.info(f
"Visualizer created successfully ({width}x{height}, AA:{antialiasing_samples}, headless:{headless})")
241 except Exception
as e:
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")
250 """Context manager entry."""
253 def __exit__(self, exc_type, exc_value, traceback):
254 """Context manager exit with proper cleanup."""
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}")
265 @validate_build_geometry_params
268 Build Context geometry in the visualizer.
270 This method loads geometry from a Helios Context into the visualizer
271 for rendering. If no UUIDs are specified, all geometry is loaded.
274 context: Helios Context instance containing geometry
275 uuids: Optional list of primitive UUIDs to visualize (default: all)
278 VisualizerError: If geometry building fails
279 ValueError: If parameters are invalid
283 if not isinstance(context, Context):
284 raise ValueError(
"context must be a Context instance")
296 visualizer_wrapper.build_context_geometry(self.
visualizer, context.getNativePtr())
297 logger.debug(
"Built all Context geometry in visualizer")
301 raise ValueError(
"UUIDs list cannot be empty")
302 visualizer_wrapper.build_context_geometry_uuids(
303 self.
visualizer, context.getNativePtr(), uuids
305 logger.debug(f
"Built {len(uuids)} primitives in visualizer")
307 except Exception
as e:
312 Open interactive visualization window.
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.
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
326 VisualizerError: If visualization fails
334 visualizer_wrapper.plot_interactive(self.
visualizer)
335 logger.debug(
"Interactive visualization completed")
336 except Exception
as e:
341 Update visualization (non-interactive).
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.
347 In headless mode, automatically hides the window to prevent graphics driver crashes on some platforms.
350 VisualizerError: If visualization update fails
360 logger.debug(
"Visualization updated")
361 except Exception
as e:
364 @validate_print_window_params
365 def printWindow(self, filename: str, image_format: Optional[str] =
None) ->
None:
367 Save current visualization to image file.
369 This method exports the current visualization to an image file.
370 Starting from v1.3.53, supports both JPEG and PNG formats.
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.
381 VisualizerError: If image saving fails
382 ValueError: If filename or format is invalid
385 PNG format is required to preserve transparent backgrounds when using
386 setBackgroundTransparent(). JPEG format will render transparent areas as black.
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
397 raise ValueError(
"Filename cannot be empty")
403 if image_format
is None:
404 if resolved_filename.lower().endswith(
'.png'):
406 elif resolved_filename.lower().endswith((
'.jpg',
'.jpeg')):
407 image_format =
'jpeg'
410 image_format =
'jpeg'
411 logger.debug(f
"No format specified and extension not recognized, defaulting to JPEG")
414 if image_format.lower()
not in [
'jpeg',
'png']:
415 raise ValueError(f
"Image format must be 'jpeg' or 'png', got '{image_format}'")
421 visualizer_wrapper.print_window_with_format(
426 logger.debug(f
"Visualization saved to {resolved_filename} ({image_format.upper()} format)")
427 except (AttributeError, NotImplementedError):
429 if image_format.lower() !=
'jpeg':
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."
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:
441 Close visualization window.
443 This method closes any open visualization window. It's safe to call
444 even if no window is open.
447 VisualizerError: If window closing fails
453 visualizer_wrapper.close_window(self.
visualizer)
454 logger.debug(
"Visualization window closed")
455 except Exception
as e:
460 Set camera position using Cartesian coordinates.
463 position: Camera position as vec3 in world coordinates
464 lookAt: Camera look-at point as vec3 in world coordinates
467 VisualizerError: If camera positioning fails
468 ValueError: If parameters are invalid
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__}")
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:
487 Set camera position using spherical coordinates.
490 angle: Camera position as SphericalCoord (radius, elevation, azimuth)
491 lookAt: Camera look-at point as vec3 in world coordinates
494 VisualizerError: If camera positioning fails
495 ValueError: If parameters are invalid
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__}")
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}")
514 Set background color.
517 color: Background color as RGBcolor with values in range [0, 1]
520 VisualizerError: If color setting fails
521 ValueError: If color values are invalid
527 if not isinstance(color, RGBcolor):
528 raise ValueError(f
"Color must be an RGBcolor, got {type(color).__name__}")
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]")
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:
542 Enable transparent background mode (v1.3.53+).
544 Sets the background to transparent with checkerboard pattern display.
545 Requires PNG output format to preserve transparency.
547 Note: When using transparent background, use printWindow() with PNG
548 format to save transparent images.
551 VisualizerError: If transparent background setting fails
557 visualizer_wrapper.set_background_transparent(self.
visualizer)
558 logger.debug(
"Background set to transparent mode")
559 except Exception
as e:
564 Set custom background image texture (v1.3.53+).
567 texture_file: Path to background image file
568 Can be absolute or relative to working directory
571 VisualizerError: If background image setting fails
572 ValueError: If texture file path is invalid
577 if not texture_file
or not isinstance(texture_file, str):
578 raise ValueError(
"Texture file path must be a non-empty string")
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:
591 Set sky sphere texture background with automatic scaling (v1.3.53+).
593 Creates a sky sphere that automatically scales with the scene.
594 Replaces the deprecated addSkyDomeByCenter() method.
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
603 VisualizerError: If sky texture setting fails
604 ValueError: If parameters are invalid
607 >>> visualizer.setBackgroundSkyTexture() # Default gradient sky
608 >>> visualizer.setBackgroundSkyTexture("sky_hdri.jpg", divisions=100)
613 if not isinstance(divisions, _INT_TYPE)
or divisions <= 0:
614 raise ValueError(
"Divisions must be a positive integer")
619 if not isinstance(texture_file, str):
620 raise ValueError(
"Texture file must be a string")
624 visualizer_wrapper.set_background_sky_texture(
630 logger.debug(f
"Sky texture background set: {resolved_path}, divisions={divisions}")
632 logger.debug(f
"Default sky texture background set with divisions={divisions}")
633 except Exception
as e:
641 direction: Light direction vector as vec3 (will be normalized)
644 VisualizerError: If light direction setting fails
645 ValueError: If direction is invalid
651 if not isinstance(direction, vec3):
652 raise ValueError(f
"Direction must be a vec3, got {type(direction).__name__}")
655 if direction.x == 0
and direction.y == 0
and direction.z == 0:
656 raise ValueError(
"Light direction cannot be zero vector")
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:
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
675 VisualizerError: If lighting model setting fails
676 ValueError: If lighting model is invalid
682 if isinstance(lighting_model, str):
683 lighting_model_lower = lighting_model.lower()
684 if lighting_model_lower
in [
'none',
'no',
'off']:
686 elif lighting_model_lower
in [
'phong',
'phong_lighting']:
688 elif lighting_model_lower
in [
'phong_shadowed',
'phong_shadows',
'shadowed']:
691 raise ValueError(f
"Unknown lighting model string: {lighting_model}")
695 raise ValueError(f
"Lighting model must be 0 (NONE), 1 (PHONG), or 2 (PHONG_SHADOWED), got {lighting_model}")
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:
706 Color context primitives based on primitive data values.
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.
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.
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.
721 VisualizerError: If visualizer is not initialized or operation fails
722 ValueError: If data_name is invalid or UUIDs are malformed
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)
729 >>> # Build geometry and color by data
730 >>> visualizer.buildContextGeometry(context)
731 >>> visualizer.colorContextPrimitivesByData("radiation_flux_SW")
732 >>> visualizer.plotInteractive()
734 >>> # Color only specific primitives
735 >>> visualizer.colorContextPrimitivesByData("temperature", [uuid1, uuid2, uuid3])
740 if not data_name
or not isinstance(data_name, str):
741 raise ValueError(
"Data name must be a non-empty string")
746 visualizer_wrapper.color_context_primitives_by_data(self.
visualizer, data_name)
747 logger.debug(f
"Colored all primitives by data: {data_name}")
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")
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}")
761 except Exception
as e:
762 raise VisualizerError(f
"Failed to color primitives by data '{data_name}': {e}")
768 Set camera field of view angle.
771 angle_FOV: Field of view angle in degrees
774 ValueError: If angle is invalid
775 VisualizerError: If operation fails
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")
788 helios_lib.setCameraFieldOfView(self.
visualizer, ctypes.c_float(angle_FOV))
789 except Exception
as e:
794 Get current camera position and look-at point.
797 Tuple of (camera_position, look_at_point) as vec3 objects
800 VisualizerError: If operation fails
807 camera_pos = (ctypes.c_float * 3)()
808 look_at = (ctypes.c_float * 3)()
810 helios_lib.getCameraPosition(self.
visualizer, camera_pos, look_at)
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:
819 Get current background color.
822 Background color as RGBcolor object
825 VisualizerError: If operation fails
832 color = (ctypes.c_float * 3)()
834 helios_lib.getBackgroundColor(self.
visualizer, color)
836 return RGBcolor(color[0], color[1], color[2])
837 except Exception
as e:
844 Set light intensity scaling factor.
847 intensity_factor: Light intensity scaling factor (typically 0.1 to 10.0)
850 ValueError: If intensity factor is invalid
851 VisualizerError: If operation fails
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")
862 helios_lib.setLightIntensityFactor(self.
visualizer, ctypes.c_float(intensity_factor))
863 except Exception
as e:
870 Get window size in pixels.
873 Tuple of (width, height) in pixels
876 VisualizerError: If operation fails
882 width = ctypes.c_uint()
883 height = ctypes.c_uint()
885 helios_lib.getWindowSize(self.
visualizer, ctypes.byref(width), ctypes.byref(height))
887 return (width.value, height.value)
888 except Exception
as e:
893 Get framebuffer size in pixels.
896 Tuple of (width, height) in pixels
899 VisualizerError: If operation fails
905 width = ctypes.c_uint()
906 height = ctypes.c_uint()
908 helios_lib.getFramebufferSize(self.
visualizer, ctypes.byref(width), ctypes.byref(height))
910 return (width.value, height.value)
911 except Exception
as e:
916 Print window with default filename.
919 VisualizerError: If operation fails
926 helios_lib.printWindowDefault(self.
visualizer)
927 except Exception
as e:
932 Display image from RGBA pixel data.
935 pixel_data: RGBA pixel data as list of integers (0-255)
936 width: Image width in pixels
937 height: Image height in pixels
940 ValueError: If parameters are invalid
941 VisualizerError: If operation fails
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")
953 expected_size = width * height * 4
954 if len(pixel_data) != expected_size:
955 raise ValueError(f
"Pixel data size mismatch: expected {expected_size}, got {len(pixel_data)}")
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:
966 Display image from file.
969 filename: Path to image file
972 ValueError: If filename is invalid
973 VisualizerError: If operation fails
978 if not isinstance(filename, str)
or not filename.strip():
979 raise ValueError(
"Filename must be a non-empty string")
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}")
990 Get RGB pixel data from current window.
993 buffer: Pre-allocated buffer to store pixel data
996 ValueError: If buffer is invalid
997 VisualizerError: If operation fails
1003 if not isinstance(buffer, list):
1004 raise ValueError(
"Buffer must be a list")
1008 buffer_array = (ctypes.c_uint * len(buffer))(*buffer)
1009 helios_lib.getWindowPixelsRGB(self.
visualizer, buffer_array)
1012 for i
in range(len(buffer)):
1013 buffer[i] = buffer_array[i]
1014 except Exception
as e:
1017 def getDepthMap(self) -> Tuple[List[float], int, int]:
1019 Get depth map from current window.
1022 Tuple of (depth_pixels, width, height)
1025 VisualizerError: If operation fails
1032 depth_ptr = ctypes.POINTER(ctypes.c_float)()
1033 width = ctypes.c_uint()
1034 height = ctypes.c_uint()
1035 buffer_size = ctypes.c_uint()
1037 helios_lib.getDepthMap(self.
visualizer, ctypes.byref(depth_ptr),
1038 ctypes.byref(width), ctypes.byref(height),
1039 ctypes.byref(buffer_size))
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)
1047 except Exception
as e:
1052 Plot depth map visualization.
1055 VisualizerError: If operation fails
1063 except Exception
as e:
1070 Clear all geometry from visualizer.
1073 VisualizerError: If operation fails
1080 except Exception
as e:
1085 Clear context geometry from visualizer.
1088 VisualizerError: If operation fails
1094 helios_lib.clearContextGeometry(self.
visualizer)
1095 except Exception
as e:
1100 Delete specific geometry by ID.
1103 geometry_id: ID of geometry to delete
1106 ValueError: If geometry ID is invalid
1107 VisualizerError: If operation fails
1112 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
1113 raise ValueError(
"Geometry ID must be a non-negative integer")
1116 helios_lib.deleteGeometry(self.
visualizer, geometry_id)
1117 except Exception
as e:
1118 raise VisualizerError(f
"Failed to delete geometry {geometry_id}: {e}")
1122 Update context primitive colors.
1125 VisualizerError: If operation fails
1131 helios_lib.updateContextPrimitiveColors(self.
visualizer)
1132 except Exception
as e:
1133 raise VisualizerError(f
"Failed to update context primitive colors: {e}")
1139 Get vertices of a geometry primitive.
1142 geometry_id: Unique identifier of the geometry primitive
1145 List of vertices as vec3 objects
1148 ValueError: If geometry ID is invalid
1149 VisualizerError: If operation fails
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})")
1160 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
1161 raise ValueError(
"Geometry ID must be a non-negative integer")
1164 vertices_list = visualizer_wrapper.get_geometry_vertices(self.
visualizer, geometry_id)
1166 return [
vec3(v[0], v[1], v[2])
for v
in vertices_list]
1167 except Exception
as e:
1172 Set vertices of a geometry primitive.
1174 This allows dynamic modification of geometry shapes during visualization.
1175 Useful for animating geometry or adjusting shapes based on simulation results.
1178 geometry_id: Unique identifier of the geometry primitive
1179 vertices: List of new vertices as vec3 objects
1182 ValueError: If parameters are invalid
1183 VisualizerError: If operation fails
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)
1195 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
1196 raise ValueError(
"Geometry ID must be a non-negative integer")
1198 if not vertices
or not isinstance(vertices, (list, tuple)):
1199 raise ValueError(
"Vertices must be a non-empty list")
1201 if not all(isinstance(v, vec3)
for v
in vertices):
1202 raise ValueError(
"All vertices must be vec3 objects")
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:
1214 Add coordinate axes at origin with unit length.
1217 VisualizerError: If operation fails
1223 helios_lib.addCoordinateAxes(self.
visualizer)
1224 except Exception
as e:
1229 Add coordinate axes with custom properties.
1232 origin: Axes origin position
1233 length: Axes length in each direction
1234 sign: Axis direction ("both" or "positive")
1237 ValueError: If parameters are invalid
1238 VisualizerError: If operation fails
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'")
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:
1259 Remove coordinate axes.
1262 VisualizerError: If operation fails
1268 helios_lib.disableCoordinateAxes(self.
visualizer)
1269 except Exception
as e:
1272 def addGridWireFrame(self, center: vec3, size: vec3, subdivisions: List[int]) ->
None:
1277 center: Grid center position
1278 size: Grid size in each direction
1279 subdivisions: Grid subdivisions [x, y, z]
1282 ValueError: If parameters are invalid
1283 VisualizerError: If operation fails
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")
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:
1312 VisualizerError: If operation fails
1319 except Exception
as e:
1327 VisualizerError: If operation fails
1334 except Exception
as e:
1339 Set colorbar position.
1342 position: Colorbar position
1345 ValueError: If position is invalid
1346 VisualizerError: If operation fails
1351 if not isinstance(position, vec3):
1352 raise ValueError(
"Position must be a vec3")
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:
1365 width: Colorbar width
1366 height: Colorbar height
1369 ValueError: If size is invalid
1370 VisualizerError: If operation fails
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")
1381 size_array = (ctypes.c_float * 2)(float(width), float(height))
1382 helios_lib.setColorbarSize(self.
visualizer, size_array)
1383 except Exception
as e:
1391 min_val: Minimum value
1392 max_val: Maximum value
1395 ValueError: If range is invalid
1396 VisualizerError: If operation fails
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")
1409 helios_lib.setColorbarRange(self.
visualizer, float(min_val), float(max_val))
1410 except Exception
as e:
1415 Set colorbar tick marks.
1418 ticks: List of tick values
1421 ValueError: If ticks are invalid
1422 VisualizerError: If operation fails
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")
1434 ticks_array = (ctypes.c_float * len(ticks))(*ticks)
1435 helios_lib.setColorbarTicks(self.
visualizer, ticks_array, len(ticks))
1437 helios_lib.setColorbarTicks(self.
visualizer,
None, 0)
1438 except Exception
as e:
1446 title: Colorbar title
1449 ValueError: If title is invalid
1450 VisualizerError: If operation fails
1455 if not isinstance(title, str):
1456 raise ValueError(
"Title must be a string")
1459 helios_lib.setColorbarTitle(self.
visualizer, title.encode(
'utf-8'))
1460 except Exception
as e:
1465 Set colorbar font color.
1471 ValueError: If color is invalid
1472 VisualizerError: If operation fails
1477 if not isinstance(color, RGBcolor):
1478 raise ValueError(
"Color must be an RGBcolor")
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:
1488 Set colorbar font size.
1491 font_size: Font size
1494 ValueError: If font size is invalid
1495 VisualizerError: If operation fails
1500 if not isinstance(font_size, _INT_TYPE)
or font_size <= 0:
1501 raise ValueError(
"Font size must be a positive integer")
1504 helios_lib.setColorbarFontSize(self.
visualizer, font_size)
1505 except Exception
as e:
1510 def setColormap(self, colormap: Union[int, str]) ->
None:
1512 Set predefined colormap.
1515 colormap: Colormap ID (0-5) or name ("HOT", "COOL", "RAINBOW", "LAVA", "PARULA", "GRAY")
1518 ValueError: If colormap is invalid
1519 VisualizerError: If operation fails
1525 "HOT": 0,
"COOL": 1,
"RAINBOW": 2,
1526 "LAVA": 3,
"PARULA": 4,
"GRAY": 5
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
1538 raise ValueError(
"Colormap must be integer ID or string name")
1541 helios_lib.setColormap(self.
visualizer, colormap_id)
1542 except Exception
as e:
1545 def setCustomColormap(self, colors: List[RGBcolor], divisions: List[float]) ->
None:
1547 Set custom colormap.
1550 colors: List of RGB colors
1551 divisions: List of division points (same length as colors)
1554 ValueError: If parameters are invalid
1555 VisualizerError: If operation fails
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")
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")
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
1580 divisions_array = (ctypes.c_float * len(divisions))(*divisions)
1582 helios_lib.setCustomColormap(self.
visualizer, color_array, divisions_array, len(colors))
1583 except Exception
as e:
1590 Color context primitives by object data.
1593 data_name: Name of object data to use for coloring
1594 obj_ids: Optional list of object IDs to color (None for all)
1597 ValueError: If parameters are invalid
1598 VisualizerError: If operation fails
1603 if not isinstance(data_name, str)
or not data_name.strip():
1604 raise ValueError(
"Data name must be a non-empty string")
1608 helios_lib.colorContextPrimitivesByObjectData(self.
visualizer, data_name.encode(
'utf-8'))
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")
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))
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}")
1625 Color context primitives randomly.
1628 uuids: Optional list of primitive UUIDs to color (None for all)
1631 ValueError: If UUIDs are invalid
1632 VisualizerError: If operation fails
1639 helios_lib.colorContextPrimitivesRandomly(self.
visualizer,
None, 0)
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")
1647 uuid_array = (ctypes.c_uint * len(uuids))(*uuids)
1648 helios_lib.colorContextPrimitivesRandomly(self.
visualizer, uuid_array, len(uuids))
1650 helios_lib.colorContextPrimitivesRandomly(self.
visualizer,
None, 0)
1651 except Exception
as e:
1656 Color context objects randomly.
1659 obj_ids: Optional list of object IDs to color (None for all)
1662 ValueError: If object IDs are invalid
1663 VisualizerError: If operation fails
1670 helios_lib.colorContextObjectsRandomly(self.
visualizer,
None, 0)
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")
1678 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
1679 helios_lib.colorContextObjectsRandomly(self.
visualizer, obj_ids_array, len(obj_ids))
1681 helios_lib.colorContextObjectsRandomly(self.
visualizer,
None, 0)
1682 except Exception
as e:
1687 Clear primitive colors from previous coloring operations.
1690 VisualizerError: If operation fails
1697 except Exception
as e:
1704 Hide Helios logo watermark.
1707 VisualizerError: If operation fails
1714 except Exception
as e:
1719 Show Helios logo watermark.
1722 VisualizerError: If operation fails
1729 except Exception
as e:
1734 Update watermark geometry to match current window size.
1737 VisualizerError: If operation fails
1744 except Exception
as e:
1751 Hide navigation gizmo (coordinate axes indicator in corner).
1753 The navigation gizmo shows XYZ axes orientation and can be clicked
1754 to snap the camera to standard views (top, front, side, etc.).
1757 VisualizerError: If operation fails
1763 visualizer_wrapper.hide_navigation_gizmo(self.
visualizer)
1764 logger.debug(
"Navigation gizmo hidden")
1765 except Exception
as e:
1770 Show navigation gizmo (coordinate axes indicator in corner).
1772 The navigation gizmo shows XYZ axes orientation and can be clicked
1773 to snap the camera to standard views (top, front, side, etc.).
1775 Note: Navigation gizmo is shown by default in v1.3.53+.
1778 VisualizerError: If operation fails
1784 visualizer_wrapper.show_navigation_gizmo(self.
visualizer)
1785 logger.debug(
"Navigation gizmo shown")
1786 except Exception
as e:
1793 Enable standard output from visualizer plugin.
1796 VisualizerError: If operation fails
1803 except Exception
as e:
1808 Disable standard output from visualizer plugin.
1811 VisualizerError: If operation fails
1818 except Exception
as e:
1821 def plotOnce(self, get_keystrokes: bool =
True) ->
None:
1823 Run one rendering loop.
1826 get_keystrokes: Whether to process keystrokes
1829 VisualizerError: If operation fails
1835 helios_lib.plotOnce(self.
visualizer, get_keystrokes)
1836 except Exception
as e:
1841 Update visualization with window visibility control.
1844 hide_window: Whether to hide the window during update
1847 VisualizerError: If operation fails
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}")
1863 Enable or disable point cloud culling optimization.
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.
1870 enabled: True to enable culling, False to disable (default: True)
1873 ValueError: If enabled is not a boolean
1874 VisualizerError: If operation fails
1877 >>> with Visualizer(800, 600) as vis:
1878 ... vis.setPointCullingEnabled(False) # Disable for highest quality
1879 ... vis.setPointCullingEnabled(True) # Enable for better performance
1883 if not isinstance(enabled, bool):
1884 raise ValueError(f
"Enabled must be a boolean, got {type(enabled).__name__}")
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:
1894 Set the minimum number of points required to trigger culling.
1896 Culling is only activated when the total point count exceeds this threshold.
1897 This prevents unnecessary culling overhead for small point clouds.
1900 threshold: Point count threshold (default: 10000). Set to 0 to always enable.
1903 ValueError: If threshold is not a non-negative integer
1904 VisualizerError: If operation fails
1907 >>> vis.setPointCullingThreshold(50000) # Only cull for >50k points
1908 >>> vis.setPointCullingThreshold(0) # Always enable culling
1912 if not isinstance(threshold, int):
1913 raise ValueError(f
"Threshold must be an integer, got {type(threshold).__name__}")
1915 raise ValueError(
"Point culling threshold must be non-negative")
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:
1925 Set the maximum rendering distance for points.
1927 Points beyond this distance from the camera are not rendered, improving
1928 performance for large scenes. The distance is measured in world units.
1931 distance: Maximum distance in world units. Use 0 for auto mode (scene_size * 5.0)
1934 ValueError: If distance is negative
1935 VisualizerError: If operation fails
1938 >>> vis.setPointMaxRenderDistance(0.0) # Auto mode
1939 >>> vis.setPointMaxRenderDistance(100.0) # Fixed distance
1942 Setting distance to 0 enables automatic mode, which calculates the
1943 render distance based on the scene bounding box dimensions.
1947 if not isinstance(distance, (int, float)):
1948 raise ValueError(f
"Distance must be numeric, got {type(distance).__name__}")
1950 raise ValueError(
"Point max render distance cannot be negative")
1953 visualizer_wrapper.set_point_max_render_distance(self.
visualizer, float(distance))
1955 logger.debug(
"Point max render distance set to auto mode")
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}")
1963 Set the level-of-detail factor for distance-based culling.
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).
1970 factor: LOD factor (default: 10.0, typical range: 1.0-50.0). Must be positive.
1973 ValueError: If factor is not positive
1974 VisualizerError: If operation fails
1977 >>> vis.setPointLODFactor(5.0) # Conservative culling
1978 >>> vis.setPointLODFactor(10.0) # Default culling
1979 >>> vis.setPointLODFactor(25.0) # Aggressive culling
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.
1988 if not isinstance(factor, (int, float)):
1989 raise ValueError(f
"LOD factor must be numeric, got {type(factor).__name__}")
1991 raise ValueError(
"Point LOD factor must be positive")
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")
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:
2007 Get point cloud rendering performance metrics.
2009 Provides detailed statistics about point cloud culling and rendering
2010 performance, useful for optimizing visualization settings.
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
2019 VisualizerError: If operation fails
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}%")
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.
2037 metrics = visualizer_wrapper.get_point_rendering_metrics(self.
visualizer)
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"
2044 except Exception
as e:
2048 """Destructor to ensure proper cleanup."""
2049 if hasattr(self,
'visualizer')
and self.
visualizer is not None:
2052 visualizer_wrapper.destroy_visualizer(self.
visualizer)