PyGEL3D 0.8.0__py3-none-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.
- pygel3d/PyGEL.dll +0 -0
- pygel3d/__init__.py +389 -0
- pygel3d/experimental/__init__.py +1 -0
- pygel3d/experimental/hmesh.py +32 -0
- pygel3d/gl_display.py +127 -0
- pygel3d/graph.py +290 -0
- pygel3d/hmesh.py +1112 -0
- pygel3d/jupyter_display.py +91 -0
- pygel3d/spatial.py +40 -0
- pygel3d-0.8.0.dist-info/METADATA +159 -0
- pygel3d-0.8.0.dist-info/RECORD +14 -0
- pygel3d-0.8.0.dist-info/WHEEL +5 -0
- pygel3d-0.8.0.dist-info/licenses/LICENSE.md +27 -0
- pygel3d-0.8.0.dist-info/top_level.txt +1 -0
pygel3d/hmesh.py
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
""" The hmesh module provides an halfedge based mesh representation.
|
|
2
|
+
In addition this module contains a variety of functions for mesh manipulation and inspection.
|
|
3
|
+
Specifcally, the module contains functions for mesh simplification, smoothing, subdivision,
|
|
4
|
+
and editing of vertices, faces, and edges. The volumetric_isocontour function allows us to
|
|
5
|
+
create a polygonal mesh from volumetric data by isocontouring. The skeleton_to_feq function
|
|
6
|
+
allows us to turn a skeleton graph into a Face Extrusion Quad Mesh.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'Manifold', 'MeshDistance', 'valid', 'closed', 'area', 'volume', 'bbox', 'bsphere', 'stitch',
|
|
11
|
+
'obj_save', 'off_save', 'x3d_save', 'obj_load', 'off_load', 'ply_load', 'x3d_load', 'load', 'save',
|
|
12
|
+
'remove_caps', 'remove_needles', 'close_holes', 'flip_orientation', 'merge_coincident_boundary_vertices',
|
|
13
|
+
'minimize_curvature', 'minimize_dihedral_angle', 'maximize_min_angle', 'optimize_valency',
|
|
14
|
+
'randomize_mesh', 'quadric_simplify', 'average_edge_length', 'median_edge_length', 'refine_edges',
|
|
15
|
+
'cc_split', 'loop_split', 'root3_subdivide', 'rootCC_subdivide', 'butterfly_subdivide', 'cc_smooth',
|
|
16
|
+
'cc_subdivide', 'loop_subdivide', 'volume_preserving_cc_smooth', 'regularize_quads', 'loop_smooth',
|
|
17
|
+
'taubin_smooth', 'laplacian_smooth', 'anisotropic_smooth', 'volumetric_isocontour', 'triangulate',
|
|
18
|
+
'extrude_faces', 'kill_face_loop', 'kill_degenerate_face_loops', 'graph_to_feq', 'skeleton_to_feq',
|
|
19
|
+
'graph_to_cylinders', 'graph_to_isosurface', 'fit_mesh_to_ref', 'rsr_recon', 'hrsr_recon', 'connected_components',
|
|
20
|
+
'count_boundary_curves', 'analyze_topology', 'sphere_delaunay'
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
import ctypes as ct
|
|
24
|
+
import numpy as np
|
|
25
|
+
from numpy import ndarray
|
|
26
|
+
from numpy.typing import ArrayLike
|
|
27
|
+
from typing import List, Dict, Any, Self
|
|
28
|
+
from pygel3d import lib_py_gel, IntVector
|
|
29
|
+
from pygel3d.graph import Graph
|
|
30
|
+
from scipy.sparse import csc_matrix, vstack
|
|
31
|
+
from scipy.sparse.linalg import lsqr
|
|
32
|
+
from scipy.spatial import KDTree
|
|
33
|
+
|
|
34
|
+
def _n_vec3(arr: ndarray) -> int:
|
|
35
|
+
"""Number of 3D points in a (N,3) or flat 3N array."""
|
|
36
|
+
if arr.ndim >= 2:
|
|
37
|
+
return int(arr.shape[0])
|
|
38
|
+
return int(arr.size // 3)
|
|
39
|
+
|
|
40
|
+
def _as_vec3_f(arr: ArrayLike | None) -> tuple[ndarray, int]:
|
|
41
|
+
"""Fortran (N,3) array of points, or an empty 2D array if arr is missing.
|
|
42
|
+
|
|
43
|
+
ctypes requires ndim==2 even when there are no normals, so None and []
|
|
44
|
+
both become a (0, 3) placeholder.
|
|
45
|
+
"""
|
|
46
|
+
if arr is None:
|
|
47
|
+
return np.zeros((0, 3), dtype=ct.c_double, order='F'), 0
|
|
48
|
+
a = np.asarray(arr, dtype=ct.c_double)
|
|
49
|
+
if a.size == 0:
|
|
50
|
+
return np.zeros((0, 3), dtype=ct.c_double, order='F'), 0
|
|
51
|
+
n = _n_vec3(a)
|
|
52
|
+
return np.asfortranarray(np.reshape(a, (n, 3)), dtype=ct.c_double), n
|
|
53
|
+
|
|
54
|
+
class Manifold:
|
|
55
|
+
""" The Manifold class represents a halfedge based mesh. It is maybe a bit grand to call
|
|
56
|
+
a mesh class Manifold, but meshes based on the halfedge representation are manifold (if we
|
|
57
|
+
ignore a few corner cases) unlike some other representations. This class contains a number of
|
|
58
|
+
methods for mesh manipulation and inspection. Note also that numerous further functions are
|
|
59
|
+
available to manipulate meshes stored as Manifolds.
|
|
60
|
+
|
|
61
|
+
Many of the functions below accept arguments called hid, fid, or vid. These are simply indices
|
|
62
|
+
of halfedges, faces and vertices, respectively: integer numbers that identify the corresponding
|
|
63
|
+
mesh element. Using a plain integer to identify a mesh entity means that, for instance, a
|
|
64
|
+
vertex index can also be used as an index into, say, a NumPy array without any conversion.
|
|
65
|
+
"""
|
|
66
|
+
def __init__(self, orig: Self | ct.c_void_p | None = None):
|
|
67
|
+
""" Construct a Manifold object. If orig is None, a new empty Manifold is created. If
|
|
68
|
+
orig is a Manifold, a copy of it is created. If orig is a c_void_p, it is assumed to be
|
|
69
|
+
a pointer to a Manifold object created in C++. In this case, the object is not copied,
|
|
70
|
+
but the pointer is used directly. If orig is anything else, a TypeError is raised.
|
|
71
|
+
"""
|
|
72
|
+
if orig == None:
|
|
73
|
+
self.obj = lib_py_gel.Manifold_new()
|
|
74
|
+
elif isinstance(orig, Manifold):
|
|
75
|
+
self.obj = lib_py_gel.Manifold_copy(orig.obj)
|
|
76
|
+
elif isinstance(orig, ct.c_void_p):
|
|
77
|
+
self.obj = orig
|
|
78
|
+
else:
|
|
79
|
+
raise TypeError(f"Manifold constructor takes either a Manifold or a c_void_p as argument, not {type(orig)}")
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_triangles(cls, vertices: ArrayLike, faces: ArrayLike) -> Self:
|
|
83
|
+
""" Given a list of vertices and triangles (faces), this function produces
|
|
84
|
+
a Manifold mesh."""
|
|
85
|
+
m = cls()
|
|
86
|
+
vertices = np.asarray(vertices,dtype=np.float64, order='C')
|
|
87
|
+
faces = np.asarray(faces,dtype=ct.c_int, order='C')
|
|
88
|
+
m.obj = lib_py_gel.Manifold_from_triangles(_n_vec3(vertices), _n_vec3(faces), vertices, faces)
|
|
89
|
+
return m
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_points(cls, pts: ArrayLike, xaxis: ArrayLike = np.array([1,0,0]), yaxis: ArrayLike = np.array([0,1,0])) -> Self:
|
|
92
|
+
""" This function computes the Delaunay triangulation of pts. You need
|
|
93
|
+
to specify xaxis and yaxis if they are not canonical. The function returns
|
|
94
|
+
a Manifold with the resulting triangles. Clearly, this function will
|
|
95
|
+
give surprising results if the surface represented by the points is not
|
|
96
|
+
well represented as a 2.5D surface, aka a height field. """
|
|
97
|
+
m = cls()
|
|
98
|
+
pts = np.asarray(pts,dtype=np.float64, order='C')
|
|
99
|
+
if pts.size % 3 != 0:
|
|
100
|
+
raise ValueError("from_points: pts must be a flat array with length a multiple of 3")
|
|
101
|
+
xaxis = np.asarray(xaxis,dtype=np.float64, order='C')
|
|
102
|
+
if xaxis.size != 3:
|
|
103
|
+
raise ValueError("from_points: xaxis must be a 3D vector")
|
|
104
|
+
yaxis = np.asarray(yaxis,dtype=np.float64, order='C')
|
|
105
|
+
if yaxis.size != 3:
|
|
106
|
+
raise ValueError("from_points: yaxis must be a 3D vector")
|
|
107
|
+
m.obj = lib_py_gel.Manifold_from_points(_n_vec3(pts), pts, xaxis, yaxis)
|
|
108
|
+
return m
|
|
109
|
+
def __del__(self):
|
|
110
|
+
lib_py_gel.Manifold_delete(self.obj)
|
|
111
|
+
def merge_with(self, other: Self):
|
|
112
|
+
""" Merge this Manifold with another one given as the argument. This function
|
|
113
|
+
does not return anything. It simply combines the two meshes in the Manifold on which
|
|
114
|
+
the method is called. """
|
|
115
|
+
lib_py_gel.Manifold_merge(self.obj, other.obj)
|
|
116
|
+
def add_face(self, pts: ArrayLike) -> int:
|
|
117
|
+
""" Add a face to the Manifold.
|
|
118
|
+
This function takes a list of 3D points, pts, as argument and creates a face
|
|
119
|
+
in the mesh with those points as vertices. The function returns the index
|
|
120
|
+
of the created face.
|
|
121
|
+
"""
|
|
122
|
+
pts = np.asarray(pts,dtype=np.float64, order='C')
|
|
123
|
+
if pts.size % 3 != 0:
|
|
124
|
+
raise ValueError("add_face: pts must be a flat array with length a multiple of 3")
|
|
125
|
+
return lib_py_gel.Manifold_add_face(self.obj, _n_vec3(pts), pts)
|
|
126
|
+
def positions(self) -> ndarray:
|
|
127
|
+
""" Retrieve an array containing the vertex positions of the Manifold.
|
|
128
|
+
It is not a copy: any changes are made to the actual vertex positions. """
|
|
129
|
+
pos = ct.POINTER(ct.c_double)()
|
|
130
|
+
n = lib_py_gel.Manifold_positions(self.obj, ct.byref(pos))
|
|
131
|
+
return np.ctypeslib.as_array(pos,(n,3))
|
|
132
|
+
def no_allocated_vertices(self) -> int:
|
|
133
|
+
""" Number of vertices.
|
|
134
|
+
This number could be higher than the number of actually
|
|
135
|
+
used vertices, but corresponds to the size of the array allocated
|
|
136
|
+
for vertices."""
|
|
137
|
+
return lib_py_gel.Manifold_no_allocated_vertices(self.obj)
|
|
138
|
+
def no_allocated_faces(self) -> int:
|
|
139
|
+
""" Number of faces.
|
|
140
|
+
This number could be higher than the number of actually
|
|
141
|
+
used faces, but corresponds to the size of the array allocated
|
|
142
|
+
for faces."""
|
|
143
|
+
return lib_py_gel.Manifold_no_allocated_faces(self.obj)
|
|
144
|
+
def no_allocated_halfedges(self) -> int:
|
|
145
|
+
""" Number of halfedges.
|
|
146
|
+
This number could be higher than the number of actually
|
|
147
|
+
used halfedges, but corresponds to the size of the array allocated
|
|
148
|
+
for halfedges."""
|
|
149
|
+
return lib_py_gel.Manifold_no_allocated_halfedges(self.obj)
|
|
150
|
+
def vertices(self) -> IntVector:
|
|
151
|
+
""" Returns an iterable containing all vertex indices"""
|
|
152
|
+
verts = IntVector()
|
|
153
|
+
lib_py_gel.Manifold_vertices(self.obj, verts.obj)
|
|
154
|
+
return verts
|
|
155
|
+
def faces(self) -> IntVector:
|
|
156
|
+
""" Returns an iterable containing all face indices"""
|
|
157
|
+
faces = IntVector()
|
|
158
|
+
lib_py_gel.Manifold_faces(self.obj, faces.obj)
|
|
159
|
+
return faces
|
|
160
|
+
def halfedges(self) -> IntVector:
|
|
161
|
+
""" Returns an iterable containing all halfedge indices"""
|
|
162
|
+
hedges = IntVector()
|
|
163
|
+
lib_py_gel.Manifold_halfedges(self.obj, hedges.obj)
|
|
164
|
+
return hedges
|
|
165
|
+
def circulate_vertex(self, vid: int, mode: str ='v') -> IntVector:
|
|
166
|
+
""" Circulate a vertex. Passed a vertex index, vid, and second argument,
|
|
167
|
+
mode='f', this function will return an iterable with all faces incident
|
|
168
|
+
on vid arranged in counter clockwise order. Similarly, if mode is 'h',
|
|
169
|
+
incident halfedges (outgoing) are returned, and for mode = 'v', all
|
|
170
|
+
neighboring vertices are returned. """
|
|
171
|
+
nbrs = IntVector()
|
|
172
|
+
lib_py_gel.Manifold_circulate_vertex(self.obj, vid, ct.c_char(mode.encode('ascii')), nbrs.obj)
|
|
173
|
+
return nbrs
|
|
174
|
+
def circulate_face(self, fid: int, mode: str ='v') -> IntVector:
|
|
175
|
+
""" Circulate a face. Passed a face index, fid, and second argument,
|
|
176
|
+
mode='f', this function will return an iterable with all faces that
|
|
177
|
+
share an edge with fid (in counter clockwise order). If the argument is
|
|
178
|
+
mode='h', the halfedges themselves are returned. For mode='v', the
|
|
179
|
+
incident vertices of the face are returned. """
|
|
180
|
+
nbrs = IntVector()
|
|
181
|
+
lib_py_gel.Manifold_circulate_face(self.obj, fid, ct.c_char(mode.encode('ascii')), nbrs.obj)
|
|
182
|
+
return nbrs
|
|
183
|
+
def next_halfedge(self, hid: int) -> int:
|
|
184
|
+
""" Returns next halfedge to hid. """
|
|
185
|
+
return lib_py_gel.Walker_next_halfedge(self.obj, hid)
|
|
186
|
+
def prev_halfedge(self, hid: int) -> int:
|
|
187
|
+
""" Returns previous halfedge to hid. """
|
|
188
|
+
return lib_py_gel.Walker_prev_halfedge(self.obj, hid)
|
|
189
|
+
def opposite_halfedge(self, hid: int) -> int:
|
|
190
|
+
""" Returns opposite halfedge to hid. """
|
|
191
|
+
return lib_py_gel.Walker_opposite_halfedge(self.obj, hid)
|
|
192
|
+
def incident_face(self, hid: int) -> int:
|
|
193
|
+
""" Returns face corresponding to hid. """
|
|
194
|
+
return lib_py_gel.Walker_incident_face(self.obj, hid)
|
|
195
|
+
def incident_vertex(self, hid: int) -> int:
|
|
196
|
+
""" Returns vertex corresponding to (or pointed to by) hid. """
|
|
197
|
+
return lib_py_gel.Walker_incident_vertex(self.obj, hid)
|
|
198
|
+
def remove_vertex(self, vid: int) -> bool:
|
|
199
|
+
""" Remove vertex vid from the Manifold. This function merges all faces
|
|
200
|
+
around the vertex into one and then removes this resulting face. """
|
|
201
|
+
return lib_py_gel.Manifold_remove_vertex(self.obj, vid)
|
|
202
|
+
def remove_face(self, fid: int) -> bool:
|
|
203
|
+
""" Removes a face, fid, from the Manifold. If it is an interior face it is
|
|
204
|
+
simply replaced by an invalid index. If the face contains boundary
|
|
205
|
+
edges, these are removed. Situations may arise where the mesh is no
|
|
206
|
+
longer manifold because the situation at a boundary vertex is not
|
|
207
|
+
homeomorphic to a half disk. This, we can probably ignore since from the
|
|
208
|
+
data structure point of view it is not really a problem that a vertex is
|
|
209
|
+
incident on two holes - a hole can be seen as a special type of face.
|
|
210
|
+
The function returns false if the index of the face is not valid,
|
|
211
|
+
otherwise the function must complete. """
|
|
212
|
+
return lib_py_gel.Manifold_remove_face(self.obj, fid)
|
|
213
|
+
def remove_edge(self, hid: int) -> bool:
|
|
214
|
+
""" Remove an edge, hid, from the Manifold. This function will remove the
|
|
215
|
+
faces on either side and the edge itself in the process. Thus, it is a
|
|
216
|
+
simple application of remove_face. """
|
|
217
|
+
return lib_py_gel.Manifold_remove_edge(self.obj, hid)
|
|
218
|
+
def vertex_in_use(self, vid: int) -> bool:
|
|
219
|
+
""" check if vertex, vid, is in use. This function returns true if the id corresponds
|
|
220
|
+
to a vertex that is currently in the mesh and false otherwise. vid could
|
|
221
|
+
be invalid or it could correspond to a vertex which is not active. The function returns
|
|
222
|
+
false in both cases. It is important to call this function before using a vertex id (vid)
|
|
223
|
+
which might have been invalidated by a previous operation on the mesh."""
|
|
224
|
+
return lib_py_gel.Manifold_vertex_in_use(self.obj, vid)
|
|
225
|
+
def face_in_use(self, fid: int) -> bool:
|
|
226
|
+
""" check if face, fid, is in use. This function returns true if the id corresponds
|
|
227
|
+
to a face that is currently in the mesh and false otherwise. fid could
|
|
228
|
+
be invalid or it could correspond to a face which is not active. The function returns
|
|
229
|
+
false in both cases. It is important to call this function before using a face id (fid)
|
|
230
|
+
which might have been invalidated by a previous operation on the mesh.
|
|
231
|
+
"""
|
|
232
|
+
return lib_py_gel.Manifold_face_in_use(self.obj, fid)
|
|
233
|
+
def halfedge_in_use(self, hid: int) -> bool:
|
|
234
|
+
""" check if halfedge hid is in use. This function returns true if the id corresponds
|
|
235
|
+
to a halfedge that is currently in the mesh and false otherwise. hid could
|
|
236
|
+
be invalid or it could correspond to a halfedge which is not active. The function returns
|
|
237
|
+
false in both cases. It is important to call this function before using a halfedge id (hid)
|
|
238
|
+
which might have been invalidated by a previous operation on the mesh."""
|
|
239
|
+
return lib_py_gel.Manifold_halfedge_in_use(self.obj, hid)
|
|
240
|
+
def flip_edge(self, hid: int) -> bool:
|
|
241
|
+
""" Flip the edge, hid, separating two faces. The function first verifies that
|
|
242
|
+
the edge is flippable. This entails making sure that all of the
|
|
243
|
+
following are true.
|
|
244
|
+
1. adjacent faces are triangles.
|
|
245
|
+
2. neither end point has valency three or less.
|
|
246
|
+
3. the vertices that will be connected are not already.
|
|
247
|
+
If the tests are passed, the flip is performed and the function
|
|
248
|
+
returns True. Otherwise False."""
|
|
249
|
+
return lib_py_gel.Manifold_flip_edge(self.obj,hid)
|
|
250
|
+
def collapse_edge(self, hid: int, avg_vertices: bool = False) -> bool:
|
|
251
|
+
""" Collapse an edge hid.
|
|
252
|
+
The vertex incident_vertex(opposite_halfedge) is the one being removed
|
|
253
|
+
while incident_vertex(hid) survives. avg_vertices indicates whether the
|
|
254
|
+
positions of the two vertices should be averaged when collapsed.
|
|
255
|
+
Before collapsing hid, a number of tests are made:
|
|
256
|
+
---
|
|
257
|
+
1. For the two vertices adjacent to the edge, we generate a list of all their neighbouring vertices.
|
|
258
|
+
We then generate a list of the vertices that occur in both these lists.
|
|
259
|
+
That is, we find all vertices connected by edges to both endpoints of the edge and store these in a list.
|
|
260
|
+
2. For both faces incident on the edge, check whether they are triangular.
|
|
261
|
+
If this is the case, the face will be removed, and it is ok that the the third vertex is connected to both endpoints.
|
|
262
|
+
Thus the third vertex in such a face is removed from the list generated in 1.
|
|
263
|
+
3. If the list is now empty, all is well.
|
|
264
|
+
Otherwise, there would be a vertex in the new mesh with two edges connecting it to the same vertex. Return false.
|
|
265
|
+
4. TETRAHEDRON TEST:
|
|
266
|
+
If the valency of both vertices is three, and the incident faces are triangles, we also disallow the operation.
|
|
267
|
+
Reason: A vertex valency of two and two triangles incident on the adjacent vertices makes the construction collapse.
|
|
268
|
+
5. VALENCY 4 TEST:
|
|
269
|
+
If a triangle is adjacent to the edge being collapsed, it disappears.
|
|
270
|
+
This means the valency of the remaining edge vertex is decreased by one.
|
|
271
|
+
A valency two vertex reduced to a valency one vertex is considered illegal.
|
|
272
|
+
6. PREVENT MERGING HOLES:
|
|
273
|
+
Collapsing an edge with boundary endpoints and valid faces results in the creation where two holes meet.
|
|
274
|
+
A non manifold situation. We could relax this...
|
|
275
|
+
7. New test: if the same face is in the one-ring of both vertices but not adjacent to the common edge,
|
|
276
|
+
then the result of a collapse would be a one ring where the same face occurs twice. This is disallowed as the resulting
|
|
277
|
+
face would be non-simple.
|
|
278
|
+
If the tests are passed, the collapse is performed and the function
|
|
279
|
+
returns True. Otherwise False."""
|
|
280
|
+
return lib_py_gel.Manifold_collapse_edge(self.obj, hid, avg_vertices)
|
|
281
|
+
def split_face_by_edge(self, fid: int, v0: int, v1: int) -> int:
|
|
282
|
+
""" Split a face. The face, fid, is split by creating an edge with
|
|
283
|
+
endpoints v0 and v1 (the next two arguments). The vertices of the old
|
|
284
|
+
face between v0 and v1 (in counter clockwise order) continue to belong
|
|
285
|
+
to fid. The vertices between v1 and v0 belong to the new face. A handle to
|
|
286
|
+
the new face is returned. """
|
|
287
|
+
return lib_py_gel.Manifold_split_face_by_edge(self.obj, fid, v0, v1)
|
|
288
|
+
def split_face_by_vertex(self,fid: int) -> int:
|
|
289
|
+
""" Split a polygon, fid, by inserting a vertex at the barycenter. This
|
|
290
|
+
function is less likely to create flipped triangles than the
|
|
291
|
+
split_face_triangulate function. On the other hand, it introduces more
|
|
292
|
+
vertices and probably makes the triangles more acute. The vertex id of the
|
|
293
|
+
inserted vertex is returned. """
|
|
294
|
+
return lib_py_gel.Manifold_split_face_by_vertex(self.obj,fid)
|
|
295
|
+
def split_edge(self,hid: int) -> int:
|
|
296
|
+
""" Insert a new vertex on halfedge hid. The new halfedge is insterted
|
|
297
|
+
as the previous edge to hid. The vertex id of the inserted vertex is returned. """
|
|
298
|
+
return lib_py_gel.Manifold_split_edge(self.obj,hid)
|
|
299
|
+
def stitch_boundary_edges(self,h0: int, h1: int) -> bool:
|
|
300
|
+
""" Stitch two halfedges. Two boundary halfedges, h0 and h1, can be stitched
|
|
301
|
+
together. This can be used to build a complex mesh from a bunch of
|
|
302
|
+
simple faces. """
|
|
303
|
+
return lib_py_gel.Manifold_stitch_boundary_edges(self.obj, h0, h1)
|
|
304
|
+
def merge_faces(self,hid: int) -> bool:
|
|
305
|
+
""" Merges two faces into a single polygon. The merged faces are those shared
|
|
306
|
+
by the edge for which hid is one of the two corresponding halfedges. This function returns
|
|
307
|
+
true if the merging was possible and false otherwise. Currently merge
|
|
308
|
+
only fails if the mesh is already illegal. Thus it should, in fact,
|
|
309
|
+
never fail. """
|
|
310
|
+
if self.is_halfedge_at_boundary(hid):
|
|
311
|
+
return False
|
|
312
|
+
fid = self.incident_face(hid)
|
|
313
|
+
return lib_py_gel.Manifold_merge_faces(self.obj, fid, hid)
|
|
314
|
+
def close_hole(self,hid: int) -> int:
|
|
315
|
+
""" Close hole given by hid (i.e. the face referenced by hid). Returns
|
|
316
|
+
index of the created face or the face that was already there if, in
|
|
317
|
+
fact, hid was not next to a hole. """
|
|
318
|
+
return lib_py_gel.Manifold_close_hole(self.obj, hid)
|
|
319
|
+
def cleanup(self):
|
|
320
|
+
""" Remove unused items from Mesh. This function remaps all vertices, halfedges
|
|
321
|
+
and faces such that the arrays do not contain any holes left by unused mesh
|
|
322
|
+
entities. It is a good idea to call this function when a mesh has been simplified
|
|
323
|
+
or changed in other ways such that mesh entities have been removed. However, note
|
|
324
|
+
that it invalidates any attributes that you might have stored in auxilliary arrays."""
|
|
325
|
+
lib_py_gel.Manifold_cleanup(self.obj)
|
|
326
|
+
def is_halfedge_at_boundary(self, hid: int) -> bool:
|
|
327
|
+
""" Returns True if hid is a boundary halfedge, i.e. face on either
|
|
328
|
+
side is invalid. """
|
|
329
|
+
return lib_py_gel.is_halfedge_at_boundary(self.obj, hid)
|
|
330
|
+
def is_vertex_at_boundary(self, vid: int) -> bool:
|
|
331
|
+
""" Returns True if vid lies on a boundary. """
|
|
332
|
+
return lib_py_gel.is_vertex_at_boundary(self.obj, vid)
|
|
333
|
+
def edge_length(self, hid: int) -> float:
|
|
334
|
+
""" Returns length of edge given by halfedge hid which is passed as argument. """
|
|
335
|
+
return lib_py_gel.length(self.obj, hid)
|
|
336
|
+
def valency(self,vid: int) -> int:
|
|
337
|
+
""" Returns valency of vid, i.e. number of incident edges."""
|
|
338
|
+
return lib_py_gel.valency(self.obj,vid)
|
|
339
|
+
def face_normal(self, fid: int) -> ndarray:
|
|
340
|
+
""" Compute the normal of a face fid. The normal is the average of the normals
|
|
341
|
+
of the triangles formed from the centroid of face fix and each edge of the face."""
|
|
342
|
+
n = ndarray(3, dtype=np.float64)
|
|
343
|
+
lib_py_gel.face_normal(self.obj, fid, n)
|
|
344
|
+
return n
|
|
345
|
+
def vertex_normal(self, vid: int) -> ndarray:
|
|
346
|
+
""" Returns the vertex normal of vid. The vertex normal is computed as the
|
|
347
|
+
angle weighted average of the normals of the incident faces."""
|
|
348
|
+
n = ndarray(3,dtype=np.float64)
|
|
349
|
+
lib_py_gel.vertex_normal(self.obj, vid, n)
|
|
350
|
+
return n
|
|
351
|
+
def mixed_area(self, vid: int) -> float:
|
|
352
|
+
""" Returns the mixed area of vertex vid. The mixed area is an approximation
|
|
353
|
+
of the Voronoi area of the vertex, i.e. the area of the mesh that is closer
|
|
354
|
+
to vid than to any other vertex. For non-obtuse triangles, we can compute the
|
|
355
|
+
part of the Voronoi area inside the triangle exacxtly, but for obtuse triangles
|
|
356
|
+
the Voronoi area extends outside the triangle, and we approximate it. """
|
|
357
|
+
return lib_py_gel.mixed_area(self.obj, vid)
|
|
358
|
+
def gaussian_curvature(self, vid: int) -> float:
|
|
359
|
+
""" Returns the Gaussian curvature of vertex vid. The curvature is computed
|
|
360
|
+
as the ratio of the angle defect and the mixed area of vid.
|
|
361
|
+
The angle defect is 2*pi minus the sum of angles at the vertex. """
|
|
362
|
+
return lib_py_gel.gaussian_curvature(self.obj, vid)
|
|
363
|
+
def mean_curvature(self, vid: int) -> float:
|
|
364
|
+
""" Returns the mean curvature of vertex vid. The curvature is computed
|
|
365
|
+
as the ratio of the length of the mean curvaure normal to the mixed area
|
|
366
|
+
of vid. The mean curvature normal is obtained with the cotan formula, and
|
|
367
|
+
the sign is positive if the mean curvature normal points in the same direction
|
|
368
|
+
as the vertex normal and negative otherwise. """
|
|
369
|
+
return lib_py_gel.mean_curvature(self.obj, vid)
|
|
370
|
+
def principal_curvatures(self, vid: int) -> tuple[float, float, ndarray, ndarray]:
|
|
371
|
+
""" Returns the principal curvatures of vertex vid. The principal curvatures
|
|
372
|
+
are computed by fitting a quadratic polynomial surface to the vertex and its
|
|
373
|
+
one-ring neighbours. From the coefficients, we obtain the shape operator and
|
|
374
|
+
the principal curvatures are the eigenvalues of the shape operator. The directions
|
|
375
|
+
are the eigenvectors. The function returns a tuple consiting of four values:
|
|
376
|
+
min and max principal curvature followed by the corresponding principal directions
|
|
377
|
+
as 3D vectors"""
|
|
378
|
+
pc_data = ndarray(8, dtype=np.float64)
|
|
379
|
+
lib_py_gel.principal_curvatures(self.obj, vid, pc_data)
|
|
380
|
+
return (
|
|
381
|
+
pc_data[0], # min curvature
|
|
382
|
+
pc_data[1], # max curvature
|
|
383
|
+
pc_data[2:5], # min direction
|
|
384
|
+
pc_data[5:8] # max direction
|
|
385
|
+
)
|
|
386
|
+
def connected(self, v0: int, v1: int) -> bool:
|
|
387
|
+
""" Returns true if the two argument vertices, v0 and v1, are in each other's one-rings."""
|
|
388
|
+
return lib_py_gel.connected(self.obj,v0,v1)
|
|
389
|
+
def no_edges(self, fid: int) -> int:
|
|
390
|
+
""" Compute the number of edges of a face fid """
|
|
391
|
+
return lib_py_gel.no_edges(self.obj, fid)
|
|
392
|
+
def area(self, fid: int) -> float:
|
|
393
|
+
""" Returns the area of a face fid. """
|
|
394
|
+
return lib_py_gel.area(self.obj, fid)
|
|
395
|
+
def perimeter(self, fid: int) -> float:
|
|
396
|
+
""" Returns the perimeter of a face fid. """
|
|
397
|
+
return lib_py_gel.perimeter(self.obj, fid)
|
|
398
|
+
def centre(self, fid: int) -> ndarray:
|
|
399
|
+
""" Returns the centre of a face. """
|
|
400
|
+
c = ndarray(3, dtype=np.float64)
|
|
401
|
+
lib_py_gel.centre(self.obj, fid, c)
|
|
402
|
+
return c
|
|
403
|
+
|
|
404
|
+
def valid(m: Manifold) -> bool:
|
|
405
|
+
"""This function performs a series of tests to check that this
|
|
406
|
+
is a valid manifold. This function is not rigorously constructed but seems
|
|
407
|
+
to catch all problems so far. The function returns true if the mesh is valid
|
|
408
|
+
and false otherwise. """
|
|
409
|
+
return lib_py_gel.valid(m.obj)
|
|
410
|
+
|
|
411
|
+
def closed(m: Manifold) -> bool:
|
|
412
|
+
""" Returns true if m is closed, i.e. has no boundary."""
|
|
413
|
+
return lib_py_gel.closed(m.obj)
|
|
414
|
+
|
|
415
|
+
def area(m: Manifold) -> float:
|
|
416
|
+
""" This function computes the sum of all the faces' areas """
|
|
417
|
+
return lib_py_gel.total_area(m.obj)
|
|
418
|
+
|
|
419
|
+
def volume(m: Manifold) -> float:
|
|
420
|
+
""" Computes the volume of a mesh. Presupposes that the mesh is closed. """
|
|
421
|
+
return lib_py_gel.volume(m.obj)
|
|
422
|
+
|
|
423
|
+
def bbox(m: Manifold) -> tuple[ndarray, ndarray]:
|
|
424
|
+
""" Returns the min and max corners of the bounding box of Manifold m. """
|
|
425
|
+
pmin = ndarray(3,dtype=np.float64)
|
|
426
|
+
pmax = ndarray(3,dtype=np.float64)
|
|
427
|
+
lib_py_gel.bbox(m.obj, pmin, pmax)
|
|
428
|
+
return pmin, pmax
|
|
429
|
+
|
|
430
|
+
def bsphere(m: Manifold) -> tuple[ndarray, float]:
|
|
431
|
+
""" Calculate the bounding sphere of the manifold m.
|
|
432
|
+
Returns centre,radius """
|
|
433
|
+
c = ndarray(3,dtype=np.float64)
|
|
434
|
+
r = ct.c_double()
|
|
435
|
+
lib_py_gel.bsphere(m.obj, c, ct.byref(r))
|
|
436
|
+
return (c,r.value)
|
|
437
|
+
|
|
438
|
+
def stitch(m: Manifold, rad: float = 1e-30) -> int:
|
|
439
|
+
""" Stitch together edges of m whose endpoints coincide geometrically. This
|
|
440
|
+
function allows you to create a mesh as a bunch of faces and then stitch
|
|
441
|
+
these together to form a coherent whole. What this function adds is a
|
|
442
|
+
spatial data structure to find out which vertices coincide. The return value
|
|
443
|
+
is the number of edges that could not be stitched. Often this is because it
|
|
444
|
+
would introduce a non-manifold situation."""
|
|
445
|
+
return lib_py_gel.stitch_mesh(m.obj,rad)
|
|
446
|
+
|
|
447
|
+
def obj_save(fn: str, m: Manifold):
|
|
448
|
+
""" Save Manifold m to Wavefront obj file. """
|
|
449
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
450
|
+
lib_py_gel.obj_save(s, m.obj)
|
|
451
|
+
|
|
452
|
+
def off_save(fn: str, m: Manifold):
|
|
453
|
+
""" Save Manifold m to OFF file. """
|
|
454
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
455
|
+
lib_py_gel.off_save(s, m.obj)
|
|
456
|
+
|
|
457
|
+
def x3d_save(fn: str, m: Manifold):
|
|
458
|
+
""" Save Manifold m to X3D file. """
|
|
459
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
460
|
+
lib_py_gel.x3d_save(s, m.obj)
|
|
461
|
+
|
|
462
|
+
def obj_load(fn: str) -> Manifold | None:
|
|
463
|
+
""" Load and return Manifold from Wavefront obj file.
|
|
464
|
+
Returns None if loading failed. """
|
|
465
|
+
m = Manifold()
|
|
466
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
467
|
+
if lib_py_gel.obj_load(s, m.obj):
|
|
468
|
+
return m
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
def off_load(fn: str) -> Manifold | None:
|
|
472
|
+
""" Load and return Manifold from OFF file.
|
|
473
|
+
Returns None if loading failed."""
|
|
474
|
+
m = Manifold()
|
|
475
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
476
|
+
if lib_py_gel.off_load(s, m.obj):
|
|
477
|
+
return m
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
def ply_load(fn: str) -> Manifold | None:
|
|
481
|
+
""" Load and return Manifold from Stanford PLY file.
|
|
482
|
+
Returns None if loading failed. """
|
|
483
|
+
m = Manifold()
|
|
484
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
485
|
+
if lib_py_gel.ply_load(s, m.obj):
|
|
486
|
+
return m
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
def x3d_load(fn: str) -> Manifold | None:
|
|
490
|
+
""" Load and return Manifold from X3D file.
|
|
491
|
+
Returns None if loading failed."""
|
|
492
|
+
m = Manifold()
|
|
493
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
494
|
+
if lib_py_gel.x3d_load(s, m.obj):
|
|
495
|
+
return m
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
from os.path import splitext
|
|
499
|
+
def load(fn: str) -> Manifold | None:
|
|
500
|
+
""" Load a Manifold from an X3D/OBJ/OFF/PLY file. Return the
|
|
501
|
+
loaded Manifold. Returns None if loading failed."""
|
|
502
|
+
_, extension = splitext(fn)
|
|
503
|
+
if extension.lower() == ".x3d":
|
|
504
|
+
return x3d_load(fn)
|
|
505
|
+
if extension.lower() == ".obj":
|
|
506
|
+
return obj_load(fn)
|
|
507
|
+
if extension.lower() == ".off":
|
|
508
|
+
return off_load(fn)
|
|
509
|
+
if extension.lower() == ".ply":
|
|
510
|
+
return ply_load(fn)
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
def save(fn: str, m: Manifold):
|
|
514
|
+
""" Save a Manifold, m, to an X3D/OBJ/OFF file. """
|
|
515
|
+
_, extension = splitext(fn)
|
|
516
|
+
ext = extension.lower()
|
|
517
|
+
if ext == ".x3d":
|
|
518
|
+
x3d_save(fn, m)
|
|
519
|
+
elif ext == ".obj":
|
|
520
|
+
obj_save(fn, m)
|
|
521
|
+
elif ext == ".off":
|
|
522
|
+
off_save(fn, m)
|
|
523
|
+
else:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
f"hmesh.save: unsupported format '{extension}'. "
|
|
526
|
+
"Use .obj, .off, or .x3d"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def remove_caps(m: Manifold, thresh: float = 2.9):
|
|
531
|
+
""" Remove caps from a manifold, m, consisting of only triangles. A cap is a
|
|
532
|
+
triangle with two very small angles and an angle close to pi, however a cap
|
|
533
|
+
does not necessarily have a very short edge. Set the ang_thresh to a value
|
|
534
|
+
close to pi. The closer to pi the _less_ sensitive the cap removal. A cap is
|
|
535
|
+
removed by flipping the (long) edge E opposite to the vertex V with the
|
|
536
|
+
angle close to pi. However, the function is more complex. Read code and
|
|
537
|
+
document more carefully !!! """
|
|
538
|
+
lib_py_gel.remove_caps(m.obj,thresh)
|
|
539
|
+
|
|
540
|
+
def remove_needles(m: Manifold, thresh: float = 0.05, average_positions: bool = False):
|
|
541
|
+
""" Remove needles from a manifold, m, consisting of only triangles. A needle
|
|
542
|
+
is a triangle with a single very short edge. It is moved by collapsing the
|
|
543
|
+
short edge. The thresh parameter sets the length threshold (in terms of the
|
|
544
|
+
average edge length in the mesh). If average_positions is true then the
|
|
545
|
+
collapsed vertex is placed at the average position of the end points."""
|
|
546
|
+
abs_thresh = thresh * average_edge_length(m)
|
|
547
|
+
lib_py_gel.remove_needles(m.obj,abs_thresh, average_positions)
|
|
548
|
+
|
|
549
|
+
def close_holes(m: Manifold, max_size: int = 100):
|
|
550
|
+
""" This function replaces holes in m by faces. It is really a simple function
|
|
551
|
+
that just finds all loops of edges next to missing faces. """
|
|
552
|
+
lib_py_gel.close_holes(m.obj, max_size)
|
|
553
|
+
|
|
554
|
+
def flip_orientation(m: Manifold):
|
|
555
|
+
""" Flip the orientation of a mesh, m. After calling this function, normals
|
|
556
|
+
will point the other way and clockwise becomes counter clockwise """
|
|
557
|
+
lib_py_gel.flip_orientation(m.obj)
|
|
558
|
+
|
|
559
|
+
def merge_coincident_boundary_vertices(m: Manifold, rad: float = 1.0e-30):
|
|
560
|
+
""" Merge vertices of m that are boundary vertices and coincident.
|
|
561
|
+
However, if one belongs to the other's one ring or the one
|
|
562
|
+
rings share a vertex, they will not be merged. """
|
|
563
|
+
lib_py_gel.merge_coincident_boundary_vertices(m.obj, rad)
|
|
564
|
+
|
|
565
|
+
def minimize_curvature(m: Manifold, anneal: bool = False):
|
|
566
|
+
""" Minimizes mean curvature of m by flipping edges. Hence, no vertices are moved.
|
|
567
|
+
This is really the same as dihedral angle minimization, except that we weight by
|
|
568
|
+
edge length. """
|
|
569
|
+
lib_py_gel.minimize_curvature(m.obj, anneal)
|
|
570
|
+
|
|
571
|
+
def minimize_dihedral_angle(m: Manifold, max_iter: int = 10000, anneal: bool = False, alpha: bool = False, gamma: float = 4.0):
|
|
572
|
+
""" Minimizes dihedral angles in m by flipping edges.
|
|
573
|
+
Arguments:
|
|
574
|
+
max_iter is the maximum number of iterations for simulated annealing.
|
|
575
|
+
anneal tells us the code whether to apply simulated annealing
|
|
576
|
+
alpha=False means that we use the cosine of angles rather than true angles (faster)
|
|
577
|
+
gamma is the power to which the angles are raised."""
|
|
578
|
+
lib_py_gel.minimize_dihedral_angle(m.obj, max_iter, anneal,alpha,ct.c_double(gamma))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def maximize_min_angle(m: Manifold, dihedral_thresh: float = 0.95, anneal: bool = False):
|
|
582
|
+
""" Maximizes the minimum angle of triangles by flipping edges of m. Makes the
|
|
583
|
+
mesh more Delaunay."""
|
|
584
|
+
lib_py_gel.maximize_min_angle(m.obj,dihedral_thresh,anneal)
|
|
585
|
+
|
|
586
|
+
def optimize_valency(m: Manifold, anneal: bool = False):
|
|
587
|
+
""" Tries to achieve valence 6 internally and 4 along edges by flipping edges
|
|
588
|
+
of m. """
|
|
589
|
+
lib_py_gel.optimize_valency(m.obj, anneal)
|
|
590
|
+
|
|
591
|
+
def randomize_mesh(m: Manifold, max_iter: int = 1):
|
|
592
|
+
""" Make random flips in m. Useful for generating synthetic test cases. """
|
|
593
|
+
lib_py_gel.randomize_mesh(m.obj, max_iter)
|
|
594
|
+
|
|
595
|
+
def quadric_simplify(m: Manifold, keep_fraction: float, singular_thresh: float = 1e-4, error_thresh: float = 1):
|
|
596
|
+
""" Garland Heckbert simplification of mesh m. keep_fraction is the fraction of vertices
|
|
597
|
+
to retain. The singular_thresh determines how subtle features are preserved. For values
|
|
598
|
+
close to 1 the surface is treated as smooth even in the presence of sharp edges of low
|
|
599
|
+
dihedral angle (angle between normals). Close to zero, the method preserves even subtle
|
|
600
|
+
sharp features better. The error_thresh is the value of the QEM error at which
|
|
601
|
+
simplification stops. It is relative to the bounding box size. The default value is 1
|
|
602
|
+
meaning that simplification continues until the model has been simplified to a number of
|
|
603
|
+
vertices approximately equal to keep_fraction times the original number of vertices."""
|
|
604
|
+
lib_py_gel.quadric_simplify(m.obj, keep_fraction, singular_thresh,error_thresh)
|
|
605
|
+
|
|
606
|
+
def average_edge_length(m: Manifold):
|
|
607
|
+
""" Returns the average edge length of mesh m. """
|
|
608
|
+
return lib_py_gel.average_edge_length(m.obj)
|
|
609
|
+
|
|
610
|
+
def median_edge_length(m: Manifold):
|
|
611
|
+
""" Returns the median edge length of m"""
|
|
612
|
+
return lib_py_gel.median_edge_length(m.obj)
|
|
613
|
+
|
|
614
|
+
def refine_edges(m: Manifold, threshold: float):
|
|
615
|
+
""" Split all edges in m which are longer
|
|
616
|
+
than the threshold (second arg) length. A split edge
|
|
617
|
+
results in a new vertex of valence two."""
|
|
618
|
+
return lib_py_gel.refine_edges(m.obj, threshold)
|
|
619
|
+
|
|
620
|
+
def cc_split(m: Manifold):
|
|
621
|
+
""" Perform a Catmull-Clark split on m, i.e. a split where each face is divided
|
|
622
|
+
into new quadrilateral faces formed by connecting a corner with a point on
|
|
623
|
+
each incident edge and a point at the centre of the face."""
|
|
624
|
+
lib_py_gel.cc_split(m.obj)
|
|
625
|
+
|
|
626
|
+
def loop_split(m: Manifold):
|
|
627
|
+
""" Perform a loop split on m where each edge is divided into two segments, and
|
|
628
|
+
four new triangles are created for each original triangle. """
|
|
629
|
+
lib_py_gel.loop_split(m.obj)
|
|
630
|
+
|
|
631
|
+
def root3_subdivide(m: Manifold):
|
|
632
|
+
""" Leif Kobbelt's subdivision scheme applied to m. A vertex is placed in the
|
|
633
|
+
center of each face and all old edges are flipped. """
|
|
634
|
+
lib_py_gel.root3_subdivide(m.obj)
|
|
635
|
+
|
|
636
|
+
def rootCC_subdivide(m: Manifold):
|
|
637
|
+
""" This subdivision scheme creates a vertex inside each original (quad) face of m,
|
|
638
|
+
producing four triangles. Triangles sharing an old edge are then merged.
|
|
639
|
+
Two steps produce something similar to Catmull-Clark. """
|
|
640
|
+
lib_py_gel.rootCC_subdivide(m.obj)
|
|
641
|
+
|
|
642
|
+
def butterfly_subdivide(m: Manifold):
|
|
643
|
+
""" Butterfly subidiviosn on m. An interpolatory scheme. Creates the same connectivity as Loop. """
|
|
644
|
+
lib_py_gel.butterfly_subdivide(m.obj)
|
|
645
|
+
|
|
646
|
+
def cc_smooth(m: Manifold, no_iters: int = 1):
|
|
647
|
+
""" If called after cc_split, this function completes a step of Catmull-Clark
|
|
648
|
+
subdivision of m."""
|
|
649
|
+
for _ in range(no_iters):
|
|
650
|
+
lib_py_gel.cc_smooth(m.obj)
|
|
651
|
+
|
|
652
|
+
def cc_subdivide(m: Manifold):
|
|
653
|
+
""" Perform a full Catmull-Clark subdivision step on mesh m. """
|
|
654
|
+
lib_py_gel.cc_split(m.obj)
|
|
655
|
+
lib_py_gel.cc_smooth(m.obj)
|
|
656
|
+
|
|
657
|
+
def loop_subdivide(m: Manifold):
|
|
658
|
+
""" Perform a full Loop subdivision step on mesh m. """
|
|
659
|
+
lib_py_gel.loop_split(m.obj)
|
|
660
|
+
lib_py_gel.loop_smooth(m.obj)
|
|
661
|
+
|
|
662
|
+
def volume_preserving_cc_smooth(m: Manifold, no_iters: int = 1):
|
|
663
|
+
""" This function does the same type of smoothing as in Catmull-Clark
|
|
664
|
+
subdivision, but to preserve volume it actually performs two steps, and the
|
|
665
|
+
second step is negative as in Taubin smoothing."""
|
|
666
|
+
lib_py_gel.volume_preserving_cc_smooth(m.obj, no_iters)
|
|
667
|
+
|
|
668
|
+
def regularize_quads(m: Manifold, w: float = 0.5, shrink: float = 0.0, no_iters: int = 1):
|
|
669
|
+
""" This function smooths a quad mesh by regularizing quads. Essentially,
|
|
670
|
+
regularization just makes them more rectangular. """
|
|
671
|
+
lib_py_gel.regularize_quads(m.obj, w, shrink, no_iters)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def loop_smooth(m: Manifold):
|
|
675
|
+
""" If called after Loop split, this function completes a step of Loop
|
|
676
|
+
subdivision of m. """
|
|
677
|
+
lib_py_gel.loop_smooth(m.obj)
|
|
678
|
+
|
|
679
|
+
def taubin_smooth(m: Manifold, no_iters: int = 1):
|
|
680
|
+
""" This function performs Taubin smoothing on the mesh m for iter number
|
|
681
|
+
of iterations. """
|
|
682
|
+
lib_py_gel.taubin_smooth(m.obj, no_iters)
|
|
683
|
+
|
|
684
|
+
def laplacian_smooth(m: Manifold, w: float = 0.5, no_iters: int = 1):
|
|
685
|
+
""" This function performs Laplacian smoothing on the mesh m for iter number
|
|
686
|
+
of iterations. w is the weight applied. """
|
|
687
|
+
lib_py_gel.laplacian_smooth(m.obj, w, no_iters)
|
|
688
|
+
|
|
689
|
+
def anisotropic_smooth(m: Manifold, sharpness: float = 0.5, no_iters: int = 1):
|
|
690
|
+
""" This function performs anisotropic smoothing on the mesh m for iter number
|
|
691
|
+
of iterations. A bilateral filtering controlled by sharpness is performed on
|
|
692
|
+
the face normals followed by a rotation of the faces to match the new normals.
|
|
693
|
+
The updated vertex positions are the average positions of the corners of the
|
|
694
|
+
rotated faces. For sharpness==0 the new normal is simply the area weighted
|
|
695
|
+
average of the normals of incident faces. For sharpness>0 the weight of the
|
|
696
|
+
neighbouring face normals is a Gaussian function of the angle between the
|
|
697
|
+
face normals. The greater the sharpness, the more the smoothing is anisotropic."""
|
|
698
|
+
lib_py_gel.anisotropic_smooth(m.obj, sharpness, no_iters)
|
|
699
|
+
|
|
700
|
+
def volumetric_isocontour(data: ArrayLike,
|
|
701
|
+
bbox_min: ArrayLike | None = None,
|
|
702
|
+
bbox_max: ArrayLike | None = None,
|
|
703
|
+
tau: float = 0.0,
|
|
704
|
+
make_triangles: bool = True,
|
|
705
|
+
high_is_inside: bool = True,
|
|
706
|
+
dual_connectivity: bool = False) -> Manifold:
|
|
707
|
+
""" Creates a polygonal mesh from volumetric data by isocontouring. The dimensions
|
|
708
|
+
are given by dims, bbox_min (defaults to [0,0,0] ) and bbox_max (defaults to dims) are
|
|
709
|
+
the corners of the bounding box in R^3 that corresponds to the volumetric grid, tau is
|
|
710
|
+
the iso value (defaults to 0). If make_triangles is True (default), we turn the quads
|
|
711
|
+
into triangles. Finally, high_is_inside=True (default) means that values greater than
|
|
712
|
+
tau are interior and smaller values are exterior. If dual_connectivity is False (default)
|
|
713
|
+
the function produces marching cubes connectivity and if True it produces dual contouring
|
|
714
|
+
connectivity. MC connectivity tends to produce less nice triangle shapes but since the
|
|
715
|
+
vertices always lie on edges, the geometry is arguably better defined for MC. """
|
|
716
|
+
m = Manifold()
|
|
717
|
+
dims = data.shape
|
|
718
|
+
if bbox_min is None:
|
|
719
|
+
bbox_min = (0,0,0)
|
|
720
|
+
if bbox_max is None:
|
|
721
|
+
bbox_max = dims
|
|
722
|
+
data_float = np.asarray(data, dtype=ct.c_float, order='F')
|
|
723
|
+
bbox_min_d = np.asarray(bbox_min, dtype=np.float64, order='C')
|
|
724
|
+
bbox_max_d = np.asarray(bbox_max, dtype=np.float64, order='C')
|
|
725
|
+
lib_py_gel.volumetric_isocontour(m.obj, dims[0], dims[1], dims[2],
|
|
726
|
+
data_float, bbox_min_d, bbox_max_d, tau,
|
|
727
|
+
make_triangles, high_is_inside, dual_connectivity)
|
|
728
|
+
return m
|
|
729
|
+
|
|
730
|
+
def triangulate(m: Manifold, clip_ear: bool = True):
|
|
731
|
+
""" Turn a general polygonal mesh, m, into a triangle mesh by repeatedly
|
|
732
|
+
splitting a polygon into smaller polygons. """
|
|
733
|
+
if clip_ear:
|
|
734
|
+
lib_py_gel.ear_clip_triangulate(m.obj)
|
|
735
|
+
else:
|
|
736
|
+
lib_py_gel.shortest_edge_triangulate(m.obj)
|
|
737
|
+
|
|
738
|
+
def extrude_faces(m: Manifold, fset: ArrayLike | set[int]) -> set[int]:
|
|
739
|
+
""" Inserts a new face loop around a set of faces given by fset."""
|
|
740
|
+
fvec = np.asarray(list(fset), dtype=ct.c_int)
|
|
741
|
+
face_loop_out = IntVector()
|
|
742
|
+
lib_py_gel.extrude_faces(m.obj,fvec,len(fvec), face_loop_out.obj)
|
|
743
|
+
fset_out = set(face_loop_out)
|
|
744
|
+
del face_loop_out
|
|
745
|
+
return fset_out
|
|
746
|
+
|
|
747
|
+
def kill_face_loop(m: Manifold):
|
|
748
|
+
""" Removes the face loop surrounding the patch of smallest area. This function has
|
|
749
|
+
undefined effecto on a mesh that is not a pure quad mesh."""
|
|
750
|
+
lib_py_gel.kill_face_loop(m.obj)
|
|
751
|
+
|
|
752
|
+
def kill_degenerate_face_loops(m: Manifold, thresh: float = 0.01):
|
|
753
|
+
""" Removes face loops which contain very poorly shaped faces. Must be called on a pure
|
|
754
|
+
quad mesh. """
|
|
755
|
+
lib_py_gel.kill_degenerate_face_loops(m.obj, thresh)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class MeshDistance:
|
|
759
|
+
""" This class allows you to compute the distance from any point in space to
|
|
760
|
+
a Manifold (which must be triangulated). The constructor creates an instance
|
|
761
|
+
based on a specific mesh, and the signed_distance function computes the actual distance. """
|
|
762
|
+
def __init__(self,m: Manifold):
|
|
763
|
+
self.obj = lib_py_gel.MeshDistance_new(m.obj)
|
|
764
|
+
def __del__(self):
|
|
765
|
+
lib_py_gel.MeshDistance_delete(self.obj)
|
|
766
|
+
def signed_distance(self, pts: ArrayLike, upper: float = 1e30) -> np.ndarray:
|
|
767
|
+
""" Compute the signed distance from each point in pts to the mesh stored in
|
|
768
|
+
this class instance. pts should be convertible to a length N>=1 array of 3D
|
|
769
|
+
points. The function returns an array of N distance values with a single distance
|
|
770
|
+
for each point. The distance corresponding to a point is positive if the point
|
|
771
|
+
is outside and negative if inside. The upper parameter can be used to threshold
|
|
772
|
+
how far away the distance is of interest. """
|
|
773
|
+
p = np.asarray(pts, dtype=ct.c_float, order='C')
|
|
774
|
+
ndim = len(p.shape)
|
|
775
|
+
if ndim==1:
|
|
776
|
+
n = p.shape[0]//3
|
|
777
|
+
elif ndim==2:
|
|
778
|
+
n = p.shape[0]
|
|
779
|
+
else:
|
|
780
|
+
raise ValueError("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
|
|
781
|
+
|
|
782
|
+
d = np.ndarray(n, dtype=ct.c_float)
|
|
783
|
+
lib_py_gel.MeshDistance_signed_distance(self.obj, n, p, d, upper)
|
|
784
|
+
return d[0] if n==1 else d
|
|
785
|
+
def ray_inside_test(self, pts: ArrayLike, no_rays: int = 3) -> np.ndarray:
|
|
786
|
+
"""Check whether each point in pts is inside or outside the stored mesh by
|
|
787
|
+
casting rays. pts should be convertible to a length N>=1 array of 3D points.
|
|
788
|
+
Effectively, this is the sign of the distance. In some cases casting (multiple)
|
|
789
|
+
ray is more robust than using the sign computed locally. Returns an array of
|
|
790
|
+
N integers which are either 1 or 0 depending on whether the corresponding point
|
|
791
|
+
is inside (1) or outside (0). """
|
|
792
|
+
p = np.asarray(pts, dtype=ct.c_float, order='C')
|
|
793
|
+
ndim = len(p.shape)
|
|
794
|
+
if ndim==1:
|
|
795
|
+
n = p.shape[0]//3
|
|
796
|
+
elif ndim==2:
|
|
797
|
+
n = p.shape[0]
|
|
798
|
+
else:
|
|
799
|
+
raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
|
|
800
|
+
s = np.ndarray(n, dtype=ct.c_int)
|
|
801
|
+
lib_py_gel.MeshDistance_ray_inside_test(self.obj,n,p,s,no_rays)
|
|
802
|
+
return s[0] if n==1 else s
|
|
803
|
+
def intersect(self, p0: ArrayLike, dir: ArrayLike, _t: float = 0) -> tuple[float, np.ndarray, np.ndarray] | None:
|
|
804
|
+
""" Intersect the ray starting in p0 with direction, dir, with the stored mesh. Returns
|
|
805
|
+
the point of intersection if there is one, otherwise None. """
|
|
806
|
+
p0 = np.asarray(p0,dtype=ct.c_float)
|
|
807
|
+
dir = np.asarray(dir,dtype=ct.c_float)
|
|
808
|
+
t = ct.c_float(_t)
|
|
809
|
+
r = lib_py_gel.MeshDistance_ray_intersect(self.obj, p0, dir, ct.byref(t))
|
|
810
|
+
if r:
|
|
811
|
+
return t.value, p0, dir
|
|
812
|
+
return None
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def graph_to_feq(g: Graph, node_radii: ArrayLike | float | None = None, symmetrize: bool = True) -> Manifold:
|
|
816
|
+
""" Turn a skeleton graph g into a Face Extrusion Quad Mesh m with given node_radii for each graph node.
|
|
817
|
+
If symmetrize is True (default) the graph is made symmetrical. If node_radii are supplied then they
|
|
818
|
+
are used in the reconstruction. Otherwise, the radii are obtained from the skeleton. They are stored in
|
|
819
|
+
the green channel of the vertex color during skeletonization, so for a skeletonized shape that is how the
|
|
820
|
+
radius of each node is obtained. This is a questionable design decision and will probably change
|
|
821
|
+
in the future. """
|
|
822
|
+
m = Manifold()
|
|
823
|
+
if node_radii is None:
|
|
824
|
+
node_radii = [0.0] * len(g.nodes())
|
|
825
|
+
use_graph_radii = True
|
|
826
|
+
else:
|
|
827
|
+
use_graph_radii = False
|
|
828
|
+
if isinstance(node_radii, (int, float)):
|
|
829
|
+
if node_radii <= 0.0:
|
|
830
|
+
node_radii = 0.25 * g.average_edge_length()
|
|
831
|
+
node_radii = [node_radii] * len(g.nodes())
|
|
832
|
+
|
|
833
|
+
node_rs_flat = np.asarray(node_radii, dtype=np.float64)
|
|
834
|
+
lib_py_gel.graph_to_feq(g.obj , m.obj, node_rs_flat, symmetrize, use_graph_radii)
|
|
835
|
+
return m
|
|
836
|
+
|
|
837
|
+
# Creating an alias for backward compatibility
|
|
838
|
+
skeleton_to_feq = graph_to_feq
|
|
839
|
+
|
|
840
|
+
def graph_to_cylinders(g: Graph, fudge: float = 0.0) -> Manifold:
|
|
841
|
+
""" Creates a Manifold mesh from the graph. The first argument, g, is the
|
|
842
|
+
graph we want converted, and fudge is a constant that is used to increase the radius
|
|
843
|
+
of every node. This is useful if the radii are 0. """
|
|
844
|
+
m = Manifold()
|
|
845
|
+
lib_py_gel.graph_to_mesh_cyl(g.obj, m.obj, fudge)
|
|
846
|
+
return m
|
|
847
|
+
|
|
848
|
+
def graph_to_isosurface(g: Graph, fudge: float = 0.0, res: int = 256) -> Manifold:
|
|
849
|
+
""" Creates a Manifold mesh from the graph. The first argument, g, is the
|
|
850
|
+
graph we want converted, and fudge is a constant that is used to increase the radius
|
|
851
|
+
of every node. This is useful if the radii are 0. """
|
|
852
|
+
m = Manifold()
|
|
853
|
+
lib_py_gel.graph_to_mesh_iso(g.obj, m.obj, fudge, res)
|
|
854
|
+
return m
|
|
855
|
+
|
|
856
|
+
# def non_rigid_registration(m, ref_mesh):
|
|
857
|
+
# """ Perform non-rigid registration of m to ref_mesh. """
|
|
858
|
+
# lib_py_gel.non_rigid_registration(m.obj, ref_mesh.obj)
|
|
859
|
+
|
|
860
|
+
def _laplacian_matrix(m: Manifold):
|
|
861
|
+
num_verts = m.no_allocated_vertices()
|
|
862
|
+
laplacian = np.full((num_verts,num_verts), 0.0)
|
|
863
|
+
for i in m.vertices():
|
|
864
|
+
neighbors = m.circulate_vertex(i)
|
|
865
|
+
deg = len(neighbors)
|
|
866
|
+
laplacian[i][i] = 1.0
|
|
867
|
+
for v in neighbors:
|
|
868
|
+
laplacian[i][v] = -1/deg
|
|
869
|
+
return csc_matrix(laplacian)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _inv_correspondence_leqs(m: Manifold, ref_mesh: Manifold):
|
|
873
|
+
m_pos = m.positions()
|
|
874
|
+
ref_pos = ref_mesh.positions()
|
|
875
|
+
m_tree = KDTree(m_pos)
|
|
876
|
+
m_target_pos = np.zeros(m.positions().shape)
|
|
877
|
+
m_cnt = np.zeros(m.no_allocated_vertices())
|
|
878
|
+
_, m_indices = m_tree.query(ref_pos[ref_mesh.vertices()])
|
|
879
|
+
for r_id, m_id in enumerate(m_indices):
|
|
880
|
+
r_norm = ref_mesh.vertex_normal(r_id)
|
|
881
|
+
m_norm = m.vertex_normal(m_id)
|
|
882
|
+
dot_prod = m_norm @ r_norm
|
|
883
|
+
if (dot_prod > 0.5):
|
|
884
|
+
# v = ref_pos[r_id] - m_pos[m_id]
|
|
885
|
+
wgt = dot_prod-0.5
|
|
886
|
+
m_target_pos[m_id] += wgt*ref_pos[r_id]
|
|
887
|
+
m_cnt[m_id] += wgt
|
|
888
|
+
|
|
889
|
+
N = m.no_allocated_vertices()
|
|
890
|
+
A_list = []
|
|
891
|
+
b_list = []
|
|
892
|
+
for vid in m.vertices():
|
|
893
|
+
if (m_cnt[vid] > 0.0):
|
|
894
|
+
row_a = np.zeros(N)
|
|
895
|
+
row_a[vid] = 1.0
|
|
896
|
+
A_list.append(row_a)
|
|
897
|
+
b_list.append(m_target_pos[vid]/m_cnt[vid])
|
|
898
|
+
return csc_matrix(np.array(A_list)), np.array(b_list)
|
|
899
|
+
|
|
900
|
+
def _distance_gradient(mesh_dist: MeshDistance, p, eps=1e-3):
|
|
901
|
+
"""Compute the gradient of the distance field given by mesh_dist at point p"""
|
|
902
|
+
grad = np.zeros(3)
|
|
903
|
+
for i in range(3):
|
|
904
|
+
p1 = np.copy(p)
|
|
905
|
+
p2 = np.copy(p)
|
|
906
|
+
p1[i] += eps
|
|
907
|
+
p2[i] -= eps
|
|
908
|
+
d1 = mesh_dist.signed_distance(p1)
|
|
909
|
+
d2 = mesh_dist.signed_distance(p2)
|
|
910
|
+
grad[i] = (d1-d2) / (2 * eps)
|
|
911
|
+
if grad[i] > 1.0:
|
|
912
|
+
print("diff issue: ", d1, d2, p, i)
|
|
913
|
+
return grad
|
|
914
|
+
|
|
915
|
+
def _inflate_mesh(m: Manifold, mesh_dist: MeshDistance):
|
|
916
|
+
pos = m.positions()
|
|
917
|
+
ael = average_edge_length(m)
|
|
918
|
+
max_dist = ael
|
|
919
|
+
eps = 0.05*ael
|
|
920
|
+
new_pos = np.array(pos)
|
|
921
|
+
for v in m.vertices():
|
|
922
|
+
p = pos[v]
|
|
923
|
+
g = _distance_gradient(mesh_dist, p, eps)
|
|
924
|
+
n = m.vertex_normal(v)
|
|
925
|
+
k = max(0.0, min(1.0, n@g))
|
|
926
|
+
d = max(-k*max_dist, min(k*max_dist, mesh_dist.signed_distance(p)))
|
|
927
|
+
new_pos[v] = pos[v] - d * n
|
|
928
|
+
pos[:] = new_pos
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _barycentrics(p, p0, p1, p2):
|
|
932
|
+
""" Compute the barycentric coordinates of point p with respect to the triangle p0, p1, p2. """
|
|
933
|
+
v0 = p1 - p0
|
|
934
|
+
v1 = p2 - p1
|
|
935
|
+
v2 = p0 - p2
|
|
936
|
+
|
|
937
|
+
# Project p onto the plane of the triangle
|
|
938
|
+
normal = np.cross(v0, -v2)
|
|
939
|
+
normal /= np.linalg.norm(normal)
|
|
940
|
+
p_proj = p - np.dot(p - p0, normal) * normal
|
|
941
|
+
|
|
942
|
+
# Compute barycentric coordinates
|
|
943
|
+
u = normal@np.cross(v1, p_proj-p1)
|
|
944
|
+
v = normal@np.cross(v2, p_proj-p2)
|
|
945
|
+
w = normal@np.cross(v0, p_proj-p0)
|
|
946
|
+
s = u+v+w
|
|
947
|
+
return np.array([u/s, v/s, w/s])
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _inv_map(m: Manifold, ref, ref_orig):
|
|
951
|
+
""" Finds position of vertices from mesh m in faces of ref. Then maps those
|
|
952
|
+
positions to the corresponding triangles of ref_orig using barycentric coordinates. """
|
|
953
|
+
pos_m = m.positions()
|
|
954
|
+
pos_ref = ref.positions()
|
|
955
|
+
pos_ref_orig = ref_orig.positions()
|
|
956
|
+
|
|
957
|
+
T = KDTree(pos_ref)
|
|
958
|
+
_, closest_ref_verts = T.query(pos_m)
|
|
959
|
+
for v in m.vertices():
|
|
960
|
+
# p = pos_m[v]
|
|
961
|
+
v_ref = closest_ref_verts[v]
|
|
962
|
+
pos_m[v] = pos_ref_orig[v_ref]
|
|
963
|
+
|
|
964
|
+
def _fit_mesh_mmh(m: Manifold, ref_mesh: Manifold, iterations=10):
|
|
965
|
+
""" Fits a mesh m to ref_mesh by iteratively moving m towards ref_mesh and vice versa."""
|
|
966
|
+
mrc = Manifold(ref_mesh)
|
|
967
|
+
triangulate(mrc, clip_ear=False)
|
|
968
|
+
print("Triangulated. Now fitting")
|
|
969
|
+
taubin_smooth(mrc, no_iters=30)
|
|
970
|
+
obj_save("mrc.obj", mrc)
|
|
971
|
+
mesh_dist = MeshDistance(mrc)
|
|
972
|
+
for _ in range(iterations):
|
|
973
|
+
print("Inflate mc")
|
|
974
|
+
cc_smooth(m,1)
|
|
975
|
+
_inflate_mesh(m, mesh_dist=mesh_dist)
|
|
976
|
+
obj_save("mc.obj", m)
|
|
977
|
+
# print("")
|
|
978
|
+
# print("Doing the MMH map")
|
|
979
|
+
# inv_map(m,mrc,ref_mesh)
|
|
980
|
+
|
|
981
|
+
def _stable_marriage_registration(m, m_ref):
|
|
982
|
+
""" Register m to m_ref using Gale Shapley """
|
|
983
|
+
lib_py_gel.stable_marriage_registration(m.obj, m_ref.obj)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def fit_mesh_to_ref(m: Manifold, ref_mesh: Manifold, dist_wt: float = 0.5, lap_wt: float = 1.0, iter: int = 10):
|
|
987
|
+
""" Fits a skeletal mesh m to a reference mesh ref_mesh. """
|
|
988
|
+
v_pos = m.positions()
|
|
989
|
+
# ref_mesh = Manifold(m)
|
|
990
|
+
# stable_marriage_registration(ref_mesh, _ref_mesh)
|
|
991
|
+
# ref_pos = ref_mesh.positions()
|
|
992
|
+
# A_list = []
|
|
993
|
+
# b_list = []
|
|
994
|
+
# N = len(m.vertices())
|
|
995
|
+
# for vid in m.vertices():
|
|
996
|
+
# row_a = np.zeros(N)
|
|
997
|
+
# row_a[vid] = dist_wt
|
|
998
|
+
# A_list.append(row_a)
|
|
999
|
+
# b_list.append(ref_pos[vid]*dist_wt)
|
|
1000
|
+
# Ai, bi = csc_matrix(np.array(A_list)), np.array(b_list)
|
|
1001
|
+
for _ in range(iter):
|
|
1002
|
+
Ai, bi = _inv_correspondence_leqs(m, ref_mesh)
|
|
1003
|
+
lap_matrix = _laplacian_matrix(m)
|
|
1004
|
+
lap_b = lap_matrix @ v_pos
|
|
1005
|
+
final_A = vstack([lap_wt*lap_matrix, Ai])
|
|
1006
|
+
final_b = np.vstack([0*lap_b, bi])
|
|
1007
|
+
opt_x, _, _, _ = lsqr(final_A, final_b[:,0])[:4]
|
|
1008
|
+
opt_y, _, _, _ = lsqr(final_A, final_b[:,1])[:4]
|
|
1009
|
+
opt_z, _, _, _ = lsqr(final_A, final_b[:,2])[:4]
|
|
1010
|
+
v_pos[:,:] = np.stack([opt_x, opt_y, opt_z], axis=1)
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def rsr_recon(vertices: ArrayLike,
|
|
1014
|
+
normals: ArrayLike=None,
|
|
1015
|
+
use_Euclid_dist: bool=False,
|
|
1016
|
+
genus: int=-1,
|
|
1017
|
+
num_neighbors: int=70,
|
|
1018
|
+
max_neighbor_dist: float=20,
|
|
1019
|
+
max_normal_ang: float=60,
|
|
1020
|
+
max_handle_dist: int=50) -> Manifold:
|
|
1021
|
+
""" RsR Reconstruction. The first argument, vertices, is the point cloud. The next argument,
|
|
1022
|
+
normals, are the normals associated with the vertices or empty list (default) if normals
|
|
1023
|
+
need to be estimated during reconstruction. use_Euclid_dist should be true if we
|
|
1024
|
+
can use the Euclidean rather than projected distance. Set to true only for noise free
|
|
1025
|
+
point clouds. genus controls handle insertion: -1 lets the algorithm detect genus,
|
|
1026
|
+
0 disables handle insertion, and values > 0 request that many handles. num_neighbors is the number
|
|
1027
|
+
of nearest neighbors for each point,
|
|
1028
|
+
max_neighbor_dist is the maximum distance to farthest neighbor measured in multiples of average distance,
|
|
1029
|
+
max_normal_ang is the threshold on angles between normals: two points are only connected if the angle
|
|
1030
|
+
between their normals is less than max_normal_ang. Finally, max_handle_dist is the threshold on the distance between
|
|
1031
|
+
vertices that are connected by handle edges (check paper). For large max_handle_dist, it is harder for
|
|
1032
|
+
the algorithm to add handles. """
|
|
1033
|
+
m = Manifold()
|
|
1034
|
+
vertices_data, n_vertices = _as_vec3_f(vertices)
|
|
1035
|
+
normal_data, n_normal = _as_vec3_f(normals)
|
|
1036
|
+
|
|
1037
|
+
lib_py_gel.rsr_recon(m.obj, vertices_data, normal_data, n_vertices, n_normal,
|
|
1038
|
+
use_Euclid_dist, genus, num_neighbors, max_neighbor_dist, max_normal_ang, max_handle_dist)
|
|
1039
|
+
return m
|
|
1040
|
+
|
|
1041
|
+
def hrsr_recon(vertices: ArrayLike,
|
|
1042
|
+
normals: ArrayLike=None,
|
|
1043
|
+
collapse_iters: int=1,
|
|
1044
|
+
use_Euclid_dist: bool=False,
|
|
1045
|
+
genus: int=-1,
|
|
1046
|
+
num_neighbors: int=70,
|
|
1047
|
+
max_neighbor_dist: float=20,
|
|
1048
|
+
max_normal_ang: float=60,
|
|
1049
|
+
max_handle_dist: int=50,
|
|
1050
|
+
skip_reexpansion: bool=False) -> Manifold:
|
|
1051
|
+
""" Hierarchical RsR reconstruction.
|
|
1052
|
+
The arguments match rsr_recon, with two additions: collapse_iters controls
|
|
1053
|
+
how many collapse iterations to run, and skip_reexpansion disables the
|
|
1054
|
+
final reexpansion stage when set to True.
|
|
1055
|
+
|
|
1056
|
+
Genus semantics are intentionally the same as rsr_recon:
|
|
1057
|
+
- genus = -1: auto-detect
|
|
1058
|
+
- genus = 0: do not add handles
|
|
1059
|
+
- genus > 0: request genus handle insertions
|
|
1060
|
+
"""
|
|
1061
|
+
m = Manifold()
|
|
1062
|
+
vertices_data, n_vertices = _as_vec3_f(vertices)
|
|
1063
|
+
normal_data, n_normal = _as_vec3_f(normals)
|
|
1064
|
+
|
|
1065
|
+
lib_py_gel.hrsr_recon(m.obj, vertices_data, normal_data, n_vertices, n_normal,
|
|
1066
|
+
collapse_iters, use_Euclid_dist, genus,
|
|
1067
|
+
num_neighbors, max_neighbor_dist, max_normal_ang, max_handle_dist, skip_reexpansion)
|
|
1068
|
+
return m
|
|
1069
|
+
|
|
1070
|
+
def connected_components(m: Manifold) -> List[Manifold]:
|
|
1071
|
+
""" Returns a list of Manifolds that form the connected components of the mesh m. """
|
|
1072
|
+
comp = lib_py_gel.connected_components(m.obj)
|
|
1073
|
+
N = lib_py_gel.mesh_vec_size(comp)
|
|
1074
|
+
if N == 0:
|
|
1075
|
+
return []
|
|
1076
|
+
meshes = []
|
|
1077
|
+
for i in range(N):
|
|
1078
|
+
obj = ct.c_void_p(lib_py_gel.mesh_vec_get(comp, i))
|
|
1079
|
+
meshes.append(Manifold(obj))
|
|
1080
|
+
lib_py_gel.mesh_vec_del(comp)
|
|
1081
|
+
return meshes
|
|
1082
|
+
|
|
1083
|
+
def count_boundary_curves(m: Manifold) -> int:
|
|
1084
|
+
""" Returns the number of boundary curves in the mesh m. """
|
|
1085
|
+
return lib_py_gel.count_boundary_curves(m.obj)
|
|
1086
|
+
|
|
1087
|
+
def analyze_topology(m: Manifold) -> List[Dict[str, Any]]:
|
|
1088
|
+
""" Returns a list of dictionaries with information about the connected components of the mesh m.
|
|
1089
|
+
Each dictionary contains the Manifold ('m'), number of vertices ('V'), edges ('E'), faces ('F'),
|
|
1090
|
+
boundary curves ('b'), and the genus ('g') of the component. The genus is calculated using the
|
|
1091
|
+
Euler-Poincaré formula:"""
|
|
1092
|
+
components = connected_components(m)
|
|
1093
|
+
output = []
|
|
1094
|
+
for _,comp in enumerate(components):
|
|
1095
|
+
b = count_boundary_curves(comp)
|
|
1096
|
+
V = len(comp.vertices()) # Number of vertices
|
|
1097
|
+
E = len(comp.halfedges())//2 # Number of edges
|
|
1098
|
+
F = len(comp.faces()) # Number of faces
|
|
1099
|
+
g = -(V - E + F - 2 + b)//2 # Genus calculation
|
|
1100
|
+
output.append({'m': comp, 'V': V, 'E': E, 'F': F, 'b': b, 'g': g})
|
|
1101
|
+
return output
|
|
1102
|
+
|
|
1103
|
+
def sphere_delaunay(pts: ArrayLike) -> Manifold:
|
|
1104
|
+
""" Given a set of points on the unit sphere, compute the spherical Delaunay triangulation
|
|
1105
|
+
and return it as a Manifold mesh. The points should be given as an array-like of shape (N,3). """
|
|
1106
|
+
m = Manifold()
|
|
1107
|
+
pts_data = np.asarray(pts, dtype=ct.c_double, order='C')
|
|
1108
|
+
if pts_data.size % 3 != 0:
|
|
1109
|
+
raise ValueError("pts should be of shape (N,3)")
|
|
1110
|
+
n_pts = pts_data.shape[0]
|
|
1111
|
+
lib_py_gel.sphere_delaunay(m.obj, pts_data, n_pts)
|
|
1112
|
+
return m
|