Source code for aerocaps.geom.geometry_container

"""Storage container module"""
import time
import typing

import numpy as np
import pyvista as pv

from aerocaps.geom import Geometry
from aerocaps.iges.iges_generator import IGESGenerator
from aerocaps.stl.stl_generator import STLGenerator

__all__ = [
    "GeometryContainer"
]


[docs] class GeometryContainer: """Storage container for geometric objects that adds convenience methods for plotting and export"""
[docs] def __init__(self): r""" Storage container for geometric objects that adds convenience methods for plotting and export. The example code below shows how to add a curve and a surface to a new container, plots them in an interactive scene, exports them to IGES, and then removes both of them by varying identifiers: .. code-block:: python # Create the geometric objects curve = BezierCurve3D(np.array([ [0.0, 0.0, 0.0], [0.3, 0.2, 0.1], [0.6, 0.1, 0.3], [1.0, -0.1, 0.2] ]), name='MyCurve') surf = BezierSurface(np.array([ [ [0.0, 0.0, 0.0], [0.3, 0.2, 0.1], [0.6, 0.1, 0.3], [1.0, -0.1, 0.2] ], [ [0.0, 0.0, 1.0], [0.3, 0.4, 1.1], [0.6, 0.2, 1.3], [1.0, -0.3, 1.2] ] ])) # Instantiate a container container = GeometryContainer() # Add the geometries to the container container.add_geometry(point) container.add_geometry(curve) # List the geometries inside the container geom_names = container.geometry_name_list() print(f'{geom_names = }') # Plot the geometries in an interactive scene container.plot() # Export the geometries to an IGES file container.export_iges('curve_and_surf.igs', units='meters') # Remove the curve and surface by different methods container.remove_geometry('MyCurve') container.remove_geometry(surf) # Show that the container is now empty geom_names = container.geometry_name_list() print(f'{geom_names = }') """ self._container = dict()
def _get_max_index_associated_with_name(self, name: str) -> int: """ Gets the maximum index associated with a given name in the geometry container. If the name is the container but the name does not have an index (denoted by ``-<index>``), ``0`` will be returned. Parameters ---------- name: str Name of the geometry in the container Returns ------- int Maximum index associated with the name """ split_keys = [k.split("-") for k in self._container.keys()] indices = [int(split[-1]) for split in split_keys if split[0] == name and len(split) > 1] if not indices: return 0 # This means that "-1" will be appended to the name return max(indices)
[docs] def add_geometry(self, geom: Geometry): """ Adds a geometric object to the container, renaming the object with a higher index if necessary Parameters ---------- geom: Geometry Geometric object to add """ if geom.name in self._container.keys(): max_index = self._get_max_index_associated_with_name(geom.name) geom._name = f"{geom.name.split()[0]}-{max_index + 1}" self._container[geom.name] = geom geom.container = self
[docs] def remove_geometry(self, geom: str or Geometry) -> Geometry: """ Removes a geometric object from the container Parameters ---------- geom: str or Geometry The geometry to remove. If a :obj:`str`, this must be found in the list of geometry names that have already been added to the container or an exception will be thrown Returns ------- Geometry The geometry removed """ if isinstance(geom, str): if geom not in self._container.keys(): raise ValueError(f"Could not find geometry {geom} in container") return self._container.pop(geom) if isinstance(geom, Geometry): if geom not in self._container.values(): raise ValueError(f"Could not find geometry {geom} in container") return self._container.pop(geom.name) raise ValueError("'geom' must sub-class either 'str' or 'Geometry'")
[docs] def geometry_by_name(self, name: str) -> Geometry or None: """ Searches for a geometry in the container by name Parameters ---------- name: str Name of the geometric object Returns ------- Geometry or None If found, a geometric object is returned. Otherwise, ``None`` is returned """ if name not in self._container.keys(): return None return self._container[name]
[docs] def geometry_name_list(self, geom_type: type = None) -> typing.List[str]: """ Gets the list of geometries (by name) that have been added to the container Parameters ---------- geom_type: type If specified, only geometries with the given type will be returned. Default: ``None`` Returns ------- typing.List[str] List of geometry names """ if geom_type is None: return list(self._container.keys()) return [k for k, v in self._container.items() if isinstance(v, geom_type)]
[docs] def plot(self, show: bool = True, Nu: int = 50, Nv: int = 50, Nt: int = 50, surface_selection: bool = True, random_colors: bool = False, color_seed: int = 42 ): """ Plots all the plottable objects in the container onto a :obj:`pyvista.Plotter` scene. Also adds a surface picker to dynamically show surface information on right-click. Parameters ---------- show: bool Whether to show the plot. Default: ``True`` Nu: int The number of points in the :math:`u`-direction of each surface to evaluate. Default: ``50`` Nv: int The number of points in the :math:`u`-direction of each surface to evaluate. Default: ``50`` Nt: int The number of points to evaluate along each curve for a trimmed surface evaluation. Default: ``50`` surface_selection: bool Whether to allow interactive selection of surfaces. Default: ``True`` random_colors: bool Whether to paint each surface with a random color. Default: ``False`` color_seed: int The random number seed used to generate the random colors. Ignored if ``random_colors==False``. Default: ``42`` """ def selection_callback(mesh): if not hasattr(mesh, "aerocaps_surf"): return mesh.aerocaps_surf.plot_control_point_mesh_lines( plot, color="blue", name="selection_lines" ) mesh.aerocaps_surf.plot_control_points( plot, render_points_as_spheres=True, color="black", point_size=16, name="selection_cps" ) points = np.array([ mesh.aerocaps_surf.evaluate(0.0, 0.5), mesh.aerocaps_surf.evaluate(1.0, 0.5), mesh.aerocaps_surf.evaluate(0.5, 0.0), mesh.aerocaps_surf.evaluate(0.5, 1.0) ]) plot.add_point_labels( points=points, labels=["u0", "u1", "v0", "v1"], shape_color="white", always_visible=True, name="surf_edge_labels" ) plot.add_text( text=str(mesh.aerocaps_surf), position="lower_left", name="surf_repr" ) start_time = time.perf_counter() plot = pv.Plotter() num_geoms = len(self._container) # Initialize color array if random_colors is specified color_array = None if random_colors: rng = np.random.default_rng(seed=color_seed) color_array = rng.uniform(low=0.0, high=1.0, size=(num_geoms, 3)) for geom_idx, geom in enumerate(self._container.values()): if geom.construction: # Skip the construction geometries continue if hasattr(geom, "plot_surface"): color_kwargs = dict(color=color_array[geom_idx]) if random_colors else {} try: grid = geom.plot_surface(plot, Nu, Nv, **color_kwargs) grid.aerocaps_surf = geom except TypeError: grid = geom.plot_surface(plot, Nt=Nt, **color_kwargs) if hasattr(geom, "plot"): geom.plot(plot, color="lime") if surface_selection: plot.enable_mesh_picking( callback=selection_callback, style="surface", color="indianred", picker="hardware" ) plot.add_axes() end_time = time.perf_counter() elapsed_time = end_time - start_time print(f"\033[1;35mModel rendering time: {elapsed_time:.3f} seconds\033[0m") if show: plot.show()
[docs] def export_iges(self, file_name: str, units: str = "meters"): """ Exports all the exportable objects in the container to an IGES file Parameters ---------- file_name: str Path to the IGES file units: str Physical length units used to export the geometries. See :obj:`aerocaps.iges.iges_generator.IGESGenerator.__init__` for more details. Default: ``"meters"`` """ geoms_to_export = [] for geom in self._container.values(): if geom.construction: continue if not hasattr(geom, "to_iges") or (isinstance(geom, list) and not hasattr(geom, "to_iges")): continue entities = geom.to_iges() if isinstance(entities, list): geoms_to_export.extend(entities) else: geoms_to_export.append(entities) iges_generator = IGESGenerator(geoms_to_export, units) iges_generator.generate(file_name)
[docs] def export_stl(self, file_name: str, Nu: int = 50, Nv: int = 50): """ Exports all the exportable objects in the container to an STL file Parameters ---------- file_name: str Path to the STL file Nu: int Number of points to evaluate in the :math:`u`-parametric direction Nv: int Number of points to evaluate in the :math:`v`-parametric direction """ geoms_to_export = [] for geom in self._container.values(): if geom.construction: continue geoms_to_export.append(geom) stl_generator = STLGenerator(geoms_to_export, Nu=Nu, Nv=Nv) stl_generator.generate(file_name)