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/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