Source code for design3d.display

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Classes to define mesh for display use. Display mesh do not require good aspect ratios on elements.
"""

import warnings
from typing import List, Tuple, TypeVar, Union


import numpy as np
import pyfqmr
import trimesh
from numpy.typing import NDArray
from scipy.spatial import cKDTree
from trimesh import Trimesh

import design3d.edges
from design3d.core import Primitive3D
from design3d.geometry import numpy_cross2d

# TODO: make this module "mesh" as it is not useful only for display


[docs] class MeshMixin: """ Mixin class for 2D and 3D meshes. This is an abstract class. """ MeshType = TypeVar("MeshType", bound="MeshMixin") @property def n_vertices(self) -> int: """Get number of vertices in the mesh.""" return len(self.vertices) @property def n_triangles(self) -> int: """Get number of triangles in the mesh.""" return len(self.triangles) @property def dimension(self) -> int: """Get the dimension of the mesh ("2" for 2D mesh or "3" for 3D mesh).""" return self.vertices.shape[1] # MANIPULATION
[docs] def resize(self, scale_factor: float) -> MeshType: """ Resize the Mesh instance by scaling its vertices. :param scale_factor: The factor by which to scale the mesh. :type scale_factor: float :return: A new Mesh instance representing the scaled mesh. :rtype: MeshType """ return self.__class__(self.vertices * scale_factor, self.triangles, name=self.name)
[docs] def round_vertices(self, decimals: int = 9) -> MeshType: """ Round the vertices of the Mesh instance to a given number of decimals. :param decimals: The number of decimal places to round the vertices to (default is 9). :type decimals: int, optional :return: A new Mesh instance with rounded vertices. :rtype: MeshType """ rounded_vertices = np.round(self.vertices, decimals) return self.__class__(rounded_vertices, self.triangles, name=self.name)
[docs] def remove_degenerate_triangles(self, tol: float = 0.0) -> MeshType: """ Remove degenerate triangles from the Mesh instance. Degenerate triangles are triangles with vertices that are too close to each other. This method checks for degenerate triangles based on a tolerance value and removes them. :param tol: The tolerance value to determine whether a triangle is degenerate or not (default is 0.0). :type tol: float, optional :return: A new Mesh instance with degenerate triangles removed. :rtype: MeshType """ # Get vertices for each corner of the triangles v0, v1, v2 = ( self.vertices[self.triangles[:, 0]], self.vertices[self.triangles[:, 1]], self.vertices[self.triangles[:, 2]], ) # Calculate the squared distance between each pair of vertices dist_sq_v0_v1 = np.sum((v0 - v1) ** 2, axis=1) dist_sq_v1_v2 = np.sum((v1 - v2) ** 2, axis=1) dist_sq_v0_v2 = np.sum((v0 - v2) ** 2, axis=1) # Find triangles where all three vertices are distinct (no zero distances) valid_triangles_mask = (dist_sq_v0_v1 > tol) & (dist_sq_v1_v2 > tol) & (dist_sq_v0_v2 > tol) # Filter out invalid triangles valid_triangles = self.triangles[valid_triangles_mask] # Create a new Mesh3D instance with non-flat triangles return self.__class__(self.vertices, valid_triangles, name=self.name)
[docs] def merge(self, other: MeshType, merge_vertices: bool = False, merge_triangles: bool = False) -> MeshType: """ Merge two meshes. :param other: Another Mesh instance to merge with this instance. :type other: MeshType :param merge_vertices: Flag to indicate whether to merge vertices. :type merge_vertices: bool, optional :param merge_triangles: Flag to indicate whether to merge triangles. :type merge_triangles: bool, optional :return: A new Mesh instance representing the merged meshes. :rtype: MeshType """ if self.__class__.__name__ != other.__class__.__name__: raise ValueError("Meshes should have same dimension.") if len(self.vertices) == 0 or len(self.triangles) == 0: return other if len(other.vertices) == 0 or len(other.triangles) == 0: return self merged_vertices = np.concatenate((self.vertices, other.vertices)) # merged_triangles = np.concatenate((self.triangles, other.triangles + len(self.vertices).astype(np.int32))) merged_triangles = np.concatenate(( self.triangles.astype(np.int32), (other.triangles.astype(np.int32) + len(self.vertices)) ), dtype=np.int32) mesh = self.__class__(merged_vertices, merged_triangles, name=self.name) if merge_vertices: mesh = mesh.merge_vertices() if merge_triangles: mesh = mesh.merge_triangles() return mesh
[docs] def merge_vertices(self) -> MeshType: """ Merge duplicated vertices in the Mesh instance and remap triangles accordingly. This method identifies duplicate vertices and combines them into a single unique set of vertices, updating the triangles to use the unique vertices. :return: A new Mesh instance with merged vertices and updated triangles. :rtype: MeshType """ unique_vertices, indices_map = np.unique(self.vertices, axis=0, return_inverse=True) remapped_triangles = indices_map[self.triangles] return self.__class__(unique_vertices, remapped_triangles, name=self.name)
[docs] def merge_triangles(self) -> MeshType: """ Merge duplicated triangles in the Mesh instance. This method identifies and removes duplicate triangles, resulting in a Mesh with unique triangles. :return: A new Mesh instance with merged triangles. :rtype: MeshType """ sorted_triangles = np.sort(self.triangles, axis=1) _, unique_triangle_indices = np.unique(sorted_triangles, axis=0, return_index=True) unique_triangles = self.triangles[unique_triangle_indices] return self.__class__(self.vertices, unique_triangles, name=self.name)
[docs] def split_shared_vertices(self) -> MeshType: """ Split the shared vertices between triangles in the Mesh instance. This method recreates distinct vertices for each triangle, effectively unmerging shared vertices. The resulting mesh will have three times the number of vertices as the number of triangles. :return: A new Mesh instance with unmerged vertices and original triangles. :rtype: MeshType """ unmerged_vertices = self.vertices[self.triangles.ravel()] unmerged_triangles = np.arange(len(self.triangles) * 3).reshape(-1, 3) return self.__class__(unmerged_vertices, unmerged_triangles, name=self.name)
def __add__(self, other: MeshType) -> MeshType: """ Overload the "+" operator to merge two Mesh instances, without mutualization of vertices and triangles. :param other: Another Mesh instance to concatenate with this instance. :type other: MeshType :return: A new Mesh instance representing the merged shells. :rtype: MeshType """ return self.merge(other, merge_vertices=False, merge_triangles=False) def __or__(self, other: MeshType) -> MeshType: """ Overload the "|" operator to merge two Mesh instances, with mutualization of vertices and triangles. :param other: Another Mesh instance to concatenate with this instance. :type other: MeshType :return: A new Mesh instance representing the concatenated shells. :rtype: MeshType """ return self.merge(other, merge_vertices=True, merge_triangles=True)
[docs] @classmethod def from_meshes( cls, meshes: List[MeshType], merge_vertices: bool = False, merge_triangles: bool = False ) -> MeshType: """ Merge two meshes. :param meshes: A list of Mesh instance to merge all together. :type meshes: MeshType :param merge_vertices: Flag to indicate whether to merge vertices. :type merge_vertices: bool, optional :param merge_triangles: Flag to indicate whether to merge triangles. :type merge_triangles: bool, optional :return: A new Mesh instance representing the merged meshes. :rtype: MeshType """ merged_mesh = cls(np.array([]), np.array([])) for mesh in meshes: merged_mesh = merged_mesh.merge(mesh, merge_vertices=merge_vertices, merge_triangles=merge_triangles) return merged_mesh
# CHECK
[docs] def check_consistency(self) -> bool: """ Check the consistency of the Mesh instance. This method verifies that all vertices referenced by triangles are within the valid range of vertex indices. :return: True if the mesh is consistent, False otherwise. :rtype: bool """ max_vertex_indices = np.max(self.triangles, axis=None) return np.all(max_vertex_indices < len(self.vertices))
# COMPUTATION
[docs] def triangles_vertices(self): """ Get the actual triangles of the mesh represented by their vertices (not indices). :return: An array containing the vertices of the triangles. :rtype: np.ndarray[float] """ triangles = self.vertices.view(np.ndarray)[self.triangles] return triangles
[docs] def triangles_cross_products(self): """ Compute the cross products of edges for each triangle in the mesh. :return: An array containing the cross products of edges for each triangle. :rtype: np.ndarray[float] """ vectors = np.diff(self.triangles_vertices(), axis=1) return numpy_cross2d(vectors[:, 0], vectors[:, 1])
[docs] def plot(self, ax=None, numbering: bool = False): """Plot the mesh with Matplotlib.""" # Plot vertices for i_point, point in enumerate(self.vertices): ax = self._point_class(*point).plot(ax=ax) if numbering: ax.text(*point, f"node {i_point}", ha="center", va="center") # Plot line segments for vertex1, vertex2, vertex3 in self.triangles_vertices(): point1 = self._point_class(*vertex1) point2 = self._point_class(*vertex2) point3 = self._point_class(*vertex3) if not point1.is_close(point2): self._linesegment_class(point1, point2).plot(ax=ax) if not point2.is_close(point3): self._linesegment_class(point2, point3).plot(ax=ax) if not point1.is_close(point3): self._linesegment_class(point1, point3).plot(ax=ax) return ax
# SERIALIZATION
[docs] def to_dict(self, *args, **kwargs): """Overload of 'to_dict' for numpy usage and memory perf.""" dict_ = self.base_dict() dict_["name"] = self.name dict_["vertices"] = self.vertices.flatten().tolist() dict_["triangles"] = self.triangles.flatten().tolist() return dict_
[docs] @classmethod def dict_to_object(cls, dict_, *args, **kwargs) -> MeshType: """Overload of 'dict_to_object' for numpy usage and memory perf.""" vertices = np.array(dict_["vertices"]).reshape(-1, 3) triangles = np.array(dict_["triangles"]).reshape(-1, 3) name = dict_["name"] return cls(vertices=vertices, triangles=triangles, name=name)
# HASH AND EQUALITY def __hash__(self): """Computation of hash.""" return hash((self.__class__.__name__, self.vertices.tobytes(), self.triangles.tobytes())) def __eq__(self, other): """Equality.""" return hash(self) == hash(other) def _data_hash(self): """Computation of hash for Dessia platform usage.""" return hash(self) def _data_eq(self, other_object) -> bool: """Equality for Dessia platform usage.""" if other_object.__class__.__name__ != self.__class__.__name__: return False return self == other_object
[docs] class Mesh2D(MeshMixin): """ 2D triangle mesh. """ _linesegment_class = design3d.edges.LineSegment2D _point_class = design3d.Point2D def __init__(self, vertices: NDArray[float], triangles: NDArray[int], name: str = ""): """ Initialize a 2D mesh. :param vertices: An array of 2D vertices specifying the 2D mesh. :type vertices: ndarray[float] :param triangles: An array of triangles representing the connectivity of the 2D mesh. :type triangles: ndarray[int] :param name: A name for the mesh (default is an empty string). :type name: str, optional """ self.vertices = vertices self.triangles = triangles self.name = name
[docs] def area(self) -> float: """ Calculate the total area of the 2D mesh as the sum of areas of triangles. :return: The total area of the mesh. :rtype: float """ areas = np.sqrt((self.triangles_cross_products() ** 2)) / 2.0 return areas.sum()
[docs] class Mesh3D(MeshMixin, Primitive3D): """ 3D triangle mesh. """ # pylint: disable=too-many-public-methods _linesegment_class = design3d.edges.LineSegment3D _point_class = design3d.Point3D def __init__( self, vertices: NDArray[float], triangles: NDArray[int], color: Tuple[float, float, float] = None, alpha: float = 1.0, name: str = "", ): """ Initialize a 3D mesh. :param vertices: An array of 3D vertices specifying the 3D mesh. :param triangles: An array of triangles representing the connectivity of the 3D mesh. :param color: A color for the mesh, optional. :param alpha: An alpha value for the mesh, optional. :param name: A name for the mesh, optional (default is an empty string). """ self.vertices = vertices self.triangles = triangles self._faces = None self._bounding_box = None Primitive3D.__init__(self, color=color, alpha=alpha, name=name)
[docs] def triangulation(self) -> "Mesh3D": """Return self as triangulation to enable VolumeModel usage.""" return self
[docs] def to_babylon(self): """ Convert the mesh to the Babylon.js format. This method rounds the vertices to 6 decimal places and returns the mesh in a Babylon.js compatible format. https://doc.babylonjs.com/how_to/custom :return: A dictionary representing the mesh in Babylon.js format with 'positions' and 'indices' keys. :rtype: dict """ mesh = self.round_vertices(decimals=6) babylon_mesh = {"positions": mesh.vertices.flatten().tolist(), "indices": mesh.triangles.flatten().tolist()} return babylon_mesh
@property def bounding_box(self): """Bounding box of current mesh.""" if self._bounding_box is None: maximums = np.max(self.vertices, axis=0) minimums = np.min(self.vertices, axis=0) self._bounding_box = design3d.core.BoundingBox( minimums[0], maximums[0], minimums[1], maximums[1], minimums[2], maximums[2] ) return self._bounding_box
[docs] def area(self) -> float: """ Calculate the total surface area of the 3D mesh as the sum of areas of triangles. :return: The total surface area of the 3D mesh. :rtype: float """ areas = np.sqrt((self.triangles_cross_products() ** 2).sum(axis=1)) / 2.0 return areas.sum()
[docs] def minimum_distance(self, other_mesh: "Mesh3D", return_points: bool = False): """ Compute the minimum distance between this 3D mesh and another 3D mesh. This is an approximation: only vertices are taken in account for minimum distance computation. :param other_mesh: The other 3D mesh to compare against. :type other_mesh: Mesh3D :param return_points: Whether to return the closest points. :type return_points: bool, optional :return: The minimum distance between the two meshes, and optionally, the closest points. :rtype: float or (float, ndarray[float], ndarray[float]) """ # Create KD-Trees for both meshes (cKDTree for improved performance) self_tree = cKDTree(self.vertices) other_tree = cKDTree(other_mesh.vertices) # Query the KD-Tree to find the nearest neighbors for all vertices in one go _, self_to_other_indices = other_tree.query(self.vertices, k=1) _, other_to_self_indices = self_tree.query(other_mesh.vertices, k=1) # Calculate the minimum distance between vertices using vectorized operations self_to_other_distances = np.linalg.norm(self.vertices - other_mesh.vertices[self_to_other_indices], axis=1) other_to_self_distances = np.linalg.norm(other_mesh.vertices - self.vertices[other_to_self_indices], axis=1) min_distances = [self_to_other_distances.min(), other_to_self_distances.min()] if min_distances[0] < min_distances[1]: min_distance = min_distances[0] # Get the points corresponding to the minimum distance min_distance_index = np.argmin(self_to_other_distances) closest_point_self = self.vertices[min_distance_index] closest_point_other = other_mesh.vertices[self_to_other_indices[min_distance_index]] else: min_distance = min_distances[1] # Get the points corresponding to the minimum distance min_distance_index = np.argmin(other_to_self_distances) closest_point_self = self.vertices[other_to_self_indices[min_distance_index]] closest_point_other = other_mesh.vertices[min_distance_index] if return_points: closest_point_self = design3d.Point3D(closest_point_self[0], closest_point_self[1], closest_point_self[2]) closest_point_other = design3d.Point3D(closest_point_other[0], closest_point_other[1], closest_point_other[2]) return min_distance, closest_point_self, closest_point_other return min_distance
[docs] def get_edges_triangles(self): """ Compute lengths edges of triangles. :return: A 3D numpy array representing edges of triangles. The dimensions are n_triangles x 3 x 2, where each entry contains the start and end points of an edge. :rtype: np.ndarray """ edges = np.stack([self.triangles[:, [0, 1]], self.triangles[:, [0, 2]], self.triangles[:, [1, 2]]], axis=1) return edges
[docs] def compute_len_edges(self): """ Compute the lengths of edges for each triangle in the mesh. :return: Lengths of edges (3 edges per triangles) of dimensions n_simplices x 3 and edges of dimensions n_simplices x 3 x 2 :rtype: tuple[np.ndarray, np.ndarray] """ edges = self.get_edges_triangles() indexed_points = self.vertices[edges] vectors = indexed_points[..., 0, :] - indexed_points[..., 1, :] return np.linalg.norm(vectors, axis=-1), edges
[docs] def get_mesh_border(self): """ Retrieve the topological border of a triangle mesh. This function identifies and returns the edges that belong to only one triangle, effectively representing the border of the mesh. :return: A tuple of two numpy arrays. The first array contains the unique border edges, and the second array includes all edges of the mesh. :rtype: tuple[np.ndarray, np.ndarray] """ edges = self.get_edges_triangles().reshape((-1, 2)) unique_edges, counts = np.unique(np.sort(edges, axis=1), axis=0, return_counts=True) border_edges = unique_edges[counts == 1] return border_edges, edges
[docs] def remove_large_triangles(self, threshold_edge_length: float) -> "Mesh3D": """ Remove triangles from the mesh whose edge lengths exceed the specified threshold. :param threshold_edge_length: The maximum allowed edge length for a triangle to remain in the mesh. :type threshold_edge_length: float :return: A new Mesh3D instance with large triangles removed. :rtype: Mesh3D """ # Compute the lengths of all edges in the mesh edge_lengths, _ = self.compute_len_edges() # Find triangles where all edges are below the threshold valid_triangles = np.all(edge_lengths < threshold_edge_length, axis=1) # Keep only the triangles that are valid return Mesh3D(self.vertices, self.triangles[valid_triangles])
[docs] def decimate( self, target_count: int, update_rate: int = 5, aggressiveness: float = 7.0, max_iterations: int = 100, verbose: bool = False, lossless: bool = False, threshold_lossless: float = 1e-3, alpha: float = 1e-9, k: int = 3, preserve_border: bool = True, ) -> "Mesh3D": """ Decimate the Mesh3D, and return it as a new instance. Vertices of the mesh should be merged (and maybe rounded) for efficient decimation. Note: threshold = alpha * pow(iteration + k, aggressiveness) :param target_count: Target number of triangles. Not used if `lossless` is True. :type target_count: int :param update_rate: Number of iterations between each update. If `lossless` flag is set to True, rate is 1. :type update_rate: int :param aggressiveness: Parameter controlling the growth rate of the threshold at each iteration when `lossless` is False. :type aggressiveness: float :param max_iterations: Maximal number of iterations. :type max_iterations: int :param verbose: Control verbosity. :type verbose: bool :param lossless: Use the lossless simplification method. :type lossless: bool :param threshold_lossless: Maximal error after which a vertex is not deleted. Only for `lossless` method. :type threshold_lossless: float :param alpha: Parameter for controlling the threshold growth. :type alpha: float :param k: Parameter for controlling the threshold growth. :type k: int :param preserve_border: Flag for preserving vertices on open border. :type preserve_border: bool :return: The decimated mesh. :rtype: Mesh3D """ # pylint: disable=too-many-arguments simplifier = pyfqmr.Simplify() simplifier.setMesh(self.vertices, self.triangles) simplifier.simplify_mesh( target_count=target_count, update_rate=update_rate, aggressiveness=aggressiveness, max_iterations=max_iterations, verbose=verbose, lossless=lossless, threshold_lossless=threshold_lossless, alpha=alpha, K=k, preserve_border=preserve_border, ) vertices, triangles, _ = simplifier.getMesh() return self.__class__(vertices, triangles)
@property def faces(self): """ Get the mesh faces as Triangle3D objects. :return: The triangles comosing the mesh. :rtype: list[Triangle3D] """ if not self._faces: self._faces = self.to_triangles3d() return self._faces # IMPORT
[docs] @classmethod def from_trimesh(cls, trimesh_: Trimesh) -> "Mesh3D": """ Create a 3D mesh from a Trimesh object. :param trimesh_: A Trimesh object representing the 3D mesh. :type trimesh_: Trimesh :return: A new 3D mesh instance. :rtype: Mesh3D """ return cls(trimesh_.vertices, trimesh_.faces)
[docs] @classmethod def from_trimesh_scene(cls, trimesh_scene: trimesh.Scene, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from a Trimesh Scene. :param trimesh_scene: A Trimesh Scene containing multiple geometry objects. :type trimesh_scene: trimesh.Scene :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ return cls.from_meshes(cls.trimesh_scene_to_meshes(trimesh_scene, scale_factor))
[docs] @classmethod def trimesh_scene_to_meshes(cls, trimesh_scene: trimesh.Scene, scale_factor: float = 0.001) -> List["Mesh3D"]: """ Create a 3D mesh from a Trimesh Scene. :param trimesh_scene: A Trimesh Scene containing multiple geometry objects. :type trimesh_scene: trimesh.Scene :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A list of new 3D mesh instance. :rtype: list[Mesh3D] """ meshes = [] for trimesh_ in trimesh_scene.geometry.values(): meshes.append(cls.from_trimesh(trimesh_).resize(scale_factor)) return meshes
[docs] @classmethod def from_stl_file(cls, filepath: str, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an STL file. :param filepath: The path to the STL file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ return cls.from_trimesh(trimesh.load(filepath, "stl")).resize(scale_factor)
[docs] @classmethod def from_stl_stream(cls, stream, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an STL stream. :param stream: A binary stream containing STL data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ stream.seek(0) return cls.from_trimesh(trimesh.load(stream, "stl")).resize(scale_factor)
[docs] @classmethod def from_obj_file(cls, filepath: str, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an OBJ file. :param filepath: The path to the OBJ file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ return cls.from_trimesh(trimesh.load(filepath, "obj")).resize(scale_factor)
[docs] @classmethod def from_obj_stream(cls, stream, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an OBJ stream. :param stream: A binary stream containing OBJ data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ stream.seek(0) return cls.from_trimesh(trimesh.load(stream, "obj")).resize(scale_factor)
[docs] @classmethod def from_ply_file(cls, filepath: str, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an PLY file. :param filepath: The path to the PLY file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ return cls.from_trimesh(trimesh.load(filepath, "ply")).resize(scale_factor)
[docs] @classmethod def from_ply_stream(cls, stream, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an PLY stream. :param stream: A binary stream containing PLY data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ stream.seek(0) return cls.from_trimesh(trimesh.load(stream, "ply")).resize(scale_factor)
[docs] @classmethod def from_off_file(cls, filepath: str, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an OFF file. :param filepath: The path to the OFF file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ return cls.from_trimesh(trimesh.load(filepath, "off")).resize(scale_factor)
[docs] @classmethod def from_off_stream(cls, stream, scale_factor: float = 0.001) -> "Mesh3D": """ Create a 3D mesh from an OFF stream. :param stream: A binary stream containing OFF data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :return: A new 3D mesh instance. :rtype: Mesh3D """ stream.seek(0) return cls.from_trimesh(trimesh.load(stream, "off")).resize(scale_factor)
[docs] @classmethod def from_3mf_file( cls, filepath: str, scale_factor: float = 0.001, merge_meshes: bool = True ) -> Union["Mesh3D", List["Mesh3D"]]: """ Create a 3D mesh from an 3MF file. :param filepath: The path to the 3MF file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :param merge_meshes: A flag to choose to merge all the 3mf meshes in one, or return a list of meshes. :type merge_meshes: bool :return: A new 3D mesh instance. :rtype: Mesh3D """ if merge_meshes: return cls.from_trimesh_scene(trimesh.load(filepath, "3mf"), scale_factor) return cls.trimesh_scene_to_meshes(trimesh.load(filepath, "3mf"), scale_factor)
[docs] @classmethod def from_3mf_stream( cls, stream, scale_factor: float = 0.001, merge_meshes: bool = True ) -> Union["Mesh3D", List["Mesh3D"]]: """ Create a 3D mesh from an 3MF stream. :param stream: A binary stream containing 3MF data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 0.001). :type scale_factor: float, optional :param merge_meshes: A flag to choose to merge all the 3mf meshes in one, or return a list of meshes. :type merge_meshes: bool :return: A new 3D mesh instance. :rtype: Mesh3D """ stream.seek(0) if merge_meshes: return cls.from_trimesh_scene(trimesh.load(stream, "3mf"), scale_factor) return cls.trimesh_scene_to_meshes(trimesh.load(stream, "3mf"), scale_factor)
# EXPORT
[docs] def triangular_faces(self): """ Export the mesh faces as Triangle3D objects. :return: The triangles comosing the mesh. :rtype: list[Triangle3D] """ warnings.warn("Deprecated: use to_triangles3d instead.", DeprecationWarning) return self.to_triangles3d()
[docs] def to_triangles3d(self): """ Export the mesh faces as Triangle3D objects. :return: The triangles comosing the mesh. :rtype: list[Triangle3D] """ # pylint: disable=import-outside-toplevel, cyclic-import from design3d.faces import Triangle3D triangles3d = [] for vertex1, vertex2, vertex3 in self.remove_degenerate_triangles(tol=1e-6).triangles_vertices(): point1 = design3d.Point3D(*vertex1) point2 = design3d.Point3D(*vertex2) point3 = design3d.Point3D(*vertex3) triangles3d.append(Triangle3D(point1, point2, point3)) return triangles3d
[docs] def to_closed_shell(self): """ Convert the Mesh3D object to a closed triangle shell. :return: A closed triangle shell representation of the Mesh3D object. :rtype: ClosedTriangleShell3D """ warnings.warn( """ ClosedTriangleShell3D is not an efficient object to deal with mesh data. Try to stick to Mesh3D or Trimesh object if possible. """ ) # pylint: disable=import-outside-toplevel, cyclic-import from design3d.shells import ClosedTriangleShell3D return ClosedTriangleShell3D(faces=self.to_triangles3d(), name=self.name)
[docs] def to_open_shell(self): """ Convert the Mesh3D object to an open triangle shell. :return: An open triangle shell representation of the Mesh3D object. :rtype: OpenTriangleShell3D """ warnings.warn( """ OpenTriangleShell3D is not an efficient object to deal with mesh data. Try to stick to Mesh3D or Trimesh object if possible. """ ) # pylint: disable=import-outside-toplevel, cyclic-import from design3d.shells import OpenTriangleShell3D return OpenTriangleShell3D(faces=self.to_triangles3d(), name=self.name)
[docs] def to_trimesh(self): """ Convert the Mesh3D instance to a Trimesh object. :return: A Trimesh object representing the 3D mesh. :rtype: Trimesh """ return Trimesh(self.vertices, self.triangles)
# SAVING
[docs] def save_to_stl_file(self, filepath: str, scale_factor: float = 1000.0): """ Save the 3D mesh to an STL file. :param filepath: The path to the STL file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ if not filepath.lower().endswith(".stl"): filepath += ".stl" print(f"Changing name to {filepath}") with open(filepath, "wb") as file: self.save_to_stl_stream(file, scale_factor=scale_factor)
[docs] def save_to_stl_stream(self, stream, scale_factor: float = 1000.0): """ Save the 3D mesh to an STL stream. :param stream: A binary stream to write the STL data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ self.resize(scale_factor).to_trimesh().export(stream, "stl")
[docs] def save_to_obj_file(self, filepath: str, scale_factor: float = 1000.0): """ Save the 3D mesh to an OBJ file. :param filepath: The path to the OBJ file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ if not filepath.lower().endswith(".obj"): filepath += ".obj" print(f"Changing name to {filepath}") with open(filepath, "wb") as file: self.save_to_obj_stream(file, scale_factor=scale_factor)
[docs] def save_to_obj_stream(self, stream, scale_factor: float = 1000.0): """ Save the 3D mesh to an OBJ stream. :param stream: A binary stream to write the OBJ data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ self.resize(scale_factor).to_trimesh().export(stream, "obj")
[docs] def save_to_ply_file(self, filepath: str, scale_factor: float = 1000.0): """ Save the 3D mesh to a PLY file. :param filepath: The path to the PLY file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ if not filepath.lower().endswith(".ply"): filepath += ".ply" print(f"Changing name to {filepath}") with open(filepath, "wb") as file: self.save_to_ply_stream(file, scale_factor=scale_factor)
[docs] def save_to_ply_stream(self, stream, scale_factor: float = 1000.0): """ Save the 3D mesh to a PLY stream. :param stream: A binary stream to write the PLY data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ self.resize(scale_factor).to_trimesh().export(stream, "ply")
[docs] def save_to_off_file(self, filepath: str, scale_factor: float = 1000.0): """ Save the 3D mesh to an OFF file. :param filepath: The path to the OFF file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ if not filepath.lower().endswith(".off"): filepath += ".off" print(f"Changing name to {filepath}") with open(filepath, "wb") as file: self.save_to_off_stream(file, scale_factor=scale_factor)
[docs] def save_to_off_stream(self, stream, scale_factor: float = 1000.0): """ Save the 3D mesh to an OFF stream. :param stream: A binary stream to write the OFF data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ self.resize(scale_factor).to_trimesh().export(stream, "off")
[docs] def save_to_3mf_file(self, filepath: str, scale_factor: float = 1000.0): """ Save the 3D mesh to a 3MF file. :param filepath: The path to the 3MF file. :type filepath: str :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ if not filepath.lower().endswith(".3mf"): filepath += ".3mf" print(f"Changing name to {filepath}") with open(filepath, "wb") as file: self.save_to_3mf_stream(file, scale_factor=scale_factor)
[docs] def save_to_3mf_stream(self, stream, scale_factor: float = 1000.0): """ Save the 3D mesh to a 3MF stream. :param stream: A binary stream to write the 3MF data. :type stream: BinaryFile :param scale_factor: The scale factor to apply to the mesh (default is 1000.0). :type scale_factor: float, optional """ self.resize(scale_factor).to_trimesh().export(stream, "3mf")