Source code for pynavaltoolbox.hull

# Copyright (C) 2025 Antoine ANCEAU
#
# This file is part of pynavaltoolbox.
#
# pynavaltoolbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""
Hull geometry management module.

This module provides the Hull class for loading, manipulating, and exporting
hull geometries from STL and VTK files.
"""

import vtk
from pathlib import Path
from typing import Tuple, Optional


[docs] class Hull: """ Represents a hull geometry with loading, transformation, and export capabilities. Attributes: polydata (vtk.vtkPolyData): The VTK polydata representing the hull mesh. file_path (str): Path to the original file. """
[docs] def __init__(self, file_path: str): """ Loads a hull geometry from an STL or VTK file. Args: file_path: Path to the STL or VTK file. Raises: FileNotFoundError: If the file does not exist. ValueError: If the file format is not supported. """ self.file_path = file_path path = Path(file_path) if not path.exists(): raise FileNotFoundError(f"File not found: {file_path}") # Determine file type and load accordingly suffix = path.suffix.lower() if suffix == '.stl': reader = vtk.vtkSTLReader() reader.SetFileName(file_path) reader.Update() self.polydata = reader.GetOutput() elif suffix == '.vtk': reader = vtk.vtkPolyDataReader() reader.SetFileName(file_path) reader.Update() self.polydata = reader.GetOutput() else: raise ValueError(f"Unsupported file format: {suffix}. Use .stl or .vtk") if self.polydata.GetNumberOfPoints() == 0: raise ValueError(f"No geometry data found in file: {file_path}")
[docs] def get_bounds(self) -> Tuple[float, float, float, float, float, float]: """ Returns the bounding box of the hull. Returns: Tuple of (xmin, xmax, ymin, ymax, zmin, zmax). """ return self.polydata.GetBounds()
[docs] def transform(self, translation: Tuple[float, float, float] = (0, 0, 0), rotation: Tuple[float, float, float] = (0, 0, 0), pivot: Tuple[float, float, float] = (0, 0, 0)) -> None: """ Applies a transformation to the hull geometry. Args: translation: (dx, dy, dz) translation vector. rotation: (rx, ry, rz) rotation angles in degrees around X, Y, Z axes. pivot: (px, py, pz) point around which rotation occurs. """ transform = vtk.vtkTransform() # Translate to pivot # Apply transformation to rotate around pivot # VTK transforms are applied in reverse order of code execution (PostMultiply) # We want: Translate(P) * Rotate * Translate(-P) * x # So code order: Translate(P) ... Rotate ... Translate(-P) # 1. Translate back to pivot location (Last step) transform.Translate(pivot[0], pivot[1], pivot[2]) # 2. Apply rotations transform.RotateZ(rotation[2]) transform.RotateY(rotation[1]) transform.RotateX(rotation[0]) # 3. Translate to coordinate system origin (First step) transform.Translate(-pivot[0], -pivot[1], -pivot[2]) # 4. Apply final translation transform.Translate(translation[0], translation[1], translation[2]) # Apply transform to polydata transform_filter = vtk.vtkTransformPolyDataFilter() transform_filter.SetInputData(self.polydata) transform_filter.SetTransform(transform) transform_filter.Update() self.polydata = transform_filter.GetOutput()
[docs] def scale(self, factor: Optional[float] = None, factors: Optional[Tuple[float, float, float]] = None, target_bounds: Optional[Tuple[float, float, float, float, float, float]] = None) -> None: """ Scales the hull geometry. Args: factor: Uniform scale factor (applied to x, y, z). factors: Tuple of (sx, sy, sz) for non-uniform scaling. target_bounds: (xmin, xmax, ymin, ymax, zmin, zmax) to fit the mesh into. Overrides factor and factors if provided. """ sx, sy, sz = 1.0, 1.0, 1.0 if target_bounds is not None: # Calculate scaling factors to fit into target bounds current_bounds = self.get_bounds() # Avoid division by zero x_range = current_bounds[1] - current_bounds[0] y_range = current_bounds[3] - current_bounds[2] z_range = current_bounds[5] - current_bounds[4] target_x_range = target_bounds[1] - target_bounds[0] target_y_range = target_bounds[3] - target_bounds[2] target_z_range = target_bounds[5] - target_bounds[4] if x_range > 1e-9: sx = target_x_range / x_range if y_range > 1e-9: sy = target_y_range / y_range if z_range > 1e-9: sz = target_z_range / z_range elif factors is not None: sx, sy, sz = factors elif factor is not None: sx, sy, sz = factor, factor, factor transform = vtk.vtkTransform() transform.Scale(sx, sy, sz) transform_filter = vtk.vtkTransformPolyDataFilter() transform_filter.SetInputData(self.polydata) transform_filter.SetTransform(transform) transform_filter.Update() self.polydata = transform_filter.GetOutput()
[docs] def simplify(self, target_reduction: float = 0.5) -> None: """ Simplifies the hull mesh by reducing the number of polygons. Useful for speeding up calculations with minimal loss of accuracy. Args: target_reduction: Fraction of polygons to remove (0.0 to 1.0). e.g. 0.5 removes 50% of polygons. """ if target_reduction <= 0.0 or target_reduction >= 1.0: return decimate = vtk.vtkQuadricDecimation() decimate.SetInputData(self.polydata) decimate.SetTargetReduction(target_reduction) decimate.Update() self.polydata = decimate.GetOutput()
[docs] def export(self, file_path: str) -> None: """ Exports the current hull geometry to a file. Args: file_path: Path to save the file. Format determined by extension (.stl or .vtk). Raises: ValueError: If the file format is not supported. """ path = Path(file_path) suffix = path.suffix.lower() if suffix == '.stl': writer = vtk.vtkSTLWriter() writer.SetFileName(file_path) writer.SetInputData(self.polydata) writer.Write() elif suffix == '.vtk': writer = vtk.vtkPolyDataWriter() writer.SetFileName(file_path) writer.SetInputData(self.polydata) writer.Write() else: raise ValueError(f"Unsupported export format: {suffix}. Use .stl or .vtk")