pygel3d.hmesh

The hmesh module provides an halfedge based mesh representation. In addition this module contains a variety of functions for mesh manipulation and inspection. Specifcally, the module contains functions for mesh simplification, smoothing, subdivision, and editing of vertices, faces, and edges. The volumetric_isocontour function allows us to create a polygonal mesh from volumetric data by isocontouring. The skeleton_to_feq function allows us to turn a skeleton graph into a Face Extrusion Quad Mesh.

  1""" The hmesh module provides an halfedge based mesh representation. 
  2In addition this module contains a variety of functions for mesh manipulation and inspection.
  3Specifcally, the module contains functions for mesh simplification, smoothing, subdivision, 
  4and editing of vertices, faces, and edges. The volumetric_isocontour function allows us to 
  5create a polygonal mesh from volumetric data by isocontouring. The skeleton_to_feq function 
  6allows us to turn a skeleton graph into a Face Extrusion Quad Mesh.
  7"""
  8import ctypes as ct
  9import numpy as np
 10from math import sqrt
 11from numpy import ndarray
 12from numpy.linalg import norm
 13from pygel3d import lib_py_gel, IntVector, Vec3dVector, spatial
 14from scipy.sparse import csc_matrix, vstack
 15from scipy.sparse.linalg import lsqr
 16from collections import defaultdict
 17
 18
 19class Manifold:
 20    """ The Manifold class represents a halfedge based mesh. It is maybe a bit grand to call
 21    a mesh class Manifold, but meshes based on the halfedge representation are manifold (if we
 22    ignore a few corner cases) unlike some other representations. This class contains a number of
 23    methods for mesh manipulation and inspection. Note also that numerous further functions are
 24    available to manipulate meshes stored as Manifolds.
 25
 26    Many of the functions below accept arguments called hid, fid, or vid. These are simply indices
 27    of halfedges, faces and vertices, respectively: integer numbers that identify the corresponding
 28    mesh element. Using a plain integer to identify a mesh entity means that, for instance, a
 29    vertex index can also be used as an index into, say, a NumPy array without any conversion.
 30    """
 31    def __init__(self,orig=None):
 32        if orig == None:
 33            self.obj = lib_py_gel.Manifold_new()
 34        else:
 35            self.obj = lib_py_gel.Manifold_copy(orig.obj)
 36    @classmethod
 37    def from_triangles(cls,vertices, faces):
 38        """ Given a list of vertices and triangles (faces), this function produces
 39        a Manifold mesh."""
 40        m = cls()
 41        vertices = np.asarray(vertices,dtype=np.float64, order='C')
 42        faces = np.asarray(faces,dtype=ct.c_int, order='C')
 43        m.obj = lib_py_gel.Manifold_from_triangles(len(vertices),len(faces),vertices,faces)
 44        return m
 45    @classmethod
 46    def from_points(cls,pts,xaxis=np.array([1,0,0]),yaxis=np.array([0,1,0])):
 47        """ This function computes the Delaunay triangulation of pts. You need
 48        to specify xaxis and yaxis if they are not canonical. The function returns
 49        a Manifold with the resulting triangles. Clearly, this function will
 50        give surprising results if the surface represented by the points is not
 51        well represented as a 2.5D surface, aka a height field. """
 52        m = cls()
 53        pts = np.asarray(pts,dtype=np.float64, order='C')
 54        xaxis = np.asarray(xaxis,dtype=np.float64, order='C')
 55        yaxis = np.asarray(yaxis,dtype=np.float64, order='C')
 56        m.obj = lib_py_gel.Manifold_from_points(len(pts), pts, xaxis, yaxis)
 57        return m
 58    def __del__(self):
 59        lib_py_gel.Manifold_delete(self.obj)
 60    def merge_with(self, other):
 61        """ Merge this Manifold with another one given as the argument. This function
 62        does not return anything. It simply combines the two meshes in the Manifold on which
 63        the method is called. """
 64        lib_py_gel.Manifold_merge(self.obj, other.obj)
 65    def add_face(self,pts):
 66        """ Add a face to the Manifold.
 67        This function takes a list of 3D points, pts, as argument and creates a face
 68        in the mesh with those points as vertices. The function returns the index
 69        of the created face.
 70        """
 71        pts = np.asarray(pts,dtype=np.float64, order='C')
 72        return lib_py_gel.Manifold_add_face(self.obj, len(pts), pts)
 73    def positions(self):
 74        """ Retrieve an array containing the vertex positions of the Manifold.
 75        It is not a copy: any changes are made to the actual vertex positions. """
 76        pos = ct.POINTER(ct.c_double)()
 77        n = lib_py_gel.Manifold_positions(self.obj, ct.byref(pos))
 78        return np.ctypeslib.as_array(pos,(n,3))
 79    def no_allocated_vertices(self):
 80        """ Number of vertices.
 81        This number could be higher than the number of actually
 82        used vertices, but corresponds to the size of the array allocated
 83        for vertices."""
 84        return lib_py_gel.Manifold_no_allocated_vertices(self.obj)
 85    def no_allocated_faces(self):
 86        """ Number of faces.
 87        This number could be higher than the number of actually
 88        used faces, but corresponds to the size of the array allocated
 89        for faces."""
 90        return lib_py_gel.Manifold_no_allocated_faces(self.obj)
 91    def no_allocated_halfedges(self):
 92        """ Number of halfedges.
 93        This number could be higher than the number of actually
 94        used halfedges, but corresponds to the size of the array allocated
 95        for halfedges."""
 96        return lib_py_gel.Manifold_no_allocated_halfedges(self.obj)
 97    def vertices(self):
 98        """ Returns an iterable containing all vertex indices"""
 99        verts = IntVector()
100        n = lib_py_gel.Manifold_vertices(self.obj, verts.obj)
101        return verts
102    def faces(self):
103        """ Returns an iterable containing all face indices"""
104        faces = IntVector()
105        n = lib_py_gel.Manifold_faces(self.obj, faces.obj)
106        return faces
107    def halfedges(self):
108        """ Returns an iterable containing all halfedge indices"""
109        hedges = IntVector()
110        n = lib_py_gel.Manifold_halfedges(self.obj, hedges.obj)
111        return hedges
112    def circulate_vertex(self, vid, mode='v'):
113        """ Circulate a vertex. Passed a vertex index, vid, and second argument,
114        mode='f', this function will return an iterable with all faces incident
115        on vid arranged in counter clockwise order. Similarly, if mode is 'h',
116        incident halfedges (outgoing) are returned, and for mode = 'v', all
117        neighboring vertices are returned. """
118        nbrs = IntVector()
119        n = lib_py_gel.Manifold_circulate_vertex(self.obj, vid, ct.c_char(mode.encode('ascii')), nbrs.obj)
120        return nbrs
121    def circulate_face(self, fid, mode='v'):
122        """ Circulate a face. Passed a face index, fid, and second argument,
123        mode='f', this function will return an iterable with all faces that
124        share an edge with fid (in counter clockwise order). If the argument is
125        mode='h', the halfedges themselves are returned. For mode='v', the
126        incident vertices of the face are returned. """
127        nbrs = IntVector()
128        n = lib_py_gel.Manifold_circulate_face(self.obj, fid, ct.c_char(mode.encode('ascii')), nbrs.obj)
129        return nbrs
130    def next_halfedge(self,hid):
131        """ Returns next halfedge to hid. """
132        return lib_py_gel.Walker_next_halfedge(self.obj, hid)
133    def prev_halfedge(self,hid):
134        """ Returns previous halfedge to hid. """
135        return lib_py_gel.Walker_prev_halfedge(self.obj, hid)
136    def opposite_halfedge(self,hid):
137        """ Returns opposite halfedge to hid. """
138        return lib_py_gel.Walker_opposite_halfedge(self.obj, hid)
139    def incident_face(self,hid):
140        """ Returns face corresponding to hid. """
141        return lib_py_gel.Walker_incident_face(self.obj, hid)
142    def incident_vertex(self,hid):
143        """ Returns vertex corresponding to (or pointed to by) hid. """
144        return lib_py_gel.Walker_incident_vertex(self.obj, hid)
145    def remove_vertex(self,vid):
146        """ Remove vertex vid from the Manifold. This function merges all faces
147        around the vertex into one and then removes this resulting face. """
148        return lib_py_gel.Manifold_remove_vertex(self.obj, vid)
149    def remove_face(self,fid):
150        """ Removes a face, fid, from the Manifold. If it is an interior face it is
151        simply replaced by an invalid index. If the face contains boundary
152        edges, these are removed. Situations may arise where the mesh is no
153        longer manifold because the situation at a boundary vertex is not
154        homeomorphic to a half disk. This, we can probably ignore since from the
155        data structure point of view it is not really a problem that a vertex is
156        incident on two holes - a hole can be seen as a special type of face.
157        The function returns false if the index of the face is not valid,
158        otherwise the function must complete. """
159        return lib_py_gel.Manifold_remove_face(self.obj, fid)
160    def remove_edge(self,hid):
161        """ Remove an edge, hid, from the Manifold. This function will remove the
162        faces on either side and the edge itself in the process. Thus, it is a
163        simple application of remove_face. """
164        return lib_py_gel.Manifold_remove_edge(self.obj, hid)
165    def vertex_in_use(self,vid):
166        """ check if vertex, vid, is in use. This function returns true if the id corresponds
167        to a vertex that is currently in the mesh and false otherwise. vid could
168        be outside the range of used ids and it could also correspond to a vertex
169        which is not active. The function returns false in both cases. """
170        return lib_py_gel.Manifold_vertex_in_use(self.obj, vid)
171    def face_in_use(self,fid):
172        """ check if face, fid, is in use. This function returns true if the id corresponds
173        to a face that is currently in the mesh and false otherwise. fid could
174        be outside the range of used ids and it could also correspond to a face
175        which is not active. The function returns false in both cases. """
176        return lib_py_gel.Manifold_face_in_use(self.obj, fid)
177    def halfedge_in_use(self,hid):
178        """ check if halfedge hid is in use. This function returns true if the id corresponds
179        to a halfedge that is currently in the mesh and false otherwise. hid could
180        be outside the range of used ids and it could also correspond to a halfedge
181        which is not active. The function returns false in both cases. """
182        return lib_py_gel.Manifold_halfedge_in_use(self.obj, hid)
183    def flip_edge(self,hid):
184        """ Flip the edge, hid, separating two faces. The function first verifies that
185        the edge is flippable. This entails making sure that all of the
186        following are true.
187        1. adjacent faces are triangles.
188        2. neither end point has valency three or less.
189        3. the vertices that will be connected are not already.
190        If the tests are passed, the flip is performed and the function
191        returns True. Otherwise False."""
192        return lib_py_gel.Manifold_flip_edge(self.obj,hid)
193    def collapse_edge(self,hid, avg_vertices=False):
194        """ Collapse an edge hid.
195        Before collapsing hid, a number of tests are made:
196        ---
197        1.  For the two vertices adjacent to the edge, we generate a list of all their neighbouring vertices.
198        We then generate a  list of the vertices that occur in both these lists.
199        That is, we find all vertices connected by edges to both endpoints of the edge and store these in a list.
200        2.  For both faces incident on the edge, check whether they are triangular.
201        If this is the case, the face will be removed, and it is ok that the the third vertex is connected to both endpoints.
202        Thus the third vertex in such a face is removed from the list generated in 1.
203        3.  If the list is now empty, all is well.
204        Otherwise, there would be a vertex in the new mesh with two edges connecting it to the same vertex. Return false.
205        4.  TETRAHEDRON TEST:
206        If the valency of both vertices is three, and the incident faces are triangles, we also disallow the operation.
207        Reason: A vertex valency of two and two triangles incident on the adjacent vertices makes the construction collapse.
208        5.  VALENCY 4 TEST:
209        If a triangle is adjacent to the edge being collapsed, it disappears.
210        This means the valency of the remaining edge vertex is decreased by one.
211        A valency two vertex reduced to a valency one vertex is considered illegal.
212        6.  PREVENT MERGING HOLES:
213        Collapsing an edge with boundary endpoints and valid faces results in the creation where two holes meet.
214        A non manifold situation. We could relax this...
215        7. New test: if the same face is in the one-ring of both vertices but not adjacent to the common edge,
216        then the result of a collapse would be a one ring where the same face occurs twice. This is disallowed as the resulting
217        face would be non-simple.
218        If the tests are passed, the collapse is performed and the function
219        returns True. Otherwise False."""
220        return lib_py_gel.Manifold_collapse_edge(self.obj, hid, avg_vertices)
221    def split_face_by_edge(self,fid,v0,v1):
222        """   Split a face. The face, fid, is split by creating an edge with
223        endpoints v0 and v1 (the next two arguments). The vertices of the old
224        face between v0 and v1 (in counter clockwise order) continue to belong
225        to fid. The vertices between v1 and v0 belong to the new face. A handle to
226        the new face is returned. """
227        return lib_py_gel.Manifold_split_face_by_edge(self.obj, fid, v0, v1)
228    def split_face_by_vertex(self,fid):
229        """   Split a polygon, fid, by inserting a vertex at the barycenter. This
230        function is less likely to create flipped triangles than the
231        split_face_triangulate function. On the other hand, it introduces more
232        vertices and probably makes the triangles more acute. The vertex id of the
233        inserted vertex is returned. """
234        return lib_py_gel.Manifold_split_face_by_vertex(self.obj,fid)
235    def split_edge(self,hid):
236        """   Insert a new vertex on halfedge hid. The new halfedge is insterted
237        as the previous edge to hid. The vertex id of the inserted vertex is returned. """
238        return lib_py_gel.Manifold_split_edge(self.obj,hid)
239    def stitch_boundary_edges(self,h0,h1):
240        """   Stitch two halfedges. Two boundary halfedges, h0 and h1, can be stitched
241        together. This can be used to build a complex mesh from a bunch of
242        simple faces. """
243        return lib_py_gel.Manifold_stitch_boundary_edges(self.obj, h0, h1)
244    def merge_faces(self,hid):
245        """   Merges two faces into a single polygon. The merged faces are those shared
246        by the edge for which hid is one of the two corresponding halfedges. This function returns
247        true if the merging was possible and false otherwise. Currently merge
248        only fails if the mesh is already illegal. Thus it should, in fact,
249        never fail. """
250        if self.is_halfedge_at_boundary(hid):
251            return False
252        fid = self.incident_face(hid)
253        return lib_py_gel.Manifold_merge_faces(self.obj, fid, hid)
254    def close_hole(self,hid):
255        """ Close hole given by hid (i.e. the face referenced by hid). Returns
256        index of the created face or the face that was already there if, in
257        fact, hid was not next to a hole. """
258        return lib_py_gel.Manifold_close_hole(self.obj, hid)
259    def cleanup(self):
260        """ Remove unused items from Mesh. This function remaps all vertices, halfedges
261        and faces such that the arrays do not contain any holes left by unused mesh
262        entities. It is a good idea to call this function when a mesh has been simplified
263        or changed in other ways such that mesh entities have been removed. However, note
264        that it invalidates any attributes that you might have stored in auxilliary arrays."""
265        lib_py_gel.Manifold_cleanup(self.obj)
266    def is_halfedge_at_boundary(self, hid):
267        """ Returns True if hid is a boundary halfedge, i.e. face on either
268        side is invalid. """
269        return lib_py_gel.is_halfedge_at_boundary(self.obj, hid)
270    def is_vertex_at_boundary(self, vid):
271        """ Returns True if vid lies on a boundary. """
272        return lib_py_gel.is_vertex_at_boundary(self.obj, vid)
273    def edge_length(self, hid):
274        """ Returns length of edge given by halfedge hid which is passed as argument. """
275        return lib_py_gel.length(self.obj, hid)
276    def valency(self,vid):
277        """ Returns valency of vid, i.e. number of incident edges."""
278        return lib_py_gel.valency(self.obj,vid)
279    def vertex_normal(self, vid):
280        """ Returns vertex normal (angle weighted) of vertex given by vid """
281        n = ndarray(3,dtype=np.float64)
282        lib_py_gel.vertex_normal(self.obj, vid, n)
283        return n
284    def connected(self, v0, v1):
285        """ Returns true if the two argument vertices, v0 and v1, are in each other's one-rings."""
286        return lib_py_gel.connected(self.obj,v0,v1)
287    def no_edges(self, fid):
288        """ Compute the number of edges of a face fid """
289        return lib_py_gel.no_edges(self.obj, fid)
290    def face_normal(self, fid):
291        """ Compute the normal of a face fid. If the face is not a triangle,
292        the normal is not defined, but computed using the first three
293        vertices of the face. """
294        n = ndarray(3, dtype=np.float64)
295        lib_py_gel.face_normal(self.obj, fid, n)
296        return n
297    def area(self, fid):
298        """ Returns the area of a face fid. """
299        return lib_py_gel.area(self.obj, fid)
300    def perimeter(self, fid):
301        """ Returns the perimeter of a face fid. """
302        return lib_py_gel.perimeter(self.obj, fid)
303    def centre(self, fid):
304        """ Returns the centre of a face. """
305        c = ndarray(3, dtype=np.float64)
306        lib_py_gel.centre(self.obj, fid, c)
307        return c
308
309def valid(m):
310    """This function performs a series of tests to check that this
311    is a valid manifold. This function is not rigorously constructed but seems
312    to catch all problems so far. The function returns true if the mesh is valid
313    and false otherwise. """
314    return lib_py_gel.valid(m.obj)
315
316def closed(m):
317    """ Returns true if m is closed, i.e. has no boundary."""
318    return lib_py_gel.closed(m.obj)
319
320def bbox(m):
321    """ Returns the min and max corners of the bounding box of Manifold m. """
322    pmin = ndarray(3,dtype=np.float64)
323    pmax = ndarray(3,dtype=np.float64)
324    lib_py_gel.bbox(m.obj, pmin, pmax)
325    return pmin, pmax
326
327def bsphere(m):
328    """ Calculate the bounding sphere of the manifold m.
329    Returns centre,radius """
330    c = ndarray(3,dtype=np.float64)
331    r = (ct.c_double)()
332    lib_py_gel.bsphere(m.obj, c, ct.byref(r))
333    return (c,r)
334
335def stitch(m, rad=1e-30):
336    """ Stitch together edges of m whose endpoints coincide geometrically. This
337    function allows you to create a mesh as a bunch of faces and then stitch
338    these together to form a coherent whole. What this function adds is a
339    spatial data structure to find out which vertices coincide. The return value
340    is the number of edges that could not be stitched. Often this is because it
341    would introduce a non-manifold situation."""
342    return lib_py_gel.stitch_mesh(m.obj,rad)
343
344def obj_save(fn, m):
345    """ Save Manifold m to Wavefront obj file. """
346    s = ct.c_char_p(fn.encode('utf-8'))
347    lib_py_gel.obj_save(s, m.obj)
348
349def off_save(fn, m):
350    """ Save Manifold m to OFF file. """
351    s = ct.c_char_p(fn.encode('utf-8'))
352    lib_py_gel.off_save(s, m.obj)
353
354def x3d_save(fn, m):
355    """ Save Manifold m to X3D file. """
356    s = ct.c_char_p(fn.encode('utf-8'))
357    lib_py_gel.x3d_save(s, m.obj)
358
359def obj_load(fn):
360    """ Load and return Manifold from Wavefront obj file.
361    Returns None if loading failed. """
362    m = Manifold()
363    s = ct.c_char_p(fn.encode('utf-8'))
364    if lib_py_gel.obj_load(s, m.obj):
365        return m
366    return None
367
368def off_load(fn):
369    """ Load and return Manifold from OFF file.
370    Returns None if loading failed."""
371    m = Manifold()
372    s = ct.c_char_p(fn.encode('utf-8'))
373    if lib_py_gel.off_load(s, m.obj):
374        return m
375    return None
376
377def ply_load(fn):
378    """ Load and return Manifold from Stanford PLY file.
379    Returns None if loading failed. """
380    m = Manifold()
381    s = ct.c_char_p(fn.encode('utf-8'))
382    if lib_py_gel.ply_load(s, m.obj):
383        return m
384    return None
385
386def x3d_load(fn):
387    """ Load and return Manifold from X3D file.
388    Returns None if loading failed."""
389    m = Manifold()
390    s = ct.c_char_p(fn.encode('utf-8'))
391    if lib_py_gel.x3d_load(s, m.obj):
392        return m
393    return None
394
395from os.path import splitext
396def load(fn):
397    """ Load a Manifold from an X3D/OBJ/OFF/PLY file. Return the
398    loaded Manifold. Returns None if loading failed."""
399    name, extension = splitext(fn)
400    if extension.lower() == ".x3d":
401        return x3d_load(fn)
402    if extension.lower() == ".obj":
403        return obj_load(fn)
404    if extension.lower() == ".off":
405        return off_load(fn)
406    if extension.lower() == ".ply":
407        return ply_load(fn)
408    return None
409
410def save(fn, m):
411    """ Save a Manifold, m, to an X3D/OBJ/OFF file. """
412    name, extension = splitext(fn)
413    if extension.lower() == ".x3d":
414        x3d_save(fn, m)
415    elif extension.lower() == ".obj":
416        obj_save(fn, m)
417    elif extension.lower() == ".off":
418        off_save(fn, m)
419
420
421def remove_caps(m, thresh=2.9):
422    """ Remove caps from a manifold, m, consisting of only triangles. A cap is a
423    triangle with two very small angles and an angle close to pi, however a cap
424    does not necessarily have a very short edge. Set the ang_thresh to a value
425    close to pi. The closer to pi the _less_ sensitive the cap removal. A cap is
426    removed by flipping the (long) edge E opposite to the vertex V with the
427    angle close to pi. However, the function is more complex. Read code and
428    document more carefully !!! """
429    lib_py_gel.remove_caps(m.obj,thresh)
430
431def remove_needles(m, thresh=0.05, average_positions=False):
432    """  Remove needles from a manifold, m, consisting of only triangles. A needle
433    is a triangle with a single very short edge. It is moved by collapsing the
434    short edge. The thresh parameter sets the length threshold (in terms of the average edge length
435    in the mesh). If average_positions is true then the collapsed vertex is placed at the average position of the end points."""
436    abs_thresh = thresh * average_edge_length(m)
437    lib_py_gel.remove_needles(m.obj,abs_thresh, average_positions)
438
439def close_holes(m, max_size=100):
440    """  This function replaces holes in m by faces. It is really a simple function
441    that just finds all loops of edges next to missing faces. """
442    lib_py_gel.close_holes(m.obj, max_size)
443
444def flip_orientation(m):
445    """  Flip the orientation of a mesh, m. After calling this function, normals
446    will point the other way and clockwise becomes counter clockwise """
447    lib_py_gel.flip_orientation(m.obj)
448
449def merge_coincident_boundary_vertices(m, rad = 1.0e-30):
450    """  Merge vertices of m that are boundary vertices and coincident.
451        However, if one belongs to the other's one ring or the one
452        rings share a vertex, they will not be merged. """
453    lib_py_gel.merge_coincident_boundary_vertices(m.obj, rad)
454
455def minimize_curvature(m,anneal=False):
456    """ Minimizes mean curvature of m by flipping edges. Hence, no vertices are moved.
457     This is really the same as dihedral angle minimization, except that we weight by edge length. """
458    lib_py_gel.minimize_curvature(m.obj, anneal)
459
460def minimize_dihedral_angle(m,max_iter=10000, anneal=False, alpha=False, gamma=4.0):
461    """ Minimizes dihedral angles in m by flipping edges.
462        Arguments:
463        max_iter is the maximum number of iterations for simulated annealing.
464        anneal tells us the code whether to apply simulated annealing
465        alpha=False means that we use the cosine of angles rather than true angles (faster)
466        gamma is the power to which the angles are raised."""
467    lib_py_gel.minimize_dihedral_angle(m.obj, max_iter, anneal,alpha,ct.c_double(gamma))
468
469
470def maximize_min_angle(m,dihedral_thresh=0.95,anneal=False):
471    """ Maximizes the minimum angle of triangles by flipping edges of m. Makes the mesh more Delaunay."""
472    lib_py_gel.maximize_min_angle(m.obj,dihedral_thresh,anneal)
473
474def optimize_valency(m,anneal=False):
475    """ Tries to achieve valence 6 internally and 4 along edges by flipping edges of m. """
476    lib_py_gel.optimize_valency(m.obj, anneal)
477
478def randomize_mesh(m,max_iter=1):
479    """  Make random flips in m. Useful for generating synthetic test cases. """
480    lib_py_gel.randomize_mesh(m.obj, max_iter)
481
482def quadric_simplify(m,keep_fraction,singular_thresh=1e-4,error_thresh=1):
483    """ Garland Heckbert simplification of mesh m. keep_fraction is the fraction of vertices
484    to retain. The singular_thresh determines how subtle features are preserved. For values
485    close to 1 the surface is treated as smooth even in the presence of sharp edges of low
486    dihedral angle (angle between normals). Close to zero, the method preserves even subtle
487    sharp features better. The error_thresh is the value of the QEM error at which
488    simplification stops. It is relative to the bounding box size. The default value is 1
489    meaning that simplification continues until the model has been simplified to a number of
490    vertices approximately equal to keep_fraction times the original number of vertices."""
491    lib_py_gel.quadric_simplify(m.obj, keep_fraction, singular_thresh,error_thresh)
492
493def average_edge_length(m):
494    """ Returns the average edge length of mesh m. """
495    return lib_py_gel.average_edge_length(m.obj)
496
497def median_edge_length(m):
498    """ Returns the median edge length of m"""
499    return lib_py_gel.median_edge_length(m.obj)
500
501def refine_edges(m,threshold):
502    """ Split all edges in m which are longer
503    than the threshold (second arg) length. A split edge
504    results in a new vertex of valence two."""
505    return lib_py_gel.refine_edges(m.obj, threshold)
506
507def cc_split(m):
508    """ Perform a Catmull-Clark split on m, i.e. a split where each face is divided
509    into new quadrilateral faces formed by connecting a corner with a point on
510    each incident edge and a point at the centre of the face."""
511    lib_py_gel.cc_split(m.obj)
512
513def loop_split(m):
514    """ Perform a loop split on m where each edge is divided into two segments, and
515    four new triangles are created for each original triangle. """
516    lib_py_gel.loop_split(m.obj)
517
518def root3_subdivide(m):
519    """ Leif Kobbelt's subdivision scheme applied to m. A vertex is placed in the
520    center of each face and all old edges are flipped. """
521    lib_py_gel.root3_subdivide(m.obj)
522
523def rootCC_subdivide(m):
524    """ This subdivision scheme creates a vertex inside each original (quad) face of m,
525    producing four triangles. Triangles sharing an old edge are then merged.
526    Two steps produce something similar to Catmull-Clark. """
527    lib_py_gel.rootCC_subdivide(m.obj)
528
529def butterfly_subdivide(m):
530    """ Butterfly subidiviosn on m. An interpolatory scheme. Creates the same connectivity as Loop. """
531    lib_py_gel.butterfly_subdivide(m.obj)
532
533def cc_smooth(m):
534    """ If called after cc_split, this function completes a step of Catmull-Clark
535    subdivision of m."""
536    lib_py_gel.cc_smooth(m.obj)
537
538def cc_subdivide(m):
539    """ Perform a full Catmull-Clark subdivision step on mesh m. """
540    lib_py_gel.cc_split(m.obj)
541    lib_py_gel.cc_smooth(m.obj)
542
543def loop_subdivide(m):
544    """ Perform a full Loop subdivision step on mesh m. """
545    lib_py_gel.loop_split(m.obj)
546    lib_py_gel.loop_smooth(m.obj)   
547
548def volume_preserving_cc_smooth(m, iter):
549    """ This function does the same type of smoothing as in Catmull-Clark
550    subdivision, but to preserve volume it actually performs two steps, and the
551    second step is negative as in Taubin smoothing."""
552    lib_py_gel.volume_preserving_cc_smooth(m.obj, iter)
553
554def regularize_quads(m, w=0.5, shrink=0.0, iter=1):
555    """ This function smooths a quad mesh by regularizing quads. Essentially,
556    regularization just makes them more rectangular. """
557    lib_py_gel.regularize_quads(m.obj, w, shrink, iter)
558
559
560def loop_smooth(m):
561    """ If called after Loop split, this function completes a step of Loop
562    subdivision of m. """
563    lib_py_gel.loop_smooth(m.obj)
564    
565def taubin_smooth(m, iter=1):
566    """ This function performs Taubin smoothing on the mesh m for iter number
567    of iterations. """
568    lib_py_gel.taubin_smooth(m.obj, iter)
569
570def laplacian_smooth(m, w=0.5, iter=1):
571    """ This function performs Laplacian smoothing on the mesh m for iter number
572    of iterations. w is the weight applied. """
573    lib_py_gel.laplacian_smooth(m.obj, w, iter)
574
575def anisotropic_smooth(m, sharpness=0.5, iter=1):
576    """ This function performs anisotropic smoothing on the mesh m for iter number
577    of iterations. A bilateral filtering controlled by sharpness is performed on 
578    the face normals followed by a rotation of the faces to match the new normals.
579    The updated vertex positions are the average positions of the corners of the 
580    rotated faces. For sharpness==0 the new normal is simply the area weighted 
581    average of the normals of incident faces. For sharpness>0 the weight of the
582    neighbouring face normals is a Gaussian function of the angle between the
583    face normals. The greater the sharpness, the more the smoothing is anisotropic."""
584    lib_py_gel.anisotropic_smooth(m.obj, sharpness, iter)
585    
586def volumetric_isocontour(data, bbox_min = None, bbox_max = None,
587                          tau=0.0,
588                          make_triangles=True,
589                          high_is_inside=True,
590                          dual_connectivity=False):
591    """ Creates a polygonal mesh from volumetric data by isocontouring. The dimensions
592    are given by dims, bbox_min (defaults to [0,0,0] ) and bbox_max (defaults to dims) are
593    the corners of the bounding box in R^3 that corresponds to the volumetric grid, tau is
594    the iso value (defaults to 0). If make_triangles is True (default), we turn the quads
595    into triangles. Finally, high_is_inside=True (default) means that values greater than
596    tau are interior and smaller values are exterior. If dual_connectivity is False (default)
597    the function produces marching cubes connectivity and if True it produces dual contouring
598    connectivity. MC connectivity tends to produce less nice triangle shapes but since the
599    vertices always lie on edges, the geometry is arguably better defined for MC. """
600    m = Manifold()
601    dims = data.shape
602    if bbox_min is None:
603        bbox_min = (0,0,0)
604    if bbox_max is None:
605        bbox_max = dims
606    data_float = np.asarray(data, dtype=ct.c_float, order='F')
607    bbox_min_d = np.asarray(bbox_min, dtype=np.float64, order='C')
608    bbox_max_d = np.asarray(bbox_max, dtype=np.float64, order='C')
609    lib_py_gel.volumetric_isocontour(m.obj, dims[0], dims[1], dims[2],
610                                     data_float, bbox_min_d, bbox_max_d, tau,
611                                     make_triangles, high_is_inside, dual_connectivity)
612    return m
613
614def triangulate(m, clip_ear=True):
615    """ Turn a general polygonal mesh, m, into a triangle mesh by repeatedly
616        splitting a polygon into smaller polygons. """
617    if clip_ear:
618        lib_py_gel.ear_clip_triangulate(m.obj)
619    else:
620        lib_py_gel.shortest_edge_triangulate(m.obj)
621
622def skeleton_to_feq(g, node_radii = None, symmetrize=True):
623    """ Turn a skeleton graph g into a Face Extrusion Quad Mesh m with given node_radii for each graph node.
624    If symmetrize is True (default) the graph is made symmetrical. If node_radii are supplied then they
625    are used in the reconstruction. Otherwise, the radii are obtained from the skeleton. They are stored in 
626    the green channel of the vertex color during skeletonization, so for a skeletonized shape that is how the
627    radius of each node is obtained. This is a questionable design decision and will probably change 
628    in the future. """
629    m = Manifold()
630    if node_radii is None:
631        node_radii = [0.0] * len(g.nodes())
632        use_graph_radii = True
633    else:
634        use_graph_radii = False
635        if isinstance(node_radii, (int, float)):
636            if node_radii <= 0.0:
637                node_radii = 0.25 * g.average_edge_length()
638            node_radii = [node_radii] * len(g.nodes())
639
640    node_rs_flat = np.asarray(node_radii, dtype=np.float64)
641    lib_py_gel.graph_to_feq(g.obj , m.obj, node_rs_flat, symmetrize, use_graph_radii)
642    return m
643
644# def non_rigid_registration(m, ref_mesh):
645#     """ Perform non-rigid registration of m to ref_mesh. """
646#     lib_py_gel.non_rigid_registration(m.obj, ref_mesh.obj)
647
648def laplacian_matrix(m):
649    num_verts = m.no_allocated_vertices()
650    laplacian = np.full((num_verts,num_verts), 0.0)
651    for i in m.vertices():
652        neighbors = m.circulate_vertex(i)
653        deg = len(neighbors)
654        laplacian[i][i] = 1.0
655        for v in neighbors:
656            laplacian[i][v] = -1/deg
657    return csc_matrix(laplacian)
658
659
660def inv_correspondence_leqs(m, ref_mesh):
661    m_pos = m.positions()
662    ref_pos = ref_mesh.positions()
663
664    m_tree = spatial.I3DTree()
665    for v in m.vertices():
666        m_tree.insert(m_pos[v],v)
667    m_tree.build()
668
669    m_target_pos = np.zeros(m.positions().shape)
670    m_cnt = np.zeros(m.no_allocated_vertices())
671    for r_id in ref_mesh.vertices():
672        query_pt = ref_pos[r_id]
673        closest_pt_obj = m_tree.closest_point(query_pt,1e32)
674        if closest_pt_obj is not None:
675            key,m_id = closest_pt_obj
676            r_norm = ref_mesh.vertex_normal(r_id)
677            m_norm = m.vertex_normal(m_id)
678            dot_prod = m_norm @ r_norm
679            if dot_prod > 0.5:
680                v = query_pt - m_pos[m_id]
681                wgt = dot_prod - 0.5
682                m_target_pos[m_id] += wgt*query_pt
683                m_cnt[m_id] += wgt
684                
685    N = m.no_allocated_vertices()
686    A_list = []
687    b_list = []
688    for vid in m.vertices():
689        if m_cnt[vid] > 0.0:
690            row_a = np.zeros(N)
691            row_a[vid] = 1.0
692            A_list.append(row_a)
693            b_list.append(m_target_pos[vid]/m_cnt[vid])
694    return csc_matrix(np.array(A_list)), np.array(b_list)
695
696def fit_mesh_to_ref(m, ref_mesh, iter = 10, dist_wt = 1.0, lap_wt = 0.3):
697    """ Fits a skeletal mesh m to a reference mesh ref_mesh. """
698
699    v_pos = m.positions()
700    for i in range(iter):
701        Ai, bi = inv_correspondence_leqs(m, ref_mesh)
702        lap_matrix = laplacian_matrix(m)
703        lap_b = lap_matrix @ v_pos
704        final_A = vstack([lap_wt*lap_matrix, dist_wt*Ai])
705        final_b = np.vstack([lap_wt*lap_b, dist_wt*bi])
706        opt_x, _, _, _ = lsqr(final_A, final_b[:,0])[:4]
707        opt_y, _, _, _ = lsqr(final_A, final_b[:,1])[:4]
708        opt_z, _, _, _ = lsqr(final_A, final_b[:,2])[:4]
709        v_pos[:,:] = np.stack([opt_x, opt_y, opt_z], axis=1)
710        regularize_quads(m, w=0.5, shrink=0.8, iter=3)
711    return m
712
713class MeshDistance:
714    """ This class allows you to compute the distance from any point in space to
715    a Manifold (which must be triangulated). The constructor creates an instance
716    based on a specific mesh, and the signed_distance function computes the actual distance. """
717    def __init__(self,m):
718        self.obj = lib_py_gel.MeshDistance_new(m.obj)
719    def __del__(self):
720        lib_py_gel.MeshDistance_delete(self.obj)
721    def signed_distance(self,pts,upper=1e30):
722        """ Compute the signed distance from each point in pts to the mesh stored in
723        this class instance. pts should be convertible to a length N>=1 array of 3D
724        points. The function returns an array of N distance values with a single distance
725        for each point. The distance corresponding to a point is positive if the point
726        is outside and negative if inside. The upper parameter can be used to threshold
727        how far away the distance is of interest. """
728        p = np.asarray(pts, dtype=ct.c_float, order='C')
729        ndim = len(p.shape)
730        if ndim==1:
731            n = p.shape[0]//3
732        elif ndim==2:
733            n = p.shape[0]
734        else:
735            raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
736        
737        d = np.ndarray(n, dtype=ct.c_float)
738        lib_py_gel.MeshDistance_signed_distance(self.obj, n, p, d, upper)
739        return d
740    def ray_inside_test(self,pts,no_rays=3):
741        """Check whether each point in pts is inside or outside the stored mesh by
742        casting rays. pts should be convertible to a length N>=1 array of 3D points.
743        Effectively, this is the sign of the distance. In some cases casting (multiple)
744        ray is more robust than using the sign computed locally. Returns an array of
745        N integers which are either 1 or 0 depending on whether the corresponding point
746        is inside (1) or outside (0). """
747        p = np.asarray(pts, dtype=ct.c_float, order='C')
748        ndim = len(p.shape)
749        if ndim==1:
750            n = p.shape[0]//3
751        elif ndim==2:
752            n = p.shape[0]
753        else:
754            raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
755        s = np.ndarray(n, dtype=ct.c_int)
756        lib_py_gel.MeshDistance_ray_inside_test(self.obj,n,p,s,no_rays)
757        return s
758    def intersect(self, p0, dir, _t=0):
759        """ Intersect the ray starting in p0 with direction, dir, with the stored mesh. Returns
760        the point of intersection if there is one, otherwise None. """
761        p0 = np.asarray(p0,dtype=ct.c_float)
762        dir = np.asarray(dir,dtype=ct.c_float)
763        t = ct.c_float(_t)
764        r = lib_py_gel.MeshDistance_ray_intersect(self.obj, p0, dir, ct.byref(t))
765        if r:
766            return t.value, p0, dir
767        return None
class Manifold:
 20class Manifold:
 21    """ The Manifold class represents a halfedge based mesh. It is maybe a bit grand to call
 22    a mesh class Manifold, but meshes based on the halfedge representation are manifold (if we
 23    ignore a few corner cases) unlike some other representations. This class contains a number of
 24    methods for mesh manipulation and inspection. Note also that numerous further functions are
 25    available to manipulate meshes stored as Manifolds.
 26
 27    Many of the functions below accept arguments called hid, fid, or vid. These are simply indices
 28    of halfedges, faces and vertices, respectively: integer numbers that identify the corresponding
 29    mesh element. Using a plain integer to identify a mesh entity means that, for instance, a
 30    vertex index can also be used as an index into, say, a NumPy array without any conversion.
 31    """
 32    def __init__(self,orig=None):
 33        if orig == None:
 34            self.obj = lib_py_gel.Manifold_new()
 35        else:
 36            self.obj = lib_py_gel.Manifold_copy(orig.obj)
 37    @classmethod
 38    def from_triangles(cls,vertices, faces):
 39        """ Given a list of vertices and triangles (faces), this function produces
 40        a Manifold mesh."""
 41        m = cls()
 42        vertices = np.asarray(vertices,dtype=np.float64, order='C')
 43        faces = np.asarray(faces,dtype=ct.c_int, order='C')
 44        m.obj = lib_py_gel.Manifold_from_triangles(len(vertices),len(faces),vertices,faces)
 45        return m
 46    @classmethod
 47    def from_points(cls,pts,xaxis=np.array([1,0,0]),yaxis=np.array([0,1,0])):
 48        """ This function computes the Delaunay triangulation of pts. You need
 49        to specify xaxis and yaxis if they are not canonical. The function returns
 50        a Manifold with the resulting triangles. Clearly, this function will
 51        give surprising results if the surface represented by the points is not
 52        well represented as a 2.5D surface, aka a height field. """
 53        m = cls()
 54        pts = np.asarray(pts,dtype=np.float64, order='C')
 55        xaxis = np.asarray(xaxis,dtype=np.float64, order='C')
 56        yaxis = np.asarray(yaxis,dtype=np.float64, order='C')
 57        m.obj = lib_py_gel.Manifold_from_points(len(pts), pts, xaxis, yaxis)
 58        return m
 59    def __del__(self):
 60        lib_py_gel.Manifold_delete(self.obj)
 61    def merge_with(self, other):
 62        """ Merge this Manifold with another one given as the argument. This function
 63        does not return anything. It simply combines the two meshes in the Manifold on which
 64        the method is called. """
 65        lib_py_gel.Manifold_merge(self.obj, other.obj)
 66    def add_face(self,pts):
 67        """ Add a face to the Manifold.
 68        This function takes a list of 3D points, pts, as argument and creates a face
 69        in the mesh with those points as vertices. The function returns the index
 70        of the created face.
 71        """
 72        pts = np.asarray(pts,dtype=np.float64, order='C')
 73        return lib_py_gel.Manifold_add_face(self.obj, len(pts), pts)
 74    def positions(self):
 75        """ Retrieve an array containing the vertex positions of the Manifold.
 76        It is not a copy: any changes are made to the actual vertex positions. """
 77        pos = ct.POINTER(ct.c_double)()
 78        n = lib_py_gel.Manifold_positions(self.obj, ct.byref(pos))
 79        return np.ctypeslib.as_array(pos,(n,3))
 80    def no_allocated_vertices(self):
 81        """ Number of vertices.
 82        This number could be higher than the number of actually
 83        used vertices, but corresponds to the size of the array allocated
 84        for vertices."""
 85        return lib_py_gel.Manifold_no_allocated_vertices(self.obj)
 86    def no_allocated_faces(self):
 87        """ Number of faces.
 88        This number could be higher than the number of actually
 89        used faces, but corresponds to the size of the array allocated
 90        for faces."""
 91        return lib_py_gel.Manifold_no_allocated_faces(self.obj)
 92    def no_allocated_halfedges(self):
 93        """ Number of halfedges.
 94        This number could be higher than the number of actually
 95        used halfedges, but corresponds to the size of the array allocated
 96        for halfedges."""
 97        return lib_py_gel.Manifold_no_allocated_halfedges(self.obj)
 98    def vertices(self):
 99        """ Returns an iterable containing all vertex indices"""
100        verts = IntVector()
101        n = lib_py_gel.Manifold_vertices(self.obj, verts.obj)
102        return verts
103    def faces(self):
104        """ Returns an iterable containing all face indices"""
105        faces = IntVector()
106        n = lib_py_gel.Manifold_faces(self.obj, faces.obj)
107        return faces
108    def halfedges(self):
109        """ Returns an iterable containing all halfedge indices"""
110        hedges = IntVector()
111        n = lib_py_gel.Manifold_halfedges(self.obj, hedges.obj)
112        return hedges
113    def circulate_vertex(self, vid, mode='v'):
114        """ Circulate a vertex. Passed a vertex index, vid, and second argument,
115        mode='f', this function will return an iterable with all faces incident
116        on vid arranged in counter clockwise order. Similarly, if mode is 'h',
117        incident halfedges (outgoing) are returned, and for mode = 'v', all
118        neighboring vertices are returned. """
119        nbrs = IntVector()
120        n = lib_py_gel.Manifold_circulate_vertex(self.obj, vid, ct.c_char(mode.encode('ascii')), nbrs.obj)
121        return nbrs
122    def circulate_face(self, fid, mode='v'):
123        """ Circulate a face. Passed a face index, fid, and second argument,
124        mode='f', this function will return an iterable with all faces that
125        share an edge with fid (in counter clockwise order). If the argument is
126        mode='h', the halfedges themselves are returned. For mode='v', the
127        incident vertices of the face are returned. """
128        nbrs = IntVector()
129        n = lib_py_gel.Manifold_circulate_face(self.obj, fid, ct.c_char(mode.encode('ascii')), nbrs.obj)
130        return nbrs
131    def next_halfedge(self,hid):
132        """ Returns next halfedge to hid. """
133        return lib_py_gel.Walker_next_halfedge(self.obj, hid)
134    def prev_halfedge(self,hid):
135        """ Returns previous halfedge to hid. """
136        return lib_py_gel.Walker_prev_halfedge(self.obj, hid)
137    def opposite_halfedge(self,hid):
138        """ Returns opposite halfedge to hid. """
139        return lib_py_gel.Walker_opposite_halfedge(self.obj, hid)
140    def incident_face(self,hid):
141        """ Returns face corresponding to hid. """
142        return lib_py_gel.Walker_incident_face(self.obj, hid)
143    def incident_vertex(self,hid):
144        """ Returns vertex corresponding to (or pointed to by) hid. """
145        return lib_py_gel.Walker_incident_vertex(self.obj, hid)
146    def remove_vertex(self,vid):
147        """ Remove vertex vid from the Manifold. This function merges all faces
148        around the vertex into one and then removes this resulting face. """
149        return lib_py_gel.Manifold_remove_vertex(self.obj, vid)
150    def remove_face(self,fid):
151        """ Removes a face, fid, from the Manifold. If it is an interior face it is
152        simply replaced by an invalid index. If the face contains boundary
153        edges, these are removed. Situations may arise where the mesh is no
154        longer manifold because the situation at a boundary vertex is not
155        homeomorphic to a half disk. This, we can probably ignore since from the
156        data structure point of view it is not really a problem that a vertex is
157        incident on two holes - a hole can be seen as a special type of face.
158        The function returns false if the index of the face is not valid,
159        otherwise the function must complete. """
160        return lib_py_gel.Manifold_remove_face(self.obj, fid)
161    def remove_edge(self,hid):
162        """ Remove an edge, hid, from the Manifold. This function will remove the
163        faces on either side and the edge itself in the process. Thus, it is a
164        simple application of remove_face. """
165        return lib_py_gel.Manifold_remove_edge(self.obj, hid)
166    def vertex_in_use(self,vid):
167        """ check if vertex, vid, is in use. This function returns true if the id corresponds
168        to a vertex that is currently in the mesh and false otherwise. vid could
169        be outside the range of used ids and it could also correspond to a vertex
170        which is not active. The function returns false in both cases. """
171        return lib_py_gel.Manifold_vertex_in_use(self.obj, vid)
172    def face_in_use(self,fid):
173        """ check if face, fid, is in use. This function returns true if the id corresponds
174        to a face that is currently in the mesh and false otherwise. fid could
175        be outside the range of used ids and it could also correspond to a face
176        which is not active. The function returns false in both cases. """
177        return lib_py_gel.Manifold_face_in_use(self.obj, fid)
178    def halfedge_in_use(self,hid):
179        """ check if halfedge hid is in use. This function returns true if the id corresponds
180        to a halfedge that is currently in the mesh and false otherwise. hid could
181        be outside the range of used ids and it could also correspond to a halfedge
182        which is not active. The function returns false in both cases. """
183        return lib_py_gel.Manifold_halfedge_in_use(self.obj, hid)
184    def flip_edge(self,hid):
185        """ Flip the edge, hid, separating two faces. The function first verifies that
186        the edge is flippable. This entails making sure that all of the
187        following are true.
188        1. adjacent faces are triangles.
189        2. neither end point has valency three or less.
190        3. the vertices that will be connected are not already.
191        If the tests are passed, the flip is performed and the function
192        returns True. Otherwise False."""
193        return lib_py_gel.Manifold_flip_edge(self.obj,hid)
194    def collapse_edge(self,hid, avg_vertices=False):
195        """ Collapse an edge hid.
196        Before collapsing hid, a number of tests are made:
197        ---
198        1.  For the two vertices adjacent to the edge, we generate a list of all their neighbouring vertices.
199        We then generate a  list of the vertices that occur in both these lists.
200        That is, we find all vertices connected by edges to both endpoints of the edge and store these in a list.
201        2.  For both faces incident on the edge, check whether they are triangular.
202        If this is the case, the face will be removed, and it is ok that the the third vertex is connected to both endpoints.
203        Thus the third vertex in such a face is removed from the list generated in 1.
204        3.  If the list is now empty, all is well.
205        Otherwise, there would be a vertex in the new mesh with two edges connecting it to the same vertex. Return false.
206        4.  TETRAHEDRON TEST:
207        If the valency of both vertices is three, and the incident faces are triangles, we also disallow the operation.
208        Reason: A vertex valency of two and two triangles incident on the adjacent vertices makes the construction collapse.
209        5.  VALENCY 4 TEST:
210        If a triangle is adjacent to the edge being collapsed, it disappears.
211        This means the valency of the remaining edge vertex is decreased by one.
212        A valency two vertex reduced to a valency one vertex is considered illegal.
213        6.  PREVENT MERGING HOLES:
214        Collapsing an edge with boundary endpoints and valid faces results in the creation where two holes meet.
215        A non manifold situation. We could relax this...
216        7. New test: if the same face is in the one-ring of both vertices but not adjacent to the common edge,
217        then the result of a collapse would be a one ring where the same face occurs twice. This is disallowed as the resulting
218        face would be non-simple.
219        If the tests are passed, the collapse is performed and the function
220        returns True. Otherwise False."""
221        return lib_py_gel.Manifold_collapse_edge(self.obj, hid, avg_vertices)
222    def split_face_by_edge(self,fid,v0,v1):
223        """   Split a face. The face, fid, is split by creating an edge with
224        endpoints v0 and v1 (the next two arguments). The vertices of the old
225        face between v0 and v1 (in counter clockwise order) continue to belong
226        to fid. The vertices between v1 and v0 belong to the new face. A handle to
227        the new face is returned. """
228        return lib_py_gel.Manifold_split_face_by_edge(self.obj, fid, v0, v1)
229    def split_face_by_vertex(self,fid):
230        """   Split a polygon, fid, by inserting a vertex at the barycenter. This
231        function is less likely to create flipped triangles than the
232        split_face_triangulate function. On the other hand, it introduces more
233        vertices and probably makes the triangles more acute. The vertex id of the
234        inserted vertex is returned. """
235        return lib_py_gel.Manifold_split_face_by_vertex(self.obj,fid)
236    def split_edge(self,hid):
237        """   Insert a new vertex on halfedge hid. The new halfedge is insterted
238        as the previous edge to hid. The vertex id of the inserted vertex is returned. """
239        return lib_py_gel.Manifold_split_edge(self.obj,hid)
240    def stitch_boundary_edges(self,h0,h1):
241        """   Stitch two halfedges. Two boundary halfedges, h0 and h1, can be stitched
242        together. This can be used to build a complex mesh from a bunch of
243        simple faces. """
244        return lib_py_gel.Manifold_stitch_boundary_edges(self.obj, h0, h1)
245    def merge_faces(self,hid):
246        """   Merges two faces into a single polygon. The merged faces are those shared
247        by the edge for which hid is one of the two corresponding halfedges. This function returns
248        true if the merging was possible and false otherwise. Currently merge
249        only fails if the mesh is already illegal. Thus it should, in fact,
250        never fail. """
251        if self.is_halfedge_at_boundary(hid):
252            return False
253        fid = self.incident_face(hid)
254        return lib_py_gel.Manifold_merge_faces(self.obj, fid, hid)
255    def close_hole(self,hid):
256        """ Close hole given by hid (i.e. the face referenced by hid). Returns
257        index of the created face or the face that was already there if, in
258        fact, hid was not next to a hole. """
259        return lib_py_gel.Manifold_close_hole(self.obj, hid)
260    def cleanup(self):
261        """ Remove unused items from Mesh. This function remaps all vertices, halfedges
262        and faces such that the arrays do not contain any holes left by unused mesh
263        entities. It is a good idea to call this function when a mesh has been simplified
264        or changed in other ways such that mesh entities have been removed. However, note
265        that it invalidates any attributes that you might have stored in auxilliary arrays."""
266        lib_py_gel.Manifold_cleanup(self.obj)
267    def is_halfedge_at_boundary(self, hid):
268        """ Returns True if hid is a boundary halfedge, i.e. face on either
269        side is invalid. """
270        return lib_py_gel.is_halfedge_at_boundary(self.obj, hid)
271    def is_vertex_at_boundary(self, vid):
272        """ Returns True if vid lies on a boundary. """
273        return lib_py_gel.is_vertex_at_boundary(self.obj, vid)
274    def edge_length(self, hid):
275        """ Returns length of edge given by halfedge hid which is passed as argument. """
276        return lib_py_gel.length(self.obj, hid)
277    def valency(self,vid):
278        """ Returns valency of vid, i.e. number of incident edges."""
279        return lib_py_gel.valency(self.obj,vid)
280    def vertex_normal(self, vid):
281        """ Returns vertex normal (angle weighted) of vertex given by vid """
282        n = ndarray(3,dtype=np.float64)
283        lib_py_gel.vertex_normal(self.obj, vid, n)
284        return n
285    def connected(self, v0, v1):
286        """ Returns true if the two argument vertices, v0 and v1, are in each other's one-rings."""
287        return lib_py_gel.connected(self.obj,v0,v1)
288    def no_edges(self, fid):
289        """ Compute the number of edges of a face fid """
290        return lib_py_gel.no_edges(self.obj, fid)
291    def face_normal(self, fid):
292        """ Compute the normal of a face fid. If the face is not a triangle,
293        the normal is not defined, but computed using the first three
294        vertices of the face. """
295        n = ndarray(3, dtype=np.float64)
296        lib_py_gel.face_normal(self.obj, fid, n)
297        return n
298    def area(self, fid):
299        """ Returns the area of a face fid. """
300        return lib_py_gel.area(self.obj, fid)
301    def perimeter(self, fid):
302        """ Returns the perimeter of a face fid. """
303        return lib_py_gel.perimeter(self.obj, fid)
304    def centre(self, fid):
305        """ Returns the centre of a face. """
306        c = ndarray(3, dtype=np.float64)
307        lib_py_gel.centre(self.obj, fid, c)
308        return c

The Manifold class represents a halfedge based mesh. It is maybe a bit grand to call a mesh class Manifold, but meshes based on the halfedge representation are manifold (if we ignore a few corner cases) unlike some other representations. This class contains a number of methods for mesh manipulation and inspection. Note also that numerous further functions are available to manipulate meshes stored as Manifolds.

Many of the functions below accept arguments called hid, fid, or vid. These are simply indices of halfedges, faces and vertices, respectively: integer numbers that identify the corresponding mesh element. Using a plain integer to identify a mesh entity means that, for instance, a vertex index can also be used as an index into, say, a NumPy array without any conversion.

@classmethod
def from_triangles(cls, vertices, faces):
37    @classmethod
38    def from_triangles(cls,vertices, faces):
39        """ Given a list of vertices and triangles (faces), this function produces
40        a Manifold mesh."""
41        m = cls()
42        vertices = np.asarray(vertices,dtype=np.float64, order='C')
43        faces = np.asarray(faces,dtype=ct.c_int, order='C')
44        m.obj = lib_py_gel.Manifold_from_triangles(len(vertices),len(faces),vertices,faces)
45        return m

Given a list of vertices and triangles (faces), this function produces a Manifold mesh.

@classmethod
def from_points(cls, pts, xaxis=array([1, 0, 0]), yaxis=array([0, 1, 0])):
46    @classmethod
47    def from_points(cls,pts,xaxis=np.array([1,0,0]),yaxis=np.array([0,1,0])):
48        """ This function computes the Delaunay triangulation of pts. You need
49        to specify xaxis and yaxis if they are not canonical. The function returns
50        a Manifold with the resulting triangles. Clearly, this function will
51        give surprising results if the surface represented by the points is not
52        well represented as a 2.5D surface, aka a height field. """
53        m = cls()
54        pts = np.asarray(pts,dtype=np.float64, order='C')
55        xaxis = np.asarray(xaxis,dtype=np.float64, order='C')
56        yaxis = np.asarray(yaxis,dtype=np.float64, order='C')
57        m.obj = lib_py_gel.Manifold_from_points(len(pts), pts, xaxis, yaxis)
58        return m

This function computes the Delaunay triangulation of pts. You need to specify xaxis and yaxis if they are not canonical. The function returns a Manifold with the resulting triangles. Clearly, this function will give surprising results if the surface represented by the points is not well represented as a 2.5D surface, aka a height field.

def merge_with(self, other):
61    def merge_with(self, other):
62        """ Merge this Manifold with another one given as the argument. This function
63        does not return anything. It simply combines the two meshes in the Manifold on which
64        the method is called. """
65        lib_py_gel.Manifold_merge(self.obj, other.obj)

Merge this Manifold with another one given as the argument. This function does not return anything. It simply combines the two meshes in the Manifold on which the method is called.

def add_face(self, pts):
66    def add_face(self,pts):
67        """ Add a face to the Manifold.
68        This function takes a list of 3D points, pts, as argument and creates a face
69        in the mesh with those points as vertices. The function returns the index
70        of the created face.
71        """
72        pts = np.asarray(pts,dtype=np.float64, order='C')
73        return lib_py_gel.Manifold_add_face(self.obj, len(pts), pts)

Add a face to the Manifold. This function takes a list of 3D points, pts, as argument and creates a face in the mesh with those points as vertices. The function returns the index of the created face.

def positions(self):
74    def positions(self):
75        """ Retrieve an array containing the vertex positions of the Manifold.
76        It is not a copy: any changes are made to the actual vertex positions. """
77        pos = ct.POINTER(ct.c_double)()
78        n = lib_py_gel.Manifold_positions(self.obj, ct.byref(pos))
79        return np.ctypeslib.as_array(pos,(n,3))

Retrieve an array containing the vertex positions of the Manifold. It is not a copy: any changes are made to the actual vertex positions.

def no_allocated_vertices(self):
80    def no_allocated_vertices(self):
81        """ Number of vertices.
82        This number could be higher than the number of actually
83        used vertices, but corresponds to the size of the array allocated
84        for vertices."""
85        return lib_py_gel.Manifold_no_allocated_vertices(self.obj)

Number of vertices. This number could be higher than the number of actually used vertices, but corresponds to the size of the array allocated for vertices.

def no_allocated_faces(self):
86    def no_allocated_faces(self):
87        """ Number of faces.
88        This number could be higher than the number of actually
89        used faces, but corresponds to the size of the array allocated
90        for faces."""
91        return lib_py_gel.Manifold_no_allocated_faces(self.obj)

Number of faces. This number could be higher than the number of actually used faces, but corresponds to the size of the array allocated for faces.

def no_allocated_halfedges(self):
92    def no_allocated_halfedges(self):
93        """ Number of halfedges.
94        This number could be higher than the number of actually
95        used halfedges, but corresponds to the size of the array allocated
96        for halfedges."""
97        return lib_py_gel.Manifold_no_allocated_halfedges(self.obj)

Number of halfedges. This number could be higher than the number of actually used halfedges, but corresponds to the size of the array allocated for halfedges.

def vertices(self):
 98    def vertices(self):
 99        """ Returns an iterable containing all vertex indices"""
100        verts = IntVector()
101        n = lib_py_gel.Manifold_vertices(self.obj, verts.obj)
102        return verts

Returns an iterable containing all vertex indices

def faces(self):
103    def faces(self):
104        """ Returns an iterable containing all face indices"""
105        faces = IntVector()
106        n = lib_py_gel.Manifold_faces(self.obj, faces.obj)
107        return faces

Returns an iterable containing all face indices

def halfedges(self):
108    def halfedges(self):
109        """ Returns an iterable containing all halfedge indices"""
110        hedges = IntVector()
111        n = lib_py_gel.Manifold_halfedges(self.obj, hedges.obj)
112        return hedges

Returns an iterable containing all halfedge indices

def circulate_vertex(self, vid, mode='v'):
113    def circulate_vertex(self, vid, mode='v'):
114        """ Circulate a vertex. Passed a vertex index, vid, and second argument,
115        mode='f', this function will return an iterable with all faces incident
116        on vid arranged in counter clockwise order. Similarly, if mode is 'h',
117        incident halfedges (outgoing) are returned, and for mode = 'v', all
118        neighboring vertices are returned. """
119        nbrs = IntVector()
120        n = lib_py_gel.Manifold_circulate_vertex(self.obj, vid, ct.c_char(mode.encode('ascii')), nbrs.obj)
121        return nbrs

Circulate a vertex. Passed a vertex index, vid, and second argument, mode='f', this function will return an iterable with all faces incident on vid arranged in counter clockwise order. Similarly, if mode is 'h', incident halfedges (outgoing) are returned, and for mode = 'v', all neighboring vertices are returned.

def circulate_face(self, fid, mode='v'):
122    def circulate_face(self, fid, mode='v'):
123        """ Circulate a face. Passed a face index, fid, and second argument,
124        mode='f', this function will return an iterable with all faces that
125        share an edge with fid (in counter clockwise order). If the argument is
126        mode='h', the halfedges themselves are returned. For mode='v', the
127        incident vertices of the face are returned. """
128        nbrs = IntVector()
129        n = lib_py_gel.Manifold_circulate_face(self.obj, fid, ct.c_char(mode.encode('ascii')), nbrs.obj)
130        return nbrs

Circulate a face. Passed a face index, fid, and second argument, mode='f', this function will return an iterable with all faces that share an edge with fid (in counter clockwise order). If the argument is mode='h', the halfedges themselves are returned. For mode='v', the incident vertices of the face are returned.

def next_halfedge(self, hid):
131    def next_halfedge(self,hid):
132        """ Returns next halfedge to hid. """
133        return lib_py_gel.Walker_next_halfedge(self.obj, hid)

Returns next halfedge to hid.

def prev_halfedge(self, hid):
134    def prev_halfedge(self,hid):
135        """ Returns previous halfedge to hid. """
136        return lib_py_gel.Walker_prev_halfedge(self.obj, hid)

Returns previous halfedge to hid.

def opposite_halfedge(self, hid):
137    def opposite_halfedge(self,hid):
138        """ Returns opposite halfedge to hid. """
139        return lib_py_gel.Walker_opposite_halfedge(self.obj, hid)

Returns opposite halfedge to hid.

def incident_face(self, hid):
140    def incident_face(self,hid):
141        """ Returns face corresponding to hid. """
142        return lib_py_gel.Walker_incident_face(self.obj, hid)

Returns face corresponding to hid.

def incident_vertex(self, hid):
143    def incident_vertex(self,hid):
144        """ Returns vertex corresponding to (or pointed to by) hid. """
145        return lib_py_gel.Walker_incident_vertex(self.obj, hid)

Returns vertex corresponding to (or pointed to by) hid.

def remove_vertex(self, vid):
146    def remove_vertex(self,vid):
147        """ Remove vertex vid from the Manifold. This function merges all faces
148        around the vertex into one and then removes this resulting face. """
149        return lib_py_gel.Manifold_remove_vertex(self.obj, vid)

Remove vertex vid from the Manifold. This function merges all faces around the vertex into one and then removes this resulting face.

def remove_face(self, fid):
150    def remove_face(self,fid):
151        """ Removes a face, fid, from the Manifold. If it is an interior face it is
152        simply replaced by an invalid index. If the face contains boundary
153        edges, these are removed. Situations may arise where the mesh is no
154        longer manifold because the situation at a boundary vertex is not
155        homeomorphic to a half disk. This, we can probably ignore since from the
156        data structure point of view it is not really a problem that a vertex is
157        incident on two holes - a hole can be seen as a special type of face.
158        The function returns false if the index of the face is not valid,
159        otherwise the function must complete. """
160        return lib_py_gel.Manifold_remove_face(self.obj, fid)

Removes a face, fid, from the Manifold. If it is an interior face it is simply replaced by an invalid index. If the face contains boundary edges, these are removed. Situations may arise where the mesh is no longer manifold because the situation at a boundary vertex is not homeomorphic to a half disk. This, we can probably ignore since from the data structure point of view it is not really a problem that a vertex is incident on two holes - a hole can be seen as a special type of face. The function returns false if the index of the face is not valid, otherwise the function must complete.

def remove_edge(self, hid):
161    def remove_edge(self,hid):
162        """ Remove an edge, hid, from the Manifold. This function will remove the
163        faces on either side and the edge itself in the process. Thus, it is a
164        simple application of remove_face. """
165        return lib_py_gel.Manifold_remove_edge(self.obj, hid)

Remove an edge, hid, from the Manifold. This function will remove the faces on either side and the edge itself in the process. Thus, it is a simple application of remove_face.

def vertex_in_use(self, vid):
166    def vertex_in_use(self,vid):
167        """ check if vertex, vid, is in use. This function returns true if the id corresponds
168        to a vertex that is currently in the mesh and false otherwise. vid could
169        be outside the range of used ids and it could also correspond to a vertex
170        which is not active. The function returns false in both cases. """
171        return lib_py_gel.Manifold_vertex_in_use(self.obj, vid)

check if vertex, vid, is in use. This function returns true if the id corresponds to a vertex that is currently in the mesh and false otherwise. vid could be outside the range of used ids and it could also correspond to a vertex which is not active. The function returns false in both cases.

def face_in_use(self, fid):
172    def face_in_use(self,fid):
173        """ check if face, fid, is in use. This function returns true if the id corresponds
174        to a face that is currently in the mesh and false otherwise. fid could
175        be outside the range of used ids and it could also correspond to a face
176        which is not active. The function returns false in both cases. """
177        return lib_py_gel.Manifold_face_in_use(self.obj, fid)

check if face, fid, is in use. This function returns true if the id corresponds to a face that is currently in the mesh and false otherwise. fid could be outside the range of used ids and it could also correspond to a face which is not active. The function returns false in both cases.

def halfedge_in_use(self, hid):
178    def halfedge_in_use(self,hid):
179        """ check if halfedge hid is in use. This function returns true if the id corresponds
180        to a halfedge that is currently in the mesh and false otherwise. hid could
181        be outside the range of used ids and it could also correspond to a halfedge
182        which is not active. The function returns false in both cases. """
183        return lib_py_gel.Manifold_halfedge_in_use(self.obj, hid)

check if halfedge hid is in use. This function returns true if the id corresponds to a halfedge that is currently in the mesh and false otherwise. hid could be outside the range of used ids and it could also correspond to a halfedge which is not active. The function returns false in both cases.

def flip_edge(self, hid):
184    def flip_edge(self,hid):
185        """ Flip the edge, hid, separating two faces. The function first verifies that
186        the edge is flippable. This entails making sure that all of the
187        following are true.
188        1. adjacent faces are triangles.
189        2. neither end point has valency three or less.
190        3. the vertices that will be connected are not already.
191        If the tests are passed, the flip is performed and the function
192        returns True. Otherwise False."""
193        return lib_py_gel.Manifold_flip_edge(self.obj,hid)

Flip the edge, hid, separating two faces. The function first verifies that the edge is flippable. This entails making sure that all of the following are true.

  1. adjacent faces are triangles.
  2. neither end point has valency three or less.
  3. the vertices that will be connected are not already. If the tests are passed, the flip is performed and the function returns True. Otherwise False.
def collapse_edge(self, hid, avg_vertices=False):
194    def collapse_edge(self,hid, avg_vertices=False):
195        """ Collapse an edge hid.
196        Before collapsing hid, a number of tests are made:
197        ---
198        1.  For the two vertices adjacent to the edge, we generate a list of all their neighbouring vertices.
199        We then generate a  list of the vertices that occur in both these lists.
200        That is, we find all vertices connected by edges to both endpoints of the edge and store these in a list.
201        2.  For both faces incident on the edge, check whether they are triangular.
202        If this is the case, the face will be removed, and it is ok that the the third vertex is connected to both endpoints.
203        Thus the third vertex in such a face is removed from the list generated in 1.
204        3.  If the list is now empty, all is well.
205        Otherwise, there would be a vertex in the new mesh with two edges connecting it to the same vertex. Return false.
206        4.  TETRAHEDRON TEST:
207        If the valency of both vertices is three, and the incident faces are triangles, we also disallow the operation.
208        Reason: A vertex valency of two and two triangles incident on the adjacent vertices makes the construction collapse.
209        5.  VALENCY 4 TEST:
210        If a triangle is adjacent to the edge being collapsed, it disappears.
211        This means the valency of the remaining edge vertex is decreased by one.
212        A valency two vertex reduced to a valency one vertex is considered illegal.
213        6.  PREVENT MERGING HOLES:
214        Collapsing an edge with boundary endpoints and valid faces results in the creation where two holes meet.
215        A non manifold situation. We could relax this...
216        7. New test: if the same face is in the one-ring of both vertices but not adjacent to the common edge,
217        then the result of a collapse would be a one ring where the same face occurs twice. This is disallowed as the resulting
218        face would be non-simple.
219        If the tests are passed, the collapse is performed and the function
220        returns True. Otherwise False."""
221        return lib_py_gel.Manifold_collapse_edge(self.obj, hid, avg_vertices)

Collapse an edge hid.

Before collapsing hid, a number of tests are made:

  1. For the two vertices adjacent to the edge, we generate a list of all their neighbouring vertices. We then generate a list of the vertices that occur in both these lists. That is, we find all vertices connected by edges to both endpoints of the edge and store these in a list.
  2. For both faces incident on the edge, check whether they are triangular. If this is the case, the face will be removed, and it is ok that the the third vertex is connected to both endpoints. Thus the third vertex in such a face is removed from the list generated in 1.
  3. If the list is now empty, all is well. Otherwise, there would be a vertex in the new mesh with two edges connecting it to the same vertex. Return false.
  4. TETRAHEDRON TEST: If the valency of both vertices is three, and the incident faces are triangles, we also disallow the operation. Reason: A vertex valency of two and two triangles incident on the adjacent vertices makes the construction collapse.
  5. VALENCY 4 TEST: If a triangle is adjacent to the edge being collapsed, it disappears. This means the valency of the remaining edge vertex is decreased by one. A valency two vertex reduced to a valency one vertex is considered illegal.
  6. PREVENT MERGING HOLES: Collapsing an edge with boundary endpoints and valid faces results in the creation where two holes meet. A non manifold situation. We could relax this...
  7. New test: if the same face is in the one-ring of both vertices but not adjacent to the common edge, then the result of a collapse would be a one ring where the same face occurs twice. This is disallowed as the resulting face would be non-simple. If the tests are passed, the collapse is performed and the function returns True. Otherwise False.
def split_face_by_edge(self, fid, v0, v1):
222    def split_face_by_edge(self,fid,v0,v1):
223        """   Split a face. The face, fid, is split by creating an edge with
224        endpoints v0 and v1 (the next two arguments). The vertices of the old
225        face between v0 and v1 (in counter clockwise order) continue to belong
226        to fid. The vertices between v1 and v0 belong to the new face. A handle to
227        the new face is returned. """
228        return lib_py_gel.Manifold_split_face_by_edge(self.obj, fid, v0, v1)

Split a face. The face, fid, is split by creating an edge with endpoints v0 and v1 (the next two arguments). The vertices of the old face between v0 and v1 (in counter clockwise order) continue to belong to fid. The vertices between v1 and v0 belong to the new face. A handle to the new face is returned.

def split_face_by_vertex(self, fid):
229    def split_face_by_vertex(self,fid):
230        """   Split a polygon, fid, by inserting a vertex at the barycenter. This
231        function is less likely to create flipped triangles than the
232        split_face_triangulate function. On the other hand, it introduces more
233        vertices and probably makes the triangles more acute. The vertex id of the
234        inserted vertex is returned. """
235        return lib_py_gel.Manifold_split_face_by_vertex(self.obj,fid)

Split a polygon, fid, by inserting a vertex at the barycenter. This function is less likely to create flipped triangles than the split_face_triangulate function. On the other hand, it introduces more vertices and probably makes the triangles more acute. The vertex id of the inserted vertex is returned.

def split_edge(self, hid):
236    def split_edge(self,hid):
237        """   Insert a new vertex on halfedge hid. The new halfedge is insterted
238        as the previous edge to hid. The vertex id of the inserted vertex is returned. """
239        return lib_py_gel.Manifold_split_edge(self.obj,hid)

Insert a new vertex on halfedge hid. The new halfedge is insterted as the previous edge to hid. The vertex id of the inserted vertex is returned.

def stitch_boundary_edges(self, h0, h1):
240    def stitch_boundary_edges(self,h0,h1):
241        """   Stitch two halfedges. Two boundary halfedges, h0 and h1, can be stitched
242        together. This can be used to build a complex mesh from a bunch of
243        simple faces. """
244        return lib_py_gel.Manifold_stitch_boundary_edges(self.obj, h0, h1)

Stitch two halfedges. Two boundary halfedges, h0 and h1, can be stitched together. This can be used to build a complex mesh from a bunch of simple faces.

def merge_faces(self, hid):
245    def merge_faces(self,hid):
246        """   Merges two faces into a single polygon. The merged faces are those shared
247        by the edge for which hid is one of the two corresponding halfedges. This function returns
248        true if the merging was possible and false otherwise. Currently merge
249        only fails if the mesh is already illegal. Thus it should, in fact,
250        never fail. """
251        if self.is_halfedge_at_boundary(hid):
252            return False
253        fid = self.incident_face(hid)
254        return lib_py_gel.Manifold_merge_faces(self.obj, fid, hid)

Merges two faces into a single polygon. The merged faces are those shared by the edge for which hid is one of the two corresponding halfedges. This function returns true if the merging was possible and false otherwise. Currently merge only fails if the mesh is already illegal. Thus it should, in fact, never fail.

def close_hole(self, hid):
255    def close_hole(self,hid):
256        """ Close hole given by hid (i.e. the face referenced by hid). Returns
257        index of the created face or the face that was already there if, in
258        fact, hid was not next to a hole. """
259        return lib_py_gel.Manifold_close_hole(self.obj, hid)

Close hole given by hid (i.e. the face referenced by hid). Returns index of the created face or the face that was already there if, in fact, hid was not next to a hole.

def cleanup(self):
260    def cleanup(self):
261        """ Remove unused items from Mesh. This function remaps all vertices, halfedges
262        and faces such that the arrays do not contain any holes left by unused mesh
263        entities. It is a good idea to call this function when a mesh has been simplified
264        or changed in other ways such that mesh entities have been removed. However, note
265        that it invalidates any attributes that you might have stored in auxilliary arrays."""
266        lib_py_gel.Manifold_cleanup(self.obj)

Remove unused items from Mesh. This function remaps all vertices, halfedges and faces such that the arrays do not contain any holes left by unused mesh entities. It is a good idea to call this function when a mesh has been simplified or changed in other ways such that mesh entities have been removed. However, note that it invalidates any attributes that you might have stored in auxilliary arrays.

def is_halfedge_at_boundary(self, hid):
267    def is_halfedge_at_boundary(self, hid):
268        """ Returns True if hid is a boundary halfedge, i.e. face on either
269        side is invalid. """
270        return lib_py_gel.is_halfedge_at_boundary(self.obj, hid)

Returns True if hid is a boundary halfedge, i.e. face on either side is invalid.

def is_vertex_at_boundary(self, vid):
271    def is_vertex_at_boundary(self, vid):
272        """ Returns True if vid lies on a boundary. """
273        return lib_py_gel.is_vertex_at_boundary(self.obj, vid)

Returns True if vid lies on a boundary.

def edge_length(self, hid):
274    def edge_length(self, hid):
275        """ Returns length of edge given by halfedge hid which is passed as argument. """
276        return lib_py_gel.length(self.obj, hid)

Returns length of edge given by halfedge hid which is passed as argument.

def valency(self, vid):
277    def valency(self,vid):
278        """ Returns valency of vid, i.e. number of incident edges."""
279        return lib_py_gel.valency(self.obj,vid)

Returns valency of vid, i.e. number of incident edges.

def vertex_normal(self, vid):
280    def vertex_normal(self, vid):
281        """ Returns vertex normal (angle weighted) of vertex given by vid """
282        n = ndarray(3,dtype=np.float64)
283        lib_py_gel.vertex_normal(self.obj, vid, n)
284        return n

Returns vertex normal (angle weighted) of vertex given by vid

def connected(self, v0, v1):
285    def connected(self, v0, v1):
286        """ Returns true if the two argument vertices, v0 and v1, are in each other's one-rings."""
287        return lib_py_gel.connected(self.obj,v0,v1)

Returns true if the two argument vertices, v0 and v1, are in each other's one-rings.

def no_edges(self, fid):
288    def no_edges(self, fid):
289        """ Compute the number of edges of a face fid """
290        return lib_py_gel.no_edges(self.obj, fid)

Compute the number of edges of a face fid

def face_normal(self, fid):
291    def face_normal(self, fid):
292        """ Compute the normal of a face fid. If the face is not a triangle,
293        the normal is not defined, but computed using the first three
294        vertices of the face. """
295        n = ndarray(3, dtype=np.float64)
296        lib_py_gel.face_normal(self.obj, fid, n)
297        return n

Compute the normal of a face fid. If the face is not a triangle, the normal is not defined, but computed using the first three vertices of the face.

def area(self, fid):
298    def area(self, fid):
299        """ Returns the area of a face fid. """
300        return lib_py_gel.area(self.obj, fid)

Returns the area of a face fid.

def perimeter(self, fid):
301    def perimeter(self, fid):
302        """ Returns the perimeter of a face fid. """
303        return lib_py_gel.perimeter(self.obj, fid)

Returns the perimeter of a face fid.

def centre(self, fid):
304    def centre(self, fid):
305        """ Returns the centre of a face. """
306        c = ndarray(3, dtype=np.float64)
307        lib_py_gel.centre(self.obj, fid, c)
308        return c

Returns the centre of a face.

def valid(m):
310def valid(m):
311    """This function performs a series of tests to check that this
312    is a valid manifold. This function is not rigorously constructed but seems
313    to catch all problems so far. The function returns true if the mesh is valid
314    and false otherwise. """
315    return lib_py_gel.valid(m.obj)

This function performs a series of tests to check that this is a valid manifold. This function is not rigorously constructed but seems to catch all problems so far. The function returns true if the mesh is valid and false otherwise.

def closed(m):
317def closed(m):
318    """ Returns true if m is closed, i.e. has no boundary."""
319    return lib_py_gel.closed(m.obj)

Returns true if m is closed, i.e. has no boundary.

def bbox(m):
321def bbox(m):
322    """ Returns the min and max corners of the bounding box of Manifold m. """
323    pmin = ndarray(3,dtype=np.float64)
324    pmax = ndarray(3,dtype=np.float64)
325    lib_py_gel.bbox(m.obj, pmin, pmax)
326    return pmin, pmax

Returns the min and max corners of the bounding box of Manifold m.

def bsphere(m):
328def bsphere(m):
329    """ Calculate the bounding sphere of the manifold m.
330    Returns centre,radius """
331    c = ndarray(3,dtype=np.float64)
332    r = (ct.c_double)()
333    lib_py_gel.bsphere(m.obj, c, ct.byref(r))
334    return (c,r)

Calculate the bounding sphere of the manifold m. Returns centre,radius

def stitch(m, rad=1e-30):
336def stitch(m, rad=1e-30):
337    """ Stitch together edges of m whose endpoints coincide geometrically. This
338    function allows you to create a mesh as a bunch of faces and then stitch
339    these together to form a coherent whole. What this function adds is a
340    spatial data structure to find out which vertices coincide. The return value
341    is the number of edges that could not be stitched. Often this is because it
342    would introduce a non-manifold situation."""
343    return lib_py_gel.stitch_mesh(m.obj,rad)

Stitch together edges of m whose endpoints coincide geometrically. This function allows you to create a mesh as a bunch of faces and then stitch these together to form a coherent whole. What this function adds is a spatial data structure to find out which vertices coincide. The return value is the number of edges that could not be stitched. Often this is because it would introduce a non-manifold situation.

def obj_save(fn, m):
345def obj_save(fn, m):
346    """ Save Manifold m to Wavefront obj file. """
347    s = ct.c_char_p(fn.encode('utf-8'))
348    lib_py_gel.obj_save(s, m.obj)

Save Manifold m to Wavefront obj file.

def off_save(fn, m):
350def off_save(fn, m):
351    """ Save Manifold m to OFF file. """
352    s = ct.c_char_p(fn.encode('utf-8'))
353    lib_py_gel.off_save(s, m.obj)

Save Manifold m to OFF file.

def x3d_save(fn, m):
355def x3d_save(fn, m):
356    """ Save Manifold m to X3D file. """
357    s = ct.c_char_p(fn.encode('utf-8'))
358    lib_py_gel.x3d_save(s, m.obj)

Save Manifold m to X3D file.

def obj_load(fn):
360def obj_load(fn):
361    """ Load and return Manifold from Wavefront obj file.
362    Returns None if loading failed. """
363    m = Manifold()
364    s = ct.c_char_p(fn.encode('utf-8'))
365    if lib_py_gel.obj_load(s, m.obj):
366        return m
367    return None

Load and return Manifold from Wavefront obj file. Returns None if loading failed.

def off_load(fn):
369def off_load(fn):
370    """ Load and return Manifold from OFF file.
371    Returns None if loading failed."""
372    m = Manifold()
373    s = ct.c_char_p(fn.encode('utf-8'))
374    if lib_py_gel.off_load(s, m.obj):
375        return m
376    return None

Load and return Manifold from OFF file. Returns None if loading failed.

def ply_load(fn):
378def ply_load(fn):
379    """ Load and return Manifold from Stanford PLY file.
380    Returns None if loading failed. """
381    m = Manifold()
382    s = ct.c_char_p(fn.encode('utf-8'))
383    if lib_py_gel.ply_load(s, m.obj):
384        return m
385    return None

Load and return Manifold from Stanford PLY file. Returns None if loading failed.

def x3d_load(fn):
387def x3d_load(fn):
388    """ Load and return Manifold from X3D file.
389    Returns None if loading failed."""
390    m = Manifold()
391    s = ct.c_char_p(fn.encode('utf-8'))
392    if lib_py_gel.x3d_load(s, m.obj):
393        return m
394    return None

Load and return Manifold from X3D file. Returns None if loading failed.

def load(fn):
397def load(fn):
398    """ Load a Manifold from an X3D/OBJ/OFF/PLY file. Return the
399    loaded Manifold. Returns None if loading failed."""
400    name, extension = splitext(fn)
401    if extension.lower() == ".x3d":
402        return x3d_load(fn)
403    if extension.lower() == ".obj":
404        return obj_load(fn)
405    if extension.lower() == ".off":
406        return off_load(fn)
407    if extension.lower() == ".ply":
408        return ply_load(fn)
409    return None

Load a Manifold from an X3D/OBJ/OFF/PLY file. Return the loaded Manifold. Returns None if loading failed.

def save(fn, m):
411def save(fn, m):
412    """ Save a Manifold, m, to an X3D/OBJ/OFF file. """
413    name, extension = splitext(fn)
414    if extension.lower() == ".x3d":
415        x3d_save(fn, m)
416    elif extension.lower() == ".obj":
417        obj_save(fn, m)
418    elif extension.lower() == ".off":
419        off_save(fn, m)

Save a Manifold, m, to an X3D/OBJ/OFF file.

def remove_caps(m, thresh=2.9):
422def remove_caps(m, thresh=2.9):
423    """ Remove caps from a manifold, m, consisting of only triangles. A cap is a
424    triangle with two very small angles and an angle close to pi, however a cap
425    does not necessarily have a very short edge. Set the ang_thresh to a value
426    close to pi. The closer to pi the _less_ sensitive the cap removal. A cap is
427    removed by flipping the (long) edge E opposite to the vertex V with the
428    angle close to pi. However, the function is more complex. Read code and
429    document more carefully !!! """
430    lib_py_gel.remove_caps(m.obj,thresh)

Remove caps from a manifold, m, consisting of only triangles. A cap is a triangle with two very small angles and an angle close to pi, however a cap does not necessarily have a very short edge. Set the ang_thresh to a value close to pi. The closer to pi the _less_ sensitive the cap removal. A cap is removed by flipping the (long) edge E opposite to the vertex V with the angle close to pi. However, the function is more complex. Read code and document more carefully !!!

def remove_needles(m, thresh=0.05, average_positions=False):
432def remove_needles(m, thresh=0.05, average_positions=False):
433    """  Remove needles from a manifold, m, consisting of only triangles. A needle
434    is a triangle with a single very short edge. It is moved by collapsing the
435    short edge. The thresh parameter sets the length threshold (in terms of the average edge length
436    in the mesh). If average_positions is true then the collapsed vertex is placed at the average position of the end points."""
437    abs_thresh = thresh * average_edge_length(m)
438    lib_py_gel.remove_needles(m.obj,abs_thresh, average_positions)

Remove needles from a manifold, m, consisting of only triangles. A needle is a triangle with a single very short edge. It is moved by collapsing the short edge. The thresh parameter sets the length threshold (in terms of the average edge length in the mesh). If average_positions is true then the collapsed vertex is placed at the average position of the end points.

def close_holes(m, max_size=100):
440def close_holes(m, max_size=100):
441    """  This function replaces holes in m by faces. It is really a simple function
442    that just finds all loops of edges next to missing faces. """
443    lib_py_gel.close_holes(m.obj, max_size)

This function replaces holes in m by faces. It is really a simple function that just finds all loops of edges next to missing faces.

def flip_orientation(m):
445def flip_orientation(m):
446    """  Flip the orientation of a mesh, m. After calling this function, normals
447    will point the other way and clockwise becomes counter clockwise """
448    lib_py_gel.flip_orientation(m.obj)

Flip the orientation of a mesh, m. After calling this function, normals will point the other way and clockwise becomes counter clockwise

def merge_coincident_boundary_vertices(m, rad=1e-30):
450def merge_coincident_boundary_vertices(m, rad = 1.0e-30):
451    """  Merge vertices of m that are boundary vertices and coincident.
452        However, if one belongs to the other's one ring or the one
453        rings share a vertex, they will not be merged. """
454    lib_py_gel.merge_coincident_boundary_vertices(m.obj, rad)

Merge vertices of m that are boundary vertices and coincident. However, if one belongs to the other's one ring or the one rings share a vertex, they will not be merged.

def minimize_curvature(m, anneal=False):
456def minimize_curvature(m,anneal=False):
457    """ Minimizes mean curvature of m by flipping edges. Hence, no vertices are moved.
458     This is really the same as dihedral angle minimization, except that we weight by edge length. """
459    lib_py_gel.minimize_curvature(m.obj, anneal)

Minimizes mean curvature of m by flipping edges. Hence, no vertices are moved. This is really the same as dihedral angle minimization, except that we weight by edge length.

def minimize_dihedral_angle(m, max_iter=10000, anneal=False, alpha=False, gamma=4.0):
461def minimize_dihedral_angle(m,max_iter=10000, anneal=False, alpha=False, gamma=4.0):
462    """ Minimizes dihedral angles in m by flipping edges.
463        Arguments:
464        max_iter is the maximum number of iterations for simulated annealing.
465        anneal tells us the code whether to apply simulated annealing
466        alpha=False means that we use the cosine of angles rather than true angles (faster)
467        gamma is the power to which the angles are raised."""
468    lib_py_gel.minimize_dihedral_angle(m.obj, max_iter, anneal,alpha,ct.c_double(gamma))

Minimizes dihedral angles in m by flipping edges. Arguments: max_iter is the maximum number of iterations for simulated annealing. anneal tells us the code whether to apply simulated annealing alpha=False means that we use the cosine of angles rather than true angles (faster) gamma is the power to which the angles are raised.

def maximize_min_angle(m, dihedral_thresh=0.95, anneal=False):
471def maximize_min_angle(m,dihedral_thresh=0.95,anneal=False):
472    """ Maximizes the minimum angle of triangles by flipping edges of m. Makes the mesh more Delaunay."""
473    lib_py_gel.maximize_min_angle(m.obj,dihedral_thresh,anneal)

Maximizes the minimum angle of triangles by flipping edges of m. Makes the mesh more Delaunay.

def optimize_valency(m, anneal=False):
475def optimize_valency(m,anneal=False):
476    """ Tries to achieve valence 6 internally and 4 along edges by flipping edges of m. """
477    lib_py_gel.optimize_valency(m.obj, anneal)

Tries to achieve valence 6 internally and 4 along edges by flipping edges of m.

def randomize_mesh(m, max_iter=1):
479def randomize_mesh(m,max_iter=1):
480    """  Make random flips in m. Useful for generating synthetic test cases. """
481    lib_py_gel.randomize_mesh(m.obj, max_iter)

Make random flips in m. Useful for generating synthetic test cases.

def quadric_simplify(m, keep_fraction, singular_thresh=0.0001, error_thresh=1):
483def quadric_simplify(m,keep_fraction,singular_thresh=1e-4,error_thresh=1):
484    """ Garland Heckbert simplification of mesh m. keep_fraction is the fraction of vertices
485    to retain. The singular_thresh determines how subtle features are preserved. For values
486    close to 1 the surface is treated as smooth even in the presence of sharp edges of low
487    dihedral angle (angle between normals). Close to zero, the method preserves even subtle
488    sharp features better. The error_thresh is the value of the QEM error at which
489    simplification stops. It is relative to the bounding box size. The default value is 1
490    meaning that simplification continues until the model has been simplified to a number of
491    vertices approximately equal to keep_fraction times the original number of vertices."""
492    lib_py_gel.quadric_simplify(m.obj, keep_fraction, singular_thresh,error_thresh)

Garland Heckbert simplification of mesh m. keep_fraction is the fraction of vertices to retain. The singular_thresh determines how subtle features are preserved. For values close to 1 the surface is treated as smooth even in the presence of sharp edges of low dihedral angle (angle between normals). Close to zero, the method preserves even subtle sharp features better. The error_thresh is the value of the QEM error at which simplification stops. It is relative to the bounding box size. The default value is 1 meaning that simplification continues until the model has been simplified to a number of vertices approximately equal to keep_fraction times the original number of vertices.

def average_edge_length(m):
494def average_edge_length(m):
495    """ Returns the average edge length of mesh m. """
496    return lib_py_gel.average_edge_length(m.obj)

Returns the average edge length of mesh m.

def median_edge_length(m):
498def median_edge_length(m):
499    """ Returns the median edge length of m"""
500    return lib_py_gel.median_edge_length(m.obj)

Returns the median edge length of m

def refine_edges(m, threshold):
502def refine_edges(m,threshold):
503    """ Split all edges in m which are longer
504    than the threshold (second arg) length. A split edge
505    results in a new vertex of valence two."""
506    return lib_py_gel.refine_edges(m.obj, threshold)

Split all edges in m which are longer than the threshold (second arg) length. A split edge results in a new vertex of valence two.

def cc_split(m):
508def cc_split(m):
509    """ Perform a Catmull-Clark split on m, i.e. a split where each face is divided
510    into new quadrilateral faces formed by connecting a corner with a point on
511    each incident edge and a point at the centre of the face."""
512    lib_py_gel.cc_split(m.obj)

Perform a Catmull-Clark split on m, i.e. a split where each face is divided into new quadrilateral faces formed by connecting a corner with a point on each incident edge and a point at the centre of the face.

def loop_split(m):
514def loop_split(m):
515    """ Perform a loop split on m where each edge is divided into two segments, and
516    four new triangles are created for each original triangle. """
517    lib_py_gel.loop_split(m.obj)

Perform a loop split on m where each edge is divided into two segments, and four new triangles are created for each original triangle.

def root3_subdivide(m):
519def root3_subdivide(m):
520    """ Leif Kobbelt's subdivision scheme applied to m. A vertex is placed in the
521    center of each face and all old edges are flipped. """
522    lib_py_gel.root3_subdivide(m.obj)

Leif Kobbelt's subdivision scheme applied to m. A vertex is placed in the center of each face and all old edges are flipped.

def rootCC_subdivide(m):
524def rootCC_subdivide(m):
525    """ This subdivision scheme creates a vertex inside each original (quad) face of m,
526    producing four triangles. Triangles sharing an old edge are then merged.
527    Two steps produce something similar to Catmull-Clark. """
528    lib_py_gel.rootCC_subdivide(m.obj)

This subdivision scheme creates a vertex inside each original (quad) face of m, producing four triangles. Triangles sharing an old edge are then merged. Two steps produce something similar to Catmull-Clark.

def butterfly_subdivide(m):
530def butterfly_subdivide(m):
531    """ Butterfly subidiviosn on m. An interpolatory scheme. Creates the same connectivity as Loop. """
532    lib_py_gel.butterfly_subdivide(m.obj)

Butterfly subidiviosn on m. An interpolatory scheme. Creates the same connectivity as Loop.

def cc_smooth(m):
534def cc_smooth(m):
535    """ If called after cc_split, this function completes a step of Catmull-Clark
536    subdivision of m."""
537    lib_py_gel.cc_smooth(m.obj)

If called after cc_split, this function completes a step of Catmull-Clark subdivision of m.

def cc_subdivide(m):
539def cc_subdivide(m):
540    """ Perform a full Catmull-Clark subdivision step on mesh m. """
541    lib_py_gel.cc_split(m.obj)
542    lib_py_gel.cc_smooth(m.obj)

Perform a full Catmull-Clark subdivision step on mesh m.

def loop_subdivide(m):
544def loop_subdivide(m):
545    """ Perform a full Loop subdivision step on mesh m. """
546    lib_py_gel.loop_split(m.obj)
547    lib_py_gel.loop_smooth(m.obj)   

Perform a full Loop subdivision step on mesh m.

def volume_preserving_cc_smooth(m, iter):
549def volume_preserving_cc_smooth(m, iter):
550    """ This function does the same type of smoothing as in Catmull-Clark
551    subdivision, but to preserve volume it actually performs two steps, and the
552    second step is negative as in Taubin smoothing."""
553    lib_py_gel.volume_preserving_cc_smooth(m.obj, iter)

This function does the same type of smoothing as in Catmull-Clark subdivision, but to preserve volume it actually performs two steps, and the second step is negative as in Taubin smoothing.

def regularize_quads(m, w=0.5, shrink=0.0, iter=1):
555def regularize_quads(m, w=0.5, shrink=0.0, iter=1):
556    """ This function smooths a quad mesh by regularizing quads. Essentially,
557    regularization just makes them more rectangular. """
558    lib_py_gel.regularize_quads(m.obj, w, shrink, iter)

This function smooths a quad mesh by regularizing quads. Essentially, regularization just makes them more rectangular.

def loop_smooth(m):
561def loop_smooth(m):
562    """ If called after Loop split, this function completes a step of Loop
563    subdivision of m. """
564    lib_py_gel.loop_smooth(m.obj)

If called after Loop split, this function completes a step of Loop subdivision of m.

def taubin_smooth(m, iter=1):
566def taubin_smooth(m, iter=1):
567    """ This function performs Taubin smoothing on the mesh m for iter number
568    of iterations. """
569    lib_py_gel.taubin_smooth(m.obj, iter)

This function performs Taubin smoothing on the mesh m for iter number of iterations.

def laplacian_smooth(m, w=0.5, iter=1):
571def laplacian_smooth(m, w=0.5, iter=1):
572    """ This function performs Laplacian smoothing on the mesh m for iter number
573    of iterations. w is the weight applied. """
574    lib_py_gel.laplacian_smooth(m.obj, w, iter)

This function performs Laplacian smoothing on the mesh m for iter number of iterations. w is the weight applied.

def anisotropic_smooth(m, sharpness=0.5, iter=1):
576def anisotropic_smooth(m, sharpness=0.5, iter=1):
577    """ This function performs anisotropic smoothing on the mesh m for iter number
578    of iterations. A bilateral filtering controlled by sharpness is performed on 
579    the face normals followed by a rotation of the faces to match the new normals.
580    The updated vertex positions are the average positions of the corners of the 
581    rotated faces. For sharpness==0 the new normal is simply the area weighted 
582    average of the normals of incident faces. For sharpness>0 the weight of the
583    neighbouring face normals is a Gaussian function of the angle between the
584    face normals. The greater the sharpness, the more the smoothing is anisotropic."""
585    lib_py_gel.anisotropic_smooth(m.obj, sharpness, iter)

This function performs anisotropic smoothing on the mesh m for iter number of iterations. A bilateral filtering controlled by sharpness is performed on the face normals followed by a rotation of the faces to match the new normals. The updated vertex positions are the average positions of the corners of the rotated faces. For sharpness==0 the new normal is simply the area weighted average of the normals of incident faces. For sharpness>0 the weight of the neighbouring face normals is a Gaussian function of the angle between the face normals. The greater the sharpness, the more the smoothing is anisotropic.

def volumetric_isocontour( data, bbox_min=None, bbox_max=None, tau=0.0, make_triangles=True, high_is_inside=True, dual_connectivity=False):
587def volumetric_isocontour(data, bbox_min = None, bbox_max = None,
588                          tau=0.0,
589                          make_triangles=True,
590                          high_is_inside=True,
591                          dual_connectivity=False):
592    """ Creates a polygonal mesh from volumetric data by isocontouring. The dimensions
593    are given by dims, bbox_min (defaults to [0,0,0] ) and bbox_max (defaults to dims) are
594    the corners of the bounding box in R^3 that corresponds to the volumetric grid, tau is
595    the iso value (defaults to 0). If make_triangles is True (default), we turn the quads
596    into triangles. Finally, high_is_inside=True (default) means that values greater than
597    tau are interior and smaller values are exterior. If dual_connectivity is False (default)
598    the function produces marching cubes connectivity and if True it produces dual contouring
599    connectivity. MC connectivity tends to produce less nice triangle shapes but since the
600    vertices always lie on edges, the geometry is arguably better defined for MC. """
601    m = Manifold()
602    dims = data.shape
603    if bbox_min is None:
604        bbox_min = (0,0,0)
605    if bbox_max is None:
606        bbox_max = dims
607    data_float = np.asarray(data, dtype=ct.c_float, order='F')
608    bbox_min_d = np.asarray(bbox_min, dtype=np.float64, order='C')
609    bbox_max_d = np.asarray(bbox_max, dtype=np.float64, order='C')
610    lib_py_gel.volumetric_isocontour(m.obj, dims[0], dims[1], dims[2],
611                                     data_float, bbox_min_d, bbox_max_d, tau,
612                                     make_triangles, high_is_inside, dual_connectivity)
613    return m

Creates a polygonal mesh from volumetric data by isocontouring. The dimensions are given by dims, bbox_min (defaults to [0,0,0] ) and bbox_max (defaults to dims) are the corners of the bounding box in R^3 that corresponds to the volumetric grid, tau is the iso value (defaults to 0). If make_triangles is True (default), we turn the quads into triangles. Finally, high_is_inside=True (default) means that values greater than tau are interior and smaller values are exterior. If dual_connectivity is False (default) the function produces marching cubes connectivity and if True it produces dual contouring connectivity. MC connectivity tends to produce less nice triangle shapes but since the vertices always lie on edges, the geometry is arguably better defined for MC.

def triangulate(m, clip_ear=True):
615def triangulate(m, clip_ear=True):
616    """ Turn a general polygonal mesh, m, into a triangle mesh by repeatedly
617        splitting a polygon into smaller polygons. """
618    if clip_ear:
619        lib_py_gel.ear_clip_triangulate(m.obj)
620    else:
621        lib_py_gel.shortest_edge_triangulate(m.obj)

Turn a general polygonal mesh, m, into a triangle mesh by repeatedly splitting a polygon into smaller polygons.

def skeleton_to_feq(g, node_radii=None, symmetrize=True):
623def skeleton_to_feq(g, node_radii = None, symmetrize=True):
624    """ Turn a skeleton graph g into a Face Extrusion Quad Mesh m with given node_radii for each graph node.
625    If symmetrize is True (default) the graph is made symmetrical. If node_radii are supplied then they
626    are used in the reconstruction. Otherwise, the radii are obtained from the skeleton. They are stored in 
627    the green channel of the vertex color during skeletonization, so for a skeletonized shape that is how the
628    radius of each node is obtained. This is a questionable design decision and will probably change 
629    in the future. """
630    m = Manifold()
631    if node_radii is None:
632        node_radii = [0.0] * len(g.nodes())
633        use_graph_radii = True
634    else:
635        use_graph_radii = False
636        if isinstance(node_radii, (int, float)):
637            if node_radii <= 0.0:
638                node_radii = 0.25 * g.average_edge_length()
639            node_radii = [node_radii] * len(g.nodes())
640
641    node_rs_flat = np.asarray(node_radii, dtype=np.float64)
642    lib_py_gel.graph_to_feq(g.obj , m.obj, node_rs_flat, symmetrize, use_graph_radii)
643    return m

Turn a skeleton graph g into a Face Extrusion Quad Mesh m with given node_radii for each graph node. If symmetrize is True (default) the graph is made symmetrical. If node_radii are supplied then they are used in the reconstruction. Otherwise, the radii are obtained from the skeleton. They are stored in the green channel of the vertex color during skeletonization, so for a skeletonized shape that is how the radius of each node is obtained. This is a questionable design decision and will probably change in the future.

def fit_mesh_to_ref(m, ref_mesh, iter=10, dist_wt=1.0, lap_wt=0.3):
697def fit_mesh_to_ref(m, ref_mesh, iter = 10, dist_wt = 1.0, lap_wt = 0.3):
698    """ Fits a skeletal mesh m to a reference mesh ref_mesh. """
699
700    v_pos = m.positions()
701    for i in range(iter):
702        Ai, bi = inv_correspondence_leqs(m, ref_mesh)
703        lap_matrix = laplacian_matrix(m)
704        lap_b = lap_matrix @ v_pos
705        final_A = vstack([lap_wt*lap_matrix, dist_wt*Ai])
706        final_b = np.vstack([lap_wt*lap_b, dist_wt*bi])
707        opt_x, _, _, _ = lsqr(final_A, final_b[:,0])[:4]
708        opt_y, _, _, _ = lsqr(final_A, final_b[:,1])[:4]
709        opt_z, _, _, _ = lsqr(final_A, final_b[:,2])[:4]
710        v_pos[:,:] = np.stack([opt_x, opt_y, opt_z], axis=1)
711        regularize_quads(m, w=0.5, shrink=0.8, iter=3)
712    return m

Fits a skeletal mesh m to a reference mesh ref_mesh.

class MeshDistance:
714class MeshDistance:
715    """ This class allows you to compute the distance from any point in space to
716    a Manifold (which must be triangulated). The constructor creates an instance
717    based on a specific mesh, and the signed_distance function computes the actual distance. """
718    def __init__(self,m):
719        self.obj = lib_py_gel.MeshDistance_new(m.obj)
720    def __del__(self):
721        lib_py_gel.MeshDistance_delete(self.obj)
722    def signed_distance(self,pts,upper=1e30):
723        """ Compute the signed distance from each point in pts to the mesh stored in
724        this class instance. pts should be convertible to a length N>=1 array of 3D
725        points. The function returns an array of N distance values with a single distance
726        for each point. The distance corresponding to a point is positive if the point
727        is outside and negative if inside. The upper parameter can be used to threshold
728        how far away the distance is of interest. """
729        p = np.asarray(pts, dtype=ct.c_float, order='C')
730        ndim = len(p.shape)
731        if ndim==1:
732            n = p.shape[0]//3
733        elif ndim==2:
734            n = p.shape[0]
735        else:
736            raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
737        
738        d = np.ndarray(n, dtype=ct.c_float)
739        lib_py_gel.MeshDistance_signed_distance(self.obj, n, p, d, upper)
740        return d
741    def ray_inside_test(self,pts,no_rays=3):
742        """Check whether each point in pts is inside or outside the stored mesh by
743        casting rays. pts should be convertible to a length N>=1 array of 3D points.
744        Effectively, this is the sign of the distance. In some cases casting (multiple)
745        ray is more robust than using the sign computed locally. Returns an array of
746        N integers which are either 1 or 0 depending on whether the corresponding point
747        is inside (1) or outside (0). """
748        p = np.asarray(pts, dtype=ct.c_float, order='C')
749        ndim = len(p.shape)
750        if ndim==1:
751            n = p.shape[0]//3
752        elif ndim==2:
753            n = p.shape[0]
754        else:
755            raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
756        s = np.ndarray(n, dtype=ct.c_int)
757        lib_py_gel.MeshDistance_ray_inside_test(self.obj,n,p,s,no_rays)
758        return s
759    def intersect(self, p0, dir, _t=0):
760        """ Intersect the ray starting in p0 with direction, dir, with the stored mesh. Returns
761        the point of intersection if there is one, otherwise None. """
762        p0 = np.asarray(p0,dtype=ct.c_float)
763        dir = np.asarray(dir,dtype=ct.c_float)
764        t = ct.c_float(_t)
765        r = lib_py_gel.MeshDistance_ray_intersect(self.obj, p0, dir, ct.byref(t))
766        if r:
767            return t.value, p0, dir
768        return None

This class allows you to compute the distance from any point in space to a Manifold (which must be triangulated). The constructor creates an instance based on a specific mesh, and the signed_distance function computes the actual distance.

def signed_distance(self, pts, upper=1e+30):
722    def signed_distance(self,pts,upper=1e30):
723        """ Compute the signed distance from each point in pts to the mesh stored in
724        this class instance. pts should be convertible to a length N>=1 array of 3D
725        points. The function returns an array of N distance values with a single distance
726        for each point. The distance corresponding to a point is positive if the point
727        is outside and negative if inside. The upper parameter can be used to threshold
728        how far away the distance is of interest. """
729        p = np.asarray(pts, dtype=ct.c_float, order='C')
730        ndim = len(p.shape)
731        if ndim==1:
732            n = p.shape[0]//3
733        elif ndim==2:
734            n = p.shape[0]
735        else:
736            raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
737        
738        d = np.ndarray(n, dtype=ct.c_float)
739        lib_py_gel.MeshDistance_signed_distance(self.obj, n, p, d, upper)
740        return d

Compute the signed distance from each point in pts to the mesh stored in this class instance. pts should be convertible to a length N>=1 array of 3D points. The function returns an array of N distance values with a single distance for each point. The distance corresponding to a point is positive if the point is outside and negative if inside. The upper parameter can be used to threshold how far away the distance is of interest.

def ray_inside_test(self, pts, no_rays=3):
741    def ray_inside_test(self,pts,no_rays=3):
742        """Check whether each point in pts is inside or outside the stored mesh by
743        casting rays. pts should be convertible to a length N>=1 array of 3D points.
744        Effectively, this is the sign of the distance. In some cases casting (multiple)
745        ray is more robust than using the sign computed locally. Returns an array of
746        N integers which are either 1 or 0 depending on whether the corresponding point
747        is inside (1) or outside (0). """
748        p = np.asarray(pts, dtype=ct.c_float, order='C')
749        ndim = len(p.shape)
750        if ndim==1:
751            n = p.shape[0]//3
752        elif ndim==2:
753            n = p.shape[0]
754        else:
755            raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
756        s = np.ndarray(n, dtype=ct.c_int)
757        lib_py_gel.MeshDistance_ray_inside_test(self.obj,n,p,s,no_rays)
758        return s

Check whether each point in pts is inside or outside the stored mesh by casting rays. pts should be convertible to a length N>=1 array of 3D points. Effectively, this is the sign of the distance. In some cases casting (multiple) ray is more robust than using the sign computed locally. Returns an array of N integers which are either 1 or 0 depending on whether the corresponding point is inside (1) or outside (0).

def intersect(self, p0, dir, _t=0):
759    def intersect(self, p0, dir, _t=0):
760        """ Intersect the ray starting in p0 with direction, dir, with the stored mesh. Returns
761        the point of intersection if there is one, otherwise None. """
762        p0 = np.asarray(p0,dtype=ct.c_float)
763        dir = np.asarray(dir,dtype=ct.c_float)
764        t = ct.c_float(_t)
765        r = lib_py_gel.MeshDistance_ray_intersect(self.obj, p0, dir, ct.byref(t))
766        if r:
767            return t.value, p0, dir
768        return None

Intersect the ray starting in p0 with direction, dir, with the stored mesh. Returns the point of intersection if there is one, otherwise None.