# 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 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")