capytaine 2.3__cp310-cp310-win_amd64.whl → 3.0.0a1__cp310-cp310-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capytaine/__about__.py +7 -2
- capytaine/__init__.py +11 -15
- capytaine/bem/engines.py +234 -354
- capytaine/bem/problems_and_results.py +30 -21
- capytaine/bem/solver.py +205 -81
- capytaine/bodies/bodies.py +279 -862
- capytaine/bodies/dofs.py +136 -9
- capytaine/bodies/hydrostatics.py +540 -0
- capytaine/bodies/multibodies.py +216 -0
- capytaine/green_functions/{libs/Delhommeau_float32.cp310-win_amd64.dll.a → Delhommeau_float32.cp310-win_amd64.dll.a} +0 -0
- capytaine/green_functions/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
- capytaine/green_functions/{libs/Delhommeau_float64.cp310-win_amd64.dll.a → Delhommeau_float64.cp310-win_amd64.dll.a} +0 -0
- capytaine/green_functions/Delhommeau_float64.cp310-win_amd64.pyd +0 -0
- capytaine/green_functions/abstract_green_function.py +2 -2
- capytaine/green_functions/delhommeau.py +50 -31
- capytaine/green_functions/hams.py +19 -13
- capytaine/io/legacy.py +3 -103
- capytaine/io/xarray.py +15 -10
- capytaine/meshes/__init__.py +2 -6
- capytaine/meshes/abstract_meshes.py +375 -0
- capytaine/meshes/clean.py +302 -0
- capytaine/meshes/clip.py +347 -0
- capytaine/meshes/export.py +89 -0
- capytaine/meshes/geometry.py +244 -394
- capytaine/meshes/io.py +433 -0
- capytaine/meshes/meshes.py +621 -676
- capytaine/meshes/predefined/cylinders.py +22 -56
- capytaine/meshes/predefined/rectangles.py +26 -85
- capytaine/meshes/predefined/spheres.py +4 -11
- capytaine/meshes/quality.py +118 -407
- capytaine/meshes/surface_integrals.py +48 -29
- capytaine/meshes/symmetric_meshes.py +641 -0
- capytaine/meshes/visualization.py +353 -0
- capytaine/post_pro/free_surfaces.py +1 -4
- capytaine/post_pro/kochin.py +10 -10
- capytaine/tools/block_circulant_matrices.py +275 -0
- capytaine/tools/lists_of_points.py +2 -2
- capytaine/tools/memory_monitor.py +45 -0
- capytaine/tools/symbolic_multiplication.py +31 -5
- capytaine/tools/timer.py +68 -42
- capytaine-3.0.0a1.dist-info/DELVEWHEEL +2 -0
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/METADATA +8 -14
- capytaine-3.0.0a1.dist-info/RECORD +70 -0
- capytaine/bodies/predefined/__init__.py +0 -6
- capytaine/bodies/predefined/cylinders.py +0 -151
- capytaine/bodies/predefined/rectangles.py +0 -111
- capytaine/bodies/predefined/spheres.py +0 -70
- capytaine/green_functions/FinGreen3D/.gitignore +0 -1
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +0 -3589
- capytaine/green_functions/FinGreen3D/LICENSE +0 -165
- capytaine/green_functions/FinGreen3D/Makefile +0 -16
- capytaine/green_functions/FinGreen3D/README.md +0 -24
- capytaine/green_functions/FinGreen3D/test_program.f90 +0 -39
- capytaine/green_functions/LiangWuNoblesse/.gitignore +0 -1
- capytaine/green_functions/LiangWuNoblesse/LICENSE +0 -504
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +0 -751
- capytaine/green_functions/LiangWuNoblesse/Makefile +0 -18
- capytaine/green_functions/LiangWuNoblesse/README.md +0 -2
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +0 -28
- capytaine/green_functions/libs/Delhommeau_float32.cp310-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cp310-win_amd64.pyd +0 -0
- capytaine/green_functions/libs/__init__.py +0 -0
- capytaine/io/mesh_loaders.py +0 -1086
- capytaine/io/mesh_writers.py +0 -692
- capytaine/io/meshio.py +0 -38
- capytaine/matrices/__init__.py +0 -16
- capytaine/matrices/block.py +0 -592
- capytaine/matrices/block_toeplitz.py +0 -325
- capytaine/matrices/builders.py +0 -89
- capytaine/matrices/linear_solvers.py +0 -232
- capytaine/matrices/low_rank.py +0 -395
- capytaine/meshes/clipper.py +0 -465
- capytaine/meshes/collections.py +0 -334
- capytaine/meshes/mesh_like_protocol.py +0 -37
- capytaine/meshes/properties.py +0 -276
- capytaine/meshes/quadratures.py +0 -80
- capytaine/meshes/symmetric.py +0 -392
- capytaine/tools/lru_cache.py +0 -49
- capytaine/ui/vtk/__init__.py +0 -3
- capytaine/ui/vtk/animation.py +0 -329
- capytaine/ui/vtk/body_viewer.py +0 -28
- capytaine/ui/vtk/helpers.py +0 -82
- capytaine/ui/vtk/mesh_viewer.py +0 -461
- capytaine-2.3.dist-info/DELVEWHEEL +0 -2
- capytaine-2.3.dist-info/RECORD +0 -97
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/LICENSE +0 -0
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/WHEEL +0 -0
- {capytaine-2.3.dist-info → capytaine-3.0.0a1.dist-info}/entry_points.txt +0 -0
capytaine/meshes/meshes.py
CHANGED
|
@@ -1,81 +1,186 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
""
|
|
4
|
-
#
|
|
5
|
-
#
|
|
1
|
+
# Copyright 2025 Mews Labs
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
6
16
|
|
|
7
17
|
import logging
|
|
8
|
-
from
|
|
18
|
+
from functools import cached_property
|
|
19
|
+
from typing import List, Union, Tuple, Dict, Optional, Literal
|
|
9
20
|
|
|
10
21
|
import numpy as np
|
|
11
22
|
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
from .abstract_meshes import AbstractMesh
|
|
24
|
+
from .geometry import (
|
|
25
|
+
compute_faces_areas,
|
|
26
|
+
compute_faces_centers,
|
|
27
|
+
compute_faces_normals,
|
|
28
|
+
compute_faces_radii,
|
|
29
|
+
compute_gauss_legendre_2_quadrature,
|
|
30
|
+
get_vertices_face,
|
|
31
|
+
)
|
|
32
|
+
from .clip import clip_faces
|
|
33
|
+
from .clean import clean_mesh
|
|
34
|
+
from .export import export_mesh
|
|
35
|
+
from .quality import _is_valid, check_mesh_quality
|
|
36
|
+
from .visualization import show_3d
|
|
19
37
|
|
|
20
38
|
LOG = logging.getLogger(__name__)
|
|
21
39
|
|
|
22
40
|
|
|
23
|
-
class Mesh(
|
|
24
|
-
"""
|
|
41
|
+
class Mesh(AbstractMesh):
|
|
42
|
+
"""Mesh class for representing and manipulating 3D surface meshes.
|
|
25
43
|
|
|
26
44
|
Parameters
|
|
27
45
|
----------
|
|
28
|
-
vertices :
|
|
29
|
-
Array of mesh vertices coordinates
|
|
30
|
-
coordinates
|
|
31
|
-
faces :
|
|
32
|
-
|
|
33
|
-
vertices that form
|
|
34
|
-
|
|
46
|
+
vertices : np.ndarray, optional
|
|
47
|
+
Array of mesh vertices coordinates with shape (n_vertices, 3).
|
|
48
|
+
Each row represents one vertex's (x, y, z) coordinates.
|
|
49
|
+
faces : List[List[int]] or np.ndarray, optional
|
|
50
|
+
Array of mesh connectivities for panels. Each row contains indices
|
|
51
|
+
of vertices that form a face (triangles or quads).
|
|
52
|
+
faces_metadata: Dict[str, np.ndarray]
|
|
53
|
+
Some arrays with the same first dimension (should be the number of faces)
|
|
54
|
+
storing some fields defined on all the faces of the mesh.
|
|
35
55
|
name : str, optional
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
_ids = count(0) # A counter for automatic naming of new meshes.
|
|
56
|
+
Optional name for the mesh instance.
|
|
57
|
+
auto_clean : bool, optional
|
|
58
|
+
Whether to automatically clean the mesh upon initialization. Defaults to True.
|
|
59
|
+
auto_check : bool, optional
|
|
60
|
+
Whether to automatically check mesh quality upon initialization. Defaults to True.
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
Attributes
|
|
63
|
+
----------
|
|
64
|
+
vertices : np.ndarray
|
|
65
|
+
Array of vertex coordinates with shape (n_vertices, 3).
|
|
66
|
+
name : str or None
|
|
67
|
+
Name of the mesh instance.
|
|
68
|
+
"""
|
|
45
69
|
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
vertices: np.ndarray = None,
|
|
73
|
+
faces: Union[List[List[int]], np.ndarray] = None,
|
|
74
|
+
*,
|
|
75
|
+
faces_metadata: Optional[Dict[str, np.ndarray]] = None,
|
|
76
|
+
quadrature_method: Optional[str] = None,
|
|
77
|
+
name: Optional[str] = None,
|
|
78
|
+
auto_clean: bool = True,
|
|
79
|
+
auto_check: bool = True,
|
|
80
|
+
):
|
|
81
|
+
# --- Vertices: always a NumPy array with shape (n,3) ---
|
|
82
|
+
if vertices is None:
|
|
83
|
+
self.vertices = np.empty((0, 3), dtype=np.float64)
|
|
84
|
+
else:
|
|
85
|
+
self.vertices = np.array(vertices, dtype=np.float64)
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
# --- Faces: process using helper method ---
|
|
88
|
+
self._faces: List[List[int]] = self._process_faces(faces)
|
|
51
89
|
|
|
52
|
-
if
|
|
53
|
-
self.
|
|
90
|
+
if faces_metadata is None:
|
|
91
|
+
self.faces_metadata = {}
|
|
54
92
|
else:
|
|
55
|
-
self.
|
|
93
|
+
self.faces_metadata = {k: np.asarray(faces_metadata[k]) for k in faces_metadata}
|
|
56
94
|
|
|
57
|
-
self.
|
|
58
|
-
|
|
59
|
-
self.faces = faces # Not a direct assignment, goes through the setter method below.
|
|
95
|
+
for m in self.faces_metadata:
|
|
96
|
+
assert self.faces_metadata[m].shape[0] == len(self._faces)
|
|
60
97
|
|
|
61
|
-
|
|
98
|
+
# Optional name
|
|
99
|
+
self.name = str(name) if name is not None else None
|
|
62
100
|
|
|
63
101
|
self.quadrature_method = quadrature_method
|
|
64
102
|
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
# Cleaning/quality (unless mesh is completely empty)
|
|
104
|
+
if not (len(self.vertices) == 0 and len(self._faces) == 0):
|
|
105
|
+
if not _is_valid(vertices, faces):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"Mesh is invalid: faces contain out-of-bounds or negative indices."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if np.any(np.isnan(vertices)):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"Mesh is invalid: vertices coordinates contains NaN values."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if auto_clean:
|
|
116
|
+
self.vertices, self._faces, self.faces_metadata = clean_mesh(
|
|
117
|
+
self.vertices, self._faces, self.faces_metadata, max_iter=5, tol=1e-8
|
|
118
|
+
)
|
|
119
|
+
LOG.debug("Cleaned %s", str(self))
|
|
120
|
+
|
|
121
|
+
if auto_check:
|
|
122
|
+
check_mesh_quality(self)
|
|
123
|
+
LOG.debug("Checked quality of %s", str(self))
|
|
124
|
+
|
|
125
|
+
LOG.debug("New %s", str(self))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
## MAIN METRICS AND DISPLAY
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def nb_vertices(self) -> int:
|
|
132
|
+
"""Number of vertices in the mesh."""
|
|
133
|
+
return len(self.vertices)
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def nb_faces(self) -> int:
|
|
137
|
+
"""Number of faces in the mesh."""
|
|
138
|
+
return len(self._faces)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def nb_triangles(self) -> int:
|
|
142
|
+
"""Number of triangular faces (3-vertex) in the mesh."""
|
|
143
|
+
return sum(1 for f in self._faces if len(f) == 3)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def nb_quads(self) -> int:
|
|
147
|
+
"""Number of quadrilateral faces (4-vertex) in the mesh."""
|
|
148
|
+
return sum(1 for f in self._faces if len(f) == 4)
|
|
149
|
+
|
|
150
|
+
def summary(self):
|
|
151
|
+
"""Print a summary of the mesh properties.
|
|
67
152
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
153
|
+
Notes
|
|
154
|
+
-----
|
|
155
|
+
Displays the mesh name, vertex count, face count, and bounding box.
|
|
156
|
+
"""
|
|
157
|
+
print("Mesh Summary")
|
|
158
|
+
print(f" Name : {self.name}")
|
|
159
|
+
print(f" Vertices count : {self.nb_vertices}")
|
|
160
|
+
print(f" Faces count : {self.nb_faces}")
|
|
161
|
+
print(
|
|
162
|
+
f" Bounding box : {self.vertices.min(axis=0)} to {self.vertices.max(axis=0)}"
|
|
163
|
+
)
|
|
164
|
+
print(f" Metadata keys : {self.faces_metadata.keys()}")
|
|
165
|
+
print(f" Quadrature : {self.quadrature_method}")
|
|
166
|
+
|
|
167
|
+
def __str__(self) -> str:
|
|
168
|
+
return (
|
|
169
|
+
f"Mesh(vertices=[[... {self.nb_vertices} vertices ...]], "
|
|
170
|
+
+ f"faces=[[... {self.nb_faces} faces ...]]"
|
|
171
|
+
+ (f", quadrature_method={repr(self.quadrature_method)}" if self.quadrature_method is not None else "")
|
|
172
|
+
+ (f", name={repr(self.name)}" if self.name is not None else "")
|
|
173
|
+
+ ")"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def __short_str__(self) -> str:
|
|
177
|
+
if self.name is not None:
|
|
178
|
+
return f"Mesh(..., name={repr(self.name)})"
|
|
179
|
+
else:
|
|
180
|
+
return "Mesh(...)"
|
|
71
181
|
|
|
72
|
-
def __repr__(self):
|
|
73
|
-
|
|
74
|
-
# vert_str = np.array_repr(self.vertices).replace('\n', '\n' + (shift + 9)*' ')
|
|
75
|
-
# faces_str = np.array_repr(self.faces).replace('\n', '\n' + (shift + 6)*' ')
|
|
76
|
-
# return f"{self.__class__.__name__}(\n{' '*shift}vertices={vert_str},\n{' '*shift}faces={faces_str}\n{' '*shift}name=\"{self.name}\"\n)"
|
|
77
|
-
return (f"{self.__class__.__name__}(vertices=[[... {self.nb_vertices} vertices ...]], "
|
|
78
|
-
f"faces=[[... {self.nb_faces} faces ...]], name=\"{self.name}\")")
|
|
182
|
+
def __repr__(self) -> str:
|
|
183
|
+
return self.__str__()
|
|
79
184
|
|
|
80
185
|
def _repr_pretty_(self, p, cycle):
|
|
81
186
|
p.text(self.__str__())
|
|
@@ -89,677 +194,438 @@ class Mesh(ClippableMixin, SurfaceIntegralsMixin, Abstract3DObject):
|
|
|
89
194
|
return "[[... {} {} ...]]".format(self.n, self.kind)
|
|
90
195
|
yield "vertices", CustomRepr(self.nb_vertices, "vertices")
|
|
91
196
|
yield "faces", CustomRepr(self.nb_faces, "faces")
|
|
197
|
+
yield "quadrature_method", self.quadrature_method
|
|
92
198
|
yield "name", self.name
|
|
93
199
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"""Get the number of vertices in the mesh."""
|
|
97
|
-
return self._vertices.shape[0]
|
|
200
|
+
def show(self, *, backend=None, **kwargs):
|
|
201
|
+
"""Visualize the mesh using the specified backend.
|
|
98
202
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
backend : str, optional
|
|
206
|
+
Visualization backend to use. Options are 'pyvista' or 'matplotlib'.
|
|
207
|
+
By default, try several until an installed one is found.
|
|
208
|
+
normal_vectors: bool, optional
|
|
209
|
+
If True, display normal vectors on each face.
|
|
210
|
+
**kwargs
|
|
211
|
+
Additional keyword arguments passed to the visualization backend.
|
|
212
|
+
See :mod:`~capytaine.meshes.visualization`
|
|
103
213
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"Vertices of a mesh should be provided as a sequence of 3-ple."
|
|
109
|
-
self.__internals__.clear()
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
object
|
|
217
|
+
Visualization object returned by the backend (e.g., matplotlib figure).
|
|
110
218
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
219
|
+
Raises
|
|
220
|
+
------
|
|
221
|
+
NotImplementedError
|
|
222
|
+
If the specified backend is not supported.
|
|
223
|
+
"""
|
|
224
|
+
return show_3d(self, backend=backend, **kwargs)
|
|
115
225
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def faces(self, faces):
|
|
123
|
-
faces = np.array(faces, dtype=int)
|
|
124
|
-
assert np.all(faces >= 0), \
|
|
125
|
-
"Faces of a mesh should be provided as positive integers (ids of vertices)"
|
|
126
|
-
assert faces.shape[1] == 4, \
|
|
127
|
-
"Faces of a mesh should be provided as a sequence of 4-ple."
|
|
128
|
-
assert len(faces) == 0 or faces.max()+1 <= self.nb_vertices, \
|
|
129
|
-
"The array of faces should only reference vertices that are in the mesh."
|
|
130
|
-
self._faces = faces
|
|
131
|
-
self.__internals__.clear()
|
|
132
|
-
|
|
133
|
-
def copy(self, name=None) -> 'Mesh':
|
|
134
|
-
"""Get a copy of the current mesh instance.
|
|
226
|
+
|
|
227
|
+
## INITIALISATION
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _has_leading_count_column(arr: np.ndarray) -> bool:
|
|
231
|
+
"""Check if a 2D array has a leading column containing vertex counts.
|
|
135
232
|
|
|
136
233
|
Parameters
|
|
137
234
|
----------
|
|
138
|
-
|
|
139
|
-
|
|
235
|
+
arr : np.ndarray
|
|
236
|
+
2D array of face data
|
|
140
237
|
|
|
141
238
|
Returns
|
|
142
239
|
-------
|
|
143
|
-
|
|
144
|
-
|
|
240
|
+
bool
|
|
241
|
+
True if the first column appears to be vertex counts
|
|
145
242
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def path_to_leaf(self):
|
|
161
|
-
"""Dummy method to be generalized for collection of meshes."""
|
|
162
|
-
return [[]]
|
|
163
|
-
|
|
164
|
-
def to_meshmagick(self):
|
|
165
|
-
"""Convert the Mesh object as a Mesh object from meshmagick.
|
|
166
|
-
Mostly for debugging."""
|
|
167
|
-
from meshmagick.mesh import Mesh
|
|
168
|
-
meshmagick_mesh = Mesh(self.vertices, self.faces, name=self.name)
|
|
169
|
-
meshmagick_mesh.heal_mesh()
|
|
170
|
-
return meshmagick_mesh
|
|
171
|
-
|
|
172
|
-
##################
|
|
173
|
-
# Extract face #
|
|
174
|
-
##################
|
|
175
|
-
|
|
176
|
-
def get_face(self, face_id):
|
|
177
|
-
"""Get the face described by its vertices connectivity.
|
|
243
|
+
if arr.ndim != 2 or arr.shape[1] <= 3:
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
expected_count = arr.shape[1] - 1
|
|
247
|
+
for row in arr:
|
|
248
|
+
# Check if first value could be a vertex count (3 or 4)
|
|
249
|
+
# and if it matches the expected count (total cols - 1)
|
|
250
|
+
if row[0] != expected_count and row[0] not in [3, 4]:
|
|
251
|
+
return False
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
def _process_faces(self, faces: List[List[int]] | np.ndarray) -> List[List[int]]:
|
|
255
|
+
"""Process the faces input for the Mesh class.
|
|
178
256
|
|
|
179
257
|
Parameters
|
|
180
258
|
----------
|
|
181
|
-
|
|
182
|
-
|
|
259
|
+
faces : np.ndarray or list
|
|
260
|
+
The faces data to process.
|
|
183
261
|
|
|
184
262
|
Returns
|
|
185
263
|
-------
|
|
186
|
-
|
|
187
|
-
|
|
264
|
+
list
|
|
265
|
+
A list of faces, where each face is a list of vertex indices.
|
|
266
|
+
|
|
267
|
+
Notes
|
|
268
|
+
-----
|
|
269
|
+
If the input is a 2D array with a leading column containing face vertex counts
|
|
270
|
+
(e.g., [[3, v1, v2, v3], [4, v1, v2, v3, v4]]), the count column will be
|
|
271
|
+
automatically stripped. This is checked per-row to support mixed triangle/quad meshes.
|
|
188
272
|
"""
|
|
189
|
-
if
|
|
190
|
-
return
|
|
273
|
+
if faces is None:
|
|
274
|
+
return []
|
|
275
|
+
elif isinstance(faces, list):
|
|
276
|
+
# assume it's already a list of lists of ints
|
|
277
|
+
return [list(f) for f in faces]
|
|
191
278
|
else:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
279
|
+
# fallback: convert array → nested list
|
|
280
|
+
arr = np.asarray(faces, dtype=int)
|
|
281
|
+
|
|
282
|
+
# Detect & strip a leading "count" column if present
|
|
283
|
+
if self._has_leading_count_column(arr):
|
|
284
|
+
arr = arr[:, 1:]
|
|
285
|
+
|
|
286
|
+
return arr.tolist()
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def from_list_of_faces(
|
|
290
|
+
cls,
|
|
291
|
+
list_faces,
|
|
292
|
+
*,
|
|
293
|
+
quadrature_method=None,
|
|
294
|
+
faces_metadata=None,
|
|
295
|
+
name=None,
|
|
296
|
+
auto_clean=True,
|
|
297
|
+
auto_check=True
|
|
298
|
+
) -> "Mesh":
|
|
205
299
|
"""
|
|
206
|
-
|
|
300
|
+
Create a Mesh instance from a list of faces defined by vertex coordinates.
|
|
207
301
|
|
|
208
302
|
Parameters
|
|
209
303
|
----------
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
304
|
+
list_faces : list of list of list of float
|
|
305
|
+
Each face is defined by a list of 3D coordinates. For example:
|
|
306
|
+
[
|
|
307
|
+
[[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]],
|
|
308
|
+
[[x4, y4, z4], [x5, y5, z5], [x6, y6, z6]]
|
|
309
|
+
]
|
|
310
|
+
faces_metadata: Optional[Dict[str, np.ndarray]]
|
|
311
|
+
name: str, optional
|
|
312
|
+
A name for the new mesh.
|
|
313
|
+
auto_clean : bool, optional
|
|
314
|
+
Whether to automatically clean the mesh upon initialization. Defaults to True.
|
|
315
|
+
auto_check : bool, optional
|
|
316
|
+
Whether to automatically check mesh quality upon initialization. Defaults to True.
|
|
216
317
|
|
|
217
318
|
Returns
|
|
218
319
|
-------
|
|
219
320
|
Mesh
|
|
220
|
-
|
|
321
|
+
An instance of Mesh with:
|
|
322
|
+
- unique vertices extracted from the input
|
|
323
|
+
- faces defined as indices into the vertex array
|
|
221
324
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
325
|
+
unique_vertices = []
|
|
326
|
+
vertices_map = {}
|
|
327
|
+
indexed_faces = []
|
|
328
|
+
|
|
329
|
+
for face in list_faces:
|
|
330
|
+
indexed_face = []
|
|
331
|
+
for coord in face:
|
|
332
|
+
key = tuple(coord)
|
|
333
|
+
if key not in vertices_map:
|
|
334
|
+
vertices_map[key] = len(unique_vertices)
|
|
335
|
+
unique_vertices.append(coord)
|
|
336
|
+
indexed_face.append(vertices_map[key])
|
|
337
|
+
indexed_faces.append(indexed_face)
|
|
338
|
+
|
|
339
|
+
return cls(
|
|
340
|
+
vertices=np.array(unique_vertices),
|
|
341
|
+
faces=indexed_faces,
|
|
342
|
+
quadrature_method=quadrature_method,
|
|
343
|
+
faces_metadata=faces_metadata,
|
|
344
|
+
name=name
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def as_list_of_faces(self) -> List[List[List[float]]]:
|
|
348
|
+
"""
|
|
349
|
+
Convert the Mesh instance to a list of faces defined by vertex coordinates.
|
|
228
350
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
list of list of list of float
|
|
354
|
+
Each face is defined by a list of 3D coordinates. For example:
|
|
355
|
+
[
|
|
356
|
+
[[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]],
|
|
357
|
+
[[x4, y4, z4], [x5, y5, z5], [x6, y6, z6]]
|
|
358
|
+
]
|
|
359
|
+
"""
|
|
360
|
+
list_faces = []
|
|
361
|
+
for face in self._faces:
|
|
362
|
+
face_coords = [self.vertices[idx].tolist() for idx in face]
|
|
363
|
+
list_faces.append(face_coords)
|
|
364
|
+
if len(self.faces_metadata) > 0:
|
|
365
|
+
LOG.debug(f"Dropping metadata of {self} to export as list of faces.")
|
|
366
|
+
return list_faces
|
|
367
|
+
|
|
368
|
+
def as_array_of_faces(self) -> np.ndarray:
|
|
369
|
+
"""Similar to as_list_of_faces but returns an array of shape
|
|
370
|
+
(nb_faces, 3, 3) if only triangles, or (nb_faces, 4, 3) otherwise.
|
|
371
|
+
"""
|
|
372
|
+
array = self.vertices[self.faces[:, :], :]
|
|
373
|
+
if self.nb_quads == 0:
|
|
374
|
+
array = array[:, :3, :]
|
|
375
|
+
return array
|
|
233
376
|
|
|
234
|
-
|
|
235
|
-
|
|
377
|
+
def export(self, format, **kwargs):
|
|
378
|
+
return export_mesh(self, format, **kwargs)
|
|
236
379
|
|
|
237
|
-
|
|
380
|
+
## INTERFACE FOR BEM SOLVER
|
|
238
381
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
382
|
+
@cached_property
|
|
383
|
+
def faces_vertices_centers(self) -> np.ndarray:
|
|
384
|
+
"""Calculate the center of vertices that form the faces.
|
|
242
385
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
np.ndarray
|
|
389
|
+
Array of shape (n_faces, 3) containing the centroid of each face's vertices.
|
|
390
|
+
"""
|
|
391
|
+
centers_vertices = []
|
|
392
|
+
for face in self._faces:
|
|
393
|
+
if face[3] != face[2]:
|
|
394
|
+
a, b, c, d = get_vertices_face(face, self.vertices)
|
|
395
|
+
mean = (a + b + c + d) / 4
|
|
396
|
+
centers_vertices.append(mean)
|
|
246
397
|
else:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if return_index:
|
|
252
|
-
return extracted_mesh, id_v
|
|
253
|
-
else:
|
|
254
|
-
return extracted_mesh
|
|
255
|
-
|
|
256
|
-
def sliced_by_plane(self, plane: Plane):
|
|
257
|
-
from capytaine.meshes.collections import CollectionOfMeshes
|
|
258
|
-
faces_ids_on_one_side = np.where(plane.distance_to_point(self.faces_centers) < 0)[0]
|
|
259
|
-
if len(faces_ids_on_one_side) == 0 or len(faces_ids_on_one_side) == self.nb_faces:
|
|
260
|
-
return self.copy()
|
|
261
|
-
else:
|
|
262
|
-
mesh_part_1 = self.extract_faces(faces_ids_on_one_side)
|
|
263
|
-
mesh_part_2 = self.extract_faces(list(set(range(self.nb_faces)) - set(faces_ids_on_one_side)))
|
|
264
|
-
return CollectionOfMeshes([mesh_part_1, mesh_part_2],
|
|
265
|
-
name=f"{self.name}_splitted_by_{plane}")
|
|
398
|
+
a, b, c = get_vertices_face(face, self.vertices)
|
|
399
|
+
mean = (a + b + c) / 3
|
|
400
|
+
centers_vertices.append(mean)
|
|
401
|
+
return np.array(centers_vertices)
|
|
266
402
|
|
|
403
|
+
@cached_property
|
|
404
|
+
def faces_normals(self) -> np.ndarray:
|
|
405
|
+
"""Normal vectors for each face.
|
|
267
406
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"""(Non-weighted) center of mass of the nodes of the mesh."""
|
|
275
|
-
if 'center_of_mass_of_nodes' not in self.__internals__:
|
|
276
|
-
center_of_mass_of_nodes = np.mean(self.vertices, axis=0)
|
|
277
|
-
self.__internals__['center_of_mass_of_nodes'] = center_of_mass_of_nodes
|
|
278
|
-
return center_of_mass_of_nodes
|
|
279
|
-
return self.__internals__['center_of_mass_of_nodes']
|
|
280
|
-
|
|
281
|
-
@property
|
|
282
|
-
def diameter_of_nodes(self):
|
|
283
|
-
"""Maximum distance between two nodes of the mesh."""
|
|
284
|
-
if 'diameter_of_nodes' not in self.__internals__:
|
|
285
|
-
diameter_of_nodes = 2*np.max(
|
|
286
|
-
np.linalg.norm(self.vertices - self.center_of_mass_of_nodes, axis=-1)
|
|
287
|
-
)
|
|
288
|
-
self.__internals__['diameter_of_nodes'] = diameter_of_nodes
|
|
289
|
-
return diameter_of_nodes
|
|
290
|
-
return self.__internals__['diameter_of_nodes']
|
|
291
|
-
|
|
292
|
-
######################
|
|
293
|
-
# Faces properties #
|
|
294
|
-
######################
|
|
407
|
+
Returns
|
|
408
|
+
-------
|
|
409
|
+
np.ndarray
|
|
410
|
+
Array of shape (n_faces, 3) containing unit normal vectors.
|
|
411
|
+
"""
|
|
412
|
+
return compute_faces_normals(self.vertices, self._faces)
|
|
295
413
|
|
|
296
|
-
@
|
|
414
|
+
@cached_property
|
|
297
415
|
def faces_areas(self) -> np.ndarray:
|
|
298
|
-
"""
|
|
299
|
-
if 'faces_areas' not in self.__internals__:
|
|
300
|
-
self.__internals__.update(compute_faces_properties(self))
|
|
301
|
-
return self.__internals__['faces_areas']
|
|
416
|
+
"""Surface area of each face.
|
|
302
417
|
|
|
303
|
-
|
|
418
|
+
Returns
|
|
419
|
+
-------
|
|
420
|
+
np.ndarray
|
|
421
|
+
Array of shape (n_faces,) containing the area of each face.
|
|
422
|
+
"""
|
|
423
|
+
return compute_faces_areas(self.vertices, self._faces)
|
|
424
|
+
|
|
425
|
+
@cached_property
|
|
304
426
|
def faces_centers(self) -> np.ndarray:
|
|
305
|
-
"""
|
|
306
|
-
if 'faces_centers' not in self.__internals__:
|
|
307
|
-
self.__internals__.update(compute_faces_properties(self))
|
|
308
|
-
return self.__internals__['faces_centers']
|
|
427
|
+
"""Geometric centers of each face.
|
|
309
428
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return self.
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
np.ndarray
|
|
432
|
+
Array of shape (n_faces, 3) containing the center point of each face.
|
|
433
|
+
"""
|
|
434
|
+
return compute_faces_centers(self.vertices, self._faces)
|
|
316
435
|
|
|
317
|
-
@
|
|
436
|
+
@cached_property
|
|
318
437
|
def faces_radiuses(self) -> np.ndarray:
|
|
319
|
-
"""
|
|
320
|
-
if 'faces_radiuses' not in self.__internals__:
|
|
321
|
-
self.__internals__.update(compute_faces_properties(self))
|
|
322
|
-
return self.__internals__['faces_radiuses']
|
|
323
|
-
|
|
324
|
-
@property
|
|
325
|
-
def quadrature_points(self):
|
|
326
|
-
if 'quadrature' not in self.__internals__:
|
|
327
|
-
self.compute_quadrature(self.quadrature_method)
|
|
328
|
-
return self.__internals__['quadrature']
|
|
329
|
-
|
|
330
|
-
def compute_quadrature(self, method):
|
|
331
|
-
self.heal_triangles()
|
|
332
|
-
all_faces = self.vertices[self.faces[:, :], :]
|
|
333
|
-
if method is None:
|
|
334
|
-
points = self.faces_centers.reshape((self.nb_faces, 1, 3))
|
|
335
|
-
weights = self.faces_areas.reshape((self.nb_faces, 1))
|
|
336
|
-
else:
|
|
337
|
-
points, weights = compute_quadrature_on_faces(all_faces, method)
|
|
338
|
-
self.__internals__['quadrature'] = (points, weights)
|
|
339
|
-
self.quadrature_method = method
|
|
340
|
-
return points, weights
|
|
341
|
-
|
|
438
|
+
"""Radii of each face (circumradius or characteristic size).
|
|
342
439
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def is_triangle(self, face_id) -> bool:
|
|
348
|
-
"""Returns if a face is a triangle
|
|
349
|
-
|
|
350
|
-
Parameters
|
|
351
|
-
----------
|
|
352
|
-
face_id : int
|
|
353
|
-
Face id
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
np.ndarray
|
|
443
|
+
Array of shape (n_faces,) containing the radius of each face.
|
|
354
444
|
"""
|
|
355
|
-
|
|
356
|
-
return self._faces[face_id, 0] == self._faces[face_id, -1]
|
|
357
|
-
|
|
358
|
-
def _compute_triangles_quadrangles(self):
|
|
359
|
-
triangle_mask = (self._faces[:, 0] == self._faces[:, -1])
|
|
360
|
-
quadrangles_mask = np.invert(triangle_mask)
|
|
361
|
-
triangles_quadrangles = {'triangles_ids': np.where(triangle_mask)[0],
|
|
362
|
-
'quadrangles_ids': np.where(quadrangles_mask)[0]}
|
|
363
|
-
self.__internals__.update(triangles_quadrangles)
|
|
445
|
+
return compute_faces_radii(self.vertices, self._faces)
|
|
364
446
|
|
|
365
|
-
@
|
|
366
|
-
def
|
|
367
|
-
"""
|
|
368
|
-
if 'triangles_ids' not in self.__internals__:
|
|
369
|
-
self._compute_triangles_quadrangles()
|
|
370
|
-
return self.__internals__['triangles_ids']
|
|
371
|
-
|
|
372
|
-
@property
|
|
373
|
-
def nb_triangles(self) -> int:
|
|
374
|
-
"""Get the number of triangles in the mesh."""
|
|
375
|
-
if 'triangles_ids'not in self.__internals__:
|
|
376
|
-
self._compute_triangles_quadrangles()
|
|
377
|
-
return len(self.__internals__['triangles_ids'])
|
|
378
|
-
|
|
379
|
-
@property
|
|
380
|
-
def quadrangles_ids(self) -> np.ndarray:
|
|
381
|
-
"""Get the array of ids of quadrangle shaped faces."""
|
|
382
|
-
if 'triangles_ids' not in self.__internals__:
|
|
383
|
-
self._compute_triangles_quadrangles()
|
|
384
|
-
return self.__internals__['quadrangles_ids']
|
|
385
|
-
|
|
386
|
-
@property
|
|
387
|
-
def nb_quadrangles(self) -> int:
|
|
388
|
-
"""Get the number of quadrangles in the mesh."""
|
|
389
|
-
if 'triangles_ids' not in self.__internals__:
|
|
390
|
-
self._compute_triangles_quadrangles()
|
|
391
|
-
return len(self.__internals__['quadrangles_ids'])
|
|
392
|
-
|
|
393
|
-
#############
|
|
394
|
-
# Display #
|
|
395
|
-
#############
|
|
396
|
-
|
|
397
|
-
@property
|
|
398
|
-
def axis_aligned_bbox(self):
|
|
399
|
-
"""Get the axis aligned bounding box of the mesh.
|
|
447
|
+
@cached_property
|
|
448
|
+
def faces(self) -> np.ndarray:
|
|
449
|
+
"""Face connectivity as quadrilateral array.
|
|
400
450
|
|
|
401
451
|
Returns
|
|
402
452
|
-------
|
|
403
|
-
|
|
404
|
-
(
|
|
453
|
+
np.ndarray
|
|
454
|
+
Array of shape (n_faces, 4) where triangular faces are padded
|
|
455
|
+
by repeating the last vertex.
|
|
456
|
+
|
|
457
|
+
Notes
|
|
458
|
+
-----
|
|
459
|
+
This property converts all faces to a uniform quad representation
|
|
460
|
+
for compatibility with libraries expecting fixed-width face arrays.
|
|
405
461
|
"""
|
|
406
|
-
if self.
|
|
407
|
-
|
|
408
|
-
return (x.min(), x.max(),
|
|
409
|
-
y.min(), y.max(),
|
|
410
|
-
z.min(), z.max())
|
|
411
|
-
else:
|
|
412
|
-
return tuple(np.zeros(6))
|
|
462
|
+
faces_as_quad = [f if len(f) == 4 else f + [f[-1]] for f in self._faces]
|
|
463
|
+
return np.array(faces_as_quad, dtype=int)
|
|
413
464
|
|
|
414
|
-
@
|
|
415
|
-
def
|
|
416
|
-
"""
|
|
465
|
+
@cached_property
|
|
466
|
+
def quadrature_points(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
467
|
+
"""Quadrature points and weights for numerical integration.
|
|
417
468
|
|
|
418
469
|
Returns
|
|
419
470
|
-------
|
|
420
|
-
tuple
|
|
421
|
-
(
|
|
422
|
-
|
|
423
|
-
Note
|
|
424
|
-
----
|
|
425
|
-
This method differs from `axis_aligned_bbox()` by the fact that
|
|
426
|
-
the bounding box that is returned is squared but have the same center as the `axis_aligned_bbox()`.
|
|
471
|
+
tuple[np.ndarray, np.ndarray]
|
|
472
|
+
(points, weights) where points has shape (n_faces, 1, 3) and
|
|
473
|
+
weights has shape (n_faces, 1), using face centers and areas.
|
|
427
474
|
"""
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
475
|
+
if self.quadrature_method is None:
|
|
476
|
+
return (self.faces_centers.reshape((-1, 1, 3)), self.faces_areas.reshape(-1, 1))
|
|
477
|
+
elif self.quadrature_method == "Gauss-Legendre 2":
|
|
478
|
+
return compute_gauss_legendre_2_quadrature(self.vertices, self.faces)
|
|
479
|
+
else:
|
|
480
|
+
raise ValueError(f"Unknown quadrature_method: {self.quadrature_method}")
|
|
481
|
+
|
|
482
|
+
## TRANSFORMATIONS
|
|
483
|
+
|
|
484
|
+
def with_quadrature(self, quadrature_method):
|
|
485
|
+
return Mesh(
|
|
486
|
+
self.vertices,
|
|
487
|
+
self.faces,
|
|
488
|
+
faces_metadata=self.faces_metadata,
|
|
489
|
+
quadrature_method=quadrature_method,
|
|
490
|
+
name=self.name,
|
|
491
|
+
auto_clean=False,
|
|
492
|
+
auto_check=False
|
|
493
|
+
)
|
|
445
494
|
|
|
446
|
-
def
|
|
447
|
-
|
|
448
|
-
saveas=None, color_field=None, cmap=None,
|
|
449
|
-
cbar_label=None,
|
|
450
|
-
**kwargs):
|
|
451
|
-
"""Poor man's viewer with matplotlib.
|
|
495
|
+
def extract_faces(self, faces_id, *, name=None) -> "Mesh":
|
|
496
|
+
"""Extract a subset of faces by their indices and return a new Mesh instance.
|
|
452
497
|
|
|
453
498
|
Parameters
|
|
454
499
|
----------
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
Scalar field to be plot on the mesh (optional).
|
|
465
|
-
cmap: matplotlib colormap
|
|
466
|
-
Colormap to use for field plotting.
|
|
467
|
-
cbar_label: string
|
|
468
|
-
Label for colormap
|
|
469
|
-
|
|
470
|
-
Other parameters are passed to Poly3DCollection.
|
|
500
|
+
faces_id : array_like
|
|
501
|
+
Indices of faces to extract.
|
|
502
|
+
name: str, optional
|
|
503
|
+
A name for the new mesh
|
|
504
|
+
|
|
505
|
+
Returns
|
|
506
|
+
-------
|
|
507
|
+
Mesh
|
|
508
|
+
New mesh containing only the specified faces.
|
|
471
509
|
"""
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
510
|
+
if isinstance(faces_id, np.ndarray):
|
|
511
|
+
faces_id = faces_id.ravel()
|
|
512
|
+
all_faces = self.as_list_of_faces()
|
|
513
|
+
selected_faces = [all_faces[i] for i in faces_id]
|
|
514
|
+
return Mesh.from_list_of_faces(
|
|
515
|
+
selected_faces,
|
|
516
|
+
faces_metadata={k: self.faces_metadata[k][selected_faces, ...] for k in self.faces_metadata},
|
|
517
|
+
name=name,
|
|
518
|
+
quadrature_method=self.quadrature_method,
|
|
519
|
+
auto_clean=False,
|
|
520
|
+
auto_check=False
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def translated(self, shift, *, name=None) -> "Mesh":
|
|
524
|
+
"""Return a new Mesh translated along vector-like `shift`."""
|
|
525
|
+
return Mesh(
|
|
526
|
+
vertices=self.vertices + np.asarray(shift),
|
|
527
|
+
faces=self._faces,
|
|
528
|
+
faces_metadata=self.faces_metadata,
|
|
529
|
+
name=name,
|
|
530
|
+
quadrature_method=self.quadrature_method,
|
|
531
|
+
auto_clean=False,
|
|
532
|
+
auto_check=False,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def rotated_with_matrix(self, R, *, name=None) -> "Mesh":
|
|
536
|
+
"""Return a new Mesh rotated using the provided 3×3 rotation matrix."""
|
|
537
|
+
new_vertices = self.vertices @ R.T
|
|
538
|
+
return Mesh(
|
|
539
|
+
vertices=new_vertices,
|
|
540
|
+
faces=self._faces,
|
|
541
|
+
name=name,
|
|
542
|
+
faces_metadata=self.faces_metadata,
|
|
543
|
+
quadrature_method=self.quadrature_method,
|
|
544
|
+
auto_clean=False,
|
|
545
|
+
auto_check=False,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def mirrored(self, plane: Literal['xOz', 'yOz'], *, name=None) -> "Mesh":
|
|
549
|
+
new_vertices = self.vertices.copy()
|
|
550
|
+
if plane == "xOz":
|
|
551
|
+
new_vertices[:, 1] *= -1
|
|
552
|
+
elif plane == "yOz":
|
|
553
|
+
new_vertices[:, 0] *= -1
|
|
495
554
|
else:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
# Plot normal vectors.
|
|
516
|
-
if normal_vectors:
|
|
517
|
-
if scale_normal_vector is not None:
|
|
518
|
-
vectors = (scale_normal_vector * self.faces_normals.T).T
|
|
519
|
-
else:
|
|
520
|
-
vectors = self.faces_normals
|
|
521
|
-
ax.quiver(*zip(*self.faces_centers), *zip(*vectors), length=0.2)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
ax.set_xlabel("x")
|
|
525
|
-
ax.set_ylabel("y")
|
|
526
|
-
ax.set_zlabel("z")
|
|
527
|
-
|
|
528
|
-
xmin, xmax, ymin, ymax, zmin, zmax = self.squared_axis_aligned_bbox
|
|
529
|
-
ax.set_xlim(xmin, xmax)
|
|
530
|
-
ax.set_ylim(ymin, ymax)
|
|
531
|
-
ax.set_zlim(zmin, zmax)
|
|
532
|
-
|
|
533
|
-
if default_axis:
|
|
534
|
-
if saveas is not None:
|
|
535
|
-
plt.tight_layout()
|
|
536
|
-
plt.savefig(saveas)
|
|
537
|
-
else:
|
|
538
|
-
plt.show()
|
|
539
|
-
|
|
540
|
-
################################
|
|
541
|
-
# Transformation of the mesh #
|
|
542
|
-
################################
|
|
543
|
-
|
|
544
|
-
@inplace_transformation
|
|
545
|
-
def translate(self, vector) -> 'Mesh':
|
|
546
|
-
"""Translates the mesh in 3D giving the 3 distances along coordinate axes.
|
|
555
|
+
raise ValueError(f"Unsupported value for plane: {plane}")
|
|
556
|
+
new_faces = [f[::-1] for f in self._faces] # Invert normals
|
|
557
|
+
if name is None and self.name is not None:
|
|
558
|
+
name = f"mirrored_{self.name}"
|
|
559
|
+
return Mesh(
|
|
560
|
+
new_vertices,
|
|
561
|
+
new_faces,
|
|
562
|
+
faces_metadata=self.faces_metadata,
|
|
563
|
+
quadrature_method=self.quadrature_method,
|
|
564
|
+
name=name,
|
|
565
|
+
auto_clean=False,
|
|
566
|
+
auto_check=False
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def join_meshes(*meshes: List["Mesh"], return_masks=False, name=None) -> "Mesh":
|
|
570
|
+
"""Join several meshes and return a new Mesh instance.
|
|
547
571
|
|
|
548
572
|
Parameters
|
|
549
573
|
----------
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return self
|
|
574
|
+
meshes: List[Mesh]
|
|
575
|
+
Meshes to be joined
|
|
576
|
+
return_masks: bool, optional
|
|
577
|
+
If True, additionally return a list of numpy masks establishing the
|
|
578
|
+
origin of each face in the new mesh.
|
|
579
|
+
(Default: False)
|
|
580
|
+
name: str, optional
|
|
581
|
+
A name for the new object
|
|
559
582
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
583
|
+
Returns
|
|
584
|
+
-------
|
|
585
|
+
Mesh
|
|
586
|
+
New mesh containing vertices and faces from all meshes.
|
|
563
587
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
angle : float
|
|
588
|
+
See Also
|
|
589
|
+
--------
|
|
590
|
+
__add__ : Implements the + operator for mesh joining.
|
|
568
591
|
"""
|
|
592
|
+
if not all(isinstance(m, Mesh) for m in meshes):
|
|
593
|
+
raise TypeError("Only Mesh instances can be added together.")
|
|
569
594
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
return self
|
|
595
|
+
faces = sum((m.as_list_of_faces() for m in meshes), [])
|
|
573
596
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
597
|
+
if return_masks:
|
|
598
|
+
# Add a temporary metadata to keep track of the origin of each face
|
|
599
|
+
meshes = [m.with_metadata(origin_mesh_index=np.array([i]*m.nb_faces))
|
|
600
|
+
for i, m in enumerate(meshes)]
|
|
578
601
|
|
|
579
|
-
|
|
602
|
+
faces_metadata = {k: np.concatenate([m.faces_metadata[k] for m in meshes], axis=0)
|
|
603
|
+
for k in AbstractMesh._common_metadata_keys(*meshes)}
|
|
580
604
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
@inplace_transformation
|
|
584
|
-
def mirror(self, plane) -> 'Mesh':
|
|
585
|
-
"""Flip the mesh with respect to a plane.
|
|
586
|
-
|
|
587
|
-
Parameters
|
|
588
|
-
----------
|
|
589
|
-
plane : Plane
|
|
590
|
-
The mirroring plane
|
|
591
|
-
"""
|
|
592
|
-
self.vertices -= 2 * np.outer(np.dot(self.vertices, plane.normal) - plane.c, plane.normal)
|
|
593
|
-
self.flip_normals()
|
|
594
|
-
return self
|
|
595
|
-
|
|
596
|
-
def symmetrized(self, plane):
|
|
597
|
-
from capytaine.meshes.symmetric import ReflectionSymmetricMesh
|
|
598
|
-
half = self.clipped(plane, name=f"{self.name}_half")
|
|
599
|
-
return ReflectionSymmetricMesh(half, plane=plane, name=f"symmetrized_of_{self.name}")
|
|
600
|
-
|
|
601
|
-
@inplace_transformation
|
|
602
|
-
def clip(self, plane) -> 'Mesh':
|
|
603
|
-
from capytaine.meshes.clipper import clip
|
|
604
|
-
clipped_self = clip(self, plane=plane)
|
|
605
|
-
self.vertices = clipped_self.vertices
|
|
606
|
-
self.faces = clipped_self.faces
|
|
607
|
-
self._clipping_data = clipped_self._clipping_data
|
|
608
|
-
return self
|
|
609
|
-
|
|
610
|
-
@inplace_transformation
|
|
611
|
-
def triangulate_quadrangles(self) -> 'Mesh':
|
|
612
|
-
"""Triangulates every quadrangles of the mesh by simple splitting.
|
|
613
|
-
Each quadrangle gives two triangles.
|
|
614
|
-
|
|
615
|
-
Note
|
|
616
|
-
----
|
|
617
|
-
No checking on the triangle quality is done.
|
|
618
|
-
"""
|
|
619
|
-
# Defining both triangles id lists to be generated from quadrangles
|
|
620
|
-
t1 = (0, 1, 2)
|
|
621
|
-
t2 = (0, 2, 3)
|
|
622
|
-
|
|
623
|
-
faces = self._faces
|
|
624
|
-
|
|
625
|
-
# Triangulation
|
|
626
|
-
new_faces = faces[self.quadrangles_ids].copy()
|
|
627
|
-
new_faces[:, :3] = new_faces[:, t1]
|
|
628
|
-
new_faces[:, -1] = new_faces[:, 0]
|
|
629
|
-
|
|
630
|
-
faces[self.quadrangles_ids, :3] = faces[:, t2][self.quadrangles_ids]
|
|
631
|
-
faces[self.quadrangles_ids, -1] = faces[self.quadrangles_ids, 0]
|
|
632
|
-
|
|
633
|
-
faces = np.concatenate((faces, new_faces))
|
|
634
|
-
|
|
635
|
-
LOG.info('\nTriangulating quadrangles')
|
|
636
|
-
if self.nb_quadrangles != 0:
|
|
637
|
-
LOG.info('\t-->{:d} quadrangles have been split in triangles'.format(self.nb_quadrangles))
|
|
638
|
-
|
|
639
|
-
self._faces = faces
|
|
640
|
-
|
|
641
|
-
return self
|
|
642
|
-
|
|
643
|
-
####################
|
|
644
|
-
# Combine meshes #
|
|
645
|
-
####################
|
|
646
|
-
|
|
647
|
-
def join_meshes(*meshes, name=None):
|
|
648
|
-
from capytaine.meshes.collections import CollectionOfMeshes
|
|
649
|
-
return CollectionOfMeshes(meshes, name=name).merged()
|
|
650
|
-
|
|
651
|
-
def __add__(self, mesh_to_add) -> 'Mesh':
|
|
652
|
-
return self.join_meshes(mesh_to_add)
|
|
653
|
-
|
|
654
|
-
####################
|
|
655
|
-
# Compare meshes #
|
|
656
|
-
####################
|
|
657
|
-
# The objective is to write a mesh as a set of faces in order to check for equality or to
|
|
658
|
-
# compute differences of meshes. Each face can be represented as a 4x3 array (4 triplets of
|
|
659
|
-
# coordinates).
|
|
660
|
-
# However, it is tricky on several aspects:
|
|
661
|
-
# * The builtin set class compares the hashes of its objects. Since numpy ndarray are not
|
|
662
|
-
# hashable, the 4x3 array can be transformed into a tuple of tuples (which is hashable).
|
|
663
|
-
# * Two faces with different numbering of the faces (but the same ordering) are recorded as
|
|
664
|
-
# different.
|
|
665
|
-
# * Two vertices equal up to machine precision can be recorded as different, due to rounding
|
|
666
|
-
# errors.
|
|
667
|
-
#
|
|
668
|
-
# A possible solution is to define a Face class and a Vertex class with the appropriate __eq__
|
|
669
|
-
# and __hash__.
|
|
670
|
-
#
|
|
671
|
-
# The current implementation below is a rough draft.
|
|
672
|
-
# However, the equality shall still be use for testing.
|
|
673
|
-
|
|
674
|
-
def as_set_of_faces(self):
|
|
675
|
-
return frozenset(frozenset(tuple(vertex) for vertex in face) for face in self.vertices[self.faces])
|
|
676
|
-
|
|
677
|
-
@staticmethod
|
|
678
|
-
def from_set_of_faces(set_of_faces):
|
|
679
|
-
faces = []
|
|
680
|
-
vertices = []
|
|
681
|
-
for face in set_of_faces:
|
|
682
|
-
ids_of_vertices_in_face = []
|
|
683
|
-
|
|
684
|
-
for vertex in face:
|
|
685
|
-
if vertex not in vertices:
|
|
686
|
-
i = len(vertices)
|
|
687
|
-
vertices.append(vertex)
|
|
688
|
-
else:
|
|
689
|
-
i = vertices.index(vertex)
|
|
690
|
-
ids_of_vertices_in_face.append(i)
|
|
691
|
-
|
|
692
|
-
if len(ids_of_vertices_in_face) == 3:
|
|
693
|
-
# Add a fourth node identical to the first one
|
|
694
|
-
ids_of_vertices_in_face.append(ids_of_vertices_in_face[0])
|
|
695
|
-
|
|
696
|
-
faces.append(ids_of_vertices_in_face)
|
|
697
|
-
return Mesh(vertices=vertices, faces=faces)
|
|
698
|
-
|
|
699
|
-
def __eq__(self, other):
|
|
700
|
-
if not isinstance(other, Mesh):
|
|
701
|
-
return NotImplemented
|
|
605
|
+
if all(meshes[0].quadrature_method == m.quadrature_method for m in meshes[1:]):
|
|
606
|
+
quadrature_method = meshes[0].quadrature_method
|
|
702
607
|
else:
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
return heal_triangles(self, **kwargs)
|
|
725
|
-
|
|
726
|
-
def remove_degenerated_faces(self, **kwargs):
|
|
727
|
-
return remove_degenerated_faces(self, **kwargs)
|
|
728
|
-
|
|
729
|
-
@inplace_transformation
|
|
730
|
-
def heal_mesh(self, closed_mesh=True):
|
|
731
|
-
"""Heals the mesh for different tests available.
|
|
732
|
-
|
|
733
|
-
It applies:
|
|
734
|
-
|
|
735
|
-
* Unused vertices removal
|
|
736
|
-
* Degenerate faces removal
|
|
737
|
-
* Duplicate vertices merging
|
|
738
|
-
* Triangles healing
|
|
739
|
-
* Normal healing
|
|
740
|
-
"""
|
|
741
|
-
self.remove_unused_vertices()
|
|
742
|
-
self.remove_degenerated_faces()
|
|
743
|
-
self.merge_duplicates()
|
|
744
|
-
self.heal_triangles()
|
|
745
|
-
if closed_mesh:
|
|
746
|
-
self.heal_normals()
|
|
747
|
-
return self
|
|
748
|
-
|
|
749
|
-
##########
|
|
750
|
-
# Lids #
|
|
751
|
-
##########
|
|
752
|
-
|
|
753
|
-
def lowest_lid_position(self, omega_max, *, g=9.81):
|
|
754
|
-
z_lid = 0.0
|
|
755
|
-
for comp in connected_components(self):
|
|
756
|
-
for ccomp in connected_components_of_waterline(comp):
|
|
757
|
-
x_span = ccomp.vertices[:, 0].max() - ccomp.vertices[:, 0].min()
|
|
758
|
-
y_span = ccomp.vertices[:, 1].max() - ccomp.vertices[:, 1].min()
|
|
759
|
-
p = np.hypot(1/x_span, 1/y_span)
|
|
760
|
-
z_lid_comp = -np.arctanh(np.pi*g*p/omega_max**2) / (np.pi * p)
|
|
761
|
-
z_lid = min(z_lid, z_lid_comp)
|
|
762
|
-
return 0.9*z_lid # Add a small safety margin
|
|
608
|
+
LOG.info("Dropping inconsistent quadrature method when joining meshes")
|
|
609
|
+
quadrature_method = None
|
|
610
|
+
|
|
611
|
+
if name is None and all(m.name is not None for m in meshes):
|
|
612
|
+
name = "+".join([m.name for m in meshes])
|
|
613
|
+
|
|
614
|
+
joined_mesh = Mesh.from_list_of_faces(
|
|
615
|
+
faces,
|
|
616
|
+
quadrature_method=quadrature_method,
|
|
617
|
+
faces_metadata=faces_metadata,
|
|
618
|
+
name=name,
|
|
619
|
+
auto_check=False,
|
|
620
|
+
)
|
|
621
|
+
# If list of faces is trimmed for some reason, metadata will be updated accordingly
|
|
622
|
+
|
|
623
|
+
if return_masks:
|
|
624
|
+
# Extract the temporary metadata
|
|
625
|
+
masks = [joined_mesh.faces_metadata['origin_mesh_index'] == i for i in range(len(meshes))]
|
|
626
|
+
return joined_mesh.without_metadata('origin_mesh_index'), masks
|
|
627
|
+
else:
|
|
628
|
+
return joined_mesh
|
|
763
629
|
|
|
764
630
|
def generate_lid(self, z=0.0, faces_max_radius=None, name=None):
|
|
765
631
|
"""
|
|
@@ -782,12 +648,17 @@ class Mesh(ClippableMixin, SurfaceIntegralsMixin, Abstract3DObject):
|
|
|
782
648
|
"""
|
|
783
649
|
from capytaine.meshes.predefined.rectangles import mesh_rectangle
|
|
784
650
|
|
|
785
|
-
|
|
651
|
+
LOG.debug(f"Generating lid for {self.__str__()}")
|
|
652
|
+
|
|
653
|
+
if name is None and self.name is not None:
|
|
654
|
+
name = "lid for {}".format(self.name)
|
|
655
|
+
|
|
656
|
+
clipped_hull_mesh = self.clipped(normal=(0, 0, 1), origin=(0, 0, z))
|
|
786
657
|
# Alternatively: could keep only faces below z without proper clipping,
|
|
787
658
|
# and it would work similarly.
|
|
788
659
|
|
|
789
660
|
if clipped_hull_mesh.nb_faces == 0:
|
|
790
|
-
return Mesh(None, None, name=
|
|
661
|
+
return Mesh(None, None, name=name)
|
|
791
662
|
|
|
792
663
|
x_span = clipped_hull_mesh.vertices[:, 0].max() - clipped_hull_mesh.vertices[:, 0].min()
|
|
793
664
|
y_span = clipped_hull_mesh.vertices[:, 1].max() - clipped_hull_mesh.vertices[:, 1].min()
|
|
@@ -808,6 +679,7 @@ class Mesh(ClippableMixin, SurfaceIntegralsMixin, Abstract3DObject):
|
|
|
808
679
|
faces_max_radius=faces_max_radius,
|
|
809
680
|
center=(x_mean, y_mean, z),
|
|
810
681
|
normal=(0.0, 0.0, -1.0),
|
|
682
|
+
name="candidate_lid_mesh"
|
|
811
683
|
)
|
|
812
684
|
|
|
813
685
|
candidate_lid_points = candidate_lid_mesh.vertices[:, 0:2]
|
|
@@ -833,34 +705,13 @@ class Mesh(ClippableMixin, SurfaceIntegralsMixin, Abstract3DObject):
|
|
|
833
705
|
|
|
834
706
|
lid_faces = candidate_lid_mesh.faces[np.all(np.isin(candidate_lid_mesh.faces, needs_lid), axis=-1), :]
|
|
835
707
|
|
|
836
|
-
if name is None:
|
|
837
|
-
name = "lid for {}".format(self.name)
|
|
838
|
-
|
|
839
708
|
if len(lid_faces) == 0:
|
|
840
709
|
return Mesh(None, None, name=name)
|
|
841
710
|
|
|
842
|
-
lid_mesh = Mesh(candidate_lid_mesh.vertices, lid_faces, name=name)
|
|
843
|
-
lid_mesh.heal_mesh()
|
|
844
|
-
|
|
711
|
+
lid_mesh = Mesh(candidate_lid_mesh.vertices, lid_faces, name=name, auto_check=False)
|
|
845
712
|
return lid_mesh
|
|
846
713
|
|
|
847
|
-
|
|
848
|
-
def with_normal_vector_going_down(self):
|
|
849
|
-
# For lid meshes for irregular frequencies removal
|
|
850
|
-
if np.allclose(self.faces_normals[:, 2], np.ones((self.nb_faces,))):
|
|
851
|
-
# The mesh is horizontal with normal vectors going up
|
|
852
|
-
LOG.warning(f"Inverting the direction of the normal vectors of {self} to be downward.")
|
|
853
|
-
self.faces = self.faces[:, ::-1]
|
|
854
|
-
else:
|
|
855
|
-
return self
|
|
856
|
-
|
|
857
|
-
def _face_on_plane(self, i_face, plane):
|
|
858
|
-
return (
|
|
859
|
-
self.faces_centers[i_face, :] in plane
|
|
860
|
-
and plane.is_orthogonal_to(self.faces_normals[i_face, :])
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
def extract_lid(self, plane=xOy_Plane):
|
|
714
|
+
def extract_lid(self, z=0.0):
|
|
864
715
|
"""
|
|
865
716
|
Split the mesh into a mesh of the hull and a mesh of the lid.
|
|
866
717
|
By default, the lid is composed of the horizontal faces on the z=0 plane.
|
|
@@ -875,7 +726,101 @@ class Mesh(ClippableMixin, SurfaceIntegralsMixin, Abstract3DObject):
|
|
|
875
726
|
2-ple of Mesh
|
|
876
727
|
hull mesh and lid mesh
|
|
877
728
|
"""
|
|
878
|
-
|
|
729
|
+
def is_on_plane(i_face):
|
|
730
|
+
return np.isclose(self.faces_centers[i_face, 2], z) and (\
|
|
731
|
+
np.allclose(self.faces_normals[i_face, :], np.array([0.0, 0.0, 1.0])) or \
|
|
732
|
+
np.allclose(self.faces_normals[i_face, :], np.array([0.0, 0.0, -1.0]))
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
faces_on_plane = [
|
|
736
|
+
i_face for i_face in range(self.nb_faces) if is_on_plane(i_face)
|
|
737
|
+
]
|
|
879
738
|
lid_mesh = self.extract_faces(faces_on_plane)
|
|
880
739
|
hull_mesh = self.extract_faces(list(set(range(self.nb_faces)) - set(faces_on_plane)))
|
|
881
740
|
return hull_mesh, lid_mesh
|
|
741
|
+
|
|
742
|
+
def with_normal_vector_going_down(self, **kwargs) -> "Mesh":
|
|
743
|
+
# Kwargs are for backward compatibility with former inplace implementation of this.
|
|
744
|
+
# It could be removed in the final release.
|
|
745
|
+
"""Ensure normal vectors point downward (negative z-direction).
|
|
746
|
+
|
|
747
|
+
Returns
|
|
748
|
+
-------
|
|
749
|
+
Mesh
|
|
750
|
+
Self if normals already point down, otherwise modifies face orientation.
|
|
751
|
+
|
|
752
|
+
Notes
|
|
753
|
+
-----
|
|
754
|
+
Used for lid meshes to avoid irregular frequency issues by ensuring
|
|
755
|
+
consistent normal vector direction.
|
|
756
|
+
"""
|
|
757
|
+
# For lid meshes for irregular frequencies removal
|
|
758
|
+
if np.allclose(self.faces_normals[:, 2], np.ones((self.nb_faces,))):
|
|
759
|
+
# The mesh is horizontal with normal vectors going up
|
|
760
|
+
LOG.warning(
|
|
761
|
+
f"Inverting the direction of the normal vectors of {self} to be downward."
|
|
762
|
+
)
|
|
763
|
+
return Mesh(
|
|
764
|
+
vertices=self.vertices,
|
|
765
|
+
faces=self.faces[:, ::-1],
|
|
766
|
+
faces_metadata=self.faces_metadata,
|
|
767
|
+
quadrature_method=self.quadrature_method,
|
|
768
|
+
name=self.name,
|
|
769
|
+
auto_clean=False,
|
|
770
|
+
auto_check=False,
|
|
771
|
+
)
|
|
772
|
+
else:
|
|
773
|
+
return self
|
|
774
|
+
|
|
775
|
+
def copy(self, *, faces_metadata=None, name=None) -> Mesh:
|
|
776
|
+
# No-op for backward compatibility
|
|
777
|
+
if faces_metadata is None:
|
|
778
|
+
faces_metadata = self.faces_metadata.copy()
|
|
779
|
+
if name is None:
|
|
780
|
+
name = self.name
|
|
781
|
+
return Mesh(
|
|
782
|
+
vertices=self.vertices,
|
|
783
|
+
faces=self._faces,
|
|
784
|
+
faces_metadata=faces_metadata,
|
|
785
|
+
quadrature_method=self.quadrature_method,
|
|
786
|
+
name=name,
|
|
787
|
+
auto_clean=False,
|
|
788
|
+
auto_check=False
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
def merged(self, *, name=None) -> Mesh:
|
|
792
|
+
# No-op to be extended to symmetries
|
|
793
|
+
return self.copy(name=name)
|
|
794
|
+
|
|
795
|
+
def clipped(self, *, origin, normal, name=None) -> "Mesh":
|
|
796
|
+
"""
|
|
797
|
+
Clip the mesh by a plane defined by `origin` and `normal`.
|
|
798
|
+
|
|
799
|
+
Parameters
|
|
800
|
+
----------
|
|
801
|
+
origin : np.ndarray
|
|
802
|
+
The point in space where the clipping plane intersects (3D point).
|
|
803
|
+
normal : np.ndarray
|
|
804
|
+
The normal vector defining the orientation of the clipping plane.
|
|
805
|
+
name: Optional[str]
|
|
806
|
+
A name for the newly created mesh
|
|
807
|
+
|
|
808
|
+
Returns
|
|
809
|
+
-------
|
|
810
|
+
Mesh
|
|
811
|
+
A new Mesh instance that has been clipped.
|
|
812
|
+
"""
|
|
813
|
+
LOG.debug(f"Clipping {self.__str__()} with origin={origin} and normal={normal}")
|
|
814
|
+
new_vertices, new_faces, face_parent = \
|
|
815
|
+
clip_faces(self.vertices, self._faces, normal, origin)
|
|
816
|
+
new_metadata = {k: self.faces_metadata[k][face_parent] for k in self.faces_metadata}
|
|
817
|
+
if name is None and self.name is not None:
|
|
818
|
+
name = f"{self.name}_clipped"
|
|
819
|
+
return Mesh(
|
|
820
|
+
vertices=new_vertices,
|
|
821
|
+
faces=new_faces,
|
|
822
|
+
faces_metadata=new_metadata,
|
|
823
|
+
quadrature_method=self.quadrature_method,
|
|
824
|
+
name=name,
|
|
825
|
+
auto_check=False,
|
|
826
|
+
)
|